new feature: JEV Integration. Now there are a current (native) LLM decision model and a JEV decision model

This commit is contained in:
2026-11-05 07:58:58 -03:00
parent 645ecdfb07
commit de0eb5e28a
210 changed files with 1275 additions and 25 deletions

Binary file not shown.

View File

@@ -0,0 +1,28 @@
import pytest
from agent_framework.decisions.adapters.typesafe import build_request, normalize_answers, TypeSafeContractError
from agent_framework.decisions import client
def test_choice_wire_uses_verified_noul():
questions = [client.choice('decision', ['CONTINUE','ROUTE','END_SESSION'])]
body = build_request('state', questions)
assert len(body['questions']) == 3
assert all(x['type'] == 'noul' for x in body['questions'].values())
answer = {'answers': {'__jev_q0_o0': {'noul': .12}, '__jev_q0_o1': {'noul': .94}, '__jev_q0_o2': {'noul': .07}}}
assert normalize_answers(answer, questions) == [{'name': 'decision', 'type': 'choice', 'choice': 'ROUTE', 'confidence': .94}]
def test_ambiguous_choice_fails_closed():
questions = [client.choice('decision', ['CONTINUE','ROUTE'])]
with pytest.raises(TypeSafeContractError, match='AMBIGUOUS_DIRECT_CHOICE'):
normalize_answers({'answers': {'__jev_q0_o0': {'noul': .81}, '__jev_q0_o1': {'noul': .78}}}, questions)
def test_predicate_wire():
questions = [client.predicate('allowed')]
assert normalize_answers({'answers': {'__jev_q0': {'noul': .9}}}, questions)[0]['probability'] == .9
def test_score_fails_closed():
with pytest.raises(TypeSafeContractError, match='UNSUPPORTED_DIRECT_SCORE'):
build_request('state', [client.score('score')])

133
tests/test_jev_decisions.py Normal file
View 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()

View File

