mirror of
https://github.com/hoshikawa2/first_contas.git
synced 2026-07-09 18:24:20 +00:00
193 lines
16 KiB
Python
193 lines
16 KiB
Python
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")}})
|