nova funcionalidade: reconciliacao temporal
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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:
|
||||
|
||||
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