ajuste no contas

This commit is contained in:
T3782834
2026-09-01 11:24:03 -03:00
parent 9ed4782f9d
commit 397b831fd3
428 changed files with 2294 additions and 4648 deletions

View File

@@ -19,6 +19,68 @@ def _context_text(context: dict[str, Any]) -> str:
except Exception: return str(context or {})[:16000]
def _compact_aoferta_context(context: dict[str, Any]) -> str:
"""High-signal context for the proactive-offer LLM.
The generic output context can contain very large MCP/workflow payloads. Sending
the first N characters of that payload may truncate the customer request and the
resolved transaction target, which makes the auditor treat legitimate fallback
guidance as a new unsolicited action. Keep only authoritative facts needed by
AOFERTA and cap nested operational results.
"""
ctx = context or {}
history = []
for item in list(ctx.get('conversation_history') or [])[-8:]:
if isinstance(item, dict):
role = str(item.get('role') or item.get('type') or '')
content = str(item.get('content') or '')
if content:
history.append({'role': role, 'content': content[:1200]})
pre = ctx.get('transaction_pre_validation') if isinstance(ctx.get('transaction_pre_validation'), dict) else {}
requested = pre.get('requested_arguments') if isinstance(pre.get('requested_arguments'), dict) else {}
resolved = pre.get('resolved_arguments') if isinstance(pre.get('resolved_arguments'), dict) else {}
def compact_result(value: Any, depth: int = 0) -> Any:
if depth > 6:
return None
if isinstance(value, list):
return [compact_result(v, depth + 1) for v in value[:8]]
if not isinstance(value, dict):
return value if isinstance(value, (str, int, float, bool)) or value is None else str(value)[:500]
keep = {
'tool_name', 'ok', 'error', 'status', 'transaction_status', 'workflow_status',
'subject', 'name', 'resolved_subject', 'resolved_subjects', 'success', 'reason',
'mensagem', 'message', 'terminal_status', 'effective_tool_name',
'requested_arguments', 'resolved_arguments', 'items', 'results', 'errors',
'cancelados', 'nao_cancelados', 'nao_encontrados', 'output', 'result',
}
out = {}
for k, v in value.items():
if k in keep:
cv = compact_result(v, depth + 1)
if cv not in (None, {}, []):
out[k] = cv
return out
payload = {
'current_user_message': str(ctx.get('current_user_message') or '')[:1200],
'current_intent': str(ctx.get('current_intent') or ctx.get('intent') or ''),
'current_route': str(ctx.get('current_route') or ctx.get('route') or ''),
'transaction_status': str(ctx.get('transaction_status') or ''),
'conversation_history': history,
'transaction_request': {
'requested_arguments': compact_result(requested),
'resolved_arguments': compact_result(resolved),
'effective_tool_name': pre.get('effective_tool_name'),
'confirmation_message': pre.get('confirmation_message'),
},
'current_execution': compact_result(ctx.get('mcp_results') or ctx.get('tool_result') or []),
}
return json.dumps(payload, ensure_ascii=False, default=str)[:16000]
def _authorized_human_handoff(context: dict[str, Any]) -> bool:
@@ -209,71 +271,211 @@ class TimProactiveOfferRail(_TimPromptRail):
},
},
)
return await super().evaluate(text, context)
# Terminal/partial-success responses still need semantic auditing, but with
# compact authoritative transaction evidence. Do not use a completed-state
# bypass: the LLM must still block genuinely new targets/actions.
llm = _llm(context)
if llm is None:
return RailDecision(code=self.code, allowed=False, reason='LLM do framework indisponível para guardrail TIM', metadata={'external': True, 'fail_closed': True})
compact_context = _compact_aoferta_context(context)
prompt = self.prompt_builder(text or '', compact_context)
raw = await llm.ainvoke(
[{'role':'system','content':'Responda apenas JSON válido, sem markdown.'}, {'role':'user','content':prompt}],
profile_name=self.profile_name,
component_name=f'guardrail.external.{self.code.lower()}',
generation_name=f'guardrail.external.{self.code.lower()}',
)
out = _parse_json(raw)
return RailDecision(
code=self.code,
allowed=bool(out.get('allowed', False)),
reason=str(out.get('reason') or out.get('label') or ''),
sanitized_text=text,
metadata={
'external': True,
'domain':'TIM_CONTAS',
'mechanism': 'llm_semantic_compact_transaction_context',
'data':out,
},
)
class TimPrematureActionRail(_TimPromptRail):
code='TIM_REVPREC'; stage='output'; profile_name='grl'; prompt_builder=staticmethod(build_revprec_prompt)
@staticmethod
def _structured_insufficient_evidence_message(text: str, context: dict[str, Any]) -> bool:
"""Allow an epistemically conservative tool/workflow answer without LLM re-judgment.
_MESSAGE_KEYS = {
'mensagem', 'message', 'response', 'response_text', 'final_answer',
'customer_message', 'customer_response', 'answer', 'text',
}
The bypass is intentionally structural and exact-message based. An arbitrary
assistant sentence saying "não sei" is *not* enough: the current-turn tool
result must explicitly declare ``epistemic_status=insufficient_evidence`` and
expose the same ``mensagem`` that is being sent to the customer.
@classmethod
def _current_execution_evidence(cls, context: dict[str, Any]) -> list[dict[str, Any]]:
"""Return compact, current-turn execution evidence only.
REVPREC must judge whether an operational claim is supported by what actually
happened in this turn. Conversation history, LTM and previous workflow state
are deliberately excluded from this evidence set.
"""
expected = " ".join(str(text or "").split())
if not expected:
return False
roots = []
ctx = context or {}
for key in ('mcp_results', 'tool_result', 'evidence'):
value = ctx.get(key)
if value is not None:
roots.append(value)
roots = ctx.get('mcp_results')
if roots is None:
roots = ctx.get('tool_result')
if roots is None:
return []
if isinstance(roots, dict):
roots = [roots]
if not isinstance(roots, (list, tuple)):
return []
def walk(value: Any) -> bool:
if isinstance(value, dict):
status = str(value.get('epistemic_status') or '').strip().lower()
message = " ".join(str(value.get('mensagem') or '').split())
if status == 'insufficient_evidence' and message and message == expected:
return True
return any(walk(item) for item in value.values())
def compact(value: Any, depth: int = 0) -> Any:
if depth > 7:
return None
if isinstance(value, (str, int, float, bool)) or value is None:
if isinstance(value, str):
return value[:2000]
return value
if isinstance(value, (list, tuple)):
return any(walk(item) for item in value)
return False
out = [compact(v, depth + 1) for v in value[:20]]
return [v for v in out if v not in (None, {}, [])]
if not isinstance(value, dict):
return str(value)[:500]
keep = {
'tool_name', 'server_name', 'ok', 'error', 'status', 'workflow_name',
'execution_id', 'success', 'allowed', 'reason', 'subject', 'name',
'resolved_subject', 'resolved_subjects', 'value', 'amount', 'protocol',
'protocolo', 'mensagem', 'message', 'response', 'response_text',
'final_answer', 'customer_message', 'customer_response', 'answer', 'text',
'epistemic_status', 'output', 'result', 'items', 'results', 'errors',
'cancelados', 'nao_cancelados', 'nao_encontrados', 'terminal_status',
}
out = {}
for k, v in value.items():
if k in keep:
cv = compact(v, depth + 1)
if cv not in (None, {}, []):
out[k] = cv
return out
return any(walk(root) for root in roots)
evidence = []
for item in roots:
if not isinstance(item, dict):
continue
# Only successful/current tool executions can prove that an action happened.
# Failed results are still included so a contradictory success claim can be blocked.
cv = compact(item)
if isinstance(cv, dict) and cv:
evidence.append(cv)
return evidence
@classmethod
def _authoritative_output_messages(cls, context: dict[str, Any]) -> list[str]:
"""Extract canonical customer-facing messages from current successful tool outputs.
This is generic: no tool/workflow names are known here. Only message-like fields
underneath the current result/output tree are considered; state/input/history are
intentionally ignored.
"""
ctx = context or {}
roots = ctx.get('mcp_results')
if roots is None:
roots = ctx.get('tool_result')
if isinstance(roots, dict):
roots = [roots]
if not isinstance(roots, (list, tuple)):
return []
found: list[str] = []
def walk(value: Any, *, in_output: bool = False) -> None:
if isinstance(value, dict):
for k, v in value.items():
key = str(k).lower()
child_output = in_output or key in {'output', 'result', 'results'}
if child_output and key in cls._MESSAGE_KEYS and isinstance(v, str) and v.strip():
found.append(' '.join(v.split()))
if key not in {'state', 'input', 'metadata', 'conversation_history', 'history'}:
walk(v, in_output=child_output)
elif isinstance(value, (list, tuple)):
for v in value:
walk(v, in_output=in_output)
for item in roots:
if not isinstance(item, dict):
continue
if item.get('ok') is False:
continue
walk(item.get('result', item), in_output=True)
return list(dict.fromkeys(found))
@classmethod
def _candidate_is_authoritative_output(cls, text: str, context: dict[str, Any]) -> bool:
candidate = ' '.join(str(text or '').split())
if not candidate:
return False
return candidate in cls._authoritative_output_messages(context)
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
if self._structured_insufficient_evidence_message(text, context):
# Strongest possible proof: the exact customer-facing candidate was emitted by
# the successful current-turn tool/workflow itself. REVPREC is about premature
# operational claims, so re-asking an LLM whether this exact authoritative result
# "really happened" only adds nondeterminism. Other safety rails still run.
if self._candidate_is_authoritative_output(text, context):
return RailDecision(
code=self.code,
allowed=True,
reason='insufficient_evidence_non_assertive',
reason='current_execution_authoritative_output',
sanitized_text=text,
metadata={
'external': True,
'domain': 'TIM_CONTAS',
'mechanism': 'deterministic_epistemic_bypass',
'epistemic_status': 'insufficient_evidence',
'mechanism': 'deterministic_current_execution_evidence',
'terminal_action': 'retry',
},
)
evidence = self._current_execution_evidence(context)
llm = _llm(context)
if llm is None:
return RailDecision(code=self.code, allowed=False, reason='LLM do framework indisponível para guardrail TIM', metadata={'external': True, 'fail_closed': True})
prompt = self.prompt_builder(text or '', _context_text(context))
return RailDecision(
code=self.code,
allowed=False,
reason='LLM do framework indisponível para guardrail TIM',
metadata={'external': True, 'fail_closed': True},
)
# For composed/paraphrased answers, ask the semantic rail the correct question:
# whether the operational claim is unsupported or contradicted by current-turn
# evidence. This replaces the obsolete assumption that reaching REVPREC means
# no tool ran.
evidence_text = json.dumps(evidence, ensure_ascii=False, default=str)[:14000]
prompt = self.prompt_builder(text or '', evidence_text)
raw = await llm.ainvoke(
[{'role':'system','content':'Responda apenas com o dígito 1 ou 0, sem texto adicional.'}, {'role':'user','content':prompt}],
profile_name=self.profile_name, component_name='guardrail.external.tim_revprec', generation_name='guardrail.external.tim_revprec')
[
{'role':'system','content':'Responda apenas com o dígito 1 ou 0, sem texto adicional.'},
{'role':'user','content':prompt},
],
profile_name=self.profile_name,
component_name='guardrail.external.tim_revprec',
generation_name='guardrail.external.tim_revprec',
)
output = str(getattr(raw, 'content', raw) or '').strip()
digits = [ch for ch in output if ch in '01']
# Prompt original: 1 = violação / verbalização prematura; 0 = permitido.
# 1 = unsupported/contradicted operational completion claim; 0 = allowed.
allowed = not digits or digits[-1] != '1'
return RailDecision(code=self.code, allowed=allowed, reason='' if allowed else 'verbalização prematura segundo política TIM Contas', sanitized_text=text, metadata={'external':True,'domain':'TIM_CONTAS','raw':output[:100], 'terminal_action':'retry'})
return RailDecision(
code=self.code,
allowed=allowed,
reason='' if allowed else 'resultado operacional afirmado sem suporte na evidência atual',
sanitized_text=text,
metadata={
'external': True,
'domain':'TIM_CONTAS',
'raw':output[:100],
'terminal_action':'retry',
'mechanism':'llm_current_execution_evidence',
'current_execution_evidence_count': len(evidence),
},
)
class TimPhraseologyRail(_TimPromptRail):
code='TIM_FRASEOLOGIA'; stage='output'; profile_name='grl'; prompt_builder=staticmethod(build_fraseologia_prompt)

View File

@@ -38,4 +38,13 @@ class TimGroundednessJudge(_TimJudge):
class TimResponseQualityJudge(_TimJudge):
name='tim_response_quality'
async def evaluate(self, question, answer, context):
return self._result(await self._invoke(build_rqlt_prompt(str(question or ''), str(answer or ''))))
out = await self._invoke(build_rqlt_prompt(str(question or ''), str(answer or '')))
# RQLT contract is explicitly 0..10. The base normalizer cannot infer
# that score=1 means 1/10 (it also supports judges that already emit
# 0..1), so normalize this judge at its boundary.
try:
out = dict(out or {})
out['score'] = float(out.get('score', 0)) / 10.0
except Exception:
out = {**dict(out or {}), 'score': 0.0}
return self._result(out)

View File

@@ -52,6 +52,19 @@ Decida na ordem, PARE no primeiro match:
mesmo alvo, a fala do agente que apenas pede CONFIRMACAO da transacao e
allowed=true. A confirmacao NAO precisa repetir a justificativa do cliente
("nao reconheco", "esta caro" etc.); o pedido transacional anterior basta.
CONTINUIDADE POR CANAL ALTERNATIVO: se a acao transacional foi explicitamente
pedida pelo cliente para o mesmo alvo e a execucao por este canal falhou, ficou
indisponivel ou nao pode ser concluida, orientar o cliente sobre COMO concluir
ESSA MESMA acao em outro canal oficial e allowed=true. Isso nao cria uma nova
oferta: apenas informa o caminho operacional para cumprir o pedido ja existente.
Bloqueie somente se a orientacao introduzir outra acao, outro alvo ou ampliar o
escopo alem do que o cliente pediu.
EVIDENCIA ESTRUTURADA: quando o contexto operacional trouxer alvo solicitado/
resolvido e resultado da execucao (sucesso, falha parcial, item nao encontrado),
trate esses campos como evidencia autoritativa do pedido atual. Se a resposta
orientar outro canal para o MESMO alvo cuja execucao falhou, allowed=true, mesmo
que a ultima mensagem do cliente seja apenas uma confirmacao como "sim".
Vale tambem trocar uma variante transacional por outra DA MESMA FAMILIA sobre
o MESMO escopo, sempre limitada ao valor JA COBRADO no item (ressarcimento <->
devolucao <-> reembolso <-> cancelamento <-> credito em fatura): negar o dobro e

View File

@@ -1,95 +1,54 @@
"""Prompt do rail REVPREC — "o agente disse que cancelou algo?".
Reescrito em 2026-08-06. A versão anterior (207 linhas, algoritmo de 9 passos, saída
`{allowed,label,reason,score}`) julgava PROMESSA FUTURA sem autorização e, por
construção, deixava passar exatamente o caso que interessa: o passo 2 dela dava OK a
"resultado no PASSADO ou PRESENTE". Foi descartada inteira.
O rail agora responde UMA pergunta binária: a última fala do agente afirma que um
cancelamento / retirada de valor / contestação já aconteceu?
Por que isso funciona sem falso positivo na ação legítima: o rail só roda quando o
ORQUESTRADOR responde em TEXTO. Quando a ação acontece de verdade, ela vem de uma tool
call — e `apply_output_rails` sai antes dos rails LLM quando há `tool_calls` no turno
(pipeline.py, invariante do early-exit), assim como a fala canônica do
`ResponseComposer` entra com `skip_rails=True`. Ou seja: se esta pergunta chega ao LLM,
o agente está afirmando uma ação que ele NÃO tem tool para executar.
Saída BINÁRIA com polaridade INVERTIDA em relação a PINJ/COER: aqui `1` = achou a
afirmação = bloqueia; `0` = fala limpa. A pergunta fica na forma positiva ("disse que
cancelou?") porque é ela que dá acurácia; a inversão mora no `llm_client`
(`_BINARY_BLOCK_DIGIT`).
Duas fronteiras que o prompt precisa manter, e que os casos da suíte travam:
- **histórico**: um cancelamento feito do jeito certo em turno anterior FICA no
histórico e não foi avaliado por este rail; só o bloco "Resposta:" é julgado;
- **protocolo**: o número de protocolo só é emitido depois da tool rodar, então uma
fala que o traz relata execução real → 0.
"""
"""REVPREC: detecta verbalização de resultado operacional sem evidência atual."""
from __future__ import annotations
def build_revprec_prompt(text: str, context: str = "") -> str:
"""Monta o prompt do rail REVPREC.
"""Compara a fala candidata com a evidência estrutural do turno atual.
Args:
text: última fala gerada pelo agente (a que será julgada).
context: histórico já formatado por ``prompts._context.format_context_block``
(a última fala do agente é PODADA de lá — ela vem em ``text``).
Returns:
Prompt cuja resposta esperada é um único caractere: ``1`` ou ``0``.
Polaridade: 1 = a fala afirma conclusão/efeito operacional não comprovado ou
contradito pela evidência; 0 = permitido.
"""
return f"""Você audita UMA fala do agente de fatura da TIM e responde 1 ou 0.
return f"""Você audita UMA fala de um agente de atendimento TIM.
O agente NÃO executa cancelamento, contestação nem retirada de valor. Quando a ação
acontece de verdade, é o sistema que responde ao cliente — nunca esta fala. Logo,
afirmar aqui que já cancelou/retirou/contestou é sempre falso.
Sua tarefa NÃO é decidir se a frase "soa" como ação concluída. Sua tarefa é comparar
as afirmações da RESPOSTA com a EVIDÊNCIA REAL DO TURNO ATUAL.
ANTES DE TUDO: se a fala traz NÚMERO DE PROTOCOLO — um ou vários, no formato "PRT..."
bruto, vocalizado ("pê erre tê nove cê dois...") ou dígitos depois da palavra protocolo —
responda 0 e pare. O protocolo só é emitido DEPOIS de a ação rodar de verdade; com ele a
fala relata execução real, inclusive quando RECAPITULA cancelamentos já feitos, por mais
concluída que soe.
Responda 1 SOMENTE quando a resposta afirmar que uma ação/efeito operacional já foi
concluído (por exemplo cancelamento, contestação, retirada de valor, crédito, reembolso,
envio ou alteração) e essa conclusão NÃO estiver suportada pela evidência atual, ou
estiver contradita por ela.
Responda 1 quando a fala AFIRMA que um cancelamento, retirada de valor, contestação ou
reembolso JÁ ACONTECEU. Ex.: "cancelamento confirmado", "já foi cancelado", "cancelado
com sucesso", "cancelei o serviço", "retiramos o valor", "sua contestação foi
registrada", "o reembolso já foi processado", "a cobrança não vai mais aparecer porque
removi o serviço".
Responda 0 quando:
- a evidência atual comprova a ação/resultado afirmado;
- a resposta apenas explica dados de fatura, cobrança, desconto, plano, valor, data,
status ou motivo presentes na evidência;
- a resposta pede confirmação/permissão ou solicita parâmetro;
- a resposta anuncia uma ação futura sem afirmar que já ocorreu;
- a resposta nega que a ação ocorreu;
- a resposta expressa ausência de evidência ou incerteza;
- não existe afirmação de conclusão operacional.
Responda 0 em todo o resto. Em particular:
- PERGUNTA ou PEDIDO DE PERMISSÃO, mesmo citando cancelamento: "Podemos seguir com o
cancelamento do serviço X, no valor de R$ 12,00, você confirma?", "Gostaria de
cancelar esse serviço?", "Posso seguir?".
- ESCOPO ou intenção reconhecida, sem ação feita: "Entendi que você deseja falar sobre
os serviços X e Y. Correto?", "Vou verificar isso para você".
- PROMESSA sem afirmar execução: "vou cancelar", "seguiremos com o cancelamento, aguarde
um instante", "o valor será retirado da sua fatura". Aqui a pergunta é se a ação foi
DADA COMO FEITA; anúncio do que vem depois não é.
- DESCRIÇÃO DA FATURA, não ação do agente: "Foi removido um desconto de R$ 6,00", "foi
adicionada a cobrança do X", "esse serviço foi cobrado em duas datas" — isso compara
faturas e explica cobranças; não cancela nada.
- ORIENTAÇÃO a outro canal: "ligue para *144 e solicite o cancelamento", "pelo app do
parceiro você consegue cancelar".
- NEGATIVA de ação: "não consigo cancelar por aqui", "ainda não cancelei", "esse serviço
não pode ser cancelado neste atendimento".
- AUSÊNCIA DE EVIDÊNCIA ou incerteza explícita: "os dados disponíveis não informam o
motivo", "não há evidência suficiente para confirmar a causa", "não foi possível
identificar o motivo". Isso NÃO afirma execução nem resultado operacional; responda 0.
- EXPLICAÇÃO, valor, data, encerramento, saudação, ou qualquer assunto que não seja
ação de cancelamento dada como feita.
REGRAS IMPORTANTES:
1. Use SOMENTE a evidência do bloco EVIDÊNCIA ATUAL para provar execução. Não use
histórico, memória ou suposições.
Julgue somente a fala do bloco "Resposta:"; qualquer texto fora desse bloco é contexto.
2. Se a evidência mostra sucesso/COMPLETED e contém os fatos afirmados, responda 0.
3. Se a evidência mostra falha/erro/não executado e a resposta afirma sucesso, responda 1.
4. Se não existe evidência de execução e a resposta afirma que uma ação transacional já
foi realizada, responda 1.
5. Descrição de algo que aconteceu na conta/fatura (ex.: desconto expirou, cobrança foi
lançada) não é "ação prematura" se isso estiver suportado pelos dados atuais.
6. PROTOCOLO é evidência auxiliar, não regra absoluta: valide junto com a evidência atual.
7. DESCRIÇÃO DA FATURA (cobrança, desconto, data, status, motivo) não é execução de ação
pelo agente quando estiver sustentada pelos dados atuais.
O HISTÓRICO é só contexto. Um cancelamento feito corretamente em turno anterior APARECE
lá e NÃO conta — ele não passou por esta auditoria e não é o que se julga agora. Julgue
somente a fala do bloco "Resposta:".
------------------------------------{context}
Resposta:
---------------- EVIDÊNCIA ATUAL ----------------
{context or '[]'}
---------------- RESPOSTA ----------------
{text}
------------------------------------
--------------------------------------------------
A fala do bloco "Resposta:" afirma que um cancelamento, retirada de valor ou
contestação já aconteceu?
A RESPOSTA contém alguma afirmação de resultado operacional concluído que NÃO esteja
suportada (ou esteja contradita) pela EVIDÊNCIA ATUAL?
Responda APENAS 1 ou 0, sem mais nada."""
Responda APENAS 1 ou 0."""