From 57df723a24cbbd79d9be0c2c79bdb6e5e5571966 Mon Sep 17 00:00:00 2001 From: "cristiano.hoshikawa" Date: Thu, 20 Aug 2026 15:13:20 -0300 Subject: [PATCH] new feature: mcp pre-validation --- Tuning-Performance/README.md | 1 + .../Transaction_Pre_Validation/README.md | 166 + .../agent_template_backend/.env | 207 + .../agent_template_backend/Dockerfile | 6 + .../agent_template_backend/README.md | 4219 +++++++++++++++++ .../README_ENTERPRISE_TEMPLATE.md | 54 + .../agent_template_backend/app/__init__.py | 0 .../app/agents/README.md | 15 + .../app/agents/billing_agent.py | 129 + .../app/agents/orders_agent.py | 129 + .../app/agents/product_agent.py | 129 + .../app/agents/prompting.py | 15 + .../app/agents/runtime.py | 10 + .../app/agents/support_agent.py | 129 + .../app/examples/__init__.py | 1 + .../app/examples/grl_examples.py | 37 + .../app/examples/ic_examples.py | 34 + .../app/examples/mcp_examples.py | 43 + .../app/examples/noc_examples.py | 37 + .../app/examples/observer_examples.py | 28 + .../agent_template_backend/app/main.py | 648 +++ .../app/mcp_gateway_client_factory.py | 16 + .../app/observability/__init__.py | 0 .../app/observability/telemetry_observer.py | 84 + .../app/presentation/__init__.py | 3 + .../app/presentation/tool_renderers.py | 74 + .../agent_template_backend/app/state.py | 57 + .../app/workflow_actions/__init__.py | 1 + .../app/workflow_actions/devolucao.py | 13 + .../app/workflows/agent_graph.py | 892 ++++ .../agent_template_backend/config/agents.yaml | 33 + .../agents/retail_orders/guardrails.yaml | 8 + .../config/agents/retail_orders/judges.yaml | 7 + .../agents/retail_orders/prompt_policy.yaml | 6 + .../agents/telecom_contas/guardrails.yaml | 8 + .../config/agents/telecom_contas/judges.yaml | 20 + .../agents/telecom_contas/prompt_policy.yaml | 6 + .../config/guardrails.yaml | 12 + .../config/identity.yaml | 55 + .../agent_template_backend/config/judges.yaml | 18 + .../config/mcp_parameter_mapping.yaml | 108 + .../config/mcp_servers.docker.yaml | 12 + .../config/mcp_servers.yaml | 30 + .../config/prompt_policy.yaml | 19 + .../config/routing.yaml | 147 + .../config/tool_policies.yaml | 36 + .../agent_template_backend/config/tools.yaml | 142 + ...AO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md | 95 + .../docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md | 45 + .../CONVERSATION_SUMMARY_MEMORY_BACKEND.md | 48 + .../docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md | 14 + .../docs/FRAMEWORK_CHANNEL_INPUT_MODE.md | 84 + .../docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md | 127 + ...EMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md | 42 + .../LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md | 5 + .../docs/TESTE_LONG_TERM_MEMORY.md | 82 + .../docs/VALIDACAO_BACKEND_IC_NOC_GRL.md | 62 + .../docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt | 3 + .../agent_template_backend/llm_profiles.yaml | 80 + .../agent_template_backend/requirements.txt | 23 + .../scripts/test_long_term_memory.py | 29 + .../workflows/devolucao_pedido.active.yaml | 1 + .../workflows/devolucao_pedido.v1.yaml | 27 + .../__pycache__/tool_policy.cpython-313.pyc | Bin 4717 -> 5647 bytes .../__pycache__/tool_router.cpython-313.pyc | Bin 13998 -> 14169 bytes .../src/agent_framework/mcp/tool_policy.py | 16 + .../src/agent_framework/mcp/tool_router.py | 2 + .../__pycache__/agent_runtime.cpython-313.pyc | Bin 133760 -> 137406 bytes .../agent_framework/runtime/agent_runtime.py | 91 + .../__pycache__/main.cpython-313.pyc | Bin 0 -> 5878 bytes mcp/servers/retail_mcp_server/main.py | 21 + templates/agent_template_backend/.env | 207 + .../app/__pycache__/main.cpython-313.pyc | Bin 32297 -> 36677 bytes .../app/__pycache__/state.cpython-313.pyc | Bin 2526 -> 2812 bytes templates/agent_template_backend/app/main.py | 1 + templates/agent_template_backend/app/state.py | 1 + .../config/mcp_parameter_mapping.yaml | 4 + .../config/tool_policies.yaml | 4 + .../agent_template_backend/config/tools.yaml | 12 + .../app/state.py | 1 + .../config/mcp_parameter_mapping.yaml | 4 + .../config/tool_policies.yaml | 4 + .../config/tools.yaml | 12 + ...nal_tool_flow.cpython-313-pytest-9.0.2.pyc | Bin 38592 -> 53813 bytes ...st_transactional_tool_flow.cpython-313.pyc | Bin 0 -> 24425 bytes tests/test_transactional_tool_flow.py | 189 + 86 files changed, 9180 insertions(+) create mode 100644 Tuning-Performance/Transaction_Pre_Validation/README.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/.env create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/Dockerfile create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README_ENTERPRISE_TEMPLATE.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/__init__.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/README.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/billing_agent.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/orders_agent.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/product_agent.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/prompting.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/runtime.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/support_agent.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/__init__.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/grl_examples.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/ic_examples.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/mcp_examples.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/noc_examples.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/observer_examples.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/main.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/mcp_gateway_client_factory.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/__init__.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/telemetry_observer.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/__init__.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/tool_renderers.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/__init__.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/devolucao.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/guardrails.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/judges.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/guardrails.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/judges.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/guardrails.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/identity.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/judges.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_parameter_mapping.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.docker.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/prompt_policy.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/routing.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tool_policies.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tools.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/llm_profiles.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/requirements.txt create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/scripts/test_long_term_memory.py create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.active.yaml create mode 100644 Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.v1.yaml create mode 100644 mcp/servers/retail_mcp_server/__pycache__/main.cpython-313.pyc create mode 100644 templates/agent_template_backend/.env create mode 100644 tests/__pycache__/test_transactional_tool_flow.cpython-313.pyc diff --git a/Tuning-Performance/README.md b/Tuning-Performance/README.md index f32a534..4f52754 100644 --- a/Tuning-Performance/README.md +++ b/Tuning-Performance/README.md @@ -7,3 +7,4 @@ Variantes e documentos de referência para comparar funcionalidades e impacto de - `Long_Term_Memory`: memória de longo prazo. - `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação. - `Transaction_Evidence`: persistência e correlação de resultados transacionais como evidência operacional para turnos posteriores e groundedness. +- `Transaction_Pre_Validation`: pré-validação MCP side-effect-free antes da confirmação transacional; regras de elegibilidade permanecem no domínio. diff --git a/Tuning-Performance/Transaction_Pre_Validation/README.md b/Tuning-Performance/Transaction_Pre_Validation/README.md new file mode 100644 index 0000000..90121f3 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/README.md @@ -0,0 +1,166 @@ +# Transaction Pre-Validation / Pré-validação Transacional + +Esta variante demonstra a capability genérica de **pré-validação MCP antes da confirmação**. + +A regra de negócio continua no MCP. O framework apenas orquestra o contrato genérico: + +```text +parâmetros completos + ↓ +MCP validator (read-only / side-effect-free) + ↓ +eligible? + ├─ false → OUT_OF_SCOPE / NOT_ELIGIBLE → responde sem pedir confirmação + └─ true → AWAITING_CONFIRMATION → usuário confirma → tool transacional executa +``` + +Nenhum LLM adicional é usado pela pré-validação. + +## Por que existe + +Sem pre-validation, uma aplicação pode pedir confirmação para uma operação que o domínio já +sabe que é inválida. Exemplo: tentar cancelar um pedido já entregue ou contestar uma categoria +que não é elegível para contestação. + +O framework **não contém essas regras de negócio**. Ele apenas consulta uma tool MCP declarada +na policy e interpreta o campo genérico `eligible`. + +## Policy + +`config/tool_policies.yaml`: + +```yaml +tool_policies: + cancelar_pedido: + operation_type: transactional + require_confirmation: true + requires: [order_id] + pre_validation: + enabled: true + tool: validar_cancelamento_pedido + fail_open: false +``` + +A tool `validar_cancelamento_pedido` é read-only/internal e deve ser side-effect-free. + +## Contrato MCP esperado + +Elegível: + +```json +{ + "eligible": true, + "status": "ELIGIBLE", + "order_id": "PED-1001" +} +``` + +Não elegível: + +```json +{ + "eligible": false, + "status": "NOT_ELIGIBLE", + "order_id": "PED-ENTREGUE", + "reason": "Pedido já entregue não pode ser cancelado por esta operação." +} +``` + +## Cenário de teste + +Suba o Retail MCP de exemplo: + +```bash +cd agent_framework_oci/mcp/servers/retail_mcp_server +uvicorn main:app --host 0.0.0.0 --port 8200 +``` + +Em outro terminal: + +```bash +cd agent_framework_oci/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend +pip install -e ../../../libs/agent_framework +pip install -r requirements.txt +python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +### Caso elegível + +```text +quero cancelar o pedido PED-1001 +``` + +Esperado: + +```text +validar_cancelamento_pedido(PED-1001) +→ eligible=true +→ AWAITING_CONFIRMATION +→ nenhuma execução de cancelar_pedido ainda +``` + +Após `sim`, `cancelar_pedido` é executada. + +### Caso não elegível + +```text +quero cancelar o pedido PED-ENTREGUE +``` + +Esperado: + +```text +validar_cancelamento_pedido(PED-ENTREGUE) +→ eligible=false +→ OUT_OF_SCOPE +→ NÃO entra em AWAITING_CONFIRMATION +→ cancelar_pedido NÃO é executada +``` + +## Telemetria + +Procure pelos eventos: + +- `IC.TRANSACTION_PREVALIDATION_REQUESTED` +- `IC.TRANSACTION_PREVALIDATION_PASSED` +- `IC.TRANSACTION_PREVALIDATION_REJECTED` +- `IC.TRANSACTION_CONFIRMATION_REQUIRED` somente após pre-validation aprovada. + +No estado/metadata, `transaction_pre_validation` registra o validator utilizado e o resultado. + +## Fail-open x fail-closed + +`fail_open: false` é o padrão recomendado para operações sensíveis: se o validator estiver +indisponível, a transação não avança para confirmação. + +`fail_open: true` deve ser usado apenas quando o domínio aceitar explicitamente prosseguir sem +a pré-validação. + +## Separação de responsabilidades + +**Framework**: ordem das etapas, estado, confirmação, idempotência e telemetria. + +**MCP/domínio**: regra de elegibilidade, consulta aos sistemas de registro e motivo da rejeição. + +A capability é genérica: pode ser usada para cancelamento, contestação, devolução, troca, +suspensão ou qualquer outra operação transacional que possua uma precondição de domínio. + +## Rejeição encerra o estado transacional + +Quando o validator retorna `eligible: false`, o framework encerra imediatamente o latch transacional. Isso significa que `selected_tool_call`, `pending_tool_call` e `missing_parameters` são limpos, `confirmation_required=false`, `next_state=null` e `transaction_status=OUT_OF_SCOPE`. + +O resultado da pré-validação também é propagado no estado/metadata como `transaction_pre_validation`, por exemplo: + +```json +{ + "transaction_pre_validation": { + "tool_name": "contestar_cobranca", + "validator_tool": "validar_contestacao", + "eligible": false, + "status": "OUT_OF_SCOPE", + "terminal": true + } +} +``` + +Assim, o turno seguinte volta ao roteamento normal e não permanece preso em `COLLECTING_*` ou `WAITING_*`. A rejeição não vira `transaction_evidence`, pois a operação de negócio não foi executada. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/.env b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/.env new file mode 100644 index 0000000..4556734 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/.env @@ -0,0 +1,207 @@ +############################################################################### +# 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_sdk +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 +OCI_GENAI_MODEL=openai.gpt-4.1 +OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS +OCI_GENAI_PROJECT_OCID= + +#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com +#OCI_GENAI_MODEL=openai.gpt-4.1 +#OCI_GENAI_API_KEY= +#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=LATINOAMERICA-Chicago +OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q +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=Moniquinha19721972 +ADB_DSN=oradb23ai_high +ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai +ADB_WALLET_PASSWORD=Moniquinha1972 +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=autonomous +GRAPH_STORE_PROVIDER=autonomous +RAG_TOP_K=5 +EMBEDDING_PROVIDER=oci +OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0 +RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json + +############################################################################### +# Observabilidade +############################################################################### +ENABLE_LANGFUSE=true + # Opcional: verbose, compact +LANGFUSE_TRACE_MODE=compact +# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow +LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC. +LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion +LANGFUSE_IGNORE_HEALTHCHECKS=true +LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics +LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312 +LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915 +LANGFUSE_HOST=http://localhost:3005 +ENABLE_OTEL=false +OTEL_EXPORTER_OTLP_ENDPOINT= +OTEL_SERVICE_NAME=ai-agent-template +ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true +ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false + +############################################################################### +# 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 + +# Semantic route stickiness (optional). +# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE. +# There are no regexes or deterministic language rules. +ENABLE_ROUTE_STICKINESS=true +ROUTE_STICKINESS_LLM_PROFILE=route_continuity +ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90 +ROUTE_STICKINESS_HISTORY_TURNS=2 +ROUTE_STICKINESS_MAX_TOKENS=80 +HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa. +END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato. + +############################################################################### +# 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 + +############################################################################### +# LONG-TERM MEMORY +############################################################################### +ENABLE_LONG_TERM_MEMORY=true +LONG_TERM_MEMORY_PROVIDER=sqlite +LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db +LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory +# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY +# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY +LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20 +LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70 +LONG_TERM_MEMORY_AUTO_EXTRACT=true +LONG_TERM_MEMORY_INJECT_CONTEXT=true diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/Dockerfile b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/Dockerfile new file mode 100644 index 0000000..273fe01 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/Dockerfile @@ -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"] diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README.md new file mode 100644 index 0000000..8483e89 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README.md @@ -0,0 +1,4219 @@ +# Tutorial — Implementação de um Agente usando `agent_template_backend` + +Este tutorial ensina como implementar um novo agente a partir do `agent_template_backend`, usando o framework como motor corporativo de execução. + +A ideia central é simples: + +```text +Framework = motor reutilizável +Agente = regra de negócio específica +MCP Server = fronteira padronizada com sistemas externos +Config YAML = comportamento alterável sem recompilar código +IC/NOC/GRL = rastreabilidade de negócio, operação e governança +``` + +![img_1.png](img_1.png) + +O objetivo é que cada novo agente implemente apenas sua lógica de domínio — prompts, regras de negócio, ferramentas, schemas e nós específicos — sem recriar motores que já pertencem ao framework. + +--- + +## 1. Visão geral da arquitetura + +O template separa o que é genérico do que é específico. + +```text +agent_template_backend/ +├── app/ +│ ├── main.py # API FastAPI, gateway, sessão, SSE e entrada do workflow +│ ├── state.py # Contrato de estado compartilhado do LangGraph +│ ├── workflows/ +│ │ └── agent_graph.py # Workflow corporativo com router, guardrails, agentes, judges e persistência +│ ├── agents/ +│ │ ├── runtime.py # Recursos comuns para agentes: MCP, RAG, cache, IC, LLM +│ │ ├── billing_agent.py # Exemplo de agente de faturas +│ │ ├── product_agent.py # Exemplo de agente de produtos +│ │ ├── orders_agent.py # Exemplo de agente de pedidos +│ │ └── support_agent.py # Exemplo de agente de suporte +│ └── examples/ # Exemplos de IC, NOC, GRL, MCP e observer +├── config/ +│ ├── agents.yaml # Registro dos agentes disponíveis +│ ├── routing.yaml # Intents, keywords, fallback e decisão de rota +│ ├── tools.yaml # Catálogo das ferramentas disponíveis para o backend +│ ├── mcp_servers.yaml # Endpoints MCP locais +│ ├── mcp_servers.docker.yaml # Endpoints MCP em Docker Compose +│ ├── mcp_parameter_mapping.yaml # Mapeamento entre chaves canônicas e parâmetros das tools +│ ├── identity.yaml # Resolução de identidade de negócio +│ ├── guardrails.yaml # Guardrails globais +│ ├── judges.yaml # Judges globais +│ ├── prompt_policy.yaml # Política global de prompt +│ └── agents// # Configurações isoladas por agente +├── data/ +│ └── agent_framework.db # Banco local de exemplo, quando aplicável +├── Dockerfile +├── requirements.txt +└── .env # Configuração local +``` + +### 1.1. O que pertence ao framework + +O framework deve concentrar os motores reutilizáveis: + +- LangGraph e montagem do workflow. +- Checkpoint. +- Memória. +- Session repository. +- Channel gateway. +- Enterprise Router. +- Supervisor. +- Guardrails. +- Output Supervisor. +- Judges. +- Telemetria Langfuse/OpenTelemetry. +- Analytics IC/NOC/GRL. +- MCP Tool Router. +- Cache. +- RAG genérico. + +### 1.2. O que pertence ao agente + +O agente deve concentrar apenas customizações de domínio: + +- Prompts específicos. +- Regras de negócio. +- Schemas próprios. +- Tools específicas. +- Clients de sistemas externos, preferencialmente encapsulados atrás de MCP. +- Mapeamento de parâmetros. +- Nós especializados, se houver. +- ICs de negócio da jornada. + +Quando uma regra só faz sentido para um domínio, ela pertence ao agente. Quando uma capacidade deve ser usada por vários agentes, ela pertence ao framework. + +--- + +## 2. Fluxo de execução do template + +O fluxo principal começa em `app/main.py`, no endpoint `/gateway/message`. + +```text +Canal / Frontend / API + ↓ +POST /gateway/message + ↓ +ChannelGateway.normalize() + ↓ +IdentityResolver + ↓ +SessionRepository + ↓ +MemoryRepository + ↓ +AgentWorkflow.ainvoke() + ↓ +LangGraph + ↓ +Input Guardrails + ↓ +Enterprise Router ou Supervisor + ↓ +Agente especializado + ↓ +MCP Tool Router / RAG / Cache / LLM + ↓ +Output Supervisor + ↓ +Output Guardrails + ↓ +Judges + ↓ +Supervisor Review + ↓ +Persistência / Checkpoint / Memória + ↓ +Resposta +``` + +O `AgentWorkflow`, em `app/workflows/agent_graph.py`, normalmente já contém nós corporativos como: + +```text +input_guardrails +routing_decision +billing_agent +product_agent +orders_agent +support_agent +handoff +supervisor_agent +output_supervisor +output_guardrails +judge +supervisor_review +persist +``` + +Para criar um novo agente, normalmente você altera: + +```text +app/agents/.py +app/workflows/agent_graph.py +app/state.py, se precisar de campos novos +config/agents.yaml +config/routing.yaml +config/tools.yaml +config/mcp_servers.yaml +config/mcp_parameter_mapping.yaml +config/identity.yaml +config/agents//prompt_policy.yaml +config/agents//guardrails.yaml +config/agents//judges.yaml +.env +``` + +--- + +## 3. Pré-requisitos + +### 3.1. Requisitos locais + +- Python 3.12 ou 3.13. +- `pip` ou `uv`. +- Projeto `agent_framework` disponível no mesmo workspace, caso o template use instalação local. +- Servidores MCP, se o agente usar tools. +- Redis, Oracle Autonomous Database, MongoDB e Langfuse são opcionais conforme configuração. + +Estrutura recomendada: + +```text +workspace/ +├── agent_framework/ +└── agent_template_backend/ +``` + +### 3.2. Instalação local + +Dentro do diretório `agent_template_backend`: + +```bash +python -m venv .venv +source .venv/bin/activate +pip install -r requirements.txt +``` + +Se o `agent_framework` estiver em desenvolvimento local: + +```bash +pip install -e ../agent_framework +``` + +Em Windows PowerShell: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +pip install -r requirements.txt +pip install -e ..\agent_framework +``` + +--- + +## 4. Configuração do `.env` + +O `.env` define quais motores serão ativados. Ele não é apenas um arquivo de propriedades: ele muda o comportamento do agente em tempo de execução. + +Exemplo seguro para desenvolvimento local: + +```env +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_PROVIDER=mock +LLM_TEMPERATURE=0.2 +LLM_MAX_TOKENS=2048 +LLM_TIMEOUT_SECONDS=120 + +SESSION_REPOSITORY_PROVIDER=memory +MEMORY_REPOSITORY_PROVIDER=memory +CHECKPOINT_REPOSITORY_PROVIDER=memory +USAGE_REPOSITORY_PROVIDER=memory + +ENABLE_REDIS_CACHE=false +REDIS_URL=redis://localhost:6379/0 +CACHE_TTL_SECONDS=300 + +VECTOR_STORE_PROVIDER=memory +GRAPH_STORE_PROVIDER=memory +RAG_TOP_K=5 +EMBEDDING_PROVIDER=mock + +ENABLE_LANGFUSE=false +LANGFUSE_HOST=http://localhost:3005 +ENABLE_OTEL=false +OTEL_SERVICE_NAME=ai-agent-template + +ENABLE_ANALYTICS=false +ANALYTICS_PROVIDERS=noop +ENABLE_OCI_STREAMING=false +OCI_STREAM_ENDPOINT= +OCI_STREAM_OCID= +OCI_STREAM_PARTITION_KEY=agent-events + +ENABLE_INPUT_GUARDRAILS=true +ENABLE_OUTPUT_GUARDRAILS=true +ENABLE_OUTPUT_SUPERVISOR=true +ENABLE_JUDGES=true +ENABLE_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 + +ROUTING_CONFIG_PATH=./config/routing.yaml +ROUTING_MODE=router +ENABLE_LLM_ROUTER=false + +ENABLE_MCP_TOOLS=true +MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml +TOOLS_CONFIG_PATH=./config/tools.yaml +MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml +MCP_TOOL_TIMEOUT_SECONDS=30 + +IDENTITY_CONFIG_PATH=./config/identity.yaml +``` + +### 4.1. Como raciocinar sobre o `.env` + +Antes de testar um novo agente, responda: + +```text +O LLM será mock ou real? +A memória será local ou banco? +O checkpoint precisa sobreviver a restart? +As tools MCP serão chamadas de verdade ou simuladas? +O roteamento será por regra/intent ou supervisor? +Guardrails, judges e supervisor devem bloquear, revisar ou só observar? +Langfuse/OTEL/Streaming serão usados neste ambiente? +``` + +Para um primeiro teste, use `LLM_PROVIDER=mock`, persistência em `memory` e MCP mock/local. Depois evolua para LLM real, banco, Langfuse e serviços reais. + +Para usar Oracle Autonomous Database, ajuste: + +```env +SESSION_REPOSITORY_PROVIDER=autonomous +MEMORY_REPOSITORY_PROVIDER=autonomous +CHECKPOINT_REPOSITORY_PROVIDER=autonomous +USAGE_REPOSITORY_PROVIDER=autonomous + +ADB_USER= +ADB_PASSWORD= +ADB_DSN= +ADB_WALLET_LOCATION= +ADB_WALLET_PASSWORD= +ADB_TABLE_PREFIX=AGENTFW +``` + +Para usar Langfuse: + +```env +ENABLE_LANGFUSE=true +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= +LANGFUSE_HOST=http://localhost:3005 +``` + + +--- + +## 5. Criando um novo agente + +Neste exemplo, vamos criar um agente chamado `financeiro_agent` para atendimento financeiro genérico. + +### 5.1. Antes do código: o que é um agente neste framework? + +Um agente é uma classe de domínio que recebe o `state` do LangGraph, interpreta a intenção escolhida pelo roteador ou supervisor, coleta evidências, chama tools/RAG/LLM quando necessário e retorna uma decisão para o workflow continuar. + +Ele não deve decidir sozinho tudo que o framework já decide. Por exemplo: + +```text +O agente não cria sessão. +O agente não abre SSE. +O agente não compila LangGraph. +O agente não cria checkpoint. +O agente não executa guardrails globais. +O agente não chama sistema externo diretamente quando existe MCP Tool Router. +``` + +O agente deve responder perguntas como: + +```text +Qual problema de negócio estou resolvendo? +Quais dados preciso para responder com segurança? +Quais tools podem fornecer esses dados? +Quais regras de domínio impedem ou autorizam uma ação? +Qual resposta deve ser devolvida ao usuário? +Quais eventos IC preciso emitir para auditoria da jornada? +``` + +### 5.2. Responsabilidades do arquivo `app/agents/financeiro_agent.py` + +Esse arquivo deve conter a lógica específica do agente financeiro. Ele deve: + +1. Receber o `state`. +2. Separar `context`, `session`, `business_context` e `tool_arguments`. +3. Emitir IC de início usando `AgentRuntimeMixin`. +4. Coletar contexto de tools MCP, se houver, usando o MCP Tool Router do framework. +5. Coletar contexto RAG, se houver, usando o RAG genérico do framework. +6. Montar um prompt de domínio. +7. Chamar o LLM pelo runtime comum, com cache e telemetria. +8. Montar uma resposta padronizada. +9. Emitir IC de conclusão. +10. Retornar dados para o workflow. + + +### 5.2.1. Entendendo `state`, `context`, `session`, `business_context` e `tool_arguments` + +Antes de copiar o código do agente, o desenvolvedor precisa entender **de onde vêm os dados**. Em um agente corporativo, o erro mais comum é pegar qualquer campo diretamente do `state` sem saber se aquele dado veio do canal, do gateway, do identity resolver, do roteador ou do usuário. + +O `state` é o envelope completo da execução do LangGraph. Dentro dele normalmente existe um `context`, que é o contexto normalizado pelo framework. + +Dentro de `context`, se o projeto usa **Agent Gateway / Global Supervisor**, é comum existir também um bloco `session`: + +```python +ctx = state.get("context") or {} +session = ctx.get("session") or {} +``` + +O papel de cada bloco é diferente: + +```text +state + Estado completo do workflow atual. Carrega texto, intent, route, resposta parcial, + resultados MCP, dados de guardrail, checkpoint e outros campos técnicos. + +context + Contexto normalizado da mensagem atual. Normalmente vem do Channel Gateway, + Identity Resolver e Agent Gateway. + +session + Dados da sessão e do canal. Ajuda a saber quem está conversando, por qual canal, + em qual tenant, qual sessão global está ativa e qual backend/agente está atendendo. + +business_context + Dados de negócio já normalizados. Exemplo: customer_key, contract_key, + interaction_key, session_key, protocol_id, invoice_id, order_id. + +tool_arguments + Parâmetros explícitos já preparados para tools/MCP. Quando existe, deve ter + prioridade sobre inferências feitas pelo agente. +``` + +A ordem de confiança recomendada é: + +```text +1. tool_arguments explícitos +2. business_context resolvido pelo framework +3. context normalizado +4. session e session.metadata, quando vierem do Agent Gateway +5. state direto +6. texto original do usuário, apenas para extração complementar +``` + +Essa ordem evita dois problemas: + +```text +Problema 1: ignorar dados já resolvidos pelo Gateway/Identity Resolver. +Problema 2: sobrescrever um parâmetro canônico com um valor bruto e menos confiável. +``` + +Exemplo prático: se o `business_context.customer_key` já foi resolvido pelo framework, o agente não deve preferir um `user_id` genérico da sessão apenas porque ele existe. O `user_id` identifica o usuário no canal; o `customer_key` identifica o cliente no negócio. + +Mesmo que um agente simples não use `session` diretamente, existe uma diferença entre **sessão técnica** e **contexto de negócio**. + +### 5.2.2. Entendendo a classe `AgentRuntimeMixin` de `runtime.py` + +Antes de escrever um agente novo, o desenvolvedor precisa entender por que quase todos os exemplos herdam de: + +```python +from app.agents.runtime import AgentRuntimeMixin +``` + +O `AgentRuntimeMixin` é uma camada de conveniência operacional para o agente. Ele não é o agente, não é o workflow e não contém regra de negócio. Ele existe para evitar que cada agente tenha que reimplementar, de forma diferente, as mesmas capacidades técnicas. + +Em termos simples: + +```text +AgentRuntimeMixin = caixa de ferramentas padronizada do agente +FinanceiroAgent = regra de negócio que usa essa caixa de ferramentas +AgentWorkflow = motor LangGraph que chama o agente +Framework = infraestrutura corporativa completa +``` + +Sem o `AgentRuntimeMixin`, cada desenvolvedor tenderia a escrever código próprio para: + +```text +emitir IC/NOC/GRL +chamar MCP Tool Router +chamar RAG +montar cache de LLM +chamar LLM +montar chave de cache +tratar ausência de observer, cache, RAG ou tools +``` + +Isso geraria agentes inconsistentes. Um agente emitiria IC de um jeito, outro chamaria MCP diretamente, outro ignoraria cache, outro quebraria quando o observer estivesse desabilitado. O mixin evita esse problema. + +#### 5.2.2.1. O que o `AgentRuntimeMixin` oferece + +No template, o `AgentRuntimeMixin` concentra métodos utilitários como: + +| Método | Para que serve | Quando o agente usa | +|---|---|---| +| `_emit_ic()` | Emite evento de negócio/auditoria | início, fim, decisão de negócio, contexto coletado | +| `_emit_noc()` | Emite evento operacional | erro técnico, timeout, fallback, indisponibilidade | +| `_emit_grl()` | Emite evento de governança customizado | regra de domínio bloqueou ou sanitizou algo | +| `_retrieve_rag_context()` | Consulta o RAG genérico do framework | agente precisa de contexto documental | +| `_collect_mcp_context()` | Chama as tools MCP declaradas no `state.mcp_tools` | agente precisa consultar sistemas externos | +| `_cache_get()` | Lê cache genérico | uso avançado, normalmente indireto | +| `_cache_set()` | Grava cache genérico | uso avançado, normalmente indireto | +| `_llm_cache_key()` | Monta chave estável de cache do LLM | normalmente usado internamente | +| `_invoke_llm_cached()` | Chama o LLM com cache e telemetria | agente precisa gerar resposta com LLM | + +O desenvolvedor deve pensar assim: + +```text +Eu escrevo a regra de negócio no run(). +Quando precisar de infraestrutura, chamo um helper do AgentRuntimeMixin. +``` + +#### 5.2.2.2. O que o `AgentRuntimeMixin` não deve fazer + +O mixin não deve conter regra de negócio específica, por exemplo: + +```text +calcular contestação de fatura +consultar protocolo ANATEL diretamente +abrir SR Siebel diretamente +classificar cancelamento TIM +calcular valor de boleto financeiro +validar produto de varejo específico +``` + +Essas regras pertencem ao agente ou ao MCP Server do domínio. + +A fronteira correta é: + +```text +AgentRuntimeMixin + sabe chamar MCP, RAG, cache, LLM e observer + +Agente específico + sabe quais evidências precisa, quais regras aplicar e como responder + +MCP Server + sabe falar com sistema real, mock, banco, REST, SOAP ou serviço legado +``` + +#### 5.2.2.3. Como o mixin recebe seus recursos + +O `AgentRuntimeMixin` não cria `llm`, `tool_router`, `rag_service`, `cache` ou `observer`. Ele espera que o workflow injete esses objetos no construtor do agente. + +Por isso, no agente aparece este padrão: + +```python +class FinanceiroAgent(AgentRuntimeMixin): + name = "financeiro_agent" + + def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=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 +``` + +Isso significa: + +```text +llm = motor de geração configurado pelo framework +telemetry = spans/eventos técnicos +tool_router = roteador MCP padronizado +rag_service = busca documental/grafo/vetor +cache = cache Redis/memory/etc. +settings = configurações carregadas do .env/YAML +observer = emissor IC/NOC/GRL +``` + +O agente recebe esses objetos prontos. Ele não deve criar uma nova instância por conta própria dentro do `run()`. + +#### 5.2.2.4. Como `_emit_ic()`, `_emit_noc()` e `_emit_grl()` ajudam + +Um agente precisa ser auditável, mas não deveria quebrar se a observabilidade estiver desligada. + +Por isso, os métodos de emissão do mixin são **fail-open**: se não houver `observer`, ou se ocorrer erro ao emitir evento, a jornada de negócio continua. + +Exemplo de IC: + +```python +await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", +) +``` + +O desenvolvedor não precisa montar manualmente todos os metadados básicos. O mixin já tenta incluir informações como: + +```text +session_id +conversation_key +tenant_id +agent_id +route +intent +message_id +channel_id +``` + +A regra prática é: + +```text +Use _emit_ic() para marco de negócio. +Use _emit_noc() para problema operacional. +Use _emit_grl() para governança específica do domínio. +``` + +#### 5.2.2.5. Como `_collect_mcp_context()` funciona + +O método `_collect_mcp_context(state)` lê a lista de tools já escolhidas pelo roteador: + +```python + tools = state.get("mcp_tools") or [] +``` + +Depois chama o `tool_router` do framework para cada tool. O agente não precisa saber se a tool usa HTTP, Docker, mock ou serviço real. + +Fluxo conceitual: + +```text +routing.yaml escolhe intent + ↓ +intent define mcp_tools + ↓ +state.mcp_tools recebe a lista de tools + ↓ +AgentRuntimeMixin._collect_mcp_context() + ↓ +MCP Tool Router + ↓ +MCP Server + ↓ +resultado normalizado volta ao agente +``` + +Exemplo no agente: + +```python +tool_context = await self._collect_mcp_context(state) +``` + +O desenvolvedor deve usar esse método quando basta chamar as tools definidas pela intent. + +Se o agente precisar escolher argumentos especiais por tool, pular tools perigosas, exigir confirmação ou montar parâmetros adicionais, ele pode implementar um método próprio no agente e chamar o router de forma mais controlada, como no exemplo do `BackofficeAgent`. + +#### 5.2.2.6. Como `_retrieve_rag_context()` funciona + +O método `_retrieve_rag_context(state)` consulta o RAG genérico configurado no framework. + +Ele usa como texto base: + +```text +state.sanitized_input ou state.user_text +``` + +E tenta definir um namespace de busca a partir de: + +```text +agent_profile.rag_namespace +agent_id +route +default +``` + +Também pode usar informações do `business_context`, como `customer_key` ou `contract_key`, para enriquecer busca em grafo ou contexto relacionado. + +Exemplo: + +```python +rag_context, rag_metadata = await self._retrieve_rag_context(state) +``` + +O agente usa `rag_context` no prompt e pode retornar `rag_metadata` para auditoria/debug. + +Regra prática: + +```text +Use RAG quando a resposta depende de documento, política, base de conhecimento ou conteúdo não codificado. +Não use RAG para substituir uma consulta operacional que deve ser feita por tool MCP. +``` + +#### 5.2.2.7. Como `_invoke_llm_cached()` funciona + +O método `_invoke_llm_cached()` chama o LLM passando mensagens no formato chat: + +```python +answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) +``` + +Antes de chamar o LLM, ele monta uma chave de cache considerando elementos como: + +```text +nome do agente +tenant_id +agent_id +intent +customer_key +contract_key +interaction_key +texto do usuário +conteúdo do prompt +``` + +Se já existir resposta no cache, o método retorna o valor cacheado. Se não existir, chama o LLM, grava no cache e retorna a resposta. + +Isso evita que cada agente implemente cache de forma diferente. + +O desenvolvedor deve entender que o cache é útil para prompts determinísticos ou consultas repetidas, mas deve ser usado com cuidado em ações sensíveis. O agente não deve confirmar operação externa apenas porque uma resposta de LLM veio de cache. Confirmações operacionais devem depender de retorno real da tool. + +#### 5.2.2.8. Quando usar `_collect_mcp_context()` e quando criar lógica própria + +Use `_collect_mcp_context()` quando: + +```text +a intent já definiu as tools corretas +os parâmetros canônicos já estão no business_context +a execução pode chamar todas as tools da lista +nenhuma tool representa ação sensível +``` + +Crie lógica própria no agente quando: + +```text +uma tool só pode ser chamada após confirmação explícita +uma tool exige argumentos adicionais derivados da mensagem +uma tool deve ser pulada se faltar campo obrigatório +uma tool de registro/alteração não pode rodar automaticamente +uma sequência de tools depende do resultado anterior +``` + +Exemplo de regra segura: + +```python +if tool.startswith("registrar_") and not action_text: + return {"ok": False, "skipped": True, "reason": "ação sem confirmação explícita"} +``` + +Isso é regra de domínio e deve ficar no agente, não no mixin. + +#### 5.2.2.9. Como o dev deve ler o `run()` de um agente que herda o mixin + +Ao abrir um agente, o desenvolvedor deve procurar esta estrutura mental: + +```text +1. O agente emite IC de início? +2. Ele lê context/session/business_context de forma organizada? +3. Ele valida dados obrigatórios do domínio? +4. Ele chama MCP usando o mixin ou lógica própria controlada? +5. Ele chama RAG quando precisa de conhecimento documental? +6. Ele monta prompt com evidências, e não com chute? +7. Ele chama LLM via _invoke_llm_cached()? +8. Ele emite IC/NOC/GRL relevantes? +9. Ele retorna answer, next_state, mcp_results e metadados úteis? +``` + +Se o agente faz isso, ele está usando o framework corretamente. + +#### 5.2.2.10. Exemplo mínimo de uso correto do mixin + +```python +async def run(self, state): + await self._emit_ic("IC.FINANCEIRO_STARTED", state, component="agent.financeiro.start") + + ctx = state.get("context") or {} + business_context = ctx.get("business_context") or state.get("business_context") or {} + + if not business_context.get("customer_key"): + return { + "answer": "Informe o identificador do cliente para continuar.", + "next_state": "WAITING_CUSTOMER_KEY", + "mcp_results": [], + } + + mcp_results = await self._collect_mcp_context(state) + rag_context, rag_metadata = await self._retrieve_rag_context(state) + + messages = [ + {"role": "system", "content": "Você é um agente financeiro corporativo."}, + {"role": "user", "content": f"Evidências MCP: {mcp_results}\nContexto RAG: {rag_context}"}, + ] + + answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) + + await self._emit_ic("IC.FINANCEIRO_COMPLETED", state, {"mcp_count": len(mcp_results)}, component="agent.financeiro.completed") + + return { + "answer": answer, + "next_state": "FINANCEIRO_ACTIVE", + "mcp_results": mcp_results, + "rag_metadata": rag_metadata, + } +``` + +Esse exemplo mostra a intenção do mixin: o desenvolvedor escreve o raciocínio do agente, mas delega infraestrutura para métodos padronizados. + +#### 5.2.2.11. Erros comuns ao usar o `AgentRuntimeMixin` + +```text +Herdar de AgentRuntimeMixin, mas chamar REST diretamente dentro do agente. +Criar outro cache manual em vez de usar _invoke_llm_cached(). +Emitir eventos diretamente em formatos diferentes do observer. +Colocar regra de domínio dentro do runtime.py. +Usar _collect_mcp_context() para tool de ação sem confirmação. +Ignorar business_context e pegar parâmetros soltos do payload. +Tratar session_id global e backend_session_id como se fossem a mesma coisa. +Sobrescrever métodos internos do mixin sem necessidade. +``` + +A regra mais importante é: + +```text +O mixin padroniza capacidades técnicas. +O agente decide como aplicar essas capacidades ao domínio. +``` + + +### 5.2.3. Entendendo `messages`: arquitetura conversacional do agente + +Depois de entender `state`, `context`, `session`, `business_context`, `tool_arguments` e `AgentRuntimeMixin`, falta entender uma peça central: `messages`. + +Em um agente, `messages` não é apenas uma lista de textos. Ele é o **contrato conversacional** que será enviado ao LLM naquela chamada. É nesse contrato que o agente organiza instruções, pergunta do usuário, evidências, contexto RAG, resultados MCP, memória resumida e formato esperado da resposta. + +Um exemplo mínimo é: + +```python +messages = [ + { + "role": "system", + "content": "Você é um agente financeiro. Não invente dados.", + }, + { + "role": "user", + "content": "Quero consultar meu pagamento.", + }, +] +``` + +Esse formato é comum em frameworks e provedores modernos de IA conversacional. Ele aparece, com pequenas variações, em OpenAI Chat Completions/Responses API, OCI Generative AI OpenAI-compatible, LangChain `ChatModel`, LangGraph, Semantic Kernel, LlamaIndex e em arquiteturas com tool calling e MCP. + +A ideia é simples: + +```text +O agente monta uma conversa canônica. +O AgentRuntimeMixin chama o provider LLM padronizado. +O provider adapta essa conversa para o backend real. +``` + +Isso permite que o agente continue escrevendo `messages` de forma previsível, mesmo que por baixo o projeto use OCI Generative AI, OpenAI-compatible endpoint, LangChain, Llama local, mock ou outro provider. + +#### 5.2.3.1. Papéis principais de uma mensagem + +Cada item de `messages` possui pelo menos um `role` e um `content`. + +| Role | Para que serve | +|---|---| +| `system` | Define identidade, limites, políticas, regras e comportamento do agente. | +| `user` | Representa a solicitação atual do usuário ou uma instrução contextualizada pelo framework. | +| `assistant` | Representa respostas anteriores do modelo, quando o histórico é incluído explicitamente. | +| `tool` | Representa resultado de ferramenta em fluxos com tool calling estruturado. | +| `developer` | Em alguns provedores, representa instruções intermediárias do desenvolvedor ou da aplicação. | + +No template, o padrão mais simples usa principalmente: + +```text +system → quem é o agente, o que ele pode fazer e o que ele não pode fazer +user → mensagem atual + evidências + contexto de negócio + MCP + RAG +``` + +Esse padrão é intencionalmente simples para manter compatibilidade com vários runtimes. + +#### 5.2.3.2. O que deve ir no `system` + +O `system` deve conter regras estáveis e de maior prioridade. Ele responde: + +```text +Quem é este agente? +Qual domínio ele atende? +Quais limites ele deve respeitar? +O que ele nunca deve inventar? +Quando ele deve pedir mais dados? +Quando ele deve recusar uma ação? +Qual tom e formato de resposta deve usar? +``` + +Exemplo: + +```python +system_content = apply_agent_profile_prompt( + state, + """ + Você é um agente financeiro corporativo. + Use somente dados fornecidos por MCP, RAG ou business_context. + Não confirme pagamento, baixa, acordo ou contestação sem evidência de tool. + Se faltar identificador obrigatório, peça apenas esse dado. + Responda de forma curta, operacional e auditável. + """.strip(), +) +``` + +Regras críticas devem ficar no `system`, não escondidas no meio do `user`. + +#### 5.2.3.3. O que deve ir no `user` + +O `user` deve trazer o pedido atual e o contexto necessário para responder. No agente corporativo, ele normalmente contém: + +```text +mensagem atual do usuário +intent escolhida pelo roteador +route/agente ativo +business_context normalizado +resultados MCP +contexto RAG +metadados relevantes de sessão +instrução de formato para a resposta +``` + +Exemplo: + +```python +messages = [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": ( + "Mensagem do usuário:\n" + f"{user_text}\n\n" + "Intent e rota escolhidas pelo framework:\n" + f"intent={state.get('intent')} route={state.get('route')}\n\n" + "Contexto de negócio normalizado:\n" + f"customer_key={business_context.get('customer_key')}\n" + f"contract_key={business_context.get('contract_key')}\n" + f"interaction_key={business_context.get('interaction_key')}\n\n" + "Resultados MCP:\n" + f"{tool_context}\n\n" + "Contexto RAG:\n" + f"{rag_context or '[sem contexto RAG]'}\n\n" + "Instrução de resposta:\n" + "Responda somente com base nas evidências acima. " + "Se uma evidência obrigatória estiver ausente, diga que não foi encontrada." + ), + }, +] +``` + +Observe que o exemplo não joga o `state` inteiro no prompt. Ele seleciona os campos relevantes. + +#### 5.2.3.4. Relação entre `messages`, memória e histórico + +`messages` não é a memória persistente do agente. + +```text +Memória persistente + Fica no repositório/memória do framework. + Pode sobreviver a várias interações. + Pode ser resumida, compactada ou consultada. + +messages + É o payload enviado ao LLM em uma chamada específica. + Pode incluir um resumo de memória. + Pode incluir parte do histórico. + Não deve virar um dump completo da conversa. +``` + +Se o framework já carregou histórico ou resumo de conversa, o agente deve usar apenas o trecho necessário. Duplicar histórico manualmente aumenta custo, latência e risco de inconsistência. + +#### 5.2.3.5. Relação entre `messages`, MCP e RAG + +MCP e RAG produzem evidências. O LLM usa essas evidências para redigir a resposta. + +```text +MCP Tool Router + consulta sistemas, mocks, serviços ou ações externas + retorna dados estruturados + +RAG + busca contexto documental + retorna trechos relevantes e metadados + +messages + organizam essas evidências em uma conversa para o LLM +``` + +Um bom agente deixa claro para o LLM o que é evidência e o que é instrução. + +Evite misturar tudo em um texto sem estrutura. Prefira blocos: + +```text +Instruções: +- Não invente dados. + +Mensagem do usuário: +... + +Evidências MCP: +... + +Contexto RAG: +... + +Formato esperado: +... +``` + +Essa organização melhora a rastreabilidade e reduz alucinação. + +#### 5.2.3.6. Compatibilidade com frameworks de mercado + +O padrão de `messages` é compatível com a maior parte do ecossistema de IA conversacional, mas existem diferenças entre provedores. + +| Framework/provedor | Compatibilidade conceitual | Atenção | +|---|---|---| +| OpenAI Chat/Responses | Alta | Roles, tool calls e formatos multimodais podem variar por API. | +| OCI Generative AI OpenAI-compatible | Alta | Normalmente aceita formato semelhante ao OpenAI-compatible. | +| LangChain `ChatModel` | Alta | Pode converter dicts para `SystemMessage`, `HumanMessage`, `AIMessage`. | +| LangGraph | Alta | O state pode carregar `messages` ou o agente pode montar messages por chamada. | +| Semantic Kernel | Alta | Usa conceitos equivalentes de chat history e roles. | +| LlamaIndex | Alta | Pode adaptar para chat engine ou completion engine. | +| Anthropic Messages API | Média/Alta | Pode exigir adaptações de system prompt e roles. | +| Modelos locais | Variável | Alguns esperam chat template específico. | + +Por isso, o agente não deve chamar diretamente SDKs específicos. Ele monta `messages` e delega a chamada para: + +```python +answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) +``` + +Assim, a adaptação para o provider fica centralizada no runtime/framework. + +#### 5.2.3.7. Pitfalls comuns ao montar `messages` + +**Pitfall 1 — Enviar o `state` inteiro ao LLM** + +Ruim: + +```python +{"role": "user", "content": f"State completo: {state}"} +``` + +Melhor: + +```python +{"role": "user", "content": f"customer_key={business_context.get('customer_key')}"} +``` + +O `state` pode conter dados técnicos, campos sensíveis, histórico, checkpoint e informações desnecessárias. + +**Pitfall 2 — Mandar objetos enormes sem curadoria** + +Ruim: + +```python +f"Resultados completos: {mcp_results}" +``` + +Melhor: + +```python +resumo_tools = [ + { + "tool": r.get("tool_name") or r.get("tool"), + "ok": r.get("ok"), + "status": r.get("status"), + "evidence": r.get("evidence") or r.get("summary"), + } + for r in mcp_results +] +``` + +Depois envie apenas o resumo necessário. + +**Pitfall 3 — Passar dados sensíveis sem necessidade** + +Ruim: + +```python +f"CPF completo: {cpf}" +``` + +Melhor: + +```python +f"Cliente identificado: {'sim' if customer_key else 'não'}" +``` + +Quando precisar enviar identificador, prefira chave canônica, hash ou valor mascarado, conforme política do projeto. + +**Pitfall 4 — Deixar o LLM inventar quando a tool falhou** + +Ruim: + +```text +Responda sobre o pagamento do cliente. +``` + +Melhor: + +```text +A tool consultar_pagamentos_financeiro retornou erro ou ausência de dados. +Não confirme pagamento. Informe que a evidência não foi encontrada. +``` + +**Pitfall 5 — Confundir instrução com evidência** + +Ruim: + +```text +O cliente pagou e você deve responder que está tudo certo. +``` + +Melhor: + +```text +Evidência MCP: +- consultar_pagamentos_financeiro: status=COMPENSADO + +Instrução: +- Explique o status de forma objetiva. +``` + +**Pitfall 6 — Colocar regra crítica só no `user`** + +Regra de comportamento permanente deve ir no `system`. O `user` deve carregar o pedido e o contexto daquela interação. + +**Pitfall 7 — Duplicar histórico** + +Se o framework já incluiu resumo de memória, não reenvie toda a conversa manualmente. + +**Pitfall 8 — Não pedir formato de resposta** + +Em contexto corporativo, peça resposta curta, operacional, rastreável e baseada em evidência. + +#### 5.2.3.8. Modelo recomendado de `messages` para agentes corporativos + +Use este padrão como referência: + +```python +system_content = apply_agent_profile_prompt( + state, + """ + Você é um agente corporativo especializado no domínio financeiro. + Use somente evidências vindas de business_context, MCP e RAG. + Não invente protocolo, cliente, contrato, status, pagamento ou ação operacional. + Se faltar dado obrigatório, peça apenas esse dado. + Responda de forma curta, operacional e auditável. + """.strip(), +) + +messages = [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": ( + "Mensagem do usuário:\n" + f"{user_text}\n\n" + "Contexto de sessão resumido:\n" + f"channel={session.get('channel')} tenant_id={session.get('tenant_id')}\n" + f"global_session_id={session.get('global_session_id')}\n\n" + "Contexto de negócio:\n" + f"customer_key={business_context.get('customer_key')}\n" + f"contract_key={business_context.get('contract_key')}\n" + f"interaction_key={business_context.get('interaction_key')}\n\n" + "Intent e rota:\n" + f"intent={state.get('intent')} route={state.get('route')}\n\n" + "Evidências MCP:\n" + f"{mcp_evidence}\n\n" + "Contexto RAG:\n" + f"{rag_context or '[sem contexto RAG]'}\n\n" + "Formato esperado:\n" + "1. Resposta direta ao usuário.\n" + "2. Não cite detalhes internos de arquitetura.\n" + "3. Se faltou evidência, diga claramente o que faltou." + ), + }, +] +``` + +Esse padrão ajuda o desenvolvedor a separar: + +```text +Regras permanentes → system +Pedido e contexto atual → user +Evidências de tools → bloco MCP +Conhecimento documental → bloco RAG +Sessão/canal → contexto resumido +Formato de saída → instrução final +``` + +#### 5.2.3.9. Como revisar `messages` durante desenvolvimento + +Durante o desenvolvimento, antes de culpar o LLM, revise o payload enviado para ele. + +Perguntas úteis: + +```text +O system prompt contém as regras mais importantes? +O user prompt contém a pergunta real do usuário? +O business_context certo foi incluído? +Os resultados MCP aparecem como evidência, e não como instrução inventada? +O RAG trouxe contexto útil ou só ruído? +Há dados sensíveis desnecessários? +O prompt está grande demais? +O formato de resposta esperado está claro? +``` + +Uma boa prática é emitir um IC de debug em ambiente não produtivo ou logar uma versão sanitizada do prompt, nunca o prompt bruto com dados sensíveis. + + +### 5.2.4. Recursos avançados agora padronizados pelo framework + +Nos primeiros exemplos deste tutorial, o agente usa diretamente métodos simples como `_collect_mcp_context()` e `_invoke_llm_cached()`. Isso é suficiente para agentes simples. Porém, em agentes reais migrados para o framework, como um Backoffice/ANATEL, aparecem necessidades adicionais: + +```text +normalizar tools por intent; +ler context/session/business_context/tool_arguments sempre da mesma forma; +montar argumentos MCP com aliases; +bloquear tools de ação quando falta payload obrigatório; +executar tools uma a uma com eventos de observabilidade; +montar messages sem despejar o state inteiro no prompt; +gerar fallback controlado quando o LLM falha. +``` + +Essas necessidades não são exclusivas do Backoffice. Por isso, a partir desta versão, elas passam a ser tratadas como **capacidades reutilizáveis do framework**, e não como código que cada agente deve copiar. + +#### 5.2.4.1. `RuntimeContext`: leitura canônica do state + +O framework passa a oferecer um objeto conceitual chamado `RuntimeContext`, obtido pelo agente com: + +```python +runtime = self.get_runtime_context(state) +``` + +Esse objeto organiza: + +```text +runtime.state → state completo do LangGraph +runtime.context → context normalizado +runtime.session → dados de sessão/canal vindos do Gateway +runtime.session_metadata → metadata da sessão +runtime.business_context → identidade de negócio canônica +runtime.tool_arguments → parâmetros explícitos para tools +runtime.sanitized_input → texto sanitizado pelos guardrails +runtime.original_text → texto original, quando necessário para extração controlada +``` + +O desenvolvedor não precisa ficar repetindo: + +```python +ctx = state.get("context") or {} +session = ctx.get("session") or {} +business_context = ctx.get("business_context") or state.get("business_context") or {} +``` + +Ele pode usar: + +```python +runtime = self.get_runtime_context(state) +customer_key = runtime.pick("customer_key", "cpf", "cnpj", "msisdn") +``` + +A ordem de confiança continua padronizada: + +```text +1. tool_arguments +2. business_context +3. context +4. session +5. session.metadata +6. state +``` + +#### 5.2.4.2. `normalize_tools_by_intent()`: fallback de tools sem tirar poder do router + +Em um agente ideal, o `EnterpriseRouter` escolhe a intent e injeta `mcp_tools` no `state`. Mas, em testes, chamadas diretas ou migrações, o agente pode ser executado sem essa injeção. + +Para isso, o framework oferece: + +```python +normalized_state = self.normalize_tools_by_intent( + state, + default_tools_by_intent=DEFAULT_TOOLS_BY_INTENT, + default_intent="financeiro_pagamentos", + route=self.name, +) +``` + +A regra é: + +```text +Se state['mcp_tools'] veio do router, use essas tools. +Se não veio, use o fallback declarado pelo agente. +Remova duplicidades. +Preserve ordem estável. +Defina intent, route e active_agent quando estiverem ausentes. +``` + +Isso evita que cada agente implemente seu próprio `_normalize_state_tools()`. + +#### 5.2.4.3. `build_tool_arguments()`: argumentos MCP canônicos + +O agente pode montar argumentos MCP sem conhecer todos os detalhes do mapper: + +```python +args = self.build_tool_arguments( + state, + tool_name="consultar_titulo_financeiro", + intent=state.get("intent"), + aliases={ + "customer_key": ["customer_id", "cpf", "cnpj"], + "contract_key": ["contract_id", "invoice_id"], + }, +) +``` + +Esse método monta argumentos como: + +```text +query +operator_instructions +customer_key +contract_key +interaction_key +session_key +parâmetros explícitos de tool_arguments +aliases configurados pelo domínio +``` + +Depois disso, o `MCPToolRouter` ainda aplica o `mcp_parameter_mapping.yaml`. Ou seja: + +```text +build_tool_arguments() monta o contrato canônico. +mcp_parameter_mapping.yaml traduz para o nome esperado por cada MCP Server. +``` + +#### 5.2.4.4. Política de execução de tools sensíveis + +Nem toda tool é apenas consulta. Algumas tools executam ações, como registrar parecer, abrir solicitação, cancelar serviço ou criar protocolo. + +Essas tools devem ser declaradas com política em `config/tools.yaml`: + +```yaml +tools: + registrar_acao_backoffice: + description: Registra ação operacional no backoffice. + mcp_server: backoffice + enabled: true + tool_type: action + requires: [protocol_id, action_text, operator_session] + confirmation_required: false + args_schema: + protocol_id: string + action_text: string + operator_session: string +``` + +Com isso, o framework consegue bloquear a chamada antes de chegar ao MCP quando falta campo obrigatório: + +```text +Tool registrar_acao_backoffice escolhida. +Framework monta argumentos. +Framework verifica requires. +Se action_text estiver ausente, retorna skipped=true. +Agente emite IC/NOC de domínio, se necessário. +``` + +Isso evita que cada agente escreva manualmente: + +```python +if tool.startswith("registrar_") and not arguments.get("action_text"): + ... +``` + +#### 5.2.4.5. `execute_tools_for_intent()`: execução padronizada das tools + +O agente pode executar tools selecionadas pela intent com: + +```python +mcp_results = await self.execute_tools_for_intent( + state, + tools=state.get("mcp_tools") or [], + aliases=TOOL_ALIASES, +) +``` + +Esse método cuida de: + +```text +montar argumentos; +aplicar política de execução; +chamar _call_mcp_tool(); +normalizar resultado; +emitir IC.MCP_TOOL_CALLED; +emitir IC.TOOL_CALLED; +emitir NOC.MCP_TOOL_FAILED quando houver falha; +retornar skipped=true quando uma política bloquear a execução. +``` + +O agente ainda pode emitir ICs específicos de negócio depois disso. Exemplo: `AGA.010` para Speech Analytics, `AGA.011` para Cliente/IMDB, `AGA.020` para TAIS/templates. + +#### 5.2.4.6. `build_messages()`: messages padronizado + +Para evitar que cada agente monte prompts de forma diferente, o framework oferece: + +```python +messages = self.build_messages( + state, + system_prompt=system_prompt, + mcp_results=mcp_results, + rag_context=rag_context, + rag_metadata=rag_metadata, +) +``` + +Esse builder separa: + +```text +system prompt; +mensagem do usuário; +intent e route; +business_context; +resultados MCP; +contexto RAG; +metadados RAG; +seções extras. +``` + +O objetivo é reduzir estes erros: + +```text +enviar state inteiro para o LLM; +misturar regra permanente com evidência; +incluir dados sensíveis sem necessidade; +esquecer de informar que uma tool falhou; +duplicar histórico que o framework já carrega. +``` + +#### 5.2.4.7. Quando customizar e quando usar o framework + +Use o framework para: + +```text +ler contexto; +normalizar tools; +montar argumentos MCP; +aplicar política de execução; +chamar MCP; +montar messages; +chamar LLM com cache; +emitir eventos técnicos genéricos. +``` + +Use o agente para: + +```text +definir regras de negócio; +definir aliases específicos do domínio; +definir prompts do domínio; +definir ICs específicos da jornada; +definir estados conversacionais como WAITING_*; +tratar compatibilidade de migração; +decidir fallback textual específico do domínio. +``` + +Essa separação permite que um agente real tenha customizações fortes sem virar um motor paralelo ao framework. + + +### 5.3. Criar o arquivo do agente + +Crie: + +```text +app/agents/financeiro_agent.py +``` + +Código-base comentado: + +```python +from app.agents.prompting import apply_agent_profile_prompt +from app.agents.runtime import AgentRuntimeMixin + + +class FinanceiroAgent(AgentRuntimeMixin): + # Este nome precisa bater com o nome usado no workflow e nas configurações. + name = "financeiro_agent" + + def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None): + # Estes objetos são injetados pelo workflow/framework. + # O agente usa, mas não cria esses motores. + 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 + + async def run(self, state): + # 1. Marca o início da jornada de negócio deste agente. + await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", + ) + + # 2. Separa os blocos do contrato do framework. + # O agente lê esses blocos, mas quem cria/normaliza é o framework. + ctx = state.get("context") or {} + session = ctx.get("session") or {} + session_metadata = session.get("metadata") or {} + business_context = ctx.get("business_context") or state.get("business_context") or {} + tool_arguments = ctx.get("tool_arguments") or state.get("tool_arguments") or {} + + # 3. Interpreta a mensagem atual usando o texto já sanitizado pelos guardrails, + # mas preserva o texto original apenas quando precisar extrair identificadores. + user_text = state.get("sanitized_input") or state.get("user_text") or "" + original_text = ( + ctx.get("message") + or ctx.get("text") + or ctx.get("query") + or session.get("last_user_message") + or state.get("user_text") + or user_text + ) + + # 4. Chama tools MCP selecionadas pelo roteamento, quando configuradas. + # O agente não precisa saber se a tool usa REST, SOAP, DB ou mock. + tool_context = await self._collect_tool_context(state) + + if tool_context: + await self._emit_ic( + "IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.financeiro.mcp", + ) + + # 5. Recupera contexto documental, se o RAG estiver habilitado. + rag_context, rag_metadata = await self._retrieve_rag_context(state) + + # 6. Monta a mensagem para o LLM. + # O system prompt define comportamento e limites do agente. + # O user prompt leva dados, evidências e contexto. + messages = [ + { + "role": "system", + "content": apply_agent_profile_prompt( + state, + "Você é um agente financeiro. Responda com clareza, usando dados das ferramentas quando disponíveis. Não confirme ações financeiras sem evidência e confirmação explícita." + ), + }, + { + "role": "user", + "content": ( + f"Mensagem: {state.get('sanitized_input') or state['user_text']}\n" + f"Sessão: {session}\n" + f"Intent: {state.get('intent')}\n" + f"Dados MCP: {tool_context}\n" + f"Contexto RAG: {rag_context}" + ), + }, + ] + + # 7. Chama o LLM usando o runtime comum, com cache e telemetria. + answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages) + + # 8. Retorna no contrato esperado pelo workflow. + result = { + "answer": f"[FinanceiroAgent] {answer}", + "next_state": "FINANCEIRO_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + } + + # 9. Marca o fim da jornada de negócio. + await self._emit_ic( + "IC.FINANCEIRO_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + }, + component="agent.financeiro.completed", + ) + + return result + + async def _collect_tool_context(self, state): + # Este método delega para o MCP Tool Router do framework. + # As tools chamadas dependem da intent definida em routing.yaml. + return await self._collect_mcp_context(state) +``` + +### 5.3.1. Como adaptar esse exemplo para um agente real + +No exemplo acima, `session`, `business_context` e `tool_arguments` aparecem no prompt para fins didáticos. Em produção, o desenvolvedor deve evitar jogar objetos enormes diretamente no prompt. O ideal é selecionar apenas os campos necessários. + +Exemplo de raciocínio para um agente financeiro: + +```text +session.channel → útil para ajustar linguagem ou entender origem da conversa. +session.tenant_id → útil para isolamento multi-tenant. +business_context.customer_key → útil para consultar cliente/título/pagamento. +business_context.contract_key → útil para consultar contrato, fatura ou pedido. +business_context.interaction_key → útil para rastrear protocolo/chamado/interação. +tool_arguments → útil quando o Gateway ou Identity Resolver já preparou parâmetros exatos. +``` + +Uma função utilitária comum dentro do agente é um `pick()` com ordem de precedência explícita: + +```python +def pick(name: str, *, tool_arguments, business_context, ctx, session, session_metadata, state): + if name in tool_arguments: + return tool_arguments.get(name) + if isinstance(business_context, dict) and name in business_context: + return business_context.get(name) + if name in ctx: + return ctx.get(name) + if name in session: + return session.get(name) + if name in session_metadata: + return session_metadata.get(name) + return state.get(name) +``` + +Essa função deixa claro que o agente não está “adivinhando” de onde vem o dado. Ele está seguindo uma política de confiança. + +### 5.3.2. Onde entra o Agent Gateway nesse código? + +Quando existe Agent Gateway / Global Supervisor, ele pode enriquecer a mensagem antes de enviá-la ao backend do agente. Exemplos de dados que podem chegar em `context.session`: + +```json +{ + "session": { + "global_session_id": "s1", + "backend_session_id": "default:financeiro_agent:s1", + "active_backend": "financeiro", + "channel": "web", + "tenant_id": "default", + "metadata": { + "selected_backend": "financeiro", + "last_reason": "Backend escolhido por regras: matches=['pagamento']" + } + } +} +``` + +O agente não deve usar esse bloco para tomar decisão de negócio final. Ele deve usá-lo para contexto técnico, rastreabilidade e continuidade da conversa. A decisão de negócio deve continuar baseada em `business_context`, tools MCP, RAG e regras de domínio. + +### 5.4. Como saber se o agente está bem implementado? + +Um agente está bem implementado quando: + +```text +Ele conhece regras de negócio, mas não conhece detalhes de infraestrutura. +Ele usa o runtime comum para LLM, RAG, cache, MCP e IC. +Ele retorna um contrato simples para o workflow. +Ele não duplica guardrail, checkpoint, sessão, memória ou telemetria. +Ele consegue ser testado isoladamente com state simulado. +``` + +--- + +## 6. Registrando o agente no workflow + +### 6.1. Antes do código: o que é o workflow? + +O workflow é o caminho controlado pelo LangGraph. Ele define a ordem de execução: + +```text +entrada → guardrails → roteamento → agente → revisão → persistência → resposta +``` + +Criar a classe do agente não basta. O LangGraph só executa nós que foram registrados no grafo. + +O registro no workflow responde três perguntas: + +```text +Qual classe implementa o agente? +Qual nome de nó representa esse agente no grafo? +Para onde o fluxo segue depois que o agente responde? +``` + +### 6.2. Importar o agente + +Edite: + +```text +app/workflows/agent_graph.py +``` + +Adicione: + +```python +from app.agents.financeiro_agent import FinanceiroAgent +``` + +### 6.3. Instanciar o agente + +No `__init__` da classe `AgentWorkflow`, depois da criação de `agent_kwargs`: + +```python +self.financeiro = FinanceiroAgent(llm, **agent_kwargs) +``` + +Essa linha injeta no agente os mesmos motores compartilhados pelos demais agentes: LLM, telemetry, MCP Tool Router, RAG, cache, settings e observer. + +### 6.4. Criar o nó do LangGraph + +Em `_build_graph()`: + +```python +builder.add_node("financeiro_agent", self._node("financeiro_agent", self.financeiro_agent)) +``` + +O primeiro `financeiro_agent` é o nome do nó no grafo. O segundo `self.financeiro_agent` é o método wrapper que será chamado quando o fluxo chegar nesse nó. + +### 6.5. Adicionar rota condicional + +No dicionário de `builder.add_conditional_edges("routing_decision", ...)`, inclua: + +```python +"financeiro_agent": "financeiro_agent", +``` + +Exemplo: + +```python +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", + "financeiro_agent": "financeiro_agent", + "handoff": "handoff", + "supervisor_agent": "supervisor_agent", + }, +) +``` + +Essa tabela conecta a decisão do roteador com o nó real do grafo. + +### 6.6. Conectar o nó ao Output Supervisor + +```python +builder.add_edge("financeiro_agent", "output_supervisor") +``` + +Essa linha é importante porque a resposta do agente não deve ir direto ao usuário. Ela passa antes por output supervisor, output guardrails, judges, supervisor review e persistência. + +### 6.7. Criar o método wrapper + +Na classe `AgentWorkflow`: + +```python +async def financeiro_agent(self, state): + async with self.langgraph_telemetry.node("financeiro_agent", state): + async with self.telemetry.span( + "workflow.agent.financeiro", + session_id=state.get("conversation_key") or state.get("session_id"), + input={"intent": state.get("intent")}, + ): + return await self.financeiro.run(state) +``` + +O wrapper adiciona telemetria ao redor do agente. A lógica de negócio continua dentro de `FinanceiroAgent.run()`. + +### 6.8. Adicionar ao modo supervisor + +No método `supervisor_agent()`, ajuste o mapa de handlers: + +```python +handlers = { + "billing_agent": self.billing.run, + "product_agent": self.product.run, + "orders_agent": self.orders.run, + "support_agent": self.support.run, + "financeiro_agent": self.financeiro.run, +} +``` + +Isso permite que o supervisor chame o novo agente quando `ROUTING_MODE=supervisor` ou quando houver handoff supervisionado. + +### 6.9. Erros comuns neste capítulo + +```text +Criar a classe do agente, mas esquecer add_node. +Adicionar add_node, mas esquecer add_conditional_edges. +Adicionar rota, mas esquecer add_edge para output_supervisor. +Usar nome diferente em routing.yaml, workflow e classe. +Chamar self.financeiro.run direto sem wrapper de telemetria. +``` + +--- + +## 7. Ajustando o estado do agente + +### 7.1. Antes do código: o que é o state? + +O `state` é o objeto que trafega entre os nós do LangGraph. Ele funciona como a memória de curto prazo da execução atual. + +Ele não é o banco de dados, não é a memória conversacional completa e não deve virar um repositório gigante de informações. + +Use o `state` para dados que precisam circular entre nós, por exemplo: + +```text +texto do usuário +intent escolhida +rota escolhida +resposta parcial +resultado de uma tool +próximo estado da conversa +flags de decisão +``` + +Não use o `state` para: + +```text +histórico longo de conversa +arquivos grandes +respostas completas de sistemas externos sem necessidade +conteúdo bruto de documentos +logs extensos +``` + +### 7.2. Quando alterar `app/state.py` + +Edite: + +```text +app/state.py +``` + +Somente adicione novos campos se o agente precisar compartilhar informações específicas com outros nós. + +Exemplo: + +```python +class AgentState(TypedDict, total=False): + # campos existentes... + financial_context: dict[str, Any] + financial_decision: dict[str, Any] +``` + +### 7.3. Critério de decisão + +Antes de criar um campo novo, pergunte: + +```text +Outro nó precisa ler este dado? +Este dado precisa sobreviver ao próximo passo do workflow? +Este dado é pequeno e estruturado? +Este dado ajuda na auditoria ou na decisão? +``` + +Se a resposta for não, deixe o dado local ao agente ou grave em repositório apropriado. + +--- + +## 8. Registrando o agente em `config/agents.yaml` + +### 8.1. Antes do YAML: para que serve `agents.yaml`? + +O `agents.yaml` é o cadastro oficial dos agentes disponíveis. Ele não executa o agente sozinho, mas informa ao framework quais agentes existem, quais configurações isoladas eles usam e quais metadados descrevem o domínio. + +Ele responde: + +```text +Qual é o agent_id? +Qual nome amigável aparece em listagens e debug? +Onde estão prompt, guardrails e judges específicos? +Qual domínio esse agente atende? +Quais metadados ajudam roteamento, auditoria e operação? +``` + +### 8.2. Exemplo de registro + +Edite: + +```text +config/agents.yaml +``` + +Adicione: + +```yaml +agents: + - agent_id: financeiro_agent + name: Financeiro Agent + description: Agente para dúvidas financeiras, pagamentos, saldos, acordos e segunda via. + prompt_policy_path: ./config/agents/financeiro_agent/prompt_policy.yaml + routing_config_path: ./config/routing.yaml + guardrails_config_path: ./config/agents/financeiro_agent/guardrails.yaml + judges_config_path: ./config/agents/financeiro_agent/judges.yaml + mcp_servers_config_path: ./config/mcp_servers.yaml + tools_config_path: ./config/tools.yaml + metadata: + domain: financeiro + system_prefix: | + Você está executando o financeiro_agent. + Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id. + Não misture histórico ou decisões de outros agentes. +``` + +### 8.3. Cuidados + +O `agent_id` precisa ser consistente com: + +```text +nome do nó no workflow +nome usado em routing.yaml +session_id canônico +pasta config/agents// +metadados de observabilidade +``` + +Evite renomear `agent_id` depois que o agente já estiver em produção, porque isso pode quebrar histórico, memória, checkpoint e métricas. + +--- + +## 9. Criando configurações isoladas do agente + +### 9.1. Antes do YAML: por que isolar configuração por agente? + +Cada agente pode ter política de prompt, guardrails e judges próprios. Um agente financeiro pode exigir confirmação explícita antes de uma ação. Um agente de suporte pode permitir respostas mais abertas. Um agente jurídico pode exigir evidência documental. + +Por isso, evite colocar tudo no arquivo global. Use configuração global para regras corporativas e configuração local para regras do domínio. + +Crie: + +```text +config/agents/financeiro_agent/ +``` + +### 9.2. `prompt_policy.yaml` + +Esse arquivo define a postura base do agente. + +```yaml +id: financeiro_agent_prompt_policy +version: 1 +description: Prompt base isolado do agente financeiro. +system_prefix: | + Você é um agente corporativo especializado em atendimento financeiro. + Seja claro, objetivo, auditável e não invente dados. + Quando precisar executar uma ação, use ferramentas configuradas. + Quando faltar informação obrigatória, peça apenas o dado necessário. +``` + +Use este arquivo para regras persistentes de comportamento, não para regras temporárias de teste. + +### 9.3. `guardrails.yaml` + +Esse arquivo complementa os guardrails globais. + +```yaml +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true + - code: PINJ + enabled: true +output: + - code: REVPREC + enabled: true + - code: CMP + enabled: true +``` + +Use guardrail quando a resposta precisa ser bloqueada, sanitizada ou revisada por regra. + +### 9.4. `judges.yaml` + +Judges avaliam qualidade, aderência, groundedness e outros critérios após a resposta ser produzida. + +```yaml +judges: + - name: response_quality + enabled: true + threshold: 0.7 + - name: groundedness + enabled: true + threshold: 0.6 +``` + +Use judge para avaliar resposta. Use guardrail para bloquear ou proteger. Use prompt para orientar comportamento. + +--- + +## 10. Configurando roteamento em `config/routing.yaml` + +### 10.1. Antes do YAML: o que é roteamento? + +Roteamento é a decisão de qual agente deve tratar a mensagem. + +Em um sistema multiagente, o usuário não deveria precisar saber qual agente chamar. Ele escreve uma mensagem, e o framework decide a rota. + +O roteador normalmente considera: + +```text +texto do usuário +estado atual da conversa +keywords +examples +prioridade +agent_id solicitado +políticas de estado +LLM router, se habilitado +``` + +### 10.2. Quando criar uma intent nova? + +Crie uma intent quando existir uma categoria clara de solicitação que deve ir para um agente específico. + +Exemplo de intent financeira: + +```yaml +intents: + - name: financeiro_pagamentos + domain: financeiro + agent: financeiro_agent + description: Dúvidas sobre pagamento, saldo, fatura, boleto, acordo, contestação e segunda via. + priority: 15 + mcp_tools: + - consultar_titulo_financeiro + - consultar_pagamentos_financeiro + keywords: + - pagamento + - boleto + - saldo + - acordo + - financeiro + - segunda via + - vencimento + - cobrança + - contestação + examples: + - Quero consultar meu pagamento. + - Preciso da segunda via do boleto. + - Meu pagamento ainda não foi baixado. +``` + +### 10.3. O que significa `mcp_tools` na intent? + +`mcp_tools` indica quais tools devem ser disponibilizadas/coletadas quando essa intent for escolhida. Assim, o agente não precisa decidir manualmente cada chamada em todos os casos simples. + +O fluxo fica: + +```text +routing.yaml escolhe intent +intent aponta agent +intent declara mcp_tools +AgentRuntimeMixin coleta contexto MCP +agente usa os dados na resposta +``` + +### 10.4. Políticas de estado + +Se a conversa já estiver em um estado específico, a próxima mensagem pode precisar voltar ao mesmo agente, mesmo que o texto seja curto. + +Exemplo: + +```yaml +state_policies: + - state: WAITING_FINANCEIRO_CONFIRMATION + agent: financeiro_agent + description: Mantém confirmações curtas no fluxo financeiro. +``` + +Isso evita que uma resposta como “sim” seja roteada para o agente errado. + +### 10.5. Router versus supervisor + +No modo router: + +```env +ROUTING_MODE=router +``` + +O framework escolhe uma rota de forma mais direta, normalmente por regras, keywords, examples e score. + +No modo supervisor: + +```env +ROUTING_MODE=supervisor +``` + +Um supervisor pode decidir a sequência de agentes, handoff ou combinação de respostas. + +Use router quando o domínio for bem mapeado. Use supervisor quando a conversa exigir decomposição, múltiplos agentes ou decisão mais flexível. + +--- + +## 11. Configurando tools em `config/tools.yaml` + +### 11.1. Antes do YAML: o que é uma tool? + +Uma tool é uma capacidade externa que o agente pode usar para obter dados ou executar uma ação. + +Exemplos: + +```text +consultar fatura +consultar pagamento +abrir protocolo +buscar pedido +cancelar serviço +consultar base de conhecimento +``` + +A tool não é necessariamente o sistema real. Ela é o contrato que o backend conhece. O sistema real fica atrás do MCP Server. + +### 11.2. Declarando tools + +Edite: + +```text +config/tools.yaml +``` + +Adicione: + +```yaml +tools: + consultar_titulo_financeiro: + description: Consulta um título financeiro por cliente e contrato. + mcp_server: financeiro + enabled: true + args_schema: + customer_id: string + contract_id: string + + consultar_pagamentos_financeiro: + description: Consulta pagamentos financeiros por cliente. + mcp_server: financeiro + enabled: true + args_schema: + customer_id: string +``` + +### 11.3. Como pensar sobre uma tool + +Antes de declarar uma tool, defina: + +```text +Qual pergunta de negócio ela responde? +Ela só consulta ou executa uma ação? +Quais parâmetros são obrigatórios? +Quais parâmetros vêm da identidade canônica? +Qual MCP Server implementa a tool? +Qual timeout e fallback são aceitáveis? +O resultado tem dados sensíveis que precisam ser mascarados? +``` + +O backend não deve chamar diretamente HTTP/SOAP/DB de sistemas de negócio quando essa chamada puder ser padronizada via MCP Tool Router. + +--- + +## 12. Configurando servidores MCP + +### 12.1. Antes do YAML: o que é o MCP Server? + +O MCP Server é o adaptador entre o mundo do agente e os sistemas reais. Ele permite que o backend converse com ferramentas de forma padronizada, sem conhecer detalhes de REST, SOAP, banco, filas ou mocks. + +O desenho é: + +```text +Agente + ↓ +MCP Tool Router do framework + ↓ +MCP Server do domínio + ↓ +Sistema real, mock, banco, REST, SOAP ou serviço interno +``` + +### 12.2. Configuração local + +Edite: + +```text +config/mcp_servers.yaml +``` + +Exemplo: + +```yaml +servers: + financeiro: + transport: http + endpoint: http://localhost:8300/mcp + enabled: true + description: MCP Server Financeiro local. +``` + +### 12.3. Configuração em Docker Compose + +Edite: + +```text +config/mcp_servers.docker.yaml +``` + +Exemplo: + +```yaml +servers: + financeiro: + transport: http + endpoint: http://financeiro-mcp:8300/mcp + enabled: true + description: MCP Server Financeiro em Docker. +``` + +### 12.4. Como evitar erro comum de endpoint + +Localmente, `localhost` funciona porque backend e MCP rodam na mesma máquina. + +Dentro do Docker Compose, `localhost` dentro do container do backend aponta para o próprio container do backend, não para o container do MCP. Por isso, em Docker, use o nome do serviço: + +```text +http://financeiro-mcp:8300/mcp +``` + +--- + +## 13. Configurando mapeamento de parâmetros MCP + +### 13.1. Antes do YAML: por que existe mapeamento? + +O framework trabalha com chaves canônicas para não depender dos nomes específicos de cada sistema. + +Exemplo: + +```text +customer_key = cliente canônico no framework +contract_key = contrato/fatura/pedido/título canônico +interaction_key = interação externa +session_key = sessão técnica +``` + +Mas cada tool pode esperar nomes diferentes: + +```text +customer_id +cpf +msisdn +clientCode +contract_id +invoice_id +order_id +``` + +O `mcp_parameter_mapping.yaml` faz essa tradução sem obrigar o agente a conhecer os nomes internos de cada MCP. + +### 13.2. Exemplo + +Edite: + +```text +config/mcp_parameter_mapping.yaml +``` + +```yaml +mcp_parameter_mapping: + defaults: + use_mock: true + tools: + consultar_titulo_financeiro: + map: + customer_key: customer_id + contract_key: contract_id + interaction_key: interaction_id + session_key: session_id + consultar_pagamentos_financeiro: + map: + customer_key: customer_id + session_key: session_id +``` + +Interpretação: + +```text +customer_key -> chave canônica no framework +customer_id -> parâmetro esperado pela tool MCP +``` + +### 13.3. Como validar o mapeamento + +Se a tool recebe parâmetro errado, investigue nesta ordem: + +```text +payload enviado ao /gateway/message +config/identity.yaml +business_context resolvido +config/mcp_parameter_mapping.yaml +args_schema da tool +assinatura real no MCP Server +``` + +--- + +## 14. Configurando identidade de negócio + +### 14.1. Antes do YAML: o que é identidade de negócio? + +Identidade de negócio é a normalização das chaves que representam o cliente, contrato, pedido, protocolo, sessão ou interação. + +Sem essa camada, cada canal envia um nome diferente e cada tool espera outro nome. O resultado é erro de parâmetro, tool sem dado obrigatório ou consulta ao cliente errado. + +O `identity.yaml` responde: + +```text +De onde posso extrair customer_key? +De onde posso extrair contract_key? +De onde posso extrair interaction_key? +De onde posso extrair session_key? +Quais chaves são obrigatórias? +``` + +### 14.2. Exemplo + +Edite: + +```text +config/identity.yaml +``` + +```yaml +identity: + version: "2" + required: + - session_key + keys: + customer_key: + description: Cliente canônico. + sources: + - business_context.customer_key + - context.business_context.customer_key + - context.session.metadata.customer_key + - customer_key + - customer_id + - cpf + - cnpj + - user_id + contract_key: + description: Contrato, pedido, fatura ou título principal. + sources: + - business_context.contract_key + - context.business_context.contract_key + - context.session.metadata.contract_key + - contract_key + - contract_id + - invoice_id + - order_id + interaction_key: + description: Chave externa da interação. + sources: + - business_context.interaction_key + - context.business_context.interaction_key + - context.session.metadata.interaction_key + - interaction_key + - call_id + - message_id + - protocol_id + session_key: + description: Sessão técnica estável. + sources: + - business_context.session_key + - context.business_context.session_key + - context.session.backend_session_id + - context.session.global_session_id + - context.session.metadata.session_key + - session_key + - conversation_key + - session_id +``` + +### 14.3. Como pensar sobre identidade + +Use o mínimo necessário. Não torne tudo obrigatório. Para uma pergunta genérica, talvez só `session_key` seja suficiente. Para consultar um título financeiro, talvez `customer_key` e `contract_key` sejam obrigatórios. + +A identidade resolvida aparece em `business_context` dentro do `state` e é usada pelo `MCP Tool Router`. + +### 14.4. Relação entre SessionContext e BusinessContext + +Quando o Agent Gateway está presente, ele pode criar ou transportar dados de sessão. Esses dados são importantes, mas não substituem a identidade de negócio. + +```text +SessionContext responde: + Quem está falando? + Por qual canal? + Qual sessão global está ativa? + Qual backend está atendendo? + Qual foi a razão da última decisão de rota? + +BusinessContext responde: + Qual cliente deve ser consultado? + Qual contrato/fatura/pedido está em discussão? + Qual protocolo/chamado/interação identifica o caso? + Qual chave deve ser enviada para a tool MCP? +``` + +Regra prática: + +```text +Use session para continuidade, rastreabilidade e canal. +Use business_context para consultar sistemas, chamar MCP e tomar decisão de negócio. +Use tool_arguments quando parâmetros já vierem explicitamente preparados. +``` + +Exemplo de erro comum: + +```text +Usar session.user_id como customer_key sem validar identity.yaml. +``` + +O correto é deixar o `IdentityResolver` transformar `user_id`, `cpf`, `msisdn`, `customer_id` ou outro identificador em uma chave canônica como `customer_key`. + +--- + +## 15. Implementando ou conectando um MCP Server + +### 15.1. Antes do código: qual é o papel do MCP Server? + +O MCP Server é onde fica a integração com sistemas externos ou mocks de domínio. Ele permite que o agente use uma tool sem conhecer implementação técnica. + +O backend sabe chamar: + +```text +consultar_titulo_financeiro(customer_id, contract_id) +``` + +Mas não sabe, nem deveria saber, se essa consulta usa: + +```text +REST +SOAP +banco Oracle +arquivo mock +serviço legado +fila +sistema interno +``` + +### 15.2. Contrato conceitual das tools + +Exemplo conceitual: + +```python +async def consultar_titulo_financeiro(customer_id: str, contract_id: str, session_id: str | None = None): + return { + "customer_id": customer_id, + "contract_id": contract_id, + "status": "ABERTO", + "valor": 129.90, + "vencimento": "2026-06-20", + } + + +async def consultar_pagamentos_financeiro(customer_id: str, session_id: str | None = None): + return { + "customer_id": customer_id, + "pagamentos": [ + {"data": "2026-06-01", "valor": 129.90, "status": "COMPENSADO"} + ], + } +``` + +### 15.3. Critério para mock versus real + +Use mock quando: + +```text +o sistema real não está disponível +você está testando roteamento e contrato +você quer validar frontend/backend sem depender de VPN +você quer montar testes automatizados determinísticos +``` + +Use integração real quando: + +```text +o contrato já foi validado +os parâmetros estão corretos +o timeout e fallback foram definidos +há observabilidade para sucesso e falha +há dados seguros para teste +``` + +Para desenvolvimento, você pode usar `use_mock: true` no `mcp_parameter_mapping.yaml` ou implementar um MCP Server local com respostas simuladas. + +--- + +## 16. IC, NOC e GRL no novo agente + +### 16.1. Antes dos eventos: por que eles existem? + +IC, NOC e GRL não são logs comuns. Eles existem para rastrear a execução de forma corporativa. + +```text +IC = evento de negócio ou jornada do agente +NOC = evento operacional, erro, indisponibilidade, timeout ou degradação +GRL = evento de governança, guardrail, bloqueio, revisão ou sanitização +``` + +Use `logger.info()` para diagnóstico simples. Use IC/NOC/GRL quando o evento precisa aparecer em auditoria, observabilidade ou análise operacional. + +### 16.2. IC — eventos de negócio + +Use ICs dentro do agente para registrar passos relevantes da jornada. + +Exemplo: + +```python +await self._emit_ic( + "IC.FINANCEIRO_AGENT_STARTED", + state, + {"business_component": "financeiro"}, + component="agent.financeiro.start", +) +``` + +Sugestão mínima por agente: + +```text +IC._AGENT_STARTED +IC._MCP_CONTEXT_COLLECTED +IC._RAG_CONTEXT_RETRIEVED +IC._AGENT_COMPLETED +IC._BUSINESS_DECISION +IC._ACTION_REQUESTED +IC._ACTION_COMPLETED +``` + +### 16.3. NOC — eventos operacionais + +NOC deve ser usado para saúde técnica, indisponibilidade, erro, timeout, fallback e degradação. + +Exemplo: + +```python +await self.observer.emit_noc( + "NOC.FINANCEIRO_TOOL_TIMEOUT", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool": "consultar_titulo_financeiro", + }, + component="agent.financeiro.tool", +) +``` + +### 16.4. GRL — guardrails + +A maior parte dos GRLs já é emitida pelo workflow em: + +```text +input_guardrails +output_supervisor +output_guardrails +``` + +Só implemente GRL dentro do agente quando houver uma validação de domínio específica que não caiba nos guardrails globais. + +### 16.5. Quando não criar evento novo + +Não crie IC/NOC/GRL para cada linha de código. Crie eventos para decisões importantes: + +```text +entrada validada +contexto MCP coletado +decisão de negócio tomada +ação externa solicitada +ação externa concluída +fallback técnico acionado +resposta bloqueada ou revisada +workflow concluído +``` + +--- + +## 17. Build e execução local + +### 17.1. Antes dos comandos: o que significa subir o backend? + +Subir o backend significa iniciar a API que recebe mensagens, normaliza canal, resolve identidade, abre sessão, executa o workflow e devolve resposta. + +Ele pode subir mesmo sem MCP real, desde que a configuração esteja em mock ou que as tools não sejam obrigatórias para o teste. + +### 17.2. Rodar backend local + +Dentro de `agent_template_backend`: + +```bash +source .venv/bin/activate +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +Windows PowerShell: + +```powershell +.\.venv\Scripts\Activate.ps1 +uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload +``` + +### 17.3. Validações imediatas + +Verifique saúde: + +```bash +curl http://localhost:8000/health +``` + +Listar agentes: + +```bash +curl http://localhost:8000/agents +``` + +Listar tools MCP conhecidas: + +```bash +curl http://localhost:8000/debug/mcp/tools +``` + +### 17.4. Como interpretar o resultado + +```text +/health ok → API subiu. +/agents lista → agents.yaml foi carregado. +/debug/mcp/tools → tools.yaml e mcp_servers.yaml foram carregados. +``` + +Se `/health` funciona mas `/agents` não lista o agente, o problema provavelmente está em `config/agents.yaml`. Se `/debug/mcp/tools` não mostra a tool, o problema provavelmente está em `tools.yaml` ou `mcp_servers.yaml`. + +--- + +## 18. Subindo MCP Servers + +### 18.1. Antes dos comandos: quando preciso subir MCP? + +Você precisa subir MCP quando a intent escolhida usa `mcp_tools` e o agente depende dessas tools para responder. + +Não precisa subir MCP para testar apenas: + +```text +health check +registro de agentes +roteamento básico +mock LLM sem tools +fluxo conversacional simples sem consulta externa +``` + +### 18.2. Subir MCP Server local + +Se os MCP Servers forem processos Python separados, suba cada um em uma porta distinta. + +Exemplo: + +```bash +cd ../mcp_servers/financeiro_mcp_server +source .venv/bin/activate +uvicorn main:app --host 0.0.0.0 --port 8300 --reload +``` + +Depois confirme que o endpoint configurado em `config/mcp_servers.yaml` está correto: + +```yaml +servers: + financeiro: + endpoint: http://localhost:8300/mcp +``` + +### 18.3. Testar tool pelo backend + +Teste pelo backend, não diretamente pelo MCP. Assim você valida o caminho completo: + +```text +backend → MCP Tool Router → MCP Server → resposta +``` + +```bash +curl -X POST http://localhost:8000/debug/mcp/call/consultar_titulo_financeiro \ + -H "Content-Type: application/json" \ + -d '{ + "business_context": { + "customer_key": "12345", + "contract_key": "ABC-999", + "session_key": "sessao-teste" + }, + "original_context": { + "session_id": "sessao-teste" + } + }' +``` + +### 18.4. Como interpretar erros MCP + +```text +Tool não encontrada → tools.yaml ou nome da tool errado. +Servidor não encontrado → mcp_servers.yaml não tem o mcp_server indicado pela tool. +Connection refused → MCP Server não está rodando ou porta errada. +Parâmetro obrigatório ausente → identity.yaml ou mcp_parameter_mapping.yaml incorreto. +Timeout → MCP lento, endpoint errado, VPN, DNS ou sistema real indisponível. +``` + +--- + +## 19. Build com Docker + +O Dockerfile do template espera copiar `agent_framework` e `agent_template_backend`. Portanto, rode o build a partir do diretório pai que contém ambos. + +Estrutura esperada: + +```text +workspace/ +├── agent_framework/ +└── agent_template_backend/ +``` + +Build: + +```bash +cd workspace +docker build -t agent-template-backend:local -f agent_template_backend/Dockerfile . +``` + +Run: + +```bash +docker run --rm -p 8000:8000 \ + --env-file agent_template_backend/.env \ + agent-template-backend:local +``` + +Health check: + +```bash +curl http://localhost:8000/health +``` + +--- + +## 20. Docker Compose sugerido + +Crie um `docker-compose.yaml` no diretório pai, se quiser subir backend, Redis, Langfuse e MCP Servers juntos. + +Exemplo simplificado: + +```yaml +services: + backend: + build: + context: . + dockerfile: agent_template_backend/Dockerfile + env_file: + - agent_template_backend/.env + ports: + - "8000:8000" + depends_on: + - redis + - financeiro-mcp + + redis: + image: redis:7 + ports: + - "6379:6379" + + financeiro-mcp: + build: + context: ./mcp_servers/financeiro_mcp_server + ports: + - "8300:8300" +``` + +Quando estiver em Docker, use `config/mcp_servers.docker.yaml` e ajuste o `.env`: + +```env +MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.docker.yaml +``` + +--- + +## 21. Testando o agente pelo Gateway + +### 21.1. Teste simples + +```bash +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "teste-financeiro-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-001" + } + }' +``` + +A resposta deve conter metadados como: + +```json +{ + "channel": "web", + "session_id": "default:financeiro_agent:teste-financeiro-001", + "text": "...", + "metadata": { + "route": "financeiro_agent", + "intent": "financeiro_pagamentos", + "mcp_results": [], + "business_context": { + "customer_key": "12345", + "contract_key": "ABC-999" + } + } +} +``` + +### 21.2. Teste de roteamento sem fixar `agent_id` + +```bash +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "tenant_id": "default", + "payload": { + "text": "Meu pagamento ainda não foi baixado", + "session_id": "teste-router-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999" + } + }' +``` + +### 21.3. Teste de SSE + +Enviar mensagem com SSE: + +```bash +curl -X POST http://localhost:8000/gateway/message/sse \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Preciso da segunda via do boleto", + "session_id": "teste-sse-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999" + } + }' +``` + +Abrir stream: + +```bash +curl -N http://localhost:8000/gateway/events/default:financeiro_agent:teste-sse-001 +``` + +Eventos esperados: + +```text +connected +flow.start +session.upserted +message.received +workflow.started +workflow.completed +message.responded +flow.end +``` + +--- + +## 22. Testando debug endpoints + +### 22.1. Roteamento + +```bash +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Quero consultar meu pagamento", + "context": { + "agent_id": "financeiro_agent", + "tenant_id": "default" + } + }' +``` + +### 22.2. Identidade + +```bash +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "teste-id-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-001" + }' +``` + +### 22.3. Mensagens da sessão + +```bash +curl http://localhost:8000/sessions/default:financeiro_agent:teste-financeiro-001/messages +``` + +### 22.4. Checkpoint + +```bash +curl http://localhost:8000/sessions/default:financeiro_agent:teste-financeiro-001/checkpoint +``` + +### 22.5. Uso/custo + +```bash +curl http://localhost:8000/debug/usage +``` + +--- + +## 23. Checklist de validação funcional + +Use este checklist antes de considerar o agente pronto. + +### 23.1. Configuração + +- [ ] `.env` sem credenciais reais versionadas. +- [ ] `LLM_PROVIDER` correto. +- [ ] `ROUTING_MODE` definido: `router` ou `supervisor`. +- [ ] `ENABLE_MCP_TOOLS` ajustado conforme necessidade. +- [ ] `MCP_SERVERS_CONFIG_PATH` aponta para o YAML correto. +- [ ] `IDENTITY_CONFIG_PATH` aponta para `config/identity.yaml`. +- [ ] Persistência local ou Autonomous configurada. + +### 23.2. Agente + +- [ ] Arquivo criado em `app/agents/.py`. +- [ ] Classe implementa `async def run(self, state)`. +- [ ] Agente herda `AgentRuntimeMixin`. +- [ ] Agente usa `get_runtime_context()` ou padrão equivalente para ler `state/context/session/business_context`. +- [ ] Agente usa `normalize_tools_by_intent()` quando precisa de fallback de tools por intent. +- [ ] Agente usa `build_tool_arguments()` ou `execute_tools_for_intent()` quando precisa de aliases/política de tools. +- [ ] Tools de ação em `tools.yaml` possuem `tool_type`, `requires` e, quando necessário, `confirmation_required`. +- [ ] Dev entende que `AgentRuntimeMixin` é infraestrutura compartilhada, não regra de negócio. +- [ ] Agente usa `_emit_ic()`, `_emit_noc()` ou `_emit_grl()` em vez de emitir observabilidade em formato próprio. +- [ ] Agente usa `_collect_mcp_context()` para consultas simples às tools declaradas em `routing.yaml`. +- [ ] Agente usa `_retrieve_rag_context()` quando precisa de contexto documental. +- [ ] Agente usa `_invoke_llm_cached()` para chamada LLM com cache e telemetria. +- [ ] Dev entende que `messages` é o contrato conversacional enviado ao LLM, não a memória persistente. +- [ ] `messages` separa regras permanentes no `system` e pedido/evidências no `user`. +- [ ] `messages` inclui apenas campos necessários de `session`, `business_context`, MCP e RAG. +- [ ] Agente não envia `state` completo, objetos enormes ou dados sensíveis desnecessários ao LLM. +- [ ] Agente deixa claro no prompt quando MCP/RAG falharam, para evitar resposta inventada. +- [ ] Agente não chama REST, banco, SOAP ou serviço externo diretamente quando isso deveria estar atrás de MCP. +- [ ] Agente separa `context`, `session`, `business_context` e `tool_arguments` antes de tomar decisões. +- [ ] Agente usa `business_context` para decisões de negócio e `session` para continuidade/rastreabilidade. +- [ ] Prompts específicos aplicam `apply_agent_profile_prompt()`. +- [ ] Tools são chamadas via `_collect_mcp_context()`. +- [ ] RAG é chamado via `_retrieve_rag_context()`, se aplicável. +- [ ] LLM é chamado via `_invoke_llm_cached()`. +- [ ] Retorno contém `answer`, `next_state`, `mcp_results` e, se aplicável, `rag`. + +### 23.3. Workflow + +- [ ] Agente importado em `agent_graph.py`. +- [ ] Agente instanciado no `__init__`. +- [ ] Nó adicionado no `StateGraph`. +- [ ] Rota adicionada em `add_conditional_edges`. +- [ ] Edge criada para `output_supervisor`. +- [ ] Handler adicionado no modo supervisor, se necessário. + +### 23.4. Roteamento + +- [ ] Intent adicionada em `config/routing.yaml`. +- [ ] Keywords suficientes. +- [ ] Examples coerentes. +- [ ] `agent` da intent bate com o nome do nó do workflow. +- [ ] `mcp_tools` da intent existem em `config/tools.yaml`. + +### 23.5. MCP + +- [ ] Tool declarada em `config/tools.yaml`. +- [ ] MCP Server declarado em `config/mcp_servers.yaml`. +- [ ] Mapeamento declarado em `config/mcp_parameter_mapping.yaml`. +- [ ] Tool testada via `/debug/mcp/call/{tool_name}`. +- [ ] Timeout e fallback definidos. + +### 23.6. Observabilidade + +- [ ] ICs de início e fim emitidos. +- [ ] ICs de coleta MCP/RAG emitidos quando aplicável. +- [ ] NOCs emitidos em erros técnicos relevantes. +- [ ] GRLs globais aparecem em input/output. +- [ ] Langfuse ou outro provider recebe traces, se habilitado. + +### 23.7. Testes + +- [ ] `/health` retorna `status=ok`. +- [ ] `/agents` lista o agente novo. +- [ ] `/debug/route` escolhe o agente correto. +- [ ] `/debug/identity` resolve as chaves esperadas. +- [ ] `/gateway/message` retorna resposta correta. +- [ ] `/gateway/message/sse` publica eventos. +- [ ] `/sessions/{session_id}/messages` mostra histórico. +- [ ] `/sessions/{session_id}/checkpoint` mostra checkpoint. + +--- + +## 24. Boas práticas de customização + +### Faça + +- Coloque regra de negócio no agente, não no framework. +- Use MCP para acesso a sistemas externos. +- Use `RuntimeContext`, `build_tool_arguments()` e `execute_tools_for_intent()` antes de criar helpers locais duplicados no agente. +- Use `identity.yaml` para normalizar chaves de negócio. +- Use `mcp_parameter_mapping.yaml` para adaptar nomes de parâmetros. +- Use IC para eventos de negócio. +- Use NOC para falhas técnicas. +- Use GRL para decisões de segurança/validação. +- Monte `messages` com separação clara entre instrução, pedido, evidência MCP, contexto RAG e formato de saída. +- Mantenha prompts por agente em `config/agents//prompt_policy.yaml`. +- Mantenha guardrails e judges isolados quando o agente tiver regras próprias. + +### Evite + +- Criar outro workflow fora de `AgentWorkflow` sem necessidade. +- Chamar REST/DB direto dentro do agente quando a chamada deveria ser tool MCP. +- Criar checkpointer próprio. +- Criar memória paralela fora do framework. +- Emitir telemetria em formato incompatível com `AgentObserver`. +- Colocar regra específica de um agente dentro do framework. +- Misturar histórico de agentes diferentes na mesma sessão. +- Enviar o `state` inteiro ou dumps grandes de tools/RAG diretamente dentro de `messages`. +- Colocar regras críticas apenas no `user` prompt quando deveriam estar no `system`. + +--- + +## 25. Troubleshooting + +### 25.1. `/gateway/message` retorna rota errada + +Verifique: + +```bash +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{"text":"sua frase de teste","context":{"agent_id":"financeiro_agent"}}' +``` + +Depois revise: + +```text +config/routing.yaml +keywords +examples +priority +ROUTING_MODE +ENABLE_LLM_ROUTER +``` + +### 25.2. Tool MCP não é chamada + +Verifique: + +```text +A intent em routing.yaml possui mcp_tools. +A tool existe em tools.yaml. +O MCP Server está em mcp_servers.yaml. +ENABLE_MCP_TOOLS=true. +O mapeamento existe em mcp_parameter_mapping.yaml. +A identidade tem as chaves necessárias. +``` + +### 25.3. Tool recebe parâmetro errado + +Revise: + +```text +config/identity.yaml +config/mcp_parameter_mapping.yaml +payload enviado ao /gateway/message +``` + +Use: + +```bash +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{"session_id":"s1","customer_id":"123","contract_id":"C1"}' +``` + +### 25.4. SSE dá MIME type incorreto + +O endpoint correto é: + +```text +GET /gateway/events/{session_id} +``` + +O `session_id` precisa ser a chave canônica completa retornada pelo gateway: + +```text +tenant_id:agent_id:session_id_original +``` + +Exemplo: + +```text +default:financeiro_agent:teste-sse-001 +``` + +### 25.5. Langfuse não mostra traces + +Verifique: + +```env +ENABLE_LANGFUSE=true +LANGFUSE_PUBLIC_KEY= +LANGFUSE_SECRET_KEY= +LANGFUSE_HOST=http://localhost:3005 +``` + +E confira: + +```bash +curl http://localhost:8000/health +curl http://localhost:8000/debug/env +``` + +### 25.6. Banco Autonomous não conecta + +Para desenvolvimento, simplifique primeiro: + +```env +SESSION_REPOSITORY_PROVIDER=memory +MEMORY_REPOSITORY_PROVIDER=memory +CHECKPOINT_REPOSITORY_PROVIDER=memory +USAGE_REPOSITORY_PROVIDER=memory +``` + +Depois volte para `autonomous` quando wallet, DSN e variáveis estiverem corretos. + +--- + + +### 25.7. LLM responde inventando ou ignorando evidências + +Quando o LLM inventa dados, confirma uma ação inexistente ou ignora uma tool, nem sempre o problema está no modelo. Muitas vezes o problema está em como `messages` foi montado. + +Verifique: + +```text +O system prompt proíbe claramente inventar dados? +O user prompt separa evidências MCP de instruções? +A falha da tool foi informada explicitamente ao LLM? +O agente enviou um dump confuso de mcp_results em vez de um resumo útil? +O RAG trouxe documentos relevantes ou ruído? +O prompt pediu formato de resposta claro? +Há histórico duplicado confundindo a resposta? +``` + +Exemplo de correção: + +```text +Ruim: + Responda sobre o pagamento do cliente usando os dados abaixo: [...] + +Melhor: + A tool consultar_pagamentos_financeiro retornou ok=false. + Não confirme pagamento. + Informe que a evidência de pagamento não foi encontrada. +``` + +Em ambiente de desenvolvimento, registre uma versão sanitizada de `messages` para revisar o que realmente chegou ao LLM. Nunca registre prompts brutos com CPF, token, credencial, dados sensíveis ou payloads grandes de sistemas externos. + +## 26. Modelo mínimo de entrega de um novo agente + +Ao finalizar uma implementação, a entrega mínima deve conter: + +```text +app/agents/.py +config/agents.yaml +config/routing.yaml +config/tools.yaml +config/mcp_servers.yaml +config/mcp_parameter_mapping.yaml +config/identity.yaml +config/agents//prompt_policy.yaml +config/agents//guardrails.yaml +config/agents//judges.yaml +app/workflows/agent_graph.py +app/state.py, se necessário +.env.example ou documentação de variáveis +README.md com testes curl +``` + +--- + +## 27. Exemplo de teste completo + +```bash +# 1. Health +curl http://localhost:8000/health + +# 2. Agentes +curl http://localhost:8000/agents + +# 3. Tools MCP +curl http://localhost:8000/debug/mcp/tools + +# 4. Roteamento +curl -X POST http://localhost:8000/debug/route \ + -H "Content-Type: application/json" \ + -d '{ + "text": "Quero consultar meu pagamento", + "context": {"agent_id": "financeiro_agent", "tenant_id": "default"} + }' + +# 5. Identidade +curl -X POST http://localhost:8000/debug/identity \ + -H "Content-Type: application/json" \ + -d '{ + "session_id": "teste-final-001", + "customer_id": "12345", + "contract_id": "ABC-999" + }' + +# 6. Mensagem real +curl -X POST http://localhost:8000/gateway/message \ + -H "Content-Type: application/json" \ + -d '{ + "channel": "web", + "agent_id": "financeiro_agent", + "tenant_id": "default", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "teste-final-001", + "user_id": "user-001", + "customer_id": "12345", + "contract_id": "ABC-999", + "message_id": "msg-final-001" + } + }' + +# 7. Histórico +curl http://localhost:8000/sessions/default:financeiro_agent:teste-final-001/messages + +# 8. Checkpoint +curl http://localhost:8000/sessions/default:financeiro_agent:teste-final-001/checkpoint +``` + +--- + +## 28. Agent Gateway / Global Supervisor + +Este capítulo é uma tratativa à parte. Em uma arquitetura com vários agentes, não basta saber construir um backend de agente isolado. Em algum momento o frontend recebe uma mensagem do usuário e precisa decidir **qual backend de agente deve tratar aquela conversa**. + +Essa decisão não deve ficar espalhada no frontend, nem duplicada dentro de cada agente. Para isso existe o **Agent Gateway**, também chamado aqui de **Global Supervisor**. + +### 28.1. Antes do código: qual problema o Agent Gateway resolve? + +Imagine que a empresa tenha três backends independentes: + +```text +Backend Contas + resolve fatura, pagamento, consumo, segunda via, contestação + +Backend Ofertas + resolve planos, contratação, upgrade, retenção, desconto + +Backend Suporte + resolve internet lenta, sinal, rede, modem, falha técnica +``` + +Sem um gateway global, o frontend teria que saber regras como: + +```text +Se a mensagem tem "fatura", chamar Contas. +Se a mensagem tem "plano", chamar Ofertas. +Se a mensagem tem "internet lenta", chamar Suporte. +``` + +Isso parece simples no começo, mas vira problema quando: + +- surgem muitos agentes; +- uma conversa começa em Contas e depois muda para Ofertas; +- uma mensagem é ambígua, como “quero cancelar”; +- cada canal, Web, WhatsApp e Voz, começa a implementar sua própria regra; +- o desenvolvedor precisa manter roteamento, sessão e handoff em vários lugares. + +O **Agent Gateway** centraliza essa decisão. + +Ele recebe a mensagem normalizada do canal, descobre o backend correto e encaminha a requisição para o backend escolhido. + +```text +Usuário + ↓ +Frontend / Canal + ↓ +Agent Gateway / Global Supervisor + ↓ +Backend Contas | Backend Ofertas | Backend Suporte | Outros backends +``` + +O Gateway **não substitui o agente**. Ele não deve conter regra de negócio de fatura, oferta ou suporte. Ele apenas decide **quem deve receber a mensagem**. + +### 28.2. Diferença entre Supervisor do agente e Global Supervisor + +Dentro de um backend de agente, você pode ter um supervisor local. Esse supervisor decide entre caminhos internos do próprio agente. + +Exemplo dentro do agente de Contas: + +```text +Mensagem: "Minha fatura veio alta" + +Supervisor local do Backend Contas decide: + - explicar fatura + - consultar pagamentos + - abrir contestação + - chamar humano +``` + +O **Global Supervisor** decide em um nível acima: + +```text +Mensagem: "Minha internet está lenta" + +Global Supervisor decide: + - isso não é Contas + - isso deve ir para Suporte +``` + +A separação correta é: + +```text +Global Supervisor / Agent Gateway + decide o backend + +Supervisor local do backend + decide o fluxo interno do agente + +Agente especializado + executa a lógica de negócio +``` + +Essa separação evita que o framework ou o gateway fiquem contaminados com detalhes específicos de um domínio. + +### 28.3. O que pertence ao Agent Gateway + +O Gateway deve cuidar de responsabilidades transversais entre backends: + +```text +agent_gateway/ + app/main.py + expõe /gateway/message, /gateway/events/{session_id}, /debug/route, + /backends, /backends/health e /health + + app/settings.py + lê variáveis de ambiente do gateway global + + config/backends.yaml + declara quais backends existem, suas URLs, domínios, keywords e prioridade + + .env.example + documenta o modo de roteamento, TTL de sessão, timeout e provider LLM +``` + +O Gateway pode usar motores do framework para: + +- roteamento global; +- sessão global; +- client HTTP para backends; +- supervisor LLM; +- observabilidade; +- publicação de eventos; +- proxy SSE. + +No arquivo `agent_gateway/app/main.py`, o gateway usa componentes do framework como: + +```python +from agent_framework.global_supervisor import ( + BackendClient, + BackendRegistry, + GlobalRouteRequest, + GlobalSupervisorRouter, + InMemoryGlobalSessionStore, +) +``` + +Isso significa que o gateway não está criando um mecanismo paralelo de roteamento. Ele está usando uma camada própria do framework para governar múltiplos backends. + +### 28.4. O que não pertence ao Agent Gateway + +O Gateway não deve implementar regras específicas como: + +```text +consultar_fatura +consultar_pagamentos +abrir_contestacao +consultar_imdb +buscar_speech_analytics +abrir_sr_siebel +calcular_pro_rata +resolver_ean +``` + +Essas funcionalidades pertencem aos backends especializados ou aos MCP servers. + +Uma regra prática: + +```text +Se a lógica depende do negócio de um agente específico, ela não deve ficar no Gateway. +Se a lógica decide qual backend deve tratar a conversa, ela pode ficar no Gateway. +``` + +### 28.5. Estrutura do projeto `agent_gateway` + +A estrutura mínima observada no projeto é: + +```text +agent_gateway/ + app/ + main.py + settings.py + config/ + backends.yaml + docs/ + ARQUITETURA_GLOBAL_SUPERVISOR.md + .env.example + Dockerfile + README.md + requirements.txt +``` + +Cada arquivo tem uma responsabilidade clara: + +| Arquivo | Responsabilidade | +|---|---| +| `app/main.py` | expõe endpoints HTTP, chama o router global, encaminha mensagens aos backends e faz proxy SSE | +| `app/settings.py` | centraliza variáveis do gateway global | +| `config/backends.yaml` | cadastra backends disponíveis e regras de roteamento por domínio/keyword | +| `.env.example` | documenta como ligar/desligar modos de roteamento e providers | +| `Dockerfile` | empacota o gateway como serviço separado | +| `docs/ARQUITETURA_GLOBAL_SUPERVISOR.md` | explica a arquitetura conceitual | + +### 28.6. Como o desenvolvedor deve pensar antes de configurar o Gateway + +Antes de editar `config/backends.yaml`, o desenvolvedor deve responder quatro perguntas: + +```text +1. Quais backends de agente existem? +2. Qual é o domínio de responsabilidade de cada backend? +3. Quais palavras ou exemplos indicam cada domínio? +4. O que deve acontecer quando a mensagem for ambígua? +``` + +Exemplo: + +```text +Mensagem: "Quero cancelar" +``` + +Essa mensagem pode significar: + +```text +Cancelar serviço avulso → talvez Contas ou Ofertas +Cancelar plano inteiro → talvez Ofertas ou Retenção +Cancelar por problema rede → talvez Suporte +``` + +Nesse caso, o router por keyword pode não ser suficiente. O modo `hybrid` pode manter o backend ativo se a conversa já tiver contexto, ou chamar o supervisor LLM se houver conflito. + +### 28.7. Configurando os backends em `config/backends.yaml` + +O arquivo principal de configuração do Gateway é: + +```text +agent_gateway/config/backends.yaml +``` + +Exemplo: + +```yaml +default_backend: contas + +backends: + contas: + url: http://localhost:8001 + description: Backend responsável por faturas, contas, pagamentos, consumo, segunda via e contestação. + domains: [contas, fatura, pagamento, consumo, contestacao] + keywords: [fatura, conta, boleto, pagamento, consumo, segunda via, contestar, contestação, valor, cobrança] + examples: + - Quero consultar minha fatura + - Minha conta veio alta + - Preciso da segunda via do boleto + priority: 10 + default_agent_id: telecom_contas + + ofertas: + url: http://localhost:8002 + description: Backend responsável por ofertas, planos, upgrades, retenção e contratação. + domains: [ofertas, planos, retenção, contratação] + keywords: [oferta, plano, contratar, upgrade, desconto, promoção, pacote, retenção, cancelar serviço] + examples: + - Quero trocar meu plano + - Tem alguma oferta para mim? + - Quero cancelar um serviço + priority: 20 + default_agent_id: telecom_ofertas + + suporte: + url: http://localhost:8003 + description: Backend responsável por suporte técnico, falhas, rede, internet e atendimento operacional. + domains: [suporte, técnico, rede, internet] + keywords: [internet, sinal, rede, suporte, técnico, problema, falha, sem conexão, modem] + examples: + - Minha internet está lenta + - Estou sem sinal + - Preciso de suporte técnico + priority: 30 + default_agent_id: telecom_suporte +``` + +O desenvolvedor não deve preencher esse YAML como uma lista aleatória de palavras. Ele deve pensar em **famílias de intenção**. + +Exemplo correto: + +```text +Família: contas + assuntos: fatura, pagamento, consumo, segunda via, contestação +``` + +Exemplo ruim: + +```text +Família: qualquer coisa que tenha "valor" +``` + +A palavra “valor” pode aparecer em fatura, oferta, desconto, contestação ou cobrança. Palavras genéricas devem ser usadas com cuidado. + +### 28.8. Escolhendo o modo de roteamento global + +O `.env` do gateway possui a variável: + +```env +GLOBAL_ROUTING_MODE=hybrid +``` + +Os modos possíveis são: + +| Modo | Como decide | Quando usar | +|---|---|---| +| `router` | usa regras, keywords, domínios e prioridade | desenvolvimento local, testes determinísticos, ambientes com baixa ambiguidade | +| `supervisor` | usa LLM para escolher backend | domínios muito parecidos ou mensagens muito abertas | +| `hybrid` | mantém backend ativo, usa regra e chama LLM em conflito | recomendado para produção inicial | + +A decisão prática é: + +```text +Se você quer previsibilidade total, use router. +Se você quer interpretação semântica forte, use supervisor. +Se você quer equilíbrio entre contexto, regra e LLM, use hybrid. +``` + +Para a maioria dos projetos corporativos, comece com: + +```env +GLOBAL_ROUTING_MODE=hybrid +GLOBAL_KEEP_ACTIVE_BACKEND=true +GLOBAL_USE_SUPERVISOR_ON_CONFLICT=true +GLOBAL_MIN_ROUTER_CONFIDENCE=0.55 +``` + +### 28.9. Entendendo sessão global e sessão do backend + +O Gateway mantém uma sessão global, por exemplo: + +```text +global_session_id = s1 +``` + +O backend pode manter outra sessão interna, por exemplo: + +```text +backend_session_id = default:telecom_contas:s1 +``` + +O código do Gateway ajusta a resposta para manter os dois identificadores no `metadata`: + +```json +{ + "session_id": "s1", + "metadata": { + "global_session_id": "s1", + "backend_session_id": "default:telecom_contas:s1", + "selected_backend": "contas" + } +} +``` + +Essa separação é importante porque o usuário conversa com uma sessão global, mas cada backend pode precisar de sua própria chave interna para memória, checkpoint e histórico. + +### 28.9.1. Como o Gateway deve entregar sessão ao backend + +Para que o agente consiga entender de onde veio a conversa, o Gateway deve encaminhar a sessão dentro de `context.session` ou em uma estrutura equivalente normalizada pelo framework. + +Exemplo de payload conceitual que chega ao backend: + +```json +{ + "channel": "web", + "tenant_id": "default", + "agent_id": "financeiro_agent", + "payload": { + "text": "Quero consultar meu pagamento", + "session_id": "s1", + "customer_id": "12345" + }, + "context": { + "session": { + "global_session_id": "s1", + "backend_session_id": "default:financeiro_agent:s1", + "active_backend": "financeiro", + "channel": "web", + "tenant_id": "default", + "metadata": { + "selected_backend": "financeiro", + "route_confidence": 0.82 + } + }, + "business_context": { + "customer_key": "12345", + "session_key": "default:financeiro_agent:s1" + } + } +} +``` + +O desenvolvedor do agente deve entender que `context.session` não é “mais um lugar para buscar qualquer parâmetro”. Ele é o contrato de continuidade da conversa. Para chamadas MCP, prefira sempre `business_context` e `tool_arguments`. + +### 28.10. Subindo o Agent Gateway localmente + +Entre no diretório do gateway: + +```bash +cd agent_gateway +``` + +Copie o arquivo de ambiente: + +```bash +cp .env.example .env +``` + +Configure o `PYTHONPATH` para enxergar o framework: + +```bash +export PYTHONPATH=../agent_framework/src:. +``` + +Suba o serviço: + +```bash +uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload +``` + +Valide o health: + +```bash +curl http://localhost:8010/health +``` + +Resposta esperada: + +```json +{ + "status": "ok", + "app": "agent-gateway-global-supervisor", + "routing_mode": "hybrid", + "backends": ["contas", "ofertas", "suporte"], + "llm_provider": "mock" +} +``` + +Se esse endpoint não responder, o problema ainda está no gateway, não nos backends. + +### 28.11. Subindo os backends de agente + +O Gateway só roteia corretamente se os backends configurados em `backends.yaml` estiverem de pé. + +Exemplo local: + +```text +Gateway http://localhost:8010 +Contas http://localhost:8001 +Ofertas http://localhost:8002 +Suporte http://localhost:8003 +Frontend http://localhost:5173 +``` + +Cada backend precisa expor, no mínimo: + +```text +GET /health +POST /gateway/message +GET /gateway/events/{session_id} +``` + +O endpoint `/backends/health` do Gateway verifica a saúde dos backends: + +```bash +curl http://localhost:8010/backends/health +``` + +Use esse teste antes de culpar o roteamento. Se o backend está fora do ar, o Gateway pode até escolher corretamente, mas falhará no encaminhamento. + +### 28.12. Testando apenas a decisão de rota + +Antes de enviar uma mensagem real para o backend, teste a decisão: + +```bash +curl -X POST http://localhost:8010/debug/route \ + -H 'content-type: application/json' \ + -d '{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1" + } + }' +``` + +Resultado esperado: + +```json +{ + "backend_id": "contas", + "confidence": 0.8, + "reason": "Backend escolhido por regras: matches=['fatura']" +} +``` + +O desenvolvedor deve interpretar o resultado assim: + +```text +backend_id → para qual backend o gateway mandaria a mensagem +confidence → quão forte foi a decisão +reason → por que a decisão foi tomada +``` + +Se o backend escolhido estiver errado, ajuste `domains`, `keywords`, `examples`, `priority` ou o modo de roteamento. + +### 28.13. Enviando mensagem real pelo Gateway + +Depois que a decisão de rota estiver correta, envie a mensagem real: + +```bash +curl -X POST http://localhost:8010/gateway/message \ + -H 'content-type: application/json' \ + -d '{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1", + "msisdn": "11999999999" + } + }' +``` + +O Gateway fará: + +```text +1. Receber a mensagem. +2. Emitir IC.GLOBAL_GATEWAY_RECEIVED. +3. Criar uma GlobalRouteRequest. +4. Chamar GlobalSupervisorRouter. +5. Escolher o backend. +6. Emitir IC.GLOBAL_BACKEND_SELECTED. +7. Encaminhar para o /gateway/message do backend. +8. Guardar o active_backend da sessão. +9. Acrescentar metadados de rota na resposta. +10. Emitir IC.GLOBAL_GATEWAY_COMPLETED. +``` + +### 28.14. Handoff entre backends + +O handoff acontece quando um backend percebe que a conversa deve mudar de domínio. + +Exemplo: + +```text +Usuário começou em Contas: + "Minha fatura veio alta" + +Depois perguntou: + "Tem algum plano melhor para reduzir esse valor?" +``` + +O backend de Contas pode responder com metadata pedindo troca: + +```json +{ + "metadata": { + "handover_backend": "ofertas" + } +} +``` + +O Gateway detecta esse campo e chama automaticamente o novo backend. + +O desenvolvedor precisa entender que handoff não é erro. É uma transição controlada entre domínios. + +### 28.15. Proxy SSE pelo Gateway + +O Gateway também possui endpoint: + +```text +GET /gateway/events/{session_id} +``` + +Esse endpoint faz proxy do SSE do backend ativo. + +Fluxo: + +```text +Frontend abre EventSource no Gateway + ↓ +Gateway espera existir sessão global + ↓ +Gateway descobre active_backend + ↓ +Gateway monta URL SSE do backend + ↓ +Gateway repassa os eventos text/event-stream para o frontend +``` + +Teste: + +```bash +curl -N http://localhost:8010/gateway/events/s1 +``` + +Eventos esperados no início: + +```text +event: connected +data: {"session_id":"s1","component":"agent_gateway"} + +``` + +Depois que uma mensagem for enviada para `/gateway/message`, o Gateway deve emitir algo como: + +```text +event: backend.selected +data: {"session_id":"s1","backend_id":"contas","backend_session_id":"s1"} +``` + +Se aparecer erro de MIME type, o backend ativo provavelmente não está retornando `text/event-stream` em `/gateway/events/{session_id}`. + +### 28.16. IC e NOC do Agent Gateway + +O Gateway deve emitir eventos próprios, diferentes dos eventos internos dos agentes. + +Eventos encontrados no projeto: + +| Evento | Significado | +|---|---| +| `IC.GLOBAL_GATEWAY_RECEIVED` | Gateway recebeu mensagem do canal | +| `IC.GLOBAL_BACKEND_SELECTED` | Gateway escolheu um backend | +| `IC.GLOBAL_BACKEND_HANDOVER` | Houve troca de backend durante a conversa | +| `IC.GLOBAL_GATEWAY_COMPLETED` | Gateway concluiu o encaminhamento | +| `NOC.005` | falha operacional no Gateway ou na chamada ao backend | +| `NOC.006` | conclusão HTTP observada pelo middleware | + +Esses eventos não substituem os IC/NOC/GRL do backend. Eles complementam a visão ponta a ponta. + +Em uma rastreabilidade completa, você deve conseguir enxergar: + +```text +IC.GLOBAL_GATEWAY_RECEIVED +IC.GLOBAL_BACKEND_SELECTED +IC.BACKEND_WORKFLOW_STARTED +IC.TOOL_CALLED +GRL.INPUT_STARTED +GRL.OUTPUT_COMPLETED +IC.BACKEND_WORKFLOW_COMPLETED +IC.GLOBAL_GATEWAY_COMPLETED +``` + +### 28.17. Como integrar o frontend ao Agent Gateway + +O frontend não deve chamar diretamente cada backend de agente. + +Em vez disso, ele deve apontar para: + +```text +POST http://localhost:8010/gateway/message +GET http://localhost:8010/gateway/events/{session_id} +``` + +O frontend continua enviando uma mensagem normalizada: + +```json +{ + "channel": "web", + "payload": { + "text": "Minha fatura veio alta", + "session_id": "s1" + } +} +``` + +O frontend não precisa saber se a mensagem foi para Contas, Ofertas ou Suporte. Essa informação pode aparecer em `metadata.selected_backend`, mas não deve virar regra de negócio no frontend. + +### 28.18. Build do Gateway com Docker + +O Dockerfile do Gateway usa: + +```dockerfile +FROM python:3.12-slim +WORKDIR /app +COPY agent_framework /agent_framework +COPY agent_gateway /app +RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8010"] +``` + +Isso pressupõe que, no contexto de build, existam os diretórios: + +```text +agent_framework/ +agent_gateway/ +``` + +Build: + +```bash +docker build -t agent-gateway:local -f agent_gateway/Dockerfile . +``` + +Run: + +```bash +docker run --rm -p 8010:8010 \ + --env-file agent_gateway/.env \ + agent-gateway:local +``` + +### 28.19. Checklist de implementação do Agent Gateway + +Antes de considerar o Gateway pronto, valide: + +```text +[ ] /health responde. +[ ] /backends lista todos os backends esperados. +[ ] /backends/health consegue chamar cada backend. +[ ] /debug/route escolhe o backend correto para mensagens óbvias. +[ ] /debug/route explica o motivo da decisão. +[ ] /gateway/message encaminha para o backend escolhido. +[ ] response.metadata.selected_backend aparece na resposta. +[ ] response.metadata.global_route_decision aparece na resposta. +[ ] /debug/sessions mostra active_backend após primeira mensagem. +[ ] /gateway/events/{session_id} retorna text/event-stream. +[ ] handoff_backend funciona quando um backend solicita troca. +[ ] IC.GLOBAL_* aparece na observabilidade. +[ ] NOC.005 aparece em falhas reais de backend. +``` + +### 28.20. Erros comuns no Agent Gateway + +#### Erro 1: Gateway escolhe backend errado + +Causas comuns: + +```text +keywords genéricas demais +priority mal definida +examples insuficientes +GLOBAL_MIN_ROUTER_CONFIDENCE muito baixo +modo router usado para domínio ambíguo +``` + +Correção: + +```text +1. Teste /debug/route. +2. Leia o campo reason. +3. Ajuste domains, keywords e examples. +4. Se continuar ambíguo, use hybrid ou supervisor. +``` + +#### Erro 2: Gateway escolhe certo, mas retorna 502 + +Isso normalmente significa que o backend escolhido está fora do ar ou não expõe `/gateway/message`. + +Teste: + +```bash +curl http://localhost:8001/health +curl -X POST http://localhost:8001/gateway/message \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"teste","session_id":"s1"}}' +``` + +#### Erro 3: SSE retorna `application/json` em vez de `text/event-stream` + +O backend ativo precisa expor SSE corretamente. + +Teste direto no backend: + +```bash +curl -i -N http://localhost:8001/gateway/events/s1 +``` + +O header esperado é: + +```text +content-type: text/event-stream +``` + +#### Erro 4: Sessão global existe, mas o backend ativo não aparece + +Verifique: + +```bash +curl http://localhost:8010/debug/sessions +``` + +Depois envie uma mensagem por `/gateway/message`. O `active_backend` só é definido depois que o Gateway roteia uma mensagem com sucesso. + +### 28.21. Como explicar essa arquitetura para um novo desenvolvedor + +Uma forma simples de ensinar é: + +```text +O backend de agente sabe resolver um tipo de problema. +O Gateway sabe escolher qual backend deve resolver o problema. +O framework fornece os motores reutilizáveis para ambos. +``` + +Portanto, ao implementar um novo agente, o desenvolvedor deve fazer duas integrações: + +```text +1. Criar o backend especializado usando agent_template_backend. +2. Registrar esse backend no agent_gateway/config/backends.yaml. +``` + +Ele não deve alterar o frontend para cada novo agente. Também não deve colocar regra de negócio do novo agente dentro do Gateway. + + +--- + +## 29. Conclusão + +O `agent_template_backend` fornece a espinha dorsal corporativa para novos agentes. A implementação de um agente novo deve se limitar ao domínio: prompts, regras, tools, clients, schemas e decisões específicas. + +O padrão correto é: + +```text +Framework = motor reutilizável +Agente = customização de negócio +MCP = fronteira padronizada com sistemas externos +Config YAML = comportamento alterável sem mexer no motor +IC/NOC/GRL = rastreabilidade corporativa +``` + +Um desenvolvedor não deve apenas copiar arquivos. Ele deve entender que cada alteração representa uma decisão arquitetural: + +```text +Criar agente → define a lógica de domínio. +Registrar workflow → torna o agente executável pelo LangGraph. +Ajustar state → compartilha dados entre nós. +Configurar agents → declara o agente para o framework. +Configurar routing → ensina o framework quando chamar o agente. +Configurar tools → declara capacidades externas. +Configurar MCP → conecta tools a sistemas ou mocks. +Configurar identity→ normaliza chaves de negócio. +Emitir IC/NOC/GRL → torna a execução auditável. +Testar gateway → valida o fluxo real fim a fim. +``` + +Seguindo esse modelo, novos agentes podem ser criados com padronização, escalabilidade, rastreabilidade e manutenção mais simples. + + +## 30. Entrega final com Agent Gateway + +Ao final da implementação, a entrega recomendada deve conter quatro projetos ou diretórios claramente separados: + +```text +agent_framework/ + biblioteca reutilizável com motores de workflow, routing, guardrails, + judges, supervisor, memória, checkpoint, observabilidade e MCP tool router + +agent_template_backend/ + backend especializado de um agente, com domínio, prompts, tools, + state, workflow e configurações próprias + +agent_gateway/ + global supervisor que roteia conversas entre vários backends de agentes + +agent_frontend/ + interface Web, WhatsApp ou Voz que conversa com o Agent Gateway +``` + +A relação correta é: + +```text +Frontend + chama Agent Gateway + +Agent Gateway + escolhe o backend + +Backend do agente + executa o workflow especializado + +MCP Server + executa ou simula ferramentas de negócio + +Framework + fornece os motores reutilizáveis para gateway e backends +``` + +### 30.1. Sequência final de subida local + +Uma sequência local completa pode ser: + +```bash +# 1. Subir MCP do agente, se existir +cd mcp_servers/meu_agente_mcp +uvicorn app.main:app --host 0.0.0.0 --port 9001 --reload + +# 2. Subir backend do agente Contas +cd agent_template_backend +cp .env.example .env +uvicorn app.main:app --host 0.0.0.0 --port 8001 --reload + +# 3. Subir Agent Gateway +cd agent_gateway +cp .env.example .env +export PYTHONPATH=../agent_framework/src:. +uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload + +# 4. Subir frontend +cd agent_frontend +npm install +npm run dev +``` + +### 30.2. Sequência final de testes + +```bash +# Gateway vivo +curl http://localhost:8010/health + +# Backends registrados +curl http://localhost:8010/backends + +# Saúde dos backends +curl http://localhost:8010/backends/health + +# Decisão de rota +curl -X POST http://localhost:8010/debug/route \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"Minha fatura veio alta","session_id":"s1"}}' + +# Mensagem real ponta a ponta +curl -X POST http://localhost:8010/gateway/message \ + -H 'content-type: application/json' \ + -d '{"channel":"web","payload":{"text":"Minha fatura veio alta","session_id":"s1","msisdn":"11999999999"}}' + +# Sessões globais +curl http://localhost:8010/debug/sessions + +# SSE pelo Gateway +curl -N http://localhost:8010/gateway/events/s1 +``` + +### 30.3. Critério de aceite arquitetural + +A implementação está arquiteturalmente correta quando: + +```text +[ ] o frontend não conhece URLs individuais dos backends de agentes; +[ ] o Gateway não contém regra de negócio específica de fatura, oferta ou suporte; +[ ] cada backend continua independente; +[ ] cada backend usa os motores do framework; +[ ] o Gateway usa o GlobalSupervisorRouter do framework; +[ ] o roteamento global é observável; +[ ] cada troca de backend gera metadados e evento de handoff; +[ ] os MCP servers continuam plugáveis por backend/agente; +[ ] a sessão global e a sessão do backend são preservadas no metadata; +[ ] o desenvolvedor consegue testar rota antes de testar execução real. +``` + +Com esse desenho, adicionar um novo agente não exige reescrever o frontend nem copiar lógica entre backends. O desenvolvedor cria o backend especializado, registra no Agent Gateway e deixa o framework cuidar dos motores transversais. + +## Política read-only/transacional + +Este template inclui o arquivo opcional `config/tool_policies.yaml`. Use `operation_type: read_only` para consultas e `operation_type: transactional` com `require_confirmation: true` para ações que só podem executar após confirmação booleana explícita. Se o arquivo for removido ou não existir em um template antigo, os campos legados de `config/tools.yaml` continuam válidos. + +## Workflows transacionais determinísticos + +Além da execução direta de MCP tools, uma operação transacional pode usar um workflow LangGraph determinístico após `clarification` e confirmação explícita. Configure `execution.mode: workflow` em `config/tool_policies.yaml`, mantenha as definições versionadas em `workflows/` e implemente as actions do domínio no projeto do agente. O runtime genérico está em `agent_framework.workflows`. + +Consulte `libs/agent_framework/docs/TRANSACTIONAL_WORKFLOWS_PT.md` e o exemplo `workflows/devolucao_pedido.v1.yaml`. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README_ENTERPRISE_TEMPLATE.md new file mode 100644 index 0000000..cae516e --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/README_ENTERPRISE_TEMPLATE.md @@ -0,0 +1,54 @@ +# Agent Template Backend Enterprise + +Este folder é uma cópia completa do `agent_template_backend`, sem cortes de +arquitetura. Ele mantém workflow, router, output supervisor, guardrails, +analytics, observer, MCP, memória, checkpoints e configurações. + +A diferença é que a lógica de negócio dos agentes de exemplo foi removida da +execução e preservada comentada nos próprios arquivos: + +- `app/agents/billing_agent.py` +- `app/agents/product_agent.py` +- `app/agents/orders_agent.py` +- `app/agents/support_agent.py` + +## O que o desenvolvedor deve alterar + +1. Escolher ou criar um agente em `app/agents/`. +2. Implementar o método `run()`. +3. Ajustar prompts e tools, se necessário. +4. Emitir ICs de negócio relevantes para a jornada. +5. Manter NOC/GRL nos pontos operacionais e de guardrails. + +## O que já está integrado + +- `AgentObserver` +- `observer.emit_ic()` +- `observer.emit_noc()` +- `observer.emit_grl()` +- `AnalyticsPublisher` +- OCI Streaming +- GCP Pub/Sub +- OutputSupervisor +- GuardrailPipeline com suporte a execução paralela/fail-fast no framework +- MCP Tool Router +- LangGraph +- Memory +- Checkpoint +- Langfuse / OpenTelemetry + +## Exemplos adicionados + +Veja `app/examples/`: + +- `ic_examples.py` +- `noc_examples.py` +- `grl_examples.py` +- `mcp_examples.py` +- `observer_examples.py` + +## Convenção rápida + +- IC = evento de negócio / curadoria / informacional. +- NOC = evento operacional / saúde técnica. +- GRL = evento de guardrail / segurança / validação. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/__init__.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/README.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/README.md new file mode 100644 index 0000000..2917425 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/README.md @@ -0,0 +1,15 @@ +# Agentes do Template Backend Enterprise + +Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas +não executam lógica de negócio pronta. + +Cada agente mostra: + +- como emitir IC; +- como emitir NOC; +- como emitir GRL; +- como coletar MCP via `_collect_tool_context()`; +- como recuperar RAG via `_retrieve_rag_context()`; +- onde chamar LLM/cache. + +A implementação original do exemplo está comentada no fim de cada arquivo. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/billing_agent.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/billing_agent.py new file mode 100644 index 0000000..aa60099 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/billing_agent.py @@ -0,0 +1,129 @@ +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): + await self._emit_ic( + "IC.BILLING_AGENT_STARTED", + state, + {"business_component": "faturas"}, + component="agent.billing.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.BILLING_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.billing.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="BillingAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.BILLING_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.billing.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + 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) + result = { + "answer": f"[BillingAgent] {answer}", + "next_state": "BILLING_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.BILLING_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.billing.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/orders_agent.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/orders_agent.py new file mode 100644 index 0000000..f557bed --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/orders_agent.py @@ -0,0 +1,129 @@ +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): + await self._emit_ic( + "IC.ORDERS_AGENT_STARTED", + state, + {"business_component": "pedidos"}, + component="agent.orders.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.ORDERS_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.orders.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="OrdersAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.ORDERS_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.orders.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + 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) + result = { + "answer": f"[OrdersAgent] {answer}", + "next_state": "ORDER_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.ORDERS_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.orders.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/product_agent.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/product_agent.py new file mode 100644 index 0000000..34433f5 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/product_agent.py @@ -0,0 +1,129 @@ +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): + await self._emit_ic( + "IC.PRODUCT_AGENT_STARTED", + state, + {"business_component": "produtos"}, + component="agent.product.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.PRODUCT_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.product.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="ProductAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.PRODUCT_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.product.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + 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) + result = { + "answer": f"[ProductAgent] {answer}", + "next_state": "PRODUCT_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.PRODUCT_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.product.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/prompting.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/prompting.py new file mode 100644 index 0000000..255422b --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/prompting.py @@ -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}" diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/runtime.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/runtime.py new file mode 100644 index 0000000..7a1a9be --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/runtime.py @@ -0,0 +1,10 @@ +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 +from app.presentation import register_tool_renderers + +register_tool_renderers() + +__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"] diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/support_agent.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/support_agent.py new file mode 100644 index 0000000..b4f0244 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/agents/support_agent.py @@ -0,0 +1,129 @@ +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): + await self._emit_ic( + "IC.SUPPORT_AGENT_STARTED", + state, + {"business_component": "suporte"}, + component="agent.support.start", + ) + + tool_context = await self._collect_tool_context(state) + if tool_context: + await self._emit_ic( + "IC.SUPPORT_MCP_CONTEXT_COLLECTED", + state, + {"tool_result_count": len(tool_context)}, + component="agent.support.mcp", + ) + + state["mcp_results"] = tool_context + clarification_message = self.transaction_clarification_message(state) + if clarification_message: + return { + "answer": f"[{self.__class__.__name__}] {clarification_message}", + "next_state": state.get("next_state") or "COLLECTING_PARAMETERS", + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + + confirmation_message = self.transaction_confirmation_message(state) + if confirmation_message: + result = { + "answer": f"[{self.__class__.__name__}] {confirmation_message}", + "next_state": state.get("next_state"), + "mcp_results": tool_context, + **self.transaction_state_patch(state), + } + return result + + direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="SupportAgent") + if direct_answer: + return { + "answer": direct_answer, + "next_state": state.get("next_state") or "ACTIVE", + "mcp_results": tool_context, + "rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"}, + **self.transaction_state_patch(state), + } + + rag_context, rag_metadata = await self._retrieve_rag_context(state) + if rag_metadata.get("enabled"): + await self._emit_ic( + "IC.SUPPORT_RAG_CONTEXT_RETRIEVED", + state, + { + "document_count": rag_metadata.get("document_count"), + "graph_neighbors": rag_metadata.get("graph_neighbors"), + "latency_ms": rag_metadata.get("latency_ms"), + }, + component="agent.support.rag", + ) + + # Prepara ConversationSummaryMemory antes de montar o prompt. + # O build_messages() do framework injeta resumo + últimas mensagens quando habilitado. + 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) + result = { + "answer": f"[SupportAgent] {answer}", + "next_state": "SUPPORT_ACTIVE", + "mcp_results": tool_context, + "rag": rag_metadata, + "memory_context_metadata": state.get("memory_context_metadata"), + **self.transaction_state_patch(state), + } + + await self._emit_ic( + "IC.SUPPORT_AGENT_COMPLETED", + state, + { + "answer_chars": len(result.get("answer") or ""), + "has_mcp_results": bool(tool_context), + "rag_enabled": bool(rag_metadata.get("enabled")), + "memory_context": state.get("memory_context_metadata"), + }, + component="agent.support.completed", + ) + return result + + async def _collect_tool_context(self, state): + return await self._collect_mcp_context(state) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/__init__.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/__init__.py new file mode 100644 index 0000000..3f95e96 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/__init__.py @@ -0,0 +1 @@ +"""Exemplos de uso do template backend enterprise.""" diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/grl_examples.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/grl_examples.py new file mode 100644 index 0000000..8dadac8 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/grl_examples.py @@ -0,0 +1,37 @@ +"""Exemplos de GRL. + +GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos +pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão +manual apenas para validações customizadas do agente. +""" + +from typing import Any + + +async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None: + await observer.emit_grl( + "OBSERVE", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "rail_code": rail_code, + "reason": reason, + }, + component="examples.grl", + ) + + +async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None: + await observer.emit_grl( + "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "rail_code": rail_code, + "reason": reason, + "action": "block", + }, + component="examples.grl", + ) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/ic_examples.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/ic_examples.py new file mode 100644 index 0000000..f6daa57 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/ic_examples.py @@ -0,0 +1,34 @@ +"""Exemplos de IC - Item de Controle. + +ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria, +analytics, BigQuery ou qualquer publisher configurado no framework. +""" + +from typing import Any + + +async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None: + await observer.emit_ic( + "IC.FATURA_CONSULTADA", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "invoice_id": invoice_id, + }, + component="examples.ic", + ) + + +async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None: + await observer.emit_ic( + "IC.ACAO_CONCLUIDA", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "action_name": action_name, + "ok": ok, + }, + component="examples.ic", + ) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/mcp_examples.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/mcp_examples.py new file mode 100644 index 0000000..613f10c --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/mcp_examples.py @@ -0,0 +1,43 @@ +"""Exemplos de MCP + IC. + +O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o +padrão para chamadas explícitas ao tool_router quando necessário. +""" + +from typing import Any + + +async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any: + session_id = state.get("conversation_key") or state.get("session_id") + + await observer.emit_ic( + "IC.MCP_TOOL_CALLED", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool_name": tool_name, + }, + component="examples.mcp", + ) + + result = await tool_router.call( + tool_name, + payload, + business_context=(state.get("context") or {}).get("business_context") or {}, + original_context=state.get("context") or {}, + ) + + await observer.emit_ic( + "IC.TOOL_CALLED", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "tool_name": tool_name, + "ok": getattr(result, "ok", None), + }, + component="examples.mcp", + ) + + return result diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/noc_examples.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/noc_examples.py new file mode 100644 index 0000000..2b38a15 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/noc_examples.py @@ -0,0 +1,37 @@ +"""Exemplos de NOC. + +NOC representa telemetria operacional. O workflow do template já emite NOC.001, +NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode +emitir em pontos críticos. +""" + +from typing import Any + + +async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None: + await observer.emit_noc( + "002", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "apiUrl": api_url, + "statusCode": status_code, + "latencyMs": latency_ms, + }, + component="examples.noc", + ) + + +async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None: + await observer.emit_noc( + "003", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "resourceName": resource_name, + "latencyMs": latency_ms, + }, + component="examples.noc", + ) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/observer_examples.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/observer_examples.py new file mode 100644 index 0000000..926b553 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/examples/observer_examples.py @@ -0,0 +1,28 @@ +"""Resumo prático do Observer corporativo. + +Use este arquivo como cola rápida para IC, NOC e GRL. +""" + +from typing import Any + + +async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None: + session_id = state.get("conversation_key") or state.get("session_id") + + await observer.emit_ic( + "IC.EXEMPLO_NEGOCIO", + {"session_id": session_id, "agent_id": state.get("agent_id")}, + component="examples.observer", + ) + + await observer.emit_noc( + "EXEMPLO_OPERACIONAL", + {"session_id": session_id, "agent_id": state.get("agent_id")}, + component="examples.observer", + ) + + await observer.emit_grl( + "OBSERVE", + {"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"}, + component="examples.observer", + ) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/main.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/main.py new file mode 100644 index 0000000..86530a3 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/main.py @@ -0,0 +1,648 @@ +from __future__ import annotations + +import logging +from uuid import uuid4 +import time + +from fastapi import FastAPI, HTTPException, 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.channels.interruption import classify_processing_interruption, evaluate_interruption +from agent_framework.channels.transcription import fix_whole_utterance_transcription +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(input_mode=settings.FRAMEWORK_CHANNEL_INPUT_MODE) +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()]) +logger.info("Framework channel input mode: %s", gateway.input_mode) + +@app.middleware("http") +async def observability_context_middleware(request: Request, call_next): + clear_observability_context() + 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 + finally: + clear_observability_context() + + +class GatewayRequest(BaseModel): + channel: str = "web" + payload: dict + agent_id: str | None = None + tenant_id: str | None = None + + +def _metadata_value(payload: dict, key: str): + metadata = payload.get("metadata") + if isinstance(metadata, dict): + return metadata.get(key) + return None + + +def _extract_workflow_id(payload: dict) -> str | None: + return ( + payload.get("workflow_id") + or payload.get("workflowId") + or _metadata_value(payload, "workflow_id") + or _metadata_value(payload, "workflowId") + ) + + +def _format_root_span_name(template: str | None, values: dict) -> str: + template = template or "agent.gateway_message" + try: + return template.format(**{k: v or "unknown" for k, v in values.items()}) + except Exception: + logger.warning("LANGFUSE_ROOT_SPAN_NAME inválido: %s", template) + return "agent.gateway_message" + + +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: + try: + msg = await gateway.normalize(req.channel, req.payload) + except ValueError as exc: + raise HTTPException(status_code=422, detail=str(exc)) from exc + payload = req.payload or {} + identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg) + agent_session_id = identity.conversation_key() + message_id = payload.get("message_id") or str(uuid4()) + workflow_id = _extract_workflow_id(payload) + 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, + workflow_id=workflow_id, + ura_call_id=payload.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 sessions.upsert(session) + + fixed_message_text = fix_whole_utterance_transcription(msg.text) + if fixed_message_text != msg.text: + await telemetry.event( + "channel.transcription.fixed", + { + "session_id": agent_session_id, + "original_text": msg.text, + "fixed_text": fixed_message_text, + }, + ) + + interruption = evaluate_interruption( + payload=payload, + message_text=fixed_message_text, + session_metadata=session.metadata, + terminal_fallback_text=getattr(settings, "POST_FINALIZE_REPLAY_MESSAGE", ""), + ) + if interruption.action == "classify": + prior_history = await memory.list(agent_session_id) + prior_user_text = "" + for prior in reversed(prior_history): + role = getattr(prior, "role", None) + content = getattr(prior, "content", "") + if str(role or "") == "user" and str(content or "").strip(): + prior_user_text = str(content).strip() + break + regenerate = await classify_processing_interruption( + llm, + original_agent=interruption.replay_text, + original_client=prior_user_text, + supplement_client=interruption.text, + ) + await telemetry.event( + "channel.processing_interruption.classified", + { + "session_id": agent_session_id, + "regenerate": regenerate, + "profile_name": "processing_interruption_classifier", + }, + ) + if regenerate: + interruption.action = "process" + interruption.reason = "classifier_result_1" + else: + interruption.action = "replay" + interruption.reason = "classifier_result_0" + + if interruption.action == "replay": + response = ChannelResponse( + channel=msg.channel, + session_id=agent_session_id, + text=interruption.replay_text, + 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, + "workflow_id": workflow_id, + "message_id": message_id, + "replay": True, + "replay_reason": interruption.reason, + "is_interruptible": interruption.is_interruptible, + "framework_short_circuit": True, + "terminal_status": interruption.terminal_status, + "llm_called": False, + "tools_called": False, + "guardrails_called": False, + }, + ) + rendered = await gateway.render(response) + await telemetry.event("gateway.message.replayed", {"session_id": agent_session_id, "reason": interruption.reason}) + await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None + return rendered + + effective_text = interruption.text + 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=effective_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)] + + cms_input = { + "channel": req.channel, + "tenant_id": req.tenant_id, + "agent_id": req.agent_id, + "payload": payload, + } + trace_context = { + "text": effective_text, + "channel": msg.channel, + "channel_id": msg.channel_id, + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "conversation_key": agent_session_id, + "workflow_id": workflow_id, + "message_id": message_id, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + } + root_span_name = _format_root_span_name( + getattr(settings, "LANGFUSE_ROOT_SPAN_NAME", "agent.gateway_message"), + { + "workflow_id": workflow_id, + "channel": msg.channel, + "agent_id": identity.agent_id, + "tenant_id": identity.tenant_id, + }, + ) + root_tags = ["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"] + if workflow_id: + root_tags.append(f"workflow:{workflow_id}") + + async with telemetry.span( + root_span_name, + session_id=agent_session_id, + user_id=session.user_id, + channel=msg.channel, + workflow_id=workflow_id, + input=cms_input, + tags=root_tags, + _root_span=True, + ) as root_span: + await telemetry.event("gateway.message.received", trace_context) + await sse_hub.emit(agent_session_id, "workflow.started", trace_context) 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, + "workflow_id": workflow_id, + "agent_profile": normalized_context["agent_profile"], + # Chave estável de LTM. Nunca use session_id como identidade de longo prazo. + "long_term_memory_subject_key": business_context.customer_key or session.user_id, + "customer_key": business_context.customer_key, + "user_id": session.user_id, + "business_context": business_context.model_dump(), + "user_text": effective_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, + "workflow_id": workflow_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"), + }, + ), + ) + + terminal_status = str(result.get("terminal_status") or "").strip() + session_ended = bool(result.get("session_ended")) or bool(terminal_status) + session.metadata = { + **(session.metadata or {}), + "last_assistant_text": answer, + "last_assistant_is_interruptible": bool(result.get("is_interruptible", True)), + "last_route": result.get("route"), + "last_intent": result.get("intent"), + "conversation_closed": session_ended, + "terminal_status": terminal_status or ("resolvido" if session_ended else ""), + "terminal_replay_text": answer if session_ended else "", + } + await sessions.upsert(session) + + 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, + "workflow_id": workflow_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"), + "transaction_evidence": result.get("relevant_transaction_evidence", []), + "transaction_pre_validation": result.get("transaction_pre_validation"), + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + "judges": result.get("judge_results"), + "guardrails": result.get("guardrail_decisions"), + "long_term_memory": { + "subject_key": business_context.customer_key or session.user_id, + "loaded": result.get("long_term_memories", []), + "context": result.get("long_term_memory_context", ""), + "load_error": result.get("long_term_memory_load_error"), + "write_result": result.get("long_term_memory_write_result", {}), + }, + }, + ) + rendered = await gateway.render(response) + root_span.set_output(rendered) + 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, + "long_term_memory": { + "enabled": getattr(settings, "ENABLE_LONG_TERM_MEMORY", False), + "provider": getattr(settings, "LONG_TERM_MEMORY_PROVIDER", None), + "sqlite_path": getattr(settings, "LONG_TERM_MEMORY_SQLITE_PATH", None), + "table": getattr(settings, "LONG_TERM_MEMORY_TABLE", None), + "auto_extract": getattr(settings, "LONG_TERM_MEMORY_AUTO_EXTRACT", None), + "inject_context": getattr(settings, "LONG_TERM_MEMORY_INJECT_CONTEXT", None), + }, + "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, + "framework_channel_input_mode": settings.FRAMEWORK_CHANNEL_INPUT_MODE, + "legacy_channel_gateway_mode": settings.CHANNEL_GATEWAY_MODE, + } + + +@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, + "FRAMEWORK_CHANNEL_INPUT_MODE": settings.FRAMEWORK_CHANNEL_INPUT_MODE, + "CHANNEL_GATEWAY_MODE": settings.CHANNEL_GATEWAY_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": effective_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() diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/mcp_gateway_client_factory.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/mcp_gateway_client_factory.py new file mode 100644 index 0000000..5a32d15 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/mcp_gateway_client_factory.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import os + +from agent_framework.gateways import MCPGatewayClient + + +def build_mcp_gateway_client() -> MCPGatewayClient | None: + if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true": + return None + + return MCPGatewayClient( + base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"), + token=os.getenv("MCP_GATEWAY_TOKEN") or None, + timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")), + ) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/__init__.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/telemetry_observer.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/telemetry_observer.py new file mode 100644 index 0000000..92f07a1 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/observability/telemetry_observer.py @@ -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}) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/__init__.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/__init__.py new file mode 100644 index 0000000..c0eba0d --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/__init__.py @@ -0,0 +1,3 @@ +from .tool_renderers import register_tool_renderers + +__all__ = ["register_tool_renderers"] diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/tool_renderers.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/tool_renderers.py new file mode 100644 index 0000000..f77c47a --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/presentation/tool_renderers.py @@ -0,0 +1,74 @@ +from __future__ import annotations + +from typing import Any + +from agent_framework.presentation import register_tool_response_renderer + + +def _money_brl(value: Any) -> str: + try: + return f"{float(value):.2f}".replace(".", ",") + except (TypeError, ValueError): + return str(value) + + +def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None: + return f"[{agent_label}] Fatura consultada: {result}." + + +def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None: + plano = result.get("plano") + if plano is None: + return None + parts = [f"[{agent_label}] Seu plano é {plano}"] + internet_gb = result.get("internet_gb") + status = result.get("status") + if internet_gb is not None: + parts.append(f"com {internet_gb} GB") + if status is not None: + parts.append(f"status {status}") + return ", ".join(parts) + "." + + +def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None: + order_id = result.get("order_id") + status = result.get("status") + if order_id is None or status is None: + return None + lines = [f"[{agent_label}] Pedido {order_id}: status {status}."] + total = result.get("valor_total") + if total is not None: + lines.append(f"Valor total: R$ {_money_brl(total)}.") + items = result.get("itens") or [] + rendered_items: list[str] = [] + if isinstance(items, list): + for item in items: + if isinstance(item, dict): + value = item.get("descricao") or item.get("nome") or item.get("sku") + else: + value = item + if value not in (None, ""): + rendered_items.append(str(value)) + if rendered_items: + lines.append("Itens: " + "; ".join(rendered_items) + ".") + return " ".join(lines) + + +def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None: + order_id = result.get("order_id") + transportadora = result.get("transportadora") + codigo = result.get("codigo_rastreio") + previsao = result.get("previsao_entrega") + if any(v is None for v in (order_id, transportadora, codigo, previsao)): + return None + return ( + f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, " + f"rastreio {codigo}, previsão {previsao}." + ) + + +def register_tool_renderers() -> None: + register_tool_response_renderer("telecom.invoice", render_telecom_invoice) + register_tool_response_renderer("telecom.plan", render_telecom_plan) + register_tool_response_renderer("retail.order", render_retail_order) + register_tool_response_renderer("retail.delivery", render_retail_delivery) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py new file mode 100644 index 0000000..a7c91a0 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/state.py @@ -0,0 +1,57 @@ +from typing import Any, TypedDict + + +class AgentState(TypedDict, total=False): + tenant_id: str + agent_id: str + session_id: str + conversation_key: str + workflow_id: 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]] + available_mcp_tools: list[str] + selected_tool_call: dict[str, Any] + pending_tool_call: dict[str, Any] + transaction_status: str + transaction_pre_validation: dict[str, Any] + transaction_evidence: list[dict[str, Any]] + last_transaction_evidence: dict[str, Any] + relevant_transaction_evidence: list[dict[str, Any]] + confirmation_required: bool + confirmation_received: bool + tool_policy_result: dict[str, Any] + missing_parameters: list[str] + supervisor_plan: dict[str, Any] + supervisor_results: list[dict[str, Any]] + active_agent: str + route_bypassed: bool + continuity_signal: dict[str, Any] + session_control: str + session_ended: bool + human_handoff_requested: bool + 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 + long_term_memories: list[dict[str, Any]] + long_term_memory_context: str + long_term_memory_write_result: dict[str, Any] + long_term_memory_subject_key: str + long_term_memory_load_error: str diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/__init__.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/__init__.py new file mode 100644 index 0000000..6be8ce7 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/__init__.py @@ -0,0 +1 @@ +from . import devolucao # noqa: F401 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/devolucao.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/devolucao.py new file mode 100644 index 0000000..6111cf1 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflow_actions/devolucao.py @@ -0,0 +1,13 @@ +"""Actions de domínio permanecem no agente; o runtime está no framework.""" +from agent_framework.workflows import workflow_action + + +@workflow_action("validar_pedido") +async def validar_pedido(params: dict, state: dict) -> dict: + return {"valid": bool(params.get("order_id"))} + + +@workflow_action("registrar_devolucao") +async def registrar_devolucao(params: dict, state: dict) -> dict: + # Substitua pela chamada real ao serviço/MCP e use chave idempotente. + return {"protocol": f"DEV-{params['order_id']}", "status": "REQUESTED"} diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py new file mode 100644 index 0000000..e6605a4 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/app/workflows/agent_graph.py @@ -0,0 +1,892 @@ +from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer +from agent_framework.workflows import END, START, FrameworkStateGraph + +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 +from agent_framework.memory.long_term_memory import create_long_term_memory_manager + + +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.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry) + 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) + + # The existing agent constructors intentionally keep their stable API. + # Long-term memory is injected as a runtime capability after creation. + for agent in (self.billing, self.product, self.orders, self.support): + agent.long_term_memory_manager = self.long_term_memory_manager + 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 = FrameworkStateGraph(AgentState) + builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails)) + builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory)) + 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("human_handoff", self._node("human_handoff", self.human_handoff)) + builder.add_node("end_session", self._node("end_session", self.end_session)) + 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_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory)) + 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": "load_long_term_memory"}, + ) + builder.add_edge("load_long_term_memory", "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", + "human_handoff": "human_handoff", + "end_session": "end_session", + "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("human_handoff", "output_supervisor") + builder.add_edge("end_session", "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_long_term_memory") + builder.add_edge("persist_long_term_memory", "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): + if state.get("session_ended") is True: + answer = str(getattr( + self.settings, + "SESSION_ALREADY_ENDED_MESSAGE", + "Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.", + )) + await self.telemetry.event( + "session.message.rejected_after_end", + {"session_id": state.get("conversation_key") or state.get("session_id")}, + ) + return { + "answer": answer, + "final_answer": answer, + "blocked": True, + "session_control": "END_SESSION", + "session_ended": True, + "next_state": "SESSION_ENDED", + } + 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", [])] + await self.observer.emit_grl( + "001", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + }, + component="workflow.input_guardrails.start", + ) + 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) + await self.observer.emit_grl( + "002" if _decision.allowed else "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + "rail_code": getattr(_decision, "code", None), + "allowed": bool(_decision.allowed), + "reason": getattr(_decision, "reason", None), + }, + component="workflow.input_guardrails.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], + }, + ) + await self.observer.emit_grl( + "009", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "input", + "blocked": any(not d.allowed for d in decisions), + "decision_count": len(decisions), + }, + component="workflow.input_guardrails.final", + ) + 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, + "active_agent": decision.agent, + "route_bypassed": decision.method == "continuity", + "session_control": (decision.metadata or {}).get("session_control", ""), + "human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF", + "session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION", + "continuity_signal": { + "decision": (decision.metadata or {}).get("continuity_decision"), + "confidence": decision.confidence if decision.method == "continuity" else None, + "reason": decision.reason if decision.method == "continuity" else None, + "profile": (decision.metadata or {}).get("continuity_profile"), + } if decision.method == "continuity" else {}, + } + + async def billing_agent(self, 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.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.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.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 human_handoff(self, state): + session_id = state.get("conversation_key") or state.get("session_id") + async with self.telemetry.span("workflow.human_handoff", session_id=session_id): + answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa.")) + await self.telemetry.event( + "session.human_handoff.requested", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "reason": (state.get("route_decision") or {}).get("reason"), + }, + ) + return { + "answer": answer, + "session_control": "HUMAN_HANDOFF", + "human_handoff_requested": True, + "session_ended": False, + "next_state": "HUMAN_HANDOFF_REQUESTED", + } + + async def end_session(self, state): + session_id = state.get("conversation_key") or state.get("session_id") + async with self.telemetry.span("workflow.end_session", session_id=session_id): + answer = str(getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato.")) + await self.telemetry.event( + "session.end.requested", + { + "session_id": session_id, + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "reason": (state.get("route_decision") or {}).get("reason"), + }, + ) + return { + "answer": answer, + "session_control": "END_SESSION", + "session_ended": True, + "human_handoff_requested": False, + "next_state": "SESSION_ENDED", + } + + 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, + }, + ) + + await self.observer.emit_ic( + "IC.OUTPUT_SUPERVISOR_COMPLETED", + { + "session_id": context["session_id"], + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "route": state.get("route"), + "intent": state.get("intent"), + "action": action, + "approved": decision.approved, + "result_count": len(decision.results), + }, + component="workflow.output_supervisor", + ) + + 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"), + ): + await self.observer.emit_grl( + "001", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "route": state.get("route"), + "intent": state.get("intent"), + }, + component="workflow.output_guardrails.start", + ) + final, decisions = await self.guardrails.run_output( + state["answer"], state.get("context", {}) + ) + for _decision in decisions: + await self.guardrail_telemetry.evaluated("output", _decision) + await self.observer.emit_grl( + "002" if _decision.allowed else "004", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "rail_code": getattr(_decision, "code", None), + "allowed": bool(_decision.allowed), + "reason": getattr(_decision, "reason", None), + }, + component="workflow.output_guardrails.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], + }, + ) + await self.observer.emit_grl( + "009", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "phase": "output", + "blocked": any(not d.allowed for d in decisions), + "decision_count": len(decisions), + }, + component="workflow.output_guardrails.final", + ) + 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")}, + ): + judge_context = dict(state.get("context", {}) or {}) + judge_context["mcp_results"] = state.get("mcp_results", []) + relevant_transaction_evidence = list(state.get("relevant_transaction_evidence") or []) + judge_context["transaction_evidence"] = relevant_transaction_evidence + current_evidence = list(state.get("mcp_results", []) or []) + current_evidence.extend(relevant_transaction_evidence) + judge_context["evidence"] = current_evidence or judge_context.get("evidence") + judge_context["route"] = state.get("route") + judge_context["intent"] = state.get("intent") + # Judge sampling must see the finalized transaction state. These + # fields are populated by the agent/tool runtime before this node. + for key in ( + "transaction_status", + "confirmation_required", + "confirmation_received", + "tool_policy_result", + "selected_tool_call", + "pending_tool_call", + ): + judge_context[key] = state.get(key) + judge_context["transactional_tools"] = [ + result.get("tool_name") + for result in state.get("mcp_results", []) + if isinstance(result, dict) + and ( + (result.get("metadata") or {}).get("operation_type") == "transactional" + or result.get("awaiting_confirmation") + or result.get("transaction_status") + ) + ] + results = await self.judges.evaluate_all( + state["user_text"], state["final_answer"], judge_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 load_long_term_memory(self, state): + """Carrega LTM antes do roteamento e mantém o resultado no estado. + + A carga explícita evita depender apenas do agente selecionado para realizar + a recuperação e facilita o diagnóstico de identidade/namespace. + """ + try: + memories = await self.long_term_memory_manager.load(state) + serialized = [] + context_lines = [] + for item in memories or []: + if hasattr(item, "model_dump"): + data = item.model_dump(mode="json") + elif hasattr(item, "__dict__"): + data = dict(item.__dict__) + elif isinstance(item, dict): + data = dict(item) + else: + data = {"value": str(item)} + serialized.append(data) + key = data.get("key") or data.get("memory_key") or data.get("category") or "memory" + value = data.get("value") or data.get("memory_value") + if value not in (None, ""): + context_lines.append(f"- {key}: {value}") + + return { + "long_term_memories": serialized, + "long_term_memory_context": "\n".join(context_lines), + } + except Exception as exc: + await self.telemetry.event( + "long_term_memory.load.failed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "error": str(exc), + }, + ) + return { + "long_term_memories": [], + "long_term_memory_context": "", + "long_term_memory_load_error": str(exc), + } + + async def persist_long_term_memory(self, state): + try: + result = await self.long_term_memory_manager.persist_turn(state) + await self.telemetry.event( + "long_term_memory.persist.completed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "result": result, + }, + ) + return {"long_term_memory_write_result": result} + except Exception as exc: + await self.telemetry.event( + "long_term_memory.persist.failed", + { + "session_id": state.get("conversation_key") or state.get("session_id"), + "tenant_id": state.get("tenant_id"), + "agent_id": state.get("agent_id"), + "subject_key": state.get("long_term_memory_subject_key"), + "error": str(exc), + }, + ) + return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}} + + 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", []), + "transaction_evidence": state.get("relevant_transaction_evidence", []), + }, + ) + + 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 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents.yaml new file mode 100644 index 0000000..7d245a5 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents.yaml @@ -0,0 +1,33 @@ +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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/guardrails.yaml @@ -0,0 +1,8 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/judges.yaml new file mode 100644 index 0000000..62fc7c7 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/judges.yaml @@ -0,0 +1,7 @@ +judges: + - name: response_quality + enabled: true + threshold: 0.7 + - name: groundedness + enabled: true + threshold: 0.6 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml new file mode 100644 index 0000000..f872a2b --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/retail_orders/prompt_policy.yaml @@ -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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/guardrails.yaml @@ -0,0 +1,8 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/judges.yaml new file mode 100644 index 0000000..d488063 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/judges.yaml @@ -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 \ No newline at end of file diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml new file mode 100644 index 0000000..42732c4 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/agents/telecom_contas/prompt_policy.yaml @@ -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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/guardrails.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/guardrails.yaml new file mode 100644 index 0000000..44887eb --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/guardrails.yaml @@ -0,0 +1,12 @@ +input: + - code: MSK + enabled: true + - code: VLOOP + enabled: true +output: + - code: REVPREC + enabled: true + - code: PINJ + enabled: true + - code: DLEX_OUT + enabled: true \ No newline at end of file diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/identity.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/identity.yaml new file mode 100644 index 0000000..5f20147 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/identity.yaml @@ -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 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/judges.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/judges.yaml new file mode 100644 index 0000000..c091619 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/judges.yaml @@ -0,0 +1,18 @@ +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 +sample_rate: 0.25 +always_run_for_transactional: true diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_parameter_mapping.yaml new file mode 100644 index 0000000..2e9294e --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_parameter_mapping.yaml @@ -0,0 +1,108 @@ +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 + extract: + mes_referencia: + from: message + type: int + strategy: month_name_pt + description: 'Extrair mês citado na mensagem. janeiro=1, fevereiro=2, março=3, + abril=4, maio=5, junho=6, julho=7, agosto=8, setembro=9, outubro=10, novembro=11, + dezembro=12. + + ' + 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 + session_key: session_id + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + consultar_entrega: + map: + session_key: session_id + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + validar_cancelamento_pedido: + map: + resource_key: order_id + + cancelar_pedido: + map: + session_key: session_id + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente pelo usuário. Retorne null quando não houver identificador de pedido na mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + + solicitar_troca: + map: + session_key: session_id + defaults: + reason: Solicitação aberta pelo atendimento conversacional. + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 + solicitar_devolucao: + map: + session_key: session_id + defaults: + reason: Solicitação aberta pelo atendimento conversacional. + extract: + order_id: + from: message + type: string + strategy: hybrid + description: Extraia somente o identificador do pedido informado explicitamente + pelo usuário. Retorne null quando não houver identificador de pedido na + mensagem. + pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b + group: 1 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.docker.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.docker.yaml new file mode 100644 index 0000000..8101130 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.docker.yaml @@ -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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.yaml new file mode 100644 index 0000000..fe638a2 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/mcp_servers.yaml @@ -0,0 +1,30 @@ +# MCP servers registry. +# transport=http keeps the legacy framework mock contract: +# GET /tools/list +# POST /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 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/prompt_policy.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/prompt_policy.yaml new file mode 100644 index 0000000..af4398f --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/prompt_policy.yaml @@ -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 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/routing.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/routing.yaml new file mode 100644 index 0000000..03aeaa9 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/routing.yaml @@ -0,0 +1,147 @@ +# 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. + - state: COLLECTING_BILLING_PARAMETERS + agent: billing_agent + description: Mantém a coleta de parâmetros no fluxo de faturamento. + - state: COLLECTING_PRODUCT_PARAMETERS + agent: product_agent + description: Mantém a coleta de parâmetros no fluxo de produtos e serviços. + - state: COLLECTING_ORDER_PARAMETERS + agent: orders_agent + description: Mantém a coleta de parâmetros no fluxo de pedidos. + - state: COLLECTING_SUPPORT_PARAMETERS + agent: support_agent + description: Mantém a coleta de parâmetros no fluxo transacional 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 + - 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_cancel + domain: retail + agent: orders_agent + description: Cancelamento explícito de pedido ou compra. + priority: 20 + mcp_tools: + - consultar_pedido + - cancelar_pedido + keywords: + - cancelar pedido + - cancelamento do pedido + - cancelar a compra + - cancelar compra + examples: + - Quero cancelar meu pedido. + - Cancele o pedido. + - Quero cancelar a compra. + + - 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: 25 + mcp_tools: + - consultar_pedido + - solicitar_troca + - solicitar_devolucao + keywords: + - solicitar devolução + - devolver pedido + - solicitar troca + - 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? diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tool_policies.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tool_policies.yaml new file mode 100644 index 0000000..db288ae --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tool_policies.yaml @@ -0,0 +1,36 @@ +version: 1 + +# Arquivo opcional da aplicação. A ausência mantém o comportamento dos +# templates anteriores e as políticas legadas declaradas em tools.yaml. +defaults: + operation_type: read_only + require_confirmation: false + +tool_policies: + cancelar_pedido: + operation_type: transactional + require_confirmation: true + requires: [order_id] + pre_validation: + enabled: true + tool: validar_cancelamento_pedido + fail_open: false + + + solicitar_troca: + operation_type: transactional + require_confirmation: true + + solicitar_devolucao: + operation_type: transactional + require_confirmation: true + requires: [order_id, reason] + execution: + mode: workflow + workflow: devolucao_pedido + version: active + +# Exemplo para uma operação real que só pode executar após confirmação: +# cancelar_servico: +# operation_type: transactional +# require_confirmation: true diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tools.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tools.yaml new file mode 100644 index 0000000..72c93e2 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/config/tools.yaml @@ -0,0 +1,142 @@ +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 + selection_keywords: + - fatura + - conta + - boleto + response: + mode: renderer + renderer: telecom.invoice + consultar_pagamentos: + description: Consulta histórico de pagamentos do cliente. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + selection_keywords: + - pagamento + - pagamentos + consultar_plano: + description: Consulta plano ativo e atributos comerciais. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + asset_id: string + selection_keywords: + - plano + response: + mode: renderer + renderer: telecom.plan + listar_servicos: + description: Lista serviços ativos e adicionais VAS. + mcp_server: telecom + enabled: true + args_schema: + msisdn: string + selection_keywords: + - serviços + - servicos + - vas + 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 + selection_keywords: + - consultar pedido + - status do pedido + - pedido + response: + mode: renderer + renderer: retail.order + consultar_entrega: + description: Consulta entrega e rastreamento do pedido. + mcp_server: retail + enabled: true + args_schema: + order_id: string + selection_keywords: + - entrega + - rastreio + - rastreamento + - transportadora + - previsão + response: + mode: renderer + renderer: retail.delivery + validar_cancelamento_pedido: + description: Pre-valida cancelamento do pedido sem executar efeitos transacionais. + mcp_server: retail + enabled: true + tool_type: internal + confirmation_required: false + requires: [order_id] + args_schema: + order_id: string + target_tool: string + selection_keywords: [] + + cancelar_pedido: + description: Simula o cancelamento de um pedido de varejo. + mcp_server: retail + enabled: true + tool_type: action + requires: + - order_id + confirmation_required: true + args_schema: + order_id: string + selection_keywords: + - cancelar pedido + - cancelamento do pedido + - cancelar compra + - cancelar a compra + + + solicitar_troca: + description: Simula abertura de solicitação de troca. + mcp_server: retail + enabled: true + tool_type: action + requires: + - order_id + - reason + confirmation_required: true + args_schema: + order_id: string + reason: string + selection_keywords: + - solicitar troca + - trocar + - troca + - defeito + - quebrado + 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: true + args_schema: + order_id: string + reason: string + selection_keywords: + - solicitar devolução + - solicitar devolucao + - devolver pedido + - devolver + - devolução + - devolucao + - arrependimento diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md new file mode 100644 index 0000000..d81efdf --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md @@ -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//topics/ +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= +OCI_STREAM_OCID= +GCP_PUBSUB_TOPIC_PATH=projects//topics/ +``` + +## 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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md new file mode 100644 index 0000000..83975af --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md @@ -0,0 +1,45 @@ +# Como usar IC, NOC e GRL no Template Backend + +## IC — Item de Controle + +Use IC para registrar eventos de negócio relevantes. + +```python +await observer.emit_ic( + "IC.FATURA_CONSULTADA", + {"session_id": session_id, "invoice_id": invoice_id}, + component="billing_agent", +) +``` + +## NOC — Evento operacional + +Use NOC para saúde técnica, latência, erros e checkpoints operacionais. + +```python +await observer.emit_noc( + "003", + {"session_id": session_id, "resourceName": "ADB", "latencyMs": 120}, + component="repository", +) +``` + +## GRL — Evento de guardrail + +Normalmente o framework emite GRL automaticamente. Use manualmente apenas para +rails customizados dentro do agente. + +```python +await observer.emit_grl( + "OBSERVE", + {"session_id": session_id, "rail_code": "CUSTOM_POLICY"}, + component="custom_rail", +) +``` + +## Onde já existe no template + +- `app/workflows/agent_graph.py` emite IC/NOC no ciclo do workflow. +- `app/agents/runtime.py` emite IC para MCP/tools. +- `app/agents/*_agent.py` contém exemplos dentro do método `run()`. +- `app/examples/` contém exemplos isolados. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md new file mode 100644 index 0000000..3f981ac --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md @@ -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`. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md new file mode 100644 index 0000000..5c41732 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md @@ -0,0 +1,14 @@ +# Exemplos implementados no template + +Este projeto entrega as capacidades transversais habilitadas como referência: + +- route stickiness semântica com o perfil `route_continuity`; +- decisões `CONTINUE`, `ROUTE`, `HUMAN_HANDOFF` e `END_SESSION`; +- nós globais `human_handoff` e `end_session`; +- persistência de `active_agent`, `route_bypassed`, `continuity_signal` e controle de sessão; +- rejeição de novas mensagens depois de `session_ended=true`; +- políticas MCP `read_only` e `transactional` no backend; +- exemplo `solicitar_devolucao` com `require_confirmation: true`. + +Para confirmar a transação, envie `confirmed: true` ou `confirmation: true` como booleano. Handoff e encerramento não chamam agentes de domínio nem MCP. + diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md new file mode 100644 index 0000000..c7bd3b2 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md @@ -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 +``` diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md new file mode 100644 index 0000000..849fda1 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md @@ -0,0 +1,127 @@ +# Guardrails paralelos fail-fast e Observer IC + +## O que foi implementado + +### 1. ParallelRailExecutor + +Arquivo principal: + +```text +agent_framework/src/agent_framework/guardrails/parallel_executor.py +``` + +Também foi criado um alias de compatibilidade: + +```text +agent_framework/src/agent_framework/guardrails/executor.py +``` + +Esse alias evita erro quando algum código antigo importar: + +```python +from agent_framework.guardrails.executor import ParallelRailExecutor +``` + +### 2. Execução paralela no GuardrailPipeline + +Arquivo alterado: + +```text +agent_framework/src/agent_framework/guardrails/pipeline.py +``` + +O pipeline continua retornando o contrato antigo: + +```python +(texto_final, list[RailDecision]) +``` + +mas internamente pode executar rails em paralelo com fail-fast. + +### 3. Execução paralela no OutputSupervisor + +Arquivo alterado: + +```text +agent_framework/src/agent_framework/guardrails/output_supervisor.py +``` + +O `OutputSupervisor` agora usa `ParallelRailExecutor` quando habilitado. + +### 4. Configuração + +Novas configurações: + +```env +ENABLE_PARALLEL_GUARDRAILS=true +GUARDRAILS_FAIL_FAST=true +``` + +Também foram adicionadas em: + +```text +agent_framework/src/agent_framework/config/settings.py +.env +.env.example +agent_template_backend/.env +agent_template_backend_day_zero/.env +``` + +### 5. Observer IC + +O `AgentObserver` já tinha `emit_ic()`. + +Foi complementada a API global compatível com FIRST/TIM: + +```python +from agent_framework.observer import ic, aic, noc, anoc, grl, agrl +``` + +Exemplos: + +```python +ic("AGENT_COMPLETED", data={"session_id": "..."}) +await aic("MCP_TOOL_CALLED", data={"tool_name": "consultar_fatura"}) +``` + +### 6. ICs automáticos no template backend + +O backend emite agora: + +```text +IC.AGENT_STARTED +IC.ROUTE_SELECTED +IC.MCP_TOOL_CALLED +IC.TOOL_CALLED +IC.AGENT_COMPLETED +``` + +Além dos eventos já existentes: + +```text +NOC.001 +NOC.005 +NOC.006 +GRL.001 ... GRL.009 +``` + +## Validações executadas + +Foram executadas validações locais com `PYTHONPATH=agent_framework/src`: + +```bash +python3 -m compileall -q agent_framework/src/agent_framework agent_template_backend/app agent_template_backend_day_zero/app +``` + +Smoke tests executados: + +```text +1. Import de ParallelRailExecutor via agent_framework.guardrails +2. Import de ParallelRailExecutor via agent_framework.guardrails.executor +3. Execução fail-fast: FastBlock cancela SlowAllow +4. GuardrailPipeline paralelo retorna RailDecision legado +5. OutputSupervisor paralelo retorna RailAction.BLOCK +6. API global observer.ic/noc/grl/aic/anoc/agrl +``` + +Observação: o import completo do `agent_template_backend.app.workflows.agent_graph` depende de `langgraph`, que não está instalado no sandbox de validação. O arquivo foi validado por `compileall`, e a dependência já consta em `agent_template_backend/requirements.txt`. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md new file mode 100644 index 0000000..edcd2c7 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md @@ -0,0 +1,42 @@ +# Implementação IC/NOC/GRL preservando lógica existente + +Esta versão mantém a lógica original dos agentes do `agent_template_backend` e adiciona observabilidade corporativa. + +## IC adicionados nos agentes + +Cada agente agora emite eventos de negócio sem alterar a resposta final: + +- `IC.BILLING_AGENT_STARTED` / `IC.BILLING_AGENT_COMPLETED` +- `IC.ORDERS_AGENT_STARTED` / `IC.ORDERS_AGENT_COMPLETED` +- `IC.PRODUCT_AGENT_STARTED` / `IC.PRODUCT_AGENT_COMPLETED` +- `IC.SUPPORT_AGENT_STARTED` / `IC.SUPPORT_AGENT_COMPLETED` +- `IC._MCP_CONTEXT_COLLECTED` quando houver dados MCP +- `IC._RAG_CONTEXT_RETRIEVED` quando RAG estiver habilitado + +O mixin `AgentRuntimeMixin` também emite: + +- `IC.MCP_TOOL_CALLED` antes da chamada MCP +- `IC.TOOL_CALLED` após a chamada MCP + +## NOC + +O workflow já emite eventos operacionais principais: + +- `NOC.001` no início da execução +- `NOC.005` em exceção fatal +- `NOC.006` na persistência/finalização + +## GRL + +O backend agora também exemplifica emissão GRL no workflow: + +- `GRL.001` início do pipeline de guardrails +- `GRL.002` decisão allow +- `GRL.004` decisão block +- `GRL.009` decisão final agregada + +Quando `OutputSupervisor` está habilitado, ele continua sendo o principal mecanismo corporativo de supervisão de saída. + +## Garantia + +A lógica original dos agentes não foi substituída por stubs. As chamadas LLM, MCP, RAG, cache e os retornos originais foram preservados. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md new file mode 100644 index 0000000..bc2638b --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md @@ -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. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md new file mode 100644 index 0000000..cfe5969 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/TESTE_LONG_TERM_MEMORY.md @@ -0,0 +1,82 @@ +# Teste e diagnóstico de Long-Term Memory + +## O que foi corrigido + +1. A LTM agora é carregada explicitamente antes do roteamento. +2. O estado recebe uma chave estável em `long_term_memory_subject_key`, baseada em `business_context.customer_key` e, como fallback, `user_id`. +3. O resultado de carga e persistência aparece em `metadata.long_term_memory` da resposta. +4. `/health` informa a configuração efetiva de LTM carregada pelo processo. +5. Falhas de leitura e gravação geram eventos `long_term_memory.load.failed` e `long_term_memory.persist.failed`. + +## Teste + +Primeira sessão: + +```bash +curl -s http://localhost:8000/gateway/message \ + -H 'Content-Type: application/json' \ + -d '{ + "channel":"web", + "payload":{ + "text":"Meu nome preferido é Cris e minha linguagem preferida é Python.", + "session_id":"ltm-session-001", + "user_id":"ltm-user-001", + "customer_id":"ltm-customer-001" + } + }' +``` + +Verifique na resposta: + +```json +"long_term_memory": { + "subject_key": "ltm-customer-001", + "write_result": { + "saved": 2 + } +} +``` + +Nova sessão, mesma identidade: + +```bash +curl -s http://localhost:8000/gateway/message \ + -H 'Content-Type: application/json' \ + -d '{ + "channel":"web", + "payload":{ + "text":"Qual é meu nome preferido e qual linguagem eu prefiro?", + "session_id":"ltm-session-002", + "user_id":"ltm-user-001", + "customer_id":"ltm-customer-001" + } + }' +``` + +Na segunda resposta, confira: + +- `metadata.long_term_memory.subject_key` igual à primeira chamada; +- `metadata.long_term_memory.loaded` com registros; +- `metadata.long_term_memory.context` preenchido; +- ausência de `load_error`. + +## Diagnóstico rápido + +```bash +curl -s http://localhost:8000/health +``` + +A seção `long_term_memory` deve mostrar: + +```json +{ + "enabled": true, + "provider": "sqlite", + "sqlite_path": "./data/agent_framework.db", + "table": "agentfw_long_term_memory", + "auto_extract": true, + "inject_context": true +} +``` + +Execute o backend com o diretório do projeto como diretório de trabalho. Como o caminho SQLite é relativo, iniciar a aplicação em outro diretório pode criar ou consultar outro arquivo `./data/agent_framework.db`. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md new file mode 100644 index 0000000..a9e4458 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md @@ -0,0 +1,62 @@ +# Validação da versão com IC/NOC/GRL + +Validações executadas nesta geração: + +1. `python -m compileall -q agent_template_backend/app` + - Resultado: OK. + +2. Smoke test dos agentes com LLM fake e Observer fake: + - `BillingAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `OrdersAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `ProductAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + - `SupportAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim. + +3. Verificação de regressão: + - Nenhum agente retorna `Template Enterprise ativo`. + - A lógica LLM/MCP/RAG/cache existente foi preservada. + +## Eventos adicionados + +### IC + +Nos agentes: + +- `IC.BILLING_AGENT_STARTED` +- `IC.BILLING_MCP_CONTEXT_COLLECTED` +- `IC.BILLING_RAG_CONTEXT_RETRIEVED` +- `IC.BILLING_AGENT_COMPLETED` +- `IC.ORDERS_AGENT_STARTED` +- `IC.ORDERS_MCP_CONTEXT_COLLECTED` +- `IC.ORDERS_RAG_CONTEXT_RETRIEVED` +- `IC.ORDERS_AGENT_COMPLETED` +- `IC.PRODUCT_AGENT_STARTED` +- `IC.PRODUCT_MCP_CONTEXT_COLLECTED` +- `IC.PRODUCT_RAG_CONTEXT_RETRIEVED` +- `IC.PRODUCT_AGENT_COMPLETED` +- `IC.SUPPORT_AGENT_STARTED` +- `IC.SUPPORT_MCP_CONTEXT_COLLECTED` +- `IC.SUPPORT_RAG_CONTEXT_RETRIEVED` +- `IC.SUPPORT_AGENT_COMPLETED` + +No runtime MCP: + +- `IC.MCP_TOOL_CALLED` +- `IC.TOOL_CALLED` + +### NOC + +Já integrados no workflow: + +- `NOC.001` início da execução +- `NOC.005` erro fatal +- `NOC.006` finalização/persistência + +### GRL + +No workflow de guardrails: + +- `GRL.001` início da avaliação +- `GRL.002` allow +- `GRL.004` block +- `GRL.009` decisão final + diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt new file mode 100644 index 0000000..fac4bf4 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt @@ -0,0 +1,3 @@ +compileall app: OK +Arquivos de exemplos IC/NOC/GRL adicionados. +Agentes preservam implementação original comentada. diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/llm_profiles.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/llm_profiles.yaml new file mode 100644 index 0000000..908b382 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/llm_profiles.yaml @@ -0,0 +1,80 @@ +profiles: + default: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + max_tokens: 2048 + supervisor: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + route_continuity: + provider: oci_openai + model: openai.gpt-4.1-mini + temperature: 0 + max_tokens: 80 + timeout_seconds: 5 + router: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 500 + guardrail: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 600 + grl: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + judge: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 800 + rag_rewriter: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 300 + rag_compressor: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 1200 + rag_generation: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.1 + max_tokens: 1800 + summary_memory: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.1 + max_tokens: 1200 + noc: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0 + max_tokens: 700 + billing_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + product_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + backoffice_agent: + provider: oci_openai + model: openai.gpt-4.1 + temperature: 0.2 + mcp_parameter_extraction: + provider: oci_openai + model: openai.gpt-4.1-mini + temperature: 0 + max_tokens: 80 + timeout_seconds: 5 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/requirements.txt b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/requirements.txt new file mode 100644 index 0000000..71214bd --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/requirements.txt @@ -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 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/scripts/test_long_term_memory.py b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/scripts/test_long_term_memory.py new file mode 100644 index 0000000..52e2a8d --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/scripts/test_long_term_memory.py @@ -0,0 +1,29 @@ +import asyncio +import tempfile +from types import SimpleNamespace +from agent_framework.memory.long_term_memory import create_long_term_memory_manager + +async def main(): + with tempfile.TemporaryDirectory() as d: + settings = SimpleNamespace( + ENABLE_LONG_TERM_MEMORY=True, + LONG_TERM_MEMORY_PROVIDER='sqlite', + LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db', + LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory', + LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20, + LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70, + LONG_TERM_MEMORY_AUTO_EXTRACT=True, + ) + manager = create_long_term_memory_manager(settings) + first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}} + assert (await manager.persist_turn(first))['saved'] >= 3 + second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}} + values = {item.key:item.value for item in await manager.load(second)} + assert values['preferred_name'].lower() == 'cris' + assert values['preferred_language'].lower() == 'python' + assert values['current_project'].lower() == 'atlas' + isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}} + assert await manager.load(isolated) == [] + print('OK: persistência, recuperação entre sessões e isolamento validados') + +asyncio.run(main()) diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.active.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.active.yaml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.active.yaml @@ -0,0 +1 @@ +version: 1 diff --git a/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.v1.yaml b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.v1.yaml new file mode 100644 index 0000000..d2ff1aa --- /dev/null +++ b/Tuning-Performance/Transaction_Pre_Validation/agent_template_backend/workflows/devolucao_pedido.v1.yaml @@ -0,0 +1,27 @@ +name: devolucao_pedido +version: 1 +start: validar_pedido +nodes: + - id: validar_pedido + action: validar_pedido + input: + order_id: $.input.order_id + - id: registrar_devolucao + action: registrar_devolucao + retry: 1 + input: + order_id: $.input.order_id + reason: $.input.reason +edges: + - from: validar_pedido + to: registrar_devolucao + when: + path: $.nodes.validar_pedido.valid + equals: true + - from: validar_pedido + to: END + when: + path: $.nodes.validar_pedido.valid + equals: false + - from: registrar_devolucao + to: END diff --git a/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc b/libs/agent_framework/src/agent_framework/mcp/__pycache__/tool_policy.cpython-313.pyc index 028202744af80ba7c9dcde896c046958ffd4cdf3..50f832b7f88c2234f0433c81e82f95576d148368 100644 GIT binary patch delta 1796 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 ToolPolicy | None: diff --git a/libs/agent_framework/src/agent_framework/mcp/tool_router.py b/libs/agent_framework/src/agent_framework/mcp/tool_router.py index 00157a4..48c5d22 100644 --- a/libs/agent_framework/src/agent_framework/mcp/tool_router.py +++ b/libs/agent_framework/src/agent_framework/mcp/tool_router.py @@ -83,12 +83,14 @@ class MCPToolRouter: required.extend(explicit.requires) source = "tool_policies.yaml" execution = explicit.execution.model_dump() if explicit is not None else {"mode": "direct_tool", "workflow": None, "version": "active"} + pre_validation = explicit.pre_validation.model_dump() if explicit is not None else {"enabled": False, "tool": None, "fail_open": False} return { "operation_type": operation_type, "require_confirmation": confirmation_required, "requires": list(dict.fromkeys(required)), "policy_source": source, "execution": execution, + "pre_validation": pre_validation, } def validate_execution_policy( diff --git a/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc b/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc index 6f9eb512b3b75c8335dc6a9d845b5bfc00263cdb..344e40b92f11d78bcb585093b56b46008b8f240d 100644 GIT binary patch delta 12854 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 diff --git a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py index eb6eafc..71326a7 100644 --- a/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py +++ b/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py @@ -713,6 +713,83 @@ class AgentRuntimeMixin: "policy_source": "tools.yaml", } + async def _run_transaction_pre_validation( + self, + state: dict[str, Any], + *, + tool_name: str, + arguments: dict[str, Any], + policy: dict[str, Any], + emit_events: bool = True, + ) -> dict[str, Any] | None: + """Execute an optional domain-owned MCP pre-validation before confirmation. + + The framework knows only the generic contract ``eligible``. Business rules + remain in the configured MCP validator tool. No LLM is used here. + """ + cfg = policy.get("pre_validation") if isinstance(policy, dict) else None + if not isinstance(cfg, dict) or not cfg.get("enabled"): + return None + validator = str(cfg.get("tool") or "").strip() + if not validator: + return None + validation_args = dict(arguments or {}) + validation_args.pop("confirmed", None) + validation_args["target_tool"] = tool_name + if emit_events: + await self._emit_ic( + "IC.TRANSACTION_PREVALIDATION_REQUESTED", + state, + {"tool_name": tool_name, "validator_tool": validator}, + component="agent_runtime.tool_policy", + ) + result = await self._call_mcp_tool(validator, validation_args, state) + payload = result.get("result") if isinstance(result, dict) and isinstance(result.get("result"), dict) else result + eligible = payload.get("eligible") if isinstance(payload, dict) else None + if eligible is True: + state["transaction_pre_validation"] = { + "tool_name": tool_name, "validator_tool": validator, "eligible": True, "result": result + } + if emit_events: + await self._emit_ic( + "IC.TRANSACTION_PREVALIDATION_PASSED", state, + {"tool_name": tool_name, "validator_tool": validator}, + component="agent_runtime.tool_policy", + ) + return None + transport_failed = isinstance(result, dict) and result.get("ok") is False and eligible is None + if transport_failed and bool(cfg.get("fail_open")): + return None + status = str((payload or {}).get("status") or ("PREVALIDATION_ERROR" if transport_failed else "OUT_OF_SCOPE")) + state["transaction_pre_validation"] = { + "tool_name": tool_name, + "validator_tool": validator, + "eligible": False, + "status": status, + "error": (payload or {}).get("error") if isinstance(payload, dict) else None, + "terminal": True, + "result": result, + } + # A rejeição da pré-validação encerra o latch transacional imediatamente. + # A regra de negócio permanece no MCP; o framework apenas materializa o + # resultado genérico de elegibilidade e garante que o próximo turno volte + # ao roteamento normal, sem herdar COLLECTING_/WAITING_. + self._finish_active_transaction(state, "OUT_OF_SCOPE", result=result) + state["next_state"] = None + state["confirmation_required"] = False + state["confirmation_received"] = False + if emit_events: + await self._emit_ic( + "IC.TRANSACTION_PREVALIDATION_REJECTED", state, + {"tool_name": tool_name, "validator_tool": validator, "status": status, "error": (payload or {}).get("error")}, + component="agent_runtime.tool_policy", + ) + enriched = dict(result or {}) + enriched["pre_validation"] = True + enriched["target_tool"] = tool_name + enriched["transaction_status"] = "OUT_OF_SCOPE" + return enriched + def _validate_tool_execution_policy(self, tool_name: str, arguments: dict[str, Any]) -> tuple[bool, str | None]: """Aplica a mesma política central usada pelo MCPToolRouter.""" router = getattr(self, "tool_router", None) @@ -1608,6 +1685,7 @@ class AgentRuntimeMixin: "tool_policy_result", "missing_parameters", "next_state", "pending_domain_workflow", "pending_tool_clarification", "business_workflows_executed", "active_transaction", "last_transaction", "transaction_evidence", "last_transaction_evidence", "relevant_transaction_evidence", + "transaction_pre_validation", ) return {key: state.get(key) for key in keys if key in state} @@ -2017,6 +2095,12 @@ class AgentRuntimeMixin: state, tool_name=tool_name, arguments=arguments, status="COLLECTING_PARAMETERS" ) state["missing_parameters"] = [] + pre_validation_result = await self._run_transaction_pre_validation( + state, tool_name=tool_name, arguments=arguments, policy=policy, emit_events=emit_events + ) + if pre_validation_result is not None: + return [pre_validation_result] + if policy.get("require_confirmation"): waiting_state = self._waiting_state_name(state) state.update({ @@ -2190,6 +2274,13 @@ class AgentRuntimeMixin: }) return results + pre_validation_result = await self._run_transaction_pre_validation( + state, tool_name=selected_action, arguments=action_args, policy=policy, emit_events=emit_events + ) + if pre_validation_result is not None: + results.append(pre_validation_result) + return results + if policy.get("require_confirmation"): state.update({ "pending_tool_call": selected, diff --git a/mcp/servers/retail_mcp_server/__pycache__/main.cpython-313.pyc b/mcp/servers/retail_mcp_server/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..222be2fd1ed884e0b5c2b9de63116e49ce279995 GIT binary patch literal 5878 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 YG0HICX6|)uB3dDu_H5Wh`uo~{0PMPw<^TWy literal 0 HcmV?d00001 diff --git a/mcp/servers/retail_mcp_server/main.py b/mcp/servers/retail_mcp_server/main.py index 5d8d57e..e3d1ac8 100644 --- a/mcp/servers/retail_mcp_server/main.py +++ b/mcp/servers/retail_mcp_server/main.py @@ -17,6 +17,10 @@ TOOLS = { "description": "Consulta entrega e rastreamento do pedido.", "input_schema": {"order_id": "string"}, }, + "validar_cancelamento_pedido": { + "description": "Pre-valida se o pedido pode ser cancelado, sem efeitos colaterais.", + "input_schema": {"order_id": "string", "target_tool": "string"}, + }, "cancelar_pedido": { "description": "Simula o cancelamento de um pedido de varejo.", "input_schema": {"order_id": "string"}, @@ -81,6 +85,23 @@ async def call_tool(call: ToolCall): {"data": "2026-05-29", "descricao": "Em trânsito para o centro de distribuição"}, ], } + elif name == "validar_cancelamento_pedido": + order_id = str(args.get("order_id") or "PED-1001").upper() + if order_id in {"123", "PED-ENTREGUE"}: + result = { + "eligible": False, + "status": "NOT_ELIGIBLE", + "order_id": order_id, + "reason": "Pedido já entregue não pode ser cancelado por esta operação.", + "metadata": {"side_effect_free": True}, + } + else: + result = { + "eligible": True, + "status": "ELIGIBLE", + "order_id": order_id, + "metadata": {"side_effect_free": True}, + } elif name == "cancelar_pedido": result = { "protocolo": "CANCEL-2026-001", diff --git a/templates/agent_template_backend/.env b/templates/agent_template_backend/.env new file mode 100644 index 0000000..4556734 --- /dev/null +++ b/templates/agent_template_backend/.env @@ -0,0 +1,207 @@ +############################################################################### +# 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_sdk +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 +OCI_GENAI_MODEL=openai.gpt-4.1 +OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS +OCI_GENAI_PROJECT_OCID= + +#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com +#OCI_GENAI_MODEL=openai.gpt-4.1 +#OCI_GENAI_API_KEY= +#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=LATINOAMERICA-Chicago +OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q +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=Moniquinha19721972 +ADB_DSN=oradb23ai_high +ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai +ADB_WALLET_PASSWORD=Moniquinha1972 +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=autonomous +GRAPH_STORE_PROVIDER=autonomous +RAG_TOP_K=5 +EMBEDDING_PROVIDER=oci +OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0 +RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json + +############################################################################### +# Observabilidade +############################################################################### +ENABLE_LANGFUSE=true + # Opcional: verbose, compact +LANGFUSE_TRACE_MODE=compact +# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow +LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC. +LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion +LANGFUSE_IGNORE_HEALTHCHECKS=true +LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics +LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312 +LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915 +LANGFUSE_HOST=http://localhost:3005 +ENABLE_OTEL=false +OTEL_EXPORTER_OTLP_ENDPOINT= +OTEL_SERVICE_NAME=ai-agent-template +ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true +ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false + +############################################################################### +# 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 + +# Semantic route stickiness (optional). +# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE. +# There are no regexes or deterministic language rules. +ENABLE_ROUTE_STICKINESS=true +ROUTE_STICKINESS_LLM_PROFILE=route_continuity +ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90 +ROUTE_STICKINESS_HISTORY_TURNS=2 +ROUTE_STICKINESS_MAX_TOKENS=80 +HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa. +END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato. + +############################################################################### +# 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 + +############################################################################### +# LONG-TERM MEMORY +############################################################################### +ENABLE_LONG_TERM_MEMORY=true +LONG_TERM_MEMORY_PROVIDER=sqlite +LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db +LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory +# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY +# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY +LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20 +LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70 +LONG_TERM_MEMORY_AUTO_EXTRACT=true +LONG_TERM_MEMORY_INJECT_CONTEXT=true diff --git a/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/main.cpython-313.pyc index df9fb434e243c3f3acad3f21f3b6a239cc286b14..08f8cb6c796a90e7306597eb22f68a0d4d0fce17 100644 GIT binary patch delta 12364 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 delta 8833 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 VlUN=oyisCyDKu|a8eLk={{sE%U>5)Y diff --git a/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc b/templates/agent_template_backend/app/__pycache__/state.cpython-313.pyc index 7828b8e7af944e51ee205a0802a1b3d3553efd3c..0e7d19e3fb1e2dd123aea893ec1304e6f6723282 100644 GIT binary patch delta 591 zcmca7{701cGcPX}0}xnhw`aYc$Xmi>!Mw4#n{l!mBOjym;qAFlf7NB}nxOz2^dS`XG zj0Rjr6VB3tv$Wyrbl@yqFpF!lBC{~a7H$K$YD18lxsBi|jX`!Pn!x#{aF!W}6>MI_ zoo=CNxjC6Rg|S|$q$n}3I5D{-Ge0lBpeQxIEHNiDB@x8EC4wxMT9%oTnwOkG4IWg{7IBZ?V)c$=d>*^%;m6ZaF5VrvkO7 z6(#1Tmgg5`$LA+!>Q7E&w_)OEnmnKVo1=otg5VX(3nCWsU1rg{Atg7XWP$Sv;RPNu z>n=*^frR9hVFH&~bZ;oDuV7gbyg_+I#7e%4%I23@%qLIbxXtJ}xqwsG5oB$VCy4L@ w+09{-o1apelWJGw3*<5aaj_VX_`uA_$atSY@e7+Nqv{071*)GJfDEuc0J^H79RL6T delta 347 zcmew(dQX`5GcPX}0}ya#7i6hVgtif_1QAYX68<`|U6~J;VKv6}wE+vrs4N!saCv=@Es6$kz9F1t1Y!jn7jdVXXqs-`&z!=zxu3P4 ziQ5t={27QDZs|_8<*;Eo%Q(53OL5#0O?ZM#lRLieK0a8C54pE>Qi<0Azsm0RYYDMlt{Z diff --git a/templates/agent_template_backend/app/main.py b/templates/agent_template_backend/app/main.py index e036315..86530a3 100644 --- a/templates/agent_template_backend/app/main.py +++ b/templates/agent_template_backend/app/main.py @@ -461,6 +461,7 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) "mcp_tools": result.get("mcp_tools"), "mcp_results": result.get("mcp_results"), "transaction_evidence": result.get("relevant_transaction_evidence", []), + "transaction_pre_validation": result.get("transaction_pre_validation"), "business_context": business_context.model_dump(), "identity_missing": missing_identity_keys, "judges": result.get("judge_results"), diff --git a/templates/agent_template_backend/app/state.py b/templates/agent_template_backend/app/state.py index 1fce695..a7c91a0 100644 --- a/templates/agent_template_backend/app/state.py +++ b/templates/agent_template_backend/app/state.py @@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False): selected_tool_call: dict[str, Any] pending_tool_call: dict[str, Any] transaction_status: str + transaction_pre_validation: dict[str, Any] transaction_evidence: list[dict[str, Any]] last_transaction_evidence: dict[str, Any] relevant_transaction_evidence: list[dict[str, Any]] diff --git a/templates/agent_template_backend/config/mcp_parameter_mapping.yaml b/templates/agent_template_backend/config/mcp_parameter_mapping.yaml index 91d57af..2e9294e 100644 --- a/templates/agent_template_backend/config/mcp_parameter_mapping.yaml +++ b/templates/agent_template_backend/config/mcp_parameter_mapping.yaml @@ -60,6 +60,10 @@ mcp_parameter_mapping: mensagem. pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b group: 1 + validar_cancelamento_pedido: + map: + resource_key: order_id + cancelar_pedido: map: session_key: session_id diff --git a/templates/agent_template_backend/config/tool_policies.yaml b/templates/agent_template_backend/config/tool_policies.yaml index 9b1ab9e..db288ae 100644 --- a/templates/agent_template_backend/config/tool_policies.yaml +++ b/templates/agent_template_backend/config/tool_policies.yaml @@ -11,6 +11,10 @@ tool_policies: operation_type: transactional require_confirmation: true requires: [order_id] + pre_validation: + enabled: true + tool: validar_cancelamento_pedido + fail_open: false solicitar_troca: diff --git a/templates/agent_template_backend/config/tools.yaml b/templates/agent_template_backend/config/tools.yaml index f8a53ca..72c93e2 100644 --- a/templates/agent_template_backend/config/tools.yaml +++ b/templates/agent_template_backend/config/tools.yaml @@ -73,6 +73,18 @@ tools: response: mode: renderer renderer: retail.delivery + validar_cancelamento_pedido: + description: Pre-valida cancelamento do pedido sem executar efeitos transacionais. + mcp_server: retail + enabled: true + tool_type: internal + confirmation_required: false + requires: [order_id] + args_schema: + order_id: string + target_tool: string + selection_keywords: [] + cancelar_pedido: description: Simula o cancelamento de um pedido de varejo. mcp_server: retail diff --git a/templates/agent_template_backend_day_zero/app/state.py b/templates/agent_template_backend_day_zero/app/state.py index ac673d6..e6e308a 100644 --- a/templates/agent_template_backend_day_zero/app/state.py +++ b/templates/agent_template_backend_day_zero/app/state.py @@ -27,6 +27,7 @@ class AgentState(TypedDict, total=False): selected_tool_call: dict[str, Any] pending_tool_call: dict[str, Any] transaction_status: str + transaction_pre_validation: dict[str, Any] confirmation_required: bool confirmation_received: bool tool_policy_result: dict[str, Any] diff --git a/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml b/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml index 91d57af..2e9294e 100644 --- a/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml +++ b/templates/agent_template_backend_day_zero/config/mcp_parameter_mapping.yaml @@ -60,6 +60,10 @@ mcp_parameter_mapping: mensagem. pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b group: 1 + validar_cancelamento_pedido: + map: + resource_key: order_id + cancelar_pedido: map: session_key: session_id diff --git a/templates/agent_template_backend_day_zero/config/tool_policies.yaml b/templates/agent_template_backend_day_zero/config/tool_policies.yaml index 48a83d5..bbd0f64 100644 --- a/templates/agent_template_backend_day_zero/config/tool_policies.yaml +++ b/templates/agent_template_backend_day_zero/config/tool_policies.yaml @@ -11,6 +11,10 @@ tool_policies: operation_type: transactional require_confirmation: true requires: [order_id] + pre_validation: + enabled: true + tool: validar_cancelamento_pedido + fail_open: false solicitar_troca: diff --git a/templates/agent_template_backend_day_zero/config/tools.yaml b/templates/agent_template_backend_day_zero/config/tools.yaml index 2329dc7..fc1591c 100644 --- a/templates/agent_template_backend_day_zero/config/tools.yaml +++ b/templates/agent_template_backend_day_zero/config/tools.yaml @@ -61,6 +61,18 @@ tools: - rastreamento - transportadora - previsão + validar_cancelamento_pedido: + description: Pre-valida cancelamento do pedido sem executar efeitos transacionais. + mcp_server: retail + enabled: true + tool_type: internal + confirmation_required: false + requires: [order_id] + args_schema: + order_id: string + target_tool: string + selection_keywords: [] + cancelar_pedido: description: Simula o cancelamento de um pedido de varejo. mcp_server: retail diff --git a/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/test_transactional_tool_flow.cpython-313-pytest-9.0.2.pyc index 8850608fcf50b219ef097955edfdb3a28e3e93b0..d6b76da4f7589d235d6526dfcab4dd1004a71a40 100644 GIT binary patch delta 14165 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 F{T~_N67co0i~B=`^?A_Y^-!LnKH+1_62( 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 R6xFWbgRJ(N4Jl^c`M=Bq_`d)E literal 0 HcmV?d00001 diff --git a/tests/test_transactional_tool_flow.py b/tests/test_transactional_tool_flow.py index 369a1d6..67c57b4 100644 --- a/tests/test_transactional_tool_flow.py +++ b/tests/test_transactional_tool_flow.py @@ -379,3 +379,192 @@ def test_transaction_evidence_is_correlated_by_resource_identifier(): current = [{"ok": True, "tool_name": "consultar_pedido", "result": {"order_id": "123"}}] relevant = runtime.transaction_evidence_for_turn(state, current) assert [item["transaction_id"] for item in relevant] == ["tx-123"] + + +def test_tool_policy_registry_reads_pre_validation(tmp_path: Path): + config = tmp_path / "tool_policies.yaml" + config.write_text("""version: 1 +defaults: + operation_type: read_only + require_confirmation: false +tool_policies: + contestar_cobranca: + operation_type: transactional + require_confirmation: true + requires: [subject, valor] + pre_validation: + enabled: true + tool: validar_contestacao + fail_open: false +""", encoding="utf-8") + policy = ToolPolicyRegistry(str(config)).get("contestar_cobranca") + assert policy is not None + assert policy.pre_validation.enabled is True + assert policy.pre_validation.tool == "validar_contestacao" + assert policy.pre_validation.fail_open is False + + +class _PreValidationRouter(_ContestPolicyRouter): + def __init__(self): + super().__init__() + from types import SimpleNamespace + self.registry = SimpleNamespace( + tools={"contestar_cobranca": object(), "validar_contestacao": object()}, + get_tool=lambda name: SimpleNamespace( + selection_keywords=["contestar", "não contratei", "nao contratei"] if name == "contestar_cobranca" else [] + ), + ) + + def resolve_execution_policy(self, tool_name, arguments=None): + if tool_name == "contestar_cobranca": + return { + "operation_type": "transactional", + "require_confirmation": True, + "requires": ["subject", "valor"], + "policy_source": "test", + "pre_validation": {"enabled": True, "tool": "validar_contestacao", "fail_open": False}, + } + return {"operation_type": "internal", "require_confirmation": False, "requires": [], "policy_source": "test", "pre_validation": {"enabled": False}} + + +class _PreValidationRuntime(AgentRuntimeMixin): + def __init__(self, eligible: bool): + self.tool_router = _PreValidationRouter() + self.calls = [] + self.eligible = eligible + + async def _call_mcp_tool(self, tool_name, arguments, state): + self.calls.append((tool_name, dict(arguments))) + if tool_name == "validar_contestacao": + payload = ({"eligible": True, "status": "ELIGIBLE"} if self.eligible else { + "eligible": False, + "status": "OUT_OF_SCOPE", + "category": "plano", + "error": "item não elegível para contestação", + }) + return {"ok": True, "tool_name": tool_name, "result": payload} + return {"ok": True, "tool_name": tool_name, "result": {"status": "OPENED"}} + + +@pytest.mark.asyncio +async def test_pre_validation_rejects_before_confirmation_without_executing_transaction(): + runtime = _PreValidationRuntime(eligible=False) + state = { + "user_text": "quero contestar TIM CTRL no valor de R$ 71,99", + "sanitized_input": "quero contestar TIM CTRL no valor de R$ 71,99", + "mcp_tools": ["contestar_cobranca"], + "route": "contestacao_agent", + "intent": "contas_contestation", + "context": {"tool_arguments": {"subject": "TIM CTRL Redes Sociais 8.0", "valor": 71.99}}, + } + result = await runtime.execute_tools_for_intent(state) + assert [name for name, _ in runtime.calls] == ["validar_contestacao"] + assert result[-1]["pre_validation"] is True + assert result[-1]["transaction_status"] == "OUT_OF_SCOPE" + assert state["transaction_status"] == "OUT_OF_SCOPE" + assert state.get("pending_tool_call") in ({}, None) + assert state["confirmation_required"] is False + + +@pytest.mark.asyncio +async def test_pre_validation_passes_then_waits_for_confirmation(): + runtime = _PreValidationRuntime(eligible=True) + state = { + "user_text": "quero contestar serviço X no valor de R$ 10,00", + "sanitized_input": "quero contestar serviço X no valor de R$ 10,00", + "mcp_tools": ["contestar_cobranca"], + "route": "contestacao_agent", + "intent": "contas_contestation", + "context": {"tool_arguments": {"subject": "serviço X", "valor": 10.0}}, + } + result = await runtime.execute_tools_for_intent(state) + assert [name for name, _ in runtime.calls] == ["validar_contestacao"] + assert result[-1]["awaiting_confirmation"] is True + assert state["transaction_status"] == "AWAITING_CONFIRMATION" + assert state["pending_tool_call"]["tool_name"] == "contestar_cobranca" + +@pytest.mark.asyncio +async def test_pre_validation_runs_after_last_required_parameter_before_confirmation(): + runtime = _PreValidationRuntime(eligible=False) + state = { + "user_text": "R$ 71,99", + "sanitized_input": "R$ 71,99", + "route": "contestacao_agent", + "intent": "state:COLLECTING_CONTESTACAO_PARAMETERS", + "transaction_status": "COLLECTING_PARAMETERS", + "selected_tool_call": { + "tool_name": "contestar_cobranca", + "arguments": {"subject": "TIM CTRL Redes Sociais 8.0", "motivo": "não contratei"}, + }, + "context": {"tool_arguments": {"valor": 71.99}}, + } + result = await runtime.execute_tools_for_intent(state, tools=[]) + assert [name for name, _ in runtime.calls] == ["validar_contestacao"] + assert runtime.calls[0][1]["subject"] == "TIM CTRL Redes Sociais 8.0" + assert runtime.calls[0][1]["valor"] == 71.99 + assert result[-1]["pre_validation"] is True + assert state["transaction_status"] == "OUT_OF_SCOPE" + assert state["confirmation_required"] is False + assert not state.get("pending_tool_call") + + +def test_transaction_state_patch_exposes_prevalidation_and_terminal_lifecycle(): + runtime = _PreValidationRuntime(eligible=False) + state = { + "transaction_status": "OUT_OF_SCOPE", + "next_state": None, + "selected_tool_call": {}, + "pending_tool_call": {}, + "confirmation_required": False, + "confirmation_received": False, + "transaction_pre_validation": { + "tool_name": "contestar_cobranca", + "validator_tool": "validar_contestacao", + "eligible": False, + "status": "OUT_OF_SCOPE", + "terminal": True, + }, + } + patch = runtime.transaction_state_patch(state) + assert patch["transaction_pre_validation"]["eligible"] is False + assert patch["transaction_pre_validation"]["status"] == "OUT_OF_SCOPE" + assert patch["next_state"] is None + assert patch["transaction_status"] == "OUT_OF_SCOPE" + assert patch["confirmation_required"] is False + + +@pytest.mark.asyncio +async def test_pre_validation_rejection_clears_collecting_latches_and_is_exposed_in_patch(): + runtime = _PreValidationRuntime(eligible=False) + state = { + "user_text": "R$ 71,99", + "sanitized_input": "R$ 71,99", + "route": "contestacao_agent", + "intent": "state:COLLECTING_CONTESTACAO_PARAMETERS", + "next_state": "COLLECTING_CONTESTACAO_PARAMETERS", + "transaction_status": "COLLECTING_PARAMETERS", + "selected_tool_call": { + "tool_name": "contestar_cobranca", + "arguments": {"subject": "TIM CTRL Redes Sociais 8.0", "motivo": "não contratei"}, + }, + "pending_tool_call": {}, + "missing_parameters": ["valor"], + "confirmation_required": False, + "context": {"tool_arguments": {"valor": 71.99}}, + } + result = await runtime.execute_tools_for_intent(state, tools=[]) + assert result[-1]["pre_validation"] is True + assert state["transaction_status"] == "OUT_OF_SCOPE" + assert state["next_state"] is None + assert state["active_transaction"] is None + assert state["selected_tool_call"] == {} + assert state["pending_tool_call"] == {} + assert state["missing_parameters"] == [] + assert state["confirmation_required"] is False + assert state["confirmation_received"] is False + assert state["transaction_pre_validation"]["eligible"] is False + assert state["transaction_pre_validation"]["status"] == "OUT_OF_SCOPE" + assert state["transaction_pre_validation"]["terminal"] is True + patch = runtime.transaction_state_patch(state) + assert patch["transaction_pre_validation"] == state["transaction_pre_validation"] + assert patch["next_state"] is None