nova funcionalidade: reconciliacao temporal
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user