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