diff --git a/.env b/.env index 672d296..cd6feb8 100644 --- a/.env +++ b/.env @@ -214,24 +214,24 @@ GOOGLE_APPLICATION_CREDENTIALS=/Users/cristianohoshikawa/Dropbox/ORACLE/TIM/comp SPEECH_TIMEOUT=30 SPEECH_SIMILARITY_THRESHOLD=70 -TAIS_DB_USER=USR_ADB_AGNTATEND_W_DEV -TAIS_DB_PASSWORD=T!M#Esta026! -TAIS_DB_DSN="(description= (retry_count=3)(retry_delay=7)(address=(protocol=tcps)(port=1522)(host=10.152.100.72))(connect_data=(service_name=gf9a4a2e79cfeb2_agntatendimentodev_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=no)))" +TAIS_DB_USER=admin +TAIS_DB_PASSWORD=Moniquinha1972 +TAIS_DB_DSN=" (description= (retry_count=20)(retry_delay=3)(address=(protocol=tcps)(port=1522)(host=adb.sa-saopaulo-1.oraclecloud.com))(connect_data=(service_name=jy2otyfomimhaoc_oradb23ai_high.adb.oraclecloud.com))(security=(ssl_server_dn_match=yes)))" TAIS_DB_TIMEOUT=30 -TAIS_GENAI_ENDPOINT=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com -TAIS_GENAI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaa3prjvf7mkvijwn5ng5h6n5ftanbbwqfg6cu44kmffwamhyy267iq +TAIS_GENAI_ENDPOINT=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com +TAIS_GENAI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q TAIS_GENAI_EMBED_MODEL_ID=cohere.embed-multilingual-v3.0 TAIS_TABLE_CHUNKS=CHUNKS_CHAR_COHERE_3 TAIS_TOP_K=3 # LLM CLASSIFICATION -CLASSIFICATION_LLM_MODEL=bo_gptoss20b_dev +CLASSIFICATION_LLM_MODEL=openai.gpt-4.1 CLASSIFICATION_LLM_TEMPERATURE=0.3 CLASSIFICATION_LLM_MAX_TOKENS=2000 CLASSIFICATION_LLM_TOP_P=0.9 CLASSIFICATION_LLM_TOP_K=250 -CLASSIFICATION_LARGE_LLM_MODEL=bo_gptoss120b_dev +CLASSIFICATION_LARGE_LLM_MODEL=openai.gpt-4.1 CLASSIFICATION_LARGE_LLM_TEMPERATURE=0.3 CLASSIFICATION_LARGE_LLM_MAX_TOKENS=4000 CLASSIFICATION_LARGE_LLM_TOP_P=0.9 diff --git a/BACKOFFICE_REST_CHANNEL_PATCH_SUMMARY.md b/BACKOFFICE_REST_CHANNEL_PATCH_SUMMARY.md new file mode 100644 index 0000000..fdaf0dc --- /dev/null +++ b/BACKOFFICE_REST_CHANNEL_PATCH_SUMMARY.md @@ -0,0 +1,53 @@ +# Patch Summary — Backoffice REST as Channel + +## Goal + +Adapt the Backoffice project so legacy REST endpoints behave as framework channel entrypoints instead of directly invoking `BackofficeWorkflowExecutor` from FastAPI routes. + +## Added + +- `app/channels/__init__.py` +- `app/channels/backoffice_rest_adapter.py` +- `app/workflows/backoffice_workflow_dispatcher.py` +- `docs/BACKOFFICE_REST_AS_CHANNEL_ARCHITECTURE.md` + +## Modified + +- `app/main.py` + - Instantiates `BackofficeRestChannelAdapter` and `BackofficeWorkflowDispatcher`. + - Updates `/agent/*` routes to create a `BackofficeChannelEnvelope` and dispatch through the channel path. + - Updates response metadata with `framework_entrypoint=channel_gateway` and `source_channel=backoffice_rest`. + - Updates readiness/debug metadata to show the channel gateway entrypoint. + +- `app/agents/backoffice_agent.py` + - Removes guidance that operational checklist should be called through direct `BackofficeWorkflowExecutor` usage. + - Documents that operational ticket/emulator flows enter through the channel adapter and dispatcher. + +- `app/workflows/backoffice_workflow_executor.py` + - Repositioned as the workflow execution engine behind the dispatcher, not the REST entrypoint. + +- `src/api/LEGACY_ROUTES_DISABLED.md` + - Updated to describe the new REST → ChannelGateway → dispatcher → workflow path. + +## New execution chain + +```text +Legacy REST route + → BackofficeRestChannelAdapter + → ChannelGateway.normalize(...) + → BackofficeWorkflowDispatcher + → BackofficeWorkflowExecutor / LangGraph workflow + → Response builder preserving legacy contract +``` + +## Validation done + +Python syntax compilation succeeded for the changed Python files: + +```text +app/main.py +app/agents/backoffice_agent.py +app/channels/backoffice_rest_adapter.py +app/workflows/backoffice_workflow_dispatcher.py +app/workflows/backoffice_workflow_executor.py +``` diff --git a/README_BACKOFFICE_FRAMEWORK_NATIVE_100.md b/README_BACKOFFICE_FRAMEWORK_NATIVE_100.md index 31c9c30..877120e 100644 --- a/README_BACKOFFICE_FRAMEWORK_NATIVE_100.md +++ b/README_BACKOFFICE_FRAMEWORK_NATIVE_100.md @@ -6,7 +6,7 @@ Esta versão substitui a execução direta dos grafos legados por workflows comp - O framework é dono do motor de workflow/LangGraph, checkpoint, telemetry, guardrails, judges, supervisor e persistência de execução. - O backend fornece apenas customizações de domínio: nós, services/clients, prompts, schemas e contratos REST. -- Os contratos REST antigos continuam existindo, mas agora são adapters finos para `BackofficeNativeRuntime.execute_workflow(...)`. +- Os contratos REST antigos continuam existindo, mas agora são adapters finos para `BackofficeWorkflowExecutor.execute_workflow(...)`. ## O que foi removido do caminho ativo @@ -26,7 +26,7 @@ legacy_reference_disabled/original_develop/ ## Runtime ativo ```text -app/workflows/backoffice_native_runtime.py +app/workflows/backoffice_workflow_executor.py ``` Esse runtime monta dois workflows com o motor do framework: diff --git a/app/__pycache__/__init__.cpython-313.pyc b/app/__pycache__/__init__.cpython-313.pyc index 0186b34..74dd46d 100644 Binary files a/app/__pycache__/__init__.cpython-313.pyc and b/app/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/__pycache__/identity_extraction.cpython-313.pyc b/app/__pycache__/identity_extraction.cpython-313.pyc index b0c8dcf..60d5b47 100644 Binary files a/app/__pycache__/identity_extraction.cpython-313.pyc and b/app/__pycache__/identity_extraction.cpython-313.pyc differ diff --git a/app/__pycache__/main.cpython-313.pyc b/app/__pycache__/main.cpython-313.pyc index ce055f5..ef970eb 100644 Binary files a/app/__pycache__/main.cpython-313.pyc and b/app/__pycache__/main.cpython-313.pyc differ diff --git a/app/__pycache__/state.cpython-313.pyc b/app/__pycache__/state.cpython-313.pyc index d757da9..3b1f661 100644 Binary files a/app/__pycache__/state.cpython-313.pyc and b/app/__pycache__/state.cpython-313.pyc differ diff --git a/app/agents/__pycache__/backoffice_agent.cpython-313.pyc b/app/agents/__pycache__/backoffice_agent.cpython-313.pyc index d3745fa..d274885 100644 Binary files a/app/agents/__pycache__/backoffice_agent.cpython-313.pyc and b/app/agents/__pycache__/backoffice_agent.cpython-313.pyc differ diff --git a/app/agents/__pycache__/billing_agent.cpython-313.pyc b/app/agents/__pycache__/billing_agent.cpython-313.pyc index 9962052..59b62df 100644 Binary files a/app/agents/__pycache__/billing_agent.cpython-313.pyc and b/app/agents/__pycache__/billing_agent.cpython-313.pyc differ diff --git a/app/agents/__pycache__/orders_agent.cpython-313.pyc b/app/agents/__pycache__/orders_agent.cpython-313.pyc index 9244e2e..f141e92 100644 Binary files a/app/agents/__pycache__/orders_agent.cpython-313.pyc and b/app/agents/__pycache__/orders_agent.cpython-313.pyc differ diff --git a/app/agents/__pycache__/product_agent.cpython-313.pyc b/app/agents/__pycache__/product_agent.cpython-313.pyc index af4b48f..cbbab22 100644 Binary files a/app/agents/__pycache__/product_agent.cpython-313.pyc and b/app/agents/__pycache__/product_agent.cpython-313.pyc differ diff --git a/app/agents/__pycache__/prompting.cpython-313.pyc b/app/agents/__pycache__/prompting.cpython-313.pyc index b5a95e3..995e8f3 100644 Binary files a/app/agents/__pycache__/prompting.cpython-313.pyc and b/app/agents/__pycache__/prompting.cpython-313.pyc differ diff --git a/app/agents/__pycache__/runtime.cpython-313.pyc b/app/agents/__pycache__/runtime.cpython-313.pyc index f14d167..eb5576a 100644 Binary files a/app/agents/__pycache__/runtime.cpython-313.pyc and b/app/agents/__pycache__/runtime.cpython-313.pyc differ diff --git a/app/agents/__pycache__/support_agent.cpython-313.pyc b/app/agents/__pycache__/support_agent.cpython-313.pyc index 2d77413..8ef5397 100644 Binary files a/app/agents/__pycache__/support_agent.cpython-313.pyc and b/app/agents/__pycache__/support_agent.cpython-313.pyc differ diff --git a/app/agents/backoffice_agent.py b/app/agents/backoffice_agent.py index 65e8465..716b368 100644 --- a/app/agents/backoffice_agent.py +++ b/app/agents/backoffice_agent.py @@ -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", }, } diff --git a/app/channels/__init__.py b/app/channels/__init__.py new file mode 100644 index 0000000..14fa58d --- /dev/null +++ b/app/channels/__init__.py @@ -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"] diff --git a/app/channels/__pycache__/__init__.cpython-313.pyc b/app/channels/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000..764df6a Binary files /dev/null and b/app/channels/__pycache__/__init__.cpython-313.pyc differ diff --git a/app/channels/__pycache__/backoffice_rest_adapter.cpython-313.pyc b/app/channels/__pycache__/backoffice_rest_adapter.cpython-313.pyc new file mode 100644 index 0000000..fba0c8a Binary files /dev/null and b/app/channels/__pycache__/backoffice_rest_adapter.cpython-313.pyc differ diff --git a/app/channels/backoffice_rest_adapter.py b/app/channels/backoffice_rest_adapter.py new file mode 100644 index 0000000..da4cc3f --- /dev/null +++ b/app/channels/backoffice_rest_adapter.py @@ -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" diff --git a/app/domain/__pycache__/__init__.cpython-313.pyc b/app/domain/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 0f87383..0000000 Binary files a/app/domain/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/app/domain/backoffice/__pycache__/__init__.cpython-313.pyc b/app/domain/backoffice/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 9983ee4..0000000 Binary files a/app/domain/backoffice/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/__init__.cpython-313.pyc b/app/examples/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index eac1e1c..0000000 Binary files a/app/examples/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/grl_examples.cpython-313.pyc b/app/examples/__pycache__/grl_examples.cpython-313.pyc deleted file mode 100644 index 305f3f2..0000000 Binary files a/app/examples/__pycache__/grl_examples.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/ic_examples.cpython-313.pyc b/app/examples/__pycache__/ic_examples.cpython-313.pyc deleted file mode 100644 index 026b8e0..0000000 Binary files a/app/examples/__pycache__/ic_examples.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/mcp_examples.cpython-313.pyc b/app/examples/__pycache__/mcp_examples.cpython-313.pyc deleted file mode 100644 index cd4702c..0000000 Binary files a/app/examples/__pycache__/mcp_examples.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/noc_examples.cpython-313.pyc b/app/examples/__pycache__/noc_examples.cpython-313.pyc deleted file mode 100644 index 5cc3796..0000000 Binary files a/app/examples/__pycache__/noc_examples.cpython-313.pyc and /dev/null differ diff --git a/app/examples/__pycache__/observer_examples.cpython-313.pyc b/app/examples/__pycache__/observer_examples.cpython-313.pyc deleted file mode 100644 index 3c3b1aa..0000000 Binary files a/app/examples/__pycache__/observer_examples.cpython-313.pyc and /dev/null differ diff --git a/app/main.py b/app/main.py index 9649c0b..b4acf43 100644 --- a/app/main.py +++ b/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, diff --git a/app/workflows/__pycache__/agent_graph.cpython-313.pyc b/app/workflows/__pycache__/agent_graph.cpython-313.pyc index e0318af..e7275a0 100644 Binary files a/app/workflows/__pycache__/agent_graph.cpython-313.pyc and b/app/workflows/__pycache__/agent_graph.cpython-313.pyc differ diff --git a/app/workflows/__pycache__/backoffice_workflow_dispatcher.cpython-313.pyc b/app/workflows/__pycache__/backoffice_workflow_dispatcher.cpython-313.pyc new file mode 100644 index 0000000..a073a37 Binary files /dev/null and b/app/workflows/__pycache__/backoffice_workflow_dispatcher.cpython-313.pyc differ diff --git a/app/workflows/__pycache__/backoffice_native_runtime.cpython-313.pyc b/app/workflows/__pycache__/backoffice_workflow_executor.cpython-313.pyc similarity index 60% rename from app/workflows/__pycache__/backoffice_native_runtime.cpython-313.pyc rename to app/workflows/__pycache__/backoffice_workflow_executor.cpython-313.pyc index c39919f..1c56f5c 100644 Binary files a/app/workflows/__pycache__/backoffice_native_runtime.cpython-313.pyc and b/app/workflows/__pycache__/backoffice_workflow_executor.cpython-313.pyc differ diff --git a/app/workflows/backoffice_native_runtime.py b/app/workflows/backoffice_native_runtime.py index 26c65e0..6d5e878 100644 --- a/app/workflows/backoffice_native_runtime.py +++ b/app/workflows/backoffice_native_runtime.py @@ -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"] diff --git a/app/workflows/backoffice_workflow_dispatcher.py b/app/workflows/backoffice_workflow_dispatcher.py new file mode 100644 index 0000000..c38a610 --- /dev/null +++ b/app/workflows/backoffice_workflow_dispatcher.py @@ -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 diff --git a/app/workflows/backoffice_workflow_executor.py b/app/workflows/backoffice_workflow_executor.py new file mode 100644 index 0000000..f5ccb0d --- /dev/null +++ b/app/workflows/backoffice_workflow_executor.py @@ -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" diff --git a/debug_events.jsonl b/debug_events.jsonl index b7d6a42..7c508f6 100644 --- a/debug_events.jsonl +++ b/debug_events.jsonl @@ -144,3 +144,16 @@ {"ts": "2026-06-13T13:13:33.430808Z", "key": "man-1448d83f", "payload": {"transactionId": "man-1448d83f", "processing": {"status": "processing", "current_step": "reclassification_analysis", "action": "await_response", "note": "[13/06/2026 10:13:27] Chamado recebido, iniciando processamento.\n[13/06/2026 10:13:27] Enriquecendo chamado no IMDB....\n[13/06/2026 10:13:27] Realizando verificação de CPF divergente....\n[13/06/2026 10:13:27] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 10:13:27] Consultando procedimentos na base de conhecimento....\n[13/06/2026 10:13:28] Realizando análise de cancelamento....\n[13/06/2026 10:13:30] Realizando análise de reencaminhamento....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....", "timestamp": "2026-06-13T13:13:33.430794Z"}}} {"ts": "2026-06-13T13:13:37.097443Z", "key": "man-1448d83f", "payload": {"transactionId": "man-1448d83f", "processing": {"status": "processing", "current_step": "siebel_sr_opening", "action": "await_response", "note": "[13/06/2026 10:13:27] Chamado recebido, iniciando processamento.\n[13/06/2026 10:13:27] Enriquecendo chamado no IMDB....\n[13/06/2026 10:13:27] Realizando verificação de CPF divergente....\n[13/06/2026 10:13:27] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 10:13:27] Consultando procedimentos na base de conhecimento....\n[13/06/2026 10:13:28] Realizando análise de cancelamento....\n[13/06/2026 10:13:30] Realizando análise de reencaminhamento....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....\n[13/06/2026 10:13:37] Abrindo chamado no Siebel....", "timestamp": "2026-06-13T13:13:37.097424Z"}}} {"ts": "2026-06-13T13:13:37.103780Z", "key": "man-1448d83f", "payload": {"transactionId": "man-1448d83f", "processing": {"status": "processing", "current_step": "siebel_sr_opened", "action": "await_response", "note": "[13/06/2026 10:13:27] Chamado recebido, iniciando processamento.\n[13/06/2026 10:13:27] Enriquecendo chamado no IMDB....\n[13/06/2026 10:13:27] Realizando verificação de CPF divergente....\n[13/06/2026 10:13:27] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 10:13:27] Consultando procedimentos na base de conhecimento....\n[13/06/2026 10:13:28] Realizando análise de cancelamento....\n[13/06/2026 10:13:30] Realizando análise de reencaminhamento....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....\n[13/06/2026 10:13:33] Realizando análise de reclassificação do chamado....\n[13/06/2026 10:13:37] Abrindo chamado no Siebel....\n[13/06/2026 10:13:37] Chamado aberto no Siebel.", "timestamp": "2026-06-13T13:13:37.103766Z"}}} +{"ts": "2026-06-13T18:36:33.638465Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "fetching_ticket", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.", "timestamp": "2026-06-13T18:36:33.638442Z"}}} +{"ts": "2026-06-13T18:36:33.647743Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "validation", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.", "timestamp": "2026-06-13T18:36:33.647718Z"}}} +{"ts": "2026-06-13T18:36:33.665164Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "imdb_enrichment", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....", "timestamp": "2026-06-13T18:36:33.665136Z"}}} +{"ts": "2026-06-13T18:36:37.714001Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "identity_verification", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....", "timestamp": "2026-06-13T18:36:37.713972Z"}}} +{"ts": "2026-06-13T18:36:37.723098Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "speech_enrichment", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.", "timestamp": "2026-06-13T18:36:37.723075Z"}}} +{"ts": "2026-06-13T18:36:37.798786Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "knowledge_base_enrichment", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....", "timestamp": "2026-06-13T18:36:37.798757Z"}}} +{"ts": "2026-06-13T18:37:08.751641Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "knowledge_base_enrichment_unavailable", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.", "timestamp": "2026-06-13T18:37:08.751624Z"}}} +{"ts": "2026-06-13T18:37:08.759915Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "canceling_analysis", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....", "timestamp": "2026-06-13T18:37:08.759889Z"}}} +{"ts": "2026-06-13T18:37:12.004864Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "tim_complaint_analysis", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....\n[13/06/2026 15:37:12] Realizando análise de reencaminhamento....", "timestamp": "2026-06-13T18:37:12.004841Z"}}} +{"ts": "2026-06-13T18:37:15.091211Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "reclassification_analysis", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....\n[13/06/2026 15:37:12] Realizando análise de reencaminhamento....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....", "timestamp": "2026-06-13T18:37:15.091188Z"}}} +{"ts": "2026-06-13T18:37:15.100227Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "reclassification_analysis", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....\n[13/06/2026 15:37:12] Realizando análise de reencaminhamento....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....", "timestamp": "2026-06-13T18:37:15.100198Z"}}} +{"ts": "2026-06-13T18:37:18.928802Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "siebel_sr_opening", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....\n[13/06/2026 15:37:12] Realizando análise de reencaminhamento....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....\n[13/06/2026 15:37:18] Abrindo chamado no Siebel....", "timestamp": "2026-06-13T18:37:18.928779Z"}}} +{"ts": "2026-06-13T18:37:18.937657Z", "key": "man-e10208c7", "payload": {"transactionId": "man-e10208c7", "processing": {"status": "processing", "current_step": "siebel_sr_opened", "action": "await_response", "note": "[13/06/2026 15:36:33] Chamado recebido, iniciando processamento.\n[13/06/2026 15:36:33] Enriquecendo chamado no IMDB....\n[13/06/2026 15:36:37] Realizando verificação de CPF divergente....\n[13/06/2026 15:36:37] Enriquecendo chamado via Speech Analytics.\n[13/06/2026 15:36:37] Consultando procedimentos na base de conhecimento....\n[13/06/2026 15:37:08] Base de conhecimento consultada - indisponível no momento.\n[13/06/2026 15:37:08] Realizando análise de cancelamento....\n[13/06/2026 15:37:12] Realizando análise de reencaminhamento....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....\n[13/06/2026 15:37:15] Realizando análise de reclassificação do chamado....\n[13/06/2026 15:37:18] Abrindo chamado no Siebel....\n[13/06/2026 15:37:18] Chamado aberto no Siebel.", "timestamp": "2026-06-13T18:37:18.937634Z"}}} diff --git a/docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md b/docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md index e7322b4..46778b1 100644 --- a/docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md +++ b/docs/AJUSTES_IC_NOC_GRL_OCI_OPENAI.md @@ -33,7 +33,7 @@ Esta versão corrige dois pontos do runtime nativo do backoffice: ## Arquivos alterados -- `app/workflows/backoffice_native_runtime.py` +- `app/workflows/backoffice_workflow_executor.py` - `src/core/config.py` - `.env.example` - `tools/validate_parity.py` diff --git a/docs/BACKOFFICE_REST_AS_CHANNEL_ARCHITECTURE.md b/docs/BACKOFFICE_REST_AS_CHANNEL_ARCHITECTURE.md new file mode 100644 index 0000000..390c97a --- /dev/null +++ b/docs/BACKOFFICE_REST_AS_CHANNEL_ARCHITECTURE.md @@ -0,0 +1,97 @@ +# Backoffice REST as a Framework Channel + +This project now treats the TIM/ANATEL Backoffice REST contracts as a corporate channel instead of letting the HTTP routes call a domain workflow executor directly. + +## Target execution model + +```text +Legacy-compatible REST endpoint + ↓ +BackofficeRestChannelAdapter + ↓ +ChannelGateway normalization path + ↓ +BackofficeWorkflowDispatcher + ↓ +Backoffice workflow LangGraph + ↓ +Framework layers: guardrails, supervisor, judges, checkpoint, telemetry, MCP + ↓ +Legacy-compatible REST response adapter +``` + +## What changed + +### New files + +- `app/channels/backoffice_rest_adapter.py` + - Converts legacy Backoffice REST payloads into `BackofficeChannelEnvelope`. + - Preserves the original payload under `request_context`. + - Extracts canonical `business_context` such as `customer_key`, `contract_key`, `interaction_key` and `session_key`. + - Passes the request through `ChannelGateway.normalize(...)` using `channel = backoffice_rest`. + - Falls back to the web adapter shape when the installed framework does not yet have a dedicated `backoffice_rest` adapter. + +- `app/workflows/backoffice_workflow_dispatcher.py` + - Receives a normalized channel envelope. + - Emits framework telemetry events for channel normalization and workflow dispatch. + - Invokes the correct operational workflow: + - `backoffice_checklist` + - `backoffice_response_emulator` + +### Updated file + +- `app/main.py` + - The compatibility routes no longer call `BackofficeWorkflowExecutor.execute_workflow(...)` directly. + - They now call: + +```python +BackofficeRestChannelAdapter + ↓ +BackofficeWorkflowDispatcher + ↓ +BackofficeWorkflowExecutor +``` + +The `BackofficeWorkflowExecutor` still owns the compiled LangGraph workflows, but it is now behind the channel/dispatcher boundary rather than being the direct REST entrypoint. + +## Why this is architecturally cleaner + +The REST routes are now only transport adapters. They preserve the old external contract, but the request enters the same conceptual pipeline used by other framework channels. + +This avoids the previous coupling: + +```text +REST route → BackofficeWorkflowExecutor +``` + +and replaces it with: + +```text +REST route → channel adapter → ChannelGateway → workflow dispatcher → LangGraph workflow +``` + +## Compatibility + +The existing routes remain available: + +- `POST /agent/process-ticket` +- `POST /agent/execute` +- `POST /agent/process-and-stream` +- `POST /case/{transaction_id}/response-emulator/generate` +- `POST /case/{transaction_id}/response-emulator/finalize` + +The response builders are preserved, so external clients should not need to change their payload or response handling. + +## Runtime notes + +The metadata added to workflow state now includes: + +```text +framework_entrypoint = channel_gateway +channel = backoffice_rest +normalized_channel = backoffice_rest or web fallback +business_context = canonical business keys +channel_context = normalized gateway context +``` + +This makes Langfuse/telemetry easier to interpret because Backoffice REST is visible as a channel entrypoint, not as an ad-hoc executor call. diff --git a/docs/RELATORIO_FRAMEWORK_NATIVE_100.md b/docs/RELATORIO_FRAMEWORK_NATIVE_100.md index a2ad1ef..68365a3 100644 --- a/docs/RELATORIO_FRAMEWORK_NATIVE_100.md +++ b/docs/RELATORIO_FRAMEWORK_NATIVE_100.md @@ -9,7 +9,7 @@ A versão foi refatorada para que o backoffice não execute mais grafos LangGrap O backend ativo usa: - `app/main.py` como camada de rotas/adapters. -- `app/workflows/backoffice_native_runtime.py` como runtime de workflows do framework. +- `app/workflows/backoffice_workflow_executor.py` como executor de workflows de domínio sobre o runtime LangGraph do framework. - `src/agent/nodes/*` como nós de domínio. - `src/components/clients/*` como services/clients de domínio. - `src/agent/local_prompts/*` como prompts de domínio. @@ -75,7 +75,7 @@ Agora: ```text app/main.py chama backoffice_runtime.execute_workflow -BackofficeNativeRuntime compila os workflows +BackofficeWorkflowExecutor compila os workflows os nós originais são apenas plugins de domínio guardrails/judges/supervisor/checkpoint/telemetry entram no runtime ``` diff --git a/legacy_backend/__pycache__/mock_imdb_server.cpython-313.pyc b/legacy_backend/__pycache__/mock_imdb_server.cpython-313.pyc new file mode 100644 index 0000000..9c70fd0 Binary files /dev/null and b/legacy_backend/__pycache__/mock_imdb_server.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/__init__.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/__init__.cpython-313.pyc index 2355ae7..cb866bc 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/__init__.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/__init__.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/clients.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/clients.cpython-313.pyc index e23da4a..872d7eb 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/clients.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/clients.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/main.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/main.cpython-313.pyc index b253c11..aab9e5d 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/main.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/main.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/models.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/models.cpython-313.pyc index ff077b3..afd3977 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/models.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/models.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/settings.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/settings.cpython-313.pyc index f069ff1..d1cac44 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/settings.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/settings.cpython-313.pyc differ diff --git a/mcp_servers/backoffice_mcp_server/__pycache__/store.cpython-313.pyc b/mcp_servers/backoffice_mcp_server/__pycache__/store.cpython-313.pyc index b19bcd9..bef44b5 100644 Binary files a/mcp_servers/backoffice_mcp_server/__pycache__/store.cpython-313.pyc and b/mcp_servers/backoffice_mcp_server/__pycache__/store.cpython-313.pyc differ diff --git a/mock_servers/mock_imdb_server.py b/mock_servers/mock_imdb_server.py index 7f6ab28..f47e020 100644 --- a/mock_servers/mock_imdb_server.py +++ b/mock_servers/mock_imdb_server.py @@ -1,47 +1,67 @@ -from __future__ import annotations +# /compass_backoffice/legacy_backend/ +# uvicorn mock_imdb_server:app --port 8011 from fastapi import FastAPI, Request -app = FastAPI(title="Mock IMDB TIM", version="1.0.0") +app = FastAPI(title="Mock IMDB TIM") - -def _payload(msisdn: str | None = None, cpf_cnpj: str | None = None): +@app.get("/access/v1/info") +async def get_access_info(request: Request): return { - "msisdn": msisdn or "62981152324", - "cpfCnpj": cpf_cnpj or "06252533106", - "socialSecNo": cpf_cnpj or "06252533106", + "cpfCnpj": request.query_params.get("cpfCnpj") or "06252533106", + "msisdn": request.query_params.get("msisdn") or "62981152324", "plan": { "Type": "POS_PAGO", - "name": "TIM Black Família 50GB", - "commercialName": "TIM Black Família 50GB", + "name": "TIM Black Família 50GB" }, - "statusType": "ACTIVE", - "statusDescription": "Cliente ativo", "customer": { "segment": "consumer", "status": "active", "contumazCustomer": False, - "odcCustomer": False, + "odcCustomer": False }, + "contracts": [ + { + "contractId": "mock-contract-001", + "status": "active", + "product": "Celular Pós-pago" + } + ] } - -@app.get("/health") -async def health(): - return {"status": "ok", "service": "mock_imdb"} - - -@app.get("/access/v1/info") -async def get_access_info(request: Request): - return _payload( - msisdn=request.query_params.get("msisdn"), - cpf_cnpj=request.query_params.get("cpfCnpj") or request.query_params.get("cpf_cnpj"), - ) - - @app.get("/access/v1/info/{msisdn}") -async def get_access_info_by_msisdn(msisdn: str, request: Request): - return _payload( - msisdn=msisdn, - cpf_cnpj=request.query_params.get("cpfCnpj") or request.query_params.get("cpf_cnpj"), - ) +async def get_access_info_by_msisdn(msisdn: str): + return { + "msisdn": msisdn, + "cpfCnpj": "06252533106", + "plan": { + "Type": "POS_PAGO", + "name": "TIM Black Família 50GB" + }, + "customer": { + "segment": "consumer", + "status": "active", + "contumazCustomer": False, + "odcCustomer": False + } + } + +from uuid import uuid4 + +@app.post("/customers/v1/backOfficeSRopening") +async def backoffice_sr_opening(payload: dict): + protocol = f"SR-MOCK-{uuid4().hex[:8].upper()}" + + return { + "status": "success", + "interactionProtocol": protocol, + "crmProtocol": protocol, + "fieldsToUpdate": { + "status": "Aberto", + "reason": "Mock Siebel SR Opening" + }, + "case_response": { + "message": "Chamado Siebel aberto com sucesso no mock" + }, + "transitions": [] + } \ No newline at end of file diff --git a/src/__pycache__/__init__.cpython-313.pyc b/src/__pycache__/__init__.cpython-313.pyc index e4c5c46..c68503c 100644 Binary files a/src/__pycache__/__init__.cpython-313.pyc and b/src/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/__pycache__/__init__.cpython-39.pyc b/src/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index c93d715..0000000 Binary files a/src/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/src/agent/__pycache__/__init__.cpython-313.pyc b/src/agent/__pycache__/__init__.cpython-313.pyc index 25d56ce..de9ea8a 100644 Binary files a/src/agent/__pycache__/__init__.cpython-313.pyc and b/src/agent/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/__pycache__/__init__.cpython-39.pyc b/src/agent/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 736c036..0000000 Binary files a/src/agent/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/src/agent/graphs_README_DISABLED.md b/src/agent/graphs_README_DISABLED.md index 43cf2a2..f65d5df 100644 --- a/src/agent/graphs_README_DISABLED.md +++ b/src/agent/graphs_README_DISABLED.md @@ -2,4 +2,4 @@ The original `src/agent/graphs` package was moved to `legacy_reference_disabled/original_develop/src_agent_graphs`. -It is intentionally not importable by the active backend. The active backend builds and executes workflows through `app.workflows.backoffice_native_runtime.BackofficeNativeRuntime`, which uses the framework runtime and the original domain nodes/services/prompts as customizations. +It is intentionally not importable by the active backend. The active backend builds and executes workflows through `app.workflows.backoffice_workflow_executor.BackofficeWorkflowExecutor`, which uses the framework runtime and the original domain nodes/services/prompts as customizations. diff --git a/src/agent/local_prompts/__pycache__/canceling_analysis.cpython-313.pyc b/src/agent/local_prompts/__pycache__/canceling_analysis.cpython-313.pyc index a246bb7..5de576e 100644 Binary files a/src/agent/local_prompts/__pycache__/canceling_analysis.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/canceling_analysis.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/duplicate_analysis.cpython-313.pyc b/src/agent/local_prompts/__pycache__/duplicate_analysis.cpython-313.pyc index d0c884e..b04e02d 100644 Binary files a/src/agent/local_prompts/__pycache__/duplicate_analysis.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/duplicate_analysis.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/postprocess_tais_kb_query.cpython-313.pyc b/src/agent/local_prompts/__pycache__/postprocess_tais_kb_query.cpython-313.pyc index 425f2b7..114cc01 100644 Binary files a/src/agent/local_prompts/__pycache__/postprocess_tais_kb_query.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/postprocess_tais_kb_query.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/preprocess_tais_kb_query.cpython-313.pyc b/src/agent/local_prompts/__pycache__/preprocess_tais_kb_query.cpython-313.pyc index d4486a5..b935f7c 100644 Binary files a/src/agent/local_prompts/__pycache__/preprocess_tais_kb_query.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/preprocess_tais_kb_query.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/speech_history_analysis.cpython-313.pyc b/src/agent/local_prompts/__pycache__/speech_history_analysis.cpython-313.pyc index 237e733..be08244 100644 Binary files a/src/agent/local_prompts/__pycache__/speech_history_analysis.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/speech_history_analysis.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/ticket_reclassification.cpython-313.pyc b/src/agent/local_prompts/__pycache__/ticket_reclassification.cpython-313.pyc index f08979a..c5d650d 100644 Binary files a/src/agent/local_prompts/__pycache__/ticket_reclassification.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/ticket_reclassification.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/tim_complaint_analysis.cpython-313.pyc b/src/agent/local_prompts/__pycache__/tim_complaint_analysis.cpython-313.pyc index 309bc57..fe6d67b 100644 Binary files a/src/agent/local_prompts/__pycache__/tim_complaint_analysis.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/tim_complaint_analysis.cpython-313.pyc differ diff --git a/src/agent/local_prompts/__pycache__/treatment_decision_prompt.cpython-313.pyc b/src/agent/local_prompts/__pycache__/treatment_decision_prompt.cpython-313.pyc index 4ab6fc6..7bc6b19 100644 Binary files a/src/agent/local_prompts/__pycache__/treatment_decision_prompt.cpython-313.pyc and b/src/agent/local_prompts/__pycache__/treatment_decision_prompt.cpython-313.pyc differ diff --git a/src/agent/local_prompts/anatel_motives/__pycache__/anatel_motives.cpython-313.pyc b/src/agent/local_prompts/anatel_motives/__pycache__/anatel_motives.cpython-313.pyc index 0d48d0c..a59e4ab 100644 Binary files a/src/agent/local_prompts/anatel_motives/__pycache__/anatel_motives.cpython-313.pyc and b/src/agent/local_prompts/anatel_motives/__pycache__/anatel_motives.cpython-313.pyc differ diff --git a/src/agent/local_prompts/emulator/__pycache__/__init__.cpython-313.pyc b/src/agent/local_prompts/emulator/__pycache__/__init__.cpython-313.pyc index 2c14817..e621af9 100644 Binary files a/src/agent/local_prompts/emulator/__pycache__/__init__.cpython-313.pyc and b/src/agent/local_prompts/emulator/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/local_prompts/emulator/__pycache__/response_emulator_generation.cpython-313.pyc b/src/agent/local_prompts/emulator/__pycache__/response_emulator_generation.cpython-313.pyc index 0f83abb..050acc4 100644 Binary files a/src/agent/local_prompts/emulator/__pycache__/response_emulator_generation.cpython-313.pyc and b/src/agent/local_prompts/emulator/__pycache__/response_emulator_generation.cpython-313.pyc differ diff --git a/src/agent/logic/__pycache__/validation.cpython-313.pyc b/src/agent/logic/__pycache__/validation.cpython-313.pyc index b9525d7..52bc34c 100644 Binary files a/src/agent/logic/__pycache__/validation.cpython-313.pyc and b/src/agent/logic/__pycache__/validation.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/__init__.cpython-313.pyc b/src/agent/nodes/__pycache__/__init__.cpython-313.pyc index 7c18e7e..3953255 100644 Binary files a/src/agent/nodes/__pycache__/__init__.cpython-313.pyc and b/src/agent/nodes/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/__init__.cpython-39.pyc b/src/agent/nodes/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index 0b402fa..0000000 Binary files a/src/agent/nodes/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/src/agent/nodes/__pycache__/bypass_rules_node.cpython-313.pyc b/src/agent/nodes/__pycache__/bypass_rules_node.cpython-313.pyc index 2735373..eba6319 100644 Binary files a/src/agent/nodes/__pycache__/bypass_rules_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/bypass_rules_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/cache_check_node.cpython-313.pyc b/src/agent/nodes/__pycache__/cache_check_node.cpython-313.pyc index 89f4143..d71b2c8 100644 Binary files a/src/agent/nodes/__pycache__/cache_check_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/cache_check_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/canceling_analysis_node.cpython-313.pyc b/src/agent/nodes/__pycache__/canceling_analysis_node.cpython-313.pyc index 4f1849a..6db86e9 100644 Binary files a/src/agent/nodes/__pycache__/canceling_analysis_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/canceling_analysis_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-313.pyc b/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-313.pyc index 8ccdf82..180e358 100644 Binary files a/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-39.pyc b/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-39.pyc deleted file mode 100644 index 64a48f2..0000000 Binary files a/src/agent/nodes/__pycache__/different_complaint_operator_node.cpython-39.pyc and /dev/null differ diff --git a/src/agent/nodes/__pycache__/fetch_ticket_node.cpython-313.pyc b/src/agent/nodes/__pycache__/fetch_ticket_node.cpython-313.pyc index 65e7ad8..8631a20 100644 Binary files a/src/agent/nodes/__pycache__/fetch_ticket_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/fetch_ticket_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/identity_verification_node.cpython-313.pyc b/src/agent/nodes/__pycache__/identity_verification_node.cpython-313.pyc index 9998b4b..4de9eaa 100644 Binary files a/src/agent/nodes/__pycache__/identity_verification_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/identity_verification_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/imdb_enrichment_node.cpython-313.pyc b/src/agent/nodes/__pycache__/imdb_enrichment_node.cpython-313.pyc index 7df63de..99301fe 100644 Binary files a/src/agent/nodes/__pycache__/imdb_enrichment_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/imdb_enrichment_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/knowledge_base_enrichment_node.cpython-313.pyc b/src/agent/nodes/__pycache__/knowledge_base_enrichment_node.cpython-313.pyc index 240b7fe..97e125c 100644 Binary files a/src/agent/nodes/__pycache__/knowledge_base_enrichment_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/knowledge_base_enrichment_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/reclassification_analysis_node.cpython-313.pyc b/src/agent/nodes/__pycache__/reclassification_analysis_node.cpython-313.pyc index d994739..c7ec563 100644 Binary files a/src/agent/nodes/__pycache__/reclassification_analysis_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/reclassification_analysis_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/siebel_sr_opening_node.cpython-313.pyc b/src/agent/nodes/__pycache__/siebel_sr_opening_node.cpython-313.pyc index b37c566..04e156f 100644 Binary files a/src/agent/nodes/__pycache__/siebel_sr_opening_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/siebel_sr_opening_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/siebel_triplet_classification_node.cpython-313.pyc b/src/agent/nodes/__pycache__/siebel_triplet_classification_node.cpython-313.pyc deleted file mode 100644 index 0473397..0000000 Binary files a/src/agent/nodes/__pycache__/siebel_triplet_classification_node.cpython-313.pyc and /dev/null differ diff --git a/src/agent/nodes/__pycache__/speech_enrichment_node.cpython-313.pyc b/src/agent/nodes/__pycache__/speech_enrichment_node.cpython-313.pyc index 051a321..4527896 100644 Binary files a/src/agent/nodes/__pycache__/speech_enrichment_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/speech_enrichment_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/tim_complaint_analysis_node.cpython-313.pyc b/src/agent/nodes/__pycache__/tim_complaint_analysis_node.cpython-313.pyc index 2505174..ea16ef8 100644 Binary files a/src/agent/nodes/__pycache__/tim_complaint_analysis_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/tim_complaint_analysis_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/tim_complaint_node.cpython-313.pyc b/src/agent/nodes/__pycache__/tim_complaint_node.cpython-313.pyc index 9fbe019..ace91da 100644 Binary files a/src/agent/nodes/__pycache__/tim_complaint_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/tim_complaint_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/treatment_decision_node.cpython-313.pyc b/src/agent/nodes/__pycache__/treatment_decision_node.cpython-313.pyc index 2848823..7b2541b 100644 Binary files a/src/agent/nodes/__pycache__/treatment_decision_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/treatment_decision_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/undefined_complaint_operator_node.cpython-313.pyc b/src/agent/nodes/__pycache__/undefined_complaint_operator_node.cpython-313.pyc index a08901d..d7f3a76 100644 Binary files a/src/agent/nodes/__pycache__/undefined_complaint_operator_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/undefined_complaint_operator_node.cpython-313.pyc differ diff --git a/src/agent/nodes/__pycache__/validation_node.cpython-313.pyc b/src/agent/nodes/__pycache__/validation_node.cpython-313.pyc index 046a7f0..f1bf764 100644 Binary files a/src/agent/nodes/__pycache__/validation_node.cpython-313.pyc and b/src/agent/nodes/__pycache__/validation_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/__init__.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/__init__.cpython-313.pyc index 2d5a440..d6647e2 100644 Binary files a/src/agent/nodes/emulator/__pycache__/__init__.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/_rag_query.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/_rag_query.cpython-313.pyc index 18ee524..066f016 100644 Binary files a/src/agent/nodes/emulator/__pycache__/_rag_query.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/_rag_query.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/approve_draft_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/approve_draft_node.cpython-313.pyc index 7341903..0137397 100644 Binary files a/src/agent/nodes/emulator/__pycache__/approve_draft_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/approve_draft_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/close_case_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/close_case_node.cpython-313.pyc index f362425..f741a26 100644 Binary files a/src/agent/nodes/emulator/__pycache__/close_case_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/close_case_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/fetch_case_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/fetch_case_node.cpython-313.pyc index 12378fe..c377d97 100644 Binary files a/src/agent/nodes/emulator/__pycache__/fetch_case_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/fetch_case_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/generate_response_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/generate_response_node.cpython-313.pyc index 31a0a0a..383150d 100644 Binary files a/src/agent/nodes/emulator/__pycache__/generate_response_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/generate_response_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/persist_draft_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/persist_draft_node.cpython-313.pyc index d29a928..20af52b 100644 Binary files a/src/agent/nodes/emulator/__pycache__/persist_draft_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/persist_draft_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/retrieve_history_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/retrieve_history_node.cpython-313.pyc index ff0b035..87a16f4 100644 Binary files a/src/agent/nodes/emulator/__pycache__/retrieve_history_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/retrieve_history_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/retrieve_templates_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/retrieve_templates_node.cpython-313.pyc index bc9454d..9f3c6c5 100644 Binary files a/src/agent/nodes/emulator/__pycache__/retrieve_templates_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/retrieve_templates_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/router_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/router_node.cpython-313.pyc index d18f8ee..2d54aaa 100644 Binary files a/src/agent/nodes/emulator/__pycache__/router_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/router_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/start_response_emulation_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/start_response_emulation_node.cpython-313.pyc index eb783d7..f4f5185 100644 Binary files a/src/agent/nodes/emulator/__pycache__/start_response_emulation_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/start_response_emulation_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/validate_actions_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/validate_actions_node.cpython-313.pyc index dcf7590..9a83dce 100644 Binary files a/src/agent/nodes/emulator/__pycache__/validate_actions_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/validate_actions_node.cpython-313.pyc differ diff --git a/src/agent/nodes/emulator/__pycache__/validate_response_node.cpython-313.pyc b/src/agent/nodes/emulator/__pycache__/validate_response_node.cpython-313.pyc index 0a9ec40..741a95f 100644 Binary files a/src/agent/nodes/emulator/__pycache__/validate_response_node.cpython-313.pyc and b/src/agent/nodes/emulator/__pycache__/validate_response_node.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/__init__.cpython-313.pyc b/src/agent/state/__pycache__/__init__.cpython-313.pyc index 6dd702e..f8d3ab7 100644 Binary files a/src/agent/state/__pycache__/__init__.cpython-313.pyc and b/src/agent/state/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/__init__.cpython-39.pyc b/src/agent/state/__pycache__/__init__.cpython-39.pyc deleted file mode 100644 index c5b384b..0000000 Binary files a/src/agent/state/__pycache__/__init__.cpython-39.pyc and /dev/null differ diff --git a/src/agent/state/__pycache__/agent_state.cpython-313.pyc b/src/agent/state/__pycache__/agent_state.cpython-313.pyc index 512a35b..2c7bf5f 100644 Binary files a/src/agent/state/__pycache__/agent_state.cpython-313.pyc and b/src/agent/state/__pycache__/agent_state.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/agent_state.cpython-39.pyc b/src/agent/state/__pycache__/agent_state.cpython-39.pyc deleted file mode 100644 index 7c4f15a..0000000 Binary files a/src/agent/state/__pycache__/agent_state.cpython-39.pyc and /dev/null differ diff --git a/src/agent/state/__pycache__/step_helpers.cpython-313.pyc b/src/agent/state/__pycache__/step_helpers.cpython-313.pyc index 07d7d96..1e22c3f 100644 Binary files a/src/agent/state/__pycache__/step_helpers.cpython-313.pyc and b/src/agent/state/__pycache__/step_helpers.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/step_helpers_emulator.cpython-313.pyc b/src/agent/state/__pycache__/step_helpers_emulator.cpython-313.pyc index 1453263..4c8c95d 100644 Binary files a/src/agent/state/__pycache__/step_helpers_emulator.cpython-313.pyc and b/src/agent/state/__pycache__/step_helpers_emulator.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/step_notes.cpython-313.pyc b/src/agent/state/__pycache__/step_notes.cpython-313.pyc index c9e90ef..9e376d6 100644 Binary files a/src/agent/state/__pycache__/step_notes.cpython-313.pyc and b/src/agent/state/__pycache__/step_notes.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/step_notes_emulator.cpython-313.pyc b/src/agent/state/__pycache__/step_notes_emulator.cpython-313.pyc index 510ec60..1b4dba9 100644 Binary files a/src/agent/state/__pycache__/step_notes_emulator.cpython-313.pyc and b/src/agent/state/__pycache__/step_notes_emulator.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/steps.cpython-313.pyc b/src/agent/state/__pycache__/steps.cpython-313.pyc index 4350004..d4838ac 100644 Binary files a/src/agent/state/__pycache__/steps.cpython-313.pyc and b/src/agent/state/__pycache__/steps.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/steps_emulator.cpython-313.pyc b/src/agent/state/__pycache__/steps_emulator.cpython-313.pyc index 85dca01..d51dbfc 100644 Binary files a/src/agent/state/__pycache__/steps_emulator.cpython-313.pyc and b/src/agent/state/__pycache__/steps_emulator.cpython-313.pyc differ diff --git a/src/agent/state/__pycache__/transitions_emulator.cpython-313.pyc b/src/agent/state/__pycache__/transitions_emulator.cpython-313.pyc index 2085c6f..c9ba59b 100644 Binary files a/src/agent/state/__pycache__/transitions_emulator.cpython-313.pyc and b/src/agent/state/__pycache__/transitions_emulator.cpython-313.pyc differ diff --git a/src/api/LEGACY_ROUTES_DISABLED.md b/src/api/LEGACY_ROUTES_DISABLED.md index eab1a6c..d729e4b 100644 --- a/src/api/LEGACY_ROUTES_DISABLED.md +++ b/src/api/LEGACY_ROUTES_DISABLED.md @@ -1,5 +1,9 @@ -# Disabled legacy REST/executor layer +# Legacy routers disabled -The original route and executor modules were moved to `legacy_reference_disabled/original_develop/` and are kept only for audit/reference. +The original legacy API router/executor packages are not mounted by the active backend. -Active endpoints are implemented in `app/main.py` as thin adapters that call `BackofficeNativeRuntime.execute_workflow(...)`. +Active endpoints are implemented in `app/main.py` as compatibility REST adapters. They now translate the legacy payload into a `backoffice_rest` channel envelope, normalize it through the framework ChannelGateway path, and dispatch the requested LangGraph workflow through `BackofficeWorkflowDispatcher`. + +The dispatcher is the only layer that calls `BackofficeWorkflowExecutor.execute_workflow(...)`. FastAPI routes no longer call the workflow executor directly. + +This preserves legacy external contracts while keeping execution behind the framework channel/workflow boundary. diff --git a/src/api/__pycache__/__init__.cpython-313.pyc b/src/api/__pycache__/__init__.cpython-313.pyc index ed19873..a5ff6be 100644 Binary files a/src/api/__pycache__/__init__.cpython-313.pyc and b/src/api/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/api/dependencies/__pycache__/__init__.cpython-313.pyc b/src/api/dependencies/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 268b391..0000000 Binary files a/src/api/dependencies/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/api/dependencies/__pycache__/logging_context.cpython-313.pyc b/src/api/dependencies/__pycache__/logging_context.cpython-313.pyc deleted file mode 100644 index 5beeb8b..0000000 Binary files a/src/api/dependencies/__pycache__/logging_context.cpython-313.pyc and /dev/null differ diff --git a/src/api/middleware/__pycache__/__init__.cpython-313.pyc b/src/api/middleware/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 05c5bbe..0000000 Binary files a/src/api/middleware/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/api/middleware/__pycache__/error_handler.cpython-313.pyc b/src/api/middleware/__pycache__/error_handler.cpython-313.pyc deleted file mode 100644 index 2a7f282..0000000 Binary files a/src/api/middleware/__pycache__/error_handler.cpython-313.pyc and /dev/null differ diff --git a/src/api/middleware/__pycache__/logging.cpython-313.pyc b/src/api/middleware/__pycache__/logging.cpython-313.pyc deleted file mode 100644 index 309e361..0000000 Binary files a/src/api/middleware/__pycache__/logging.cpython-313.pyc and /dev/null differ diff --git a/src/api/schemas/__pycache__/__init__.cpython-313.pyc b/src/api/schemas/__pycache__/__init__.cpython-313.pyc index 693312f..562bab3 100644 Binary files a/src/api/schemas/__pycache__/__init__.cpython-313.pyc and b/src/api/schemas/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/abrt_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/abrt_schemas.cpython-313.pyc index 7f49b8c..fcf84f2 100644 Binary files a/src/api/schemas/__pycache__/abrt_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/abrt_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/anatel_response_emulator_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/anatel_response_emulator_schemas.cpython-313.pyc index a757953..f55225b 100644 Binary files a/src/api/schemas/__pycache__/anatel_response_emulator_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/anatel_response_emulator_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/anatel_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/anatel_schemas.cpython-313.pyc index ca0645a..8a0f512 100644 Binary files a/src/api/schemas/__pycache__/anatel_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/anatel_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/emulator_rag_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/emulator_rag_schemas.cpython-313.pyc deleted file mode 100644 index cf8d586..0000000 Binary files a/src/api/schemas/__pycache__/emulator_rag_schemas.cpython-313.pyc and /dev/null differ diff --git a/src/api/schemas/__pycache__/imdb_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/imdb_schemas.cpython-313.pyc index ecc1c13..1f1c263 100644 Binary files a/src/api/schemas/__pycache__/imdb_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/imdb_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/portability_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/portability_schemas.cpython-313.pyc index f72ad10..332eb52 100644 Binary files a/src/api/schemas/__pycache__/portability_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/portability_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/request.cpython-313.pyc b/src/api/schemas/__pycache__/request.cpython-313.pyc index 97bb5ea..b851534 100644 Binary files a/src/api/schemas/__pycache__/request.cpython-313.pyc and b/src/api/schemas/__pycache__/request.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/response.cpython-313.pyc b/src/api/schemas/__pycache__/response.cpython-313.pyc index 180fd6e..5e5ca2c 100644 Binary files a/src/api/schemas/__pycache__/response.cpython-313.pyc and b/src/api/schemas/__pycache__/response.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/siebel_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/siebel_schemas.cpython-313.pyc index 759371a..d4d89f0 100644 Binary files a/src/api/schemas/__pycache__/siebel_schemas.cpython-313.pyc and b/src/api/schemas/__pycache__/siebel_schemas.cpython-313.pyc differ diff --git a/src/api/schemas/__pycache__/tais_kb_schemas.cpython-313.pyc b/src/api/schemas/__pycache__/tais_kb_schemas.cpython-313.pyc deleted file mode 100644 index ab4dc36..0000000 Binary files a/src/api/schemas/__pycache__/tais_kb_schemas.cpython-313.pyc and /dev/null differ diff --git a/src/api/utils/__pycache__/agent_helpers.cpython-313.pyc b/src/api/utils/__pycache__/agent_helpers.cpython-313.pyc index 5e1051e..5cf30eb 100644 Binary files a/src/api/utils/__pycache__/agent_helpers.cpython-313.pyc and b/src/api/utils/__pycache__/agent_helpers.cpython-313.pyc differ diff --git a/src/api/utils/__pycache__/emulator_response_builder.cpython-313.pyc b/src/api/utils/__pycache__/emulator_response_builder.cpython-313.pyc deleted file mode 100644 index 75034e6..0000000 Binary files a/src/api/utils/__pycache__/emulator_response_builder.cpython-313.pyc and /dev/null differ diff --git a/src/compat/__pycache__/__init__.cpython-313.pyc b/src/compat/__pycache__/__init__.cpython-313.pyc index 57ae530..f10ed64 100644 Binary files a/src/compat/__pycache__/__init__.cpython-313.pyc and b/src/compat/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/compat/__pycache__/framework_observer.cpython-313.pyc b/src/compat/__pycache__/framework_observer.cpython-313.pyc index fe4fa6a..e7038b3 100644 Binary files a/src/compat/__pycache__/framework_observer.cpython-313.pyc and b/src/compat/__pycache__/framework_observer.cpython-313.pyc differ diff --git a/src/compat/__pycache__/framework_services.cpython-313.pyc b/src/compat/__pycache__/framework_services.cpython-313.pyc new file mode 100644 index 0000000..1289127 Binary files /dev/null and b/src/compat/__pycache__/framework_services.cpython-313.pyc differ diff --git a/src/components/__pycache__/__init__.cpython-313.pyc b/src/components/__pycache__/__init__.cpython-313.pyc index 46587ab..fa4c87f 100644 Binary files a/src/components/__pycache__/__init__.cpython-313.pyc and b/src/components/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/components/checkpointing/__pycache__/__init__.cpython-313.pyc b/src/components/checkpointing/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index f56add8..0000000 Binary files a/src/components/checkpointing/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/components/checkpointing/__pycache__/mongodb_saver.cpython-313.pyc b/src/components/checkpointing/__pycache__/mongodb_saver.cpython-313.pyc deleted file mode 100644 index f3167f7..0000000 Binary files a/src/components/checkpointing/__pycache__/mongodb_saver.cpython-313.pyc and /dev/null differ diff --git a/src/components/clients/__pycache__/abrt_client.cpython-313.pyc b/src/components/clients/__pycache__/abrt_client.cpython-313.pyc index 9589576..c2eb018 100644 Binary files a/src/components/clients/__pycache__/abrt_client.cpython-313.pyc and b/src/components/clients/__pycache__/abrt_client.cpython-313.pyc differ diff --git a/src/components/clients/__pycache__/emulator_rag_client.cpython-313.pyc b/src/components/clients/__pycache__/emulator_rag_client.cpython-313.pyc index 1bcd4eb..932f6a8 100644 Binary files a/src/components/clients/__pycache__/emulator_rag_client.cpython-313.pyc and b/src/components/clients/__pycache__/emulator_rag_client.cpython-313.pyc differ diff --git a/src/components/clients/__pycache__/imdb_client.cpython-313.pyc b/src/components/clients/__pycache__/imdb_client.cpython-313.pyc index 3b693bc..6bac22b 100644 Binary files a/src/components/clients/__pycache__/imdb_client.cpython-313.pyc and b/src/components/clients/__pycache__/imdb_client.cpython-313.pyc differ diff --git a/src/components/clients/__pycache__/portability_client.cpython-313.pyc b/src/components/clients/__pycache__/portability_client.cpython-313.pyc index 21f30b4..7b5f5db 100644 Binary files a/src/components/clients/__pycache__/portability_client.cpython-313.pyc and b/src/components/clients/__pycache__/portability_client.cpython-313.pyc differ diff --git a/src/components/clients/__pycache__/siebel_client.cpython-313.pyc b/src/components/clients/__pycache__/siebel_client.cpython-313.pyc index 57c0288..41c8b81 100644 Binary files a/src/components/clients/__pycache__/siebel_client.cpython-313.pyc and b/src/components/clients/__pycache__/siebel_client.cpython-313.pyc differ diff --git a/src/components/clients/__pycache__/speech_analytics_client.cpython-313.pyc b/src/components/clients/__pycache__/speech_analytics_client.cpython-313.pyc deleted file mode 100644 index b5e3d56..0000000 Binary files a/src/components/clients/__pycache__/speech_analytics_client.cpython-313.pyc and /dev/null differ diff --git a/src/components/clients/__pycache__/tais_kb_client.cpython-313.pyc b/src/components/clients/__pycache__/tais_kb_client.cpython-313.pyc index f7481e5..0e32fa5 100644 Binary files a/src/components/clients/__pycache__/tais_kb_client.cpython-313.pyc and b/src/components/clients/__pycache__/tais_kb_client.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/abrt_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/abrt_exceptions.cpython-313.pyc index 196b9b7..f74e66b 100644 Binary files a/src/components/clients/exceptions/__pycache__/abrt_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/abrt_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/emulator_rag_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/emulator_rag_exceptions.cpython-313.pyc index c2b8c53..29c4d9a 100644 Binary files a/src/components/clients/exceptions/__pycache__/emulator_rag_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/emulator_rag_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/imdb_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/imdb_exceptions.cpython-313.pyc index c03987c..55a75de 100644 Binary files a/src/components/clients/exceptions/__pycache__/imdb_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/imdb_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/portability_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/portability_exceptions.cpython-313.pyc index c8feb25..2f7ea0f 100644 Binary files a/src/components/clients/exceptions/__pycache__/portability_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/portability_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/siebel_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/siebel_exceptions.cpython-313.pyc index cf162c1..f11cdb3 100644 Binary files a/src/components/clients/exceptions/__pycache__/siebel_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/siebel_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/exceptions/__pycache__/speech_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/speech_exceptions.cpython-313.pyc deleted file mode 100644 index 63b7ee2..0000000 Binary files a/src/components/clients/exceptions/__pycache__/speech_exceptions.cpython-313.pyc and /dev/null differ diff --git a/src/components/clients/exceptions/__pycache__/tais_kb_exceptions.cpython-313.pyc b/src/components/clients/exceptions/__pycache__/tais_kb_exceptions.cpython-313.pyc index c8520f3..ef876d0 100644 Binary files a/src/components/clients/exceptions/__pycache__/tais_kb_exceptions.cpython-313.pyc and b/src/components/clients/exceptions/__pycache__/tais_kb_exceptions.cpython-313.pyc differ diff --git a/src/components/clients/rag/__pycache__/__init__.cpython-313.pyc b/src/components/clients/rag/__pycache__/__init__.cpython-313.pyc index c08dd8b..1502177 100644 Binary files a/src/components/clients/rag/__pycache__/__init__.cpython-313.pyc and b/src/components/clients/rag/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/components/clients/rag/__pycache__/history_rag_client.cpython-313.pyc b/src/components/clients/rag/__pycache__/history_rag_client.cpython-313.pyc index c2cafb5..5001fb6 100644 Binary files a/src/components/clients/rag/__pycache__/history_rag_client.cpython-313.pyc and b/src/components/clients/rag/__pycache__/history_rag_client.cpython-313.pyc differ diff --git a/src/components/clients/rag/__pycache__/templates_rag_client.cpython-313.pyc b/src/components/clients/rag/__pycache__/templates_rag_client.cpython-313.pyc index 415f0cc..9261d8a 100644 Binary files a/src/components/clients/rag/__pycache__/templates_rag_client.cpython-313.pyc and b/src/components/clients/rag/__pycache__/templates_rag_client.cpython-313.pyc differ diff --git a/src/components/tools/__pycache__/__init__.cpython-313.pyc b/src/components/tools/__pycache__/__init__.cpython-313.pyc deleted file mode 100644 index 05cfa1c..0000000 Binary files a/src/components/tools/__pycache__/__init__.cpython-313.pyc and /dev/null differ diff --git a/src/components/tools/__pycache__/example_tool.cpython-313.pyc b/src/components/tools/__pycache__/example_tool.cpython-313.pyc deleted file mode 100644 index 644590a..0000000 Binary files a/src/components/tools/__pycache__/example_tool.cpython-313.pyc and /dev/null differ diff --git a/src/core/__pycache__/__init__.cpython-313.pyc b/src/core/__pycache__/__init__.cpython-313.pyc index a1bc526..c2d81a9 100644 Binary files a/src/core/__pycache__/__init__.cpython-313.pyc and b/src/core/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/core/__pycache__/config.cpython-313.pyc b/src/core/__pycache__/config.cpython-313.pyc index 9ba030d..e8df772 100644 Binary files a/src/core/__pycache__/config.cpython-313.pyc and b/src/core/__pycache__/config.cpython-313.pyc differ diff --git a/src/core/__pycache__/exceptions.cpython-313.pyc b/src/core/__pycache__/exceptions.cpython-313.pyc index 9e05f81..c725b69 100644 Binary files a/src/core/__pycache__/exceptions.cpython-313.pyc and b/src/core/__pycache__/exceptions.cpython-313.pyc differ diff --git a/src/core/__pycache__/logging.cpython-313.pyc b/src/core/__pycache__/logging.cpython-313.pyc index e94d3de..279f458 100644 Binary files a/src/core/__pycache__/logging.cpython-313.pyc and b/src/core/__pycache__/logging.cpython-313.pyc differ diff --git a/src/core/__pycache__/prompt_manager.cpython-313.pyc b/src/core/__pycache__/prompt_manager.cpython-313.pyc index 97f973f..1aff66e 100644 Binary files a/src/core/__pycache__/prompt_manager.cpython-313.pyc and b/src/core/__pycache__/prompt_manager.cpython-313.pyc differ diff --git a/src/core/__pycache__/state_logger.cpython-313.pyc b/src/core/__pycache__/state_logger.cpython-313.pyc deleted file mode 100644 index ea466e7..0000000 Binary files a/src/core/__pycache__/state_logger.cpython-313.pyc and /dev/null differ diff --git a/src/core/streaming/__pycache__/client.cpython-313.pyc b/src/core/streaming/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 3bb0ad7..0000000 Binary files a/src/core/streaming/__pycache__/client.cpython-313.pyc and /dev/null differ diff --git a/src/infrastructure/oci/autonomous/__pycache__/__init__.cpython-313.pyc b/src/infrastructure/oci/autonomous/__pycache__/__init__.cpython-313.pyc index f6c7985..aae3160 100644 Binary files a/src/infrastructure/oci/autonomous/__pycache__/__init__.cpython-313.pyc and b/src/infrastructure/oci/autonomous/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/infrastructure/oci/autonomous/__pycache__/cases_manager.cpython-313.pyc b/src/infrastructure/oci/autonomous/__pycache__/cases_manager.cpython-313.pyc index ca8407d..95947c3 100644 Binary files a/src/infrastructure/oci/autonomous/__pycache__/cases_manager.cpython-313.pyc and b/src/infrastructure/oci/autonomous/__pycache__/cases_manager.cpython-313.pyc differ diff --git a/src/infrastructure/oci/autonomous/__pycache__/connection.cpython-313.pyc b/src/infrastructure/oci/autonomous/__pycache__/connection.cpython-313.pyc index 159a00c..6404bc2 100644 Binary files a/src/infrastructure/oci/autonomous/__pycache__/connection.cpython-313.pyc and b/src/infrastructure/oci/autonomous/__pycache__/connection.cpython-313.pyc differ diff --git a/src/infrastructure/oci/autonomous/__pycache__/memory_manager.cpython-313.pyc b/src/infrastructure/oci/autonomous/__pycache__/memory_manager.cpython-313.pyc index c9f85a3..17e54d5 100644 Binary files a/src/infrastructure/oci/autonomous/__pycache__/memory_manager.cpython-313.pyc and b/src/infrastructure/oci/autonomous/__pycache__/memory_manager.cpython-313.pyc differ diff --git a/src/infrastructure/streaming/__pycache__/client.cpython-313.pyc b/src/infrastructure/streaming/__pycache__/client.cpython-313.pyc deleted file mode 100644 index 9997f6f..0000000 Binary files a/src/infrastructure/streaming/__pycache__/client.cpython-313.pyc and /dev/null differ diff --git a/src/infrastructure/streaming/__pycache__/consumer.cpython-313.pyc b/src/infrastructure/streaming/__pycache__/consumer.cpython-313.pyc deleted file mode 100644 index 4b30cd5..0000000 Binary files a/src/infrastructure/streaming/__pycache__/consumer.cpython-313.pyc and /dev/null differ diff --git a/src/infrastructure/streaming/__pycache__/debug_producer.cpython-313.pyc b/src/infrastructure/streaming/__pycache__/debug_producer.cpython-313.pyc index 73afd34..899f6a2 100644 Binary files a/src/infrastructure/streaming/__pycache__/debug_producer.cpython-313.pyc and b/src/infrastructure/streaming/__pycache__/debug_producer.cpython-313.pyc differ diff --git a/src/infrastructure/streaming/__pycache__/producer.cpython-313.pyc b/src/infrastructure/streaming/__pycache__/producer.cpython-313.pyc deleted file mode 100644 index 26b6ce2..0000000 Binary files a/src/infrastructure/streaming/__pycache__/producer.cpython-313.pyc and /dev/null differ diff --git a/src/providers/__pycache__/langfuse_provider.cpython-313.pyc b/src/providers/__pycache__/langfuse_provider.cpython-313.pyc index 919492d..60741a3 100644 Binary files a/src/providers/__pycache__/langfuse_provider.cpython-313.pyc and b/src/providers/__pycache__/langfuse_provider.cpython-313.pyc differ diff --git a/src/providers/__pycache__/llm_provider.cpython-313.pyc b/src/providers/__pycache__/llm_provider.cpython-313.pyc index 5239a8c..d3ac139 100644 Binary files a/src/providers/__pycache__/llm_provider.cpython-313.pyc and b/src/providers/__pycache__/llm_provider.cpython-313.pyc differ diff --git a/src/utils/__pycache__/__init__.cpython-313.pyc b/src/utils/__pycache__/__init__.cpython-313.pyc index fc71056..c7b9af9 100644 Binary files a/src/utils/__pycache__/__init__.cpython-313.pyc and b/src/utils/__pycache__/__init__.cpython-313.pyc differ diff --git a/src/utils/__pycache__/cancellation_helpers.cpython-313.pyc b/src/utils/__pycache__/cancellation_helpers.cpython-313.pyc deleted file mode 100644 index d9f5570..0000000 Binary files a/src/utils/__pycache__/cancellation_helpers.cpython-313.pyc and /dev/null differ diff --git a/src/utils/__pycache__/decision_helpers.cpython-313.pyc b/src/utils/__pycache__/decision_helpers.cpython-313.pyc index 0b2d4d3..ec0586a 100644 Binary files a/src/utils/__pycache__/decision_helpers.cpython-313.pyc and b/src/utils/__pycache__/decision_helpers.cpython-313.pyc differ diff --git a/src/utils/__pycache__/external_response_builder.cpython-313.pyc b/src/utils/__pycache__/external_response_builder.cpython-313.pyc index c2eedfd..3e40e96 100644 Binary files a/src/utils/__pycache__/external_response_builder.cpython-313.pyc and b/src/utils/__pycache__/external_response_builder.cpython-313.pyc differ diff --git a/src/utils/__pycache__/forwarding_helpers.cpython-313.pyc b/src/utils/__pycache__/forwarding_helpers.cpython-313.pyc index 7d515b9..5c434d0 100644 Binary files a/src/utils/__pycache__/forwarding_helpers.cpython-313.pyc and b/src/utils/__pycache__/forwarding_helpers.cpython-313.pyc differ diff --git a/src/utils/__pycache__/http.cpython-313.pyc b/src/utils/__pycache__/http.cpython-313.pyc index 8860df0..2fb650a 100644 Binary files a/src/utils/__pycache__/http.cpython-313.pyc and b/src/utils/__pycache__/http.cpython-313.pyc differ diff --git a/src/utils/__pycache__/ics_collector.cpython-313.pyc b/src/utils/__pycache__/ics_collector.cpython-313.pyc index 72784d8..6d14f33 100644 Binary files a/src/utils/__pycache__/ics_collector.cpython-313.pyc and b/src/utils/__pycache__/ics_collector.cpython-313.pyc differ diff --git a/src/utils/__pycache__/loaders.cpython-313.pyc b/src/utils/__pycache__/loaders.cpython-313.pyc deleted file mode 100644 index be47377..0000000 Binary files a/src/utils/__pycache__/loaders.cpython-313.pyc and /dev/null differ diff --git a/src/utils/__pycache__/observer.cpython-313.pyc b/src/utils/__pycache__/observer.cpython-313.pyc index 497c33b..23b001d 100644 Binary files a/src/utils/__pycache__/observer.cpython-313.pyc and b/src/utils/__pycache__/observer.cpython-313.pyc differ diff --git a/src/utils/__pycache__/text.cpython-313.pyc b/src/utils/__pycache__/text.cpython-313.pyc index cdc0e71..e9bbd34 100644 Binary files a/src/utils/__pycache__/text.cpython-313.pyc and b/src/utils/__pycache__/text.cpython-313.pyc differ diff --git a/tools/__pycache__/validate_parity.cpython-313.pyc b/tools/__pycache__/validate_parity.cpython-313.pyc deleted file mode 100644 index ced5a45..0000000 Binary files a/tools/__pycache__/validate_parity.cpython-313.pyc and /dev/null differ diff --git a/tools/validate_parity.py b/tools/validate_parity.py index 2c81565..f97bbe3 100644 --- a/tools/validate_parity.py +++ b/tools/validate_parity.py @@ -3,7 +3,7 @@ from pathlib import Path REQUIRED = [ 'app/main.py', 'app/workflows/agent_graph.py', - 'app/workflows/backoffice_native_runtime.py', + 'app/workflows/backoffice_workflow_executor.py', 'app/agents/backoffice_agent.py', 'src/agent/nodes/fetch_ticket_node.py', 'src/agent/nodes/validation_node.py', @@ -55,7 +55,7 @@ for py in [p for p in Path('app').rglob('*.py') if '__pycache__' not in str(p)]: main = Path('app/main.py').read_text() needles = [ - 'BackofficeNativeRuntime', + 'BackofficeWorkflowExecutor', 'execute_workflow(', 'backoffice_checklist', 'backoffice_response_emulator', @@ -68,7 +68,7 @@ for n in needles: # Guardrails must be bound from the backoffice agent profile, not only from global config. -runtime = Path('app/workflows/backoffice_native_runtime.py').read_text(errors='ignore') +runtime = Path('app/workflows/backoffice_workflow_executor.py').read_text(errors='ignore') for needle in [ '_resolve_agent_profile("backoffice_anatel")', 'self.agent_profile["guardrails_config_path"]', @@ -91,7 +91,7 @@ print('OK: backoffice framework-native structural validation passed') print('python_files=', len(list(Path('.').rglob('*.py')))) # IC/NOC/GRL observability must be framework-native in the backoffice runtime. -runtime = Path('app/workflows/backoffice_native_runtime.py').read_text(errors='ignore') +runtime = Path('app/workflows/backoffice_workflow_executor.py').read_text(errors='ignore') for needle in [ '_safe_emit_ic', '_safe_emit_noc',