from __future__ import annotations from typing import Any from langgraph.graph import END, START, StateGraph from agent_framework.cache.cache import create_cache from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer 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.judges.judge import JudgePipeline 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.workflow_events import WorkflowTelemetry from agent_framework.rag.embedding_provider import create_embedding_provider from agent_framework.rag.rag_service import RagService from agent_framework.routing.enterprise_router import EnterpriseRouter from agent_framework.supervisor.supervisor import Supervisor from agent_framework.observer import AgentObserver from app.agents.contas_agent import ContasAgent from app.state import AgentState class AgentWorkflow: """Workflow LangGraph do agent_contas_first migrado. O desenho é propositalmente fino: a orquestração corporativa fica no framework. O agente executa apenas o domínio Contas; guardrails, output supervisor, judges, RAG, MCP, checkpoints e observabilidade são nativos. """ 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.guardrails = GuardrailPipeline( observer=self.observer, enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)), fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)), ) self.output_supervisor_engine = OutputSupervisor( 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(llm=llm, settings=settings) self.supervisor = Supervisor() self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry) 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, llm=llm) agent_kwargs = { "telemetry": telemetry, "tool_router": tool_router, "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory, } self.contas = ContasAgent(llm, **agent_kwargs) self.graph = self._build_graph() 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 = StateGraph(AgentState) builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails)) builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision)) builder.add_node("contas_agent", self._node("contas_agent", self.contas_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", self._node("persist", self.persist)) builder.add_edge(START, "input_guardrails") builder.add_conditional_edges("input_guardrails", lambda s: "blocked" if s.get("blocked") else "continue", {"blocked": "persist", "continue": "routing_decision"}) builder.add_edge("routing_decision", "contas_agent") builder.add_edge("contas_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") builder.add_edge("persist", END) return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings)) async def input_guardrails(self, state: AgentState) -> dict[str, Any]: session_id = state.get("conversation_key") or state.get("session_id") async with self.telemetry.span("workflow.input_guardrails", session_id=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": 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": 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) blocked = any(not d.allowed for d in decisions) await self.observer.emit_grl("009", {"session_id": session_id, "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "phase": "input", "blocked": blocked, "decision_count": len(decisions)}, component="workflow.input_guardrails.final") if blocked: 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: AgentState) -> dict[str, Any]: session_id = state.get("conversation_key") or state.get("session_id") async with self.telemetry.span("workflow.routing_decision", session_id=session_id, input={"text": state.get("sanitized_input") or state.get("user_text")}): decision = await self.router.route({**state, "route": "contas_agent"}) await self.langgraph_telemetry.edge("routing_decision", "contas_agent", state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence}) await self.observer.emit_ic("IC.CONTAS_ROUTE_SELECTED", {"session_id": session_id, "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "route": "contas_agent", "intent": decision.intent, "confidence": decision.confidence, "method": getattr(decision, "method", None), "mcp_tools": decision.mcp_tools}, component="workflow.routing_decision") return {"route": "contas_agent", "intent": decision.intent, "route_decision": decision.model_dump(mode="json"), "domain": decision.domain, "mcp_tools": decision.mcp_tools, "next_state": decision.next_state} async def contas_agent(self, state: AgentState) -> dict[str, Any]: async with self.telemetry.span("workflow.agent.contas", session_id=state.get("conversation_key") or state.get("session_id"), input={"intent": state.get("intent")}): return await self.contas.run(state) async def output_supervisor(self, state: AgentState) -> dict[str, Any]: 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))} session_id = state.get("conversation_key") or state.get("session_id") candidate = state.get("answer") or "" context = {**(state.get("context") or {}), "tenant_id": state.get("tenant_id"), "agent_id": state.get("agent_id"), "session_id": session_id, "route": state.get("route"), "intent": state.get("intent")} async with self.telemetry.span("workflow.output_supervisor", session_id=session_id, input=candidate): decision = await self.output_supervisor_engine.evaluate(candidate, context) action = decision.action.value await self.observer.emit_ic("IC.OUTPUT_SUPERVISOR_COMPLETED", {"session_id": 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} async def output_guardrails(self, state: AgentState) -> dict[str, Any]: if state.get("output_guardrails_already_applied"): return {"final_answer": state.get("final_answer") or state.get("answer") or ""} session_id = state.get("conversation_key") or state.get("session_id") async with self.telemetry.span("workflow.output_guardrails", session_id=session_id, input=state.get("answer")): await self.observer.emit_grl("001", {"session_id": 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") final, decisions = await self.guardrails.run_output(state["answer"], state.get("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": 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.observer.emit_grl("009", {"session_id": 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: AgentState) -> dict[str, Any]: 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")}): results = await self.judges.evaluate_all(state["user_text"], state["final_answer"], {**(state.get("context") or {}), "mcp_results": state.get("mcp_results", []), "rag_context": state.get("rag_context"), "route": state.get("route"), "intent": state.get("intent")}) for result in results: await self.judge_telemetry.evaluated(result) return {"judge_results": [r.model_dump() for r in results]} async def supervisor_review(self, state: AgentState) -> dict[str, Any]: 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", {})) return {"final_answer": answer if ok else answer} async def persist(self, state: AgentState) -> dict[str, Any]: session_id = state.get("conversation_key") or state["session_id"] async with self.telemetry.span("workflow.persist", session_id=session_id, input={"route": state.get("route"), "intent": state.get("intent")}): await self.observer.emit_ic("IC.CONTAS_AGENT_PERSISTED", {"session_id": 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", [])}, component="workflow.persist") await self.observer.emit_noc("006", {"session_id": 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": 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 {"final_answer": state.get("final_answer") or state.get("answer") or ""} async def ainvoke(self, state: AgentState, config: dict[str, Any] | None = None): return await self.graph.ainvoke(state, config=config or {"configurable": {"thread_id": state.get("conversation_key") or state.get("session_id")}})