bugfix: ABANDON precedence
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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(
|
||||
|
||||
Binary file not shown.
Reference in New Issue
Block a user