Sweep coordinator (Step 16): - SweepCoordinator runs all 8 agents in parallel with 60s per-agent / 300s total timeout - Aggregates findings, actions, errors into SweepCoordinatorResult - Registered in FastAPI lifespan; triggered via POST /sweep Structured logging (Step 18): - logging_utils/structured.py: JSONFormatter emitting ts/level/logger/msg + custom fields - log_directive_event() for structured directive lifecycle logging - push_to_loki() async Loki push (graceful no-op if LOKI_URL unset) - configure_logging() replaces root handler at startup Tests (Steps 17+19): - conftest.py: mock_odoo, mock_pool, mock_llm fixtures - test_tool_validator.py: 9 tests covering validation, coercion, hallucination stripping - test_llm_router.py: 6 tests covering local/cloud/hybrid modes and HIPAA enforcement - test_peer_bus.py: 6 tests covering registration, timeout, depth, circular detection - test_finance_agent.py: 10 tests covering all 6 steps + sweep + peer request - test_memory_manager.py: 3 tests covering context build + hard cap enforcement - test_dispatch_router.py: 3 tests covering dispatch, rate limit, health endpoint - test_odoo_client.py: 4 tests covering search_read, write result, unlink warning - test_e2e_dispatch.py: 2 E2E tests - full dispatch cycle + peer bus communication README (Step 20): architecture diagram, privacy modes, quick start, env vars, structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
74 lines
2.4 KiB
Python
74 lines
2.4 KiB
Python
import pytest
|
|
from agent_service.llm.tool_validator import ToolCallValidator, AgentConfigError
|
|
|
|
SAMPLE_TOOLS = [
|
|
{'name': 'get_invoices', 'parameters': {
|
|
'state': {'type': 'string', 'optional': True},
|
|
'limit': {'type': 'integer', 'optional': True},
|
|
'partner_id': {'type': 'integer'},
|
|
}},
|
|
{'name': 'send_reminder', 'parameters': {
|
|
'invoice_id': {'type': 'integer'},
|
|
'message': {'type': 'string', 'optional': True},
|
|
}},
|
|
]
|
|
|
|
|
|
def test_validator_init():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
assert 'get_invoices' in v._tool_map
|
|
|
|
|
|
def test_raises_on_too_many_tools():
|
|
many_tools = [{'name': f'tool_{i}', 'parameters': {}} for i in range(9)]
|
|
with pytest.raises(AgentConfigError, match='MAX_TOOLS_PER_AGENT'):
|
|
ToolCallValidator(many_tools)
|
|
|
|
|
|
def test_valid_tool_call():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
result = v.validate({'name': 'get_invoices', 'arguments': {'partner_id': 42}})
|
|
assert result.name == 'get_invoices'
|
|
assert result.arguments['partner_id'] == 42
|
|
|
|
|
|
def test_strips_hallucinated_params():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
result = v.validate({'name': 'get_invoices', 'arguments': {
|
|
'partner_id': 1, 'nonexistent_param': 'bad',
|
|
}})
|
|
assert 'nonexistent_param' not in result.arguments
|
|
|
|
|
|
def test_missing_required_param_raises():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
with pytest.raises(ValueError, match='partner_id'):
|
|
v.validate({'name': 'get_invoices', 'arguments': {}})
|
|
|
|
|
|
def test_type_coercion_int():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
result = v.validate({'name': 'get_invoices', 'arguments': {'partner_id': '42'}})
|
|
assert result.arguments['partner_id'] == 42
|
|
|
|
|
|
def test_unknown_tool_raises():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
with pytest.raises(ValueError, match='Unknown tool'):
|
|
v.validate({'name': 'nonexistent_tool', 'arguments': {}})
|
|
|
|
|
|
def test_parse_or_fallback_returns_none_on_bad_json():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
result = v.parse_or_fallback('not json at all', context='test')
|
|
assert result is None
|
|
|
|
|
|
def test_parse_or_fallback_valid_json():
|
|
v = ToolCallValidator(SAMPLE_TOOLS)
|
|
import json
|
|
raw = json.dumps({'name': 'send_reminder', 'arguments': {'invoice_id': 5}})
|
|
result = v.parse_or_fallback(raw, context='test')
|
|
assert result is not None
|
|
assert result.name == 'send_reminder'
|