from __future__ import annotations import logging from uuid import uuid4 import time from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse from pydantic import BaseModel from agent_framework.channels.base import ChannelResponse from agent_framework.channels.gateway import ChannelGateway from agent_framework.config.agent_registry import AgentProfileRegistry from agent_framework.config.settings import settings from agent_framework.analytics.factory import create_analytics_publisher from agent_framework.observability.observer import AgentObserver from src.compat.framework_observer import configure as configure_global_observer from src.compat.framework_services import configure as configure_framework_services from agent_framework.llm.providers import create_llm from agent_framework.memory.message_history import create_memory from agent_framework.memory.summary_memory import create_conversation_summary_memory from agent_framework.mcp.tool_router import create_mcp_tool_router from agent_framework.models.identity import AgentIdentity from agent_framework.identity import IdentityResolver, BusinessContext from agent_framework.models.session import ChatMessage, SessionContext from agent_framework.observability.telemetry import Telemetry from agent_framework.observability.context import set_observability_context, clear_observability_context from agent_framework.repositories.session_repository import create_session_repository from agent_framework.checkpoints.checkpoint_repository import create_checkpoint_repository 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.identity_extraction import enrich_payload_with_text_identity, extract_identity_from_text logging.basicConfig(level=settings.LOG_LEVEL) logger = logging.getLogger("agent_template_backend") app = FastAPI(title="Agent Template Backend FIRST-ready") app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) telemetry = Telemetry(settings) usage_repository = create_usage_repository(settings) llm = create_llm(settings, telemetry=telemetry, usage_repository=usage_repository) memory = create_memory(settings) summary_memory = create_conversation_summary_memory(settings, message_history=memory, llm=llm, telemetry=telemetry) sessions = create_session_repository(settings) checkpoints = create_checkpoint_repository(settings) cache = create_cache(settings, telemetry=telemetry) gateway = ChannelGateway(input_mode=settings.FRAMEWORK_CHANNEL_INPUT_MODE) analytics = create_analytics_publisher(settings) observer = AgentObserver(analytics=analytics) configure_global_observer({ "enabled": getattr(settings, "ENABLE_ANALYTICS", False), "providers": getattr(settings, "ANALYTICS_PROVIDERS", "oci_streaming"), "topic_path": getattr(settings, "GCP_PUBSUB_TOPIC_PATH", None) or getattr(settings, "AGENT_PUBSUB_TOPIC", None), }) tool_router = create_mcp_tool_router(settings, telemetry=telemetry) configure_framework_services(tool_router=tool_router) 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) logger.info("LLM provider carregado: %s", llm.__class__.__name__) logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST) logger.info("Analytics habilitado: %s providers=%s", getattr(settings, "ENABLE_ANALYTICS", False), getattr(settings, "ANALYTICS_PROVIDERS", "")) logger.info("Agentes disponíveis: %s", [p.agent_id for p in agent_profiles.list_profiles()]) @app.middleware("http") async def observability_context_middleware(request: Request, call_next): request_id = request.headers.get("x-request-id") or str(uuid4()) set_observability_context( request_id=request_id, channel=request.headers.get("x-channel") or "http", ura_call_id=request.headers.get("x-ura-call-id"), ) started = time.time() try: response = await call_next(request) response.headers["x-request-id"] = request_id await telemetry.event("http.request.completed", { "method": request.method, "path": request.url.path, "status_code": response.status_code, "duration_ms": int((time.time() - started) * 1000), }, kind="http") return response except Exception as exc: await telemetry.event("http.request.failed", { "method": request.method, "path": request.url.path, "error": str(exc), "duration_ms": int((time.time() - started) * 1000), }, kind="http") raise class GatewayRequest(BaseModel): channel: str = "web" payload: dict agent_id: str | None = None tenant_id: str | None = None def _resolve_identity(req: GatewayRequest, msg) -> tuple[AgentIdentity, dict, BusinessContext, list[str]]: payload = enrich_payload_with_text_identity(req.payload or {}) context = dict(msg.context or {}) tenant_id = req.tenant_id or payload.get("tenant_id") or context.get("tenant_id") or "default" agent_id = req.agent_id or payload.get("agent_id") or context.get("agent_id") or agent_profiles.default_agent_id profile = agent_profiles.get(agent_id) # 1) Identidade técnica do framework: isola tenant/agente/sessão. context.update({"tenant_id": tenant_id, "agent_id": profile.agent_id, "agent_profile": profile.__dict__}) identity = AgentIdentity.from_context(context, session_id=msg.session_id) # 2) Identidade de negócio: chaves canônicas vindas do front/canal. # Correção importante: identidade extraída explicitamente do texto da # mensagem atual (cpf/cnpj/protocolo) deve prevalecer sobre valores # estáveis herdados da sessão, como msisdn default do frontend. text_for_identity = str(payload.get("message") or payload.get("text") or payload.get("query") or msg.text or "") explicit_identity = extract_identity_from_text(text_for_identity) previous_business_context = dict(context.get("business_context") or context.get("identity") or {}) if explicit_identity.get("customer_key"): previous_business_context.pop("customer_key", None) if explicit_identity.get("interaction_key") or explicit_identity.get("protocol_id"): previous_business_context.pop("interaction_key", None) resolver_payload = {**context, **payload} # Garante que source business_context.customer_key não roube prioridade do # CPF/CNPJ explícito. O IdentityResolver do framework preserva estabilidade, # então inserimos a intenção explícita no próprio business_context da chamada. if explicit_identity: bc_override = dict(resolver_payload.get("business_context") or {}) if explicit_identity.get("customer_key"): bc_override["customer_key"] = explicit_identity["customer_key"] if explicit_identity.get("interaction_key"): bc_override["interaction_key"] = explicit_identity["interaction_key"] if bc_override: resolver_payload["business_context"] = bc_override business_context = identity_resolver.resolve( resolver_payload, session_id=identity.conversation_key(), previous=previous_business_context, ) missing_identity_keys = identity_resolver.validate(business_context) context.update({ "business_context": business_context.model_dump(), "business_keys": business_context.to_context_dict(), "identity_missing": missing_identity_keys, "conversation_key": identity.conversation_key(), "original_session_id": msg.session_id, }) return identity, context, business_context, missing_identity_keys async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) -> dict: msg = await gateway.normalize(req.channel, req.payload) identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg) agent_session_id = identity.conversation_key() message_id = (req.payload or {}).get("message_id") or str(uuid4()) set_observability_context( session_id=agent_session_id, user_id=msg.user_id, tenant_id=identity.tenant_id, agent_id=identity.agent_id, channel=msg.channel, message_id=message_id, ura_call_id=(req.payload or {}).get("ura_call_id") or normalized_context.get("ura_call_id") or business_context.interaction_key, ) stream = sse_hub.stream_for(agent_session_id) async with stream.lock: await sse_hub.emit(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}) if emit_sse else None session = await sessions.get(agent_session_id) if not session: context_fields = { k: v for k, v in normalized_context.items() if k in SessionContext.model_fields and k not in {"tenant_id", "agent_id", "session_id", "user_id", "channel", "channel_id"} } session = SessionContext( tenant_id=identity.tenant_id, agent_id=identity.agent_id, session_id=agent_session_id, user_id=msg.user_id, channel=msg.channel, channel_id=msg.channel_id, **context_fields, ) session.tenant_id = identity.tenant_id session.agent_id = identity.agent_id session.channel = msg.channel session.channel_id = msg.channel_id or session.channel_id await sessions.upsert(session) session.metadata = { **(session.metadata or {}), "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, "original_context": normalized_context, } await sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None await memory.append( agent_session_id, ChatMessage( role="user", content=msg.text, metadata={ **normalized_context, "agent_id": identity.agent_id, "tenant_id": identity.tenant_id, "message_id": message_id, "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, }, ), ) await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)] trace_input = { "text": msg.text, "channel": msg.channel, "channel_id": msg.channel_id, "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "conversation_key": agent_session_id, "message_id": message_id, "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, } async with telemetry.span( "agent.gateway_message", session_id=agent_session_id, user_id=session.user_id, channel=msg.channel, input=trace_input, tags=["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"], ): await telemetry.event("gateway.message.received", trace_input) await sse_hub.emit(agent_session_id, "workflow.started", trace_input) if emit_sse else None result = await workflow.ainvoke( { "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "session_id": agent_session_id, "conversation_key": agent_session_id, "agent_profile": normalized_context["agent_profile"], "user_text": msg.text, "history": history, "context": { **normalized_context, "session": session.model_dump(mode="json"), "original_session_id": msg.session_id, "session_id": agent_session_id, "conversation_key": agent_session_id, "user_id": session.user_id, "channel": msg.channel, "message_id": message_id, "business_context": business_context.model_dump(), "business_keys": business_context.to_context_dict(), "identity_missing": missing_identity_keys, }, } ) await checkpoints.put(agent_session_id, {"state": result, "message_id": message_id}) await sse_hub.emit(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}) if emit_sse else None answer = result.get("final_answer") or result.get("answer") or "" await memory.append( agent_session_id, ChatMessage( role="assistant", content=answer, metadata={ "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "message_id": f"assistant-{message_id}", "route": result.get("route"), "intent": result.get("intent"), "route_decision": result.get("route_decision"), "judges": result.get("judge_results"), }, ), ) await telemetry.event( "gateway.message.responded", { "session_id": agent_session_id, "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "route": result.get("route"), "intent": result.get("intent"), "answer_chars": len(answer), }, ) response = ChannelResponse( channel=msg.channel, session_id=agent_session_id, text=answer, metadata={ "channel_id": msg.channel_id, "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "original_session_id": msg.session_id, "conversation_key": agent_session_id, "message_id": message_id, "route": result.get("route"), "intent": result.get("intent"), "route_decision": result.get("route_decision"), "domain": result.get("domain"), "mcp_tools": result.get("mcp_tools"), "mcp_results": result.get("mcp_results"), "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, "judges": result.get("judge_results"), "guardrails": result.get("guardrail_decisions"), }, ) rendered = await gateway.render(response) await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None return rendered @app.get("/health") async def health(): return { "status": "ok", "llm_provider": settings.LLM_PROVIDER, "llm_class": llm.__class__.__name__, "langfuse_enabled": telemetry.is_enabled(), "agents": [p.agent_id for p in agent_profiles.list_profiles()], "default_agent_id": agent_profiles.default_agent_id, "routing_mode": settings.ROUTING_MODE, "sse_enabled": settings.ENABLE_SSE, "session_repository": settings.SESSION_REPOSITORY_PROVIDER, "memory_repository": settings.MEMORY_REPOSITORY_PROVIDER, "checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER, "usage_repository": settings.USAGE_REPOSITORY_PROVIDER, "identity_config_path": settings.IDENTITY_CONFIG_PATH, "mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH, } @app.get("/agents") async def list_agents(): return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]} @app.get("/debug/env") async def debug_env(): return { "APP_ENV": settings.APP_ENV, "LLM_PROVIDER": settings.LLM_PROVIDER, "ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE, "LANGFUSE_HOST": settings.LANGFUSE_HOST, "TELEMETRY_ENABLED": telemetry.is_enabled(), "SQLITE_DB_PATH": settings.SQLITE_DB_PATH, "SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER, "MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER, "CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER, "AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH, "ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH, "ROUTING_MODE": settings.ROUTING_MODE, } @app.get("/test-llm") async def test_llm(): async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}): answer = await llm.ainvoke([ {"role": "system", "content": "Responda de forma curta."}, {"role": "user", "content": "Diga apenas OK"}, ]) telemetry.flush() return {"provider": llm.__class__.__name__, "answer": answer} @app.post("/debug/route") async def debug_route(req: GatewayRequest): msg = await gateway.normalize(req.channel, req.payload) identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg) state = { "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "session_id": msg.session_id or "debug-session", "conversation_key": identity.conversation_key(), "agent_profile": context["agent_profile"], "user_text": msg.text, "sanitized_input": msg.text, "history": [], "context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()}, } if settings.ROUTING_MODE == "supervisor": plan = await workflow.supervisor.route_plan(state) return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata} decision = await workflow.router.route(state) data = decision.model_dump(mode="json") data["mode"] = "router" return data @app.post("/debug/identity") async def debug_identity(req: GatewayRequest): msg = await gateway.normalize(req.channel, req.payload) identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg) return { "technical_identity": { "tenant_id": identity.tenant_id, "agent_id": identity.agent_id, "conversation_key": identity.conversation_key(), "original_session_id": msg.session_id, }, "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, "context_keys": sorted(context.keys()), } @app.get("/debug/usage") async def debug_usage(tenant_id: str | None = None, session_id: str | None = None): return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id) @app.get("/debug/mcp/tools") async def debug_mcp_tools(): return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()} @app.post("/debug/mcp/call/{tool_name}") async def debug_mcp_call(tool_name: str, arguments: dict | None = None): arguments = arguments or {} ctx = arguments.get("business_context") or arguments.get("identity") or {} result = await tool_router.call( tool_name, arguments, business_context=ctx, original_context=arguments, ) return result.model_dump(mode="json") @app.post("/gateway/message") async def gateway_message(req: GatewayRequest): return await _process_gateway_message(req, emit_sse=False) @app.post("/gateway/message/sse") async def gateway_message_sse(req: GatewayRequest): return await _process_gateway_message(req, emit_sse=True) @app.get("/gateway/events/{session_id}") async def gateway_events(session_id: str, request: Request): last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0" return StreamingResponse( sse_hub.subscribe(session_id, int(last)), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}, ) @app.get("/sessions/{session_id}/messages") async def get_session_messages(session_id: str, limit: int = 50): return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]} @app.get("/sessions/{session_id}/checkpoint") async def get_session_checkpoint(session_id: str): return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)} @app.on_event("shutdown") async def shutdown(): telemetry.shutdown() # --------------------------------------------------------------------------- # Backoffice TIM/ANATEL develop — execução 100% framework-native # --------------------------------------------------------------------------- # 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. from src.api.schemas.anatel_schemas import TicketRequestEvent from src.api.schemas.anatel_response_emulator_schemas import EmulatorGenerateRequest, EmulatorFinalizeRequest import asyncio from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi import status, HTTPException @app.on_event("startup") async def startup_backoffice_native_domain(): """Inicializa apenas recursos de domínio; o motor de workflow é do framework.""" try: from src.utils.observer import setup_observer setup_observer() except Exception: logger.warning("Observer de domínio não pôde ser inicializado", exc_info=True) try: from src.infrastructure.oci.autonomous import db_manager as original_db_manager app.state.original_db_manager = original_db_manager await original_db_manager.connect() logger.info("Autonomous/Mongo manager de domínio conectado") except Exception: logger.warning("DB de domínio indisponível; fluxos continuam quando os nós suportarem fallback/mock", exc_info=True) # Compatibilidade para nós que esperam app.state.oci_producer; a criação real # continua opcional e não controla o grafo. try: from src.core.config import settings as original_settings if getattr(original_settings, "ENABLE_OCI_STREAMING", False): from src.infrastructure.streaming.producer import OciProducer app.state.oci_producer = OciProducer(original_settings.OCI_RESPONSE_STREAM_OCID) configure_framework_services(progress_producer=app.state.oci_producer) logger.info("OCI producer de domínio inicializado via framework services; consumer legado não é iniciado") else: try: from src.infrastructure.streaming.debug_producer import LocalDebugProducer if getattr(original_settings, "DEBUG", False): app.state.oci_producer = LocalDebugProducer() configure_framework_services(progress_producer=app.state.oci_producer) logger.info("LocalDebugProducer de domínio ativo via framework services") except Exception: pass except Exception: logger.warning("Producer de domínio não inicializado", exc_info=True) @app.on_event("shutdown") async def shutdown_backoffice_native_domain(): try: dbm = getattr(app.state, "original_db_manager", None) if dbm is not None: await dbm.close() except Exception: logger.warning("Falha ao encerrar DB de domínio", exc_info=True) @app.exception_handler(RequestValidationError) async def native_validation_exception_handler(request: Request, exc: RequestValidationError): """Preserva o formato de erro ANATEL sem ativar rotas/executors legados.""" try: from src.api.schemas.anatel_schemas import ERROR_CODE_MAPPING, ReasonCode error_messages = [] for err in exc.errors(): loc_tuple = tuple(l for l in err.get("loc", []) if l != "body") is_enum_error = err.get("type", "").startswith("enum") target_fields = [("complaint", "inputChannel"), ("caseType",)] if is_enum_error and loc_tuple in target_fields: reason_code = ReasonCode.INVALID_VALUE field_name = loc_tuple[-1] reason_text = f"Invalid value for field {field_name} or it's not supported yet" else: mapping_result = ERROR_CODE_MAPPING.get(loc_tuple) if mapping_result: reason_code, reason_text = mapping_result else: reason_code = ReasonCode.FIELD_ERROR loc_str = " -> ".join([str(l) for l in err.get("loc", [])]) reason_text = f"{loc_str}: {err.get('msg', 'Invalid field')}" error_messages.append({"code": reason_code.value, "text": reason_text}) try: body = await request.json() correlation_id = body.get("transactionId") or body.get("correlation_id") or "unknown" except Exception: correlation_id = "unknown" return JSONResponse( status_code=status.HTTP_400_BAD_REQUEST, content={"title": "validation error", "status": 400, "correlation_id": correlation_id, "detail": {"messages": error_messages}}, ) except Exception: return JSONResponse(status_code=422, content={"detail": exc.errors()}) async def _run_native_checklist(event, request: Request): 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", }, ) try: response_event = agent_helpers.build_cms_response_event(final_state, transaction_id) return response_event.model_dump(mode="json", by_alias=True) except Exception: return { "transactionId": transaction_id, "framework_native": True, "current_step": str(final_state.get("current_step")), "final_response": final_state.get("final_response"), "error": final_state.get("error"), "metadata": final_state.get("metadata", {}), } @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) @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) @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) @app.post("/agent/search-tais-kb", status_code=status.HTTP_200_OK) async def native_search_tais_kb(body: dict): """Adapter REST framework-native para TAIS KB via MCP Router.""" from src.compat.framework_services import call_mcp_tool query = body.get("query") or body.get("text") or body.get("complaint_description") or "" result = await call_mcp_tool( "consultar_tais_kb", {"query": query, "protocol_id": body.get("protocol_id") or body.get("complaintProtocol"), "customer_key": body.get("customer_key") or body.get("cpfCnpj")}, business_context={"customer_key": body.get("customer_key") or body.get("cpfCnpj"), "interaction_key": body.get("protocol_id") or body.get("complaintProtocol")}, original_context=body, ) if not result.get("ok"): await telemetry.event("backoffice.tais_kb.failed", {"error": result.get("error")}, kind="tool") 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", }, ) 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) except Exception: return { "transactionId": transaction_id, "framework_native": True, "current_step": str(final_state.get("current_step")), "final_response": final_state.get("final_response"), "error": final_state.get("error"), "metadata": final_state.get("metadata", {}), } @app.post("/case/{transaction_id}/response-emulator/generate", status_code=status.HTTP_200_OK) async def native_response_emulator_generate(transaction_id: str, request: Request, body: EmulatorGenerateRequest): from src.api.schemas.anatel_response_emulator_schemas import ResponseEmulatorRequestEvent, OperatorFeedback if body.transactionId != transaction_id: raise HTTPException(status_code=400, detail="transactionId path/body mismatch") if body.action == "generate": event = ResponseEmulatorRequestEvent( transactionId=transaction_id, type="first_response", flow_mode="generate", selected_actions=body.selected_actions or [], ) else: event = ResponseEmulatorRequestEvent( transactionId=transaction_id, type="regenerate", flow_mode="generate", selected_actions=[], previous_response="", feedback=OperatorFeedback(comment=body.operator_instructions or ""), ) return await _run_native_emulator(request, event) @app.post("/case/{transaction_id}/response-emulator/finalize", status_code=status.HTTP_200_OK) async def native_response_emulator_finalize(transaction_id: str, request: Request, body: EmulatorFinalizeRequest): from src.api.schemas.anatel_response_emulator_schemas import ResponseEmulatorRequestEvent if body.transactionId != transaction_id: raise HTTPException(status_code=400, detail="transactionId path/body mismatch") event = ResponseEmulatorRequestEvent( transactionId=transaction_id, type="first_response", flow_mode=body.action, selected_actions=[], ) return await _run_native_emulator(request, event) @app.get("/case/{transaction_id}/response-emulator", status_code=status.HTTP_200_OK) async def native_response_emulator_status(transaction_id: str): """Status reader nativo. Lê DB de domínio quando disponível, sem rota legada.""" try: from src.core.config import settings as original_settings dbm = getattr(app.state, "original_db_manager", None) db = getattr(dbm, "db", None) if db is None: return {"transactionId": transaction_id, "framework_native": True, "status": None, "detail": "domain DB unavailable"} coll = db[original_settings.AUTONOMOUS_NOSQL_COLLECTION] doc = await coll.find_one({"transactionId": transaction_id}) if not doc: return {"transactionId": transaction_id, "framework_native": True, "status": None} processing = doc.get("processing") or {} return { "transactionId": transaction_id, "framework_native": True, "status": processing.get("status"), "current_step": processing.get("current_step"), "case_response": processing.get("case_response") or doc.get("case_response"), "validation": (doc.get("metadata") or {}).get("validation"), "selected_actions_count": len((doc.get("metadata") or {}).get("selected_actions") or []), "transitions": doc.get("transitions") or [], "last_updated_at": processing.get("updated_at") or processing.get("timestamp"), } except Exception as exc: return {"transactionId": transaction_id, "framework_native": True, "status": None, "error": str(exc)} @app.post("/emulator-rag/search", status_code=status.HTTP_200_OK) async def native_emulator_rag_search(body: dict): try: from src.agent.nodes.emulator._rag_query import query_emulator_rag result = await query_emulator_rag(body) return {"framework_native": True, "result": result} except Exception as exc: return {"framework_native": True, "result": [], "error": str(exc)} @app.get("/health/live") async def native_health_live(): return {"status": "live", "framework_native": True} @app.get("/health/ready") async def native_health_ready(): return { "status": "ready", "framework_native": True, "workflows": list(backoffice_runtime._graphs.keys()), "framework_layers": { "gateway": True, "identity": True, "session_repository": settings.SESSION_REPOSITORY_PROVIDER, "memory_repository": settings.MEMORY_REPOSITORY_PROVIDER, "checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER, "guardrails": True, "judges": True, "supervisor": True, "mcp_router": tool_router.enabled, "telemetry": telemetry.is_enabled(), }, } @app.get("/debug/backoffice/parity") async def debug_backoffice_parity(): return { "mode": "framework_native_domain_workflows", "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", "domain_package": "src.agent.nodes + src.components.clients + src.agent.local_prompts", "workflows": { "backoffice_checklist": [ "framework_input_guardrails", "fetch_ticket", "validation", "bypass_rules", "cache_check", "imdb_enrichment", "identity_verification", "speech_enrichment", "knowledge_base_enrichment", "canceling_analysis", "tim_complaint_analysis", "operator_route", "reclassification_analysis", "treatment_decision", "siebel_sr_opening", "framework_output_supervisor", "framework_output_guardrails", "framework_judges", "framework_supervisor_review", "framework_persist" ], "backoffice_response_emulator": [ "framework_input_guardrails", "start_response_emulation", "fetch_case", "validate_actions", "router", "retrieve_templates", "retrieve_history", "generate_response", "validate_response", "persist_draft", "approve_draft", "close_case", "framework_output_supervisor", "framework_output_guardrails", "framework_judges", "framework_supervisor_review", "framework_persist" ], }, "framework_layers": { "gateway": True, "identity": True, "session_repository": settings.SESSION_REPOSITORY_PROVIDER, "memory_repository": settings.MEMORY_REPOSITORY_PROVIDER, "checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER, "guardrails": True, "judges": True, "supervisor": True, "mcp_router": tool_router.enabled, "telemetry": telemetry.is_enabled(), }, }