From 8bb04cf1d334265bdf1cd9eec8736a24f0d88cd6 Mon Sep 17 00:00:00 2001 From: "cristiano.hoshikawa" Date: Thu, 6 Aug 2026 10:53:55 -0300 Subject: [PATCH] Authorization Feature --- Implementando_Basic_Auth.md | 987 ++++ .../Authentication/DISCLAIMER.md | 24 + .../Authentication/IMPLEMENTACAO.md | 29 + Tuning-Performance/Authentication/README.md | 19 + .../.env.example | 39 + .../Dockerfile | 6 + .../README.md | 4222 +++++++++++++++++ .../README_ENTERPRISE_TEMPLATE.md | 54 + .../app/__init__.py | 0 .../app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 184 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 32754 bytes ...mcp_gateway_client_factory.cpython-313.pyc | Bin 0 -> 1032 bytes .../app/__pycache__/state.cpython-313.pyc | Bin 0 -> 2565 bytes .../app/agents/README.md | 15 + .../__pycache__/billing_agent.cpython-313.pyc | Bin 0 -> 5255 bytes .../__pycache__/orders_agent.cpython-313.pyc | Bin 0 -> 5163 bytes .../__pycache__/product_agent.cpython-313.pyc | Bin 0 -> 5279 bytes .../__pycache__/prompting.cpython-313.pyc | Bin 0 -> 1181 bytes .../__pycache__/runtime.cpython-313.pyc | Bin 0 -> 383 bytes .../__pycache__/support_agent.cpython-313.pyc | Bin 0 -> 5175 bytes .../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 | 7 + .../app/agents/support_agent.py | 129 + .../app/examples/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 255 bytes .../__pycache__/grl_examples.cpython-313.pyc | Bin 0 -> 1837 bytes .../__pycache__/ic_examples.cpython-313.pyc | Bin 0 -> 1793 bytes .../__pycache__/mcp_examples.cpython-313.pyc | Bin 0 -> 1935 bytes .../__pycache__/noc_examples.cpython-313.pyc | Bin 0 -> 1882 bytes .../observer_examples.cpython-313.pyc | Bin 0 -> 1391 bytes .../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 + .../app/main.py | 558 +++ .../app/mcp_gateway_client_factory.py | 16 + .../app/observability/__init__.py | 0 .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 198 bytes .../telemetry_observer.cpython-313.pyc | Bin 0 -> 4809 bytes .../app/observability/telemetry_observer.py | 84 + .../app/state.py | 53 + .../app/workflow_actions/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 238 bytes .../__pycache__/devolucao.cpython-313.pyc | Bin 0 -> 1016 bytes .../app/workflow_actions/devolucao.py | 13 + .../__pycache__/agent_graph.cpython-313.pyc | Bin 0 -> 54261 bytes .../app/workflows/agent_graph.py | 887 ++++ .../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/authentication.example.yaml | 39 + .../config/guardrails.yaml | 12 + .../config/identity.yaml | 55 + .../config/judges.yaml | 18 + .../config/mcp_parameter_mapping.yaml | 92 + .../config/mcp_servers.docker.yaml | 12 + .../config/mcp_servers.yaml | 30 + .../config/prompt_policy.yaml | 19 + .../config/routing.yaml | 128 + .../config/tool_policies.yaml | 26 + .../config/tools.yaml | 101 + ...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/MANUAL_AUTENTICACAO.md | 253 + .../docs/TESTE_LONG_TERM_MEMORY.md | 82 + .../docs/VALIDACAO_BACKEND_IC_NOC_GRL.md | 62 + .../docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt | 3 + .../llm_profiles.yaml | 80 + .../requirements.txt | 25 + .../scripts/generate_secret_hash.py | 23 + .../scripts/test_long_term_memory.py | 29 + .../workflows/devolucao_pedido.active.yaml | 1 + .../workflows/devolucao_pedido.v1.yaml | 27 + .../authentication_policies.example.yaml | 39 + .../governance_middleware.cpython-313.pyc | Bin 0 -> 4700 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 17489 bytes .../app/__pycache__/settings.cpython-313.pyc | Bin 0 -> 3272 bytes .../governance_loader.cpython-313.pyc | Bin 0 -> 925 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 142 bytes .../__pycache__/audit.cpython-313.pyc | Bin 0 -> 934 bytes .../evaluation_hooks.cpython-313.pyc | Bin 0 -> 881 bytes .../model_policies.cpython-313.pyc | Bin 0 -> 3383 bytes .../__pycache__/rate_limit.cpython-313.pyc | Bin 0 -> 2594 bytes .../__pycache__/usage.cpython-313.pyc | Bin 0 -> 963 bytes apps/agent_gateway/app/main.py | 2 + .../governed_proxy_example.cpython-313.pyc | Bin 0 -> 2404 bytes .../config/authentication.example.yaml | 39 + .../app/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 129 bytes .../app/__pycache__/main.cpython-313.pyc | Bin 0 -> 28096 bytes apps/mcp_gateway/app/main.py | 2 + .../config/authentication.example.yaml | 39 + libs/agent_framework/pyproject.toml | 3 +- .../__pycache__/__init__.cpython-313.pyc | Bin 266 -> 267 bytes .../__pycache__/__init__.cpython-313.pyc | Bin 195 -> 196 bytes .../__pycache__/settings.cpython-313.pyc | Bin 12231 -> 12232 bytes .../src/agent_framework/security/__init__.py | 40 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 1188 bytes .../authentication.cpython-313.pyc | Bin 0 -> 15408 bytes .../__pycache__/factory.cpython-313.pyc | Bin 0 -> 7977 bytes .../__pycache__/installer.cpython-313.pyc | Bin 0 -> 5595 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 0 -> 8879 bytes .../security/authentication.py | 190 + .../src/agent_framework/security/factory.py | 112 + .../src/agent_framework/security/installer.py | 70 + .../agent_framework/security/middleware.py | 100 + .../conftest.cpython-313-pytest-9.0.2.pyc | Bin 788 -> 789 bytes ...uthentication.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 8555 bytes ...tion_policies.cpython-313-pytest-9.0.2.pyc | Bin 0 -> 9458 bytes tests/unit/test_authentication.py | 50 + tests/unit/test_authentication_policies.py | 69 + 124 files changed, 10144 insertions(+), 1 deletion(-) create mode 100644 Implementando_Basic_Auth.md create mode 100644 Tuning-Performance/Authentication/DISCLAIMER.md create mode 100644 Tuning-Performance/Authentication/IMPLEMENTACAO.md create mode 100644 Tuning-Performance/Authentication/README.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/.env.example create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/Dockerfile create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/README.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/README_ENTERPRISE_TEMPLATE.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/__init__.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/__init__.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/main.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/state.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/README.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/billing_agent.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/orders_agent.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/product_agent.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/prompting.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/runtime.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/support_agent.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/billing_agent.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/orders_agent.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/product_agent.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/prompting.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/runtime.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/support_agent.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__init__.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/__init__.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/grl_examples.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/ic_examples.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/mcp_examples.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/noc_examples.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/observer_examples.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/grl_examples.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/ic_examples.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/mcp_examples.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/noc_examples.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/observer_examples.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/main.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/mcp_gateway_client_factory.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__init__.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/__init__.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/telemetry_observer.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/telemetry_observer.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/state.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__init__.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__pycache__/__init__.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__pycache__/devolucao.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/devolucao.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/__pycache__/agent_graph.cpython-313.pyc create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/agent_graph.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/guardrails.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/judges.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/prompt_policy.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/guardrails.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/judges.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/prompt_policy.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/authentication.example.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/guardrails.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/identity.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/judges.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_parameter_mapping.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_servers.docker.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_servers.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/prompt_policy.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/routing.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/tool_policies.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/config/tools.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/MANUAL_AUTENTICACAO.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/TESTE_LONG_TERM_MEMORY.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/llm_profiles.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/test_long_term_memory.py create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.active.yaml create mode 100644 Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.v1.yaml create mode 100644 Tuning-Performance/Authentication/authentication_policies.example.yaml create mode 100644 apps/agent_gateway/app/__pycache__/governance_middleware.cpython-313.pyc create mode 100644 apps/agent_gateway/app/__pycache__/main.cpython-313.pyc create mode 100644 apps/agent_gateway/app/__pycache__/settings.cpython-313.pyc create mode 100644 apps/agent_gateway/app/config/__pycache__/governance_loader.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/audit.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/evaluation_hooks.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/model_policies.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/rate_limit.cpython-313.pyc create mode 100644 apps/agent_gateway/app/governance/__pycache__/usage.cpython-313.pyc create mode 100644 apps/agent_gateway/app/routes/__pycache__/governed_proxy_example.cpython-313.pyc create mode 100644 apps/agent_gateway/config/authentication.example.yaml create mode 100644 apps/mcp_gateway/app/__pycache__/__init__.cpython-313.pyc create mode 100644 apps/mcp_gateway/app/__pycache__/main.cpython-313.pyc create mode 100644 apps/mcp_gateway/config/authentication.example.yaml create mode 100644 libs/agent_framework/src/agent_framework/security/__init__.py create mode 100644 libs/agent_framework/src/agent_framework/security/__pycache__/__init__.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/security/__pycache__/authentication.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/security/__pycache__/factory.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/security/__pycache__/installer.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/security/__pycache__/middleware.cpython-313.pyc create mode 100644 libs/agent_framework/src/agent_framework/security/authentication.py create mode 100644 libs/agent_framework/src/agent_framework/security/factory.py create mode 100644 libs/agent_framework/src/agent_framework/security/installer.py create mode 100644 libs/agent_framework/src/agent_framework/security/middleware.py create mode 100644 tests/unit/__pycache__/test_authentication.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/unit/__pycache__/test_authentication_policies.cpython-313-pytest-9.0.2.pyc create mode 100644 tests/unit/test_authentication.py create mode 100644 tests/unit/test_authentication_policies.py diff --git a/Implementando_Basic_Auth.md b/Implementando_Basic_Auth.md new file mode 100644 index 0000000..d6ff282 --- /dev/null +++ b/Implementando_Basic_Auth.md @@ -0,0 +1,987 @@ +# Implementando Basic Auth + +Para validar **todo o circuito com Basic Auth**, você precisa configurar três relações distintas: + +```text +Cliente de teste + └─ Basic Auth A ─► Agent Gateway :8010 + └─ Basic Auth B ─► Agent Backend :8000 + └─ Basic Auth C ─► MCP Gateway :8300 +``` + +Há um detalhe importante: no pacote atual, a autenticação Basic já funciona para chamadas **de entrada**, mas os clientes internos ainda não enviam Basic Auth: + +* `Agent Gateway → Agent Backend` não envia credencial; +* `Agent Backend → MCP Gateway` envia apenas Bearer Token. + +Portanto, para testar o circuito inteiro com Basic Auth, faça os dois pequenos ajustes de código descritos abaixo. + +--- + +# 1. Preparar o ambiente + +Considere que o ZIP foi extraído em: + +```bash +cd agent_framework_oci_authentication_v2_1 +``` + +Crie um único ambiente virtual para facilitar o teste: + +```bash +python -m venv .venv +source .venv/bin/activate +``` + +No Windows PowerShell: + +```powershell +python -m venv .venv +.\.venv\Scripts\Activate.ps1 +``` + +Instale o framework e as dependências dos três componentes: + +```bash +pip install -U pip + +pip install -e ./libs/agent_framework + +pip install \ + -r ./Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt \ + -r ./apps/agent_gateway/requirements.txt \ + -r ./apps/mcp_gateway/requirements.txt +``` + +Confirme a importação: + +```bash +python -c "from agent_framework.security import install_authentication; print('framework ok')" +``` + +--- + +# 2. Criar três pares de Client ID e Secret + +Use credenciais diferentes para cada trecho. Para teste local: + +| Fluxo | Client ID | Secret de teste | +| ----------------------- | -------------------- | --------------------------- | +| Cliente → Agent Gateway | `tia-test` | `TiaGateway-Test-2026!` | +| Agent Gateway → Backend | `agent-gateway-test` | `GatewayBackend-Test-2026!` | +| Backend → MCP Gateway | `agent-backend-test` | `BackendMcp-Test-2026!` | + +Esses valores são apenas para ambiente local. Não os reutilize em produção. + +## Gerar os hashes + +O script está em: + +```text +Tuning-Performance/Authentication/ + agent_template_backend_authentication/ + scripts/generate_secret_hash.py +``` + +Execute: + +```bash +python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \ + --secret 'TiaGateway-Test-2026!' +``` + +Depois: + +```bash +python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \ + --secret 'GatewayBackend-Test-2026!' +``` + +E: + +```bash +python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \ + --secret 'BackendMcp-Test-2026!' +``` + +Você receberá três valores semelhantes a: + +```text +pbkdf2_sha256:310000:: +``` + +Guarde-os temporariamente: + +```bash +HASH_CLIENT_GATEWAY='pbkdf2_sha256:310000:...' +HASH_GATEWAY_BACKEND='pbkdf2_sha256:310000:...' +HASH_BACKEND_MCP='pbkdf2_sha256:310000:...' +``` + +O hash muda a cada execução porque o salt é aleatório. Isso é esperado. + +--- + +# 3. Configurar o Agent Gateway + +Entre no diretório: + +```bash +cd apps/agent_gateway +``` + +Copie o exemplo: + +```bash +cp .env.example .env +``` + +Adicione ao final do `.env`: + +```env +# Entrada: cliente/TIA -> Agent Gateway +AGENT_GATEWAY_AUTH_ENABLED=true +AGENT_GATEWAY_AUTH_MODE=basic +AGENT_GATEWAY_AUTH_BASIC_CLIENT_ID=tia-test +AGENT_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_CLIENT_GATEWAY +AGENT_GATEWAY_AUTH_BASIC_REALM=agent-gateway + +AGENT_GATEWAY_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc +AGENT_GATEWAY_AUTH_PUBLIC_PREFIXES= + +# Saída: Agent Gateway -> Agent Backend +BACKEND_AUTH_MODE=basic +BACKEND_AUTH_CLIENT_ID=agent-gateway-test +BACKEND_AUTH_SECRET=GatewayBackend-Test-2026! +``` + +Não coloque aspas no `.env`: + +```env +BACKEND_AUTH_SECRET=GatewayBackend-Test-2026! +``` + +O arquivo de backends já aponta o backend Contas para: + +```yaml +contas: + url: http://localhost:8000 +``` + +Arquivo: + +```text +apps/agent_gateway/config/backends.yaml +``` + +Para este teste, mantenha apenas o backend `contas` ou force o backend no payload. Caso contrário, pedidos sobre ofertas e suporte podem ser roteados para portas em que nenhum backend está rodando. + +--- + +# 4. Fazer o Agent Gateway enviar Basic Auth ao backend + +Abra: + +```text +libs/agent_framework/src/agent_framework/global_supervisor/client.py +``` + +Substitua a classe `BackendClient` por uma versão que aceite autenticação Basic. + +No início do arquivo, adicione: + +```python +import os +``` + +Altere o construtor: + +```python +class BackendClient: + def __init__( + self, + timeout_seconds: float = 120.0, + basic_client_id: str | None = None, + basic_secret: str | None = None, + ): + self.timeout_seconds = timeout_seconds + self.basic_client_id = basic_client_id + self.basic_secret = basic_secret + + def _auth(self) -> httpx.BasicAuth | None: + if self.basic_client_id and self.basic_secret: + return httpx.BasicAuth( + username=self.basic_client_id, + password=self.basic_secret, + ) + return None +``` + +No método `call_message`, troque: + +```python +resp = await client.post(url, json=payload) +``` + +por: + +```python +resp = await client.post( + url, + json=payload, + auth=self._auth(), +) +``` + +No método `health`, você pode manter `/health` público. Caso queira enviar autenticação também, use: + +```python +resp = await client.get(url, auth=self._auth()) +``` + +Agora abra: + +```text +apps/agent_gateway/app/main.py +``` + +Adicione: + +```python +import os +``` + +Troque: + +```python +backend_client = BackendClient( + timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS +) +``` + +por: + +```python +backend_client = BackendClient( + timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS, + basic_client_id=os.getenv("BACKEND_AUTH_CLIENT_ID"), + basic_secret=os.getenv("BACKEND_AUTH_SECRET"), +) +``` + +Isso implementa: + +```text +Agent Gateway → Agent Backend +Authorization: Basic base64(agent-gateway-test:GatewayBackend-Test-2026!) +``` + +--- + +# 5. Configurar o Agent Backend autenticado + +Entre no diretório: + +```bash +cd Tuning-Performance/Authentication/agent_template_backend_authentication +``` + +Copie o exemplo: + +```bash +cp .env.example .env +``` + +Ajuste a seção de autenticação: + +```env +# Entrada: Agent Gateway -> Agent Backend +AGENT_AUTH_ENABLED=true +AGENT_AUTH_MODE=basic +AGENT_AUTH_BASIC_CLIENT_ID=agent-gateway-test +AGENT_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_GATEWAY_BACKEND +AGENT_AUTH_BASIC_REALM=agent-contas + +AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc +AGENT_AUTH_PUBLIC_PREFIXES= +``` + +Para usar o MCP Gateway: + +```env +MCP_GATEWAY_ENABLED=true +MCP_GATEWAY_URL=http://localhost:8300 +MCP_GATEWAY_TIMEOUT_SECONDS=60 + +# Saída: Agent Backend -> MCP Gateway +MCP_GATEWAY_AUTH_MODE=basic +MCP_GATEWAY_BASIC_CLIENT_ID=agent-backend-test +MCP_GATEWAY_BASIC_SECRET=BackendMcp-Test-2026! +``` + +Para evitar dependências externas durante o primeiro teste, configure também: + +```env +LLM_PROVIDER=mock +ENABLE_LANGFUSE=false +ENABLE_ANALYTICS=false + +SESSION_REPOSITORY_PROVIDER=memory +MEMORY_REPOSITORY_PROVIDER=memory +CHECKPOINT_REPOSITORY_PROVIDER=memory +CACHE_PROVIDER=memory +USAGE_REPOSITORY_PROVIDER=memory +``` + +Os nomes exatos de alguns providers podem depender do arquivo de configuração atual do framework. Caso o `.env.example` já contenha valores locais ou mock, preserve-os. + +--- + +# 6. Fazer o Backend enviar Basic Auth ao MCP Gateway + +Abra: + +```text +libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py +``` + +Substitua a implementação por: + +```python +from __future__ import annotations + +import base64 +from typing import Any + +import httpx + + +class MCPGatewayClient: + def __init__( + self, + base_url: str, + token: str | None = None, + timeout_seconds: int = 60, + auth_mode: str | None = None, + basic_client_id: str | None = None, + basic_secret: str | None = None, + ): + self.base_url = base_url.rstrip("/") + self.token = token + self.timeout_seconds = timeout_seconds + self.auth_mode = (auth_mode or "").strip().lower() + self.basic_client_id = basic_client_id + self.basic_secret = basic_secret + + def _headers(self) -> dict[str, str]: + if ( + self.auth_mode == "basic" + and self.basic_client_id + and self.basic_secret + ): + raw = f"{self.basic_client_id}:{self.basic_secret}".encode("utf-8") + encoded = base64.b64encode(raw).decode("ascii") + return {"Authorization": f"Basic {encoded}"} + + if self.token: + return {"Authorization": f"Bearer {self.token}"} + + return {} + + async def list_tools(self) -> dict[str, Any]: + async with httpx.AsyncClient( + timeout=self.timeout_seconds + ) as client: + response = await client.get( + f"{self.base_url}/v1/tools", + headers=self._headers(), + ) + response.raise_for_status() + return response.json() + + async def invoke_tool( + self, + *, + tenant_id: str, + agent_id: str, + channel: str | None, + tool_name: str, + arguments: dict[str, Any] | None = None, + business_context: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + ) -> dict[str, Any]: + payload = { + "tenant_id": tenant_id, + "agent_id": agent_id, + "channel": channel, + "tool_name": tool_name, + "arguments": arguments or {}, + "business_context": business_context or {}, + "metadata": metadata or {}, + } + + async with httpx.AsyncClient( + timeout=self.timeout_seconds + ) as client: + response = await client.post( + f"{self.base_url}/v1/tools/{tool_name}/invoke", + json=payload, + headers=self._headers(), + ) + response.raise_for_status() + return response.json() +``` + +Agora abra: + +```text +libs/agent_framework/src/agent_framework/mcp/tool_router.py +``` + +Localize: + +```python +MCPGatewayClient( + base_url=getattr( + settings, + "MCP_GATEWAY_URL", + "http://localhost:8300", + ), + token=getattr(settings, "MCP_GATEWAY_TOKEN", None), + timeout_seconds=getattr( + settings, + "MCP_GATEWAY_TIMEOUT_SECONDS", + settings.MCP_TOOL_TIMEOUT_SECONDS, + ), +) +``` + +Altere para: + +```python +MCPGatewayClient( + base_url=getattr( + settings, + "MCP_GATEWAY_URL", + "http://localhost:8300", + ), + token=getattr(settings, "MCP_GATEWAY_TOKEN", None), + timeout_seconds=getattr( + settings, + "MCP_GATEWAY_TIMEOUT_SECONDS", + settings.MCP_TOOL_TIMEOUT_SECONDS, + ), + auth_mode=getattr( + settings, + "MCP_GATEWAY_AUTH_MODE", + None, + ), + basic_client_id=getattr( + settings, + "MCP_GATEWAY_BASIC_CLIENT_ID", + None, + ), + basic_secret=getattr( + settings, + "MCP_GATEWAY_BASIC_SECRET", + None, + ), +) +``` + +Adicione estes campos em: + +```text +libs/agent_framework/src/agent_framework/config/settings.py +``` + +Próximo das configurações existentes de MCP Gateway: + +```python +MCP_GATEWAY_AUTH_MODE: str | None = None +MCP_GATEWAY_BASIC_CLIENT_ID: str | None = None +MCP_GATEWAY_BASIC_SECRET: str | None = None +``` + +Há também uma factory local em: + +```text +Tuning-Performance/Authentication/ + agent_template_backend_authentication/ + app/mcp_gateway_client_factory.py +``` + +Ajuste para: + +```python +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") + ), + auth_mode=os.getenv("MCP_GATEWAY_AUTH_MODE"), + basic_client_id=os.getenv( + "MCP_GATEWAY_BASIC_CLIENT_ID" + ), + basic_secret=os.getenv( + "MCP_GATEWAY_BASIC_SECRET" + ), + ) +``` + +--- + +# 7. Configurar o MCP Gateway + +Entre no diretório: + +```bash +cd apps/mcp_gateway +``` + +Crie `.env`: + +```bash +cp .env.example .env +``` + +Adicione: + +```env +# Entrada: Agent Backend -> MCP Gateway +MCP_GATEWAY_AUTH_ENABLED=true +MCP_GATEWAY_AUTH_MODE=basic +MCP_GATEWAY_AUTH_BASIC_CLIENT_ID=agent-backend-test +MCP_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_BACKEND_MCP +MCP_GATEWAY_AUTH_BASIC_REALM=mcp-gateway + +MCP_GATEWAY_AUTH_PUBLIC_PATHS=/health,/ready,/docs,/openapi.json,/redoc +MCP_GATEWAY_AUTH_PUBLIC_PREFIXES= + +MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml +``` + +## Desabilitar o mecanismo Bearer legado + +O MCP Gateway ainda possui um segundo mecanismo antigo, configurado dentro de: + +```text +apps/mcp_gateway/config/mcp_gateway.yaml +``` + +Localize a seção: + +```yaml +auth: + enabled: true +``` + +Altere para: + +```yaml +auth: + enabled: false +``` + +Isso é necessário porque o novo middleware já faz a autenticação Basic. Caso o `auth_check()` legado continue habilitado, a requisição passará pelo Basic e depois será rejeitada por não possuir Bearer Token. + +--- + +# 8. Subir os componentes + +Use quatro terminais. + +## Terminal 1 — MCP Servers + +O MCP Gateway precisa ter pelo menos um servidor MCP disponível para demonstrar uma chamada real. + +Na raiz do projeto: + +```bash +source .venv/bin/activate +``` + +Suba o servidor telecom: + +```bash +uvicorn mcp.servers.telecom_mcp_server.main:app \ + --host 0.0.0.0 \ + --port 8100 \ + --reload +``` + +Em outro terminal, caso queira também o retail: + +```bash +uvicorn mcp.servers.retail_mcp_server.main:app \ + --host 0.0.0.0 \ + --port 8200 \ + --reload +``` + +Confira as URLs configuradas em: + +```text +apps/mcp_gateway/config/mcp_gateway.yaml +``` + +Para execução local, devem apontar para: + +```yaml +url: http://localhost:8100 +``` + +e: + +```yaml +url: http://localhost:8200 +``` + +--- + +## Terminal 2 — MCP Gateway + +```bash +cd apps/mcp_gateway +source ../../.venv/bin/activate +``` + +Suba usando `--env-file`. Isso é importante porque o middleware lê variáveis com `os.getenv()`: + +```bash +uvicorn app.main:app \ + --host 0.0.0.0 \ + --port 8300 \ + --reload \ + --env-file .env +``` + +Teste a saúde pública: + +```bash +curl http://localhost:8300/health +``` + +Teste um endpoint protegido sem credencial: + +```bash +curl -i http://localhost:8300/v1/tools +``` + +Esperado: + +```text +HTTP/1.1 401 Unauthorized +``` + +Teste com Basic Auth: + +```bash +curl -i \ + -u 'agent-backend-test:BackendMcp-Test-2026!' \ + http://localhost:8300/v1/tools +``` + +Esperado: + +```text +HTTP/1.1 200 OK +``` + +--- + +## Terminal 3 — Agent Backend + +```bash +cd Tuning-Performance/Authentication/agent_template_backend_authentication +source ../../../.venv/bin/activate +``` + +Suba: + +```bash +uvicorn app.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --env-file .env +``` + +Teste saúde: + +```bash +curl http://localhost:8000/health +``` + +Teste endpoint protegido sem credencial: + +```bash +curl -i http://localhost:8000/agents +``` + +Esperado: + +```text +HTTP/1.1 401 Unauthorized +``` + +Teste com a credencial usada pelo Agent Gateway: + +```bash +curl -i \ + -u 'agent-gateway-test:GatewayBackend-Test-2026!' \ + http://localhost:8000/agents +``` + +Esperado: + +```text +HTTP/1.1 200 OK +``` + +Teste mensagem diretamente: + +```bash +curl -X POST http://localhost:8000/gateway/message \ + -u 'agent-gateway-test:GatewayBackend-Test-2026!' \ + -H 'Content-Type: application/json' \ + -d '{ + "channel": "web", + "agent_id": "telecom_contas", + "tenant_id": "default", + "payload": { + "text": "Quero consultar minha fatura", + "session_id": "teste-backend-001", + "user_id": "user-001", + "customer_id": "12345", + "message_id": "msg-001" + } + }' +``` + +--- + +## Terminal 4 — Agent Gateway + +```bash +cd apps/agent_gateway +source ../../.venv/bin/activate +``` + +Suba: + +```bash +uvicorn app.main:app \ + --host 0.0.0.0 \ + --port 8010 \ + --reload \ + --env-file .env +``` + +Teste saúde: + +```bash +curl http://localhost:8010/health +``` + +Teste endpoint protegido sem credencial: + +```bash +curl -i http://localhost:8010/backends +``` + +Esperado: + +```text +HTTP/1.1 401 Unauthorized +``` + +Teste com a credencial externa: + +```bash +curl -i \ + -u 'tia-test:TiaGateway-Test-2026!' \ + http://localhost:8010/backends +``` + +Esperado: + +```text +HTTP/1.1 200 OK +``` + +--- + +# 9. Validar o circuito completo + +Force o backend `contas` para evitar que o roteador selecione um backend não iniciado: + +```bash +curl -X POST http://localhost:8010/gateway/message \ + -u 'tia-test:TiaGateway-Test-2026!' \ + -H 'Content-Type: application/json' \ + -d '{ + "channel": "web", + "backend_id": "contas", + "tenant_id": "default", + "agent_id": "telecom_contas", + "session_id": "circuito-basic-001", + "payload": { + "text": "Quero consultar minha fatura", + "session_id": "circuito-basic-001", + "user_id": "user-001", + "customer_id": "12345", + "message_id": "msg-circuito-001" + } + }' +``` + +O circuito esperado é: + +```text +curl + │ Basic tia-test + ▼ +Agent Gateway :8010 + │ Basic agent-gateway-test + ▼ +Agent Backend :8000 + │ Basic agent-backend-test + ▼ +MCP Gateway :8300 + ▼ +MCP Server :8100 ou :8200 +``` + +--- + +# 10. Como comprovar cada autenticação + +Faça testes negativos em cada trecho. + +## Secret externo incorreto + +```bash +curl -i \ + -u 'tia-test:senha-errada' \ + http://localhost:8010/backends +``` + +Resultado esperado: + +```text +401 Unauthorized +``` + +## Secret do gateway para backend incorreto + +Altere temporariamente no `apps/agent_gateway/.env`: + +```env +BACKEND_AUTH_SECRET=senha-errada +``` + +Reinicie o Agent Gateway e envie uma mensagem. + +O gateway deverá retornar erro de backend, normalmente: + +```text +502 Bad Gateway +``` + +O erro interno será originado por um: + +```text +401 Unauthorized +``` + +do Agent Backend. + +## Secret do backend para MCP incorreto + +Altere temporariamente: + +```env +MCP_GATEWAY_BASIC_SECRET=senha-errada +``` + +Reinicie o backend e execute uma frase que acione uma ferramenta MCP. + +O backend deverá registrar falha na chamada ao MCP Gateway com: + +```text +401 Unauthorized +``` + +--- + +# 11. Verificação rápida de portas + +No Linux ou WSL: + +```bash +ss -lntp | grep -E ':8000|:8010|:8100|:8200|:8300' +``` + +No Windows PowerShell: + +```powershell +Get-NetTCPConnection -State Listen | + Where-Object LocalPort -in 8000,8010,8100,8200,8300 | + Sort-Object LocalPort +``` + +Você deverá ver: + +```text +8000 Agent Backend +8010 Agent Gateway +8100 Telecom MCP Server +8200 Retail MCP Server +8300 MCP Gateway +``` + +## Observação importante + +O segredo original precisa existir no componente cliente: + +```text +TIA ou curl: + TiaGateway-Test-2026! + +Agent Gateway: + GatewayBackend-Test-2026! + +Agent Backend: + BackendMcp-Test-2026! +``` + +Os componentes servidores armazenam apenas os hashes: + +```text +Agent Gateway: + hash de TiaGateway-Test-2026! + +Agent Backend: + hash de GatewayBackend-Test-2026! + +MCP Gateway: + hash de BackendMcp-Test-2026! +``` + +Em produção, os segredos originais e hashes devem vir de Vault ou Kubernetes Secret, não de arquivos `.env`. diff --git a/Tuning-Performance/Authentication/DISCLAIMER.md b/Tuning-Performance/Authentication/DISCLAIMER.md new file mode 100644 index 0000000..2c815c5 --- /dev/null +++ b/Tuning-Performance/Authentication/DISCLAIMER.md @@ -0,0 +1,24 @@ +# Disclaimer — template de autenticação + +Este conteúdo é um **template de referência técnica** criado para demonstrar como integrar mecanismos genéricos de autenticação ao Agent Framework OCI, ao Agent Gateway, ao MCP Gateway e a aplicações FastAPI independentes. + +O código, os arquivos YAML, as variáveis de ambiente, os providers, as políticas por rota e os exemplos de deployment **não constituem uma implementação final ou automaticamente adequada para produção**. Cada projeto deve lapidar e adaptar a solução conforme sua arquitetura, seus fluxos de confiança e suas exigências de segurança. + +Antes de usar em homologação ou produção, é responsabilidade da equipe do projeto avaliar e implementar, conforme aplicável: + +- integração com o provedor corporativo de identidade; +- definição de autenticação e autorização por sistema, rota, método, tenant, role e scope; +- armazenamento, distribuição e rotação de credenciais e chaves; +- TLS ou mTLS e proteção das comunicações internas e externas; +- bloqueio de acessos que contornem gateways ou proxies de confiança; +- validação de issuer, audience, algoritmo, expiração e revogação de tokens; +- proteção contra replay, brute force, credential stuffing e abuso de endpoints; +- rate limiting, timeout, circuit breaker e controles de disponibilidade; +- mascaramento de dados sensíveis em logs, traces e mensagens de erro; +- auditoria, observabilidade, alertas e resposta a incidentes; +- requisitos legais, regulatórios e políticas corporativas; +- threat modeling, security review, testes de integração, testes de carga e testes de segurança. + +Os exemplos de Basic Authentication, API Key, Bearer estático, JWT, OAuth2 Introspection e Trusted Proxy devem ser entendidos como pontos de extensão. A seleção e a configuração finais dependem do cliente, da infraestrutura e do modelo de risco. + +A promoção para produção deve ocorrer somente após aprovação formal das equipes responsáveis por arquitetura, segurança, infraestrutura e operação. diff --git a/Tuning-Performance/Authentication/IMPLEMENTACAO.md b/Tuning-Performance/Authentication/IMPLEMENTACAO.md new file mode 100644 index 0000000..f40f46d --- /dev/null +++ b/Tuning-Performance/Authentication/IMPLEMENTACAO.md @@ -0,0 +1,29 @@ +# Implementação técnica + +> [!IMPORTANT] +> **Template de referência — requer adequação antes do uso produtivo.** +> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção. +## Biblioteca + +`libs/agent_framework/src/agent_framework/security` contém: + +- contratos e resultados de autenticação; +- Basic, API Key, Bearer estático, JWT, OAuth2 Introspection e Trusted Proxy; +- provider `none` para rotas públicas; +- provider `deny` para default seguro; +- middleware de provider único; +- middleware de políticas por rota; +- factory por ambiente ou mapping; +- instalador reutilizável para qualquer app FastAPI. + +## Integrações + +- `apps/agent_gateway/app/main.py`: `AGENT_GATEWAY_AUTH_*` +- `apps/mcp_gateway/app/main.py`: `MCP_GATEWAY_AUTH_*` +- `Tuning-Performance/Authentication/agent_template_backend_authentication/app/main.py`: `AGENT_AUTH_*` + +Nenhuma integração é obrigatória. A instalação ocorre somente quando `*_AUTH_ENABLED=true`, quando um modo diferente de `none` é configurado ou quando existe `*_AUTH_POLICIES_FILE`. + +## Compatibilidade + +`AuthenticationMiddleware` e `create_authentication_provider()` foram mantidos para compatibilidade. O caminho recomendado para novos projetos é `install_authentication()`. diff --git a/Tuning-Performance/Authentication/README.md b/Tuning-Performance/Authentication/README.md new file mode 100644 index 0000000..72fdda0 --- /dev/null +++ b/Tuning-Performance/Authentication/README.md @@ -0,0 +1,19 @@ +# Authentication + +> [!IMPORTANT] +> **Template de referência — requer adequação antes do uso produtivo.** +> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção. +Implementação de referência para autenticação transversal no Agent Framework OCI. + +Inclui: + +- providers genéricos em `libs/agent_framework/security`; +- instalação opcional por `install_authentication()`; +- políticas por rota, método, roles e scopes; +- integração opcional em `apps/agent_gateway`; +- integração opcional em `apps/mcp_gateway`; +- backend independente autenticado em `agent_template_backend_authentication`; +- exemplos YAML sem secrets embutidos; +- manual completo no diretório `docs` do agente. + +A implementação não pressupõe o uso de gateways. Cada fronteira HTTP pode ativar autenticação com um prefixo de ambiente isolado. diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/.env.example b/Tuning-Performance/Authentication/agent_template_backend_authentication/.env.example new file mode 100644 index 0000000..310a33c --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/.env.example @@ -0,0 +1,39 @@ +# Select: none | basic | api_key | bearer_static | jwt | oauth2_introspection | trusted_proxy +AGENT_AUTH_MODE=basic + +# Public endpoints. Avoid exposing /debug in production. +AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc +AGENT_AUTH_PUBLIC_PREFIXES= + +# HTTP Basic - TIA -> Agent example +AGENT_AUTH_BASIC_CLIENT_ID=tia-contas +# Supported formats: plain:value | sha256:hex | pbkdf2_sha256:iterations:salt:digest +AGENT_AUTH_BASIC_SECRET_HASH=pbkdf2_sha256:310000:replace-salt:replace-digest +AGENT_AUTH_BASIC_REALM=agent-contas + +# API Key +# AGENT_AUTH_API_KEY_HEADER=x-api-key +# AGENT_AUTH_API_KEY_HASH=sha256:replace-hex +# AGENT_AUTH_API_KEY_PRINCIPAL=tia + +# Static Bearer token +# AGENT_AUTH_BEARER_TOKEN_HASH=sha256:replace-hex +# AGENT_AUTH_BEARER_PRINCIPAL=tia + +# JWT / OIDC access token validation. For production, prefer asymmetric algorithms. +# AGENT_AUTH_JWT_KEY=-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY----- +# AGENT_AUTH_JWT_ALGORITHMS=RS256 +# AGENT_AUTH_JWT_AUDIENCE=agent-contas +# AGENT_AUTH_JWT_ISSUER=https://identity.example.com/ + +# OAuth2 opaque-token introspection +# AGENT_AUTH_OAUTH2_INTROSPECTION_URL=https://identity.example.com/oauth2/introspect +# AGENT_AUTH_OAUTH2_CLIENT_ID=agent-contas +# AGENT_AUTH_OAUTH2_CLIENT_SECRET=replace-from-vault +# AGENT_AUTH_OAUTH2_TIMEOUT_SECONDS=5 + +# Authentication delegated to API Gateway / service mesh. +# Only trust these headers when direct access to the pod is blocked. +# AGENT_AUTH_PROXY_SUBJECT_HEADER=x-authenticated-subject +# AGENT_AUTH_PROXY_SHARED_SECRET_HEADER=x-internal-auth +# AGENT_AUTH_PROXY_SHARED_SECRET_HASH=sha256:replace-hex diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/Dockerfile b/Tuning-Performance/Authentication/agent_template_backend_authentication/Dockerfile new file mode 100644 index 0000000..273fe01 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/README.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/README.md new file mode 100644 index 0000000..6198130 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/README.md @@ -0,0 +1,4222 @@ +# Tutorial — Implementação de um Agente usando `agent_template_backend` + +> [!IMPORTANT] +> **Template de referência — requer adequação antes do uso produtivo.** +> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção. +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/Authentication/agent_template_backend_authentication/README_ENTERPRISE_TEMPLATE.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/README_ENTERPRISE_TEMPLATE.md new file mode 100644 index 0000000..cae516e --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/__init__.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f80d2276fbcfd5c1bf959817598ba2410473c33e GIT binary patch literal 184 zcmey&%ge<81RI(QvOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa|L2NNHYXUb=2T zYEfE#QEp;ha;m;#X-P(EUP)$hVo7Fxo_=CFkP}~$np=>QSdto_l$e~InwJuvh^DHb tKtDb{GcU6wK3=b&@)n0pZhlH>PO4oIE6^U0JBmS!kIamWj77{q768MRGCKeO literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/main.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ad5975b53a543ee8fc4d5fa2ccd05a780bbe4b6a GIT binary patch literal 32754 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

