"""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 Backoffice executor assembles the domain workflows and delegates execution to the framework-managed LangGraph/checkpoint/telemetry pipeline; * telemetry, guardrails, judges, supervisor, checkpoint and persistence hooks remain framework-owned; * BackofficeWorkflowDispatcher calls ``execute_workflow`` after REST payloads are normalized by the ChannelGateway path. """ 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.workflow_executor") 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 BackofficeWorkflowExecutor: """Domain executor for Backoffice LangGraph workflows. This class does not replace the framework LangGraph runtime. It owns the Backoffice-specific workflow assembly and delegates execution to the framework-managed LangGraph/checkpoint/telemetry pipeline. It is not a FastAPI entrypoint. REST routes must enter through ``BackofficeRestChannelAdapter`` and ``BackofficeWorkflowDispatcher`` so the Backoffice behaves like a corporate channel before the workflow is invoked. """ 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 | None, 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. Alguns nós legados mutam o state recebido e retornam None. O wrapper normaliza esse comportamento, mas esta ponte também é defensiva para não derrubar o grafo por causa da emissão de eventos legados. """ if not isinstance(state, dict): logger.debug("Skipping legacy IC bridge for node=%s because state is %s", node_name, type(state).__name__) return 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.workflow_executor.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.workflow_executor.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.workflow_executor.node.{name}", ) raise # Alguns nós do projeto original seguem o padrão "mutate in place" # e retornam None. LangGraph precisa receber um state válido para # continuar; nesses casos preservamos o mesmo objeto de entrada. if result is None: logger.debug("Node %s returned None; preserving mutated input state", name) result = state elif not isinstance(result, dict): raise TypeError(f"Backoffice workflow node {name} returned unsupported type: {type(result).__name__}") result.setdefault("metadata", {}) 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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.guardrails.input", ) return state @staticmethod def _after_input_guardrails(state): error = state.get("error") or {} return "blocked" if 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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.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.workflow_executor.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)], # Progress producer is kept in src.compat.framework_services, not in state, # so checkpoints remain JSON-stable and integrity hashes do not change. "_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.workflow_executor.execute", ) await self._safe_emit_ic( "IC.BACKOFFICE_WORKFLOW_STARTED", state, {"payload_keys": sorted(list(payload.keys()))}, component="backoffice.workflow_executor.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.workflow_executor.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"