bugfix: ABANDON precedence
This commit is contained in:
Binary file not shown.
@@ -630,9 +630,24 @@ class EnterpriseRouter:
|
||||
if state_decision:
|
||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||
|
||||
# Confirmation is the only transaction input with absolute precedence:
|
||||
# an explicit yes/no answers the confirmation contract itself.
|
||||
# Explicit abandonment must be evaluated before confirmation
|
||||
# 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 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(
|
||||
state, text=str(text), state_decision=state_decision
|
||||
)
|
||||
@@ -687,6 +702,21 @@ class EnterpriseRouter:
|
||||
next_state=tx_status,
|
||||
)
|
||||
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(
|
||||
state, text=str(text), state_decision=synthetic
|
||||
)
|
||||
@@ -846,11 +876,12 @@ class EnterpriseRouter:
|
||||
) -> RouteDecision | None:
|
||||
"""Try to consume the turn under the active transaction contract first.
|
||||
|
||||
AWAITING_CONFIRMATION consumes an explicit confirmation before any shift
|
||||
classification. COLLECTING_PARAMETERS also has precedence: if at least one
|
||||
pending parameter can be extracted, the active transaction keeps ownership
|
||||
of the turn. Semantic intent-shift is evaluated only when extraction returns
|
||||
no usable pending parameter.
|
||||
In AWAITING_CONFIRMATION, an explicit ABANDON candidate is classified
|
||||
before confirmation consumption; plain yes/no responses still keep
|
||||
deterministic confirmation precedence because they do not pass the
|
||||
abandonment gate. In COLLECTING_PARAMETERS, ABANDON is likewise checked
|
||||
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()
|
||||
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:
|
||||
tx_status = str(state.get("transaction_status") or "").strip().upper()
|
||||
|
||||
# Confirmation is the only transaction input with absolute precedence:
|
||||
# an explicit yes/no answers the confirmation contract itself.
|
||||
# Explicit abandonment must be evaluated before confirmation
|
||||
# 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 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(
|
||||
state, text=str(text), state_decision=state_decision
|
||||
)
|
||||
@@ -640,13 +655,24 @@ class EnterpriseRouter:
|
||||
await self._emit(consumed, state)
|
||||
return consumed
|
||||
|
||||
# Transaction parameter precedence is absolute while collecting:
|
||||
# first let the active transaction try to consume the current turn.
|
||||
# Only when NO pending parameter can be extracted do we ask the
|
||||
# semantic classifier whether the user changed goals. This prevents
|
||||
# value/name/reference answers (for example "a de 14,99") from being
|
||||
# stolen by a semantically plausible but incompatible intent.
|
||||
# During parameter collection, explicit abandonment has stronger
|
||||
# precedence than parameter extraction. Otherwise an LLM parameter
|
||||
# extractor can incorrectly "consume" phrases such as
|
||||
# "esquece, não quero mais cancelar" and the ABANDON classifier is
|
||||
# never reached. SHIFT remains after parameter extraction so a valid
|
||||
# value/name/reference answer is not stolen by a plausible new intent.
|
||||
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(
|
||||
state, text=str(text), state_decision=state_decision
|
||||
)
|
||||
@@ -687,6 +713,21 @@ class EnterpriseRouter:
|
||||
next_state=tx_status,
|
||||
)
|
||||
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(
|
||||
state, text=str(text), state_decision=synthetic
|
||||
)
|
||||
@@ -699,6 +740,21 @@ class EnterpriseRouter:
|
||||
return consumed
|
||||
|
||||
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(
|
||||
state, text=str(text), state_decision=synthetic
|
||||
)
|
||||
@@ -846,11 +902,12 @@ class EnterpriseRouter:
|
||||
) -> RouteDecision | None:
|
||||
"""Try to consume the turn under the active transaction contract first.
|
||||
|
||||
AWAITING_CONFIRMATION consumes an explicit confirmation before any shift
|
||||
classification. COLLECTING_PARAMETERS also has precedence: if at least one
|
||||
pending parameter can be extracted, the active transaction keeps ownership
|
||||
of the turn. Semantic intent-shift is evaluated only when extraction returns
|
||||
no usable pending parameter.
|
||||
In AWAITING_CONFIRMATION, an explicit ABANDON candidate is classified
|
||||
before confirmation consumption; plain yes/no responses still keep
|
||||
deterministic confirmation precedence because they do not pass the
|
||||
abandonment gate. In COLLECTING_PARAMETERS, ABANDON is likewise checked
|
||||
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()
|
||||
if tx_status == "AWAITING_CONFIRMATION":
|
||||
@@ -960,12 +1017,36 @@ class EnterpriseRouter:
|
||||
}
|
||||
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(
|
||||
self,
|
||||
state: dict[str, Any],
|
||||
*,
|
||||
text: str,
|
||||
state_decision: RouteDecision,
|
||||
abandon_only: bool = False,
|
||||
) -> RouteDecision | None:
|
||||
"""Detecta semanticamente mudança de intenção durante uma transação.
|
||||
|
||||
@@ -1038,18 +1119,30 @@ class EnterpriseRouter:
|
||||
else None
|
||||
),
|
||||
}
|
||||
system = (
|
||||
"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. "
|
||||
"A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. "
|
||||
"Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. "
|
||||
"Se o usuário passou claramente a perseguir outro objetivo sem desistir explicitamente da ação atual, "
|
||||
"retorne SHIFT e a nova intent permitida. "
|
||||
"Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. "
|
||||
"Quando ABANDON 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. "
|
||||
"Retorne somente JSON válido com decision, intent, agent, confidence, reason."
|
||||
)
|
||||
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 = (
|
||||
"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. "
|
||||
"A extração dos parâmetros pendentes já foi tentada antes desta etapa e não consumiu o turno. "
|
||||
"Se ainda assim a mensagem for apenas uma resposta referencial/valor/nome ao dado pendente, retorne CONTINUE. "
|
||||
"Se o usuário passou claramente a perseguir outro objetivo sem desistir explicitamente da ação atual, "
|
||||
"retorne SHIFT e a nova intent permitida. "
|
||||
"Se o usuário desistiu explicitamente da ação/transação atual, retorne ABANDON. "
|
||||
"Quando ABANDON 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. "
|
||||
"Retorne somente JSON válido com decision, intent, agent, confidence, reason."
|
||||
)
|
||||
user = {
|
||||
"message": text,
|
||||
"transaction": transaction_context,
|
||||
@@ -1074,7 +1167,10 @@ class EnterpriseRouter:
|
||||
return None
|
||||
|
||||
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
|
||||
confidence = float(data.get("confidence") or 0.0)
|
||||
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.
Reference in New Issue
Block a user