137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.extensions.tim_guardrails import TimOutOfScopeRail
|
|
|
|
|
|
class _FailIfCalledLLM:
|
|
async def ainvoke(self, *args, **kwargs):
|
|
raise AssertionError('TIM_OOS LLM must not run for structurally authorized human handoff')
|
|
|
|
|
|
class _BlockingLLM:
|
|
def __init__(self):
|
|
self.calls = 0
|
|
|
|
async def ainvoke(self, *args, **kwargs):
|
|
self.calls += 1
|
|
return '{"allowed": false, "reason": "mensagem fora do escopo de contas/faturas TIM"}'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_allows_structurally_authorized_human_handoff_without_calling_llm():
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Vou encaminhar seu atendimento para uma pessoa.',
|
|
{
|
|
'current_route': 'human_handoff',
|
|
'current_intent': 'human_handoff',
|
|
'session_control': 'HUMAN_HANDOFF',
|
|
'human_handoff_requested': True,
|
|
'guardrail_llm': _FailIfCalledLLM(),
|
|
},
|
|
)
|
|
|
|
assert decision.allowed is True
|
|
assert decision.code == 'TIM_OOS'
|
|
assert decision.reason == 'handoff_humano_autorizado'
|
|
assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_allows_route_decision_structural_handoff_without_calling_llm():
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Vou encaminhar seu atendimento para uma pessoa.',
|
|
{
|
|
'route_decision': {
|
|
'route': 'human_handoff',
|
|
'intent': 'human_handoff',
|
|
'handoff': True,
|
|
'metadata': {'session_control': 'HUMAN_HANDOFF'},
|
|
},
|
|
'guardrail_llm': _FailIfCalledLLM(),
|
|
},
|
|
)
|
|
|
|
assert decision.allowed is True
|
|
assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_does_not_bypass_transfer_phrase_without_structural_handoff():
|
|
llm = _BlockingLLM()
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Vou encaminhar seu atendimento para uma pessoa.',
|
|
{
|
|
'current_route': 'faturas_agent',
|
|
'current_intent': 'contas_invoice_query',
|
|
'guardrail_llm': llm,
|
|
},
|
|
)
|
|
|
|
assert llm.calls == 1
|
|
assert decision.allowed is False
|
|
assert decision.reason == 'mensagem fora do escopo de contas/faturas TIM'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_requires_route_and_control_evidence_not_only_handoff_boolean():
|
|
llm = _BlockingLLM()
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Vou encaminhar seu atendimento para uma pessoa.',
|
|
{
|
|
'handoff': True,
|
|
'current_route': 'faturas_agent',
|
|
'current_intent': 'contas_invoice_query',
|
|
'guardrail_llm': llm,
|
|
},
|
|
)
|
|
|
|
assert llm.calls == 1
|
|
assert decision.allowed is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_allows_terminal_handoff_from_resumed_workflow_without_router_handoff():
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Para continuar com a sua solicitação, aguarde um instante.',
|
|
{
|
|
'current_route': 'faturas_agent',
|
|
'current_intent': 'contas_invoice_explanation',
|
|
'mcp_results': [{
|
|
'tool_name': 'retomar_workflow',
|
|
'result': {
|
|
'status': 'COMPLETED',
|
|
'output': {
|
|
'session_control': 'HUMAN_HANDOFF',
|
|
'human_handoff_requested': True,
|
|
'handoff': True,
|
|
'session_ended': True,
|
|
'terminal_status': 'human_handoff',
|
|
},
|
|
},
|
|
}],
|
|
'guardrail_llm': _FailIfCalledLLM(),
|
|
},
|
|
)
|
|
|
|
assert decision.allowed is True
|
|
assert decision.reason == 'handoff_humano_autorizado'
|
|
assert decision.metadata['mechanism'] == 'deterministic_handoff_bypass'
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tim_oos_does_not_trust_non_terminal_workflow_handoff_flag():
|
|
llm = _BlockingLLM()
|
|
decision = await TimOutOfScopeRail().evaluate(
|
|
'Vou encaminhar seu atendimento para uma pessoa.',
|
|
{
|
|
'current_route': 'faturas_agent',
|
|
'current_intent': 'contas_invoice_explanation',
|
|
'mcp_results': [{'result': {'output': {'handoff': True}}}],
|
|
'guardrail_llm': llm,
|
|
},
|
|
)
|
|
|
|
assert llm.calls == 1
|
|
assert decision.allowed is False
|