nova funcionalidade: reconciliacao temporal

This commit is contained in:
2026-09-02 13:07:21 -03:00
parent e6c254ff83
commit 76c8cdc738
190 changed files with 747 additions and 82 deletions

Binary file not shown.

View File

@@ -49,6 +49,19 @@ class SuporteContasAgent(AgentRuntimeMixin):
)
state["mcp_results"] = tool_context
# A no-match retry is already a resolved control-flow decision: the
# framework/router concluded that the utterance is not understood. Do
# not ask another generative model to reinterpret it into a concrete
# business action (which can fabricate intent from noisy ASR).
if str(state.get("intent") or "") == "contas_no_match_retry":
return {
"answer": "Desculpe, não entendi. Poderia repetir de outra forma?",
"next_state": state.get("next_state") or "SUPORTE_CONTAS_ACTIVE",
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {

View File

@@ -147,6 +147,43 @@ def _authorized_human_handoff(context: dict[str, Any]) -> bool:
return any(terminal_workflow_handoff(root) for root in roots)
def _deterministic_tim_domain_response(text: str, context: dict[str, Any]) -> bool:
"""Conservative fast path for unmistakable TIM Contas domain output.
This extension may know the TIM Contas vocabulary; the generic framework does
not. Human-transfer language is intentionally excluded so handoff authorization
remains exclusively structural. Ambiguous domain prose still goes to TIM_OOS LLM.
"""
normalized = re.sub(r'\s+', ' ', str(text or '').strip().lower())
if not normalized:
return False
handoff_markers = (
'encaminhar', 'encaminhado', 'transferir', 'transferido',
'atendente', 'pessoa', 'humano', 'especialista',
)
if any(m in normalized for m in handoff_markers):
return False
domain_markers = (
'contestação', 'contestacao', 'fatura', 'cobrança', 'cobranca',
'plano', 'serviço', 'servico', 'vas', 'boleto', 'desconto',
)
completion_markers = (
'registrad', 'cancelad', 'concluíd', 'concluid', 'executad',
'processad', 'enviad', 'emitid', 'protocolo',
)
# Keep the bypass intentionally narrow: an unmistakable domain noun plus an
# operational/result marker. Ordinary explanations and ambiguous responses are
# still semantically audited by TIM_OOS.
return (
any(m in normalized for m in domain_markers)
and any(m in normalized for m in completion_markers)
)
def _parse_json(raw: Any) -> dict[str, Any]:
text = str(getattr(raw, 'content', raw) or '').strip()
m = re.search(r'\{[\s\S]*\}', text)
@@ -196,6 +233,22 @@ class TimOutOfScopeRail(_TimPromptRail):
},
},
)
if _deterministic_tim_domain_response(text, context):
return RailDecision(
code=self.code,
allowed=True,
reason='resposta suportada pelo contexto do domínio TIM Contas',
sanitized_text=text,
metadata={
'external': True,
'domain': 'TIM_CONTAS',
'mechanism': 'deterministic_domain_bypass',
'data': {
'allowed': True,
'reason': 'resposta suportada pelo contexto do domínio TIM Contas',
},
},
)
return await super().evaluate(text, context)
class TimProactiveOfferRail(_TimPromptRail):
@@ -347,6 +400,8 @@ class TimPrematureActionRail(_TimPromptRail):
'final_answer', 'customer_message', 'customer_response', 'answer', 'text',
'epistemic_status', 'output', 'result', 'items', 'results', 'errors',
'cancelados', 'nao_cancelados', 'nao_encontrados', 'terminal_status',
'contestation_registered', 'contested_invoice_amount', 'contested_invoice_amount_open',
'sms_sent', 'barcode', 'itemName', 'itemsResponse', 'contested_items', 'sr',
}
out = {}
for k, v in value.items():
@@ -414,6 +469,142 @@ class TimPrematureActionRail(_TimPromptRail):
return False
return candidate in cls._authoritative_output_messages(context)
@staticmethod
def _normalized_text(value: Any) -> str:
text = str(value or '').lower()
text = text.replace('r$', ' ')
text = re.sub(r'\s+', ' ', text)
return text.strip()
@classmethod
def _candidate_has_operational_completion_claim(cls, text: str) -> bool:
"""Detect completion/effect claims, not ordinary explanations.
REVPREC exists to stop claims that an operation already happened. A
billing explanation containing values/dates is not such a claim and must
not depend on a probabilistic binary LLM classification.
"""
normalized = cls._normalized_text(text)
patterns = (
r'\b(cancelad[oa]s?|cancelei|cancelamos|cancelou|cancelamento .*conclu)',
r'\b(contesta(?:ção|cao) .*?(?:registrad|abert|conclu|criad))',
r'\b(registrad[oa]s?|executei|executamos|executou|executad[oa]s?|conclu[ií]d[oa]s?|processad[oa]s?)\b',
r'\b(enviad[oa]s?|emitid[oa]s?|retirad[oa]s?|removid[oa]s?)\b',
r'\b(protocol(?:o)? .*?(?:abert|gerad|registrad))',
r'\b(?:foi|foram) (?:cancelad|contestad|registrad|enviad|emitid|retirad|removid)',
r'\b(?:was|were|has been) (?:cancelled|canceled|registered|sent|issued|removed)',
)
return any(re.search(pattern, normalized, re.I) for pattern in patterns)
@classmethod
def _current_execution_scalar_evidence(cls, context: dict[str, Any]) -> tuple[list[str], bool, bool]:
"""Flatten scalar facts from current successful result/output only.
Returns (facts, completed, positive_effect). Inputs, conversation state,
history and metadata are excluded so user claims can never prove an action.
"""
roots = (context or {}).get('mcp_results')
if roots is None:
roots = (context or {}).get('tool_result')
if isinstance(roots, dict):
roots = [roots]
if not isinstance(roots, (list, tuple)):
return [], False, False
facts: list[str] = []
completed = False
positive = False
positive_status = {'completed', 'opened', 'open', 'iniciada', 'iniciado', 'success', 'succeeded', 'closed', 'fechado'}
positive_keys = {
'success', 'executed', 'contestation_registered', 'sms_sent',
'cancelled', 'canceled', 'registered', 'created', 'updated',
}
skip = {'state', 'input', 'metadata', 'conversation_history', 'history', 'session', 'session_metadata'}
def walk(value: Any, path: tuple[str, ...] = ()) -> None:
nonlocal completed, positive
if isinstance(value, dict):
for k, v in value.items():
key = str(k).lower()
if key in skip:
continue
if key == 'status' and str(v).strip().lower() == 'completed':
completed = True
if key in positive_keys and v is True:
positive = True
if key == 'status' and str(v).strip().lower() in positive_status:
positive = True
# Keys such as sms_sent/barcode are themselves meaningful
# evidence and can support paraphrased customer-facing text.
if key not in {'output', 'result', 'results'}:
facts.append(key)
walk(v, path + (key,))
elif isinstance(value, (list, tuple)):
for item in value[:50]:
walk(item, path)
elif value not in (None, ''):
facts.append(str(value))
for item in roots:
if not isinstance(item, dict) or item.get('ok') is False:
continue
if str(((item.get('result') or {}) if isinstance(item.get('result'), dict) else {}).get('status') or '').upper() == 'COMPLETED':
completed = True
walk(item.get('result', item))
return facts, completed, positive
@classmethod
def _candidate_is_structurally_grounded_completion(cls, text: str, context: dict[str, Any]) -> bool:
"""Allow a paraphrased completion only when current execution proves it.
This deliberately does not know tool names. It requires a completed
current execution, a positive side-effect marker, grounding for every
material number mentioned by the candidate, and at least one meaningful
textual fact/entity shared with the execution result.
"""
if not cls._candidate_has_operational_completion_claim(text):
return False
facts, completed, positive = cls._current_execution_scalar_evidence(context)
if not (completed and positive and facts):
return False
candidate = cls._normalized_text(text)
evidence_text = cls._normalized_text(' '.join(facts))
def canon_number(raw: str) -> str:
raw = raw.strip().replace(' ', '')
if ',' in raw and '.' in raw:
if raw.rfind(',') > raw.rfind('.'):
raw = raw.replace('.', '').replace(',', '.')
else:
raw = raw.replace(',', '')
else:
raw = raw.replace(',', '.')
try:
num = float(raw)
return (f'{num:.6f}').rstrip('0').rstrip('.')
except ValueError:
return re.sub(r'\D', '', raw)
candidate_numbers = [canon_number(x) for x in re.findall(r'(?<!\w)\d[\d .,-]*\d|(?<!\w)\d', candidate)]
evidence_numbers = {canon_number(x) for x in re.findall(r'(?<!\w)\d[\d .,-]*\d|(?<!\w)\d', evidence_text)}
material_numbers = [n for n in candidate_numbers if len(re.sub(r'\D', '', n)) >= 2]
if not material_numbers:
return False
if any(n not in evidence_numbers for n in material_numbers):
return False
# Require an entity/message overlap beyond generic success vocabulary.
candidate_words = set(re.findall(r'[a-zà-ÿ0-9+_-]{4,}', candidate, re.I))
stop = {
'sucesso', 'registrada', 'registrado', 'concluido', 'concluida',
'cliente', 'valor', 'protocolo', 'cobranca', 'contestacao', 'servico',
'mensal', 'novo', 'boleto', 'recebeu', 'detalhes',
}
evidence_words = set(re.findall(r'[a-zà-ÿ0-9+_-]{4,}', evidence_text, re.I))
meaningful_overlap = (candidate_words - stop) & (evidence_words - stop)
return bool(meaningful_overlap)
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
# Strongest possible proof: the exact customer-facing candidate was emitted by
# the successful current-turn tool/workflow itself. REVPREC is about premature
@@ -433,6 +624,41 @@ class TimPrematureActionRail(_TimPromptRail):
},
)
# REVPREC is not a general factuality rail. Ordinary descriptions and
# explanations contain no claim that an operation has already completed,
# so they are deterministically outside this rail's blocking scope.
if not self._candidate_has_operational_completion_claim(text):
return RailDecision(
code=self.code,
allowed=True,
reason='no_operational_completion_claim',
sanitized_text=text,
metadata={
'external': True,
'domain': 'TIM_CONTAS',
'mechanism': 'deterministic_claim_scope',
'terminal_action': 'retry',
},
)
# A composed/paraphrased response can still be proven by structured
# current-turn execution evidence even when it is not byte-for-byte equal
# to a workflow message. This removes nondeterministic false positives
# while keeping unsupported or contradictory claims on the semantic path.
if self._candidate_is_structurally_grounded_completion(text, context):
return RailDecision(
code=self.code,
allowed=True,
reason='current_execution_structurally_grounded',
sanitized_text=text,
metadata={
'external': True,
'domain': 'TIM_CONTAS',
'mechanism': 'deterministic_structured_execution_evidence',
'terminal_action': 'retry',
},
)
evidence = self._current_execution_evidence(context)
llm = _llm(context)
if llm is None: