Compare commits

...

3 Commits

44 changed files with 829 additions and 361 deletions

View File

@@ -369,6 +369,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -662,8 +692,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -363,6 +363,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -662,8 +692,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -358,6 +358,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -660,8 +690,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -363,6 +363,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -662,8 +692,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -358,6 +358,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -660,8 +690,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -358,6 +358,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -660,8 +690,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -358,6 +358,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -660,8 +690,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -358,6 +358,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -36,6 +36,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -246,6 +246,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -264,6 +286,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -660,8 +690,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -457,6 +457,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -39,6 +39,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -662,8 +692,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -457,6 +457,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -40,6 +40,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -248,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -266,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -662,8 +692,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -1,364 +1,192 @@
const chat=document.getElementById('chat'); const chat = document.getElementById("chat");
const form=document.getElementById('form'); const form = document.getElementById("form");
let eventSource=null; let eventSource = null;
let currentSessionId = null; let currentSessionId = null;
const renderedResponses = new Set();
function add(role,text){ function status(text) { const el = document.getElementById("status"); if (el) el.textContent = text; }
const d=document.createElement('div'); function val(id) { return (document.getElementById(id)?.value || "").trim(); }
d.className='msg '+role; function uuid() { return crypto.randomUUID(); }
d.textContent=text; function humanizeAgentName(value) {
chat.appendChild(d); return String(value || "agent").trim().replace(/([a-zà-ÿ])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
chat.scrollTop=chat.scrollHeight;
} }
function status(text){const el=document.getElementById('status'); if(el) el.textContent=text;} function responseText(value) {
function val(id){return (document.getElementById(id)?.value || '').trim();} if (typeof value === "string") return value;
function uuid(){return crypto.randomUUID();} if (value == null) return "";
return JSON.stringify(value, null, 2);
function buildBusinessContext(session, messageId){
return {
customer_key: val('customerKey') || null,
contract_key: val('contractKey') || null,
interaction_key: val('interactionKey') || messageId,
account_key: val('accountKey') || null,
resource_key: val('resourceKey') || null,
session_key: session || null,
metadata: {frontend: 'agent_frontend', version: 'business-context-v2'}
};
} }
function splitTaggedAgentMessages(value) {
function syncDomainAliases(payload, businessContext){ const text = responseText(value).trim();
const agent=val('agent'); if (!text) return [];
if(agent === 'retail_orders'){ const marker = /^\[([^\]\r\n]*Agent)\]\s*/gim;
payload.customer_id = businessContext.customer_key; const matches = [...text.matchAll(marker)];
payload.order_id = businessContext.contract_key; if (!matches.length || matches[0].index !== 0) return [];
} else { return matches.map((match, index) => {
payload.msisdn = businessContext.customer_key; const start = match.index + match[0].length;
payload.invoice_id = businessContext.contract_key; const end = index + 1 < matches.length ? matches[index + 1].index : text.length;
payload.ura_call_id = businessContext.interaction_key; return { agent: match[1].trim(), text: text.slice(start, end).trim() };
payload.asset_id = businessContext.resource_key; }).filter((item) => item.agent && item.text);
}
} }
function stripOwnAgentMarker(value, agent) {
function adicionarMensagem(role, text) { const text = responseText(value).trim();
const chat = const marker = text.match(/^\[([^\]\r\n]*Agent)\]\s*/i);
document.getElementById("chat") || if (!marker) return text;
document.getElementById("messages") || const normalize = (name) => String(name || "").replace(/[_\s-]+/g, "").toLowerCase();
document.querySelector(".chat") || return normalize(marker[1]) === normalize(agent) ? text.slice(marker[0].length).trim() : text;
document.querySelector(".messages") || }
document.querySelector("[data-chat]"); function orderAgentMessages(agentResponses) {
return [...agentResponses].sort(
if (!chat) { (a, b) => Number(Boolean(a.primary)) - Number(Boolean(b.primary))
console.error("Não encontrei o container do chat no HTML."); );
console.log("Mensagem que seria exibida:", role, text); }
return; function sameAgent(left, right) {
} const normalize = (name) => String(name || "").replace(/[_\s-]+/g, "").toLowerCase();
return Boolean(normalize(left)) && normalize(left) === normalize(right);
}
function addUserMessage(text) {
const div = document.createElement("div"); const div = document.createElement("div");
div.className = "msg user chat-bubble--user";
if (role === "user") { div.textContent = text;
div.className = "msg user chat-bubble--user";
} else {
div.className = "msg assistant chat-bubble--agent";
}
div.textContent = text || "";
chat.appendChild(div); chat.appendChild(div);
chat.scrollTop = chat.scrollHeight; chat.scrollTop = chat.scrollHeight;
} }
function addAgentMessage(agentId, text, group) {
function abrirSSE(sessionId) { const article = document.createElement("article");
if (!sessionId) { article.className = "agent-message";
console.error("Não vou abrir SSE sem sessionId."); article.dataset.agentId = agentId || "agent";
return; const header = document.createElement("header");
header.className = "agent-message__header";
const avatar = document.createElement("span");
avatar.className = "agent-message__avatar";
avatar.textContent = humanizeAgentName(agentId).charAt(0) || "A";
avatar.setAttribute("aria-hidden", "true");
const name = document.createElement("span");
name.className = "agent-message__name";
name.textContent = humanizeAgentName(agentId);
const body = document.createElement("div");
body.className = "agent-message__body";
body.textContent = responseText(text);
header.append(avatar, name);
article.append(header, body);
group.appendChild(article);
}
function getAgentMessages(data) {
const metadata = data?.metadata || {};
const candidates = data?.supervisor_results || metadata.supervisor_results || data?.agent_responses || metadata.agent_responses || (Array.isArray(data?.messages) ? data.messages : null);
if (Array.isArray(candidates)) {
const messages = candidates.map((item) => ({
agent: item?.agent || item?.agent_id || item?.agentId || item?.name,
text: stripOwnAgentMarker(item?.answer ?? item?.text ?? item?.message ?? item?.response ?? item?.content, item?.agent || item?.agent_id || item?.agentId || item?.name),
primary: item?.primary === true,
})).filter((item) => item.agent && responseText(item.text).trim());
if (messages.length) return orderAgentMessages(messages);
} }
const text = data?.text ?? data?.speak ?? data?.message ?? data?.response ?? data?.content ?? data?.output;
if (eventSource) { const taggedMessages = splitTaggedAgentMessages(text);
eventSource.close(); if (taggedMessages.length) {
eventSource = null; const primaryAgent = metadata.active_agent || metadata.route || data?.route || metadata.route_decision?.agent || metadata.route_decision?.route;
return orderAgentMessages(taggedMessages.map((item) => ({
...item,
primary: sameAgent(item.agent, primaryAgent),
})));
} }
const agent = metadata.active_agent || metadata.route || data?.agent_id || metadata.agent_id || val("agent") || "agent";
const url = `${backend}/gateway/events/${sessionId}`; return responseText(text).trim() ? [{ agent, text }] : [];
}
eventSource = new EventSource(url); function responseKey(data) {
const stableId = data?.metadata?.message_id || data?.message_id;
eventSource.onopen = () => { return stableId ? `id:${stableId}` : JSON.stringify(getAgentMessages(data));
console.log("SSE OPEN"); }
function renderAgentResponse(data) {
const normalized = typeof data === "string" ? { text: data } : (data || {});
const messages = getAgentMessages(normalized);
if (!messages.length) return false;
const key = responseKey(normalized);
if (renderedResponses.has(key)) return false;
renderedResponses.add(key);
const group = document.createElement("section");
group.className = "agent-response-group";
group.setAttribute("aria-label", "Respostas dos agentes");
messages.forEach((message) => addAgentMessage(message.agent, message.text, group));
chat.appendChild(group);
chat.scrollTop = chat.scrollHeight;
return true;
}
function buildBusinessContext(session, messageId) {
return {
customer_key: val("customerKey") || null, contract_key: val("contractKey") || null,
interaction_key: val("interactionKey") || messageId, account_key: val("accountKey") || null,
resource_key: val("resourceKey") || null, session_key: session || null,
metadata: { frontend: "agent_frontend", version: "business-context-v2" },
}; };
}
eventSource.onerror = (err) => { function syncDomainAliases(payload, businessContext) {
console.error("SSE ERROR:", err); if (val("agent") === "retail_orders") {
}; payload.customer_id = businessContext.customer_key; payload.order_id = businessContext.contract_key;
} else {
const eventos = [ payload.msisdn = businessContext.customer_key; payload.invoice_id = businessContext.contract_key;
"connected", payload.ura_call_id = businessContext.interaction_key; payload.asset_id = businessContext.resource_key;
"waiting",
"backend.selected",
"flow.start",
"workflow.started",
"message.responded",
"workflow.completed",
"flow.end",
"error"
];
for (const nome of eventos) {
eventSource.addEventListener(nome, (event) => {
if (nome === "message.responded") {
try {
const data = JSON.parse(event.data);
const text =
data.text ||
data.message ||
data.response ||
data.content ||
data.output ||
event.data;
adicionarMensagem("assistant", text);
} catch {
adicionarMensagem("assistant", event.data);
}
}
if (nome === "error") {
adicionarMensagem("assistant", `Erro SSE: ${event.data}`);
}
});
} }
} }
function normalizeSessionId(value) { function normalizeSessionId(value) {
if (!value) return uuid(); if (!value) return uuid();
const parts = value.split(":"); return parts[parts.length - 1];
const parts = value.split(":");
return parts[parts.length - 1]; // mantém só o UUID final
} }
function connectSSE(backend, sessionId) { function connectSSE(backend, sessionId) {
if (!sessionId) { if (!backend || !sessionId) return;
console.warn("SSE não aberto: sessionId ausente."); if (eventSource) eventSource.close();
return;
}
if (!backend) {
console.warn("SSE não aberto: backend ausente.");
return;
}
if (eventSource) {
console.log("Fechando SSE anterior:", eventSource.url);
eventSource.close();
eventSource = null;
}
const url = `${backend.replace(/\/$/, "")}/gateway/events/${encodeURIComponent(sessionId)}`; const url = `${backend.replace(/\/$/, "")}/gateway/events/${encodeURIComponent(sessionId)}`;
console.log("Abrindo SSE:", url);
eventSource = new EventSource(url); eventSource = new EventSource(url);
eventSource._sessionId = sessionId; eventSource.onopen = () => status("SSE conectado");
eventSource.onerror = () => {
eventSource.onopen = () => { if (eventSource?.readyState === EventSource.CONNECTING) status("SSE aguardando/reconectando");
console.log("SSE OPEN:", url); else if (eventSource?.readyState === EventSource.CLOSED) status("SSE fechado");
status("SSE conectado"); else status("SSE com erro");
}; };
const statuses = { waiting: "SSE aguardando backend", "backend.selected": "Backend selecionado", "flow.start": "Fluxo iniciado", "workflow.started": "Workflow em execução", "workflow.completed": "Workflow concluído", "flow.end": "Fluxo finalizado" };
eventSource.onerror = (err) => { Object.entries(statuses).forEach(([eventName, label]) => eventSource.addEventListener(eventName, () => status(label)));
console.error("SSE ERROR raw:", err);
console.error("SSE readyState:", eventSource?.readyState);
console.error("SSE url:", eventSource?.url);
if (eventSource?.readyState === EventSource.CONNECTING) {
status("SSE aguardando/reconectando");
return;
}
if (eventSource?.readyState === EventSource.CLOSED) {
status("SSE fechado");
return;
}
status("SSE com erro");
};
eventSource.addEventListener("connected", (event) => {
console.log("SSE connected:", event.data);
status("SSE conectado");
});
eventSource.addEventListener("waiting", (event) => {
console.log("SSE waiting:", event.data);
status("SSE aguardando backend");
});
eventSource.addEventListener("backend.selected", (event) => {
console.log("SSE backend.selected:", event.data);
status("Backend selecionado");
});
eventSource.addEventListener("flow.start", (event) => {
console.log("SSE flow.start:", event.data);
status("Fluxo iniciado");
});
eventSource.addEventListener("workflow.started", (event) => {
console.log("SSE workflow.started:", event.data);
status("Workflow em execução");
});
eventSource.addEventListener("session.upserted", (event) => {
try {
const data = JSON.parse(event.data);
if (data.business_context) {
console.debug("business_context", data.business_context);
}
} catch (e) {
console.warn("Não consegui interpretar session.upserted:", event.data);
}
});
eventSource.addEventListener("message.responded", (event) => { eventSource.addEventListener("message.responded", (event) => {
try { try { renderAgentResponse(JSON.parse(event.data)); } catch { renderAgentResponse(event.data); }
const data = JSON.parse(event.data); status("Resposta recebida");
const text =
data.text ||
data.message ||
data.response ||
data.content ||
data.output ||
event.data;
if (text) {
add("assistant", text);
}
if (data.metadata?.business_context) {
console.debug("metadata.business_context", data.metadata.business_context);
}
status("Resposta recebida");
} catch (e) {
add("assistant", event.data);
status("Resposta recebida");
}
}); });
eventSource.addEventListener("workflow.completed", (event) => {
console.log("SSE workflow.completed:", event.data);
status("Workflow concluído");
});
eventSource.addEventListener("flow.end", (event) => {
console.log("SSE flow.end:", event.data);
status("Fluxo finalizado");
});
// Use um nome diferente de "error" para erro enviado pelo servidor.
// "error" é reservado/conflituoso com erro nativo do EventSource.
eventSource.addEventListener("server.error", (event) => { eventSource.addEventListener("server.error", (event) => {
console.error("SSE server.error:", event.data); renderAgentResponse({ text: `Erro SSE: ${event.data || "erro informado pelo servidor"}`, agent_id: "sistema" });
add("assistant", `Erro SSE: ${event.data || "erro informado pelo servidor"}`);
status("Erro no fluxo SSE"); status("Erro no fluxo SSE");
}); });
} }
form.addEventListener('submit', async (e) => { form.addEventListener("submit", async (event) => {
e.preventDefault(); event.preventDefault();
const input = document.getElementById("message");
const input = document.getElementById('message');
const text = input.value.trim(); const text = input.value.trim();
if (!text) return; if (!text) return;
addUserMessage(text); input.value = "";
adicionarMensagem('user', text); const backend = val("backend").replace(/\/$/, "");
input.value = ''; const channel = val("channel");
const session = normalizeSessionId(val("session"));
const backend = val('backend').replace(/\/$/, '');
const channel = val('channel');
// const session = val('session') || uuid();
const session = normalizeSessionId(val('session'));
const messageId = uuid(); const messageId = uuid();
const tenantId = val('tenant') || 'default'; const tenantId = val("tenant") || "default";
const agentId = val('agent') || 'telecom_contas'; const agentId = val("agent") || "telecom_contas";
document.getElementById("session").value = session;
document.getElementById('session').value = session;
const businessContext = buildBusinessContext(session, messageId); const businessContext = buildBusinessContext(session, messageId);
const commonContext = { channel_id: "browser", tenant_id: tenantId, agent_id: agentId, business_context: businessContext };
const commonContext = { const payload = channel === "voice"
channel_id: 'browser', ? { transcript: text, session_id: session, ani: businessContext.customer_key, message_id: messageId, tenant_id: tenantId, agent_id: agentId, context: commonContext }
tenant_id: tenantId, : { message: text, text, session_id: session, user_id: businessContext.customer_key || "web-user", message_id: messageId, tenant_id: tenantId, agent_id: agentId, context: commonContext };
agent_id: agentId,
business_context: businessContext
};
const payload = channel === 'voice'
? {
transcript: text,
session_id: session,
ani: businessContext.customer_key,
message_id: messageId,
tenant_id: tenantId,
agent_id: agentId,
context: commonContext
}
: {
message: text,
text: text,
session_id: session,
user_id: businessContext.customer_key || 'web-user',
message_id: messageId,
tenant_id: tenantId,
agent_id: agentId,
context: commonContext
};
syncDomainAliases(payload, businessContext); syncDomainAliases(payload, businessContext);
if (document.getElementById("useSse")?.checked) connectSSE(backend, session);
try { try {
status('Enviando mensagem'); status("Enviando mensagem");
const response = await fetch(`${backend}/gateway/message`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ channel, tenant_id: tenantId, agent_id: agentId, payload }) });
const res = await fetch(`${backend}/gateway/message`, { if (!response.ok) throw new Error(`${response.status} ${response.statusText}`);
method: 'POST', const data = await response.json();
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
channel,
tenant_id: tenantId,
agent_id: agentId,
payload
})
});
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
const data = await res.json();
const returnedSessionId = data.session_id || session; const returnedSessionId = data.session_id || session;
currentSessionId = returnedSessionId; currentSessionId = returnedSessionId;
document.getElementById('session').value = returnedSessionId; document.getElementById("session").value = returnedSessionId;
renderAgentResponse(data);
const resposta = status("Resposta recebida");
data.text || } catch (error) {
data.speak || renderAgentResponse({ text: `Erro ao chamar backend: ${error.message}`, agent_id: "sistema" });
data.message || status("Erro de conexão");
data.response ||
data.content ||
data.output ||
JSON.stringify(data);
adicionarMensagem('assistant', resposta);
status('Resposta recebida');
} catch (err) {
adicionarMensagem('assistant', `Erro ao chamar backend: ${err.message}`);
status('Erro de conexão');
} }
}); });

