Ajustes conforme relatorio de testes 2026-08-27

This commit is contained in:
2026-08-29 09:53:32 -03:00
parent 0ecff719b7
commit 88e1f070d7
791 changed files with 27040 additions and 29038 deletions

View File

@@ -190,7 +190,8 @@ class AgentWorkflow:
== "intent_shift"
)
stickiness_intent_shift = bool(route_metadata.get("route_stickiness_preempted"))
should_isolate_history = semantic_intent_shift or (terminal_tx and stickiness_intent_shift)
operational_context_reset = bool(state.get("operational_context_reset"))
should_isolate_history = operational_context_reset or semantic_intent_shift or (terminal_tx and stickiness_intent_shift)
current_route = str(
state.get("route")
@@ -206,6 +207,24 @@ class AgentWorkflow:
ctx["current_route"] = current_route
ctx["current_intent"] = current_intent
# Structured control evidence for domain guardrails. A rail must not
# infer an authorized transfer from prose; it receives the router/workflow
# decision explicitly for the current turn.
session_control = str(
state.get("session_control")
or route_metadata.get("session_control")
or ""
).strip().upper()
route_handoff = bool(
(route_decision.get("handoff") if isinstance(route_decision, dict) else False)
or state.get("human_handoff_requested")
or session_control == "HUMAN_HANDOFF"
)
ctx["session_control"] = session_control
ctx["human_handoff_requested"] = route_handoff
ctx["handoff"] = route_handoff
ctx["route_decision"] = route_decision
if should_isolate_history:
operational_history = (
[{"role": "user", "content": current_user_text}]
@@ -292,7 +311,9 @@ class AgentWorkflow:
builder.add_conditional_edges(
"input_guardrails",
self._after_input_guardrails,
{"blocked": "persist", "continue": "load_long_term_memory"},
# Mesmo uma resposta produzida por um bloqueio de input deve passar
# pelos guardrails de saída antes de ser entregue ao usuário.
{"blocked": "output_guardrails", "continue": "load_long_term_memory"},
)
builder.add_edge("load_long_term_memory", "routing_decision")
builder.add_conditional_edges(
@@ -318,7 +339,13 @@ class AgentWorkflow:
builder.add_edge("end_session", "output_supervisor")
builder.add_edge("supervisor_agent", "output_supervisor")
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_conditional_edges(
"output_guardrails",
lambda s: "blocked" if s.get("blocked") else "continue",
# Clarificações geradas por guardrail de entrada não devem ser
# julgadas nem gravadas em LTM como se fossem uma resposta normal.
{"blocked": "persist", "continue": "judge"},
)
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist_long_term_memory")
builder.add_edge("persist_long_term_memory", "persist")
@@ -329,6 +356,41 @@ class AgentWorkflow:
def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue"
@staticmethod
def _input_guardrail_user_message(decisions, state, sanitized_text):
"""Converte um bloqueio técnico em mensagem útil sem expor internals.
O `reason` bruto continua em `guardrail_decisions`/telemetria para
auditoria. A mensagem ao usuário é específica por classe de rail e
segue depois pelos guardrails de saída.
"""
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
first = blocked[0] if blocked else None
code = str(getattr(first, "code", "") or "").upper()
reason = str(getattr(first, "reason", "") or "").strip()
if code == "COER":
# COER representa ambiguidade/incompletude, não uma violação de
# segurança. Não ecoamos o reason bruto do modelo.
return (
"Não consegui entender sua última mensagem porque ela parece "
"incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
)
if code == "INPUT_SIZE":
return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
if code == "DLEX_IN":
return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
if code == "PINJ":
return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
if code == "TOX":
return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
if code == "CMP":
return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."
# Fallback neutro: não atribui falsamente o problema a 'segurança' e
# não expõe nomes, razões ou políticas internas dos guardrails.
return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"
async def input_guardrails(self, state):
if state.get("session_ended") is True:
answer = str(getattr(
@@ -353,7 +415,51 @@ class AgentWorkflow:
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("user_text"),
):
history_texts = [m.get("content", "") for m in state.get("history", [])]
boundary_pending = bool(state.get("operational_context_boundary_pending"))
tx_status = str(state.get("transaction_status") or "").strip().upper()
terminal_interaction = tx_status in {"COMPLETED", "FAILED", "CANCELLED", "BLOCKED", "OUT_OF_SCOPE"}
reset_operational_context = boundary_pending or terminal_interaction
# The durable history/checkpoint is preserved, but the first turn
# after a completed workflow must look operationally like a fresh
# conversation. Guardrails therefore see only the current utterance.
history_texts = (
[str(state.get("user_text") or "")]
if reset_operational_context
else [m.get("content", "") for m in state.get("history", [])]
)
if reset_operational_context:
# Tombstone every live latch that can make the next turn look
# like a continuation of the closed workflow/transaction.
state.update({
"pending_domain_workflow": None,
"pending_tool_clarification": None,
"workflow_input_reprompt": None,
"active_transaction": None,
"selected_tool_call": {},
"pending_tool_call": {},
"missing_parameters": [],
"confirmation_required": False,
"confirmation_received": False,
"transaction_pre_validation": None,
"tool_policy_result": None,
"tool_terminal_result": None,
"transaction_confirmation_message_override": None,
"next_state": None,
"mcp_tools": [],
"mcp_results": [],
"relevant_transaction_evidence": [],
"route": None,
"intent": None,
"route_decision": {},
"active_agent": None,
"route_bypassed": False,
"continuity_signal": {},
"workflow_id": None,
"transaction_status": None,
"operational_context_boundary_pending": False,
"operational_context_reset": True,
})
await self.observer.emit_grl(
"001",
{
@@ -364,6 +470,13 @@ class AgentWorkflow:
},
component="workflow.input_guardrails.start",
)
pending_workflow = None if reset_operational_context else state.get("pending_domain_workflow")
pause = (
pending_workflow.get("pause")
if isinstance(pending_workflow, dict) and isinstance(pending_workflow.get("pause"), dict)
else {}
)
expected_input = pause.get("expected_input") if isinstance(pause, dict) else None
sanitized, decisions = await self.guardrails.run_input(
state["user_text"],
{
@@ -372,6 +485,17 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {},
# Generic workflow contract context. COER delegates only
# conversational coherence to this contract; all other
# safety rails continue to execute normally.
"expected_input": expected_input,
# Active transaction parameter contracts own the semantic
# interpretation of short replies such as a product name or
# identifier. COER delegates coherence only; PINJ/DLEX/TOX
# and every other safety rail still run normally.
"transaction_status": state.get("transaction_status"),
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": state.get("active_transaction") or {},
},
)
for _decision in decisions:
@@ -413,18 +537,73 @@ class AgentWorkflow:
component="workflow.input_guardrails.final",
)
if any(not d.allowed for d in decisions):
user_message = self._input_guardrail_user_message(decisions, state, sanitized)
return {
"sanitized_input": sanitized,
"answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"answer": user_message,
# final_answer será calculado por output_guardrails.
"final_answer": None,
"guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked",
"intent": "input_guardrail_blocked",
"route_decision": {
"route": "blocked",
"agent": None,
"intent": "input_guardrail_blocked",
"confidence": 1.0,
"reason": "Entrada interrompida por guardrail antes do roteamento.",
"method": "guardrail",
"next_state": state.get("next_state"),
"handoff": False,
"metadata": {},
"domain": state.get("domain"),
"mcp_tools": [],
},
# Evita vazar resultados/rota do turno anterior quando o
# bloqueio acontece antes do roteamento do turno atual.
"mcp_tools": [],
"mcp_results": [],
"judge_results": [],
**({
"pending_domain_workflow": None,
"pending_tool_clarification": None,
"workflow_input_reprompt": None,
"active_transaction": None,
"selected_tool_call": {},
"pending_tool_call": {},
"missing_parameters": [],
"confirmation_required": False,
"confirmation_received": False,
"transaction_pre_validation": None,
"tool_policy_result": None,
"tool_terminal_result": None,
"transaction_confirmation_message_override": None,
"next_state": None,
"mcp_tools": [],
"mcp_results": [],
"relevant_transaction_evidence": [],
"route": None,
"intent": None,
"route_decision": {},
"active_agent": None,
"route_bypassed": False,
"continuity_signal": {},
"workflow_id": None,
"transaction_status": None,
"operational_context_boundary_pending": False,
"operational_context_reset": True,
} if reset_operational_context else {}),
"blocked": True,
}
return {
"sanitized_input": sanitized,
"guardrail_decisions": [d.model_dump() for d in decisions],
"blocked": False,
**({
"pending_domain_workflow": None,
"pending_tool_clarification": None,
"workflow_input_reprompt": None,
} if terminal_interaction else {}),
}
async def routing_decision(self, state):
@@ -615,6 +794,19 @@ class AgentWorkflow:
"session_ended": True,
"terminal_status": "nao_resolvido",
"next_state": "HUMAN_HANDOFF_REQUESTED",
# Human handoff terminates the operational interaction. Keep the
# durable history/checkpoint, but do not leave a paused workflow
# or transactional latch active in the live session.
"pending_domain_workflow": None,
"active_transaction": None,
"transaction_pre_validation": None,
"transaction_status": "CANCELLED",
"pending_tool_call": {},
"selected_tool_call": {},
"missing_parameters": [],
"confirmation_required": False,
"confirmation_received": False,
"mcp_results": [],
}
async def end_session(self, state):
@@ -995,6 +1187,8 @@ class AgentWorkflow:
"answer_chars": len(state.get("final_answer") or ""),
},
)
if state.get("operational_context_reset"):
state["operational_context_reset"] = False
return state
async def ainvoke(self, state):