bugfix: ABANDON precedence

This commit is contained in:
2026-09-17 10:23:02 -03:00
parent 3b03bdbdab
commit c318d9af27
60 changed files with 255 additions and 33 deletions

View File

@@ -13,6 +13,14 @@ class _RouterLLM:
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
return json.dumps({"subject": None, "valor": None})
if "nao quero cancelar. quero apenas verificar os meus servicos contratados" in prompt.lower():
return json.dumps({
"decision": "ABANDON",
"intent": "product_services_information",
"agent": "product_agent",
"confidence": 0.99,
"reason": "abandono explícito do cancelamento com novo objetivo",
})
if "nao quero mais cancelar" in prompt.lower() or "não quero mais cancelar" in prompt.lower():
return json.dumps({
"decision": "ABANDON",
@@ -48,6 +56,8 @@ router:
state_policies:
- state: COLLECTING_PARAMETERS
agent: contestacao_agent
- state: WAITING_CONTESTACAO_CONFIRMATION
agent: contestacao_agent
intents:
- name: contas_contestation
agent: contestacao_agent
@@ -61,6 +71,10 @@ intents:
agent: billing_agent
priority: 30
keywords: [fatura]
- name: product_services_information
agent: product_agent
priority: 40
keywords: [servicos contratados, serviços contratados]
""",
encoding="utf-8",
)
@@ -123,6 +137,87 @@ async def test_explicit_abandonment_without_new_goal_does_not_rearm_tool(tmp_pat
assert decision.metadata["interrupted_intent"] == "contas_contestation"
def _awaiting_confirmation_state(message):
return {
"user_text": message,
"sanitized_input": message,
"next_state": "WAITING_CONTESTACAO_CONFIRMATION",
"transaction_status": "AWAITING_CONFIRMATION",
"intent": "state:WAITING_CONTESTACAO_CONFIRMATION",
"active_agent": "contestacao_agent",
"active_transaction": {
"tool_name": "cancelar_vas_avulso",
"arguments": {"subject": "Tamboro Mensal"},
"status": "AWAITING_CONFIRMATION",
"started_from_intent": "contas_vas_cancel",
},
"pending_tool_call": {
"tool_name": "cancelar_vas_avulso",
"arguments": {"subject": "Tamboro Mensal"},
},
"history": [
{"role": "user", "content": "quero cancelar tamboro"},
{"role": "assistant", "content": "Você confirma o cancelamento?"},
{"role": "user", "content": message},
],
}
@pytest.mark.asyncio
async def test_explicit_abandonment_precedes_awaiting_confirmation_and_routes_new_goal(tmp_path):
decision = await _router(tmp_path).route(
_awaiting_confirmation_state(
"nao quero cancelar. quero apenas verificar os meus servicos contratados"
)
)
assert decision.intent == "product_services_information"
assert decision.agent == "product_agent"
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
assert decision.metadata["interrupted_intent"] == "contas_vas_cancel"
assert not decision.metadata.get("transaction_turn_consumed")
@pytest.mark.asyncio
async def test_plain_negative_confirmation_remains_confirmation_not_abandon(tmp_path):
decision = await _router(tmp_path).route(_awaiting_confirmation_state("não"))
assert decision.intent == "state:WAITING_CONTESTACAO_CONFIRMATION"
assert decision.agent == "contestacao_agent"
assert decision.metadata["transaction_turn_consumed"] is True
assert decision.metadata["transaction_confirmation_decision"] == "reject"
assert "transaction_interruption" not in decision.metadata
class _MisleadingParameterRouterLLM(_RouterLLM):
async def ainvoke(self, messages, **kwargs):
prompt = messages[-1]["content"] if isinstance(messages[-1], dict) else str(messages[-1])
# Simulate the real failure mode: the parameter extractor hallucinates a
# usable value from an abandonment utterance. ABANDON must win before
# parameter extraction is allowed to consume the turn.
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
if "nao quero mais cancelar" in prompt.lower() or "não quero mais cancelar" in prompt.lower():
return json.dumps({"valor": 14.99})
return await super().ainvoke(messages, **kwargs)
def _router_with_misleading_parameter_extractor(tmp_path):
router = _router(tmp_path)
router.llm = _MisleadingParameterRouterLLM()
return router
@pytest.mark.asyncio
async def test_explicit_abandonment_precedes_misleading_parameter_extraction(tmp_path):
decision = await _router_with_misleading_parameter_extractor(tmp_path).route(
_active_state("esquece, nao quero mais cancelar")
)
assert decision.intent == "state:TRANSACTION_ABANDONED"
assert decision.agent == "contestacao_agent"
assert decision.mcp_tools == []
assert decision.metadata["transaction_interruption"] == "explicit_abandonment"
assert not decision.metadata.get("transaction_turn_consumed")
class _PolicyRouter:
def __init__(self):
self.registry = SimpleNamespace(