Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -19,6 +19,72 @@ def _context_text(context: dict[str, Any]) -> str:
|
||||
except Exception: return str(context or {})[:16000]
|
||||
|
||||
|
||||
|
||||
|
||||
def _authorized_human_handoff(context: dict[str, Any]) -> bool:
|
||||
"""Return True only for structurally authorized human handoff on this turn."""
|
||||
ctx = context or {}
|
||||
route = str(ctx.get('current_route') or ctx.get('route') or '').strip().lower()
|
||||
intent = str(ctx.get('current_intent') or ctx.get('intent') or '').strip().lower()
|
||||
session_control = str(ctx.get('session_control') or '').strip().upper()
|
||||
requested = ctx.get('human_handoff_requested') is True
|
||||
handoff = ctx.get('handoff') is True
|
||||
|
||||
route_decision = ctx.get('route_decision') if isinstance(ctx.get('route_decision'), dict) else {}
|
||||
route_meta = route_decision.get('metadata') if isinstance(route_decision.get('metadata'), dict) else {}
|
||||
rd_route = str(route_decision.get('route') or route_decision.get('agent') or '').strip().lower()
|
||||
rd_intent = str(route_decision.get('intent') or '').strip().lower()
|
||||
rd_handoff = route_decision.get('handoff') is True
|
||||
rd_session_control = str(route_meta.get('session_control') or '').strip().upper()
|
||||
|
||||
control_evidence = (
|
||||
session_control == 'HUMAN_HANDOFF'
|
||||
or requested
|
||||
or handoff
|
||||
or rd_handoff
|
||||
or rd_session_control == 'HUMAN_HANDOFF'
|
||||
)
|
||||
route_evidence = (
|
||||
route == 'human_handoff'
|
||||
or intent == 'human_handoff'
|
||||
or rd_route == 'human_handoff'
|
||||
or rd_intent == 'human_handoff'
|
||||
)
|
||||
if control_evidence and route_evidence:
|
||||
return True
|
||||
|
||||
# A resumed domain workflow may decide the handoff after the router has
|
||||
# already been bypassed for workflow continuation. In that case the
|
||||
# current route/intent legitimately remain the domain values, while the
|
||||
# *current-turn workflow result* is the authoritative orchestration
|
||||
# decision. Accept only a terminal, internally consistent handoff result;
|
||||
# an isolated `handoff=true` or transfer-like sentence is never enough.
|
||||
roots = []
|
||||
for key in ('mcp_results', 'tool_result', 'workflow_result'):
|
||||
value = ctx.get(key)
|
||||
if value is not None:
|
||||
roots.append(value)
|
||||
|
||||
def terminal_workflow_handoff(value: Any) -> bool:
|
||||
if isinstance(value, dict):
|
||||
workflow_control = str(value.get('session_control') or '').strip().upper()
|
||||
workflow_terminal = str(value.get('terminal_status') or '').strip().lower()
|
||||
workflow_requested = value.get('human_handoff_requested') is True
|
||||
workflow_handoff = value.get('handoff') is True
|
||||
workflow_session_ended = value.get('session_ended') is True
|
||||
|
||||
has_control = workflow_control == 'HUMAN_HANDOFF'
|
||||
has_request = workflow_requested or workflow_handoff
|
||||
has_terminal = workflow_terminal == 'human_handoff' or workflow_session_ended
|
||||
if has_control and has_request and has_terminal:
|
||||
return True
|
||||
return any(terminal_workflow_handoff(item) for item in value.values())
|
||||
if isinstance(value, (list, tuple)):
|
||||
return any(terminal_workflow_handoff(item) for item in value)
|
||||
return False
|
||||
|
||||
return any(terminal_workflow_handoff(root) for root in roots)
|
||||
|
||||
def _parse_json(raw: Any) -> dict[str, Any]:
|
||||
text = str(getattr(raw, 'content', raw) or '').strip()
|
||||
m = re.search(r'\{[\s\S]*\}', text)
|
||||
@@ -49,6 +115,27 @@ class _TimPromptRail(Guardrail):
|
||||
class TimOutOfScopeRail(_TimPromptRail):
|
||||
code='TIM_OOS'; stage='output'; prompt_builder=staticmethod(build_oos_prompt)
|
||||
|
||||
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
|
||||
# A handoff structurally selected by the router/workflow is an authorized
|
||||
# orchestration response, not a domain answer to be judged as OOS by text.
|
||||
if _authorized_human_handoff(context):
|
||||
return RailDecision(
|
||||
code=self.code,
|
||||
allowed=True,
|
||||
reason='handoff_humano_autorizado',
|
||||
sanitized_text=text,
|
||||
metadata={
|
||||
'external': True,
|
||||
'domain': 'TIM_CONTAS',
|
||||
'mechanism': 'deterministic_handoff_bypass',
|
||||
'data': {
|
||||
'allowed': True,
|
||||
'reason': 'handoff_humano_autorizado',
|
||||
},
|
||||
},
|
||||
)
|
||||
return await super().evaluate(text, context)
|
||||
|
||||
class TimProactiveOfferRail(_TimPromptRail):
|
||||
code='TIM_AOFERTA'; stage='output'; prompt_builder=staticmethod(build_aoferta_prompt)
|
||||
|
||||
@@ -61,6 +148,10 @@ class TimProactiveOfferRail(_TimPromptRail):
|
||||
'AWAITING_CONFIRMATION',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _authorized_human_handoff(context: dict[str, Any]) -> bool:
|
||||
return _authorized_human_handoff(context)
|
||||
|
||||
@classmethod
|
||||
def _transaction_continuation_status(cls, context: dict[str, Any]) -> str | None:
|
||||
ctx = context or {}
|
||||
@@ -83,6 +174,23 @@ class TimProactiveOfferRail(_TimPromptRail):
|
||||
return None
|
||||
|
||||
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
|
||||
if self._authorized_human_handoff(context):
|
||||
return RailDecision(
|
||||
code=self.code,
|
||||
allowed=True,
|
||||
reason='handoff_humano_autorizado',
|
||||
sanitized_text=text,
|
||||
metadata={
|
||||
'external': True,
|
||||
'domain': 'TIM_CONTAS',
|
||||
'mechanism': 'deterministic_handoff_bypass',
|
||||
'data': {
|
||||
'allowed': True,
|
||||
'reason': 'handoff_humano_autorizado',
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
continuation_status = self._transaction_continuation_status(context)
|
||||
if continuation_status:
|
||||
return RailDecision(
|
||||
@@ -106,7 +214,54 @@ class TimProactiveOfferRail(_TimPromptRail):
|
||||
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.
|
||||
|
||||
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.
|
||||
"""
|
||||
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)
|
||||
|
||||
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())
|
||||
if isinstance(value, (list, tuple)):
|
||||
return any(walk(item) for item in value)
|
||||
return False
|
||||
|
||||
return any(walk(root) for root in roots)
|
||||
|
||||
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
|
||||
if self._structured_insufficient_evidence_message(text, context):
|
||||
return RailDecision(
|
||||
code=self.code,
|
||||
allowed=True,
|
||||
reason='insufficient_evidence_non_assertive',
|
||||
sanitized_text=text,
|
||||
metadata={
|
||||
'external': True,
|
||||
'domain': 'TIM_CONTAS',
|
||||
'mechanism': 'deterministic_epistemic_bypass',
|
||||
'epistemic_status': 'insufficient_evidence',
|
||||
},
|
||||
)
|
||||
|
||||
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})
|
||||
|
||||
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.
@@ -74,6 +74,9 @@ Responda 0 em todo o resto. Em particular:
|
||||
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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user