mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 22:04:20 +00:00
first commit
This commit is contained in:
BIN
app/workflows/__pycache__/agent_graph.cpython-313.pyc
Normal file
BIN
app/workflows/__pycache__/agent_graph.cpython-313.pyc
Normal file
Binary file not shown.
Binary file not shown.
718
app/workflows/agent_graph.py
Normal file
718
app/workflows/agent_graph.py
Normal file
@@ -0,0 +1,718 @@
|
||||
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
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.billing_agent import BillingAgent
|
||||
from app.agents.product_agent import ProductAgent
|
||||
from app.agents.orders_agent import OrdersAgent
|
||||
from app.agents.support_agent import SupportAgent
|
||||
from app.agents.backoffice_agent import BackofficeAgent
|
||||
from app.state import AgentState
|
||||
from agent_framework.rag.rag_service import RagService
|
||||
from agent_framework.cache.cache import create_cache
|
||||
|
||||
|
||||
class LegacyOutputGuardrailRail:
|
||||
"""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={"legacy_decisions": serialized},
|
||||
)
|
||||
|
||||
if final != candidate:
|
||||
return RailResult(
|
||||
code=self.code,
|
||||
action=RailAction.SANITIZE,
|
||||
reason="Resposta sanitizada por guardrail de saída legado.",
|
||||
sanitized_text=final,
|
||||
metadata={"legacy_decisions": serialized},
|
||||
)
|
||||
|
||||
return RailResult(
|
||||
code=self.code,
|
||||
action=RailAction.ALLOW,
|
||||
reason="Resposta aprovada pelos guardrails de saída legados.",
|
||||
sanitized_text=final,
|
||||
metadata={"legacy_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):
|
||||
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.tool_router = tool_router
|
||||
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(
|
||||
rails=[LegacyOutputGuardrailRail(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.rag_service = RagService(settings, 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}
|
||||
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||
self.product = ProductAgent(llm, **agent_kwargs)
|
||||
self.orders = OrdersAgent(llm, **agent_kwargs)
|
||||
self.support = SupportAgent(llm, **agent_kwargs)
|
||||
self.backoffice = BackofficeAgent(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("billing_agent", self._node("billing_agent", self.billing_agent))
|
||||
builder.add_node("product_agent", self._node("product_agent", self.product_agent))
|
||||
builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent))
|
||||
builder.add_node("support_agent", self._node("support_agent", self.support_agent))
|
||||
builder.add_node("backoffice_agent", self._node("backoffice_agent", self.backoffice_agent))
|
||||
builder.add_node("handoff", self._node("handoff", self.handoff))
|
||||
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", self._node("persist", self.persist))
|
||||
|
||||
builder.add_edge(START, "input_guardrails")
|
||||
builder.add_conditional_edges(
|
||||
"input_guardrails",
|
||||
self._after_input_guardrails,
|
||||
{"blocked": "persist", "continue": "routing_decision"},
|
||||
)
|
||||
builder.add_conditional_edges(
|
||||
"routing_decision",
|
||||
lambda s: s.get("route", "billing_agent"),
|
||||
{
|
||||
"billing_agent": "billing_agent",
|
||||
"product_agent": "product_agent",
|
||||
"orders_agent": "orders_agent",
|
||||
"support_agent": "support_agent",
|
||||
"backoffice_agent": "backoffice_agent",
|
||||
"handoff": "handoff",
|
||||
"supervisor_agent": "supervisor_agent",
|
||||
},
|
||||
)
|
||||
builder.add_edge("billing_agent", "output_supervisor")
|
||||
builder.add_edge("product_agent", "output_supervisor")
|
||||
builder.add_edge("orders_agent", "output_supervisor")
|
||||
builder.add_edge("support_agent", "output_supervisor")
|
||||
builder.add_edge("backoffice_agent", "output_supervisor")
|
||||
builder.add_edge("handoff", "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")
|
||||
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):
|
||||
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,
|
||||
}
|
||||
|
||||
async def billing_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("billing_agent", 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.billing.run(state)
|
||||
|
||||
async def product_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("product_agent", 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.product.run(state)
|
||||
|
||||
async def orders_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("orders_agent", 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.orders.run(state)
|
||||
|
||||
|
||||
async def support_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("support_agent", 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.support.run(state)
|
||||
|
||||
async def backoffice_agent(self, state):
|
||||
async with self.langgraph_telemetry.node("backoffice_agent", state):
|
||||
async with self.telemetry.span(
|
||||
"workflow.agent.backoffice",
|
||||
session_id=state.get("conversation_key") or state.get("session_id"),
|
||||
input={"intent": state.get("intent")},
|
||||
):
|
||||
return await self.backoffice.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 ["backoffice_agent"]
|
||||
handlers = {
|
||||
"billing_agent": self.billing.run,
|
||||
"product_agent": self.product.run,
|
||||
"orders_agent": self.orders.run,
|
||||
"support_agent": self.support.run,
|
||||
"backoffice_agent": self.backoffice.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 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 = {
|
||||
**(state.get("context") or {}),
|
||||
"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("legacy_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",
|
||||
)
|
||||
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": 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")},
|
||||
):
|
||||
results = await self.judges.evaluate_all(
|
||||
state["user_text"], state["final_answer"], state.get("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 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", []),
|
||||
},
|
||||
)
|
||||
|
||||
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
|
||||
748
app/workflows/backoffice_native_runtime.py
Normal file
748
app/workflows/backoffice_native_runtime.py
Normal file
@@ -0,0 +1,748 @@
|
||||
"""Backoffice TIM/ANATEL workflows executed by the framework runtime.
|
||||
|
||||
This module is the migration boundary that makes the backoffice **framework-native**:
|
||||
|
||||
* domain logic remains in business nodes/services/prompts copied from develop;
|
||||
* the backend no longer imports or executes ``src.agent.graphs.*``;
|
||||
* the framework runtime builds/compiles the LangGraph workflows, owns telemetry,
|
||||
guardrails, judges, supervisor, checkpoint and persistence hooks;
|
||||
* legacy REST contracts call ``execute_workflow`` instead of legacy executors.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable
|
||||
from pathlib import Path
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
|
||||
import yaml
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
|
||||
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.guardrails.rail_result import RailResult
|
||||
from agent_framework.judges.judge import JudgePipeline
|
||||
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 src.agent.state.agent_state import AgentState, create_initial_state, increment_iteration
|
||||
from src.agent.state.steps import GraphStep
|
||||
from src.agent.state.steps_emulator import EmulatorGraphStep
|
||||
import src.agent.nodes as checklist_nodes
|
||||
from src.agent.nodes.emulator import (
|
||||
approve_draft_node,
|
||||
close_case_node,
|
||||
fetch_case_node,
|
||||
generate_response_node,
|
||||
persist_draft_node,
|
||||
retrieve_history_node,
|
||||
retrieve_templates_node,
|
||||
router_node,
|
||||
start_response_emulation_node,
|
||||
validate_actions_node,
|
||||
validate_response_node,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("backoffice.native_runtime")
|
||||
|
||||
|
||||
def _project_root() -> Path:
|
||||
return Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def _load_yaml(path: str | Path) -> dict[str, Any]:
|
||||
resolved = Path(path)
|
||||
if not resolved.is_absolute():
|
||||
resolved = _project_root() / resolved
|
||||
if not resolved.exists():
|
||||
raise FileNotFoundError(f"Configuração não encontrada: {resolved}")
|
||||
with resolved.open("r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh) or {}
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"Configuração YAML inválida: {resolved}")
|
||||
data["_config_path"] = str(resolved)
|
||||
return data
|
||||
|
||||
|
||||
def _resolve_agent_profile(agent_id: str = "backoffice_anatel") -> dict[str, Any]:
|
||||
agents_cfg = _load_yaml("config/agents.yaml")
|
||||
for agent in agents_cfg.get("agents", []) or []:
|
||||
if agent.get("agent_id") == agent_id:
|
||||
profile = dict(agent)
|
||||
profile["_agents_config_path"] = agents_cfg.get("_config_path")
|
||||
return profile
|
||||
raise ValueError(f"agent_id={agent_id!r} não encontrado em config/agents.yaml")
|
||||
|
||||
|
||||
def _build_guardrail_pipeline(*, settings, observer: AgentObserver, guardrails_config: dict[str, Any]) -> GuardrailPipeline:
|
||||
"""Cria o GuardrailPipeline do framework amarrado ao profile do agente.
|
||||
|
||||
O framework local pode ter assinaturas diferentes conforme a versão. Este
|
||||
helper tenta usar config_path/config/profile quando suportado; em versões
|
||||
antigas, instancia o pipeline e anexa a configuração ativa para telemetry e
|
||||
debug, evitando depender apenas do config global.
|
||||
"""
|
||||
base_kwargs: dict[str, Any] = {
|
||||
"observer": observer,
|
||||
"enable_parallel": bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
|
||||
"fail_fast": bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
||||
}
|
||||
config_path = guardrails_config.get("_config_path")
|
||||
|
||||
try:
|
||||
accepted = set(inspect.signature(GuardrailPipeline.__init__).parameters)
|
||||
except Exception:
|
||||
accepted = set()
|
||||
|
||||
kwargs = dict(base_kwargs)
|
||||
if "config_path" in accepted:
|
||||
kwargs["config_path"] = config_path
|
||||
if "config_file" in accepted:
|
||||
kwargs["config_file"] = config_path
|
||||
if "config" in accepted:
|
||||
kwargs["config"] = guardrails_config
|
||||
if "profile" in accepted:
|
||||
kwargs["profile"] = guardrails_config.get("profile") or guardrails_config.get("agent_id")
|
||||
if "agent_id" in accepted:
|
||||
kwargs["agent_id"] = guardrails_config.get("agent_id")
|
||||
|
||||
try:
|
||||
pipeline = GuardrailPipeline(**kwargs)
|
||||
except TypeError:
|
||||
pipeline = GuardrailPipeline(**base_kwargs)
|
||||
|
||||
for method_name in ("load_config", "configure", "set_config", "with_config"):
|
||||
method = getattr(pipeline, method_name, None)
|
||||
if callable(method):
|
||||
try:
|
||||
method(guardrails_config)
|
||||
break
|
||||
except TypeError:
|
||||
try:
|
||||
method(config_path)
|
||||
break
|
||||
except TypeError:
|
||||
continue
|
||||
|
||||
setattr(pipeline, "active_agent_id", guardrails_config.get("agent_id"))
|
||||
setattr(pipeline, "active_profile", guardrails_config.get("profile"))
|
||||
setattr(pipeline, "active_config_path", config_path)
|
||||
setattr(pipeline, "active_config", guardrails_config)
|
||||
return pipeline
|
||||
|
||||
|
||||
class NativeOutputGuardrailRail:
|
||||
code = "NATIVE_OUTPUT_GUARDRAILS"
|
||||
|
||||
def __init__(self, pipeline: GuardrailPipeline):
|
||||
self.pipeline = pipeline
|
||||
|
||||
async def evaluate(self, candidate: str, context: dict[str, Any]):
|
||||
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={"native_decisions": serialized},
|
||||
)
|
||||
if final != candidate:
|
||||
return RailResult(
|
||||
code=self.code,
|
||||
action=RailAction.SANITIZE,
|
||||
reason="Resposta sanitizada por guardrail de saída.",
|
||||
sanitized_text=final,
|
||||
metadata={"native_decisions": serialized},
|
||||
)
|
||||
return RailResult(
|
||||
code=self.code,
|
||||
action=RailAction.ALLOW,
|
||||
reason="Resposta aprovada pelos guardrails de saída.",
|
||||
sanitized_text=final,
|
||||
metadata={"native_decisions": serialized},
|
||||
)
|
||||
|
||||
|
||||
class BackofficeNativeRuntime:
|
||||
"""Framework-owned workflow runtime for the backoffice domain."""
|
||||
|
||||
def __init__(self, *, settings, telemetry, analytics, observer: AgentObserver | None = None):
|
||||
self.settings = settings
|
||||
self.telemetry = telemetry
|
||||
self.analytics = analytics
|
||||
self.observer = observer or AgentObserver(analytics=analytics)
|
||||
self.agent_profile = _resolve_agent_profile("backoffice_anatel")
|
||||
self.guardrails_config = _load_yaml(self.agent_profile["guardrails_config_path"])
|
||||
self.guardrails = _build_guardrail_pipeline(
|
||||
settings=settings,
|
||||
observer=self.observer,
|
||||
guardrails_config=self.guardrails_config,
|
||||
)
|
||||
logger.info(
|
||||
"Backoffice guardrails bound to framework profile agent_id=%s profile=%s config_path=%s input=%s output=%s",
|
||||
self.guardrails_config.get("agent_id"),
|
||||
self.guardrails_config.get("profile"),
|
||||
self.guardrails_config.get("_config_path"),
|
||||
[r.get("code") for r in self.guardrails_config.get("input", [])],
|
||||
[r.get("code") for r in self.guardrails_config.get("output", [])],
|
||||
)
|
||||
self.output_supervisor_engine = OutputSupervisor(
|
||||
rails=[NativeOutputGuardrailRail(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._graphs: dict[str, Any] = {}
|
||||
|
||||
def _base_event_payload(self, state: AgentState | None = None, **extra: Any) -> dict[str, Any]:
|
||||
state = state or {}
|
||||
metadata = state.get("metadata", {}) or {}
|
||||
payload = {
|
||||
"workflow_id": metadata.get("framework_workflow_id"),
|
||||
"session_id": state.get("session_id") or metadata.get("session_id") or metadata.get("transaction_id"),
|
||||
"transaction_id": metadata.get("transaction_id"),
|
||||
"agent_id": metadata.get("agent_id") or self.guardrails_config.get("agent_id"),
|
||||
"guardrails_profile": metadata.get("guardrails_profile") or self.guardrails_config.get("profile"),
|
||||
"framework_native": True,
|
||||
}
|
||||
payload.update({k: v for k, v in extra.items() if v is not None})
|
||||
return payload
|
||||
|
||||
async def _safe_emit_ic(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
|
||||
try:
|
||||
await self.observer.emit_ic(code, self._base_event_payload(state, **(payload or {})), component=component)
|
||||
except Exception as exc:
|
||||
logger.debug("Falha ao emitir IC %s: %s", code, exc)
|
||||
|
||||
async def _safe_emit_noc(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
|
||||
try:
|
||||
normalized = code if str(code).startswith("NOC.") else f"NOC.{str(code).zfill(3)}"
|
||||
await self.observer.emit_noc(normalized, self._base_event_payload(state, **(payload or {})), component=component)
|
||||
except Exception as exc:
|
||||
logger.debug("Falha ao emitir NOC %s: %s", code, exc)
|
||||
|
||||
async def _safe_emit_grl(self, code: str, state: AgentState | None = None, payload: dict[str, Any] | None = None, *, component: str) -> None:
|
||||
try:
|
||||
normalized = code if str(code).startswith("GRL.") else f"GRL.{str(code).zfill(3)}"
|
||||
await self.observer.emit_grl(normalized, self._base_event_payload(state, **(payload or {})), component=component)
|
||||
except Exception as exc:
|
||||
logger.debug("Falha ao emitir GRL %s: %s", code, exc)
|
||||
|
||||
async def _emit_by_code(self, code: str, state: AgentState, payload: dict[str, Any] | None, *, component: str) -> None:
|
||||
code = str(code or "IC.UNKNOWN")
|
||||
if code.startswith("NOC."):
|
||||
await self._safe_emit_noc(code, state, payload, component=component)
|
||||
elif code.startswith("GRL."):
|
||||
await self._safe_emit_grl(code, state, payload, component=component)
|
||||
else:
|
||||
await self._safe_emit_ic(code, state, payload, component=component)
|
||||
|
||||
async def _bridge_legacy_ics(self, state: AgentState, node_name: str) -> None:
|
||||
"""Reemite IC/NOC/GRL legados do develop pelo AgentObserver do framework.
|
||||
|
||||
Os nós originais ainda chamam ``agent_framework.observer.event(...)``.
|
||||
O coletor legado captura esses eventos; esta ponte pega apenas os novos
|
||||
eventos desde a última etapa e os publica de forma padronizada no
|
||||
observer do framework, preservando AGA.*, NOC.* e GRL.* no Langfuse/OCI.
|
||||
"""
|
||||
try:
|
||||
from src.utils.ics_collector import ICsCollector
|
||||
except Exception:
|
||||
return
|
||||
metadata = state.setdefault("metadata", {})
|
||||
session_id = state.get("session_id") or metadata.get("transaction_id")
|
||||
try:
|
||||
events = ICsCollector.get_current(session_id) if session_id else []
|
||||
except Exception:
|
||||
events = []
|
||||
last_idx = int(metadata.get("_framework_ics_bridge_index", 0) or 0)
|
||||
new_events = events[last_idx:]
|
||||
if not new_events:
|
||||
return
|
||||
metadata["_framework_ics_bridge_index"] = len(events)
|
||||
bridge_log = metadata.setdefault("framework_ics_bridge", [])
|
||||
for item in new_events:
|
||||
code = str(item.get("code") or "IC.UNKNOWN")
|
||||
event_payload = dict(item.get("metadata") or {})
|
||||
event_payload.update({
|
||||
"legacy_bridge": True,
|
||||
"legacy_type": item.get("type"),
|
||||
"legacy_description": item.get("description"),
|
||||
"legacy_timestamp": item.get("timestamp"),
|
||||
"source_node": node_name,
|
||||
})
|
||||
await self._emit_by_code(code, state, event_payload, component=f"backoffice.native_runtime.legacy_bridge.{node_name}")
|
||||
bridge_log.append({"code": code, "type": item.get("type"), "node": node_name})
|
||||
|
||||
def _node(self, name: str, fn: Callable[[AgentState], Any]):
|
||||
async def _wrapped(state: AgentState) -> AgentState:
|
||||
async with self.langgraph_telemetry.node(name, state):
|
||||
await self.telemetry.event(
|
||||
"backoffice.workflow.node.started",
|
||||
{"workflow_id": state.get("metadata", {}).get("framework_workflow_id"), "node": name, "session_id": state.get("session_id")},
|
||||
kind="workflow",
|
||||
)
|
||||
await self._safe_emit_ic(
|
||||
"IC.BACKOFFICE_NODE_STARTED",
|
||||
state,
|
||||
{"node": name},
|
||||
component=f"backoffice.native_runtime.node.{name}",
|
||||
)
|
||||
try:
|
||||
result = await fn(state)
|
||||
except Exception as exc:
|
||||
await self._safe_emit_noc(
|
||||
"NOC.009",
|
||||
state,
|
||||
{"node": name, "type": "ERROR", "error_type": type(exc).__name__, "error": str(exc)},
|
||||
component=f"backoffice.native_runtime.node.{name}",
|
||||
)
|
||||
raise
|
||||
await self._bridge_legacy_ics(result, name)
|
||||
await self.telemetry.event(
|
||||
"backoffice.workflow.node.completed",
|
||||
{"workflow_id": result.get("metadata", {}).get("framework_workflow_id"), "node": name, "session_id": result.get("session_id")},
|
||||
kind="workflow",
|
||||
)
|
||||
await self._safe_emit_ic(
|
||||
"IC.BACKOFFICE_NODE_COMPLETED",
|
||||
result,
|
||||
{"node": name, "has_error": bool(result.get("error"))},
|
||||
component=f"backoffice.native_runtime.node.{name}",
|
||||
)
|
||||
return result
|
||||
return _wrapped
|
||||
|
||||
async def _framework_input_guardrails(self, state: AgentState) -> AgentState:
|
||||
await self._safe_emit_grl(
|
||||
"GRL.001",
|
||||
state,
|
||||
{"phase": "input", "status": "started", "rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)]},
|
||||
component="backoffice.native_runtime.guardrails.input",
|
||||
)
|
||||
payload = state.get("metadata", {}).get("request_context", {})
|
||||
serialized = json.dumps(payload, ensure_ascii=False, default=str)
|
||||
context = {
|
||||
**state.get("metadata", {}),
|
||||
"workflow_id": state.get("metadata", {}).get("framework_workflow_id"),
|
||||
"agent_id": self.guardrails_config.get("agent_id"),
|
||||
"guardrails_profile": self.guardrails_config.get("profile"),
|
||||
"guardrails_config_path": self.guardrails_config.get("_config_path"),
|
||||
"active_input_rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)],
|
||||
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
|
||||
}
|
||||
sanitized, decisions = await self.guardrails.run_input(serialized, context)
|
||||
state.setdefault("metadata", {})["framework_input_guardrails"] = [d.model_dump() for d in decisions]
|
||||
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
|
||||
await self._safe_emit_grl(
|
||||
"GRL.002",
|
||||
state,
|
||||
{
|
||||
"phase": "input",
|
||||
"status": "completed",
|
||||
"decision_count": len(decisions),
|
||||
"blocked": bool(blocked),
|
||||
"codes": [getattr(d, "code", None) for d in decisions],
|
||||
},
|
||||
component="backoffice.native_runtime.guardrails.input",
|
||||
)
|
||||
if blocked:
|
||||
first = blocked[0]
|
||||
state["error"] = {"type": "InputGuardrailBlocked", "message": getattr(first, "reason", "Entrada bloqueada"), "step": "framework_input_guardrails"}
|
||||
state["final_response"] = sanitized or "Entrada bloqueada por política de segurança."
|
||||
await self._safe_emit_grl(
|
||||
"GRL.003",
|
||||
state,
|
||||
{"phase": "input", "status": "blocked", "code": getattr(first, "code", None), "reason": getattr(first, "reason", None)},
|
||||
component="backoffice.native_runtime.guardrails.input",
|
||||
)
|
||||
return state
|
||||
|
||||
@staticmethod
|
||||
def _after_input_guardrails(state: AgentState) -> str:
|
||||
return "blocked" if state.get("error", {}).get("type") == "InputGuardrailBlocked" else "continue"
|
||||
|
||||
async def _framework_output_supervisor(self, state: AgentState) -> AgentState:
|
||||
await self._safe_emit_grl(
|
||||
"GRL.004",
|
||||
state,
|
||||
{"phase": "output_supervisor", "status": "started"},
|
||||
component="backoffice.native_runtime.output_supervisor",
|
||||
)
|
||||
candidate = state.get("final_response") or state.get("response") or str(state.get("metadata", {}).get("request_context", {}).get("transactionId", ""))
|
||||
context = {
|
||||
**state.get("metadata", {}),
|
||||
"session_id": state.get("session_id"),
|
||||
"agent_id": self.guardrails_config.get("agent_id"),
|
||||
"guardrails_profile": self.guardrails_config.get("profile"),
|
||||
"guardrails_config_path": self.guardrails_config.get("_config_path"),
|
||||
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
|
||||
}
|
||||
decision = await self.output_supervisor_engine.evaluate(candidate, context)
|
||||
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
|
||||
final = decision.candidate
|
||||
elif decision.action == RailAction.HANDOVER:
|
||||
final = "Encaminhado para continuidade com especialista."
|
||||
else:
|
||||
final = decision.fallback_message
|
||||
state["final_response"] = final
|
||||
state.setdefault("metadata", {})["framework_output_supervisor"] = {
|
||||
"action": decision.action.value,
|
||||
"approved": decision.approved,
|
||||
"results": [
|
||||
{"code": r.code, "action": r.action.value, "reason": r.reason, "guidance": r.guidance, "metadata": r.metadata}
|
||||
for r in decision.results
|
||||
],
|
||||
}
|
||||
await self._safe_emit_grl(
|
||||
"GRL.005",
|
||||
state,
|
||||
{
|
||||
"phase": "output_supervisor",
|
||||
"status": "completed",
|
||||
"action": decision.action.value,
|
||||
"approved": decision.approved,
|
||||
"result_codes": [r.code for r in decision.results],
|
||||
},
|
||||
component="backoffice.native_runtime.output_supervisor",
|
||||
)
|
||||
if decision.action not in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
|
||||
await self._safe_emit_grl(
|
||||
"GRL.006",
|
||||
state,
|
||||
{"phase": "output_supervisor", "status": "blocked_or_handover", "action": decision.action.value},
|
||||
component="backoffice.native_runtime.output_supervisor",
|
||||
)
|
||||
return state
|
||||
|
||||
async def _framework_output_guardrails(self, state: AgentState) -> AgentState:
|
||||
await self._safe_emit_grl(
|
||||
"GRL.007",
|
||||
state,
|
||||
{"phase": "output", "status": "started", "rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)]},
|
||||
component="backoffice.native_runtime.guardrails.output",
|
||||
)
|
||||
candidate = state.get("final_response") or ""
|
||||
context = {
|
||||
**state.get("metadata", {}),
|
||||
"agent_id": self.guardrails_config.get("agent_id"),
|
||||
"guardrails_profile": self.guardrails_config.get("profile"),
|
||||
"guardrails_config_path": self.guardrails_config.get("_config_path"),
|
||||
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
|
||||
}
|
||||
final, decisions = await self.guardrails.run_output(candidate, context)
|
||||
state["final_response"] = final
|
||||
state.setdefault("metadata", {})["framework_output_guardrails"] = [d.model_dump() for d in decisions]
|
||||
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
|
||||
await self._safe_emit_grl(
|
||||
"GRL.008",
|
||||
state,
|
||||
{
|
||||
"phase": "output",
|
||||
"status": "completed",
|
||||
"decision_count": len(decisions),
|
||||
"blocked": bool(blocked),
|
||||
"sanitized": final != candidate,
|
||||
"codes": [getattr(d, "code", None) for d in decisions],
|
||||
},
|
||||
component="backoffice.native_runtime.guardrails.output",
|
||||
)
|
||||
if blocked or final != candidate:
|
||||
await self._safe_emit_grl(
|
||||
"GRL.009",
|
||||
state,
|
||||
{"phase": "output", "status": "blocked_or_sanitized", "blocked": bool(blocked), "sanitized": final != candidate},
|
||||
component="backoffice.native_runtime.guardrails.output",
|
||||
)
|
||||
return state
|
||||
|
||||
async def _framework_judges(self, state: AgentState) -> AgentState:
|
||||
payload = state.get("metadata", {}).get("request_context", {})
|
||||
question = json.dumps(payload, ensure_ascii=False, default=str)
|
||||
answer = state.get("final_response") or ""
|
||||
results = await self.judges.evaluate_all(question, answer, state.get("metadata", {}))
|
||||
state.setdefault("metadata", {})["framework_judges"] = [r.model_dump() for r in results]
|
||||
return state
|
||||
|
||||
async def _framework_supervisor_review(self, state: AgentState) -> AgentState:
|
||||
answer = state.get("final_response") or ""
|
||||
ok, reviewed = await self.supervisor.review(answer, state.get("metadata", {}))
|
||||
state["final_response"] = reviewed if ok else reviewed
|
||||
state.setdefault("metadata", {})["framework_supervisor_review"] = {"approved": ok}
|
||||
return state
|
||||
|
||||
async def _framework_persist(self, state: AgentState) -> AgentState:
|
||||
await self._safe_emit_ic(
|
||||
"IC.BACKOFFICE_WORKFLOW_COMPLETED",
|
||||
state,
|
||||
{
|
||||
"current_step": str(state.get("current_step")),
|
||||
"has_error": bool(state.get("error")),
|
||||
},
|
||||
component="backoffice.native_runtime.persist",
|
||||
)
|
||||
await self._safe_emit_noc(
|
||||
"NOC.006",
|
||||
state,
|
||||
{
|
||||
"type": "INFO" if not state.get("error") else "FAILURE",
|
||||
"status": "Backoffice workflow completed",
|
||||
"current_step": str(state.get("current_step")),
|
||||
},
|
||||
component="backoffice.native_runtime.persist",
|
||||
)
|
||||
return state
|
||||
|
||||
def _compile(self, workflow_id: str):
|
||||
if workflow_id == "backoffice_checklist":
|
||||
return self._build_checklist_graph()
|
||||
if workflow_id == "backoffice_response_emulator":
|
||||
return self._build_emulator_graph()
|
||||
raise ValueError(f"Unknown workflow_id={workflow_id}")
|
||||
|
||||
def get_graph(self, workflow_id: str):
|
||||
graph = self._graphs.get(workflow_id)
|
||||
if graph is None:
|
||||
graph = self._compile(workflow_id)
|
||||
self._graphs[workflow_id] = graph
|
||||
return graph
|
||||
|
||||
async def execute_workflow(self, workflow_id: str, *, payload: dict[str, Any], transaction_id: str | None = None, app_state=None, metadata: dict[str, Any] | None = None) -> AgentState:
|
||||
transaction_id = transaction_id or payload.get("transactionId") or payload.get("transaction_id") or "backoffice-session"
|
||||
state = create_initial_state(session_id=transaction_id)
|
||||
state["metadata"].update(metadata or {})
|
||||
state["metadata"].update({
|
||||
"transaction_id": transaction_id,
|
||||
"request_context": payload,
|
||||
"framework_workflow_id": workflow_id,
|
||||
"framework_native": True,
|
||||
"agent_id": self.guardrails_config.get("agent_id"),
|
||||
"guardrails_profile": self.guardrails_config.get("profile"),
|
||||
"guardrails_config_path": self.guardrails_config.get("_config_path"),
|
||||
"active_input_rails": [r.get("code") for r in self.guardrails_config.get("input", []) if r.get("enabled", True)],
|
||||
"active_output_rails": [r.get("code") for r in self.guardrails_config.get("output", []) if r.get("enabled", True)],
|
||||
"_oci_producer": getattr(app_state, "oci_producer", None) if app_state is not None else None,
|
||||
"_framework_ics_bridge_index": 0,
|
||||
})
|
||||
try:
|
||||
from src.utils.ics_collector import ICsCollector
|
||||
ICsCollector.start(transaction_id)
|
||||
except Exception:
|
||||
pass
|
||||
await self._safe_emit_noc(
|
||||
"NOC.001",
|
||||
state,
|
||||
{"type": "INFO", "status": "Backoffice workflow started"},
|
||||
component="backoffice.native_runtime.execute",
|
||||
)
|
||||
await self._safe_emit_ic(
|
||||
"IC.BACKOFFICE_WORKFLOW_STARTED",
|
||||
state,
|
||||
{"payload_keys": sorted(list(payload.keys()))},
|
||||
component="backoffice.native_runtime.execute",
|
||||
)
|
||||
graph = self.get_graph(workflow_id)
|
||||
config = {"configurable": {"thread_id": f"{workflow_id}:{transaction_id}"}}
|
||||
try:
|
||||
final_state = await graph.ainvoke(state, config=config)
|
||||
await self._bridge_legacy_ics(final_state, "workflow_end")
|
||||
return final_state
|
||||
except Exception as exc:
|
||||
await self._safe_emit_noc(
|
||||
"NOC.009",
|
||||
state,
|
||||
{"type": "ERROR", "status": "Backoffice workflow failed", "error_type": type(exc).__name__, "error": str(exc)},
|
||||
component="backoffice.native_runtime.execute",
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
try:
|
||||
final_ref = locals().get("final_state", state)
|
||||
final_ref.get("metadata", {}).pop("_oci_producer", None)
|
||||
final_ref.get("metadata", {}).pop("_framework_ics_bridge_index", None)
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
from src.utils.ics_collector import ICsCollector
|
||||
ICsCollector.stop(transaction_id)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# -------------------- Checklist workflow --------------------
|
||||
def _build_checklist_graph(self):
|
||||
builder = StateGraph(AgentState)
|
||||
builder.add_node("framework_input_guardrails", self._node("framework_input_guardrails", self._framework_input_guardrails))
|
||||
builder.add_node("fetch_ticket", self._node("fetch_ticket", self._fetch_ticket))
|
||||
builder.add_node(GraphStep.VALIDATION, self._node(str(GraphStep.VALIDATION), checklist_nodes.validation_node.validate_ticket))
|
||||
builder.add_node(GraphStep.BYPASS_RULES, self._node(str(GraphStep.BYPASS_RULES), checklist_nodes.bypass_rules_node.evaluate_bypass_rules))
|
||||
builder.add_node(GraphStep.CACHE_CHECK, self._node(str(GraphStep.CACHE_CHECK), checklist_nodes.cache_check_node.check_cache_node))
|
||||
builder.add_node(GraphStep.IMDB_ENRICHMENT, self._node(str(GraphStep.IMDB_ENRICHMENT), checklist_nodes.imdb_enrichment_node.imdb_enrich_ticket))
|
||||
builder.add_node(GraphStep.IDENTITY_VERIFICATION, self._node(str(GraphStep.IDENTITY_VERIFICATION), checklist_nodes.identity_verification_node.perform_identity_verification))
|
||||
builder.add_node(GraphStep.SPEECH_ENRICHMENT, self._node(str(GraphStep.SPEECH_ENRICHMENT), checklist_nodes.speech_enrichment_node.enrich_with_speech))
|
||||
builder.add_node("knowledge_base_enrichment", self._node("knowledge_base_enrichment", checklist_nodes.knowledge_base_enrichment_node.enrich_with_knowledge_base))
|
||||
builder.add_node(GraphStep.CANCELING_ANALYSIS, self._node(str(GraphStep.CANCELING_ANALYSIS), checklist_nodes.canceling_analysis_node.perform_canceling_analysis))
|
||||
builder.add_node(GraphStep.TIM_COMPLAINT_ANALYSIS, self._node(str(GraphStep.TIM_COMPLAINT_ANALYSIS), checklist_nodes.tim_complaint_analysis_node.perform_tim_complaint_analysis))
|
||||
builder.add_node("different_complaint_operator", self._node("different_complaint_operator", checklist_nodes.different_complaint_operator_node.perform_different_operator))
|
||||
builder.add_node("undefined_complaint_operator", self._node("undefined_complaint_operator", checklist_nodes.undefined_complaint_operator_node.perform_undefined_complaint))
|
||||
builder.add_node("tim_complaint", self._node("tim_complaint", checklist_nodes.tim_complaint_node.handle_tim_complaint))
|
||||
builder.add_node(GraphStep.RECLASSIFICATION_ANALYSIS, self._node(str(GraphStep.RECLASSIFICATION_ANALYSIS), checklist_nodes.reclassification_analysis_node.perform_reclassification_analysis))
|
||||
builder.add_node(GraphStep.TREATMENT_DECISION, self._node(str(GraphStep.TREATMENT_DECISION), checklist_nodes.treatment_decision_node.treatment_decision))
|
||||
builder.add_node(GraphStep.SIEBEL_SR_OPENING, self._node(str(GraphStep.SIEBEL_SR_OPENING), checklist_nodes.siebel_sr_opening_node.open_siebel_sr))
|
||||
builder.add_node("framework_output_supervisor", self._node("framework_output_supervisor", self._framework_output_supervisor))
|
||||
builder.add_node("framework_output_guardrails", self._node("framework_output_guardrails", self._framework_output_guardrails))
|
||||
builder.add_node("framework_judges", self._node("framework_judges", self._framework_judges))
|
||||
builder.add_node("framework_supervisor_review", self._node("framework_supervisor_review", self._framework_supervisor_review))
|
||||
builder.add_node("framework_persist", self._node("framework_persist", self._framework_persist))
|
||||
|
||||
builder.add_edge(START, "framework_input_guardrails")
|
||||
builder.add_conditional_edges("framework_input_guardrails", self._after_input_guardrails, {"blocked": "framework_persist", "continue": "fetch_ticket"})
|
||||
builder.add_edge("fetch_ticket", GraphStep.VALIDATION)
|
||||
builder.add_conditional_edges(GraphStep.VALIDATION, checklist_nodes.validation_node.should_continue, {"continue": GraphStep.BYPASS_RULES, "reject": "framework_output_supervisor"})
|
||||
builder.add_edge(GraphStep.BYPASS_RULES, GraphStep.CACHE_CHECK)
|
||||
builder.add_conditional_edges(GraphStep.CACHE_CHECK, self._route_after_cache_check, {GraphStep.TREATMENT_DECISION: GraphStep.TREATMENT_DECISION, GraphStep.CANCELING_ANALYSIS: GraphStep.CANCELING_ANALYSIS, GraphStep.IMDB_ENRICHMENT: GraphStep.IMDB_ENRICHMENT})
|
||||
builder.add_conditional_edges(GraphStep.IMDB_ENRICHMENT, checklist_nodes.imdb_enrichment_node.should_continue, {"continue": GraphStep.IDENTITY_VERIFICATION, "failed": "framework_output_supervisor"})
|
||||
builder.add_conditional_edges(GraphStep.IDENTITY_VERIFICATION, checklist_nodes.identity_verification_node.route_after_identity_verification, {"proceed": GraphStep.SPEECH_ENRICHMENT, "cancel": GraphStep.SIEBEL_SR_OPENING, "smart_human": GraphStep.TREATMENT_DECISION, "failed": "framework_output_supervisor"})
|
||||
builder.add_edge(GraphStep.SPEECH_ENRICHMENT, "knowledge_base_enrichment")
|
||||
builder.add_conditional_edges("knowledge_base_enrichment", self._route_after_knowledge_base, {GraphStep.CANCELING_ANALYSIS: GraphStep.CANCELING_ANALYSIS, GraphStep.TREATMENT_DECISION: GraphStep.TREATMENT_DECISION})
|
||||
builder.add_conditional_edges(GraphStep.CANCELING_ANALYSIS, self._route_after_canceling, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.TIM_COMPLAINT_ANALYSIS, "finalize": "framework_output_supervisor"})
|
||||
builder.add_conditional_edges(GraphStep.TIM_COMPLAINT_ANALYSIS, self._route_after_tim_complaint_analysis, {"tim_complaint": "tim_complaint", "different_complaint_operator": "different_complaint_operator", "undefined_complaint_operator": "undefined_complaint_operator", "finalize": "framework_output_supervisor"})
|
||||
builder.add_edge("tim_complaint", GraphStep.RECLASSIFICATION_ANALYSIS)
|
||||
builder.add_conditional_edges("different_complaint_operator", self._route_after_operator_check, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.RECLASSIFICATION_ANALYSIS})
|
||||
builder.add_conditional_edges("undefined_complaint_operator", self._route_after_operator_check, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.RECLASSIFICATION_ANALYSIS})
|
||||
builder.add_conditional_edges(GraphStep.RECLASSIFICATION_ANALYSIS, self._route_after_reclassification, {GraphStep.SIEBEL_SR_OPENING: GraphStep.SIEBEL_SR_OPENING, GraphStep.PROCEED_GRAPH: GraphStep.TREATMENT_DECISION, "finalize": "framework_output_supervisor"})
|
||||
builder.add_edge(GraphStep.TREATMENT_DECISION, GraphStep.SIEBEL_SR_OPENING)
|
||||
builder.add_edge(GraphStep.SIEBEL_SR_OPENING, "framework_output_supervisor")
|
||||
builder.add_edge("framework_output_supervisor", "framework_output_guardrails")
|
||||
builder.add_edge("framework_output_guardrails", "framework_judges")
|
||||
builder.add_edge("framework_judges", "framework_supervisor_review")
|
||||
builder.add_edge("framework_supervisor_review", "framework_persist")
|
||||
builder.add_edge("framework_persist", END)
|
||||
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
|
||||
|
||||
async def _fetch_ticket(self, state: AgentState) -> AgentState:
|
||||
increment_iteration(state)
|
||||
return await checklist_nodes.fetch_ticket_node.fetch_ticket_data(state)
|
||||
|
||||
@staticmethod
|
||||
def _route_after_cache_check(state: AgentState) -> str:
|
||||
if state.get("cache_found") is True:
|
||||
if state.get("bypass_treatment_validations"):
|
||||
return GraphStep.TREATMENT_DECISION
|
||||
return GraphStep.CANCELING_ANALYSIS
|
||||
return GraphStep.IMDB_ENRICHMENT
|
||||
|
||||
@staticmethod
|
||||
def _route_after_knowledge_base(state: AgentState) -> str:
|
||||
return GraphStep.TREATMENT_DECISION if state.get("bypass_treatment_validations") else GraphStep.CANCELING_ANALYSIS
|
||||
|
||||
@staticmethod
|
||||
def _route_after_canceling(state: AgentState) -> str:
|
||||
step = state.get("current_step")
|
||||
if step == GraphStep.CANCELING_ANALYSIS_CANCEL_TICKET:
|
||||
return GraphStep.SIEBEL_SR_OPENING
|
||||
if step == GraphStep.PROCEED_GRAPH:
|
||||
return GraphStep.PROCEED_GRAPH
|
||||
return "finalize"
|
||||
|
||||
@staticmethod
|
||||
def _route_after_tim_complaint_analysis(state: AgentState) -> str:
|
||||
decision = state.get("metadata", {}).get("request_context", {}).get("is_tim_complaint", "")
|
||||
if decision == "sim":
|
||||
return "tim_complaint"
|
||||
if decision == "não":
|
||||
return "different_complaint_operator"
|
||||
if decision == "inconclusivo":
|
||||
return "undefined_complaint_operator"
|
||||
return "finalize"
|
||||
|
||||
@staticmethod
|
||||
def _route_after_operator_check(state: AgentState) -> str:
|
||||
context = state.get("metadata", {}).get("request_context", {})
|
||||
if context.get("forward_complaint"):
|
||||
return GraphStep.SIEBEL_SR_OPENING
|
||||
return GraphStep.PROCEED_GRAPH
|
||||
|
||||
@staticmethod
|
||||
def _route_after_reclassification(state: AgentState) -> str:
|
||||
step = state.get("current_step")
|
||||
if step == GraphStep.RECLASSIFICATION_ANALYSIS_COMPLETED:
|
||||
context = state.get("metadata", {}).get("request_context", {})
|
||||
if context.get("siebel_action") == "reclassificar":
|
||||
return GraphStep.SIEBEL_SR_OPENING
|
||||
return GraphStep.PROCEED_GRAPH
|
||||
return "finalize"
|
||||
|
||||
# -------------------- Emulator workflow --------------------
|
||||
def _build_emulator_graph(self):
|
||||
builder = StateGraph(AgentState)
|
||||
builder.add_node("framework_input_guardrails", self._node("framework_input_guardrails", self._framework_input_guardrails))
|
||||
builder.add_node(EmulatorGraphStep.RESPONSE_EMULATION_START, self._node(str(EmulatorGraphStep.RESPONSE_EMULATION_START), start_response_emulation_node.start_response_emulation))
|
||||
builder.add_node(EmulatorGraphStep.FETCH_CASE, self._node(str(EmulatorGraphStep.FETCH_CASE), fetch_case_node.fetch_case))
|
||||
builder.add_node(EmulatorGraphStep.VALIDATE_ACTIONS, self._node(str(EmulatorGraphStep.VALIDATE_ACTIONS), validate_actions_node.validate_actions))
|
||||
builder.add_node(EmulatorGraphStep.ROUTER_DECISION, self._node(str(EmulatorGraphStep.ROUTER_DECISION), router_node.route))
|
||||
builder.add_node(EmulatorGraphStep.RETRIEVE_TEMPLATES, self._node(str(EmulatorGraphStep.RETRIEVE_TEMPLATES), retrieve_templates_node.retrieve_templates))
|
||||
builder.add_node(EmulatorGraphStep.RETRIEVE_HISTORY, self._node(str(EmulatorGraphStep.RETRIEVE_HISTORY), retrieve_history_node.retrieve_history))
|
||||
builder.add_node(EmulatorGraphStep.GENERATE_RESPONSE, self._node(str(EmulatorGraphStep.GENERATE_RESPONSE), generate_response_node.generate_response))
|
||||
builder.add_node(EmulatorGraphStep.VALIDATE_RESPONSE, self._node(str(EmulatorGraphStep.VALIDATE_RESPONSE), validate_response_node.validate_response))
|
||||
builder.add_node(EmulatorGraphStep.PERSIST_DRAFT, self._node(str(EmulatorGraphStep.PERSIST_DRAFT), persist_draft_node.persist_draft))
|
||||
builder.add_node(EmulatorGraphStep.APPROVE_DRAFT, self._node(str(EmulatorGraphStep.APPROVE_DRAFT), approve_draft_node.approve_draft))
|
||||
builder.add_node(EmulatorGraphStep.CLOSE_CASE, self._node(str(EmulatorGraphStep.CLOSE_CASE), close_case_node.close_case))
|
||||
builder.add_node("framework_output_supervisor", self._node("framework_output_supervisor", self._framework_output_supervisor))
|
||||
builder.add_node("framework_output_guardrails", self._node("framework_output_guardrails", self._framework_output_guardrails))
|
||||
builder.add_node("framework_judges", self._node("framework_judges", self._framework_judges))
|
||||
builder.add_node("framework_supervisor_review", self._node("framework_supervisor_review", self._framework_supervisor_review))
|
||||
builder.add_node("framework_persist", self._node("framework_persist", self._framework_persist))
|
||||
|
||||
builder.add_edge(START, "framework_input_guardrails")
|
||||
builder.add_conditional_edges("framework_input_guardrails", self._after_input_guardrails, {"blocked": "framework_persist", "continue": EmulatorGraphStep.RESPONSE_EMULATION_START})
|
||||
builder.add_edge(EmulatorGraphStep.RESPONSE_EMULATION_START, EmulatorGraphStep.FETCH_CASE)
|
||||
builder.add_conditional_edges(EmulatorGraphStep.FETCH_CASE, self._emulator_route_after_fetch, {"generate": EmulatorGraphStep.VALIDATE_ACTIONS, "approve": EmulatorGraphStep.APPROVE_DRAFT, "close": EmulatorGraphStep.CLOSE_CASE, "failed": "framework_output_supervisor"})
|
||||
builder.add_conditional_edges(EmulatorGraphStep.VALIDATE_ACTIONS, validate_actions_node.should_continue, {"continue": EmulatorGraphStep.ROUTER_DECISION, "failed": "framework_output_supervisor"})
|
||||
builder.add_conditional_edges(EmulatorGraphStep.ROUTER_DECISION, router_node.next_step_after_router, {EmulatorGraphStep.RETRIEVE_TEMPLATES: EmulatorGraphStep.RETRIEVE_TEMPLATES, EmulatorGraphStep.RETRIEVE_HISTORY: EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE: EmulatorGraphStep.GENERATE_RESPONSE})
|
||||
builder.add_conditional_edges(EmulatorGraphStep.RETRIEVE_TEMPLATES, router_node.next_step_after_templates, {EmulatorGraphStep.RETRIEVE_HISTORY: EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE: EmulatorGraphStep.GENERATE_RESPONSE})
|
||||
builder.add_edge(EmulatorGraphStep.RETRIEVE_HISTORY, EmulatorGraphStep.GENERATE_RESPONSE)
|
||||
builder.add_conditional_edges(EmulatorGraphStep.GENERATE_RESPONSE, generate_response_node.should_continue, {"continue": EmulatorGraphStep.VALIDATE_RESPONSE, "failed": "framework_output_supervisor"})
|
||||
builder.add_edge(EmulatorGraphStep.VALIDATE_RESPONSE, EmulatorGraphStep.PERSIST_DRAFT)
|
||||
builder.add_edge(EmulatorGraphStep.PERSIST_DRAFT, "framework_output_supervisor")
|
||||
builder.add_edge(EmulatorGraphStep.APPROVE_DRAFT, "framework_output_supervisor")
|
||||
builder.add_edge(EmulatorGraphStep.CLOSE_CASE, "framework_output_supervisor")
|
||||
builder.add_edge("framework_output_supervisor", "framework_output_guardrails")
|
||||
builder.add_edge("framework_output_guardrails", "framework_judges")
|
||||
builder.add_edge("framework_judges", "framework_supervisor_review")
|
||||
builder.add_edge("framework_supervisor_review", "framework_persist")
|
||||
builder.add_edge("framework_persist", END)
|
||||
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
|
||||
|
||||
@staticmethod
|
||||
def _emulator_route_after_fetch(state: AgentState) -> str:
|
||||
if state.get("error"):
|
||||
return "failed"
|
||||
flow_mode = (state.get("metadata") or {}).get("flow_mode")
|
||||
if flow_mode == "close":
|
||||
return "close"
|
||||
if flow_mode == "approve":
|
||||
return "approve"
|
||||
return "generate"
|
||||
Reference in New Issue
Block a user