first commit

This commit is contained in:
2026-06-19 23:15:16 -03:00
parent 9742a24d44
commit cfdb96d473
93 changed files with 40 additions and 0 deletions

View File

@@ -0,0 +1,167 @@
###############################################################################
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
###############################################################################
APP_NAME=ai-agent-template
APP_ENV=local
LOG_LEVEL=INFO
API_HOST=0.0.0.0
API_PORT=8000
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
###############################################################################
# LLM - OCI Generative AI como provider principal
###############################################################################
# Opções: mock, oci_openai, oci_sdk, openai_compatible
LLM_PROVIDER=oci_openai
LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048
LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
OCI_GENAI_PROJECT_OCID=
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
OCI_AUTH_MODE=config_file
# OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=DEFAULT
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OCI_REGION=us-chicago-1
###############################################################################
# Persistência
###############################################################################
# Opções: memory, autonomous, mongodb
SESSION_REPOSITORY_PROVIDER=autonomous
MEMORY_REPOSITORY_PROVIDER=autonomous
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
ADB_PASSWORD=fjhsdf04954hf
ADB_DSN=oradb23aidev_high
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
ADB_WALLET_PASSWORD=fjhsdf04954hf
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
MONGODB_DATABASE=agent_platform
# Redis
REDIS_URL=redis://localhost:6379/0
ENABLE_REDIS_CACHE=false
###############################################################################
# RAG / Vector / Graph
###############################################################################
VECTOR_STORE_PROVIDER=memory
GRAPH_STORE_PROVIDER=memory
RAG_TOP_K=5
EMBEDDING_PROVIDER=mock
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
###############################################################################
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
###############################################################################
# Analytics / Observer corporativo
###############################################################################
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=oci_streaming
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH=
GCP_PROJECT_ID=
GCP_PUBSUB_TOPIC=
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
###############################################################################
# OCI Streaming
###############################################################################
ENABLE_OCI_STREAMING=false
OCI_STREAM_ENDPOINT=
OCI_STREAM_OCID=
OCI_STREAM_PARTITION_KEY=agent-events
###############################################################################
# Guardrails, Judges, Supervisor
###############################################################################
ENABLE_INPUT_GUARDRAILS=true
ENABLE_OUTPUT_GUARDRAILS=true
ENABLE_JUDGES=true
ENABLE_SUPERVISOR=true
ENABLE_OUTPUT_SUPERVISOR=true
ENABLE_PARALLEL_GUARDRAILS=true
GUARDRAILS_FAIL_FAST=true
OUTPUT_SUPERVISOR_MAX_RETRIES=3
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
JUDGES_CONFIG_PATH=./config/judges.yaml
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
###############################################################################
# Gateway de canais
###############################################################################
DEFAULT_CHANNEL=web
# embedded = backend may parse simple/native channel payloads.
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
ENABLE_VOICE_ADAPTER=true
ENABLE_WHATSAPP_ADAPTER=true
ENABLE_TEXT_ADAPTER=true
#################################################
# ENTERPRISE ROUTING
#################################################
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
ROUTING_CONFIG_PATH=./config/routing.yaml
# true = usa LLM para classificar quando keywords/estado não resolverem.
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
ENABLE_LLM_ROUTER=true
###############################################################################
# MCP / Tools
###############################################################################
ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
ROUTING_MODE=router
# Usage/cost accounting
USAGE_REPOSITORY_PROVIDER=autonomous
IDENTITY_CONFIG_PATH=./config/identity.yaml
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
# -----------------------------------------------------------------------------
# ConversationSummaryMemory / compressão de contexto conversacional
# -----------------------------------------------------------------------------
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
MEMORY_CONTEXT_STRATEGY=summary
MEMORY_HISTORY_LIMIT=80
MEMORY_RECENT_MESSAGES_LIMIT=8
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true

View File

@@ -0,0 +1,6 @@
FROM python:3.12-slim
WORKDIR /app
COPY agent_framework /agent_framework
COPY agent_template_backend /app
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,86 @@
# agent_template_backend_day_zero
Este folder é uma cópia do `agent_template_backend`, porém transformada em um template **Day Zero**.
A ideia é o desenvolvedor começar um agente novo sem apagar manualmente exemplos de negócio.
## O que foi mantido
Foi mantida a estrutura original do backend:
- `app/main.py`
- `app/workflows/agent_graph.py`
- `app/state.py`
- `app/agents/runtime.py`
- `app/agents/prompting.py`
- configurações em `config/`
- integração com `agent_framework`
- Analytics / Observer
- NOC / GRL
- OutputSupervisor
- MCP Router
- RAG
- cache
- memória
- checkpoints
- Langfuse / OTEL
## O que foi comentado
As implementações de exemplo dos agentes foram comentadas nos arquivos:
- `app/agents/billing_agent.py`
- `app/agents/product_agent.py`
- `app/agents/orders_agent.py`
- `app/agents/support_agent.py`
Cada arquivo contém:
1. um esqueleto funcional mínimo;
2. comentários `TODO` para o desenvolvedor;
3. a implementação original comentada no final do arquivo.
## Como desenvolver um novo agente
1. Escolha qual classe vai reutilizar inicialmente, por exemplo `BillingAgent`.
2. Edite o método `run()`.
3. Ajuste o prompt em `apply_agent_profile_prompt(...)`.
4. Descomente MCP se precisar de tools:
```python
# tool_context = await self._collect_tool_context(state)
```
5. Descomente RAG se precisar de base de conhecimento:
```python
# rag_context, rag_metadata = await self._retrieve_rag_context(state)
```
6. Ajuste o retorno:
```python
return {
"answer": answer,
"next_state": "MEU_ESTADO",
}
```
## O que o desenvolvedor normalmente altera
- `app/agents/*.py`
- `config/routing.yaml`
- `config/agents.yaml`
- `config/tools.yaml`
- `config/mcp_servers.yaml`
- `config/mcp_parameter_mapping.yaml`
- `.env`
## O que normalmente não deve ser alterado no início
- `app/main.py`
- `app/workflows/agent_graph.py`
- `app/state.py`
- `app/agents/runtime.py`
Esses arquivos são o esqueleto de execução usando o framework.