@@ -0,0 +1,33 @@
"""Offline regression tests: python -m unittest discover -s tests -p test_jev_endpoint.py"""
import importlib.util
import os
from pathlib import Path
import unittest
from unittest.mock import patch
CLIENT = Path(__file__).resolve().parents[1] / 'libs/agent_framework/src/agent_framework/decisions/client.py'
spec = importlib.util.spec_from_file_location('jev_client_patch', CLIENT)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
class EndpointTests(unittest.TestCase):
def test_default(self):
with patch.dict(os.environ, {}, clear=True):
self.assertEqual(module._jev_endpoint(), 'https://ai-gateway.vercel.sh/v1/decisions')
def test_typesafe_endpoint(self):
with patch.dict(os.environ, {'JEV_ENDPOINT': 'https://api.typesafe.ai/v1/systemone'}, clear=True):
self.assertEqual(module._jev_endpoint(), 'https://api.typesafe.ai/v1/systemone')
def test_reject_untrusted_host(self):
with patch.dict(os.environ, {'JEV_ENDPOINT': 'https://evil.example/v1/decisions'}, clear=True):
with self.assertRaises(ValueError):
module._jev_endpoint()
def test_reject_http(self):
with patch.dict(os.environ, {'JEV_ENDPOINT': 'http://api.typesafe.ai/v1/systemone'}, clear=True):
with self.assertRaises(ValueError):
module._jev_endpoint()
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,210 @@
"""Run without project dependencies: python3 tests/test_jev_transport_standalone.py."""
import ast
import asyncio
import importlib.util
import json
import os
import logging
from types import SimpleNamespace
from pathlib import Path
import unittest
from unittest.mock import AsyncMock, patch
ROOT = Path(__file__).resolve().parents[1]
MODULE = ROOT / 'libs/agent_framework/src/agent_framework/decisions/client.py'
spec = importlib.util.spec_from_file_location('jev_transport', MODULE)
client = importlib.util.module_from_spec(spec)
spec.loader.exec_module(client)
def response(questions, confidence=.95):
return {'model': 'typesafe-ai/jev', 'answers': [
{'type': q['type'], 'name': q['name'], **(
{'choice': q['choices'][0]['value'], 'confidence': confidence} if q['type'] == 'choice' else
{'probability': confidence} if q['type'] == 'predicate' else
{'score': 8, 'confidence': confidence})} for q in questions]}
class DecisionTransportTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.env = patch.dict(os.environ, {k: v for k, v in os.environ.items() if not k.startswith(('DECISION_', 'JEV_'))}, clear=True)
self.env.start()
self.addCleanup(self.env.stop)
self.llm = type('LLM', (), {'ainvoke': AsyncMock(return_value='legacy')})()
async def call(self, **kwargs):
return await client.invoke(self.llm, [{'content': 'test context'}], decision_point='router',
decision_questions=[client.choice('intent', ['billing', 'orders'])], **kwargs)
async def test_default_passes_original_kwargs_without_network(self):
with patch.object(client, '_post', side_effect=AssertionError('network')):
self.assertEqual(await self.call(profile_name='router'), 'legacy')
self.llm.ainvoke.assert_awaited_once_with([{'content': 'test context'}], profile_name='router')
async def test_individual_override(self):
os.environ.update(DECISION_PROVIDER='jev', DECISION_ROUTER_PROVIDER='current')
self.assertFalse(client.selected('router'))
self.assertTrue(client.selected('route_continuity'))
os.environ.update(DECISION_PROVIDER='current', DECISION_ROUTER_PROVIDER='jev')
self.assertTrue(client.selected('router'))
self.assertFalse(client.selected('route_continuity'))
os.environ['DECISION_JUDGE_ALUC_PROVIDER'] = 'jev'
self.assertTrue(client.selected('judge.groundedness', 'judge_aluc'))
os.environ['DECISION_JUDGE_GROUNDEDNESS_PROVIDER'] = 'current'
self.assertFalse(client.selected('judge.groundedness', 'judge_aluc'))
async def test_typed_choices_predicates_scores_shared_request(self):
os.environ['DECISION_ROUTER_PROVIDER'] = 'jev'
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])) as post:
result = json.loads(await client.invoke(self.llm, [], decision_point='router',
decision_questions=[client.choice('intent', ['billing']), client.predicate('allowed'), client.score('score')]))
self.assertEqual((result['intent'], result['allowed'], result['score']), ('billing', True, 8))
self.assertEqual(post.call_count, 1)
self.llm.ainvoke.assert_not_awaited()
async def test_invalid_response_falls_back_once(self):
os.environ['DECISION_ROUTER_PROVIDER'] = 'jev'
for failure in ['missing', 'duplicate', 'choice', 'nan', 'missing_confidence', 'low_confidence', 'type', 'network']:
with self.subTest(failure=failure):
self.llm.ainvoke.reset_mock()
def transport(body):
if failure == 'network': raise TimeoutError()
r = response(body['questions']); a = r['answers'][0]
if failure == 'missing': r['answers'] = []
if failure == 'duplicate': r['answers'] *= 2
if failure == 'choice': a['choice'] = 'unknown'
if failure == 'nan': a['confidence'] = float('nan')
if failure == 'missing_confidence': a.pop('confidence')
if failure == 'low_confidence': a['confidence'] = .1
if failure == 'type': a['type'] = 'predicate'
return r
with patch.object(client, '_post', side_effect=transport):
self.assertEqual(await self.call(), 'legacy')
self.llm.ainvoke.assert_awaited_once()
async def test_no_fallback_surfaces_error(self):
os.environ.update(DECISION_ROUTER_PROVIDER='jev', DECISION_FALLBACK_TO_CURRENT='false')
with patch.object(client, '_post', return_value={'answers': []}):
with self.assertRaises(ValueError): await self.call()
self.llm.ainvoke.assert_not_awaited()
async def test_generative_bypass(self):
os.environ['DECISION_PROVIDER'] = 'jev'
with patch.object(client, '_post', side_effect=AssertionError('network')):
self.assertEqual(await self.call(decision_generative=True), 'legacy')
async def test_invalid_provider_rejected(self):
os.environ['DECISION_ROUTER_PROVIDER'] = 'typo'
with self.assertRaises(ValueError): await self.call()
async def test_custom_renderer_failure_falls_back(self):
os.environ['DECISION_ROUTER_PROVIDER'] = 'jev'
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
self.assertEqual(await self.call(decision_render=lambda d: d['missing']), 'legacy')
async def test_threshold_override(self):
os.environ.update(DECISION_ROUTER_PROVIDER='jev', DECISION_ROUTER_MIN_CONFIDENCE='.99')
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
self.assertEqual(await self.call(), 'legacy')
async def test_predicate_rejects_bool_and_out_of_range(self):
for probability in [True, -1, 2, float('inf'), None]:
with self.subTest(probability=probability):
with patch.object(client, '_post', return_value={'answers': [{'type': 'predicate', 'name': 'allowed', 'probability': probability}]}):
with self.assertRaises(ValueError):
await client.evaluate('guardrail', [], [client.predicate('allowed')])
async def test_transport_requires_key_before_request(self):
os.environ.pop('AI_GATEWAY_API_KEY', None)
with patch.object(client.urllib.request, 'urlopen') as request:
with self.assertRaises(ValueError): client._post({})
request.assert_not_called()
async def test_transport_endpoint_auth_and_timeout(self):
os.environ.update(AI_GATEWAY_API_KEY='test-key', JEV_TIMEOUT_SECONDS='3')
import io
with patch.object(client.urllib.request, 'urlopen', return_value=io.StringIO('{"answers": []}')) as request:
self.assertEqual(client._post({'model': 'typesafe-ai/jev'}), {'answers': []})
req = request.call_args.args[0]
self.assertEqual(req.full_url, 'https://ai-gateway.vercel.sh/v1/decisions')
self.assertEqual(req.get_header('Authorization'), 'Bearer test-key')
self.assertEqual(request.call_args.kwargs['timeout'], 3)
async def test_integrated_modules_compile(self):
for path in (ROOT / 'libs/agent_framework/src/agent_framework').rglob('*.py'):
with self.subTest(path=path): ast.parse(path.read_text(), filename=str(path))
def load_functions(relative_path, names, namespace):
"""Execute actual function bodies in isolation from unavailable optional dependencies."""
path = ROOT / 'libs/agent_framework/src/agent_framework' / relative_path
tree = ast.parse(path.read_text())
nodes = [node for node in ast.walk(tree) if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names]
module = ast.Module(body=[ast.ImportFrom(module='__future__', names=[ast.alias(name='annotations')], level=0)] + nodes, type_ignores=[])
exec(compile(ast.fix_missing_locations(module), str(path), 'exec'), namespace)
return namespace
class DecisionCallsiteTests(unittest.IsolatedAsyncioTestCase):
setUp = DecisionTransportTests.setUp
async def test_actual_continuity_classifier_jev(self):
os.environ['DECISION_ROUTE_CONTINUITY_PROVIDER'] = 'jev'
ns = load_functions('routing/continuity.py', {'_classify'}, {
'json': json, 'decision_invoke': client.invoke, 'choice': client.choice,
'ContinuityEvaluation': SimpleNamespace})
self_obj = SimpleNamespace(llm=self.llm, profile_name='route_continuity',
_agent_capabilities=lambda intents: {'billing': ['invoices']},
_compact_history=lambda history: history, _parse_json=json.loads)
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
result = await ns['_classify'](self_obj, {}, text='continue', active_agent='billing', intents=[])
self.assertEqual(result.decision, 'CONTINUE')
self.assertGreater(result.confidence, .9)
self.llm.ainvoke.assert_not_awaited()
async def test_actual_router_derives_agent_from_catalog(self):
os.environ['DECISION_ROUTER_PROVIDER'] = 'jev'
ns = load_functions('routing/enterprise_router.py', {'_route_by_llm'}, {
'json': json, 'decision_invoke': client.invoke, 'choice': client.choice, 'RouteDecision': SimpleNamespace})
intent = SimpleNamespace(enabled=True, name='invoice', agent='billing', description='Invoice', examples=[], mcp_tools=[], domain='telecom')
self_obj = SimpleNamespace(llm=self.llm, intents=[intent], fallback_agent='fallback', _parse_json=json.loads,
_agent_for_intent=lambda name: 'billing', _domain_for_intent=lambda name: 'telecom', _tools_for_intent=lambda name: ['read_invoice'])
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
result = await ns['_route_by_llm'](self_obj, 'invoice', {})
self.assertEqual((result.intent, result.agent, result.mcp_tools), ('invoice', 'billing', ['read_invoice']))
async def test_actual_reconciliation_preserves_candidate_values(self):
os.environ['DECISION_TRANSACTION_PARAMETER_RECONCILIATION_PROVIDER'] = 'jev'
ns = load_functions('runtime/transaction_parameters.py', {'reconcile_transaction_parameters', '_response_text', '_coerce'}, {
'json': json, 'logger': logging.getLogger('test'), 'parse_json_object': json.loads,
'decision_invoke': client.invoke, 'selected': client.selected, 'choice': client.choice,
'_EMPTY_VALUES': (None, '', {}, [])})
self.llm.ainvoke.return_value = json.dumps({'fields': {
'id': {'value': '42', 'decision': 'resolved', 'source': 'current'},
'empty': {'value': None, 'decision': 'unresolved', 'source': ''}}})
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
result = await ns['reconcile_transaction_parameters'](self.llm, text='42', tool_name='read', parameter_names=['id', 'empty'])
self.assertEqual(result['values'], {'id': '42'})
self.assertEqual(result['decisions']['empty'], 'unresolved')
self.llm.ainvoke.assert_awaited_once()
async def test_actual_calibrated_judge_and_generative_fallback(self):
os.environ['DECISION_JUDGE_RQLT_PROVIDER'] = 'jev'
ns = load_functions('judges/calibrated/llm_client.py', {'classify'}, {
'decision_invoke': client.invoke, 'predicate': client.predicate, 'score': client.score,
'choice': client.choice, '_parse_json': json.loads})
self_obj = SimpleNamespace(llm=self.llm, default_profile='judge', _build_prompt=lambda task, payload: 'quality rubric')
with patch.object(client, '_post', side_effect=lambda b: response(b['questions'])):
result = await ns['classify'](self_obj, 'RQLT', {})
self.assertTrue(result['allowed'])
self.assertEqual(result['score'], 8)
self.llm.ainvoke.assert_not_awaited()
self.llm.ainvoke.return_value = '{"text":"fallback response"}'
os.environ['DECISION_PROVIDER'] = 'jev'
with patch.object(client, '_post', side_effect=AssertionError('no generative JEV')):
self.assertEqual(await ns['classify'](self_obj, 'FALLBACK', {}), {'text': 'fallback response'})
if __name__ == '__main__':
unittest.main(verbosity=2)