View File

@@ -1 +1 @@
body{font-family:system-ui,Arial,sans-serif;background:#f6f7fb;margin:0}.app{max-width:900px;margin:0 auto;padding:24px}header{margin-bottom:16px}.config{display:grid;grid-template-columns:1fr 160px 1fr;gap:12px;margin-bottom:16px}.config input,.config select,#message{width:100%;padding:10px;border:1px solid #ccc;border-radius:8px}.chat{background:#fff;border:1px solid #ddd;border-radius:12px;min-height:420px;padding:16px;overflow:auto}.msg{padding:10px 12px;border-radius:10px;margin:8px 0;max-width:75%}.user{background:#e8f0ff;margin-left:auto}.assistant{background:#f0f0f0}form{display:flex;gap:8px;margin-top:12px}button{padding:10px 16px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer} body{font-family:system-ui,Arial,sans-serif;background:#f6f7fb;margin:0;color:#202124}.app{max-width:900px;margin:0 auto;padding:24px}header{margin-bottom:16px}.config{display:grid;grid-template-columns:1fr 160px 1fr;gap:12px;margin-bottom:16px}.config input,.config select,#message{box-sizing:border-box;width:100%;padding:10px;border:1px solid #ccc;border-radius:8px}.chat{background:#fff;border:1px solid #ddd;border-radius:12px;min-height:420px;max-height:65vh;padding:16px;overflow:auto}.msg{padding:10px 12px;border-radius:10px;margin:8px 0;max-width:75%;white-space:pre-wrap}.user{background:#e8f0ff;margin-left:auto}.agent-response-group{display:grid;gap:10px;margin:12px 0;max-width:82%}.agent-message{overflow:hidden;border:1px solid #dfe3eb;border-radius:12px;background:#f8f9fb;box-shadow:0 1px 2px rgba(20,30,50,.05)}.agent-message__header{display:flex;align-items:center;gap:8px;margin:0;padding:8px 12px;border-bottom:1px solid #e5e8ee;background:#eef1f6}.agent-message__avatar{display:grid;place-items:center;width:24px;height:24px;border-radius:50%;background:#263f73;color:#fff;font-size:12px;font-weight:700}.agent-message__name{font-size:13px;font-weight:700;color:#34425e}.agent-message__body{padding:11px 12px;white-space:pre-wrap;overflow-wrap:anywhere;line-height:1.45}form{display:flex;gap:8px;margin-top:12px}button{padding:10px 16px;border:0;border-radius:8px;background:#111;color:#fff;cursor:pointer}@media(max-width:720px){.app{padding:12px}.config{grid-template-columns:1fr}.agent-response-group,.msg{max-width:92%}}

View File

@@ -338,9 +338,43 @@ class LoopRail(Guardrail):
code = "VLOOP" code = "VLOOP"
stage = "input" stage = "input"
@staticmethod
def _transaction_status(ctx: dict[str, Any]) -> str:
nonterminal = {
"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
"PAUSED", "WAITING_INPUT",
}
for result in reversed(list(ctx.get("mcp_results") or [])):
if isinstance(result, dict):
result_status = str(result.get("transaction_status") or "").strip().upper()
if result_status in nonterminal:
return result_status
active_transaction = ctx.get("active_transaction")
if isinstance(active_transaction, dict):
active_status = str(active_transaction.get("status") or "").strip().upper()
if active_status in nonterminal:
return active_status
return str(ctx.get("transaction_status") or "").strip().upper()
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
ctx = _ctx(context)
transaction_status = self._transaction_status(ctx)
if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
return RailDecision(
code=self.code,
allowed=True,
reason=f"continuidade_transacional:{transaction_status}",
sanitized_text=text,
metadata={
"history_window": len(list(ctx.get("history_texts") or [])[-6:]),
"repeated": False,
"mechanism": "deterministic_transaction_bypass",
"transaction_status": transaction_status,
"calibrated": True,
},
)
normalized = _lower(text).strip() normalized = _lower(text).strip()
history = [_lower(h).strip() for h in _ctx(context).get("history_texts", [])[-6:]] history = [_lower(h).strip() for h in ctx.get("history_texts", [])[-6:]]
repeated = history.count(normalized) >= 2 if normalized else False repeated = history.count(normalized) >= 2 if normalized else False
return RailDecision( return RailDecision(
code=self.code, code=self.code,

View File

@@ -358,30 +358,38 @@ class LoopRail(Guardrail):
@staticmethod @staticmethod
def _transaction_status(ctx: dict[str, Any]) -> str: def _transaction_status(ctx: dict[str, Any]) -> str:
status = str(ctx.get("transaction_status") or "").strip().upper() nonterminal = {
if status: "COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "EXECUTING",
return status "PAUSED", "WAITING_INPUT",
}
# A queued multi-intent transaction may be promoted immediately after
# the previous one completed. Prefer the newest live MCP evidence over
# a stale top-level COMPLETED status left by checkpoint merge order.
for result in reversed(list(ctx.get("mcp_results") or [])):
if isinstance(result, dict):
result_status = str(result.get("transaction_status") or "").strip().upper()
if result_status in nonterminal:
return result_status
active_transaction = ctx.get("active_transaction") active_transaction = ctx.get("active_transaction")
if isinstance(active_transaction, dict): if isinstance(active_transaction, dict):
return str(active_transaction.get("status") or "").strip().upper() active_status = str(active_transaction.get("status") or "").strip().upper()
return "" if active_status in nonterminal:
return active_status
return str(ctx.get("transaction_status") or "").strip().upper()
async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision: async def evaluate(self, text: str, context: dict[str, Any]) -> RailDecision:
ctx = _ctx(context) ctx = _ctx(context)
transaction_status = self._transaction_status(ctx) transaction_status = self._transaction_status(ctx)
# A short confirmation (for example ``sim``/``não``) may legitimately # Parameter values and short confirmations may legitimately repeat in
# appear several times in the same session because each transaction has # the same session. VLOOP must not consume input owned by the active
# its own confirmation boundary. VLOOP protects against conversational # transaction contract; extraction/confirmation remains responsible
# repetition; it must not consume the input that belongs to an active # for deciding whether the value is valid.
# transaction awaiting confirmation. The transaction runtime/classifier if transaction_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
# remains responsible for deciding whether the utterance is actually a
# valid confirm/reject response.
if transaction_status == "AWAITING_CONFIRMATION":
return RailDecision( return RailDecision(
code=self.code, code=self.code,
allowed=True, allowed=True,
reason="continuidade_transacional:AWAITING_CONFIRMATION", reason=f"continuidade_transacional:{transaction_status}",
sanitized_text=text, sanitized_text=text,
metadata={ metadata={
"history_window": len(list(ctx.get("history_texts") or [])[-6:]), "history_window": len(list(ctx.get("history_texts") or [])[-6:]),

View File

@@ -69,17 +69,30 @@ class MultiIntentPlanner:
def _intent_for_clause(self, clause: str) -> IntentDefinition | None: def _intent_for_clause(self, clause: str) -> IntentDefinition | None:
normalized = self._normalize(clause) normalized = self._normalize(clause)
clause_tokens = set(re.findall(r"[\w]+", normalized, flags=re.UNICODE)) ordered_clause_tokens = re.findall(r"[\w]+", normalized, flags=re.UNICODE)
clause_tokens = set(ordered_clause_tokens)
matches: list[tuple[int, int, IntentDefinition]] = [] matches: list[tuple[int, int, IntentDefinition]] = []
for intent in self.intents: for intent in self.intents:
for keyword in intent.keywords: for keyword in intent.keywords:
token = self._normalize(keyword) token = self._normalize(keyword)
keyword_tokens = re.findall(r"[\w]+", token, flags=re.UNICODE) keyword_tokens = re.findall(r"[\w]+", token, flags=re.UNICODE)
# Expressões configuradas representam conceitos, não somente
# substrings literais. Preserve a ordem dos termos e aceite
# qualificadores intermediários: "cancelar pedido" também
# reconhece "cancelar meu pedido", sem enumerar variações no YAML.
cursor = 0
ordered_match = bool(keyword_tokens)
for keyword_token in keyword_tokens:
try:
cursor = ordered_clause_tokens.index(keyword_token, cursor) + 1
except ValueError:
ordered_match = False
break
matched = bool( matched = bool(
keyword_tokens keyword_tokens
and ( and (
(len(keyword_tokens) == 1 and keyword_tokens[0] in clause_tokens) (len(keyword_tokens) == 1 and keyword_tokens[0] in clause_tokens)
or (len(keyword_tokens) > 1 and token in normalized) or (len(keyword_tokens) > 1 and ordered_match)
) )
) )
if matched: if matched:
@@ -146,7 +159,7 @@ class MultiIntentPlanner:
if len(operations) < 2: if len(operations) < 2:
return None return None
transactional = [item for item in operations if self._is_transactional(item)] transactional = [item for item in operations if self._is_transactional(item)]
if len(transactional) == 1: if transactional:
primary_operation = transactional[0] primary_operation = transactional[0]
operations.sort(key=lambda item: item is not primary_operation) operations.sort(key=lambda item: item is not primary_operation)
for operation in operations[1:]: for operation in operations[1:]:

View File

@@ -11,10 +11,34 @@ AgentHandler = Callable[[dict[str, Any]], Awaitable[dict[str, Any]]]
@dataclass(frozen=True) @dataclass(frozen=True)
class PendingTopicDrain: class PendingTopicDrain:
answer: str answer: str
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
mcp_results: list[dict[str, Any]] mcp_results: list[dict[str, Any]]
rag_results: list[dict[str, Any]] rag_results: list[dict[str, Any]]
state_patch: dict[str, Any]
_LIVE_TRANSACTION_STATUSES = {
"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION", "WAITING_CONFIRMATION",
"PENDING", "RUNNING", "IN_PROGRESS",
}
_TRANSACTION_STATE_KEYS = {
"active_transaction", "transaction_status", "transaction_pre_validation",
"confirmation_required", "confirmation_received", "selected_tool_call",
"pending_tool_call", "pending_tool_clarification", "pending_domain_workflow",
"missing_parameters", "tool_policy_result", "next_state", "workflow_id",
}
def _has_live_transaction(state: dict[str, Any]) -> bool:
active = state.get("active_transaction")
active_status = active.get("status") if isinstance(active, dict) else None
status = str(active_status or state.get("transaction_status") or "").upper()
if status:
return status in _LIVE_TRANSACTION_STATUSES
return bool(state.get("confirmation_required") or state.get("pending_tool_call"))
async def drain_pending_topics( async def drain_pending_topics(
@@ -33,11 +57,36 @@ async def drain_pending_topics(
combined_mcp_results = list(state.get("mcp_results") or []) combined_mcp_results = list(state.get("mcp_results") or [])
combined_rag_results = list(state.get("rag_results") or []) combined_rag_results = list(state.get("rag_results") or [])
secondary_answers: list[str] = [] secondary_answers: list[str] = []
state_patch: dict[str, Any] = {}
may_promote_deferred = not _has_live_transaction(state)
agent_responses = list(state.get("agent_responses") or [])
if not agent_responses and str(candidate or "").strip():
primary_agent = str(
state.get("active_agent")
or state.get("route")
or route.get("agent")
or route.get("route")
or state.get("agent_id")
or "agent"
)
agent_responses.append({
"agent": primary_agent,
"intent": state.get("intent") or route.get("intent"),
"answer": str(candidate).strip(),
"primary": True,
"status": "completed",
})
for topic in topics: for topic in topics:
disposition = topic.get("disposition") disposition = topic.get("disposition")
promoted = False
if disposition == "defer": if disposition == "defer":
remaining.append(topic) if not may_promote_deferred:
continue remaining.append(topic)
continue
topic = {**topic, "disposition": "execute", "status": "pending"}
disposition = "execute"
promoted = True
may_promote_deferred = False
if disposition != "execute": if disposition != "execute":
handled.append({**topic, "status": "completed"}) handled.append({**topic, "status": "completed"})
continue continue
@@ -105,6 +154,42 @@ async def drain_pending_topics(
if rag_evidence not in combined_rag_results: if rag_evidence not in combined_rag_results:
combined_rag_results.append(rag_evidence) combined_rag_results.append(rag_evidence)
secondary_answers.append(answer) secondary_answers.append(answer)
if promoted:
agent_responses = [{**item, "primary": False} for item in agent_responses]
agent_responses.append({
"operation_id": topic.get("operation_id"),
"agent": topic.get("agent"),
"intent": topic.get("intent"),
"answer": answer,
"primary": promoted,
"status": "completed",
})
if promoted:
for key in _TRANSACTION_STATE_KEYS:
if key in (result or {}):
state_patch[key] = result[key]
state_patch.update({
"route": topic.get("agent"),
"active_agent": topic.get("agent"),
"intent": topic.get("intent"),
"domain": topic.get("domain"),
"mcp_tools": topic.get("tools") or [],
# Replace the completed transaction's continuity snapshot so
# the next reply cannot be routed back to the previous agent.
"route_decision": {
"route": topic.get("agent"),
"agent": topic.get("agent"),
"intent": topic.get("intent"),
"domain": topic.get("domain"),
"mcp_tools": topic.get("tools") or [],
"next_state": (result or {}).get("next_state"),
"metadata": {
"multi_intent_secondary": True,
"promoted_from_pending_topics": True,
"operation_id": topic.get("operation_id"),
},
},
})
handled.append({**topic, "status": "completed"}) handled.append({**topic, "status": "completed"})
secondary_answers.extend(MultiIntentPlanner.public_messages(plan)) secondary_answers.extend(MultiIntentPlanner.public_messages(plan))
@@ -116,5 +201,11 @@ async def drain_pending_topics(
secondary = "\n\n".join(unique) secondary = "\n\n".join(unique)
candidate = f"{candidate.rstrip()}\n\n{secondary}".strip() candidate = f"{candidate.rstrip()}\n\n{secondary}".strip()
return PendingTopicDrain( return PendingTopicDrain(
candidate, remaining, handled, combined_mcp_results, combined_rag_results answer=candidate,
agent_responses=agent_responses,
pending_topics=remaining,
handled_topics=handled,
mcp_results=combined_mcp_results,
rag_results=combined_rag_results,
state_patch=state_patch,
) )

View File

@@ -457,6 +457,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -40,6 +40,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -249,6 +249,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -267,6 +289,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -412,7 +438,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -490,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -569,11 +597,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -650,8 +680,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -452,6 +452,8 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),

View File

@@ -37,6 +37,7 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]] pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]] handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str

View File

@@ -247,6 +247,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -265,6 +287,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -410,7 +436,8 @@ class AgentWorkflow:
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": ( "pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:] (decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan") else [] if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
), ),
} }
@@ -488,6 +515,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -567,11 +595,13 @@ class AgentWorkflow:
"final_answer": candidate, "final_answer": candidate,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
context = { context = {
@@ -648,8 +678,10 @@ class AgentWorkflow:
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics, "pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics, "handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results, "mcp_results": drained.mcp_results,
"rag_results": drained.rag_results, "rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }

View File

@@ -40,6 +40,28 @@ def test_transactional_intent_is_primary_even_when_mentioned_second():
assert [item.intent for item in plan.operations] == ["cancel", "invoice"] assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
def test_multiword_keyword_accepts_intermediate_qualifier_without_yaml_variant():
plan = _planner().plan("quero cancelar meu pedido e preciso da segunda via")
assert plan is not None
assert [item.intent for item in plan.operations] == ["cancel", "invoice"]
def test_first_of_multiple_transactions_is_primary_and_next_is_deferred():
planner = MultiIntentPlanner([
IntentDefinition(name="invoice", agent="billing", priority=30, keywords=["fatura"], mcp_tools=["consultar_fatura"]),
IntentDefinition(name="return", agent="support", priority=20, keywords=["devolver pedido"], mcp_tools=["solicitar_devolucao"]),
IntentDefinition(name="cancel", agent="orders", priority=10, keywords=["cancelar pedido"], mcp_tools=["cancelar_pedido"]),
], transactional_tools={"solicitar_devolucao", "cancelar_pedido"})
plan = planner.plan(
"quero consultar minha fatura. quero devolver pedido. quero cancelar pedido"
)
assert plan is not None
assert [item.intent for item in plan.operations] == ["return", "invoice", "cancel"]
assert [item.disposition for item in plan.operations] == ["execute", "execute", "defer"]
def test_sentence_boundary_does_not_leak_previous_intent_into_knowledge_query(): def test_sentence_boundary_does_not_leak_previous_intent_into_knowledge_query():
planner = MultiIntentPlanner([ planner = MultiIntentPlanner([
IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]), IntentDefinition(name="invoice", agent="billing", priority=20, keywords=["fatura"]),

View File

@@ -56,6 +56,9 @@ async def test_secondary_topic_does_not_inherit_primary_transaction_state():
assert captured["domain"] == "telecom_contas" assert captured["domain"] == "telecom_contas"
assert result.answer.startswith("Você confirma o cancelamento") assert result.answer.startswith("Você confirma o cancelamento")
assert result.answer.endswith("Aqui está a sua segunda via.") assert result.answer.endswith("Aqui está a sua segunda via.")
assert [item["agent"] for item in result.agent_responses] == ["agent", "faturas_agent"]
assert result.agent_responses[0]["primary"] is True
assert result.agent_responses[1]["primary"] is False
assert result.pending_topics == [] assert result.pending_topics == []
assert result.mcp_results == [ assert result.mcp_results == [
{"tool_name": "consultar_vas"}, {"tool_name": "consultar_vas"},
@@ -63,3 +66,52 @@ async def test_secondary_topic_does_not_inherit_primary_transaction_state():
] ]
assert result.rag_results[0]["metadata"]["provider"] == "kbdb" assert result.rag_results[0]["metadata"]["provider"] == "kbdb"
assert result.rag_results[0]["context"] == "Documento recuperado da base de conhecimento." assert result.rag_results[0]["context"] == "Documento recuperado da base de conhecimento."
@pytest.mark.asyncio
async def test_deferred_transaction_is_promoted_after_primary_completes():
async def cancellation_agent(state):
assert state["user_text"] == "quero cancelar meu pedido"
return {
"answer": "Você confirma o cancelamento do pedido PED-1001?",
"transaction_status": "AWAITING_CONFIRMATION",
"confirmation_required": True,
"pending_tool_call": {
"tool_name": "cancelar_pedido",
"arguments": {"order_id": "PED-1001"},
},
"active_transaction": {
"tool_name": "cancelar_pedido",
"status": "AWAITING_CONFIRMATION",
},
}
state = {
"answer": "A devolução foi registrada com sucesso.",
"route": "support_agent",
"active_agent": "support_agent",
"intent": "retail_support_exchange_return",
"transaction_status": "COMPLETED",
"pending_topics": [{
"operation_id": "op-3",
"intent": "retail_order_cancel",
"agent": "orders_agent",
"domain": "retail",
"tools": ["consultar_pedido", "cancelar_pedido"],
"source_text": "quero cancelar meu pedido",
"disposition": "defer",
"status": "pending",
}],
}
result = await drain_pending_topics(
state, state["answer"], {"orders_agent": cancellation_agent}
)
assert result.pending_topics == []
assert result.state_patch["route"] == "orders_agent"
assert result.state_patch["intent"] == "retail_order_cancel"
assert result.state_patch["transaction_status"] == "AWAITING_CONFIRMATION"
assert result.state_patch["pending_tool_call"]["tool_name"] == "cancelar_pedido"
assert result.agent_responses[-1]["primary"] is True
assert result.agent_responses[-1]["agent"] == "orders_agent"