View File

@@ -0,0 +1,12 @@
# Desenvolvimento de agentes
Os arquivos `billing_agent.py`, `product_agent.py`, `orders_agent.py` e `support_agent.py` foram mantidos com os mesmos nomes do template completo para o workflow continuar compatível.
A implementação de negócio original está comentada no final de cada arquivo.
Para criar seu agente:
1. Edite o método `run()` da classe desejada.
2. Use o bloco comentado como referência.
3. Depois, ajuste o roteamento em `config/routing.yaml`.
4. Se quiser renomear classes/arquivos, atualize também os imports em `app/workflows/agent_graph.py`.

View File

@@ -0,0 +1,67 @@
"""
DAY ZERO TEMPLATE - BillingAgent
Esqueleto mínimo já compatível com ConversationSummaryMemory.
Substitua o prompt e a regra de negócio conforme o seu agente.
"""
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class BillingAgent(AgentRuntimeMixin):
name = "billingAgent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
# OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
tool_context = []
rag_context = None
rag_metadata = {}
# Prepara a memória resumida antes do prompt.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "BillingAgent", messages)
return {
"answer": answer,
"next_state": "DAY_ZERO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,67 @@
"""
DAY ZERO TEMPLATE - OrdersAgent
Esqueleto mínimo já compatível com ConversationSummaryMemory.
Substitua o prompt e a regra de negócio conforme o seu agente.
"""
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class OrdersAgent(AgentRuntimeMixin):
name = "orders_agent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
# OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
tool_context = []
rag_context = None
rag_metadata = {}
# Prepara a memória resumida antes do prompt.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "OrdersAgent", messages)
return {
"answer": answer,
"next_state": "DAY_ZERO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,67 @@
"""
DAY ZERO TEMPLATE - ProductAgent
Esqueleto mínimo já compatível com ConversationSummaryMemory.
Substitua o prompt e a regra de negócio conforme o seu agente.
"""
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class ProductAgent(AgentRuntimeMixin):
name = "productAgent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
# OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
tool_context = []
rag_context = None
rag_metadata = {}
# Prepara a memória resumida antes do prompt.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "ProductAgent", messages)
return {
"answer": answer,
"next_state": "DAY_ZERO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,15 @@
from __future__ import annotations
def apply_agent_profile_prompt(state: dict, default_prompt: str) -> str:
"""Adiciona o prefixo de prompt configurado para o agent_template selecionado.
Cada agent_id pode definir metadata.system_prefix em config/agents.yaml. Isso
mantém prompts isolados sem duplicar o código dos agentes especializados.
"""
profile = state.get("agent_profile") or (state.get("context") or {}).get("agent_profile") or {}
metadata = profile.get("metadata") or {}
prefix = (metadata.get("system_prefix") or "").strip()
if not prefix:
return default_prompt
return f"{prefix}\n\n{default_prompt}"

View File

@@ -0,0 +1,7 @@
from __future__ import annotations
# Compatibilidade local do template/backend.
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]

View File

@@ -0,0 +1,67 @@
"""
DAY ZERO TEMPLATE - SupportAgent
Esqueleto mínimo já compatível com ConversationSummaryMemory.
Substitua o prompt e a regra de negócio conforme o seu agente.
"""
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class SupportAgent(AgentRuntimeMixin):
name = "support_agent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
# OPCIONAL: habilite quando seu agente precisar de MCP/RAG.
tool_context = []
rag_context = None
rag_metadata = {}
# Prepara a memória resumida antes do prompt.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente de suporte de varejo para troca, devolução e garantia.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
return {
"answer": answer,
"next_state": "DAY_ZERO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,468 @@
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.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.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.observability.telemetry_observer import TelemetryBackedAgentObserver
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()
analytics = create_analytics_publisher(settings)
observer = TelemetryBackedAgentObserver(telemetry=telemetry)
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, summary_memory=summary_memory)
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 = 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.
# Estas chaves são estáveis na sessão e seguem até agentes e MCP Router.
previous_business_context = context.get("business_context") or context.get("identity") or {}
business_context = identity_resolver.resolve(
{**payload, **context},
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()

View File

@@ -0,0 +1,84 @@
from __future__ import annotations
"""Observer adapter that emits IC/NOC/GRL through framework Telemetry only.
This avoids a second Langfuse root trace created by AgentObserver ->
AnalyticsPublisher while preserving the events inside the active request span.
"""
from datetime import datetime, timezone
from typing import Any
def _normalize_ic_code(code: str) -> str:
code = str(code or "UNKNOWN").strip()
return code if code.startswith(("IC.", "AGA.", "NOC.", "GRL.")) else f"IC.{code}"
def _normalize_noc_code(code: str) -> str:
code = str(code or "UNKNOWN").strip()
return code if code.startswith("NOC.") else f"NOC.{code}"
def _normalize_grl_code(code: str) -> str:
code = str(code or "UNKNOWN").strip()
return code if code.startswith("GRL.") else f"GRL.{code}"
def _kind_for(event_type: str) -> str:
if event_type.startswith(("IC.", "AGA.")):
return "ic"
if event_type.startswith("NOC."):
return "noc"
if event_type.startswith("GRL."):
return "grl"
return "event"
class TelemetryBackedAgentObserver:
"""Drop-in subset of AgentObserver backed by Telemetry.event.
Do not publish through AnalyticsPublisher here. Analytics publishing may be
configured with a Langfuse provider, and that path creates an extra root
trace for business events such as IC.AGENT_COMPLETED/NOC.006. Telemetry.event
uses the active span/trace context, so these events appear inside the single
request trace.
"""
def __init__(self, telemetry: Any, *, source: str = "agent_framework") -> None:
self.telemetry = telemetry
self.source = source
async def emit(
self,
event_type: str,
payload: dict[str, Any] | None = None,
*,
metadata: dict[str, Any] | None = None,
source: str | None = None,
) -> dict[str, Any]:
body = dict(payload or {})
meta = dict(metadata or {})
body.setdefault("tag", event_type)
event = {
"eventType": event_type,
"source": source or self.source,
"eventDate": datetime.now(timezone.utc).isoformat(),
"body": body,
"metadata": meta,
}
try:
await self.telemetry.event(event_type, event, kind=_kind_for(event_type))
except TypeError:
# Compatibility with older Telemetry.event signatures.
await self.telemetry.event(event_type, event)
return event
async def emit_ic(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
return await self.emit(_normalize_ic_code(code), payload, metadata={**metadata, "ic": True})
async def emit_noc(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
return await self.emit(_normalize_noc_code(code), payload, metadata={**metadata, "noc": True})
async def emit_grl(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
return await self.emit(_normalize_grl_code(code), payload, metadata={**metadata, "grl": True})

View File

@@ -0,0 +1,34 @@
from typing import Any, TypedDict
class AgentState(TypedDict, total=False):
tenant_id: str
agent_id: str
session_id: str
conversation_key: str
agent_profile: dict[str, Any]
user_text: str
sanitized_input: str
route: str
intent: str
route_decision: dict[str, Any]
answer: str
final_answer: str
history: list[dict[str, Any]]
context: dict[str, Any]
guardrail_decisions: list[dict[str, Any]]
judge_results: list[dict[str, Any]]
next_state: str
domain: str
mcp_tools: list[str]
mcp_results: list[dict[str, Any]]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
active_agent: str
blocked: bool
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool

View File

@@ -0,0 +1,618 @@
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from langgraph.graph import END, START, StateGraph
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.routing.enterprise_router import EnterpriseRouter
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 app.agents.billing_agent import BillingAgent
from app.agents.product_agent import ProductAgent
from app.agents.orders_agent import OrdersAgent
from app.agents.support_agent import SupportAgent
from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.rag.embedding_provider import create_embedding_provider
from agent_framework.cache.cache import create_cache
class LegacyOutputGuardrailRail:
"""Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo.
O framework antigo retornava decisões allowed=True/False. O OutputSupervisor
corporativo trabalha com RailAction (allow/sanitize/retry/block/handover).
Este adapter evita reescrever todos os rails agora e mantém compatibilidade.
"""
code = "LEGACY_OUTPUT_GUARDRAILS"
def __init__(self, pipeline: GuardrailPipeline):
self.pipeline = pipeline
async def evaluate(self, candidate: str, context: dict):
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={"legacy_decisions": serialized},
)
if final != candidate:
return RailResult(
code=self.code,
action=RailAction.SANITIZE,
reason="Resposta sanitizada por guardrail de saída legado.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
return RailResult(
code=self.code,
action=RailAction.ALLOW,
reason="Resposta aprovada pelos guardrails de saída legados.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
class AgentWorkflow:
"""Workflow principal com dois modos de roteamento.
Modos suportados por configuração:
ROUTING_MODE=router
input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails
ROUTING_MODE=supervisor
input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação
Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id.
"""
def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None):
self.llm = llm
self.memory = memory
self.telemetry = telemetry
self.analytics = analytics
self.observer = observer or AgentObserver(analytics=analytics)
self.settings = settings
self.tool_router = tool_router
self.summary_memory = summary_memory
self.guardrails = GuardrailPipeline(
observer=self.observer,
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.output_supervisor_engine = OutputSupervisor(
rails=[LegacyOutputGuardrailRail(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.cache = create_cache(settings)
self.embedding_provider = create_embedding_provider(settings)
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
agent_kwargs = {"telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory}
self.billing = BillingAgent(llm, **agent_kwargs)
self.product = ProductAgent(llm, **agent_kwargs)
self.orders = OrdersAgent(llm, **agent_kwargs)
self.support = SupportAgent(llm, **agent_kwargs)
self.graph = self._build_graph()
def _node(self, name, fn):
async def _wrapped(state):
async with self.langgraph_telemetry.node(name, state):
return await fn(state)
return _wrapped
def _build_graph(self):
builder = StateGraph(AgentState)
builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision))
builder.add_node("billing_agent", self._node("billing_agent", self.billing_agent))
builder.add_node("product_agent", self._node("product_agent", self.product_agent))
builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent))
builder.add_node("support_agent", self._node("support_agent", self.support_agent))
builder.add_node("handoff", self._node("handoff", self.handoff))
builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent))
builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor))
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
builder.add_node("judge", self._node("judge", self.judge))
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
builder.add_node("persist", self._node("persist", self.persist))
builder.add_edge(START, "input_guardrails")
builder.add_conditional_edges(
"input_guardrails",
self._after_input_guardrails,
{"blocked": "persist", "continue": "routing_decision"},
)
builder.add_conditional_edges(
"routing_decision",
lambda s: s.get("route", "billing_agent"),
{
"billing_agent": "billing_agent",
"product_agent": "product_agent",
"orders_agent": "orders_agent",
"support_agent": "support_agent",
"handoff": "handoff",
"supervisor_agent": "supervisor_agent",
},
)
builder.add_edge("billing_agent", "output_supervisor")
builder.add_edge("product_agent", "output_supervisor")
builder.add_edge("orders_agent", "output_supervisor")
builder.add_edge("support_agent", "output_supervisor")
builder.add_edge("handoff", "output_supervisor")
builder.add_edge("supervisor_agent", "output_supervisor")
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist")
builder.add_edge("persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue"
async def input_guardrails(self, state):
async with self.telemetry.span(
"workflow.input_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("user_text"),
):
history_texts = [m.get("content", "") for m in state.get("history", [])]
sanitized, decisions = await self.guardrails.run_input(
state["user_text"],
{
**(state.get("context") or {}),
"history_texts": history_texts,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {},
},
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("input", _decision)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("input", _decision)
await self.telemetry.event(
"guardrails.input.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
if any(not d.allowed for d in decisions):
return {
"sanitized_input": sanitized,
"answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
"guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked",
"blocked": True,
}
return {
"sanitized_input": sanitized,
"guardrail_decisions": [d.model_dump() for d in decisions],
"blocked": False,
}
async def routing_decision(self, state):
mode = getattr(self.settings, "ROUTING_MODE", "router")
async with self.telemetry.span(
"workflow.routing_decision",
session_id=state.get("conversation_key") or state.get("session_id"),
input={
"mode": mode,
"text": state.get("sanitized_input") or state.get("user_text"),
"previous_state": state.get("next_state"),
},
):
if mode == "supervisor":
plan = await self.supervisor.route_plan(state)
await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence})
return {
"route": "supervisor_agent",
"intent": plan.intent,
"supervisor_plan": {
"agents": plan.agents,
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"metadata": plan.metadata,
},
"route_decision": {
"route": "supervisor_agent",
"agent": "supervisor",
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"method": "supervisor",
"metadata": plan.metadata,
},
}
decision = await self.router.route(state)
await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence})
await self.observer.emit_ic(
"ROUTE_SELECTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": decision.route,
"intent": decision.intent,
"confidence": decision.confidence,
"method": getattr(decision, "method", None),
},
component="workflow.routing_decision",
)
return {
"route": decision.route,
"intent": decision.intent,
"route_decision": decision.model_dump(mode="json"),
"domain": decision.domain,
"mcp_tools": decision.mcp_tools,
"next_state": decision.next_state,
}
async def billing_agent(self, state):
async with self.langgraph_telemetry.node("billing_agent", state):
async with self.telemetry.span(
"workflow.agent.billing",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.billing.run(state)
async def product_agent(self, state):
async with self.langgraph_telemetry.node("product_agent", state):
async with self.telemetry.span(
"workflow.agent.product",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.product.run(state)
async def orders_agent(self, state):
async with self.langgraph_telemetry.node("orders_agent", state):
async with self.telemetry.span(
"workflow.agent.orders",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.orders.run(state)
async def support_agent(self, state):
async with self.langgraph_telemetry.node("support_agent", state):
async with self.telemetry.span(
"workflow.agent.support",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.support.run(state)
async def supervisor_agent(self, state):
"""Executa um ou mais agentes no modo supervisor e consolida a resposta.
Este nó mantém o desenho de supervisor sem obrigar o restante do workflow
a conhecer quantos agentes foram acionados. Cada execução especializada
recebe o mesmo estado, mas com route/active_agent atualizados.
"""
plan = state.get("supervisor_plan") or {}
agents = plan.get("agents") or ["billing_agent"]
handlers = {
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
}
partials = []
mcp_results = []
async with self.telemetry.span(
"workflow.supervisor_agent",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"agents": agents, "intent": state.get("intent")},
):
for agent_name in agents:
handler = handlers.get(agent_name)
if handler is None:
continue
child_state = {**state, "route": agent_name, "active_agent": agent_name}
result = await handler(child_state)
partials.append({"agent": agent_name, "answer": result.get("answer", "")})
mcp_results.extend(result.get("mcp_results") or [])
if len(partials) == 1:
answer = partials[0]["answer"]
else:
joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials)
answer = (
"[Supervisor] Consolidação de múltiplos agentes acionados.\n"
f"{joined}"
)
return {
"answer": answer,
"supervisor_results": partials,
"mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE",
}
async def handoff(self, state):
async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")):
target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent")
answer = (
"Vou redirecionar sua solicitação para o especialista correto. "
f"Destino sugerido: {target or 'agente especializado'}."
)
return {"answer": answer}
async def output_supervisor(self, state):
"""Valida a resposta candidata com o OutputSupervisor corporativo.
Este nó não substitui o roteador/supervisor multiagente. Ele roda após o
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
candidate = state.get("answer") or ""
context = {
**(state.get("context") or {}),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
async with self.telemetry.span(
"workflow.output_supervisor",
session_id=state.get("conversation_key") or state.get("session_id"),
input=candidate,
):
decision = await self.output_supervisor_engine.evaluate(candidate, context)
action = decision.action.value
await self.telemetry.event(
"output_supervisor.completed",
{
"session_id": context["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"action": action,
"approved": decision.approved,
"guidance": decision.guidance,
},
)
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
final_answer = decision.candidate
elif decision.action == RailAction.HANDOVER:
final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista."
else:
final_answer = decision.fallback_message
return {
"answer": final_answer,
"final_answer": final_answer,
"supervisor_action": action,
"supervisor_guidance": decision.guidance,
"supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0),
"supervisor_handover_reason": decision.handover_reason,
"output_supervisor_results": [
{
"code": r.code,
"action": r.action.value,
"reason": r.reason,
"guidance": r.guidance,
"metadata": r.metadata,
}
for r in decision.results
],
"output_guardrails_already_applied": True,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}
async def output_guardrails(self, state):
if state.get("output_guardrails_already_applied"):
return {"final_answer": state.get("final_answer") or state.get("answer") or ""}
async with self.telemetry.span(
"workflow.output_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("answer"),
):
final, decisions = await self.guardrails.run_output(
state["answer"], state.get("context", {})
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("output", _decision)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("output", _decision)
await self.telemetry.event(
"guardrails.output.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
return {
"final_answer": final,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [d.model_dump() for d in decisions],
}
async def judge(self, state):
async with self.telemetry.span(
"workflow.judge",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"question": state.get("user_text"), "answer": state.get("final_answer")},
):
results = await self.judges.evaluate_all(
state["user_text"], state["final_answer"], state.get("context", {})
)
for _result in results:
await self.judge_telemetry.evaluated(_result)
await self.telemetry.event(
"judges.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"results": [r.model_dump() for r in results],
},
)
return {"judge_results": [r.model_dump() for r in results]}
async def supervisor_review(self, state):
async with self.telemetry.span(
"workflow.supervisor_review",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("final_answer"),
):
ok, answer = await self.supervisor.review(
state["final_answer"], state.get("context", {})
)
await self.telemetry.event(
"supervisor.review.completed",
{"session_id": state.get("session_id"), "approved": ok},
)
return {"final_answer": answer if ok else answer}
async def persist(self, state):
async with self.telemetry.span(
"workflow.persist",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"route": state.get("route"), "intent": state.get("intent")},
):
await self.observer.emit_ic(
"AGENT_COMPLETED",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"route_decision": state.get("route_decision"),
"judges": state.get("judge_results", []),
"mcp_tools": state.get("mcp_tools", []),
"mcp_results": state.get("mcp_results", []),
},
)
await self.observer.emit_noc(
"006",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
component="workflow.persist",
)
await self.telemetry.event(
"agent.completed",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
)
return state
async def ainvoke(self, state):
thread_id = state.get("conversation_key") or state["session_id"]
config = {"configurable": {"thread_id": thread_id}}
async with self.telemetry.span(
"workflow.langgraph.ainvoke",
session_id=state.get("conversation_key") or state.get("session_id"),
user_id=state.get("context", {}).get("user_id"),
input={"user_text": state.get("user_text")},
tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",],
):
await self.workflow_telemetry.started("agent_workflow", state)
await self.observer.emit_noc(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"ura_call_id": (state.get("context") or {}).get("ura_call_id"),
},
component="workflow.ainvoke",
)
await self.observer.emit_ic(
"AGENT_STARTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"user_text_chars": len(state.get("user_text") or ""),
},
component="workflow.ainvoke",
)
try:
result = await self.graph.ainvoke(state, config=config)
await self.workflow_telemetry.completed("agent_workflow", result)
return result
except Exception as exc:
await self.workflow_telemetry.failed("agent_workflow", exc)
await self.observer.emit_noc(
"005",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"error": str(exc),
"exception_type": exc.__class__.__name__,
},
component="workflow.ainvoke",
)
raise

View File

@@ -0,0 +1,38 @@
# ============================================================================
# DAY ZERO
# Este arquivo foi copiado do agent_template_backend original.
# Ajuste os exemplos abaixo para o domínio do seu novo agente.
# ============================================================================
default_agent_id: telecom_contas
agents:
- agent_id: telecom_contas
name: Agente Telecom Contas
description: Template de atendimento para faturas, produtos e suporte de telecom.
prompt_policy_path: ./config/agents/telecom_contas/prompt_policy.yaml
routing_config_path: ./config/routing.yaml
guardrails_config_path: ./config/agents/telecom_contas/guardrails.yaml
judges_config_path: ./config/agents/telecom_contas/judges.yaml
mcp_servers_config_path: ./config/mcp_servers.yaml
tools_config_path: ./config/tools.yaml
metadata:
domain: telecom
system_prefix: |
Você está executando o agent_template telecom_contas.
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
Não misture histórico ou decisões de outros agentes.
- agent_id: retail_orders
name: Agente Retail Pedidos
description: Template de varejo para pedidos, produtos, troca/devolução e garantia.
prompt_policy_path: ./config/agents/retail_orders/prompt_policy.yaml
routing_config_path: ./config/routing.yaml
guardrails_config_path: ./config/agents/retail_orders/guardrails.yaml
judges_config_path: ./config/agents/retail_orders/judges.yaml
mcp_servers_config_path: ./config/mcp_servers.yaml
tools_config_path: ./config/tools.yaml
metadata:
domain: retail
system_prefix: |
Você está executando o agent_template retail_orders.
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
Não misture histórico ou decisões de outros agentes.

View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

View File

@@ -0,0 +1,7 @@
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6

View File

@@ -0,0 +1,6 @@
id: retail_orders_prompt_policy
version: 1
description: Prompt base isolado do agente de varejo/pedidos.
system_prefix: |
Você é um agente corporativo de varejo especializado em pedidos, entrega, troca, devolução e garantia.
Seja claro, objetivo e não use regras de negócio de telecom neste agente.

View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

View File

@@ -0,0 +1,20 @@
enabled: true
fail_closed: true
profile: judge
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6
- name: sentiment
enabled: true
fail_on_negative: false
- name: tone
enabled: true
fail_closed: true

View File

@@ -0,0 +1,6 @@
id: telecom_contas_prompt_policy
version: 1
description: Prompt base isolado do agente de telecom/contas.
system_prefix: |
Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte.
Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida.

View File

@@ -0,0 +1,8 @@
input:
- code: MSK
enabled: true
- code: VLOOP
enabled: true
output:
- code: REVPREC
enabled: true

View File

@@ -0,0 +1,55 @@
identity:
version: "2"
required:
- session_key
keys:
customer_key:
description: Cliente/assinante/consumidor canônico.
sources:
- business_context.customer_key
- customer_key
- msisdn
- customer_id
- user_id
- ani
- from
contract_key:
description: Contrato, conta, fatura, pedido ou asset principal.
sources:
- business_context.contract_key
- contract_key
- invoice_id
- current_invoice_number
- order_id
- pedido_id
- asset_id
interaction_key:
description: Chave externa da interação/call/chat vinda do canal.
sources:
- business_context.interaction_key
- interaction_key
- ura_call_id
- call_id
- message_id
account_key:
description: Conta de cobrança/conta comercial.
sources:
- business_context.account_key
- account_key
- account_id
- billing_account_id
resource_key:
description: Recurso/linha/produto/asset específico.
sources:
- business_context.resource_key
- resource_key
- asset_id
- product_id
- sku
session_key:
description: Sessão técnica estável já escopada por tenant e agente.
sources:
- business_context.session_key
- session_key
- conversation_key
- session_id

View File

@@ -0,0 +1,20 @@
enabled: true
fail_closed: true
profile: judge
judges:
- name: response_quality
enabled: true
threshold: 0.7
- name: groundedness
enabled: true
threshold: 0.6
- name: sentiment
enabled: true
fail_on_negative: false
- name: tone
enabled: true
fail_closed: true

View File

@@ -0,0 +1,51 @@
# ============================================================================
# DAY ZERO
# Este arquivo foi copiado do agent_template_backend original.
# Ajuste os exemplos abaixo para o domínio do seu novo agente.
# ============================================================================
mcp_parameter_mapping:
defaults:
use_mock: true
tools:
consultar_fatura:
map:
customer_key: msisdn
contract_key: invoice_id
interaction_key: ura_call_id
session_key: session_id
consultar_pagamentos:
map:
customer_key: msisdn
interaction_key: ura_call_id
session_key: session_id
consultar_plano:
map:
customer_key: msisdn
resource_key: asset_id
contract_key: asset_id
session_key: session_id
listar_servicos:
map:
customer_key: msisdn
session_key: session_id
consultar_pedido:
map:
customer_key: customer_id
contract_key: order_id
session_key: session_id
consultar_entrega:
map:
contract_key: order_id
session_key: session_id
solicitar_troca:
map:
contract_key: order_id
session_key: session_id
defaults:
reason: Solicitação aberta pelo atendimento conversacional.
solicitar_devolucao:
map:
contract_key: order_id
session_key: session_id
defaults:
reason: Solicitação aberta pelo atendimento conversacional.

View File

@@ -0,0 +1,12 @@
servers:
telecom:
transport: http
endpoint: http://telecom-mcp:8100/mcp
enabled: true
description: MCP Server Telecom via docker-compose.
retail:
transport: http
endpoint: http://retail-mcp:8200/mcp
enabled: true
description: MCP Server Retail via docker-compose.

View File

@@ -0,0 +1,30 @@
# MCP servers registry.
# transport=http keeps the legacy framework mock contract:
# GET <endpoint>/tools/list
# POST <endpoint>/tools/call
# transport=fastmcp uses official MCP Streamable HTTP, typically endpoint http://host:port/mcp
# transport=sse uses official MCP SSE, typically endpoint http://host:port/sse
servers:
# telecom:
# enabled: true
# transport: fastmcp
# endpoint: http://localhost:8001/mcp
# description: Telecom FastMCP server using official MCP protocol
#
# retail:
# enabled: true
# transport: fastmcp
# endpoint: http://localhost:8002/mcp
# description: Retail FastMCP server using official MCP protocol
telecom:
enabled: true
transport: http
endpoint: http://localhost:8100/mcp
description: Telecom legacy HTTP mock MCP server
retail:
enabled: true
transport: http
endpoint: http://localhost:8200/mcp
description: Retail legacy HTTP mock MCP server

View File

@@ -0,0 +1,19 @@
tone:
style: "claro, objetivo, empático"
forbidden_phrases:
- "procure atendimento humano"
vocabulary:
preferred:
fatura: "fatura"
contestacao: "contestação"
intents:
billing_agent:
- fatura
- boleto
- cobrança
- segunda via
product_agent:
- plano
- produto
- oferta
- serviço

View File

@@ -0,0 +1,119 @@
# ============================================================================
# DAY ZERO
# Este arquivo foi copiado do agent_template_backend original.
# Ajuste os exemplos abaixo para o domínio do seu novo agente.
# ============================================================================
# Roteamento enterprise configurável com MCP-aware intents.
router:
# mode também pode ser definido por variável de ambiente ROUTING_MODE.
# Valores: router | supervisor
mode: router
fallback_agent: billing_agent
confidence_threshold: 0.65
allow_handoff: true
state_policies:
- state: WAITING_BILLING_CONFIRMATION
agent: billing_agent
description: Mantém mensagens curtas como "sim" ou "não" no fluxo de fatura.
- state: WAITING_PRODUCT_CONFIRMATION
agent: product_agent
description: Mantém confirmações no fluxo de produtos/serviços.
- state: WAITING_ORDER_CONFIRMATION
agent: orders_agent
description: Mantém confirmações no fluxo de pedidos.
- state: WAITING_SUPPORT_CONFIRMATION
agent: support_agent
description: Mantém confirmações no fluxo de suporte retail.
intents:
- name: billing_invoice_explanation
domain: telecom
agent: billing_agent
description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
priority: 10
mcp_tools:
- consultar_fatura
- consultar_pagamentos
keywords:
- fatura
- conta
- cobrança
- boleto
- vencimento
- segunda via
- contestar
- valor alto
- invoice
examples:
- Minha fatura veio alta.
- Quero entender uma cobrança.
- Preciso da segunda via da conta.
- name: product_services_information
domain: telecom
agent: product_agent
description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
priority: 20
mcp_tools:
- consultar_plano
- listar_servicos
keywords:
- plano
- produto
- serviço
- pacote
- internet
- roaming
- vas
- benefício
- assinatura
examples:
- Quais serviços estão ativos no meu plano?
- Quero saber sobre meu pacote de internet.
- Tenho roaming internacional?
- name: retail_order_tracking
domain: retail
agent: orders_agent
description: Consulta de pedido, entrega, rastreamento, atraso e status de compra.
priority: 30
mcp_tools:
- consultar_pedido
- consultar_entrega
keywords:
- pedido
- entrega
- rastreio
- rastreamento
- encomenda
- compra
- atraso
- correios
examples:
- Meu pedido não chegou.
- Quero rastrear minha entrega.
- Qual é o status da minha compra?
- name: retail_support_exchange_return
domain: retail
agent: support_agent
description: Suporte, troca, devolução, garantia e problema com produto.
priority: 40
mcp_tools:
- consultar_pedido
- solicitar_troca
- solicitar_devolucao
keywords:
- troca
- devolução
- devolver
- garantia
- defeito
- produto quebrado
- suporte
- arrependimento
examples:
- Quero trocar um produto.
- Meu produto veio com defeito.
- Como faço uma devolução?

View File

@@ -0,0 +1,72 @@
# ============================================================================
# DAY ZERO
# Este arquivo foi copiado do agent_template_backend original.
# Ajuste os exemplos abaixo para o domínio do seu novo agente.
# ============================================================================
tools:
consultar_fatura:
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
mcp_server: telecom
enabled: true
args_schema:
msisdn: string
invoice_id: string
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
enabled: true
args_schema:
msisdn: string
consultar_plano:
description: Consulta plano ativo e atributos comerciais.
mcp_server: telecom
enabled: true
args_schema:
msisdn: string
asset_id: string
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
enabled: true
args_schema:
msisdn: string
consultar_pedido:
description: Consulta pedido de varejo por order_id/customer_id.
mcp_server: retail
enabled: true
args_schema:
order_id: string
customer_id: string
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
enabled: true
args_schema:
order_id: string
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail
enabled: true
tool_type: action
requires: [order_id, reason]
confirmation_required: false
args_schema:
order_id: string
reason: string
solicitar_devolucao:
description: Simula abertura de solicitação de devolução.
mcp_server: retail
enabled: true
tool_type: action
requires: [order_id, reason]
confirmation_required: false
args_schema:
order_id: string
reason: string

View File

@@ -0,0 +1,95 @@
# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor
Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`.
## 1. Analytics e Pub/Sub
O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`:
```python
from agent_framework.analytics.factory import create_analytics_publisher
from agent_framework.observability.observer import AgentObserver
analytics = create_analytics_publisher(settings)
observer = AgentObserver(analytics=analytics)
```
Com isso, o mesmo backend pode publicar em:
- OCI Streaming
- GCP Pub/Sub
- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub`
- Noop, quando analytics estiver desligado
## 2. Configuração mínima
```env
ENABLE_ANALYTICS=true
ANALYTICS_PROVIDERS=pubsub
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
```
Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub:
```env
ENABLE_ANALYTICS=true
ANALYTICS_PROVIDERS=oci_streaming,pubsub
ENABLE_OCI_STREAMING=true
OCI_STREAM_ENDPOINT=<endpoint>
OCI_STREAM_OCID=<stream-ocid>
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
```
## 3. Observer corporativo
O workflow recebeu emissão automática dos principais eventos corporativos:
- `NOC.001`: início do workflow
- `NOC.005`: exceção fatal no workflow
- `NOC.006`: fim do workflow antes da resposta final
- `IC.AGENT_COMPLETED`: evento informacional de conclusão
- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor`
## 4. OutputSupervisor
Foi inserido um novo nó LangGraph:
```text
agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist
```
O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo:
- `allow`
- `sanitize`
- `retry`
- `block`
- `handover`
- `observe`
Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`.
## 5. Campos adicionados ao AgentState
```python
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict]
output_guardrails_already_applied: bool
```
## 6. Arquivos alterados
- `agent_template_backend/app/main.py`
- `agent_template_backend/app/workflows/agent_graph.py`
- `agent_template_backend/app/state.py`
- `agent_template_backend/.env`
- `agent_template_backend/requirements.txt`
- `agent_framework/src/agent_framework/config/settings.py`
## 7. Observação importante
O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails.

View File

@@ -0,0 +1,48 @@
# Backends atualizados para ConversationSummaryMemory
Esta versão dos backends foi compatibilizada com a versão do framework que adiciona `ConversationSummaryMemory`.
## O que mudou
- `app/main.py` agora inicializa `create_conversation_summary_memory(...)` junto com `create_memory(...)`.
- `AgentWorkflow` recebe `summary_memory` e repassa para os agentes.
- Os agentes não montam mais prompts manuais para o LLM; agora usam `build_messages()` do framework.
- Antes da chamada ao LLM, os agentes executam `await self.prepare_memory_context(state)`.
- Quando habilitado por `.env`, o prompt passa a receber:
- resumo acumulado da conversa;
- últimas mensagens completas;
- mensagem atual;
- BusinessContext;
- MCP results;
- RAG context e metadata.
## Configuração
```env
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
MEMORY_CONTEXT_STRATEGY=summary
MEMORY_HISTORY_LIMIT=80
MEMORY_RECENT_MESSAGES_LIMIT=8
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
```
## Backends alterados
- `backoffice_convertido_framework`
- `agent_template_backend`
- `agent_template_backend_day_zero`
## Observação importante
Estes backends esperam que o pacote `agent_framework` instalado/conectado seja a versão com os módulos:
- `agent_framework.memory.summary_memory`
- `agent_framework.memory.summary_store`
- `AgentRuntimeMixin.prepare_memory_context()`
- `AgentRuntimeMixin.build_messages()` com injeção de memória
Use junto com o ZIP `agent_framework_conversation_summary_memory.zip`.

View File

@@ -0,0 +1,60 @@
# Como usar o `agent_template_backend_day_zero`
Este template é uma cópia do backend completo, mas com a lógica dos agentes de exemplo comentada.
## Fluxo mantido
```text
Gateway / Canal
-> AgentWorkflow
-> Input Guardrails
-> Router / Supervisor Router
-> Agente
-> OutputSupervisor
-> Output Guardrails
-> Judges
-> Persistência
```
## Onde escrever código
O ponto principal é o método `run()` dos agentes em `app/agents/`.
A estrutura esperada pelo workflow é:
```python
async def run(self, state):
...
return {
"answer": answer,
"next_state": "MEU_ESTADO"
}
```
## Como usar MCP
Dentro de `run()`:
```python
tool_context = await self._collect_tool_context(state)
```
## Como usar RAG
Dentro de `run()`:
```python
rag_context, rag_metadata = await self._retrieve_rag_context(state)
```
## Como chamar o LLM com cache/telemetria
```python
answer = await self._invoke_llm_cached(state, "MeuAgente", messages)
```
## Como ajustar roteamento
Edite `config/routing.yaml`.
O arquivo original foi mantido para servir de referência, mas as intents devem ser adaptadas para o domínio do novo agente.

View File

@@ -0,0 +1,84 @@
# FRAMEWORK_CHANNEL_INPUT_MODE
This backend setting controls what kind of channel input the Agent Framework backend accepts.
It replaces the ambiguous use of `CHANNEL_GATEWAY_MODE` inside the backend.
## Values
```env
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
```
The backend may use internal channel adapters to interpret simple/native channel payloads. This is useful for demos, labs, local frontend, curl tests, and simple environments.
```env
FRAMEWORK_CHANNEL_INPUT_MODE=external
```
The backend accepts only a normalized `GatewayRequest` produced by an external Channel Gateway. It does not parse native WhatsApp, Voice, Teams, or other channel payloads.
## Recommended enterprise setup
In the external channel gateway service:
```env
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
```
In this backend:
```env
FRAMEWORK_CHANNEL_INPUT_MODE=external
```
Flow:
```text
External channel / browser / customer adapter
channel_gateway:7000
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
↓ GatewayRequest
agent_template_backend:8000
FRAMEWORK_CHANNEL_INPUT_MODE=external
LangGraph / Agents / MCP / Guardrails
```
## Valid direct request to backend in external mode
```bash
curl -s -X POST "http://localhost:8000/gateway/message" \
-H "Content-Type: application/json" \
-d '{
"channel": "web",
"tenant_id": "default",
"agent_id": "telecom_contas",
"payload": {
"message": "Quero consultar minha fatura",
"session_id": "backend-external-ok-001"
}
}' | jq
```
## Invalid direct request to backend in external mode
```bash
curl -i -s -X POST "http://localhost:8000/gateway/message" \
-H "Content-Type: application/json" \
-d '{
"message": "Quero consultar minha fatura",
"session_id": "raw-payload-error-001"
}'
```
Expected result: HTTP 422.
## Legacy compatibility
`CHANNEL_GATEWAY_MODE` is still present as a legacy alias for older environments, but new deployments should use:
```env
FRAMEWORK_CHANNEL_INPUT_MODE=embedded|external
```

View File

@@ -0,0 +1,5 @@
# Langfuse single trace observer fix
This backend now uses `TelemetryBackedAgentObserver` instead of publishing IC/NOC/GRL through `AgentObserver(analytics=...)`.
Why: when analytics includes the Langfuse provider, observer events such as `IC.AGENT_COMPLETED` and `NOC.006` may create a second root trace with little detail. Emitting those events through `Telemetry.event(...)` keeps them inside the active request/workflow trace.

View File

@@ -0,0 +1,23 @@
fastapi>=0.115.0
uvicorn[standard]>=0.30.0
pydantic>=2.8.0
pydantic-settings>=2.4.0
python-dotenv>=1.0.1
langgraph>=0.2.60
langchain-core>=0.3.0
openai>=1.60.0
oci>=2.130.0
oracledb>=2.4.0
pymongo>=4.8.0
redis>=5.0.0
PyYAML>=6.0.2
langfuse>=3.0.0
httpx>=0.27.0
opentelemetry-api>=1.27.0
opentelemetry-sdk>=1.27.0
opentelemetry-exporter-otlp-proto-http>=1.27.0
pytest>=8.0.0
pytest-asyncio>=0.23.0
google-cloud-pubsub>=2.28.0