first commit

This commit is contained in:
2026-06-13 08:23:21 -03:00
commit 89c23fb0ed
439 changed files with 32801 additions and 0 deletions

826
app/main.py Normal file
View File

@@ -0,0 +1,826 @@
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 agent_framework.llm.providers import create_llm
from agent_framework.memory.message_history import create_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)
sessions = create_session_repository(settings)
checkpoints = create_checkpoint_repository(settings)
cache = create_cache(settings, telemetry=telemetry)
gateway = ChannelGateway()
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)
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)
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.
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)
logger.info("OCI producer de domínio inicializado; 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()
logger.info("LocalDebugProducer de domínio ativo")
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 para TAIS KB usando o service/cliente de domínio, não grafo legado."""
try:
from src.components.clients.tais_kb_client import TaisKbClient
query = body.get("query") or body.get("text") or body.get("complaint_description") or ""
client = TaisKbClient()
if hasattr(client, "search"):
result = await client.search(query=query, **{k: v for k, v in body.items() if k not in {"query", "text", "complaint_description"}})
elif hasattr(client, "query"):
result = await client.query(query)
else:
raise AttributeError("TAIS KB client has no search/query method")
return {"framework_native": True, "result": result}
except Exception as exc:
await telemetry.event("backoffice.tais_kb.failed", {"error": str(exc)}, kind="tool")
return {"framework_native": True, "result": [], "error": str(exc)}
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(),
},
}
# Late imports only for FastAPI annotation resolution. They are schemas, not
# workflow motors.
from src.api.schemas.anatel_schemas import TicketRequestEvent
from src.api.schemas.anatel_response_emulator_schemas import EmulatorGenerateRequest, EmulatorFinalizeRequest