mirror of
https://github.com/hoshikawa2/compass_backoffice.git
synced 2026-07-09 13:54:20 +00:00
bugfixes
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -94,7 +94,7 @@ class BackofficeAgent(AgentRuntimeMixin):
|
||||
"BACKOFFICE_TICKET_CONTEXT_DETECTED",
|
||||
normalized_state,
|
||||
{
|
||||
"status": "Contexto de ticket detectado; execução checklist deve ocorrer pelo BackofficeNativeRuntime nas rotas /agent/*",
|
||||
"status": "Contexto de ticket detectado; execução operacional deve entrar como canal backoffice_rest via ChannelGateway/dispatcher",
|
||||
"framework_native": True,
|
||||
"reason": "o agente conversacional não compila nem executa grafos de domínio",
|
||||
},
|
||||
@@ -383,14 +383,14 @@ class BackofficeAgent(AgentRuntimeMixin):
|
||||
|
||||
A migração framework-native não permite que o agente conversacional
|
||||
compile/execute ``src.agent.graphs`` diretamente. Os fluxos checklist e
|
||||
response emulator são executados pelo BackofficeNativeRuntime chamado
|
||||
pelas rotas/adapters do backend.
|
||||
response emulator são executados pelo dispatcher de workflows após
|
||||
normalização pelo canal backoffice_rest/ChannelGateway.
|
||||
"""
|
||||
return {
|
||||
"executed": False,
|
||||
"error": {
|
||||
"type": "DeprecatedDirectGraphExecution",
|
||||
"message": "Use BackofficeNativeRuntime.execute_workflow('backoffice_checklist')",
|
||||
"message": "Use BackofficeRestChannelAdapter + BackofficeWorkflowDispatcher",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
10
app/channels/__init__.py
Normal file
10
app/channels/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Channel adapters owned by the application domain.
|
||||
|
||||
The framework ChannelGateway remains the canonical normalization boundary.
|
||||
Domain REST contracts should enter the backend through adapters in this package
|
||||
instead of calling workflow runtimes directly from FastAPI routes.
|
||||
"""
|
||||
|
||||
from .backoffice_rest_adapter import BackofficeChannelEnvelope, BackofficeRestChannelAdapter
|
||||
|
||||
__all__ = ["BackofficeChannelEnvelope", "BackofficeRestChannelAdapter"]
|
||||
BIN
app/channels/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
app/channels/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
app/channels/__pycache__/backoffice_rest_adapter.cpython-313.pyc
Normal file
BIN
app/channels/__pycache__/backoffice_rest_adapter.cpython-313.pyc
Normal file
Binary file not shown.
192
app/channels/backoffice_rest_adapter.py
Normal file
192
app/channels/backoffice_rest_adapter.py
Normal file
@@ -0,0 +1,192 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
class BackofficeChannelEnvelope:
|
||||
"""Canonical channel envelope for Backoffice REST requests.
|
||||
|
||||
REST remains only a transport/channel contract. This envelope carries the
|
||||
normalized information required by the framework layers before a workflow is
|
||||
dispatched. The domain payload is preserved under ``payload`` so existing
|
||||
business nodes can continue to read the original TIM/ANATEL contract from
|
||||
``state["metadata"]["request_context"]``.
|
||||
"""
|
||||
|
||||
workflow_id: str
|
||||
payload: dict[str, Any]
|
||||
transaction_id: str
|
||||
channel: str = "backoffice_rest"
|
||||
tenant_id: str = "default"
|
||||
agent_id: str = "backoffice_anatel"
|
||||
user_id: str | None = None
|
||||
message_id: str = field(default_factory=lambda: str(uuid4()))
|
||||
text: str = ""
|
||||
business_context: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def gateway_payload(self) -> dict[str, Any]:
|
||||
"""Payload sent to the framework ChannelGateway.
|
||||
|
||||
The web adapter shape is intentionally also populated as a compatibility
|
||||
fallback for framework versions that do not yet register a dedicated
|
||||
``backoffice_rest`` adapter.
|
||||
"""
|
||||
return {
|
||||
"channel": self.channel,
|
||||
"message": self.text,
|
||||
"text": self.text,
|
||||
"session_id": self.transaction_id,
|
||||
"conversation_key": f"{self.tenant_id}:{self.agent_id}:{self.transaction_id}",
|
||||
"message_id": self.message_id,
|
||||
"user_id": self.user_id or self.business_context.get("customer_key") or self.transaction_id,
|
||||
"tenant_id": self.tenant_id,
|
||||
"agent_id": self.agent_id,
|
||||
"business_context": self.business_context,
|
||||
"metadata": {
|
||||
**self.metadata,
|
||||
"workflow_id": self.workflow_id,
|
||||
"transaction_id": self.transaction_id,
|
||||
"source_channel": self.channel,
|
||||
"request_context": self.payload,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class BackofficeRestChannelAdapter:
|
||||
"""Adapter that turns legacy Backoffice REST contracts into a framework channel.
|
||||
|
||||
This class deliberately contains translation only. It does not run LangGraph,
|
||||
does not know node order and does not call domain services. Execution is
|
||||
delegated to the framework workflow dispatcher after channel normalization.
|
||||
"""
|
||||
|
||||
channel_name = "backoffice_rest"
|
||||
agent_id = "backoffice_anatel"
|
||||
tenant_id = "default"
|
||||
|
||||
def from_ticket_event(self, event: Any, *, legacy_contract: str) -> BackofficeChannelEnvelope:
|
||||
payload = self._to_dict(event)
|
||||
transaction_id = payload.get("transactionId") or payload.get("transaction_id") or f"man-{uuid4().hex[:8]}"
|
||||
complaint = payload.get("complaint") or {}
|
||||
customer = payload.get("customer") or {}
|
||||
text = self._first_non_empty(
|
||||
complaint.get("description"),
|
||||
complaint.get("motive"),
|
||||
payload.get("description"),
|
||||
f"Processar chamado backoffice {transaction_id}",
|
||||
)
|
||||
business_context = self._business_context(payload, transaction_id)
|
||||
return BackofficeChannelEnvelope(
|
||||
workflow_id="backoffice_checklist",
|
||||
payload=payload,
|
||||
transaction_id=transaction_id,
|
||||
channel=self.channel_name,
|
||||
tenant_id=self.tenant_id,
|
||||
agent_id=self.agent_id,
|
||||
user_id=customer.get("cpfCnpj") or customer.get("msisdn") or business_context.get("customer_key"),
|
||||
text=text,
|
||||
business_context=business_context,
|
||||
metadata={
|
||||
"legacy_contract": legacy_contract,
|
||||
"case_type": payload.get("caseType"),
|
||||
"complaint_protocol": complaint.get("complaintProtocol"),
|
||||
"crm_protocol": payload.get("crmProtocol"),
|
||||
"adapter": self.__class__.__name__,
|
||||
},
|
||||
)
|
||||
|
||||
def from_emulator_event(self, event: Any, *, legacy_contract: str) -> BackofficeChannelEnvelope:
|
||||
payload = self._to_dict(event)
|
||||
transaction_id = payload.get("transactionId") or payload.get("transaction_id") or f"emu-{uuid4().hex[:8]}"
|
||||
selected_actions = payload.get("selected_actions") or payload.get("selectedActions") or []
|
||||
flow_mode = payload.get("flow_mode") or payload.get("flowMode") or payload.get("action")
|
||||
text = self._first_non_empty(
|
||||
payload.get("operator_instructions"),
|
||||
payload.get("previous_response"),
|
||||
f"Executar emulador de resposta do caso {transaction_id}",
|
||||
)
|
||||
return BackofficeChannelEnvelope(
|
||||
workflow_id="backoffice_response_emulator",
|
||||
payload=payload,
|
||||
transaction_id=transaction_id,
|
||||
channel=self.channel_name,
|
||||
tenant_id=self.tenant_id,
|
||||
agent_id=self.agent_id,
|
||||
user_id=transaction_id,
|
||||
text=text,
|
||||
business_context={"interaction_key": transaction_id, "session_key": transaction_id},
|
||||
metadata={
|
||||
"legacy_contract": legacy_contract,
|
||||
"flow_mode": flow_mode,
|
||||
"selected_actions": selected_actions,
|
||||
"adapter": self.__class__.__name__,
|
||||
},
|
||||
)
|
||||
|
||||
async def normalize(self, channel_gateway: Any, envelope: BackofficeChannelEnvelope) -> Any:
|
||||
"""Normalize through ChannelGateway, with a compatibility fallback.
|
||||
|
||||
Newer framework versions may support registering ``backoffice_rest`` as a
|
||||
first-class adapter. Older versions only know web/whatsapp/voice; in that
|
||||
case the payload is still passed through the web adapter shape and then
|
||||
tagged back as ``backoffice_rest``.
|
||||
"""
|
||||
payload = envelope.gateway_payload()
|
||||
try:
|
||||
msg = await channel_gateway.normalize(envelope.channel, payload)
|
||||
return msg
|
||||
except Exception:
|
||||
try:
|
||||
msg = await channel_gateway.normalize("web", payload)
|
||||
try:
|
||||
msg.channel = envelope.channel
|
||||
except Exception:
|
||||
pass
|
||||
return msg
|
||||
except Exception:
|
||||
return SimpleNamespace(
|
||||
channel=envelope.channel,
|
||||
channel_id=None,
|
||||
session_id=envelope.transaction_id,
|
||||
user_id=envelope.user_id,
|
||||
text=envelope.text,
|
||||
context=payload,
|
||||
)
|
||||
|
||||
def _business_context(self, payload: dict[str, Any], transaction_id: str) -> dict[str, Any]:
|
||||
customer = payload.get("customer") or {}
|
||||
complaint = payload.get("complaint") or {}
|
||||
cpf = customer.get("cpfCnpj") or (customer.get("subscriber") or {}).get("cpfCnpj")
|
||||
msisdn = customer.get("msisdn") or ((customer.get("phones") or [])[:1] or [None])[0]
|
||||
protocol = complaint.get("complaintProtocol") or payload.get("crmProtocol") or transaction_id
|
||||
return {
|
||||
"customer_key": msisdn or cpf,
|
||||
"contract_key": cpf,
|
||||
"interaction_key": protocol,
|
||||
"session_key": transaction_id,
|
||||
"metadata": {
|
||||
"cpf_cnpj": cpf,
|
||||
"msisdn": msisdn,
|
||||
"transaction_id": transaction_id,
|
||||
},
|
||||
}
|
||||
|
||||
def _to_dict(self, value: Any) -> dict[str, Any]:
|
||||
if isinstance(value, dict):
|
||||
return dict(value)
|
||||
if hasattr(value, "model_dump"):
|
||||
return value.model_dump(mode="json", by_alias=True)
|
||||
if hasattr(value, "dict"):
|
||||
return value.dict(by_alias=True)
|
||||
raise TypeError(f"Unsupported backoffice REST event type: {type(value).__name__}")
|
||||
|
||||
def _first_non_empty(self, *values: Any) -> str:
|
||||
for value in values:
|
||||
if value is not None and str(value).strip():
|
||||
return str(value).strip()
|
||||
return "Backoffice REST request"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
94
app/main.py
94
app/main.py
@@ -32,7 +32,9 @@ from agent_framework.cache.cache import create_cache
|
||||
from agent_framework.billing.usage_repository import create_usage_repository
|
||||
from agent_framework.sse.events import SSEHub
|
||||
from app.workflows.agent_graph import AgentWorkflow
|
||||
from app.workflows.backoffice_native_runtime import BackofficeNativeRuntime
|
||||
from app.workflows.backoffice_workflow_executor import BackofficeWorkflowExecutor
|
||||
from app.channels.backoffice_rest_adapter import BackofficeRestChannelAdapter
|
||||
from app.workflows.backoffice_workflow_dispatcher import BackofficeWorkflowDispatcher
|
||||
from app.identity_extraction import enrich_payload_with_text_identity, extract_identity_from_text
|
||||
|
||||
logging.basicConfig(level=settings.LOG_LEVEL)
|
||||
@@ -69,7 +71,9 @@ identity_resolver = IdentityResolver.from_yaml(settings.IDENTITY_CONFIG_PATH)
|
||||
agent_profiles = AgentProfileRegistry(settings)
|
||||
sse_hub = SSEHub(settings, telemetry=telemetry)
|
||||
workflow = AgentWorkflow(llm, memory, telemetry, analytics, settings, observer=observer, tool_router=tool_router, summary_memory=summary_memory)
|
||||
backoffice_runtime = BackofficeNativeRuntime(settings=settings, telemetry=telemetry, analytics=analytics, observer=observer)
|
||||
backoffice_executor = BackofficeWorkflowExecutor(settings=settings, telemetry=telemetry, analytics=analytics, observer=observer)
|
||||
backoffice_rest_adapter = BackofficeRestChannelAdapter()
|
||||
backoffice_dispatcher = BackofficeWorkflowDispatcher(channel_gateway=gateway, executor=backoffice_executor, telemetry=telemetry, adapter=backoffice_rest_adapter)
|
||||
|
||||
logger.info("LLM provider carregado: %s", llm.__class__.__name__)
|
||||
logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST)
|
||||
@@ -498,12 +502,12 @@ async def shutdown():
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Backoffice TIM/ANATEL develop — execução 100% framework-native
|
||||
# Backoffice TIM/ANATEL develop — REST como canal do framework
|
||||
# ---------------------------------------------------------------------------
|
||||
# As rotas antigas permanecem como adapters REST, mas não registram routers
|
||||
# legados e não executam legacy graph package nem legacy executor package.
|
||||
# Elas chamam o BackofficeNativeRuntime, que compila os workflows com o motor
|
||||
# do framework e aplica guardrails, judges, supervisor, checkpoint e telemetry.
|
||||
# As rotas antigas permanecem como contratos HTTP de compatibilidade, mas agora
|
||||
# atuam apenas como REST adapters. Elas traduzem o payload legado para um
|
||||
# BackofficeChannelEnvelope, passam pelo ChannelGateway e só então o dispatcher
|
||||
# aciona o workflow LangGraph registrado para o domínio.
|
||||
|
||||
from src.api.schemas.anatel_schemas import TicketRequestEvent
|
||||
from src.api.schemas.anatel_response_emulator_schemas import EmulatorGenerateRequest, EmulatorFinalizeRequest
|
||||
@@ -599,29 +603,23 @@ async def native_validation_exception_handler(request: Request, exc: RequestVali
|
||||
return JSONResponse(status_code=422, content={"detail": exc.errors()})
|
||||
|
||||
|
||||
async def _run_native_checklist(event, request: Request):
|
||||
async def _run_native_checklist(event, request: Request, *, legacy_contract: str = "agent.process-ticket"):
|
||||
from src.api.utils import agent_helpers
|
||||
transaction_id = event.transactionId or f"man-{uuid4().hex[:8]}"
|
||||
payload = event.model_dump(mode="json", by_alias=True)
|
||||
final_state = await backoffice_runtime.execute_workflow(
|
||||
"backoffice_checklist",
|
||||
payload=payload,
|
||||
transaction_id=transaction_id,
|
||||
app_state=request.app.state,
|
||||
metadata={
|
||||
"tenant_id": "default",
|
||||
"agent_id": "backoffice_anatel",
|
||||
"channel": "rest",
|
||||
"legacy_contract": "agent.process-ticket",
|
||||
},
|
||||
)
|
||||
envelope = backoffice_rest_adapter.from_ticket_event(event, legacy_contract=legacy_contract)
|
||||
final_state = await backoffice_dispatcher.execute(envelope, app_state=request.app.state)
|
||||
try:
|
||||
response_event = agent_helpers.build_cms_response_event(final_state, transaction_id)
|
||||
return response_event.model_dump(mode="json", by_alias=True)
|
||||
response_event = agent_helpers.build_cms_response_event(final_state, envelope.transaction_id)
|
||||
response = response_event.model_dump(mode="json", by_alias=True)
|
||||
response.setdefault("metadata", {})
|
||||
response["metadata"]["framework_entrypoint"] = "channel_gateway"
|
||||
response["metadata"]["source_channel"] = envelope.channel
|
||||
return response
|
||||
except Exception:
|
||||
return {
|
||||
"transactionId": transaction_id,
|
||||
"transactionId": envelope.transaction_id,
|
||||
"framework_native": True,
|
||||
"framework_entrypoint": "channel_gateway",
|
||||
"source_channel": envelope.channel,
|
||||
"current_step": str(final_state.get("current_step")),
|
||||
"final_response": final_state.get("final_response"),
|
||||
"error": final_state.get("error"),
|
||||
@@ -631,17 +629,17 @@ async def _run_native_checklist(event, request: Request):
|
||||
|
||||
@app.post("/agent/process-ticket", status_code=status.HTTP_200_OK)
|
||||
async def native_process_ticket(request: Request, event: TicketRequestEvent):
|
||||
return await _run_native_checklist(event, request)
|
||||
return await _run_native_checklist(event, request, legacy_contract="agent.process-ticket")
|
||||
|
||||
|
||||
@app.post("/agent/execute", status_code=status.HTTP_200_OK)
|
||||
async def native_agent_execute(request: Request, event: TicketRequestEvent):
|
||||
return await _run_native_checklist(event, request)
|
||||
return await _run_native_checklist(event, request, legacy_contract="agent.execute")
|
||||
|
||||
|
||||
@app.post("/agent/process-and-stream", status_code=status.HTTP_200_OK)
|
||||
async def native_process_and_stream(request: Request, event: TicketRequestEvent):
|
||||
return await _run_native_checklist(event, request)
|
||||
return await _run_native_checklist(event, request, legacy_contract="agent.process-and-stream")
|
||||
|
||||
|
||||
@app.post("/agent/search-tais-kb", status_code=status.HTTP_200_OK)
|
||||
@@ -660,31 +658,23 @@ async def native_search_tais_kb(body: dict):
|
||||
return {"framework_native": True, "result": result}
|
||||
|
||||
|
||||
async def _run_native_emulator(request: Request, event):
|
||||
transaction_id = event.transactionId
|
||||
payload = event.model_dump(mode="json", by_alias=True)
|
||||
final_state = await backoffice_runtime.execute_workflow(
|
||||
"backoffice_response_emulator",
|
||||
payload=payload,
|
||||
transaction_id=transaction_id,
|
||||
app_state=request.app.state,
|
||||
metadata={
|
||||
"tenant_id": "default",
|
||||
"agent_id": "backoffice_anatel",
|
||||
"channel": "rest",
|
||||
"flow_mode": event.flow_mode,
|
||||
"selected_actions": [a.model_dump(mode="json") for a in event.selected_actions],
|
||||
"legacy_contract": "case.response-emulator",
|
||||
},
|
||||
)
|
||||
async def _run_native_emulator(request: Request, event, *, legacy_contract: str = "case.response-emulator"):
|
||||
envelope = backoffice_rest_adapter.from_emulator_event(event, legacy_contract=legacy_contract)
|
||||
final_state = await backoffice_dispatcher.execute(envelope, app_state=request.app.state)
|
||||
try:
|
||||
from src.api.utils.emulator_response_builder import build_emulator_response_event
|
||||
response_event = build_emulator_response_event(final_state, transaction_id)
|
||||
return response_event.model_dump(mode="json", by_alias=True)
|
||||
response_event = build_emulator_response_event(final_state, envelope.transaction_id)
|
||||
response = response_event.model_dump(mode="json", by_alias=True)
|
||||
response.setdefault("metadata", {})
|
||||
response["metadata"]["framework_entrypoint"] = "channel_gateway"
|
||||
response["metadata"]["source_channel"] = envelope.channel
|
||||
return response
|
||||
except Exception:
|
||||
return {
|
||||
"transactionId": transaction_id,
|
||||
"transactionId": envelope.transaction_id,
|
||||
"framework_native": True,
|
||||
"framework_entrypoint": "channel_gateway",
|
||||
"source_channel": envelope.channel,
|
||||
"current_step": str(final_state.get("current_step")),
|
||||
"final_response": final_state.get("final_response"),
|
||||
"error": final_state.get("error"),
|
||||
@@ -782,6 +772,8 @@ async def native_health_ready():
|
||||
"workflows": list(backoffice_runtime._graphs.keys()),
|
||||
"framework_layers": {
|
||||
"gateway": True,
|
||||
"channel_gateway_entrypoint": True,
|
||||
"backoffice_rest_adapter": True,
|
||||
"identity": True,
|
||||
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
|
||||
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
|
||||
@@ -802,7 +794,9 @@ async def debug_backoffice_parity():
|
||||
"legacy_graph_execution": False,
|
||||
"legacy_router_registration": False,
|
||||
"forbidden_active_imports": ["legacy_reference_disabled/original_develop/src_agent_graphs", "legacy_reference_disabled/original_develop/src_api_executors"],
|
||||
"runtime": "app.workflows.backoffice_native_runtime.BackofficeNativeRuntime",
|
||||
"entrypoint": "app.channels.backoffice_rest_adapter.BackofficeRestChannelAdapter",
|
||||
"dispatcher": "app.workflows.backoffice_workflow_dispatcher.BackofficeWorkflowDispatcher",
|
||||
"runtime": "app.workflows.backoffice_workflow_executor.BackofficeWorkflowExecutor",
|
||||
"domain_package": "src.agent.nodes + src.components.clients + src.agent.local_prompts",
|
||||
"workflows": {
|
||||
"backoffice_checklist": [
|
||||
@@ -814,6 +808,8 @@ async def debug_backoffice_parity():
|
||||
},
|
||||
"framework_layers": {
|
||||
"gateway": True,
|
||||
"channel_gateway_entrypoint": True,
|
||||
"backoffice_rest_adapter": True,
|
||||
"identity": True,
|
||||
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
|
||||
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,750 +1,13 @@
|
||||
"""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
|
||||
# Compatibility shim. The framework-native name is BackofficeWorkflowExecutor.
|
||||
# Existing external imports from app.workflows.backoffice_native_runtime keep working,
|
||||
# but application code should import BackofficeWorkflowExecutor from
|
||||
# app.workflows.backoffice_workflow_executor.
|
||||
|
||||
import yaml
|
||||
from app.workflows.backoffice_workflow_executor import BackofficeWorkflowExecutor
|
||||
|
||||
from langgraph.graph import END, START, StateGraph
|
||||
# Deprecated alias for backward compatibility only.
|
||||
BackofficeNativeRuntime = BackofficeWorkflowExecutor
|
||||
|
||||
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):
|
||||
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.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)],
|
||||
# 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.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"
|
||||
__all__ = ["BackofficeWorkflowExecutor", "BackofficeNativeRuntime"]
|
||||
|
||||
69
app/workflows/backoffice_workflow_dispatcher.py
Normal file
69
app/workflows/backoffice_workflow_dispatcher.py
Normal file
@@ -0,0 +1,69 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from app.channels.backoffice_rest_adapter import BackofficeChannelEnvelope, BackofficeRestChannelAdapter
|
||||
from app.workflows.backoffice_workflow_executor import BackofficeWorkflowExecutor
|
||||
|
||||
|
||||
class BackofficeWorkflowDispatcher:
|
||||
"""Framework-facing dispatcher for Backoffice operational workflows.
|
||||
|
||||
FastAPI routes call this dispatcher with a channel envelope. The dispatcher
|
||||
first normalizes the envelope through the framework ChannelGateway and only
|
||||
then dispatches the requested LangGraph workflow. This keeps REST as a
|
||||
channel adapter concern and prevents routes from coupling directly to a
|
||||
domain workflow executor.
|
||||
"""
|
||||
|
||||
def __init__(self, *, channel_gateway: Any, executor: BackofficeWorkflowExecutor, telemetry: Any, adapter: BackofficeRestChannelAdapter | None = None):
|
||||
self.channel_gateway = channel_gateway
|
||||
self.executor = executor
|
||||
self.telemetry = telemetry
|
||||
self.adapter = adapter or BackofficeRestChannelAdapter()
|
||||
|
||||
async def execute(self, envelope: BackofficeChannelEnvelope, *, app_state: Any = None) -> dict[str, Any]:
|
||||
msg = await self.adapter.normalize(self.channel_gateway, envelope)
|
||||
context = dict(getattr(msg, "context", None) or envelope.gateway_payload())
|
||||
metadata = {
|
||||
**(envelope.metadata or {}),
|
||||
"tenant_id": envelope.tenant_id,
|
||||
"agent_id": envelope.agent_id,
|
||||
"channel": envelope.channel,
|
||||
"normalized_channel": getattr(msg, "channel", envelope.channel),
|
||||
"channel_id": getattr(msg, "channel_id", None),
|
||||
"message_id": envelope.message_id,
|
||||
"user_id": getattr(msg, "user_id", envelope.user_id),
|
||||
"business_context": envelope.business_context,
|
||||
"channel_context": context,
|
||||
"framework_entrypoint": "channel_gateway",
|
||||
}
|
||||
await self._event("backoffice.channel.normalized", {
|
||||
"workflow_id": envelope.workflow_id,
|
||||
"transaction_id": envelope.transaction_id,
|
||||
"channel": envelope.channel,
|
||||
"agent_id": envelope.agent_id,
|
||||
"legacy_contract": envelope.metadata.get("legacy_contract"),
|
||||
})
|
||||
final_state = await self.executor.execute_workflow(
|
||||
envelope.workflow_id,
|
||||
payload=envelope.payload,
|
||||
transaction_id=envelope.transaction_id,
|
||||
app_state=app_state,
|
||||
metadata=metadata,
|
||||
)
|
||||
await self._event("backoffice.workflow.dispatched", {
|
||||
"workflow_id": envelope.workflow_id,
|
||||
"transaction_id": envelope.transaction_id,
|
||||
"current_step": str(final_state.get("current_step")),
|
||||
"has_error": bool(final_state.get("error")),
|
||||
})
|
||||
return final_state
|
||||
|
||||
async def _event(self, name: str, payload: dict[str, Any]) -> None:
|
||||
try:
|
||||
await self.telemetry.event(name, payload, kind="workflow")
|
||||
except TypeError:
|
||||
await self.telemetry.event(name, payload)
|
||||
except Exception:
|
||||
pass
|
||||
779
app/workflows/backoffice_workflow_executor.py
Normal file
779
app/workflows/backoffice_workflow_executor.py
Normal file
@@ -0,0 +1,779 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user