from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer from agent_framework.workflows import END, START, FrameworkStateGraph from agent_framework.guardrails.pipeline import GuardrailPipeline from agent_framework.guardrails.output_supervisor import OutputSupervisor from agent_framework.guardrails.rail_action import RailAction from agent_framework.guardrails.rail_result import RailResult from agent_framework.judges.judge import JudgePipeline from agent_framework.routing.enterprise_router import EnterpriseRouter from agent_framework.supervisor.supervisor import Supervisor from agent_framework.observability.workflow_events import WorkflowTelemetry from agent_framework.observability.guardrail_events import GuardrailTelemetry from agent_framework.observability.judge_events import JudgeTelemetry from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry from agent_framework.observability.observer import AgentObserver from app.agents.faturas_agent import FaturasAgent from app.agents.vas_agent import VasAgent from app.agents.contestacao_agent import ContestacaoAgent from app.agents.suporte_contas_agent import SuporteContasAgent from app.state import AgentState from agent_framework.rag.rag_service import RagService from agent_framework.rag.embedding_provider import create_embedding_provider from agent_framework.cache.cache import create_cache from agent_framework.memory.long_term_memory import create_long_term_memory_manager class FrameworkOutputGuardrailRail: """Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo. O framework antigo retornava decisões allowed=True/False. O OutputSupervisor corporativo trabalha com RailAction (allow/sanitize/retry/block/handover). Este adapter evita reescrever todos os rails agora e mantém compatibilidade. """ code = "LEGACY_OUTPUT_GUARDRAILS" def __init__(self, pipeline: GuardrailPipeline): self.pipeline = pipeline async def evaluate(self, candidate: str, context: dict): final, decisions = await self.pipeline.run_output(candidate, context) serialized = [d.model_dump() for d in decisions] blocked = [d for d in decisions if not getattr(d, "allowed", True)] if blocked: first = blocked[0] code = (getattr(first, "code", "") or "").upper() action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK return RailResult( code=code or self.code, action=action, reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"), guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."), sanitized_text=final, metadata={"framework_decisions": serialized}, ) if final != candidate: return RailResult( code=self.code, action=RailAction.SANITIZE, reason="Resposta sanitizada por guardrail de saída do framework.", sanitized_text=final, metadata={"framework_decisions": serialized}, ) return RailResult( code=self.code, action=RailAction.ALLOW, reason="Resposta aprovada pelos guardrails de saída do framework.", sanitized_text=final, metadata={"framework_decisions": serialized}, ) class AgentWorkflow: """Workflow principal com dois modos de roteamento. Modos suportados por configuração: ROUTING_MODE=router input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails ROUTING_MODE=supervisor input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id. """ def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None): self.llm = llm self.memory = memory self.telemetry = telemetry self.analytics = analytics self.observer = observer or AgentObserver(analytics=analytics) self.settings = settings self.tool_router = tool_router self.summary_memory = summary_memory self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry) self.guardrails = GuardrailPipeline( observer=self.observer, llm=llm, enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)), fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)), ) self.output_supervisor_engine = OutputSupervisor( rails=[FrameworkOutputGuardrailRail(self.guardrails)], observer=self.observer, max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)), enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)), fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)), ) self.judges = JudgePipeline() self.supervisor = Supervisor() self.workflow_telemetry = WorkflowTelemetry(telemetry) self.guardrail_telemetry = GuardrailTelemetry(telemetry) self.judge_telemetry = JudgeTelemetry(telemetry) self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry) self.cache = create_cache(settings) self.embedding_provider = create_embedding_provider(settings) self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry) self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry) agent_kwargs = { "telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory, "guardrail_pipeline": self.guardrails, } self.faturas = FaturasAgent(llm, **agent_kwargs) self.vas = VasAgent(llm, **agent_kwargs) self.contestacao = ContestacaoAgent(llm, **agent_kwargs) self.suporte_contas = SuporteContasAgent(llm, **agent_kwargs) # Long-term memory is injected as a runtime capability after creation. for agent in (self.faturas, self.vas, self.contestacao, self.suporte_contas): agent.long_term_memory_manager = self.long_term_memory_manager self.graph = self._build_graph() @staticmethod def _output_guardrail_context(state: dict) -> dict: """Enriquece contexto de guardrail com evidência operacional real. CMP/ANATEL precisa dos protocolos produzidos pelas tools; GND/ALUC precisam enxergar evidências MCP. O domínio não implementa rails, apenas devolve dados. """ ctx = dict(state.get("context", {}) or {}) # Guardrails de saída (especialmente AOFERTA) precisam saber o que o # cliente efetivamente pediu. Sem o histórico, uma confirmação legítima # como "Você confirma cancelar ...?" parece uma oferta proativa isolada. # O estado do Contas mantém history como lista de dicts; preservamos esse # formato e acrescentamos o user_text corrente quando ele ainda não foi # persistido no histórico. history = list(state.get("history") or []) user_text = str(state.get("user_text") or state.get("sanitized_input") or "").strip() if user_text: last_user_text = "" for item in reversed(history): if isinstance(item, dict) and str(item.get("role", "")).lower() in {"user", "human"}: last_user_text = str(item.get("content") or "").strip() break if type(item).__name__ == "HumanMessage": last_user_text = str(getattr(item, "content", "") or "").strip() break if last_user_text != user_text: history.append({"role": "user", "content": user_text}) if history: ctx["conversation_history"] = history ctx["history_texts"] = [ str(item.get("content") or "") if isinstance(item, dict) else str(getattr(item, "content", "") or "") for item in history ] mcp_results = state.get("mcp_results") or [] ctx["evidence"] = mcp_results or ctx.get("evidence") ctx["tool_result"] = mcp_results or ctx.get("tool_result") ctx["tool_executed"] = any(isinstance(r, dict) and r.get("ok") for r in mcp_results) protocols: list[str] = [] seen: set[str] = set() protocol_keys = {"protocol_number", "protocolo_id", "interactionProtocol", "protocolNumber", "finalizacao_protocol"} def walk(value): if isinstance(value, dict): for key, item in value.items(): if key in protocol_keys and item not in (None, ""): text = str(item).strip() if text and text not in seen: seen.add(text) protocols.append(text) elif isinstance(item, (dict, list, tuple)): walk(item) elif isinstance(value, (list, tuple)): for item in value: walk(item) walk(mcp_results) if protocols: ctx["expected_protocols"] = protocols ctx["requer_protocolo"] = True ctx.setdefault("tipo_fluxo", "ajuste") return ctx def _node(self, name, fn): async def _wrapped(state): async with self.langgraph_telemetry.node(name, state): return await fn(state) return _wrapped def _build_graph(self): builder = FrameworkStateGraph(AgentState) builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails)) builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory)) builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision)) builder.add_node("faturas_agent", self._node("faturas_agent", self.faturas_agent)) builder.add_node("vas_agent", self._node("vas_agent", self.vas_agent)) builder.add_node("contestacao_agent", self._node("contestacao_agent", self.contestacao_agent)) builder.add_node("suporte_contas_agent", self._node("suporte_contas_agent", self.suporte_contas_agent)) builder.add_node("handoff", self._node("handoff", self.handoff)) builder.add_node("human_handoff", self._node("human_handoff", self.human_handoff)) builder.add_node("end_session", self._node("end_session", self.end_session)) builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent)) builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor)) builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails)) builder.add_node("judge", self._node("judge", self.judge)) builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review)) builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory)) builder.add_node("persist", self._node("persist", self.persist)) builder.add_edge(START, "input_guardrails") builder.add_conditional_edges( "input_guardrails", self._after_input_guardrails, {"blocked": "persist", "continue": "load_long_term_memory"}, ) builder.add_edge("load_long_term_memory", "routing_decision") builder.add_conditional_edges( "routing_decision", lambda s: s.get("route", "faturas_agent"), { "faturas_agent": "faturas_agent", "vas_agent": "vas_agent", "contestacao_agent": "contestacao_agent", "suporte_contas_agent": "suporte_contas_agent", "handoff": "handoff", "human_handoff": "human_handoff", "end_session": "end_session", "supervisor_agent": "supervisor_agent", }, ) builder.add_edge("faturas_agent", "output_supervisor") builder.add_edge("vas_agent", "output_supervisor") builder.add_edge("contestacao_agent", "output_supervisor") builder.add_edge("suporte_contas_agent", "output_supervisor") builder.add_edge("handoff", "output_supervisor") builder.add_edge("human_handoff", "output_supervisor") builder.add_edge("end_session", "output_supervisor") builder.add_edge("supervisor_agent", "output_supervisor") builder.add_edge("output_supervisor", "output_guardrails") builder.add_edge("output_guardrails", "judge") builder.add_edge("judge", "supervisor_review") builder.add_edge("supervisor_review", "persist_long_term_memory") builder.add_edge("persist_long_term_memory", "persist") builder.add_edge("persist", END) return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings)) def _after_input_guardrails(self, state): return "blocked" if state.get("blocked") else "continue" async def input_guardrails(self, state): if state.get("session_ended") is True: answer = str(getattr( self.settings, "SESSION_ALREADY_ENDED_MESSAGE", "Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.", )) await self.telemetry.event( "session.message.rejected_after_end", {"session_id": state.get("conversation_key") or state.get("session_id")}, ) return { "answer": answer, "final_answer": answer, "blocked": True, "session_control": "END_SESSION", "session_ended": True, "next_state": "SESSION_ENDED", } async with self.telemetry.span( "workflow.input_guardrails", session_id=state.get("conversation_key") or state.get("session_id"), input=state.get("user_text"), ): history_texts = [m.get("content", "") for m in state.get("history", [])] await self.observer.emit_grl( "001", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "input", }, component="workflow.input_guardrails.start", ) sanitized, decisions = await self.guardrails.run_input( state["user_text"], { **(state.get("context") or {}), "history_texts": history_texts, "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "agent_profile": state.get("agent_profile") or {}, }, ) for _decision in decisions: await self.guardrail_telemetry.evaluated("input", _decision) await self.observer.emit_grl( "002" if _decision.allowed else "004", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "input", "rail_code": getattr(_decision, "code", None), "allowed": bool(_decision.allowed), "reason": getattr(_decision, "reason", None), }, component="workflow.input_guardrails.decision", ) if not _decision.allowed: await self.guardrail_telemetry.blocked("input", _decision) await self.telemetry.event( "guardrails.input.completed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "decisions": [d.model_dump() for d in decisions], }, ) await self.observer.emit_grl( "009", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "input", "blocked": any(not d.allowed for d in decisions), "decision_count": len(decisions), }, component="workflow.input_guardrails.final", ) if any(not d.allowed for d in decisions): return { "sanitized_input": sanitized, "answer": "Não consegui seguir com essa mensagem por regra de segurança.", "final_answer": "Não consegui seguir com essa mensagem por regra de segurança.", "guardrail_decisions": [d.model_dump() for d in decisions], "route": "blocked", "blocked": True, } return { "sanitized_input": sanitized, "guardrail_decisions": [d.model_dump() for d in decisions], "blocked": False, } async def routing_decision(self, state): mode = getattr(self.settings, "ROUTING_MODE", "router") async with self.telemetry.span( "workflow.routing_decision", session_id=state.get("conversation_key") or state.get("session_id"), input={ "mode": mode, "text": state.get("sanitized_input") or state.get("user_text"), "previous_state": state.get("next_state"), }, ): if mode == "supervisor": plan = await self.supervisor.route_plan(state) await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence}) return { "route": "supervisor_agent", "intent": plan.intent, "supervisor_plan": { "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata, }, "route_decision": { "route": "supervisor_agent", "agent": "supervisor", "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "method": "supervisor", "metadata": plan.metadata, }, } decision = await self.router.route(state) await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence}) await self.observer.emit_ic( "ROUTE_SELECTED", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": decision.route, "intent": decision.intent, "confidence": decision.confidence, "method": getattr(decision, "method", None), }, component="workflow.routing_decision", ) return { "route": decision.route, "intent": decision.intent, "route_decision": decision.model_dump(mode="json"), "domain": decision.domain, "mcp_tools": decision.mcp_tools, "next_state": decision.next_state, "active_agent": decision.agent, "route_bypassed": decision.method == "continuity", "session_control": (decision.metadata or {}).get("session_control", ""), "human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF", "session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION", "continuity_signal": { "decision": (decision.metadata or {}).get("continuity_decision"), "confidence": decision.confidence if decision.method == "continuity" else None, "reason": decision.reason if decision.method == "continuity" else None, "profile": (decision.metadata or {}).get("continuity_profile"), } if decision.method == "continuity" else {}, } async def faturas_agent(self, state): async with self.telemetry.span( "workflow.agent.billing", session_id=state.get("conversation_key") or state.get("session_id"), input={"intent": state.get("intent")}, ): return await self.faturas.run(state) async def vas_agent(self, state): async with self.telemetry.span( "workflow.agent.product", session_id=state.get("conversation_key") or state.get("session_id"), input={"intent": state.get("intent")}, ): return await self.vas.run(state) async def contestacao_agent(self, state): async with self.telemetry.span( "workflow.agent.orders", session_id=state.get("conversation_key") or state.get("session_id"), input={"intent": state.get("intent")}, ): return await self.contestacao.run(state) async def suporte_contas_agent(self, state): async with self.telemetry.span( "workflow.agent.support", session_id=state.get("conversation_key") or state.get("session_id"), input={"intent": state.get("intent")}, ): return await self.suporte_contas.run(state) async def supervisor_agent(self, state): """Executa um ou mais agentes no modo supervisor e consolida a resposta. Este nó mantém o desenho de supervisor sem obrigar o restante do workflow a conhecer quantos agentes foram acionados. Cada execução especializada recebe o mesmo estado, mas com route/active_agent atualizados. """ plan = state.get("supervisor_plan") or {} agents = plan.get("agents") or ["faturas_agent"] handlers = { "faturas_agent": self.faturas.run, "vas_agent": self.vas.run, "contestacao_agent": self.contestacao.run, "suporte_contas_agent": self.suporte_contas.run, } partials = [] mcp_results = [] async with self.telemetry.span( "workflow.supervisor_agent", session_id=state.get("conversation_key") or state.get("session_id"), input={"agents": agents, "intent": state.get("intent")}, ): for agent_name in agents: handler = handlers.get(agent_name) if handler is None: continue child_state = {**state, "route": agent_name, "active_agent": agent_name} result = await handler(child_state) partials.append({"agent": agent_name, "answer": result.get("answer", "")}) mcp_results.extend(result.get("mcp_results") or []) if len(partials) == 1: answer = partials[0]["answer"] else: joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials) answer = ( "[Supervisor] Consolidação de múltiplos agentes acionados.\n" f"{joined}" ) return { "answer": answer, "supervisor_results": partials, "mcp_results": mcp_results, "next_state": "SUPERVISOR_ACTIVE", } async def handoff(self, state): async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")): target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent") answer = ( "Vou redirecionar sua solicitação para o especialista correto. " f"Destino sugerido: {target or 'agente especializado'}." ) return {"answer": answer} async def human_handoff(self, state): session_id = state.get("conversation_key") or state.get("session_id") async with self.telemetry.span("workflow.human_handoff", session_id=session_id): try: if self.tool_router: runtime_context = (state.get("context") or {}).get("business_context") or {} await self.tool_router.call( "finalizar_atendimento", {"status": "nao_resolvido", "summary": "Handoff humano solicitado pelo agent_framework_oci", "confirmed": True}, business_context=runtime_context, original_context=state.get("context") or {}, ) except Exception: pass answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa.")) await self.telemetry.event( "session.human_handoff.requested", { "session_id": session_id, "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "reason": (state.get("route_decision") or {}).get("reason"), }, ) return { "answer": answer, "session_control": "HUMAN_HANDOFF", "human_handoff_requested": True, "session_ended": True, "terminal_status": "nao_resolvido", "next_state": "HUMAN_HANDOFF_REQUESTED", } async def end_session(self, state): session_id = state.get("conversation_key") or state.get("session_id") async with self.telemetry.span("workflow.end_session", session_id=session_id): # Preserva efeitos colaterais de negócio do Contas (protocolos/ICs) # sem devolver a orquestração ao runtime do framework. Falha aqui não impede # o encerramento controlado pelo framework. try: if self.tool_router: runtime_context = (state.get("context") or {}).get("business_context") or {} await self.tool_router.call( "finalizar_atendimento", {"status": "resolvido", "summary": "Encerramento solicitado pelo agent_framework_oci", "confirmed": True}, business_context=runtime_context, original_context=state.get("context") or {}, ) except Exception: pass answer = str(getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato.")) await self.telemetry.event( "session.end.requested", { "session_id": session_id, "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "reason": (state.get("route_decision") or {}).get("reason"), }, ) return { "answer": answer, "session_control": "END_SESSION", "session_ended": True, "terminal_status": "resolvido", "human_handoff_requested": False, "next_state": "SESSION_ENDED", } async def output_supervisor(self, state): """Valida a resposta candidata com o OutputSupervisor corporativo. Este nó não substitui o roteador/supervisor multiagente. Ele roda após o agente gerar `answer` e antes dos judges/persistência, produzindo campos supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver. """ if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)): return { "output_guardrails_already_applied": False, "supervisor_action": "disabled", "supervisor_attempt": int(state.get("supervisor_attempt", 0)), } candidate = state.get("answer") or "" context = { **self._output_guardrail_context(state), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "session_id": state.get("conversation_key") or state.get("session_id"), "route": state.get("route"), "intent": state.get("intent"), "supervisor_attempt": int(state.get("supervisor_attempt", 0)), } async with self.telemetry.span( "workflow.output_supervisor", session_id=state.get("conversation_key") or state.get("session_id"), input=candidate, ): decision = await self.output_supervisor_engine.evaluate(candidate, context) action = decision.action.value await self.telemetry.event( "output_supervisor.completed", { "session_id": context["session_id"], "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "action": action, "approved": decision.approved, "guidance": decision.guidance, }, ) await self.observer.emit_ic( "IC.OUTPUT_SUPERVISOR_COMPLETED", { "session_id": context["session_id"], "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": state.get("route"), "intent": state.get("intent"), "action": action, "approved": decision.approved, "result_count": len(decision.results), }, component="workflow.output_supervisor", ) if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}: final_answer = decision.candidate elif decision.action == RailAction.HANDOVER: final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista." else: final_answer = decision.fallback_message return { "answer": final_answer, "final_answer": final_answer, "supervisor_action": action, "supervisor_guidance": decision.guidance, "supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0), "supervisor_handover_reason": decision.handover_reason, "output_supervisor_results": [ { "code": r.code, "action": r.action.value, "reason": r.reason, "guidance": r.guidance, "metadata": r.metadata, } for r in decision.results ], "output_guardrails_already_applied": True, "guardrail_decisions": state.get("guardrail_decisions", []) + [item for r in decision.results for item in (r.metadata or {}).get("framework_decisions", [])], } async def output_guardrails(self, state): if state.get("output_guardrails_already_applied"): return {"final_answer": state.get("final_answer") or state.get("answer") or ""} async with self.telemetry.span( "workflow.output_guardrails", session_id=state.get("conversation_key") or state.get("session_id"), input=state.get("answer"), ): await self.observer.emit_grl( "001", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "output", "route": state.get("route"), "intent": state.get("intent"), }, component="workflow.output_guardrails.start", ) guardrail_context = self._output_guardrail_context(state) final, decisions = await self.guardrails.run_output( state["answer"], guardrail_context ) for _decision in decisions: await self.guardrail_telemetry.evaluated("output", _decision) await self.observer.emit_grl( "002" if _decision.allowed else "004", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "output", "rail_code": getattr(_decision, "code", None), "allowed": bool(_decision.allowed), "reason": getattr(_decision, "reason", None), }, component="workflow.output_guardrails.decision", ) if not _decision.allowed: await self.guardrail_telemetry.blocked("output", _decision) await self.telemetry.event( "guardrails.output.completed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "decisions": [d.model_dump() for d in decisions], }, ) await self.observer.emit_grl( "009", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "output", "blocked": any(not d.allowed for d in decisions), "decision_count": len(decisions), }, component="workflow.output_guardrails.final", ) return { "final_answer": final, "guardrail_decisions": state.get("guardrail_decisions", []) + [d.model_dump() for d in decisions], } async def judge(self, state): async with self.telemetry.span( "workflow.judge", session_id=state.get("conversation_key") or state.get("session_id"), input={"question": state.get("user_text"), "answer": state.get("final_answer")}, ): judge_context = dict(state.get("context", {}) or {}) judge_context["mcp_results"] = state.get("mcp_results", []) relevant_transaction_evidence = list(state.get("relevant_transaction_evidence") or []) judge_context["transaction_evidence"] = relevant_transaction_evidence current_evidence = list(state.get("mcp_results", []) or []) current_evidence.extend(relevant_transaction_evidence) judge_context["evidence"] = current_evidence or judge_context.get("evidence") judge_context["route"] = state.get("route") judge_context["intent"] = state.get("intent") # Judge sampling must see the finalized transaction state. These # fields are populated by the agent/tool runtime before this node. for key in ( "transaction_status", "confirmation_required", "confirmation_received", "tool_policy_result", "selected_tool_call", "pending_tool_call", ): judge_context[key] = state.get(key) judge_context["transactional_tools"] = [ result.get("tool_name") for result in state.get("mcp_results", []) if isinstance(result, dict) and ( (result.get("metadata") or {}).get("operation_type") == "transactional" or result.get("awaiting_confirmation") or result.get("transaction_status") ) ] results = await self.judges.evaluate_all( state["user_text"], state["final_answer"], judge_context ) for _result in results: await self.judge_telemetry.evaluated(_result) await self.telemetry.event( "judges.completed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "results": [r.model_dump() for r in results], }, ) return {"judge_results": [r.model_dump() for r in results]} async def supervisor_review(self, state): async with self.telemetry.span( "workflow.supervisor_review", session_id=state.get("conversation_key") or state.get("session_id"), input=state.get("final_answer"), ): ok, answer = await self.supervisor.review( state["final_answer"], state.get("context", {}) ) await self.telemetry.event( "supervisor.review.completed", {"session_id": state.get("session_id"), "approved": ok}, ) return {"final_answer": answer if ok else answer} async def load_long_term_memory(self, state): """Carrega LTM antes do roteamento e mantém o resultado no estado. A carga explícita evita depender apenas do agente selecionado para realizar a recuperação e facilita o diagnóstico de identidade/namespace. """ try: memories = await self.long_term_memory_manager.load(state) serialized = [] context_lines = [] for item in memories or []: if hasattr(item, "model_dump"): data = item.model_dump(mode="json") elif hasattr(item, "__dict__"): data = dict(item.__dict__) elif isinstance(item, dict): data = dict(item) else: data = {"value": str(item)} serialized.append(data) key = data.get("key") or data.get("memory_key") or data.get("category") or "memory" value = data.get("value") or data.get("memory_value") if value not in (None, ""): context_lines.append(f"- {key}: {value}") return { "long_term_memories": serialized, "long_term_memory_context": "\n".join(context_lines), } except Exception as exc: await self.telemetry.event( "long_term_memory.load.failed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "subject_key": state.get("long_term_memory_subject_key"), "error": str(exc), }, ) return { "long_term_memories": [], "long_term_memory_context": "", "long_term_memory_load_error": str(exc), } async def persist_long_term_memory(self, state): try: result = await self.long_term_memory_manager.persist_turn(state) await self.telemetry.event( "long_term_memory.persist.completed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "subject_key": state.get("long_term_memory_subject_key"), "result": result, }, ) return {"long_term_memory_write_result": result} except Exception as exc: await self.telemetry.event( "long_term_memory.persist.failed", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "subject_key": state.get("long_term_memory_subject_key"), "error": str(exc), }, ) return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}} async def persist(self, state): async with self.telemetry.span( "workflow.persist", session_id=state.get("conversation_key") or state.get("session_id"), input={"route": state.get("route"), "intent": state.get("intent")}, ): await self.observer.emit_ic( "AGENT_COMPLETED", { "session_id": state.get("conversation_key") or state["session_id"], "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": state.get("route"), "intent": state.get("intent"), "route_decision": state.get("route_decision"), "judges": state.get("judge_results", []), "mcp_tools": state.get("mcp_tools", []), "mcp_results": state.get("mcp_results", []), "transaction_evidence": state.get("relevant_transaction_evidence", []), }, ) await self.observer.emit_noc( "006", { "session_id": state.get("conversation_key") or state["session_id"], "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": state.get("route"), "intent": state.get("intent"), "answer_chars": len(state.get("final_answer") or ""), }, component="workflow.persist", ) await self.telemetry.event( "agent.completed", { "session_id": state.get("conversation_key") or state["session_id"], "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": state.get("route"), "intent": state.get("intent"), "answer_chars": len(state.get("final_answer") or ""), }, ) return state async def ainvoke(self, state): thread_id = state.get("conversation_key") or state["session_id"] config = {"configurable": {"thread_id": thread_id}} async with self.telemetry.span( "workflow.langgraph.ainvoke", session_id=state.get("conversation_key") or state.get("session_id"), user_id=state.get("context", {}).get("user_id"), input={"user_text": state.get("user_text")}, tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",], ): await self.workflow_telemetry.started("agent_workflow", state) await self.observer.emit_noc( "001", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "channel_id": (state.get("context") or {}).get("channel"), "message_id": (state.get("context") or {}).get("message_id"), "ura_call_id": (state.get("context") or {}).get("ura_call_id"), }, component="workflow.ainvoke", ) await self.observer.emit_ic( "AGENT_STARTED", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "channel_id": (state.get("context") or {}).get("channel"), "message_id": (state.get("context") or {}).get("message_id"), "user_text_chars": len(state.get("user_text") or ""), }, component="workflow.ainvoke", ) try: result = await self.graph.ainvoke(state, config=config) await self.workflow_telemetry.completed("agent_workflow", result) return result except Exception as exc: await self.workflow_telemetry.failed("agent_workflow", exc) await self.observer.emit_noc( "005", { "session_id": state.get("conversation_key") or state.get("session_id"), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "error": str(exc), "exception_type": exc.__class__.__name__, }, component="workflow.ainvoke", ) raise