Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
Binary file not shown.
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user