View File

@@ -0,0 +1,176 @@
import json
from unittest.mock import AsyncMock
from urllib.error import HTTPError
import pytest
from agent_framework.decisions import client
from agent_framework.decisions.adapters.typesafe import build_request, normalize_answers, TypeSafeContractError
@pytest.mark.asyncio
async def test_default_provider_unchanged(monkeypatch):
monkeypatch.delenv("DECISION_PROVIDER", raising=False)
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="legacy")})()
monkeypatch.setattr(client, "_post", lambda body: pytest.fail("JEV transport called"))
assert await client.invoke(llm, [{"content": "x"}], decision_point="route_continuity",
decision_questions=[], profile_name="route_continuity") == "legacy"
llm.ainvoke.assert_awaited_once_with([{"content": "x"}], profile_name="route_continuity")
@pytest.mark.asyncio
async def test_typesafe_noul(monkeypatch):
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_MODEL", "jev-latest")
captured = []
def transport(body):
captured.append(body)
return {"model": "jev-1.14.0", "answers": {"__jev_q0": {"type": "noul", "noul": .98}}}
monkeypatch.setattr(client, "_post", transport)
result = await client.evaluate("guardrail.pinj", [], [client.predicate("allowed")])
assert result["allowed"] is True and result["confidence"] == pytest.approx(.98)
assert captured[0]["questions"]["__jev_q0"]["type"] == "noul"
assert "state" in captured[0] and "input" not in captured[0]
@pytest.mark.asyncio
async def test_vercel_contract_not_modified(monkeypatch):
monkeypatch.setenv("JEV_ENDPOINT", "https://ai-gateway.vercel.sh/v1/decisions")
captured = []
def transport(body):
captured.append(body)
return {"answers": [{"name": "allowed", "type": "predicate", "probability": .95}]}
monkeypatch.setattr(client, "_post", transport)
result = await client.evaluate("guardrail.pinj", [], [client.predicate("allowed")])
assert result["allowed"] is True
assert "input" in captured[0] and "state" not in captured[0]
assert isinstance(captured[0]["questions"], list)
@pytest.mark.asyncio
async def test_invalid_direct_response_falls_back_once(monkeypatch):
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {}})
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})()
assert await client.invoke(llm, [], decision_point="guardrail.revprec",
decision_questions=[client.predicate("allowed")]) == "original"
llm.ainvoke.assert_awaited_once()
@pytest.mark.asyncio
async def test_http_error_diagnostic_without_body(monkeypatch, caplog):
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
def reject(body):
raise HTTPError("https://api.typesafe.ai/v1/systemone", 400, "SECRET_BODY", {}, None)
monkeypatch.setattr(client, "_post", reject)
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})()
assert await client.invoke(llm, [], decision_point="route_continuity",
decision_questions=[client.choice("decision", ["CONTINUE", "ROUTE"])]) == "original"
assert "http_status=400" in caplog.text
assert "SECRET_BODY" not in caplog.text
@pytest.mark.asyncio
async def test_fallback_disabled_raises(monkeypatch):
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("DECISION_FALLBACK_TO_CURRENT", "false")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {}})
llm = type("LLM", (), {"ainvoke": AsyncMock()})()
with pytest.raises(TypeSafeContractError):
await client.invoke(llm, [], decision_point="guardrail.pinj", decision_questions=[client.predicate("allowed")])
llm.ainvoke.assert_not_awaited()
@pytest.mark.asyncio
async def test_http_422_schema_diagnostic_redacts_user_data(monkeypatch, caplog):
import io
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
def reject(body):
error = {"detail": [{"type": "missing", "loc": ["body", "questions", "private-user-input", "choices"],
"msg": "SECRET_PASSWORD", "input": "SECRET_PASSWORD"}]}
raise HTTPError("https://api.typesafe.ai/v1/systemone", 422, "invalid", {},
io.BytesIO(json.dumps(error).encode()))
monkeypatch.setattr(client, "_post", reject)
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})()
assert await client.invoke(llm, [], decision_point="route_continuity",
decision_questions=[client.choice("decision", ["CONTINUE", "ROUTE"])]) == "original"
assert "http_status=422" in caplog.text
assert "http_schema=missing:body.questions.choices" in caplog.text
assert "SECRET_PASSWORD" not in caplog.text
assert "private-user-input" not in caplog.text
@pytest.mark.asyncio
async def test_low_confidence_has_specific_error_code(monkeypatch, caplog):
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.9")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": 0.6}}})
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="original")})()
assert await client.invoke(llm, [], decision_point="guardrail.revprec",
decision_questions=[client.predicate("allowed")]) == "original"
assert "error_code=CONFIDENCE_BELOW_THRESHOLD" in caplog.text
@pytest.mark.asyncio
async def test_revprec_noul_probability_not_misread_as_confidence(monkeypatch, caplog):
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.7")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": .61}}})
with pytest.raises(ValueError, match="CONFIDENCE_BELOW_THRESHOLD"):
await client.evaluate("guardrail.revprec", [], [client.predicate("allowed")])
assert "('allowed', 0.61)" in caplog.text
assert "confidence=0.6100" in caplog.text
@pytest.mark.asyncio
async def test_revprec_negative_predicate_confidence(monkeypatch):
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.7")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {"__jev_q0": {"type": "noul", "noul": .12}}})
result = await client.evaluate("guardrail.revprec", [], [client.predicate("allowed")])
assert result["allowed"] is False
assert result["confidence"] == pytest.approx(.88)
@pytest.mark.asyncio
async def test_opt_in_choice_diagnostics_rejected_and_fallback(monkeypatch, caplog):
import logging
monkeypatch.setenv("DECISION_PROVIDER", "jev")
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_DECISION_DIAGNOSTICS", "true")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.60")
monkeypatch.setattr(client, "_post", lambda body: {"model": "jev-1.14.0", "answers": {
"__jev_q0_o0": {"type": "noul", "noul": .56},
"__jev_q0_o1": {"type": "noul", "noul": .35}}})
llm = type("LLM", (), {"ainvoke": AsyncMock(return_value="current-result")})()
with caplog.at_level(logging.INFO, logger="agent_framework.decisions"):
output = await client.invoke(llm, [{"content": "PRIVATE_CUSTOMER_TEXT"}],
decision_point="transaction_intent_shift",
decision_questions=[client.choice("decision", ["CONTINUE", "SHIFT"])])
assert output == "current-result"
assert "jev.choice.diagnostics" in caplog.text
assert '"option_probabilities":[0.56,0.35]' in caplog.text
assert "status=rejected_low_confidence" in caplog.text
assert "PRIVATE_CUSTOMER_TEXT" not in caplog.text
@pytest.mark.asyncio
async def test_opt_in_choice_diagnostics_accepted(monkeypatch, caplog):
import logging
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_DECISION_DIAGNOSTICS", "true")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.60")
monkeypatch.setattr(client, "_post", lambda body: {"model": "jev-1.14.0", "answers": {
"__jev_q0_o0": {"type": "noul", "noul": .2},
"__jev_q0_o1": {"type": "noul", "noul": .88}}})
with caplog.at_level(logging.INFO, logger="agent_framework.decisions"):
result = await client.evaluate("transaction_intent_shift", [],
[client.choice("decision", ["CONTINUE", "SHIFT"])])
assert result["decision"] == "SHIFT"
assert result["confidence"] == pytest.approx(.88)
assert '"selected_option_index":1' in caplog.text
assert "status=accepted" in caplog.text
@pytest.mark.asyncio
async def test_diagnostics_off_by_default(monkeypatch, caplog):
import logging
monkeypatch.delenv("JEV_DECISION_DIAGNOSTICS", raising=False)
monkeypatch.setenv("JEV_ENDPOINT", "https://api.typesafe.ai/v1/systemone")
monkeypatch.setenv("JEV_MIN_CONFIDENCE", "0.6")
monkeypatch.setattr(client, "_post", lambda body: {"answers": {
"__jev_q0_o0": {"noul": .9}, "__jev_q0_o1": {"noul": .1}}})
with caplog.at_level(logging.INFO, logger="agent_framework.decisions"):
await client.evaluate("transaction_intent_shift", [], [client.choice("decision", ["A", "B"])])
assert "jev.choice.diagnostics" not in caplog.text
assert "jev.decision.diagnostics" not in caplog.text

