bugfix: ABANDON precedence
This commit is contained in:
Binary file not shown.
@@ -630,9 +630,24 @@ class EnterpriseRouter:
|
|||||||
if state_decision:
|
if state_decision:
|
||||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||||
|
|
||||||
# Confirmation is the only transaction input with absolute precedence:
|
# Explicit abandonment must be evaluated before confirmation
|
||||||
# an explicit yes/no answers the confirmation contract itself.
|
# consumption. Otherwise an utterance such as
|
||||||
|
# "não quero cancelar; quero apenas consultar meus serviços" can be
|
||||||
|
# trapped by the confirmation state and never reach ABANDON. A plain
|
||||||
|
# yes/no still keeps deterministic confirmation precedence because it
|
||||||
|
# does not pass the abandonment gate.
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=state_decision,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=state_decision
|
state, text=str(text), state_decision=state_decision
|
||||||
)
|
)
|
||||||
@@ -687,6 +702,21 @@ class EnterpriseRouter:
|
|||||||
next_state=tx_status,
|
next_state=tx_status,
|
||||||
)
|
)
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=synthetic,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
abandonment.metadata = {
|
||||||
|
**(abandonment.metadata or {}),
|
||||||
|
"transaction_state_recovered": True,
|
||||||
|
}
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=synthetic
|
state, text=str(text), state_decision=synthetic
|
||||||
)
|
)
|
||||||
@@ -846,11 +876,12 @@ class EnterpriseRouter:
|
|||||||
) -> RouteDecision | None:
|
) -> RouteDecision | None:
|
||||||
"""Try to consume the turn under the active transaction contract first.
|
"""Try to consume the turn under the active transaction contract first.
|
||||||
|
|
||||||
AWAITING_CONFIRMATION consumes an explicit confirmation before any shift
|
In AWAITING_CONFIRMATION, an explicit ABANDON candidate is classified
|
||||||
classification. COLLECTING_PARAMETERS also has precedence: if at least one
|
before confirmation consumption; plain yes/no responses still keep
|
||||||
pending parameter can be extracted, the active transaction keeps ownership
|
deterministic confirmation precedence because they do not pass the
|
||||||
of the turn. Semantic intent-shift is evaluated only when extraction returns
|
abandonment gate. In COLLECTING_PARAMETERS, ABANDON is likewise checked
|
||||||
no usable pending parameter.
|
before extraction, while SHIFT remains after parameter extraction so valid
|
||||||
|
referential/value answers are not stolen from the active transaction.
|
||||||
"""
|
"""
|
||||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
|
|||||||
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.
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.
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.
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.
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.
Binary file not shown.
Binary file not shown.
@@ -630,9 +630,24 @@ class EnterpriseRouter:
|
|||||||
if state_decision:
|
if state_decision:
|
||||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||||
|
|
||||||
# Confirmation is the only transaction input with absolute precedence:
|
# Explicit abandonment must be evaluated before confirmation
|
||||||
# an explicit yes/no answers the confirmation contract itself.
|
# consumption. Otherwise an utterance such as
|
||||||
|
# "não quero cancelar; quero apenas consultar meus serviços" can be
|
||||||
|
# trapped by the confirmation state and never reach ABANDON. A plain
|
||||||
|
# yes/no still keeps deterministic confirmation precedence because it
|
||||||
|
# does not pass the abandonment gate.
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=state_decision,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=state_decision
|
state, text=str(text), state_decision=state_decision
|
||||||
)
|
)
|
||||||
@@ -640,13 +655,24 @@ class EnterpriseRouter:
|
|||||||
await self._emit(consumed, state)
|
await self._emit(consumed, state)
|
||||||
return consumed
|
return consumed
|
||||||
|
|
||||||
# Transaction parameter precedence is absolute while collecting:
|
# During parameter collection, explicit abandonment has stronger
|
||||||
# first let the active transaction try to consume the current turn.
|
# precedence than parameter extraction. Otherwise an LLM parameter
|
||||||
# Only when NO pending parameter can be extracted do we ask the
|
# extractor can incorrectly "consume" phrases such as
|
||||||
# semantic classifier whether the user changed goals. This prevents
|
# "esquece, não quero mais cancelar" and the ABANDON classifier is
|
||||||
# value/name/reference answers (for example "a de 14,99") from being
|
# never reached. SHIFT remains after parameter extraction so a valid
|
||||||
# stolen by a semantically plausible but incompatible intent.
|
# value/name/reference answer is not stolen by a plausible new intent.
|
||||||
if tx_status == "COLLECTING_PARAMETERS":
|
if tx_status == "COLLECTING_PARAMETERS":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=state_decision,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=state_decision
|
state, text=str(text), state_decision=state_decision
|
||||||
)
|
)
|
||||||
@@ -687,6 +713,21 @@ class EnterpriseRouter:
|
|||||||
next_state=tx_status,
|
next_state=tx_status,
|
||||||
)
|
)
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=synthetic,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
abandonment.metadata = {
|
||||||
|
**(abandonment.metadata or {}),
|
||||||
|
"transaction_state_recovered": True,
|
||||||
|
}
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=synthetic
|
state, text=str(text), state_decision=synthetic
|
||||||
)
|
)
|
||||||
@@ -699,6 +740,21 @@ class EnterpriseRouter:
|
|||||||
return consumed
|
return consumed
|
||||||
|
|
||||||
if tx_status == "COLLECTING_PARAMETERS":
|
if tx_status == "COLLECTING_PARAMETERS":
|
||||||
|
if self._looks_like_explicit_abandonment(str(text)):
|
||||||
|
abandonment = await self._transaction_state_interruption_candidate(
|
||||||
|
state,
|
||||||
|
text=str(text),
|
||||||
|
state_decision=synthetic,
|
||||||
|
abandon_only=True,
|
||||||
|
)
|
||||||
|
if abandonment is not None:
|
||||||
|
abandonment.metadata = {
|
||||||
|
**(abandonment.metadata or {}),
|
||||||
|
"transaction_state_recovered": True,
|
||||||
|
}
|
||||||
|
await self._emit(abandonment, state)
|
||||||
|
return abandonment
|
||||||
|
|
||||||
consumed = await self._transaction_parameter_precedence(
|
consumed = await self._transaction_parameter_precedence(
|
||||||
state, text=str(text), state_decision=synthetic
|
state, text=str(text), state_decision=synthetic
|
||||||
)
|
)
|
||||||
@@ -846,11 +902,12 @@ class EnterpriseRouter:
|
|||||||
) -> RouteDecision | None:
|
) -> RouteDecision | None:
|
||||||
"""Try to consume the turn under the active transaction contract first.
|
"""Try to consume the turn under the active transaction contract first.
|
||||||
|
|
||||||
AWAITING_CONFIRMATION consumes an explicit confirmation before any shift
|
In AWAITING_CONFIRMATION, an explicit ABANDON candidate is classified
|
||||||
classification. COLLECTING_PARAMETERS also has precedence: if at least one
|
before confirmation consumption; plain yes/no responses still keep
|
||||||
pending parameter can be extracted, the active transaction keeps ownership
|
deterministic confirmation precedence because they do not pass the
|
||||||
of the turn. Semantic intent-shift is evaluated only when extraction returns
|
abandonment gate. In COLLECTING_PARAMETERS, ABANDON is likewise checked
|
||||||
no usable pending parameter.
|
before extraction, while SHIFT remains after parameter extraction so valid
|
||||||
|
referential/value answers are not stolen from the active transaction.
|
||||||
"""
|
"""
|
||||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||||
if tx_status == "AWAITING_CONFIRMATION":
|
if tx_status == "AWAITING_CONFIRMATION":
|
||||||
@@ -960,12 +1017,36 @@ class EnterpriseRouter:
|
|||||||
}
|
}
|
||||||
return state_decision
|
return state_decision
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _looks_like_explicit_abandonment(text: str) -> bool:
|
||||||
|
"""Cheap, domain-neutral gate for semantic ABANDON classification.
|
||||||
|
|
||||||
|
This is deliberately not the final decision. It only protects clear
|
||||||
|
abandonment utterances from being swallowed by parameter extraction; the
|
||||||
|
semantic classifier still decides whether the turn is actually ABANDON.
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
normalized = str(text or "").strip().lower()
|
||||||
|
if not normalized:
|
||||||
|
return False
|
||||||
|
cues = (
|
||||||
|
r"\besquece(?:\s+isso|\s+essa|\s+esse|\s+tudo)?\b",
|
||||||
|
r"\bn[aã]o\s+quero\s+mais\b",
|
||||||
|
r"\bdesisti(?:\s+disso|\s+dessa|\s+desse)?\b",
|
||||||
|
r"\bdeixa\s+(?:isso|essa|esse|pra\s+l[aá]|para\s+l[aá])\b",
|
||||||
|
r"\bpode\s+parar\b",
|
||||||
|
r"\bquero\s+parar\b",
|
||||||
|
r"\bcancela\s+essa\s+solicita[cç][aã]o\b",
|
||||||
|
)
|
||||||
|
return any(re.search(pattern, normalized, flags=re.IGNORECASE) for pattern in cues)
|
||||||
|
|
||||||
async def _transaction_state_interruption_candidate(
|
async def _transaction_state_interruption_candidate(
|
||||||
self,
|
self,
|
||||||
state: dict[str, Any],
|
state: dict[str, Any],
|
||||||
*,
|
*,
|
||||||
text: str,
|
text: str,
|
||||||
state_decision: RouteDecision,
|
state_decision: RouteDecision,
|
||||||
|
abandon_only: bool = False,
|
||||||
) -> RouteDecision | None:
|
) -> RouteDecision | None:
|
||||||
"""Detecta semanticamente mudança de intenção durante uma transação.
|
"""Detecta semanticamente mudança de intenção durante uma transação.
|
||||||
|
|
||||||
@@ -1038,6 +1119,18 @@ class EnterpriseRouter:
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
if abandon_only:
|
||||||
|
system = (
|
||||||
|
"Você decide somente se o usuário desistiu explicitamente da ação/transação ativa. "
|
||||||
|
"Use o significado completo da mensagem e o contexto transacional; não use palavras isoladas como regra. "
|
||||||
|
"Se houver desistência explícita da ação atual, retorne ABANDON. "
|
||||||
|
"Quando o abandono vier acompanhado de um novo objetivo, também informe intent e agent desse novo objetivo. "
|
||||||
|
"Quando for apenas abandono sem novo objetivo, intent e agent podem ser nulos. "
|
||||||
|
"Se a mensagem apenas fornece parâmetro, confirma contexto, ou somente muda/adiciona outro objetivo sem "
|
||||||
|
"desistir explicitamente da ação atual, retorne CONTINUE. "
|
||||||
|
"Retorne somente JSON válido com decision, intent, agent, confidence, reason."
|
||||||
|
)
|
||||||
|
else:
|
||||||
system = (
|
system = (
|
||||||
"Você decide apenas se o turno atual continua a transação ativa ou muda de intenção. "
|
"Você decide apenas se o turno atual continua a transação ativa ou muda de intenção. "
|
||||||
"Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
|
"Use o significado da mensagem e o contexto transacional; não use palavras isoladas como regra. "
|
||||||
@@ -1074,7 +1167,10 @@ class EnterpriseRouter:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
decision_kind = str(data.get("decision") or "").strip().upper()
|
decision_kind = str(data.get("decision") or "").strip().upper()
|
||||||
if decision_kind not in {"SHIFT", "ABANDON"}:
|
if abandon_only:
|
||||||
|
if decision_kind != "ABANDON":
|
||||||
|
return None
|
||||||
|
elif decision_kind not in {"SHIFT", "ABANDON"}:
|
||||||
return None
|
return None
|
||||||
confidence = float(data.get("confidence") or 0.0)
|
confidence = float(data.get("confidence") or 0.0)
|
||||||
if confidence < self.intent_shift_threshold:
|
if confidence < self.intent_shift_threshold:
|
||||||
|
|||||||
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.
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.
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])
|
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:
|
if kwargs.get("profile_name") == "transaction_parameter_extraction" or "pending_parameters:" in prompt:
|
||||||
return json.dumps({"subject": None, "valor": None})
|
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():
|
if "nao quero mais cancelar" in prompt.lower() or "não quero mais cancelar" in prompt.lower():
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"decision": "ABANDON",
|
"decision": "ABANDON",
|
||||||
@@ -48,6 +56,8 @@ router:
|
|||||||
state_policies:
|
state_policies:
|
||||||
- state: COLLECTING_PARAMETERS
|
- state: COLLECTING_PARAMETERS
|
||||||
agent: contestacao_agent
|
agent: contestacao_agent
|
||||||
|
- state: WAITING_CONTESTACAO_CONFIRMATION
|
||||||
|
agent: contestacao_agent
|
||||||
intents:
|
intents:
|
||||||
- name: contas_contestation
|
- name: contas_contestation
|
||||||
agent: contestacao_agent
|
agent: contestacao_agent
|
||||||
@@ -61,6 +71,10 @@ intents:
|
|||||||
agent: billing_agent
|
agent: billing_agent
|
||||||
priority: 30
|
priority: 30
|
||||||
keywords: [fatura]
|
keywords: [fatura]
|
||||||
|
- name: product_services_information
|
||||||
|
agent: product_agent
|
||||||
|
priority: 40
|
||||||
|
keywords: [servicos contratados, serviços contratados]
|
||||||
""",
|
""",
|
||||||
encoding="utf-8",
|
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"
|
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:
|
class _PolicyRouter:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.registry = SimpleNamespace(
|
self.registry = SimpleNamespace(
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user