6837xrDIR z*R28r!^F3k5UBwsZc_?2kBpe924nlN39 zaX(M|o#_93)Zg}o4ZhVw=VGyPG4ebDJ$=-bN@euYPak#Cl;b&?ETMmF&t*~!KXPU< H>4yIga8z9E literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/mcp_gateway_client_factory.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7cbdba34ebf76c33f1e1d3a0e9c08311fe39b122 GIT binary patch literal 1032 zcmah|&1(}u6rcT0`jv)QO&brTNHJKGf~IOwAx&dx)25}HBGSXK-AvMLH@jhW){x>s z^dNW;o03Tn;R8m>c(XKF4F8{e_&cBw~^IBvl*(J0h0Kd|dwNUygIM zU$ZQmXvDBBC(rERmCV|LMo>+wXG{ZGME8L`@yQ4PaIFDGW*=13>tlb{%K; zBdbcUVlST>xFuWy@nD`t*y!Lwx{$q}egL!i^xSfGo{9v!h=xvvHrAK-Mz#s5&L)$l zt!w7C?U31<*QTdw_$02dn#<;C?38P9CA+#&fND0gnx9uGKQkQ{Xkbfo5OlFgC1UR) zi-w3%LAFbvgLK;}I&q2ewnODIBFK73CDX1Uj78Qq6%32K2Q7aB)F`-?VU@40AzZR? zMYD92OuJ;8ZGoY;S0t&GnE?r^R86)da7)u!y&}~9K~<~Cie81~<m*y-Bd7>BPqM zM72(%TdrXi;r}3fo-qlq&q3^X44O*tS^Y`95uSV}drI;^NqtdLo^s>UjHjf(h9ckl zW6f~n)#%I7Mr^8)$a>*>&7t8J#&e?)z2Xf`9SmLnAU311H*;?nUoSQ$GoK6I#U(GA z`=y8j@-Yx48J}a7;yjfxA}+S}F0hH8dIIB*#`Cs?;xg@nu;eljf)KNJ0C0#MG}@U; z32PNpv+-`C+qcsmal0cN@zH)EJmOyo+^*O~*F<;m7&HCVbbQ3Iz;WC+FmVWiKS1iY J9N;?a{s4yL^P&I% literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/state.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/__pycache__/state.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..851ddfad4fc9d806e8c28cb148ca96b447dac7f9 GIT binary patch literal 2565 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 b5X7hGTc4tl&#gZqh%<$@&=&|d4BY<$E3@b- literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/orders_agent.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/orders_agent.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1e1d6034fab1360bc2d11ab5e516215c1e0f9442 GIT binary patch literal 5163 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 RpAp2lvUlJsgc};~e*yZt#zX)B literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/product_agent.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/product_agent.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9173e5ab22f17b9123feae993bf9aa4ca62fcee6 GIT binary patch literal 5279 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 literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/prompting.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/__pycache__/prompting.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3c4c277b798bc1fc3ea5c2983bd067ed117f54a2 GIT binary patch literal 1181 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 UH1d`G=LB)O)E@pC;fBupU;7EoumAu6 literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/billing_agent.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/billing_agent.py new file mode 100644 index 0000000..aa60099 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/agents/orders_agent.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/orders_agent.py new file mode 100644 index 0000000..f557bed --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/agents/product_agent.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/product_agent.py new file mode 100644 index 0000000..34433f5 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/agents/prompting.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/prompting.py new file mode 100644 index 0000000..255422b --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/agents/runtime.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/runtime.py new file mode 100644 index 0000000..e6429c4 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/runtime.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +# Compatibilidade local do template/backend. +# A implementação oficial agora fica no framework para evitar duplicação entre agentes. +from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext + +__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"] diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/support_agent.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/agents/support_agent.py new file mode 100644 index 0000000..b4f0244 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/examples/__init__.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__init__.py new file mode 100644 index 0000000..3f95e96 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__init__.py @@ -0,0 +1 @@ +"""Exemplos de uso do template backend enterprise.""" diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59de564776daeb18341ac604ef3cfc9a87954d46 GIT binary patch literal 255 zcmZ8cF{%PF6iif91n-d6LQ`oaf`zRJ-~Jgl&o$~MA<2UN)-!krORux77ZCOWCZLVe zyqU*)Gfds@_ejL!Y7dh*pGkZ{KcwzPawC7lk(>@1|9o-A!k&z05?SdDlZJ(~5dqm& zWJlDJp$_P*Qyxdl0j)X!(qsU4QYY7Hvo5vL`EZ49XIvq4hJ0RzeSA}y2+C+K@+jbI w3?E|H>#s_wKfvOM9;wPRrF;!}@TlTuodv1>)~25cHlYybcN literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/grl_examples.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/grl_examples.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bbb960d3910318ccf08917551c00272faa37582 GIT binary patch literal 1837 zcmdUv&2Jk;6u@V^AGX&?NE&4|SoAky5ItfMg&-P~;*nDF|6wCc6`78+&&( zvl|*ti2e;djh2X@Sb>oPjDiSwF+V?TWE%iF{Cg zz|<-yRR|+#^mYVMIq^cW%xRl_9`e;PSrkl_6RNJ&ra?kmghni&f<%;4 zvPN6JNB6(k|A7hOCL#{o{uk7PFw~(C$Lvk!cwaS>oONE30RA9X^5D{Oy7!9I6>Nv}Ih% z*JSqgTlPZz&Vn3v!vGFS(AWOE4jn<9jk6!!-o4GIQxkgHBe@H+gFd%e))5qcDIg+RDhBV#a}h)S4{b(z!Z$u2?^*Ak-XzA-#{1 zzz>?&K4g43R9I=Ta!OqlbzhTUnXAKENzq?U4HFg;>P2N z`u6o7=Xd6J8wWS*o#Km|#f{>_n+L_&&iKX6{6_xa)r0YCo#J$7bn38R=B@4!8oAUR zL4~O3=p_jDSn|uc5)GtaTAi5gH z_%LH&{pk?GV_ma=&F{1>0=50l?#QmYJHAs(-A5mP+S6XMr(4nQDy=&;fX#2;?jlgz f`nJ70v;E1=Q0hLi@AUL%ZcneYo~krH4J-c*c8;-X literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/ic_examples.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/ic_examples.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..72b0aca7259f7c5763b9d7983938da19b5dc5709 GIT binary patch literal 1793 zcmdUvO>Y}T7{_P5FZQmJs(GPJ9MKGI1a*}-2U@DAfV8+p7^)P!F$l@9jMrmlll87U zvm0U#2+_~bLr&?D+^CR{dm<5EAXOx4?ForfZ;nER;KVcQxKt4ph#MpMd70<`?9R(? zeKb01AQ<=WwAX41LcfS)IFbq^N1uVYi!8*DC0VKWq<2#+g^?v!QIi-Te zQmcxkS?S9eE0aTO3RC|^ql3n%YKhLUvWYI;Oy@_U@8Jx1*Zp@>PhOz;C$Cq!arAZZOiK zp9gHcU_6GEqr4Q$rr(J(L5)+kL0PQwkc2cgTz?~Q9csJvSYtGdn7{1EqUCsGz~6!2 z(NAD5APk3HL1pQ<`i!%rid25K^SEZEt{ki1kS-q2)yEJ#={b6NsDDmf`G>mtw4SoG zf?}nu%#vIJL{?#S8`52QSsj75H&F&Ht5@ZQl-HNPm@E|wZ<{NXvS}A@EnAhFE9P}G zJ`KkThs8+xk%u=^pN_*4@H`-2C zogFFGouJhYd}!H8#cA4dq1B1yCJpm41Jn@{$R@2^94W>)c2*+a^_vUtQE z!c};P>m&i!CnxeGs27tb4r-gV1V4e$a^Z{Vh-@bYyyJ#%#aV_ zkU-Gkc##WmOhNH1{P-%E6L8G@7dU2K?u}0#W;NjBl;C3`;lmJo7=jNlLq4#1N!mLv znHQD43mU}vOq{mudf?c0tc9I6d^AjaM6ptLoiLVp$P$=ir4|HUvh*Ev1OhbJW9Ic% zP>($NCc`i%{=0Y!Okb8H=`f{8%99Z!o$YIeq{H%jzZQ2x@iLHLgOIPiUcT9l(bSxl7?(^fk>HF6B@uJ<%godg#%S54KhuDsk$~QB)zsg*T3uhN?zFVx@WW=Dj!b ze)HzNw+B5v3WD$9ZU4rfB0|5hMf_zt%)upK?jQqEWN-%mId_StyoL;6i8n+T(vq+Q z^I2X3J40R)jl@4BNzi4CR1w_}skE%<>FNV8ys(BVewhTCgSFYodF_2|da|S_m$ivy z?1c+eFLWz-)?ITw?Z)ms%_l)nbv4r_t@SdooJgiPtWwWr z0g68s-PZ7yo9s`~0ypoYENnK+q51}NQ+kr6; zL-|O!#_7lAqT^5okbi*{y2LYA@Oq>KkhyH(IvaVYDkv}W%H_*5=H$f8%!MgPQS4b> z2pc(S5PuX9WbaDXmG!ZHR=xZ(mvI}&ZHT#gbTv%-+- z2?{L>QI=^`W`seeNk-URSlI>Dau_hNcKAikm8$1@%fs`SUL&+(c{U!IsD{_!My}ls z;mBb~Le`$3pv|k6y@EZ*v|h3D{gF60Bdi<6i;}+{74TtqGdm53wJTZ#7_8d{XP};+ zqfOMz^?k2?r`Apk*K?&tZhGgPpY@0OqnY}L)0>x`Dfxy{+*69pOI*g4DJ;M zo5@@=d8}DHc~{@kx2NmHQlofw=Tt)--&4nHAJ5d)*@kM=zA$2E<8^hop^on4TPf5# z)Iw4(94uw!&5NxpN*7{${r`^d*Sz|}yW2ze#d>~tKP>~+EW=u8V^tVdg<)l+A7%2J zv-{@(_kJHZ+RuXYk6;X<_U**LRFQi;$W8T%kB8ceU5T6O6L;l_1ZbZ0a8v!_lWd#s zHOo1iNl**F buC3pyg}3&|H*Hx9p-KKC|AK9&8XW%@o65sO literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/noc_examples.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/__pycache__/noc_examples.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e39ccb5708f4609992748bfb68539ea5a1d81257 GIT binary patch literal 1882 zcmdUv&2Jk;6u@V_>$SbM(^8jEJN=lF0@*_CK$})k0m+Ig@gZE4ZIF;Gmg(-q*~H#; zX4Yvf0Yda|=pm<+8#y46n>o>_7cNLaInv#uRhQa`dJ$B$c>fZ3MjXcg&49FEAg)KPJ$&cvebIQ(&}nmt0yib zk`QAh>zbw4lU53zw3WG#vy2j2)tLS_u~~?X;|wbo#ibeO$EtKw?JQjB@IS1c$UqTQp=I!9klc;&_2inq_+ax=Jr-2kE>3kfM+z4s-i zEe|xAZFct6S7bs^=5V}pv5c?qkaA4py;}hf8EIj<0b4>X#Py&v?lIT{%qZAl_r4B2C*Wn{p=_k72({{WMLM|7Dchh-BzXv>i4G7u{zNWO_=x8pl7yul<8R`KLdzq%Hv|W!*|y>3Q{SU~2^`+)!?-wPX$|XrMHz zB`>MVikYbib$0eOao7ob=o%*>oV2}0yTag)6bMI5O9IzJg9a^b&!WGEqInjqyM#aXQ+SiG* zMt#>N2cp{TN?cq8YOrIY-0q5_V_{nDlTrN!Rdck|!Q->vUoUhEf7ZWT9+cP{T2EB#|9wlbTUJ7@Qg zo$F7Y=$BsT7pEWPQyF7$7!^(ro<-T?1C-6`zoKkXA4>5XKLY!GQ9WIKS=pUds;0Jk zMh9Kw;m-{^%%QeVTGWxFh%5HT3IdREzeUa8J#yVE-8W?H0d+Xbd=p@-cz{4Y%2Rzic@HWVsc z!9w-?FakQNd>akHk4B%Ee!UgAohE&gy##CWLgSx+7^sS(JW3=L?ePRso*U>nMgOWW zKrnrCtvB7{y_ublBJ+DEn)W^(&+FmH?tc1+{D=Mp{2c9%<-@JLh-@K2p z504x%5y;lv_~r*HLVwClD*D7YTmj=AB8VY^iSjwVq%g%oL~SWVYpE@*rK~6!h#?uG zujnMZh;C{u)6s3C?}58XdA}D~G5ht0B=94v9a)X*oU%>IEI(p##JnWfj2zQk=hUJ+ zp_a$K>Vpf80q93v&jN59v^^{Km}k{0XRUgpVo~d2bJZ~)%S3G~)KWMQMl|6T8J>6v z6*{9ypM`Z_kzo_b!uKw$!wrnk8_0tC)^QW;X(9OCiT7jt*5t6AM}T-iypqO;$~tO7 zPp5^bS5G|mCM`vhjH3|f&0Elopj#t0CMnN;wXf0F`v>j)~d}C@YGAI zBHJjFYV(V#&@1cYO5?Jv{>vzG%nQ1%AGN8i3AIBLk)^!=F7|~nHo_>iI;;y&O(r9U zF(%=1)Rtw5;7UIX!p^xh%5Frg=Y>8kmioyCWEJ@1vWx#zlu#%DUZ{KB^Ka3x?RryD zaa>HRET&busmF;2;+V`UU@qMR@`wWwhaVVcuR$uZRs45QkI@}8%pd!H;k$)Lg>!p( zXFp$iaO!7!%idnyTdv)?^oKdOZ!Yedi$h~+c>KgqgZqQ+#l7OEyGC_5cWRVHGldb- zX7r~>%jjT%nng47`(|O+EDVj;pO0DkFHHVf%p-Db*Jw?ydIokNuR2(eYBp7W@%Nyl zZ%eO~m+?*>m(OZDbGEcU!evL>xo`&Tho^D*y!P--UD^(o`n=S(F0!uMj(pb@dNPP% zNK6h*XzjpHgvt{(?%aKJ2wudF$}{6aFKYL@^b@uOTlrVO-v%{OF~$dqinS*u!pBGY gJl21FXN2JMAla_`GQa)V);HtrZzrEfFDn4*Z(*TLC;$Ke literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/grl_examples.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/grl_examples.py new file mode 100644 index 0000000..8dadac8 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/examples/ic_examples.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/ic_examples.py new file mode 100644 index 0000000..f6daa57 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/examples/mcp_examples.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/mcp_examples.py new file mode 100644 index 0000000..613f10c --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/examples/noc_examples.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/noc_examples.py new file mode 100644 index 0000000..2b38a15 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/examples/observer_examples.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/examples/observer_examples.py new file mode 100644 index 0000000..926b553 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/main.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/main.py new file mode 100644 index 0000000..c1d3354 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/main.py @@ -0,0 +1,558 @@ +from __future__ import annotations + +import logging +import os +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.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 agent_framework.security import install_authentication +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=["*"], +) + +# Authentication is project-configured. The framework only provides generic providers. +auth_enabled = install_authentication(app, prefix="AGENT_AUTH") + +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) +logger.info("Authentication enabled=%s mode=%s policies=%s", auth_enabled, os.getenv("AGENT_AUTH_MODE", "none"), os.getenv("AGENT_AUTH_POLICIES_FILE")) + +@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 sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None + + await memory.append( + agent_session_id, + ChatMessage( + role="user", + content=msg.text, + metadata={ + **normalized_context, + "agent_id": identity.agent_id, + "tenant_id": identity.tenant_id, + "message_id": message_id, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + }, + ), + ) + await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None + history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)] + + cms_input = { + "channel": req.channel, + "tenant_id": req.tenant_id, + "agent_id": req.agent_id, + "payload": payload, + } + trace_context = { + "text": msg.text, + "channel": msg.channel, + "channel_id": msg.channel_id, + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "conversation_key": agent_session_id, + "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": msg.text, + "history": history, + "context": { + **normalized_context, + "session": session.model_dump(mode="json"), + "original_session_id": msg.session_id, + "session_id": agent_session_id, + "conversation_key": agent_session_id, + "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"), + }, + ), + ) + + 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"), + "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": msg.text, + "sanitized_input": msg.text, + "history": [], + "context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()}, + } + if settings.ROUTING_MODE == "supervisor": + plan = await workflow.supervisor.route_plan(state) + return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata} + decision = await workflow.router.route(state) + data = decision.model_dump(mode="json") + data["mode"] = "router" + return data + + + + +@app.post("/debug/identity") +async def debug_identity(req: GatewayRequest): + msg = await gateway.normalize(req.channel, req.payload) + identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg) + return { + "technical_identity": { + "tenant_id": identity.tenant_id, + "agent_id": identity.agent_id, + "conversation_key": identity.conversation_key(), + "original_session_id": msg.session_id, + }, + "business_context": business_context.model_dump(), + "identity_missing": missing_identity_keys, + "context_keys": sorted(context.keys()), + } + +@app.get("/debug/usage") +async def debug_usage(tenant_id: str | None = None, session_id: str | None = None): + return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id) + + +@app.get("/debug/mcp/tools") +async def debug_mcp_tools(): + return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()} + + +@app.post("/debug/mcp/call/{tool_name}") +async def debug_mcp_call(tool_name: str, arguments: dict | None = None): + arguments = arguments or {} + ctx = arguments.get("business_context") or arguments.get("identity") or {} + result = await tool_router.call( + tool_name, + arguments, + business_context=ctx, + original_context=arguments, + ) + return result.model_dump(mode="json") + + +@app.post("/gateway/message") +async def gateway_message(req: GatewayRequest): + return await _process_gateway_message(req, emit_sse=False) + + +@app.post("/gateway/message/sse") +async def gateway_message_sse(req: GatewayRequest): + return await _process_gateway_message(req, emit_sse=True) + + +@app.get("/gateway/events/{session_id}") +async def gateway_events(session_id: str, request: Request): + last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0" + return StreamingResponse( + sse_hub.subscribe(session_id, int(last)), + media_type="text/event-stream", + headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"}, + ) + + +@app.get("/sessions/{session_id}/messages") +async def get_session_messages(session_id: str, limit: int = 50): + return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]} + + +@app.get("/sessions/{session_id}/checkpoint") +async def get_session_checkpoint(session_id: str): + return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)} + + +@app.on_event("shutdown") +async def shutdown(): + telemetry.shutdown() diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/mcp_gateway_client_factory.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/mcp_gateway_client_factory.py new file mode 100644 index 0000000..5a32d15 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/observability/__init__.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6a83d0db48251e8e2b29bcc9fab2cefc50a9614 GIT binary patch literal 198 zcmey&%ge<81RI+RvOx4>5CH>>P{wB#AY&>+I)f&o-%5reCLr%KNa|KQSdto_l$e~InwJuvh^DHb zKtDgJIJKxOF)1@Av!qf#K0Y%qvm`!Vub}c4hfQvNN@-52T@fqLR*gjEsy$ H%s>_ZBUCr| literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/telemetry_observer.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/__pycache__/telemetry_observer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b917a574e36a66666e31aa2b92b014ef99f2d492 GIT binary patch literal 4809 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 literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/telemetry_observer.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/observability/telemetry_observer.py new file mode 100644 index 0000000..92f07a1 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/state.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/state.py new file mode 100644 index 0000000..cc19c03 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/state.py @@ -0,0 +1,53 @@ +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 + 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/Authentication/agent_template_backend_authentication/app/workflow_actions/__init__.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__init__.py new file mode 100644 index 0000000..6be8ce7 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__init__.py @@ -0,0 +1 @@ +from . import devolucao # noqa: F401 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__pycache__/__init__.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..672ec8afcfb5e583d0065e0a2c7660da362b807e GIT binary patch literal 238 zcmZ9Gu?oU45QZ-;NEHM(S0|x^_60<6RS-e<5Yx1cZIcjF#Rus__(~mIoZN)IfLGAP zf4KYppF8f_;cx(`wl=3J_Gf2)^1jfH5Ng=No@`)EESW&+h5n0R9FnFVi;FHQ&h$3) z8n8abcWALflN~B0RXW=Vn`m1y#YMELTqcy0oH?mg#8NcWiP99z33_Ba7b>RgZ_1eH ts%@SW`brt^HqQ~IQb|YY+}xUAQR=uV#L@0#(PMFR$!0F`L*2o;L%fq8zDx7~Q_u3TjXm8bx>dqH!}oQf)5vG8 zhjkpXlW%gXNG%WV5xO&dCacIn-X~YZGTAe&kzI5mN*phRLCrb zxkfRIsBa4_?V*Y2lbG=%Va92L@_2-tsf>Hn^u0VNO})siX>? zCe*>ybe$aH>J}kou9Pr4L^LJtp)zkM3(m!_U$S|pN&jq^(pdm~V|wk46} z6D}shUQiaDm|K>;gMG)euBd__e}zeycknJLN47<#gSS+bxuuasgrmZofe+w!&%o#P zgNd(`UnVQBY9lkXYqQnvxvDz%HPEq+3|YOYynJ@|1Am+0qqs4CN7#0 zG3{#^c|ObvBO9q};Cl98x3Ybjovn_}Rfpz}-=B7`RMnMcCp{Usn$viPX*$FtU)cwI4C~GVuGX!o_^pFCo1EIbXa_ouO{?`sVfk|pWxXquz0Qk z)L-qtRfA)71mS#TunvU#?j-Jr#c~xZ0F5Wx=jlW~QG<8t2*IbYB81W(Ej0Y`a-6K? Nk-O)Kgbs<6{{ZMk{lx$P literal 0 HcmV?d00001 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/devolucao.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflow_actions/devolucao.py new file mode 100644 index 0000000..6111cf1 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/app/workflows/__pycache__/agent_graph.cpython-313.pyc b/Tuning-Performance/Authentication/agent_template_backend_authentication/app/workflows/__pycache__/agent_graph.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dbe6f0989dac2c2c33f4715a3e6c08170cf7aaba GIT binary patch literal 54261 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

>i)pBbqo9v``q8126$ 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 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 = StateGraph(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", []) + judge_context["evidence"] = state.get("mcp_results", []) 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", []), + }, + ) + + 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/Authentication/agent_template_backend_authentication/config/agents.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents.yaml new file mode 100644 index 0000000..7d245a5 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/retail_orders/guardrails.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/retail_orders/judges.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/judges.yaml new file mode 100644 index 0000000..62fc7c7 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/retail_orders/prompt_policy.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/retail_orders/prompt_policy.yaml new file mode 100644 index 0000000..f872a2b --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/guardrails.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/guardrails.yaml new file mode 100644 index 0000000..9fe094a --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/judges.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/judges.yaml new file mode 100644 index 0000000..d488063 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/prompt_policy.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/agents/telecom_contas/prompt_policy.yaml new file mode 100644 index 0000000..42732c4 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/authentication.example.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/authentication.example.yaml new file mode 100644 index 0000000..881e3c0 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/authentication.example.yaml @@ -0,0 +1,39 @@ +# Nunca coloque secrets diretamente neste arquivo. Use sempre *_env. +providers: + public: + mode: none + + deny: + mode: deny + + tia_basic: + mode: basic + client_id_env: TIA_AGENT_CLIENT_ID + secret_hash_env: TIA_AGENT_SECRET_HASH + realm: agent-contas + + platform_jwt: + mode: jwt + key_env: PLATFORM_JWT_PUBLIC_KEY + algorithms: [RS256] + audience: agent-platform + issuer: https://identity.example.com/ + +policies: + - name: health-public + provider: public + paths: [/health, /ready, /live] + + - name: tia-agent-api + provider: tia_basic + paths: [/gateway/message, /gateway/message/sse, /gateway/events/*] + methods: [GET, POST] + + - name: admin-api + provider: platform_jwt + paths: [/debug/*, /admin/*] + required_roles: [platform-admin] + required_scopes: [agent.admin] + +# Quando nenhuma política casar, rejeita. O default omitido também é deny. +default_provider: deny diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/config/guardrails.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/guardrails.yaml new file mode 100644 index 0000000..44887eb --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/identity.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/identity.yaml new file mode 100644 index 0000000..5f20147 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/judges.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/judges.yaml new file mode 100644 index 0000000..c091619 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/mcp_parameter_mapping.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_parameter_mapping.yaml new file mode 100644 index 0000000..5b29ccf --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_parameter_mapping.yaml @@ -0,0 +1,92 @@ +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 + 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/Authentication/agent_template_backend_authentication/config/mcp_servers.docker.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_servers.docker.yaml new file mode 100644 index 0000000..8101130 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/mcp_servers.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/mcp_servers.yaml new file mode 100644 index 0000000..fe638a2 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/prompt_policy.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/prompt_policy.yaml new file mode 100644 index 0000000..af4398f --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/config/routing.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/routing.yaml new file mode 100644 index 0000000..2dbe95e --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/routing.yaml @@ -0,0 +1,128 @@ +# 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_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/Authentication/agent_template_backend_authentication/config/tool_policies.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/tool_policies.yaml new file mode 100644 index 0000000..3c4fd05 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/tool_policies.yaml @@ -0,0 +1,26 @@ +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: + 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/Authentication/agent_template_backend_authentication/config/tools.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/tools.yaml new file mode 100644 index 0000000..d85fae1 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/config/tools.yaml @@ -0,0 +1,101 @@ +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 + 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 + 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 + 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 + 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/Authentication/agent_template_backend_authentication/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/ATUALIZACAO_TEMPLATE_ANALYTICS_OUTPUT_SUPERVISOR.md new file mode 100644 index 0000000..d81efdf --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/COMO_USAR_IC_NOC_GRL_NO_TEMPLATE.md new file mode 100644 index 0000000..83975af --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/CONVERSATION_SUMMARY_MEMORY_BACKEND.md new file mode 100644 index 0000000..3f981ac --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/EXEMPLOS_ROUTE_HANDOFF_TRANSACOES.md new file mode 100644 index 0000000..5c41732 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/FRAMEWORK_CHANNEL_INPUT_MODE.md new file mode 100644 index 0000000..c7bd3b2 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/GUARDRAILS_PARALLELOS_OBSERVER_IC.md new file mode 100644 index 0000000..849fda1 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/IMPLEMENTACAO_IC_NOC_GRL_SEM_REMOVER_LOGICA.md new file mode 100644 index 0000000..edcd2c7 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/LANGFUSE_SINGLE_TRACE_OBSERVER_FIX.md new file mode 100644 index 0000000..bc2638b --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/MANUAL_AUTENTICACAO.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/MANUAL_AUTENTICACAO.md new file mode 100644 index 0000000..b0e1395 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/MANUAL_AUTENTICACAO.md @@ -0,0 +1,253 @@ +# Manual geral de autenticação do Agent Framework OCI + +> [!IMPORTANT] +> **Template de referência — requer adequação antes do uso produtivo.** +> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção. +## 1. Princípio arquitetural + +A autenticação é uma capacidade transversal, opcional e reutilizável. Ela não depende da presença do `agent_gateway` ou do `mcp_gateway` e pode ser instalada em qualquer aplicação FastAPI que exponha uma fronteira protegida. + +```text +agent_framework.security + ├── backend independente de agente + ├── agent_gateway + ├── mcp_gateway + ├── channel_gateway + ├── MCP Server + └── aplicação customizada +``` + +O framework fornece providers, middleware, instalação por configuração e políticas por rota. Cada projeto ou deployment decide onde ativar e qual mecanismo usar. + +## 2. Onde autenticar + +| Arquitetura | Ponto principal de autenticação | +|---|---| +| TIA → Agente Contas diretamente | backend do Agente Contas | +| TIA → Agent Gateway → Agente | Agent Gateway; backend deve ficar inacessível externamente ou usar autenticação interna | +| Agente → MCP Gateway → MCP Servers | MCP Gateway, com autorização adicional por agente e ferramenta | +| Agente → MCP Server diretamente | MCP Server | + +Autenticar no gateway só libera o backend de autenticação externa quando o acesso direto ao backend é bloqueado por rede, `ClusterIP`, NetworkPolicy, security group, service mesh ou mTLS. + +## 3. Instalação no código + +Use a mesma função em qualquer app FastAPI, mudando apenas o prefixo: + +```python +from fastapi import FastAPI +from agent_framework.security import install_authentication + +app = FastAPI() +install_authentication(app, prefix="AGENT_AUTH") +``` + +Integrações fornecidas neste pacote: + +```python +# Backend independente/template de autenticação +install_authentication(app, prefix="AGENT_AUTH") + +# apps/agent_gateway +install_authentication(app, prefix="AGENT_GATEWAY_AUTH") + +# apps/mcp_gateway +install_authentication(app, prefix="MCP_GATEWAY_AUTH") +``` + +A autenticação permanece opcional. Sem ativação, o comportamento original da aplicação é preservado. + +## 4. Providers disponíveis + +| Modo | Uso típico | +|---|---| +| `none` | rota pública ou desenvolvimento local | +| `deny` | rejeição explícita/default seguro em políticas | +| `basic` | integração system-to-system controlada, como TIA → Contas | +| `api_key` | integração simples entre serviços | +| `bearer_static` | token estático com rotação | +| `jwt` | access token JWT emitido por OAuth2/OIDC | +| `oauth2_introspection` | token opaco validado no authorization server | +| `trusted_proxy` | identidade validada por API Gateway, ingress ou service mesh | + +mTLS deve ser terminado no ingress, API Gateway ou service mesh. A identidade resultante pode ser encaminhada com `trusted_proxy`, desde que o acesso direto e os headers internos sejam protegidos. + +## 5. Configuração simples com um provider por aplicação + +### Agente independente com Basic + +```bash +AGENT_AUTH_ENABLED=true +AGENT_AUTH_MODE=basic +AGENT_AUTH_BASIC_CLIENT_ID=tia-contas +AGENT_AUTH_BASIC_SECRET_HASH=pbkdf2_sha256:310000:: +AGENT_AUTH_BASIC_REALM=agent-contas +AGENT_AUTH_PUBLIC_PATHS=/health +``` + +### Agent Gateway com JWT + +```bash +AGENT_GATEWAY_AUTH_ENABLED=true +AGENT_GATEWAY_AUTH_MODE=jwt +AGENT_GATEWAY_AUTH_JWT_KEY= +AGENT_GATEWAY_AUTH_JWT_ALGORITHMS=RS256 +AGENT_GATEWAY_AUTH_JWT_AUDIENCE=agent-gateway +AGENT_GATEWAY_AUTH_JWT_ISSUER=https://identity.example.com/ +AGENT_GATEWAY_AUTH_PUBLIC_PATHS=/health,/ready,/live +``` + +### MCP Gateway com token de serviço + +```bash +MCP_GATEWAY_AUTH_ENABLED=true +MCP_GATEWAY_AUTH_MODE=bearer_static +MCP_GATEWAY_AUTH_BEARER_TOKEN_HASH=sha256: +MCP_GATEWAY_AUTH_BEARER_PRINCIPAL=agent-platform +MCP_GATEWAY_AUTH_PUBLIC_PATHS=/health +``` + +## 6. Políticas por rota + +Use um arquivo YAML quando uma aplicação precisar de providers ou requisitos diferentes por endpoint: + +```bash +AGENT_AUTH_ENABLED=true +AGENT_AUTH_POLICIES_FILE=config/authentication.yaml +``` + +Para gateways, use respectivamente: + +```bash +AGENT_GATEWAY_AUTH_POLICIES_FILE=config/authentication.yaml +MCP_GATEWAY_AUTH_POLICIES_FILE=config/authentication.yaml +``` + +Exemplo: + +```yaml +providers: + public: + mode: none + + deny: + mode: deny + + tia_basic: + mode: basic + client_id_env: TIA_AGENT_CLIENT_ID + secret_hash_env: TIA_AGENT_SECRET_HASH + realm: agent-contas + + platform_jwt: + mode: jwt + key_env: PLATFORM_JWT_PUBLIC_KEY + algorithms: [RS256] + audience: agent-platform + issuer: https://identity.example.com/ + +policies: + - name: health-public + provider: public + paths: [/health, /ready, /live] + + - name: tia-messages + provider: tia_basic + paths: [/gateway/message, /gateway/message/sse, /gateway/events/*] + + - name: administration + provider: platform_jwt + paths: [/debug/*, /admin/*] + required_roles: [platform-admin] + required_scopes: [agent.admin] + +default_provider: deny +``` + +As políticas são avaliadas na ordem declarada; a primeira correspondência vence. Os paths usam padrões glob, como `/gateway/events/*`. Quando `default_provider` é omitido, o comportamento é `deny`. + +Secrets nunca devem ser gravados no YAML. Use campos como `secret_hash_env`, `key_env` e `client_secret_env`. + +Arquivos de exemplo: + +- `Tuning-Performance/Authentication/authentication_policies.example.yaml` +- `apps/agent_gateway/config/authentication.example.yaml` +- `apps/mcp_gateway/config/authentication.example.yaml` +- `agent_template_backend_authentication/config/authentication.example.yaml` + +## 7. Roles e scopes + +Após autenticar, o middleware extrai roles de `roles`, `role` ou `groups`, e scopes de `scope`, `scp` ou `scopes`. Requisitos ausentes retornam `403 Forbidden`. + +```yaml +required_roles: [platform-admin] +required_scopes: [agent.admin] +``` + +Basic, API Key e tokens estáticos identificam o consumidor, mas não produzem roles/scopes por padrão. Para autorização granular, use JWT/OAuth2, um provider customizado ou uma camada de autorização específica. + +No MCP Gateway, autenticação não substitui a autorização por `agent_id`, tenant, ferramenta e tipo de operação. + +## 8. Azure DevOps, Azure Key Vault e Kubernetes + +O pipeline recupera secrets do Azure Key Vault e os injeta no deployment. O framework não chama Azure DevOps nem Key Vault diretamente. + +```yaml +env: + - name: AGENT_AUTH_ENABLED + value: "true" + - name: AGENT_AUTH_MODE + value: basic + - name: AGENT_AUTH_BASIC_CLIENT_ID + valueFrom: + secretKeyRef: + name: contas-auth + key: client-id + - name: AGENT_AUTH_BASIC_SECRET_HASH + valueFrom: + secretKeyRef: + name: contas-auth + key: secret-hash +``` + +No cenário TIA → Contas, o TIA mantém o secret original e o agente pode armazenar somente o hash de validação. + +## 9. Provider customizado + +Um provider específico implementa somente: + +```python +class AuthenticationProvider(Protocol): + async def authenticate(self, request: Request) -> AuthenticationResult: ... +``` + +Nomes e regras particulares de TIA, GStreamer, TIM, Azure ou outro cliente devem ficar no projeto do cliente. A biblioteca deve permanecer genérica. + +## 10. Testes rápidos + +Sem credencial: + +```bash +curl -i http://localhost:8000/gateway/message +``` + +Basic: + +```bash +curl -i -u 'tia-contas:secret-original' \ + -H 'Content-Type: application/json' \ + -d '{"channel":"web","payload":{"text":"Olá","session_id":"auth-test","user_id":"tia"}}' \ + http://localhost:8000/gateway/message +``` + +## 11. Requisitos mínimos + +- TLS obrigatório fora de localhost. +- Nunca registrar `Authorization`, API keys, secrets ou tokens. +- Restringir acesso direto aos backends quando a autenticação estiver centralizada no gateway. +- Usar comparação em tempo constante para credenciais estáticas. +- Usar PBKDF2 para secrets humanos e rotação periódica. +- Proteger `/debug`, sessões, métricas e documentação. +- Não confiar em headers de identidade vindos diretamente da internet. +- Separar autenticação, autorização e confirmação transacional. +- Aplicar rate limiting, limites de payload e auditoria no ingress/gateway. diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/TESTE_LONG_TERM_MEMORY.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/TESTE_LONG_TERM_MEMORY.md new file mode 100644 index 0000000..cfe5969 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/VALIDACAO_BACKEND_IC_NOC_GRL.md new file mode 100644 index 0000000..a9e4458 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt b/Tuning-Performance/Authentication/agent_template_backend_authentication/docs/VALIDACAO_TEMPLATE_ENTERPRISE.txt new file mode 100644 index 0000000..fac4bf4 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/llm_profiles.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/llm_profiles.yaml new file mode 100644 index 0000000..908b382 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/requirements.txt b/Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt new file mode 100644 index 0000000..ba0f396 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt @@ -0,0 +1,25 @@ +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 + +PyJWT[crypto]>=2.9.0 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py new file mode 100644 index 0000000..bc0a005 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py @@ -0,0 +1,23 @@ +from __future__ import annotations + +import argparse +import base64 +import getpass +import hashlib +import secrets + + +def main() -> None: + parser = argparse.ArgumentParser(description="Generate a PBKDF2-SHA256 value for AGENT_AUTH_*_HASH variables.") + parser.add_argument("--secret", help="Avoid on shared shells; omitted means secure prompt.") + parser.add_argument("--iterations", type=int, default=310_000) + args = parser.parse_args() + secret = args.secret or getpass.getpass("Secret: ") + salt = secrets.token_urlsafe(18) + digest = hashlib.pbkdf2_hmac("sha256", secret.encode(), salt.encode(), args.iterations) + encoded = base64.urlsafe_b64encode(digest).decode().rstrip("=") + print(f"pbkdf2_sha256:{args.iterations}:{salt}:{encoded}") + + +if __name__ == "__main__": + main() diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/test_long_term_memory.py b/Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/test_long_term_memory.py new file mode 100644 index 0000000..52e2a8d --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.active.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.active.yaml new file mode 100644 index 0000000..b825518 --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.active.yaml @@ -0,0 +1 @@ +version: 1 diff --git a/Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.v1.yaml b/Tuning-Performance/Authentication/agent_template_backend_authentication/workflows/devolucao_pedido.v1.yaml new file mode 100644 index 0000000..d2ff1aa --- /dev/null +++ b/Tuning-Performance/Authentication/agent_template_backend_authentication/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/Tuning-Performance/Authentication/authentication_policies.example.yaml b/Tuning-Performance/Authentication/authentication_policies.example.yaml new file mode 100644 index 0000000..881e3c0 --- /dev/null +++ b/Tuning-Performance/Authentication/authentication_policies.example.yaml @@ -0,0 +1,39 @@ +# Nunca coloque secrets diretamente neste arquivo. Use sempre *_env. +providers: + public: + mode: none + + deny: + mode: deny + + tia_basic: + mode: basic + client_id_env: TIA_AGENT_CLIENT_ID + secret_hash_env: TIA_AGENT_SECRET_HASH + realm: agent-contas + + platform_jwt: + mode: jwt + key_env: PLATFORM_JWT_PUBLIC_KEY + algorithms: [RS256] + audience: agent-platform + issuer: https://identity.example.com/ + +policies: + - name: health-public + provider: public + paths: [/health, /ready, /live] + + - name: tia-agent-api + provider: tia_basic + paths: [/gateway/message, /gateway/message/sse, /gateway/events/*] + methods: [GET, POST] + + - name: admin-api + provider: platform_jwt + paths: [/debug/*, /admin/*] + required_roles: [platform-admin] + required_scopes: [agent.admin] + +# Quando nenhuma política casar, rejeita. O default omitido também é deny. +default_provider: deny diff --git a/apps/agent_gateway/app/__pycache__/governance_middleware.cpython-313.pyc b/apps/agent_gateway/app/__pycache__/governance_middleware.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9ef1c4ff02adf89c9979a533d7e72f3935e6c6f3 GIT binary patch literal 4700 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 literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/__pycache__/main.cpython-313.pyc b/apps/agent_gateway/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fffdb5d24b75991d2a2f0b23e0413c51a3ce87e7 GIT binary patch literal 17489 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 G_kRE(wtz4I literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/__pycache__/settings.cpython-313.pyc b/apps/agent_gateway/app/__pycache__/settings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0331ed76b6d7e65a13dddb4adae5fa0dd427b93b GIT binary patch literal 3272 zcmZ8k-A^0Y6(8d<7%(5*<9 zbFVK~0|R{=eE#_B`kOzyIqqLbIv9q=^CtWdAqEtg&)U%`Ls-zl5rB>GINJolm zwc|=TQ7IWEdoA{-+aivutuD;%fFllxBVCe%ID^~)4^ekXPKlRXkDSCMb(3zX=b%S& zFTpUWm$(`0W2~P*E@1O8HbD9q8)R&V^h?8FyT#ZD@i5ycV`F51v2n(3lR;?$JiUxf zk|D_lY|3WCj7>A-u+1_SAR}yrImYJ6D6`#RY=MkPLGWB;Y>A9B+cIMza+{4? zVQiI5Ft)~6n0Q%a5yqlqlCisttrMSg59Ykj*an$mk!{+xX=eM(W`4#VF!qqluvs42 zwpnJ|V(c*qFxwNxHpm>C^K-_w$vm??wa49I}}zV+pdvau*nz zB+G1tUB)&@h_R$SZiS6|ZnIU!M8;BNjm5IZSek^(>!A!CN-Bo>bLFhlaPNDeN1A*@ zt(`Vq4dXcckakzfHI1ktCv}5XKCV<%daEPJYOSOlLDN!~`U`_7%~_?a){Jo3nl@an zYKKZStT*Z^Ij!g#p-$Z()E&#`P4`;Ujjhe~2luHbo-0Ult{^7G ztkk?6iI&)jiXJMZuhiO+9ywD^s&s587T*)H2}zFUvb$nZ&c}++Y4_{1LsB_vhU{bd z#S+mPaA23#0!tqgLS{KG{e+Gt)482kS}x@Fi()n@XL1RlNclret5TnB+Y^Mm9E%sl z7lLfBOXqCcz9h)feqJcN5T#r}&ShCZX)#_b|L(W=`R7yWI<9JpL8t8@88OS26AD)F z2_YL7D#L@YMV%ETL6St6t5{6Sk^m_tq_TB`{PY=}uw#&mVn%?lJ06rj(Mn%6Kfg{M zDJH9xDza8rYf7cV^drl_lyXTssl$DDSXE&|Jn3{s&KGho#Dq|=7lp`%P+b%rm)&Nl)F+pRtfZ=8iBThyVR(hs!AoI zD`8#H>Pn-kh1VkxaYQ3Zsj8N$TH`1J5k{>HqNnS0Bo`Ouq>zn?P!8Dt{X)7qXqhAB zx)I)ptka>7K3LLpw%G@59Qb7*tJUB%e6(VZ?g_7U=@3k~lNRK3ESubgvO*rVB$2t`yjeVyd2Xsb%CeB97g44V#I&w?j`-eS?f28(e)l2cMH+jMS5?o!Xb zl#q7{(1@|3K!@#EV%b>wRZ)yfbO4J88K}w$p54)F%XO_JI-ZQ@F;;1R2MUshh}o$a ziNRhKKQYij=32<5gm_UF6Lk28L+#3MKZBmUG3%#QrQ;vN*o}$Mt700KqGNXWxXV(p zAjF{MlXUbWg^MGR&xzp5>SX!IU{5s1KE00AQ@EUT>b)^GA1f5aA|4Oe`#*4@2zASH zO*v6znfA%@iFVYeqU@37pEZ=KZ5fi`A|$#|t<=<-Cd;G;d({KpY86fZeruCqRDt7- z7XUbpfR>CQj3Z1Sco8NMrV#uHGYA2MIfQwH1%x2NBEm942w??b6=4k_j1WPHBHTq- zN4STufq<_S@)^Pdgog-^5VjB=BjE8PpCfDo&~X_$1`b$BRt$qw4&f@)A)lvUyMkYi z&MWo09%UOY+xLGIR8if2)Nm%{mgF_P(ew1hoi-o+F7U9$doMwJ zXbPDYKhc`pY4aI#X|=`AyyM&asu{W0;)CyuHh<4tiM05@yI`A-n2Rgu;B50NW)Q@w z%Y6_(bi~(p1S};!1VdgvlsC;?>A@XV3o_!Ha}<1ylL^bFJX>1 z=F}>9Lg1_B!g7lrzi77kWpjECltuK!)g~@|xY`YIa`QZMaUb)(G05w;Qu9MFGk042 z*u_zspEqYAbhd27TwZJOlb2goNB{lvNsw#kpc57X4~q(*N9?$NTm6uiPxJ{{!qBhjIV_ literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/config/__pycache__/governance_loader.cpython-313.pyc b/apps/agent_gateway/app/config/__pycache__/governance_loader.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..49664df5a589248984e78ed7e0a29d793ce239e5 GIT binary patch literal 925 zcmY*YL2DC16rR~^Hc8i7TA|fIo27VgQM)1(6toaanl_QGWUC@UmTfYLDamY@ozy0H zD0mSsdMStq9z}nFM}I;}4=F=?>dBjsgIC`q*|g5#z3>flbzM{^p}M)??vIhqr5qDi!) z?qC+LDrWq*BsHw1+9H+CX6!9Do8HPiPrq=po6j-@J8iFK+|^BcZ6n)R zDEYKft6Ei$lhD1IQcD&d6g z3XSa50v-re_J~B_Rft>ny|PF!;+0)a_PD7CwF!TS(1#OU83b~5h9T~H&1PVERYJKt zQewfC6^a{+8n6-=Z zm&wn8CBTT7+(x~&JzTBE+fCRsi^}{ZY2=Ujj@T86r@@H$gN9#j)yWFG2BCa;K@!ZL b2*&snEuNvoDY|-!X3voRS5xr@9`ybLIepg{ literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/governance/__pycache__/__init__.cpython-313.pyc b/apps/agent_gateway/app/governance/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9facf8795a781ba428b2ecfb9810fc68fa779592 GIT binary patch literal 142 zcmey&%ge<81gn}0vKWE%V-N=hKms7}nFUBpWk_exWb|9fP{afh0*T+!O)MxV)=x}N z%`1sdPb^6-Pps4j3hAfkm!%fvCFUik>c_`t=4F<|$LkeT-r}&y%}*)KNwq6t1?mQw QUJPP?TdW#8OKOb&a;VBF(8n(W=Gbp^>6laxs==$WFE?$?P(-sU)Y0 z2Mb<$uplC+AXxni`bT)M2C0lKh&_1|f_G=?Zi91pzu$ZF=6z0gV4x4k^YKZz_8bCm zq?F|7{|M|pkYpP;z<~v@1R1mx*9scbj!|d2M7@BPQY>YGhkCI;3DLtGaL}yGpbj$~ z!%2;UHN?`sW}Cm1(S{<6DGeeV)dI?wrLHf~R(lNlghsB9Bk~BhX8d5CFp6n~R5bN0 zKB@~@FYNx5WE+&@OaPQ%l9g%>ENR!|JqC3W%J{wnT0Uy}rvM?Kj zb2%IVL^h^wozQk_id$3sj;)D)LV1%B7xPN3CQOf1akC!TX@R_2B@(*y7CMiZ(8C}U zMm_L-!UU?(Y9P=W52#3a%|^&=T_B1ZM3^CN)dTDa#Br5yRgh&_;xZ0Hp8s!8d1=b` zc9q9XuNKXOExBXGT(T~g%Ff6DYID3O#RfRY4sDsw<~J66^R7PVQD`a$yr~uI!~J4p86I!sEi0`C{Z%^t%1By>nr=^y&8A*?V8n z{jQ7(Y=fPazy_2Uk)l1Qx-ZMVuUPVftq6mWsL7~6%K;^JM)bLEwJ9ycb(w0BosvSB zYCJXS3nOZU5}8_#$oM3>dptKbq+C@;9{?Lud{jB4uQvj(StmD`B~|q;x%x!EbqL`h l$Q^;~cQAMeMt@|6_cP;PGUJ~oW;>ay8^#|4!6H14^cV4^)@}d* literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/governance/__pycache__/evaluation_hooks.cpython-313.pyc b/apps/agent_gateway/app/governance/__pycache__/evaluation_hooks.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..473f2bc23ba05bb08b2b4ae247c6a493206c7e19 GIT binary patch literal 881 zcmah|O=}cE5Urk>oviuVkdPobBt!JzK*)N@O+*iZB#_JCNu}w`_O6>udM4F9k-Z6m z{~$ji{+b-6!IOB%Oepq0iK?4-iaC}T>e+^{s3-0-;> zR9YSg8z?w~x;Xq3rySKl<3@3{L`HS$aQx?iG}`zk-wrYCb%6#l^-nOpxLO;KCh zCi4Nlbk@$>Yh%Lyodxd^3Hkie0B7jq7f{=ZezU$a5YBOQQT3h1+v+y%s{5P64Q|A( zpSRa>fp#W+_tFSw$@x9s8|Ln{b-0<(@xJvjYFDGdWeO5!krNo>$~8)-d|OJRjFKw}meoeYb2GYF&AG=XCGy~+9&{Voz-M61Kx}V}2 cQTV=D9`i>ig!l>@-(lr4Zi(b4keT%Q14yUXu>b%7 literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/governance/__pycache__/model_policies.cpython-313.pyc b/apps/agent_gateway/app/governance/__pycache__/model_policies.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1141210aa6d44d7e51b11311330f8ef4c8bb4e6d GIT binary patch literal 3383 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 literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/governance/__pycache__/rate_limit.cpython-313.pyc b/apps/agent_gateway/app/governance/__pycache__/rate_limit.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..507ee7d0c458eba75cb7aebebd8889216840fe78 GIT binary patch literal 2594 zcmb7GO>7fK6rSDn+UsA4W0Kej&6Yr02NH*$P{~jzv}x4@qSjlNfGI0uPs|c$ZD!XA z300|737OIgIG{LcrKGpCm-fhk14oXIT$E@OC=$KoRyC+dJ@w7{CzOV&jO5wp@4b2R ze&)TIHLur&VEp)H{=8pA=rQ|KAMl?IZtV=rw~&GqZU_x>gp(P~5AlSDuP`JG+K6pX zB%+1ehU|k5;*e1r+J+P{gcSQ}sb6545F$0-F)A&LGgOqSYpGsAiS7)yFSqlJh3!O%UQxMq45kcXJEhB_& zv?-P}@iA>&GY2lFFizw2ssz$Mpy9AcB~{gv<5*RxOI62n=|Yy#9#y?iNM>uCO{zMo z5yQ-CI@WW*-lD1ovo)nAO_OM63nn&Hm9UVmF@tC%na>-MWCrV|nt|OXlT!?eWO5Uj z=t(_=BLrYI%dG0nPZ4I2H8H|q8MuO|O(L7?_&>a^MUY#21K7`bZCCgZIuDsq9ExC3 zqGBWVj2M<^^9TBIJf0&{jfh~f>H!_8nnoZIVmX5!w{{GOTPV)G#j$|txomV@&a-VZU)jk4C%0^a{8#2AxxMsPpKVGK%flBy3N4ZQPKjS!7P$x z(!>NHNt&*i5c!S`Jm{@+y=+|&rZ3QPOUF!ho-f+IcCSRDcls-lBSqUcZY!RtnsJt+ z4iLATb-@hIq8I?rk66Qrb&kT-o&0xrHk9XIB(Dg%?IrfLy+}_V(#4n1BmRdTd)%fw z;(R^rNeJr5jsxI)66$Ou#8Q{#a)Vp z$DN9^2dY2nhY0Opc+6d2x&E#1Ap^gm#rFa&aQu&hrUX0F)`+|6`ATeRV2(N#y#qVL z?g>^e)=*q=31Z@ov&eY%HnNR5u&%i-wf>De>$+qFrRp7~j#>+*&4jgI!!*=9ChEAR zL%-8Tz(Q)bTAV?p)H&$iIBQTYO1W;rig)!OOw;oF)%GW|ekWV~PR6x1ERW`hJk3Yt zXd_rz&9?Wl&#Ds>(E|XyH^C!&6kybCcA5wvZabromDx zua9br5>w}JYK#Uq>Q5GnCB}FMS`Rd*&?|F0XLrs;W+MyXN@(9wD0-V;3Lahz9xlHf zuLM6?YCBU-oGE&jTia(Y-Mln&`R3*MiCrPlpFA1!*8gInj?XWQpGXFC@{mEgXm zAgnL?9&O%Q6d$^qOH!q+Z#f_@2D+C5y^Dd~a^&5+xDq&49C+yV- zg%zLwy7rZJZLBCPH*J||zu8_=7Morz+8_D?rIvYaHaM>=?D>AA66{&__^%IN9sb4B z@uz?`ZF!b^KiKn|+gF^pcHq0N68^Tk;@-aE?<&9c-d*D^zPINdzIUSh@kn|5$%_9} z*>h@H@?G~{_1;L`mqJz>5A)V>v5p747_2|jKK5Qn7$(^z!&BkYutd7qu@C?uJAwMk z1(f`mJe<=p*#!W3lkx2Yn*{;~j^SYkVJRl;VkE5o2oxzI>|!DN*u?73y6mghGs`St z6=h9qmBChBK~H#IJjy+l{9=1mk_m;~?4>bUfc@a5LD%{ zDzE7o!rB(~#0omAi2*`##43R5#_Z*>ZVZ(UR$nrQ$RS{`>&|!`rr!mQ;~t=GkCFRV c)cpXpJh5|}v*dY#;PX^!<=X#1Fj@rcL_ zB8b=_957@9$Gt=Ez(;=ocVG`9e;1gOKEkTWe55N|#qZuSHXX81{JsW75O>MgnRhEm1c6SB?`#=|E6`FgFiOv7RSQ?-SLNwNgA71+TgG?aHD{&$@w`-hJ zZ)N8AP0mkCku`sMoKI8KCQFr6Ip^4Of_(=&1a^ya?RGTg!Wc}CN+UI8)zfi>8f(=boq$)wTiOX_VC2Z&EvV&*1 dM=5S|^%lWTpE33g_I|+jMc85eUqDc6{RUW0>cjv5 literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/app/main.py b/apps/agent_gateway/app/main.py index a734d3d..06c5cd8 100644 --- a/apps/agent_gateway/app/main.py +++ b/apps/agent_gateway/app/main.py @@ -18,6 +18,7 @@ from agent_framework.global_supervisor import ( ) from agent_framework.llm.providers import create_llm from agent_framework.observability.observer import AgentObserver +from agent_framework.security import install_authentication from app.settings import settings @@ -25,6 +26,7 @@ logging.basicConfig(level=settings.LOG_LEVEL) logger = logging.getLogger("agent_gateway") app = FastAPI(title="Agent Gateway - Global Supervisor") +install_authentication(app, prefix="AGENT_GATEWAY_AUTH") app.add_middleware( CORSMiddleware, allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")], diff --git a/apps/agent_gateway/app/routes/__pycache__/governed_proxy_example.cpython-313.pyc b/apps/agent_gateway/app/routes/__pycache__/governed_proxy_example.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e4d8952b0c138830e2bd0db6ff5bd465717ce6f4 GIT binary patch literal 2404 zcmaJ@O>7fK6rR~#uh+jM*#P-*!X^QNp~3+gLilM(9TSq!h}!rac6I{^ zr?ec3)Kgng)Lz=EstUansXg=vq)6?lHmT%v6jW8T2W~;B%7GsG#`fZfs$F^KeKYUP z`8mihxiL(jZ@Hl-wv$5kt=+ABAjALxwNnYvP0_h)g^QJF8Dpa#5XjL&j5L ze@(^|CS{kG^tq@`)7Z_}9PF@O>_n55bLJAsU|r5dpj}FN1~EpX_&L8kX-{&KD_5s! zA=y};xu||pVU+S75Yl2=N-2ZlzoM1Xa#D47)v0@~8Up_RWY6?&h<|f^zl+jdH-1}4 zK}0F+W^NSb8}qKA?MOirci_DQ?*zPoF7yJb$iX>O&qqN)w5z5(3x4GU4Q+!{*3c9_ zCT>GH+_f>oTUcJuWw`xcPcsLk?Br8xV6i8o2y=V&0gwW{Boifo9Rd{DMY#eeL!8 z(>BozTcadLD1rLXoPuT%p zG?JJ&qrWl~A5TmS>*?gF%bTYi$2uH~%@i`m%yhwa4j=04>w9z-!hjO?I{8@welQ%n z-0-}GsKBHPHm~_Od(5{oF$HtpUM9P#~W>;VbIBhg3aV9;t=y9 z^H(G0O@VE-D6tGm^z%mM4Ps_>TJ0mu!yTMwfgyXr%*1E%&{CLWful^OwnOt4YoJEn zCVH+w^{RE&0vXIeZ`q*CwxEL%wuyJERK>>xr3FfP%`=6PDxWs+spOD(hxzJFO$0+x zAM?6CN#_gM1r~5!oue}>*Gn=(-daa?--C%&bVDE#}KSGOWEsVOOUq(KQ zeBN1Zk1YtJ`lR%WB_!weQx6mDX*aw13pTqIQU&v8^ZC3d^dau@RA&a z^`#~p@06ERiQ}yT>|JWZ97iyh>8xN)jt_X3cH($6xYQfu_yE_rbP#j-feJp{oahz5 z4va?`8 zhoJluNO>P+-kbrIV&y6PKS7lesAyoVaP9Wii&CG>XQ72$g0iGaMN{QUQrgV7weW#o zw3}!hMnt19Fy-iMAv-riUZVS93bzm0qcA{bF@Au$A0poawB;cR{)+lbsQ(Xi^fzDA tyX}keFQK00(2hm_nv4YHo%Xldi=kc1IJz#h;1RqoB4PLA3Z(l$+dm&_EO!6^ literal 0 HcmV?d00001 diff --git a/apps/agent_gateway/config/authentication.example.yaml b/apps/agent_gateway/config/authentication.example.yaml new file mode 100644 index 0000000..881e3c0 --- /dev/null +++ b/apps/agent_gateway/config/authentication.example.yaml @@ -0,0 +1,39 @@ +# Nunca coloque secrets diretamente neste arquivo. Use sempre *_env. +providers: + public: + mode: none + + deny: + mode: deny + + tia_basic: + mode: basic + client_id_env: TIA_AGENT_CLIENT_ID + secret_hash_env: TIA_AGENT_SECRET_HASH + realm: agent-contas + + platform_jwt: + mode: jwt + key_env: PLATFORM_JWT_PUBLIC_KEY + algorithms: [RS256] + audience: agent-platform + issuer: https://identity.example.com/ + +policies: + - name: health-public + provider: public + paths: [/health, /ready, /live] + + - name: tia-agent-api + provider: tia_basic + paths: [/gateway/message, /gateway/message/sse, /gateway/events/*] + methods: [GET, POST] + + - name: admin-api + provider: platform_jwt + paths: [/debug/*, /admin/*] + required_roles: [platform-admin] + required_scopes: [agent.admin] + +# Quando nenhuma política casar, rejeita. O default omitido também é deny. +default_provider: deny diff --git a/apps/mcp_gateway/app/__pycache__/__init__.cpython-313.pyc b/apps/mcp_gateway/app/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0e96f67474edd480be6afb954d87866dc0e523d5 GIT binary patch literal 129 zcmey&%ge<81go11vKWE%V-N=hKms7}nFUBpWk_exWb|9fP{afh0*T*JNGvEQ*3V5Y zh)+)}Ni9#T)CUUa$H!;pWtPOp>lIYq;;_lhPbtkwwJTx;Y5*Bn3}Sp_W@Kb6Vg|AR DoBJFR literal 0 HcmV?d00001 diff --git a/apps/mcp_gateway/app/__pycache__/main.cpython-313.pyc b/apps/mcp_gateway/app/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..83107d4891762a1fb9b2a09a78e2ec9bcdd99fca GIT binary patch literal 28096 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 None: diff --git a/apps/mcp_gateway/config/authentication.example.yaml b/apps/mcp_gateway/config/authentication.example.yaml new file mode 100644 index 0000000..881e3c0 --- /dev/null +++ b/apps/mcp_gateway/config/authentication.example.yaml @@ -0,0 +1,39 @@ +# Nunca coloque secrets diretamente neste arquivo. Use sempre *_env. +providers: + public: + mode: none + + deny: + mode: deny + + tia_basic: + mode: basic + client_id_env: TIA_AGENT_CLIENT_ID + secret_hash_env: TIA_AGENT_SECRET_HASH + realm: agent-contas + + platform_jwt: + mode: jwt + key_env: PLATFORM_JWT_PUBLIC_KEY + algorithms: [RS256] + audience: agent-platform + issuer: https://identity.example.com/ + +policies: + - name: health-public + provider: public + paths: [/health, /ready, /live] + + - name: tia-agent-api + provider: tia_basic + paths: [/gateway/message, /gateway/message/sse, /gateway/events/*] + methods: [GET, POST] + + - name: admin-api + provider: platform_jwt + paths: [/debug/*, /admin/*] + required_roles: [platform-admin] + required_scopes: [agent.admin] + +# Quando nenhuma política casar, rejeita. O default omitido também é deny. +default_provider: deny diff --git a/libs/agent_framework/pyproject.toml b/libs/agent_framework/pyproject.toml index 60e4936..c653a16 100644 --- a/libs/agent_framework/pyproject.toml +++ b/libs/agent_framework/pyproject.toml @@ -23,7 +23,8 @@ dependencies = [ "aiohttp>=3.9.0", "motor>=3.6.0", "google-cloud-pubsub>=2.28.0", - "mcp>=1.9.0" + "mcp>=1.9.0", + "PyJWT[crypto]>=2.9.0" ] [tool.setuptools.packages.find] diff --git a/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/__pycache__/__init__.cpython-313.pyc index 3cb376f15c43a61550e7b08cefb8093ab8e32e2a..35a52809e59818a3e0eed44e221a5656666bcdd2 100644 GIT binary patch delta 38 scmeBT>Sp5p%*)Hg00e8B3np@x^T+7t=9TEDB$g!VCzh6EOq@^!0Lxbl9RL6T delta 37 rcmeBX>SE&l%*)Hg00eXO^Cxnb^F{0D=9TEDB$g!VC#IE8oLB__wjB#Y diff --git a/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc b/libs/agent_framework/src/agent_framework/config/__pycache__/__init__.cpython-313.pyc index af2746d4f8e25378e7c4b23cd3954fe9e3ad031e..06f2a479da732e6325f2726798116cfeb7c5c8f4 100644 GIT binary patch delta 38 scmX@ic!ZJrGcPX}0}!ljE||z|%%7~En^&Trl30?cpIBOwG0~?S0M;xF2><{9 delta 37 rcmX@Yc$kseeRe>|S+GcPX}0}vSZY~)(3%9o^{n^&Trl30?cpO{v@d5_>P4?fmcByAF~hM%PGq3^6NVj48ka%QVd2PYYdDV4(S|$azrT33hITHEtRJ|9ml0H{5 ziO(RpkbwX_T#}363}o!flt(M_W&8e;y^+5W*)&pm?}TU4Usc_Oc<@}rMd5epr7CdS zIZX;Q?kiLry@(YgSt@)isKE5`SebhL6Tv}1kJ{9HJz7Qb8oMplXq1=)_ zFf&(z@s+l+bkWznxqcyFAtV##Cvhmxbx*_>ccycpYwMwmGsUB5?NGfRB|KOYvNVaL zFGXf@*;y^@r3`{dT$+mY+HIYoPp%LC&P85sA4=xwKp9~kIDsmnj~E~vL=90#;BSB? zVu)xV+K4V<1F?zNLi7+FgHv}`cUHUi->`+j?&IKb+6lR@63Ex0>iWE?0`3#gjmce) zmg>UvwZj-*0sBT~W=pj2Cb9AYuDFNUvdaWMdi>mcC3B^GB5=avxyP7{rDE(!n(NxrNfJyW@eCfBsQ7iWBZFTh%d&3lwq@Ut1!lLaePV^y6%p5D TbWOr5680Pb@dm;7TO{}pNl*{UdRe3-O4Nh2NSl%fQlbRPFa$10NFV^;1tpP| z8;#v@nTa#8(t0E}nK9j_W2s5U%5xw;4C% zW(SylmatBY>jrd0N1uA4r%#S>^vM&RJ`KbGPyK+g-$YFPW@4uGxB*MQm00_2#MW;o z_WlA=ptR>84k*tL6!sU9A}2G(4>hYpNk1Ezv65{t9t@;(%+$!Z4Q^v6TggDoRL!`} z+jMg7Hu=GjvYb*DC}mwys+_i9gFO3~eVbmcOe)+B?t*IO3`Z*6jqWD51LJ2{Qsr)T z7t-`@R;Q&k(6o9;b82ag6FS%02lzQIR>0<3~s^><>nLp$A1!Rnm(wA{oW$(`N*KOfrc6Nnr{H=@=IRzUffR z8}RvK5i&Ddqtfu7%lU9MP#|(>x;!m@bq*{5TRHp zdn5i}M<{q&%+2T!iGO99;GZVJ z*bHq?VH-^O#^?;OLywdo>%>|hf5H69UikX+3pU5=1D`n8-WX3fcV6m^+t&P?+exqh zEuJ;Kigd6`{gC~b>E%EZ@GzMMI++~k10%edls6-NGt)N^BM3}$_ zmC%jEosPgNv?GhOl`uHFCZj^e%5YwE4o1R)WR9w9(u8C!k`^R2NN@^e4z0mhy-Ki_h@S3(E**+H z)+f0w3zfB(4$gHYx!Q$V=cPk)vp?sYavNAZi_3~0W0&e6C!5P|-mRnB2Ly@mz&S&f zhydq6Y|jP91@gd&%_>bnlwmObI>WF_Hi$GaPH1|9c~&1_ud{>nldkI|P831`$w*`= z7qN=dN$@KrTjm5vO&RRZ=yhXep$4cX8i71uE-`oa|4u~h@S2WeX>WtqIkFKF2pS^E zK!n&d2~%EPH{_j;4lJCH|81Co#0h^Qis2i~FS)}qcQHMS4F*Vnb<8e>Ao4cj0V;=~ zOlB!*jm!u$q}W5X96sHwYKunL$LFj1xS4V32EXK|V}T7jTzZL{obvgl0)J#G>LY@8 zJU9VbRWg7&jf@MRpt+C`h)FyNPE5wKrlHL12OS>sfs%*?L1)2C`9dPu34=nbNtfRx zX3^?MS?lgu>#oM$edV23u05Nq-*RK*M`J%6yVaW9)}OQv#JK^=AV2Uz3mR|kLDG|X z<7V@7N43EDd86g(wkhT!toLBgGz^*4So#UGpjY`lWKE~5F}1}ZRB}1Nty62l4Cn{J zOicPVZ`&@hyIn@fIO!87LAy%U=;^cLfz4i=6|w`GkPN4NqOg6dR5VROqAwtLPjBBU z&yHjm7jTwf&4OkLM!|F%M8OXW7{tdHibC$&XP-Mxg_6PlwL(m^C zQxPLDclOeh-FeUMoUgpL{e#`tcYlA+%>(~B@Z*7>yl}TGX+L(UchOpuvNqkbHqG}Y zt?N?O%?az~q;+e`x-((jd2@f#x;M`4m1j3|O`D;4y7K=AaMarj%nm$rLwaT7kTCeF z8OEcnlF_GL@2j-0&44-sXqd99f(y0xM z2MrSq9yDb%Y}FYVAIzSUsb?k(0cN+po(ZrnK?-!ym(HR9K6zVwjjH<#CAQvrPS-PNnn?K zBqp8@#wMxyl59knikuUoL_$T6(G25Jv?MP^L%|po8%ZAwgTNibu^*I8 z`|wOuj=GHGAXJxZB7i_)+)F2Zb}$8J=AgLs)hACQtP`D>$`7?w|bK6dlNR-T+h|5 zizRJ8sJ#*Rc>i6??ZZjeaQxVd$r5ke=6z&fYz_0}N$d7FxBZJpJX2KjT|g-dr^+T!_6z%eqNTqTxk{>lZz-i7h(uf*f zMfjbpd3fv*jKxwiC zNe+HIiD^iX4WbFi8_cqf<&U#6gu$%;1KX<)2;rGk7{3pj&a@GTbtvNzd#+?Wvb$5K zF}PQ1&H#LX1HI8mDCnQ@!e$e3N+38f4Wf6Qr3l)^%FNHt_tw>inZ^12Ffb7nC8A2? z{K5D$#}kgxxM@^jxz+wemiwSSL4pj(VEGA!<(QSlay=wKV3%<4IhY)FH!>;*{{ZZA z+TeN*K^S@kz9D-j5;?bq?!F2UI4)de;a5_7|l!tcy< zHw#!!`R1^0M3>KotJZ!96-Z71MWJvHIg4Q&c6c5{MKqwDb{hcEQqP#0DnAPtw{eT! zZR*rFgPr%-)ZC#0HI{w?TGb6{Rz02WHoGnA_}vy1G`H1l>(mWtRzIVxT^(y!Kcdgu zT0y$CY2Zrqpa=J>G>q;YLe-QL)V_DK>i7G~5EpoGdq>0Qks%cOoS@bV8%7Lj%@v$; zJPqeEcy0AGsNIZk?n1Q>_1Vlk2F-wP0!E!VZ%@&?sH=w!l8S$D+v_SCl)VZO5}c)Q zU1DYt_6PmWUa4v-D2m_+^U?$_!25CZKEPgU)=uTZNkwXRqr^5|*QGrK66<_`%M3ka zCV(6iY7ktPWB`a%6%5nVmF-GA?aEaI+Y-SCTX<2@Pr#;v4@J&{+u{Xmg1Q<|Xh{z? zW`<3Pycr=!FmXQ;lqacpBpk#|$^p7nq0oqeELrvu!pI814lw{q8!!QP<%p{xAHp-~{iZ4s2~rv?z^Rl z^1)R3NTPfse(Y4T{KdJxg>{>8=XJB>rZ2f}Z^G6#Hvp)qsAc}_kGg-@oh;m!Dm?%3 ztMQTXRN!19a4y+*evbdlQTi04$-oK|$@XJO$MLx7_!p0?0K)FqZj3uR5QV{TpYUDx z;JW~(y~6%MM2qxVPxXOr_QN$j)&maaqqd$_c>CDlIaLyhdn@mZmYpG1rZGT{K~=gD5JmpJoy>kf6;I?LgjO zeyh{-HOm$oKdM+>%=g2e78FbHM_Jz5o@j7DSPlG)K_B@|FYXAkO)seQJSIssHzQ2i z7=vNfN~(sLlkDpDg4$#9Ds&)OWl^KPXX@DepHK~))@!u?6Jyiq90C0q#0?P2iF!2FqS+PI=vrPTUQXRVX#+n zs0tcn!FyJiAp*2CEE$zomx;Ur3FIOYI!9EQ(kY|b8T*!JQ=g$$O{(G)adD_yOIc02 z9y^&C09!Z>bw%6`P(4{vePz$(J*kqGL`lnhEYRzItsm_3WX< zvxidk{fYX1MW6I0O8VlqK1~Ug-l_YrE`Ic6vfYz(jKxi3zZ20D^IFd{JzG#wWUoXx zIhr+JKqavO7&B|ZhzvMEY0#onK~S#H7%pF;DGl-lzUFLx<}eKjN@IpnXj-;#{3%5# zV7?#zw17;6@JA_yMmIf`*(ZSGmE<>04}#FRU+IMQgt!yl*(CrUIjSKOm8$};lPs~w zSs^S-dlS?FW^RB7xq<`}vKIoC>UB>ZnI_w3un8LwknQT`HyuxsPCQhXb8~ z`X{&kitHyf#||YD<5U6w-Jk9!ajI9f*fOlwp!|Vie;Cng>bEk z-v;vZXss&c*phH;QMAxN{J1w!@>1OP(n4YR729RoRrh>DvaosnXri!nj$d@tULAS= z@Oy_-j;#p?_^~L&OIOO(UMG_6CzB3O+~iTT*S?m0Z7jHYP|T>nA;PiC%v2Q?Crjef zs;R%irjVd!(A1QUZ(24P`7uRJVZI;!w4j$`tEwsLDNwuyoZ>a$!E2xcuR&G+`PeGj z=>jx6=tpT#0~%cDj9vO`h`bG3R1jTefC=-XM?z{DbR{SQ$`ry8&Fu{+Mr#vL3>pF& zM)!s^BRzo|Gi4LbFyLB-jc(ZB(dwm-TSg25ToVZ!dJF+p-FCqN48h8?uKwf0&Wtq$ zt|Qn_y*wQxg6KRl1GyeQnTf_Cr(7mUf98BlvL2d>Mo3I{^4f-`VJ|kNxP0|sjNrg5 z2nT2uUuXihbFs-OQ8M|a$H5umhl3i}%1sMI-RE3`5s<$}@_i&fK=OA$B)xFKzj7-> zRdeI2v!Q9^8E8U8dA!6t)alJ5?5DQ!hX$s!;!V>+N%@`bXHs;<|W!f3kQWesnl)8~!~a{1Ns`5l!Z6TnF=krN^TGv8{*K zf5`I?&GOutpwR2M1l|G;|NqeRaH7Htp^eK4`A%8D78K?bgV+4J98!^`IRR3&Qd!K_rvfMCM;NtpM*800Xdd3n?n^goU8`8+;D&U6_yruLKLd9lg16um1T z?0gU3tUS{~Pkk-L8CH^@lIkVj0ZP7$B-?A5v6 zM<%AE{H?&1GndcImnF+vQ{_W&dfj!s>&DsSx_$6=E0ipD&lxr2U2%79{OIvy`)JZ} zB5peI#UnFQShd9HpzlRn@wW!PJ`k^LzqbE_7p}jMTE8!`e&2oDehLL{oVq)iC^`N! z+wtE&w`0Uh{!%v~or(dqp-s^|xw9`!BIAbezLBs$uhe zQ=NQ>A$v>eH~!32yV5(NS=x=<{~t^H#4`;hpM3@E$g<2K0GpFA?dRCH^@F^P33Fie z@|UlIf@FssoG6 z=d*=sei-jA!#~yD9Rd)W*}juGZpe!O3RSz^rgZ8%0Qe(+*`gI?I{}h6siPeM3y+9e zEtSskpn9QUNQDiogZn5onwY^)8kqGA zokNgSd3~A~8@+CYEscX24e0Q0j1&G1{y2i5w&kbyjnLU96~0RkYo!XuDCGdgfr_nS-f{-g_0j zcUu-3THmjEuV$gNGF951C~aS;ZkXQ|uW~I^*S)*(osHL;Q|-GF?YrVtyBF51jkk8j zYr7trnZ^wd8NRW2nc-`T-!eV4F-29WLRX^Db-!@kLRG`NYu;J&o%**7%SB9O9}8VQ zEN2SKzcu^y*{f|SM^nPlbj|RA<+|m@wp3eZqOJ3%{#z|~%RbzY>^^dT`1t+NvDB#V z-l#7*>Q4@jC%eZLug<~v(G!W1lX2V0#p1Fo_!!QDkKtQ^J7+#T6F=%n_Ku~>!||8G z$>K;RoQ(Is6hA$gY!47epVy3A4isQ25d)06^ z8@JxTO!d5NY&=mS+_MR|_YMHGybN;*(DJ!5fR>GY?aW68w$Ehv$f(D-rK_({f16|b ztcKfsGsaz3NWZ<6?JMAJZ{slDx!(bm@0eJ4xnpIa(47JrFEsSE8t;^|eO0zQ6*i1F zVWW3iSS;U4MUF6yW!fS*90L_+c@kQ}Eibx5syGNUw* zi#lE01Oz~C-TGAB=0x4*WZhOg0j&A#z-YXAN2<9y(cFE%d2js1)0o__(75$yruvoG z7af7GqnGIo;M7<3G69+cE1VlNOP_HvNKg4Rr(^n*4_ZJ~of9;~<%2oQdw)kOaRf>{ z^^K$ay=mixn>kv$fO4|VB`i5ZgjW@#sC8C+BmAZV@78(J-_3haR%sZ~xwRX>YD>xq zgb_|HE7aqLm@GeDWd+jHOQ}M2mqZDOl~)W-gnjUH3t?EYtL14<*5M{ygY-yKTZr`T zEB#rJx=^W&1<_#$D$1ON+5t&Y$5SlSOyl`a*bX*Ejc0Mym5$3Dsp7SX;J1gNc&Eaogbq{K-Qz{J&P3a_xox3l()&zIOR*sfzYQMfkcF<4$eLISxL?JcHgK>?Ho$%97;906HV^Lignix#yf`+74CSUJHtuQvEp!H&wdt< zMo}U?@9Nxrwe9bP3Py3F7T6^Y}URr|@IAQDsq1h+VnAvS}*Hzau4 zKyc*~6B6`%lK%iA83R7JC>{;UzY{XUji~3TCr);}{Ues6r(SfWqHM|coDN}Z6v;^> zpc&-5C-khDp5#zJ47F~Q!#M}jl4+OxOTcdOpHLUH9V31Y1aJY%{(@=zHDmvjDfpDJ z%Ji2^+b@|dNoLEh8QU+InqQd=IX0ElIG@1hR+R*&iYn!f~{EA>DgTm86Zn~ zMpp?Z1?;ZRZRuQ&ZFtB4QF0;nsDNdUuvbSPG5CCBV%g`}-e%D# z=M2(ii;g`(<0l|q%#K1F>2kY;Jq2;3%hfh^4B|+a>nhlj5J$SawT3+laiq(;3Ly74 J3{q+X{ud#jYhM5W literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/security/__pycache__/factory.cpython-313.pyc b/libs/agent_framework/src/agent_framework/security/__pycache__/factory.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c721f3e3d5ee299411c1f1fb6d0bbdb35894c14 GIT binary patch literal 7977 zcmbU`OKclQmi?pnC-t@Uv24ncCDT8}maXwWk}29E^|RTeZCP?Cifl=g`BAEylI2Vs zE#{)k9PC|8u*ys@F%|*V0Tvh?b=bqqVRmK~3#6GgV4h zhA;rxAvVYnPQ!6SreGDRg0_jT8Zrkh#1ga;YtTk)K|8Sr9mElI5@*mwTtPQ+1D%UE zH&gQ*spc&_!&^@=yqmWnW}YE6d^K;^dTaR_zLs}rc$(C~R~_#xeAUk~-umxQRWGgD zW0_1=ib=_AMjVB2^GIxAA(@$lCflD`{*Fd6i+Z4cAvq*0_vfTJAtNQ@+JKuRyO>M} zL~Xei6O-`@oy`M6rqaNs;F45fSmS86LXUS0JI(|$63L1SLL7CkKzf)*)8f~J7!gP% znlQ~ z97#&c-7~SclqJij7M4|KloSk$f_9`4WY@t@JP*|hwdHU<3H&_pH1UhE75}E$@ed!a z`9B}}bm;SuPe-;~jdDv^j*QFUdveo+;+m8#lbiPHmEmm|OI-)EECz4YoDgPWxs(() zCJ!M_k2b}tFm<266oz&N05X9Rp^68PjUO zj48qtd3Ay;lNLCpkaZUu`23rJV>jq{Cc^Cs9Md#t7BO{D5f*-b)3&>H=L*`FX>v)^ z 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 F{{snLn-Ty3 literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/security/__pycache__/installer.cpython-313.pyc b/libs/agent_framework/src/agent_framework/security/__pycache__/installer.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e80012498690c30d4337eece76a5fb6c9814996 GIT binary patch literal 5595 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 literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/security/__pycache__/middleware.cpython-313.pyc b/libs/agent_framework/src/agent_framework/security/__pycache__/middleware.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..57c4c9c1fb3d04b3e3ca6008392564b716b3e584 GIT binary patch literal 8879 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 literal 0 HcmV?d00001 diff --git a/libs/agent_framework/src/agent_framework/security/authentication.py b/libs/agent_framework/src/agent_framework/security/authentication.py new file mode 100644 index 0000000..17d2ba8 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/security/authentication.py @@ -0,0 +1,190 @@ +from __future__ import annotations + +import base64 +import hashlib +import hmac +import logging +import time +from dataclasses import dataclass, field +from typing import Any, Mapping, Protocol, Sequence + +import httpx +from fastapi import Request + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class AuthenticatedPrincipal: + subject: str + scheme: str + claims: Mapping[str, Any] = field(default_factory=dict) + + +@dataclass(frozen=True) +class AuthenticationResult: + authenticated: bool + principal: AuthenticatedPrincipal | None = None + error: str | None = None + challenge: str | None = None + + +class AuthenticationProvider(Protocol): + async def authenticate(self, request: Request) -> AuthenticationResult: ... + + +def _constant_time_equals(left: str, right: str) -> bool: + return hmac.compare_digest(left.encode("utf-8"), right.encode("utf-8")) + + +def _pbkdf2_hash(secret: str, salt: str, iterations: int = 310_000) -> str: + digest = hashlib.pbkdf2_hmac("sha256", secret.encode(), salt.encode(), iterations) + return base64.urlsafe_b64encode(digest).decode().rstrip("=") + + +def verify_secret(secret: str, stored_value: str) -> bool: + """Accepts plain:, sha256:, or pbkdf2_sha256:::.""" + if stored_value.startswith("plain:"): + return _constant_time_equals(secret, stored_value.removeprefix("plain:")) + if stored_value.startswith("sha256:"): + candidate = hashlib.sha256(secret.encode()).hexdigest() + return _constant_time_equals(candidate, stored_value.removeprefix("sha256:")) + if stored_value.startswith("pbkdf2_sha256:"): + try: + _, iterations, salt, expected = stored_value.split(":", 3) + return _constant_time_equals(_pbkdf2_hash(secret, salt, int(iterations)), expected) + except (ValueError, TypeError): + return False + return _constant_time_equals(secret, stored_value) + + +class NoAuthenticationProvider: + async def authenticate(self, request: Request) -> AuthenticationResult: + return AuthenticationResult(True, AuthenticatedPrincipal("anonymous", "none")) + + +class DenyAuthenticationProvider: + async def authenticate(self, request: Request) -> AuthenticationResult: + return AuthenticationResult(False, error="authentication_policy_not_configured") + + +class BasicAuthenticationProvider: + def __init__(self, client_id: str, secret_hash: str, realm: str = "agent-api"): + self.client_id = client_id + self.secret_hash = secret_hash + self.realm = realm + + async def authenticate(self, request: Request) -> AuthenticationResult: + header = request.headers.get("authorization", "") + if not header.lower().startswith("basic "): + return AuthenticationResult(False, error="missing_basic_credentials", challenge=f'Basic realm="{self.realm}"') + try: + decoded = base64.b64decode(header.split(" ", 1)[1], validate=True).decode("utf-8") + supplied_id, supplied_secret = decoded.split(":", 1) + except (ValueError, UnicodeDecodeError): + return AuthenticationResult(False, error="invalid_basic_credentials", challenge=f'Basic realm="{self.realm}"') + valid = _constant_time_equals(supplied_id, self.client_id) and verify_secret(supplied_secret, self.secret_hash) + if not valid: + return AuthenticationResult(False, error="invalid_basic_credentials", challenge=f'Basic realm="{self.realm}"') + return AuthenticationResult(True, AuthenticatedPrincipal(supplied_id, "basic")) + + +class ApiKeyAuthenticationProvider: + def __init__(self, expected_hash: str, header_name: str = "x-api-key", principal: str = "api-client"): + self.expected_hash = expected_hash + self.header_name = header_name.lower() + self.principal = principal + + async def authenticate(self, request: Request) -> AuthenticationResult: + supplied = request.headers.get(self.header_name) + if not supplied or not verify_secret(supplied, self.expected_hash): + return AuthenticationResult(False, error="invalid_api_key") + return AuthenticationResult(True, AuthenticatedPrincipal(self.principal, "api_key")) + + +class StaticBearerAuthenticationProvider: + def __init__(self, token_hash: str, principal: str = "bearer-client"): + self.token_hash = token_hash + self.principal = principal + + async def authenticate(self, request: Request) -> AuthenticationResult: + header = request.headers.get("authorization", "") + if not header.lower().startswith("bearer "): + return AuthenticationResult(False, error="missing_bearer_token", challenge="Bearer") + token = header.split(" ", 1)[1] + if not verify_secret(token, self.token_hash): + return AuthenticationResult(False, error="invalid_bearer_token", challenge="Bearer") + return AuthenticationResult(True, AuthenticatedPrincipal(self.principal, "bearer")) + + +class JwtAuthenticationProvider: + def __init__(self, key: str, algorithms: Sequence[str], audience: str | None = None, issuer: str | None = None): + try: + import jwt # type: ignore + except ImportError as exc: + raise RuntimeError("JWT authentication requires PyJWT[crypto]") from exc + self.jwt = jwt + self.key = key + self.algorithms = list(algorithms) + self.audience = audience + self.issuer = issuer + + async def authenticate(self, request: Request) -> AuthenticationResult: + header = request.headers.get("authorization", "") + if not header.lower().startswith("bearer "): + return AuthenticationResult(False, error="missing_bearer_token", challenge="Bearer") + token = header.split(" ", 1)[1] + try: + claims = self.jwt.decode(token, self.key, algorithms=self.algorithms, audience=self.audience, issuer=self.issuer) + except Exception as exc: + logger.info("JWT rejected: %s", exc.__class__.__name__) + return AuthenticationResult(False, error="invalid_jwt", challenge="Bearer") + subject = str(claims.get("sub") or claims.get("client_id") or "jwt-client") + return AuthenticationResult(True, AuthenticatedPrincipal(subject, "jwt", claims)) + + +class OAuth2IntrospectionAuthenticationProvider: + def __init__(self, introspection_url: str, client_id: str, client_secret: str, timeout_seconds: float = 5.0): + self.introspection_url = introspection_url + self.client_id = client_id + self.client_secret = client_secret + self.timeout_seconds = timeout_seconds + + async def authenticate(self, request: Request) -> AuthenticationResult: + header = request.headers.get("authorization", "") + if not header.lower().startswith("bearer "): + return AuthenticationResult(False, error="missing_bearer_token", challenge="Bearer") + token = header.split(" ", 1)[1] + try: + async with httpx.AsyncClient(timeout=self.timeout_seconds) as client: + response = await client.post( + self.introspection_url, + data={"token": token}, + auth=(self.client_id, self.client_secret), + headers={"accept": "application/json"}, + ) + response.raise_for_status() + claims = response.json() + except (httpx.HTTPError, ValueError): + return AuthenticationResult(False, error="introspection_unavailable", challenge="Bearer") + if not claims.get("active") or (claims.get("exp") and int(claims["exp"]) <= int(time.time())): + return AuthenticationResult(False, error="inactive_token", challenge="Bearer") + subject = str(claims.get("sub") or claims.get("client_id") or claims.get("username") or "oauth-client") + return AuthenticationResult(True, AuthenticatedPrincipal(subject, "oauth2_introspection", claims)) + + +class TrustedProxyAuthenticationProvider: + def __init__(self, subject_header: str = "x-authenticated-subject", shared_secret_header: str | None = None, shared_secret_hash: str | None = None): + self.subject_header = subject_header.lower() + self.shared_secret_header = shared_secret_header.lower() if shared_secret_header else None + self.shared_secret_hash = shared_secret_hash + + async def authenticate(self, request: Request) -> AuthenticationResult: + subject = request.headers.get(self.subject_header) + if not subject: + return AuthenticationResult(False, error="missing_trusted_subject") + if self.shared_secret_header and self.shared_secret_hash: + supplied = request.headers.get(self.shared_secret_header) + if not supplied or not verify_secret(supplied, self.shared_secret_hash): + return AuthenticationResult(False, error="invalid_proxy_signature") + return AuthenticationResult(True, AuthenticatedPrincipal(subject, "trusted_proxy")) diff --git a/libs/agent_framework/src/agent_framework/security/factory.py b/libs/agent_framework/src/agent_framework/security/factory.py new file mode 100644 index 0000000..391a233 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/security/factory.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import os +from collections.abc import Mapping +from typing import Any + +from .authentication import ( + ApiKeyAuthenticationProvider, + BasicAuthenticationProvider, + DenyAuthenticationProvider, + JwtAuthenticationProvider, + NoAuthenticationProvider, + OAuth2IntrospectionAuthenticationProvider, + StaticBearerAuthenticationProvider, + TrustedProxyAuthenticationProvider, +) + + +def _required_env(name: str) -> str: + value = os.getenv(name) + if value is None or not value.strip(): + raise ValueError(f"Required authentication environment variable is missing: {name}") + return value + + +def _resolve(config: Mapping[str, Any], key: str, *, required: bool = False, default: Any = None) -> Any: + env_key = config.get(f"{key}_env") + if env_key: + value = os.getenv(str(env_key)) + if required and (value is None or not value.strip()): + raise ValueError(f"Required authentication environment variable is missing: {env_key}") + return value if value is not None else default + value = config.get(key, default) + if required and (value is None or (isinstance(value, str) and not value.strip())): + raise ValueError(f"Required authentication configuration is missing: {key}") + return value + + +def create_provider_from_config(config: Mapping[str, Any]): + """Create a provider from a secret-safe mapping. + + Secret values may be supplied indirectly with ``_env`` keys so YAML + never needs to contain credentials. + """ + mode = str(config.get("mode", "none")).strip().lower() + if mode in {"none", "disabled"}: + return NoAuthenticationProvider() + if mode in {"deny", "reject"}: + return DenyAuthenticationProvider() + if mode == "basic": + return BasicAuthenticationProvider( + str(_resolve(config, "client_id", required=True)), + str(_resolve(config, "secret_hash", required=True)), + str(_resolve(config, "realm", default="agent-api")), + ) + if mode == "api_key": + return ApiKeyAuthenticationProvider( + str(_resolve(config, "api_key_hash", required=True)), + str(_resolve(config, "header", default="x-api-key")), + str(_resolve(config, "principal", default="api-client")), + ) + if mode == "bearer_static": + return StaticBearerAuthenticationProvider( + str(_resolve(config, "token_hash", required=True)), + str(_resolve(config, "principal", default="bearer-client")), + ) + if mode == "jwt": + algorithms = _resolve(config, "algorithms", default=["RS256"]) + if isinstance(algorithms, str): + algorithms = [item.strip() for item in algorithms.split(",") if item.strip()] + return JwtAuthenticationProvider( + str(_resolve(config, "key", required=True)), + algorithms, + _resolve(config, "audience"), + _resolve(config, "issuer"), + ) + if mode == "oauth2_introspection": + return OAuth2IntrospectionAuthenticationProvider( + str(_resolve(config, "introspection_url", required=True)), + str(_resolve(config, "client_id", required=True)), + str(_resolve(config, "client_secret", required=True)), + float(_resolve(config, "timeout_seconds", default=5)), + ) + if mode == "trusted_proxy": + return TrustedProxyAuthenticationProvider( + str(_resolve(config, "subject_header", default="x-authenticated-subject")), + _resolve(config, "shared_secret_header"), + _resolve(config, "shared_secret_hash"), + ) + raise ValueError(f"Unsupported authentication mode: {mode}") + + +def env_provider_config(prefix: str = "AGENT_AUTH") -> dict[str, Any]: + mode = os.getenv(f"{prefix}_MODE", "none").strip().lower() + config: dict[str, Any] = {"mode": mode} + if mode == "basic": + config.update(client_id=_required_env(f"{prefix}_BASIC_CLIENT_ID"), secret_hash=_required_env(f"{prefix}_BASIC_SECRET_HASH"), realm=os.getenv(f"{prefix}_BASIC_REALM", "agent-api")) + elif mode == "api_key": + config.update(api_key_hash=_required_env(f"{prefix}_API_KEY_HASH"), header=os.getenv(f"{prefix}_API_KEY_HEADER", "x-api-key"), principal=os.getenv(f"{prefix}_API_KEY_PRINCIPAL", "api-client")) + elif mode == "bearer_static": + config.update(token_hash=_required_env(f"{prefix}_BEARER_TOKEN_HASH"), principal=os.getenv(f"{prefix}_BEARER_PRINCIPAL", "bearer-client")) + elif mode == "jwt": + config.update(key=_required_env(f"{prefix}_JWT_KEY"), algorithms=os.getenv(f"{prefix}_JWT_ALGORITHMS", "RS256"), audience=os.getenv(f"{prefix}_JWT_AUDIENCE") or None, issuer=os.getenv(f"{prefix}_JWT_ISSUER") or None) + elif mode == "oauth2_introspection": + config.update(introspection_url=_required_env(f"{prefix}_OAUTH2_INTROSPECTION_URL"), client_id=_required_env(f"{prefix}_OAUTH2_CLIENT_ID"), client_secret=_required_env(f"{prefix}_OAUTH2_CLIENT_SECRET"), timeout_seconds=float(os.getenv(f"{prefix}_OAUTH2_TIMEOUT_SECONDS", "5"))) + elif mode == "trusted_proxy": + config.update(subject_header=os.getenv(f"{prefix}_PROXY_SUBJECT_HEADER", "x-authenticated-subject"), shared_secret_header=os.getenv(f"{prefix}_PROXY_SHARED_SECRET_HEADER") or None, shared_secret_hash=os.getenv(f"{prefix}_PROXY_SHARED_SECRET_HASH") or None) + return config + + +def create_authentication_provider(prefix: str = "AGENT_AUTH"): + return create_provider_from_config(env_provider_config(prefix)) diff --git a/libs/agent_framework/src/agent_framework/security/installer.py b/libs/agent_framework/src/agent_framework/security/installer.py new file mode 100644 index 0000000..dec02c1 --- /dev/null +++ b/libs/agent_framework/src/agent_framework/security/installer.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import os +from pathlib import Path +from typing import Any + +import yaml +from fastapi import FastAPI + +from .authentication import DenyAuthenticationProvider +from .factory import create_authentication_provider, create_provider_from_config +from .middleware import AuthenticationMiddleware, AuthenticationPolicy, PolicyAuthenticationMiddleware + + +def _csv(value: str | None, default: str = "") -> list[str]: + return [item.strip() for item in (value if value is not None else default).split(",") if item.strip()] + + +def _bool(value: str | None, default: bool = False) -> bool: + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + + +def load_authentication_policies(path: str | Path) -> tuple[list[AuthenticationPolicy], Any]: + raw = yaml.safe_load(Path(path).read_text(encoding="utf-8")) or {} + providers = { + name: create_provider_from_config(config or {}) + for name, config in (raw.get("providers") or {}).items() + } + policies: list[AuthenticationPolicy] = [] + for index, item in enumerate(raw.get("policies") or []): + provider_name = item.get("provider") + if provider_name not in providers: + raise ValueError(f"Unknown authentication provider in policy: {provider_name}") + policies.append(AuthenticationPolicy( + name=str(item.get("name") or f"policy-{index + 1}"), + provider=providers[provider_name], + paths=tuple(item.get("paths") or ["*"]), + methods=frozenset(str(method).upper() for method in (item.get("methods") or [])), + required_roles=frozenset(str(role) for role in (item.get("required_roles") or [])), + required_scopes=frozenset(str(scope) for scope in (item.get("required_scopes") or [])), + )) + default_name = raw.get("default_provider") + default_provider = providers.get(default_name) if default_name else DenyAuthenticationProvider() + return policies, default_provider + + +def install_authentication(app: FastAPI, prefix: str = "AGENT_AUTH") -> bool: + """Install optional authentication using an isolated environment prefix. + + Returns True when middleware was installed. Authentication remains disabled + unless ``_ENABLED=true`` or a non-``none`` mode/policy file is set. + """ + policy_file = os.getenv(f"{prefix}_POLICIES_FILE") + mode = os.getenv(f"{prefix}_MODE", "none").strip().lower() + enabled = _bool(os.getenv(f"{prefix}_ENABLED"), default=bool(policy_file or mode not in {"none", "disabled"})) + if not enabled: + return False + + if policy_file: + policies, default_provider = load_authentication_policies(policy_file) + app.add_middleware(PolicyAuthenticationMiddleware, policies=policies, default_provider=default_provider) + return True + + provider = create_authentication_provider(prefix) + public_paths = _csv(os.getenv(f"{prefix}_PUBLIC_PATHS"), "/health,/ready,/live,/docs,/openapi.json,/redoc") + public_prefixes = _csv(os.getenv(f"{prefix}_PUBLIC_PREFIXES")) + app.add_middleware(AuthenticationMiddleware, provider=provider, public_paths=public_paths, public_prefixes=public_prefixes) + return True diff --git a/libs/agent_framework/src/agent_framework/security/middleware.py b/libs/agent_framework/src/agent_framework/security/middleware.py new file mode 100644 index 0000000..470692c --- /dev/null +++ b/libs/agent_framework/src/agent_framework/security/middleware.py @@ -0,0 +1,100 @@ +from __future__ import annotations + +import fnmatch +import logging +from collections.abc import Iterable, Sequence +from dataclasses import dataclass, field + +from fastapi import Request +from fastapi.responses import JSONResponse +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import Response + +from .authentication import AuthenticationProvider, DenyAuthenticationProvider + +logger = logging.getLogger(__name__) + + +@dataclass(frozen=True) +class AuthenticationPolicy: + name: str + provider: AuthenticationProvider + paths: tuple[str, ...] = ("*",) + methods: frozenset[str] = field(default_factory=frozenset) + required_roles: frozenset[str] = field(default_factory=frozenset) + required_scopes: frozenset[str] = field(default_factory=frozenset) + + def matches(self, path: str, method: str) -> bool: + method_matches = not self.methods or method.upper() in self.methods + return method_matches and any(fnmatch.fnmatchcase(path, pattern) for pattern in self.paths) + + +def _claim_values(claims, names: Sequence[str]) -> set[str]: + values: set[str] = set() + for name in names: + raw = claims.get(name) + if isinstance(raw, str): + values.update(item for item in raw.replace(",", " ").split() if item) + elif isinstance(raw, (list, tuple, set)): + values.update(str(item) for item in raw) + return values + + +class AuthenticationMiddleware(BaseHTTPMiddleware): + """Backward-compatible single-provider middleware.""" + + def __init__(self, app, provider: AuthenticationProvider, public_paths: Iterable[str] = (), public_prefixes: Iterable[str] = ()): + super().__init__(app) + self.provider = provider + self.public_paths = frozenset(public_paths) + self.public_prefixes = tuple(public_prefixes) + + def _is_public(self, path: str) -> bool: + return path in self.public_paths or any(path.startswith(prefix) for prefix in self.public_prefixes) + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + if request.method == "OPTIONS" or self._is_public(request.url.path): + return await call_next(request) + return await _authenticate_request(request, call_next, self.provider) + + +class PolicyAuthenticationMiddleware(BaseHTTPMiddleware): + """Selects the first matching route policy and authenticates the request.""" + + def __init__(self, app, policies: Sequence[AuthenticationPolicy], default_provider: AuthenticationProvider | None = None): + super().__init__(app) + self.policies = tuple(policies) + self.default_provider = default_provider or DenyAuthenticationProvider() + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + if request.method == "OPTIONS": + return await call_next(request) + policy = next((item for item in self.policies if item.matches(request.url.path, request.method)), None) + if policy is None: + return await _authenticate_request(request, call_next, self.default_provider) + return await _authenticate_request( + request, + call_next, + policy.provider, + policy_name=policy.name, + required_roles=policy.required_roles, + required_scopes=policy.required_scopes, + ) + + +async def _authenticate_request(request: Request, call_next: RequestResponseEndpoint, provider: AuthenticationProvider, *, policy_name: str | None = None, required_roles: frozenset[str] = frozenset(), required_scopes: frozenset[str] = frozenset()) -> Response: + result = await provider.authenticate(request) + if not result.authenticated or result.principal is None: + headers = {"WWW-Authenticate": result.challenge} if result.challenge else None + return JSONResponse(status_code=401, content={"detail": "Unauthorized", "code": result.error or "unauthorized", "policy": policy_name}, headers=headers) + + roles = _claim_values(result.principal.claims, ("roles", "role", "groups")) + scopes = _claim_values(result.principal.claims, ("scope", "scp", "scopes")) + if required_roles and not required_roles.issubset(roles): + return JSONResponse(status_code=403, content={"detail": "Forbidden", "code": "missing_required_role", "policy": policy_name}) + if required_scopes and not required_scopes.issubset(scopes): + return JSONResponse(status_code=403, content={"detail": "Forbidden", "code": "missing_required_scope", "policy": policy_name}) + + request.state.auth_principal = result.principal + request.state.auth_policy = policy_name + return await call_next(request) diff --git a/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc b/tests/__pycache__/conftest.cpython-313-pytest-9.0.2.pyc index dc006d715c6ef90211606fb5d721424d53358e03..db53f3f8ef98d16bce001c8430beee43c5bfb92e 100644 GIT binary patch delta 40 ucmbQjHkFP0GcPX}0}yO(F4)NJ%EWJ}pPN^rpORRTsGnF`k})}sX&V6Cz6=@w delta 39 tcmbQrHieD*GcPX}0}yQT&fCcC%EV`(pPN^rpORRTsGpctJ~^Ig8vx3H3u*uW diff --git a/tests/unit/__pycache__/test_authentication.cpython-313-pytest-9.0.2.pyc b/tests/unit/__pycache__/test_authentication.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a19dcc6475f0a08bd3ffa2a31654d21a6549aace GIT binary patch literal 8555 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 Lg!ywkwex=fu0Jnt literal 0 HcmV?d00001 diff --git a/tests/unit/__pycache__/test_authentication_policies.cpython-313-pytest-9.0.2.pyc b/tests/unit/__pycache__/test_authentication_policies.cpython-313-pytest-9.0.2.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f8d2b6b8ac1725d3ef6d23655b3549c9c32fde3e GIT binary patch literal 9458 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 literal 0 HcmV?d00001 diff --git a/tests/unit/test_authentication.py b/tests/unit/test_authentication.py new file mode 100644 index 0000000..661a6fa --- /dev/null +++ b/tests/unit/test_authentication.py @@ -0,0 +1,50 @@ +from __future__ import annotations + +import base64 +import hashlib + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from agent_framework.security.authentication import ApiKeyAuthenticationProvider, BasicAuthenticationProvider +from agent_framework.security.middleware import AuthenticationMiddleware + + +def _basic(value: str) -> str: + return "Basic " + base64.b64encode(value.encode()).decode() + + +def test_basic_authentication_protects_endpoint_and_keeps_health_public(): + app = FastAPI() + app.add_middleware( + AuthenticationMiddleware, + provider=BasicAuthenticationProvider("tia", "sha256:" + hashlib.sha256(b"secret").hexdigest()), + public_paths=["/health"], + ) + + @app.get("/health") + async def health(): + return {"status": "ok"} + + @app.get("/protected") + async def protected(): + return {"status": "protected"} + + client = TestClient(app) + assert client.get("/health").status_code == 200 + assert client.get("/protected").status_code == 401 + assert client.get("/protected", headers={"Authorization": _basic("tia:wrong")}).status_code == 401 + assert client.get("/protected", headers={"Authorization": _basic("tia:secret")}).status_code == 200 + + +def test_api_key_authentication(): + app = FastAPI() + app.add_middleware(AuthenticationMiddleware, provider=ApiKeyAuthenticationProvider("plain:key-123")) + + @app.get("/protected") + async def protected(): + return {"status": "ok"} + + client = TestClient(app) + assert client.get("/protected").status_code == 401 + assert client.get("/protected", headers={"x-api-key": "key-123"}).status_code == 200 diff --git a/tests/unit/test_authentication_policies.py b/tests/unit/test_authentication_policies.py new file mode 100644 index 0000000..9b4fcf1 --- /dev/null +++ b/tests/unit/test_authentication_policies.py @@ -0,0 +1,69 @@ +from __future__ import annotations + +import base64 + +from fastapi import FastAPI, Request +from fastapi.testclient import TestClient + +from agent_framework.security import ( + AuthenticationPolicy, + BasicAuthenticationProvider, + NoAuthenticationProvider, + PolicyAuthenticationMiddleware, +) + + +def _basic(client_id: str, secret: str) -> str: + value = base64.b64encode(f"{client_id}:{secret}".encode()).decode() + return f"Basic {value}" + + +def test_policy_middleware_public_protected_and_default_deny(): + app = FastAPI() + policies = [ + AuthenticationPolicy("public", NoAuthenticationProvider(), paths=("/health",)), + AuthenticationPolicy( + "messages", + BasicAuthenticationProvider("tia", "plain:secret"), + paths=("/gateway/*",), + ), + ] + app.add_middleware(PolicyAuthenticationMiddleware, policies=policies) + + @app.get("/health") + async def health(): + return {"ok": True} + + @app.get("/gateway/message") + async def message(request: Request): + return {"subject": request.state.auth_principal.subject} + + @app.get("/unknown") + async def unknown(): + return {"unexpected": True} + + client = TestClient(app) + assert client.get("/health").status_code == 200 + assert client.get("/gateway/message").status_code == 401 + authenticated = client.get("/gateway/message", headers={"Authorization": _basic("tia", "secret")}) + assert authenticated.status_code == 200 + assert authenticated.json()["subject"] == "tia" + assert client.get("/unknown").status_code == 401 + + +def test_policy_method_filter(): + app = FastAPI() + policies = [AuthenticationPolicy("post-only", NoAuthenticationProvider(), paths=("/resource",), methods=frozenset({"POST"}))] + app.add_middleware(PolicyAuthenticationMiddleware, policies=policies) + + @app.get("/resource") + async def get_resource(): + return {"method": "GET"} + + @app.post("/resource") + async def post_resource(): + return {"method": "POST"} + + client = TestClient(app) + assert client.post("/resource").status_code == 200 + assert client.get("/resource").status_code == 401