View File

@@ -0,0 +1,28 @@
import json
from unittest.mock import patch
import pytest
from agent_framework.decisions import client
@pytest.mark.asyncio
async def test_direct_predicate_contract(monkeypatch):
monkeypatch.setenv('JEV_ENDPOINT','https://api.typesafe.ai/v1/systemone')
monkeypatch.setenv('JEV_MODEL','jev-latest')
monkeypatch.setenv('JEV_MIN_CONFIDENCE','0.7')
captured=[]
def fake_post(body):
captured.append(body)
return {'model':'jev-latest','answers':{'billing':{'type':'noul','noul':0.98}}}
monkeypatch.setattr(client,'_post',fake_post)
result=await client.evaluate('test',[{'role':'user','content':'fatura'}],[client.predicate('billing')])
assert result['billing'] is True
assert result['confidence'] == pytest.approx(.98)
assert captured[0]['model']=='jev-latest'
assert 'state' in captured[0] and 'input' not in captured[0]
assert captured[0]['questions']['billing']['type']=='noul'
@pytest.mark.asyncio
async def test_direct_missing_answers_rejected(monkeypatch):
monkeypatch.setenv('JEV_ENDPOINT','https://api.typesafe.ai/v1/systemone')
monkeypatch.setattr(client,'_post',lambda body:{'answers':{}})
with pytest.raises(ValueError):
await client.evaluate('test',[],[client.predicate('billing')])