new feature: JEV Integration. Now there are a current (native) LLM decision model and a JEV decision model
This commit is contained in:
133
tests/test_jev_decisions.py
Normal file
133
tests/test_jev_decisions.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""Offline JEV transport/contract tests; no live API key or network required."""
|
||||
import json
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
|
||||
from agent_framework.decisions import client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def clean_environment(monkeypatch):
|
||||
import os
|
||||
for key in list(os.environ):
|
||||
if key.startswith(('DECISION_', 'JEV_')) or key == 'AI_GATEWAY_API_KEY':
|
||||
monkeypatch.delenv(key)
|
||||
|
||||
|
||||
def answer(questions, *, confidence=.95):
|
||||
return {'model': 'typesafe-ai/jev', 'answers': [
|
||||
{'name': q['name'], 'type': q['type'], **(
|
||||
{'choice': q['choices'][0]['value'], 'confidence': confidence} if q['type'] == 'choice' else
|
||||
{'probability': confidence} if q['type'] == 'predicate' else
|
||||
{'score': 8.0, 'confidence': confidence})} for q in questions]}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_preserves_current_call_and_kwargs(monkeypatch):
|
||||
current = AsyncMock(return_value='legacy response')
|
||||
monkeypatch.setattr(client, '_post', lambda body: pytest.fail('JEV must not run'))
|
||||
result = await client.invoke(type('LLM', (), {'ainvoke': current})(), [{'content': 'x'}],
|
||||
decision_point='router', decision_questions=[], profile_name='router', temperature=0)
|
||||
assert result == 'legacy response'
|
||||
current.assert_awaited_once_with([{'content': 'x'}], profile_name='router', temperature=0)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_individual_selection_and_override(monkeypatch):
|
||||
monkeypatch.setenv('DECISION_PROVIDER', 'jev')
|
||||
monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'current')
|
||||
assert not client.selected('router')
|
||||
assert client.selected('route_continuity')
|
||||
monkeypatch.setenv('DECISION_PROVIDER', 'current')
|
||||
monkeypatch.setenv('DECISION_ROUTE_CONTINUITY_PROVIDER', 'jev')
|
||||
assert client.selected('route_continuity')
|
||||
assert not client.selected('router')
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_typed_contract_and_shared_request(monkeypatch):
|
||||
monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev')
|
||||
requests = []
|
||||
def transport(body):
|
||||
requests.append(body)
|
||||
return answer(body['questions'])
|
||||
monkeypatch.setattr(client, '_post', transport)
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock()})()
|
||||
result = json.loads(await client.invoke(llm, [{'content': 'context'}], decision_point='router',
|
||||
decision_questions=[client.choice('intent', ['billing', 'orders']), client.predicate('allowed'), client.score('score')]))
|
||||
assert result['intent'] == 'billing' and result['allowed'] is True and result['score'] == 8
|
||||
assert len(requests) == 1 and requests[0]['model'] == 'typesafe-ai/jev'
|
||||
assert 'context' in requests[0]['input']
|
||||
llm.ainvoke.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize('failure', ['network', 'unknown_choice', 'missing', 'duplicate', 'nan', 'low', 'no_confidence', 'wrong_type'])
|
||||
async def test_invalid_or_uncertain_jev_falls_back_once(monkeypatch, failure):
|
||||
monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev')
|
||||
def transport(body):
|
||||
if failure == 'network': raise TimeoutError()
|
||||
response = answer(body['questions'])
|
||||
a = response['answers'][0]
|
||||
if failure == 'unknown_choice': a['choice'] = 'undeclared'
|
||||
if failure == 'missing': response['answers'] = []
|
||||
if failure == 'duplicate': response['answers'] *= 2
|
||||
if failure == 'nan': a['confidence'] = float('nan')
|
||||
if failure == 'low': a['confidence'] = .4
|
||||
if failure == 'no_confidence': a.pop('confidence')
|
||||
if failure == 'wrong_type': a['type'] = 'predicate'
|
||||
return response
|
||||
monkeypatch.setattr(client, '_post', transport)
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='legacy')})()
|
||||
assert await client.invoke(llm, [], decision_point='router', decision_questions=[client.choice('intent', ['a'])]) == 'legacy'
|
||||
llm.ainvoke.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_fallback_surfaces_failure(monkeypatch):
|
||||
monkeypatch.setenv('DECISION_ROUTER_PROVIDER', 'jev')
|
||||
monkeypatch.setenv('DECISION_FALLBACK_TO_CURRENT', 'false')
|
||||
monkeypatch.setattr(client, '_post', lambda body: {'answers': []})
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock()})()
|
||||
with pytest.raises(ValueError):
|
||||
await client.invoke(llm, [], decision_point='router', decision_questions=[client.predicate('allowed')])
|
||||
llm.ainvoke.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_generative_bypass_even_with_global_jev(monkeypatch):
|
||||
monkeypatch.setenv('DECISION_PROVIDER', 'jev')
|
||||
monkeypatch.setattr(client, '_post', lambda body: pytest.fail('rewrite must use current'))
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='rewritten')})()
|
||||
assert await client.invoke(llm, [], decision_point='rewrite', decision_questions=[], decision_generative=True) == 'rewritten'
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_multi_intent_preserves_order_literal_fragments(monkeypatch):
|
||||
from types import SimpleNamespace
|
||||
monkeypatch.setenv('DECISION_MULTI_INTENT_PROVIDER', 'jev')
|
||||
candidates = {'operations': [{'source_text': 'pedido', 'intent': 'x'}, {'source_text': 'fatura', 'intent': 'y'}]}
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock(return_value=json.dumps(candidates))})()
|
||||
monkeypatch.setattr(client, '_post', lambda body: answer(body['questions']))
|
||||
result = json.loads(await client.multi_intent(llm, [], intents=[SimpleNamespace(name='billing')], text='pedido e fatura'))
|
||||
assert [o['source_text'] for o in result['operations']] == ['pedido', 'fatura']
|
||||
llm.ainvoke.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_legacy_semantic_classifier_and_jev_are_independent(monkeypatch, tmp_path):
|
||||
from types import SimpleNamespace
|
||||
from agent_framework.routing.enterprise_router import EnterpriseRouter
|
||||
config = tmp_path / 'routing.yaml'
|
||||
config.write_text('router: {fallback_agent: fallback}\nintents: []\n')
|
||||
llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='NO')})()
|
||||
router = EnterpriseRouter(SimpleNamespace(ROUTING_CONFIG_PATH=str(config), ENABLE_LLM_ROUTER=False, ENABLE_ROUTE_STICKINESS=False), llm=llm)
|
||||
expected = {'allowed_values': ['YES', 'NO'], 'semantic_classifier': {'enabled': True, 'prompt': 'Use meaning: {{ user_input }}'}}
|
||||
monkeypatch.setenv('DECISION_WORKFLOW_EXPECTED_INPUT_PROVIDER', 'jev')
|
||||
monkeypatch.setattr(client, '_post', lambda body: answer(body['questions']))
|
||||
value, _ = await router._classify_expected_input_semantically(text='yes', expected_input=expected, pause_prompt='Confirm?')
|
||||
assert value == 'YES'
|
||||
value, _ = await router._classify_expected_input_semantically(text='no', expected_input=expected, pause_prompt='Confirm?', component_name='transaction.confirmation')
|
||||
assert value == 'NO'
|
||||
llm.ainvoke.assert_awaited_once()
|
||||
Reference in New Issue
Block a user