mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-11-06 11:24:13 +00:00
Compare commits
4 Commits
main
...
762f6fb024
| Author | SHA1 | Date | |
|---|---|---|---|
| 762f6fb024 | |||
| f9c66b4792 | |||
| 560e79d21b | |||
| 23d32bbcfc |
209
.env
209
.env
@@ -1,209 +0,0 @@
|
|||||||
###############################################################################
|
|
||||||
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
|
||||||
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
APP_NAME=ai-agent-template
|
|
||||||
APP_ENV=local
|
|
||||||
LOG_LEVEL=INFO
|
|
||||||
API_HOST=0.0.0.0
|
|
||||||
API_PORT=8000
|
|
||||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# LLM - OCI Generative AI como provider principal
|
|
||||||
###############################################################################
|
|
||||||
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
|
||||||
LLM_PROVIDER=oci_openai
|
|
||||||
LLM_TEMPERATURE=0.2
|
|
||||||
LLM_MAX_TOKENS=2048
|
|
||||||
LLM_TIMEOUT_SECONDS=120
|
|
||||||
|
|
||||||
# OCI OpenAI-compatible endpoint
|
|
||||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
|
||||||
OCI_GENAI_MODEL=openai.gpt-4.1
|
|
||||||
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
|
||||||
OCI_GENAI_PROJECT_OCID=
|
|
||||||
|
|
||||||
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
|
||||||
OCI_AUTH_MODE=config_file
|
|
||||||
# OCI SDK / signer / profiles
|
|
||||||
OCI_CONFIG_FILE=~/.oci/config
|
|
||||||
OCI_PROFILE=DEFAULT
|
|
||||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
OCI_REGION=us-chicago-1
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Persistência
|
|
||||||
###############################################################################
|
|
||||||
# Opções: memory, autonomous, mongodb
|
|
||||||
SESSION_REPOSITORY_PROVIDER=autonomous
|
|
||||||
MEMORY_REPOSITORY_PROVIDER=autonomous
|
|
||||||
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
|
||||||
|
|
||||||
# Autonomous Database
|
|
||||||
ADB_USER=admin
|
|
||||||
ADB_PASSWORD=fjhsdf04954hf
|
|
||||||
ADB_DSN=oradb23aidev_high
|
|
||||||
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
|
||||||
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
|
||||||
ADB_TABLE_PREFIX=AGENTFW
|
|
||||||
|
|
||||||
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
|
||||||
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
|
||||||
MONGODB_DATABASE=agent_platform
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
REDIS_URL=redis://localhost:6379/0
|
|
||||||
ENABLE_REDIS_CACHE=false
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# RAG / Vector / Graph
|
|
||||||
###############################################################################
|
|
||||||
VECTOR_STORE_PROVIDER=memory
|
|
||||||
GRAPH_STORE_PROVIDER=memory
|
|
||||||
RAG_TOP_K=5
|
|
||||||
EMBEDDING_PROVIDER=mock
|
|
||||||
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
|
||||||
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Observabilidade
|
|
||||||
###############################################################################
|
|
||||||
ENABLE_LANGFUSE=true
|
|
||||||
LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
|
|
||||||
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
|
|
||||||
LANGFUSE_LEGACY_IO_FALLBACK=true
|
|
||||||
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
|
|
||||||
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
|
|
||||||
LANGFUSE_HOST=http://localhost:3005
|
|
||||||
ENABLE_OTEL=false
|
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
|
||||||
OTEL_SERVICE_NAME=ai-agent-template
|
|
||||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Analytics / Observer corporativo
|
|
||||||
###############################################################################
|
|
||||||
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
|
||||||
ENABLE_ANALYTICS=false
|
|
||||||
# Providers aceitos: oci_streaming,pubsub,noop
|
|
||||||
ANALYTICS_PROVIDERS=pubsub
|
|
||||||
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
|
||||||
AGENT_PUBSUB_TOPIC=
|
|
||||||
GCP_PUBSUB_TOPIC_PATH=
|
|
||||||
GCP_PROJECT_ID=
|
|
||||||
GCP_PUBSUB_TOPIC=
|
|
||||||
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
|
||||||
# Credencial GCP segue padrão Google:
|
|
||||||
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# OCI Streaming
|
|
||||||
###############################################################################
|
|
||||||
ENABLE_OCI_STREAMING=false
|
|
||||||
OCI_STREAM_ENDPOINT=
|
|
||||||
OCI_STREAM_OCID=
|
|
||||||
OCI_STREAM_PARTITION_KEY=agent-events
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Guardrails, Judges, Supervisor
|
|
||||||
###############################################################################
|
|
||||||
ENABLE_INPUT_GUARDRAILS=true
|
|
||||||
ENABLE_OUTPUT_GUARDRAILS=true
|
|
||||||
ENABLE_JUDGES=true
|
|
||||||
ENABLE_SUPERVISOR=true
|
|
||||||
ENABLE_OUTPUT_SUPERVISOR=true
|
|
||||||
ENABLE_PARALLEL_GUARDRAILS=true
|
|
||||||
GUARDRAILS_FAIL_FAST=true
|
|
||||||
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
|
||||||
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
|
||||||
JUDGES_CONFIG_PATH=./config/judges.yaml
|
|
||||||
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# Gateway de canais
|
|
||||||
###############################################################################
|
|
||||||
DEFAULT_CHANNEL=web
|
|
||||||
# embedded = backend may parse simple/native channel payloads.
|
|
||||||
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
|
||||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
|
||||||
ENABLE_VOICE_ADAPTER=true
|
|
||||||
ENABLE_WHATSAPP_ADAPTER=true
|
|
||||||
ENABLE_TEXT_ADAPTER=true
|
|
||||||
|
|
||||||
#################################################
|
|
||||||
# ENTERPRISE ROUTING
|
|
||||||
#################################################
|
|
||||||
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
|
||||||
ROUTING_CONFIG_PATH=./config/routing.yaml
|
|
||||||
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
|
||||||
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
|
||||||
ENABLE_LLM_ROUTER=true
|
|
||||||
|
|
||||||
# Semantic route stickiness (optional).
|
|
||||||
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
|
||||||
# There are no regexes or deterministic language rules.
|
|
||||||
ENABLE_ROUTE_STICKINESS=false
|
|
||||||
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
|
||||||
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
|
||||||
ROUTE_STICKINESS_HISTORY_TURNS=2
|
|
||||||
ROUTE_STICKINESS_MAX_TOKENS=80
|
|
||||||
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
|
||||||
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# MCP / Tools
|
|
||||||
###############################################################################
|
|
||||||
ENABLE_MCP_TOOLS=true
|
|
||||||
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
|
||||||
TOOLS_CONFIG_PATH=./config/tools.yaml
|
|
||||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
|
||||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
|
||||||
|
|
||||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
|
||||||
ROUTING_MODE=router
|
|
||||||
|
|
||||||
# Usage/cost accounting
|
|
||||||
USAGE_REPOSITORY_PROVIDER=autonomous
|
|
||||||
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
|
||||||
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
# ConversationSummaryMemory / compressão de contexto conversacional
|
|
||||||
# -----------------------------------------------------------------------------
|
|
||||||
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
|
||||||
MEMORY_CONTEXT_STRATEGY=summary
|
|
||||||
MEMORY_HISTORY_LIMIT=80
|
|
||||||
MEMORY_RECENT_MESSAGES_LIMIT=8
|
|
||||||
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
|
||||||
MEMORY_MAX_SUMMARY_CHARS=6000
|
|
||||||
MEMORY_SUMMARY_USE_LLM=true
|
|
||||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
|
||||||
MEMORY_INJECT_SUMMARY=true
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# MCP Gateway
|
|
||||||
###############################################################################
|
|
||||||
# true = framework routes tool calls to the dedicated MCP Gateway.
|
|
||||||
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
|
||||||
MCP_GATEWAY_ENABLED=true
|
|
||||||
MCP_GATEWAY_URL=http://localhost:8300
|
|
||||||
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
|
||||||
# MCP_GATEWAY_TOKEN=
|
|
||||||
MCP_GATEWAY_AGENT_ID=telecom_contas
|
|
||||||
MCP_GATEWAY_TENANT_ID=default
|
|
||||||
|
|
||||||
###############################################################################
|
|
||||||
# LONG-TERM MEMORY
|
|
||||||
###############################################################################
|
|
||||||
ENABLE_LONG_TERM_MEMORY=true
|
|
||||||
LONG_TERM_MEMORY_PROVIDER=sqlite
|
|
||||||
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
|
||||||
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
|
||||||
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
|
||||||
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
|
||||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
|
||||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
|
||||||
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
|
||||||
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
|
||||||
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Implementação — workflows transacionais determinísticos
|
||||||
|
|
||||||
|
## Entrega
|
||||||
|
|
||||||
|
Foi adicionada ao `agent_framework_oci` uma capacidade opcional para executar transações multi-etapas como workflows determinísticos compilados em LangGraph.
|
||||||
|
|
||||||
|
### Módulo novo
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/workflows/`
|
||||||
|
|
||||||
|
- `models.py`: contratos Pydantic e validação estrutural;
|
||||||
|
- `repository.py`: resolução de versão ativa e leitura de YAML imutável;
|
||||||
|
- `registry.py`: registro desacoplado de actions sync/async;
|
||||||
|
- `runtime.py`: compilação, cache e execução do StateGraph;
|
||||||
|
- `tool_executor.py`: integração com a política da tool;
|
||||||
|
- `__init__.py`: API pública.
|
||||||
|
|
||||||
|
### Política expandida
|
||||||
|
|
||||||
|
`ToolPolicy` agora aceita:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
execution:
|
||||||
|
mode: direct_tool | workflow | agent
|
||||||
|
workflow: nome_do_workflow
|
||||||
|
version: active | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
O default permanece `direct_tool`, preservando compatibilidade.
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Foram adicionados:
|
||||||
|
|
||||||
|
- `ENABLE_TRANSACTIONAL_WORKFLOWS=false`;
|
||||||
|
- `WORKFLOWS_PATH=./workflows`.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
Inclui um exemplo completo de devolução de pedido com:
|
||||||
|
|
||||||
|
- confirmação e campos obrigatórios pela política;
|
||||||
|
- workflow YAML versionado;
|
||||||
|
- actions de domínio no backend;
|
||||||
|
- bifurcação determinística baseada no resultado da validação.
|
||||||
|
|
||||||
|
## Validação realizada
|
||||||
|
|
||||||
|
- `tests/unit/test_tool_policies.py`: 4 testes aprovados;
|
||||||
|
- compilação Python de framework, template e novos testes: aprovada;
|
||||||
|
- o teste funcional novo do LangGraph foi criado, mas não pôde ser executado neste container porque `langgraph` não está instalado no ambiente. A dependência já está declarada no `pyproject.toml` do framework.
|
||||||
|
|
||||||
|
## Escopo e segurança
|
||||||
|
|
||||||
|
Esta entrega cria o motor e a integração de política. Para operações críticas em produção ainda é necessário conectar:
|
||||||
|
|
||||||
|
- execution store persistente;
|
||||||
|
- idempotência de negócio nas actions/APIs;
|
||||||
|
- autorização por escopo;
|
||||||
|
- telemetria IC/NOC específica de workflow;
|
||||||
|
- compensação/Saga quando aplicável;
|
||||||
|
- estratégia corporativa de timeout e retry.
|
||||||
|
|
||||||
|
Esses itens foram explicitamente documentados para evitar a falsa impressão de que retry por si só garante segurança transacional.
|
||||||
987
Documentacao/Implementando_Basic_Auth.md
Normal file
987
Documentacao/Implementando_Basic_Auth.md
Normal file
@@ -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:<salt>:<digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
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`.
|
||||||
@@ -3,5 +3,8 @@ from __future__ import annotations
|
|||||||
# Compatibilidade local do template/backend.
|
# Compatibilidade local do template/backend.
|
||||||
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
from app.presentation import register_tool_renderers
|
||||||
|
|
||||||
|
register_tool_renderers()
|
||||||
|
|
||||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -10,6 +10,9 @@ tools:
|
|||||||
- fatura
|
- fatura
|
||||||
- conta
|
- conta
|
||||||
- boleto
|
- boleto
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
consultar_pagamentos:
|
consultar_pagamentos:
|
||||||
description: Consulta histórico de pagamentos do cliente.
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -28,6 +31,9 @@ tools:
|
|||||||
asset_id: string
|
asset_id: string
|
||||||
selection_keywords:
|
selection_keywords:
|
||||||
- plano
|
- plano
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
listar_servicos:
|
listar_servicos:
|
||||||
description: Lista serviços ativos e adicionais VAS.
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -49,6 +55,9 @@ tools:
|
|||||||
- consultar pedido
|
- consultar pedido
|
||||||
- status do pedido
|
- status do pedido
|
||||||
- pedido
|
- pedido
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
consultar_entrega:
|
consultar_entrega:
|
||||||
description: Consulta entrega e rastreamento do pedido.
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
@@ -61,6 +70,9 @@ tools:
|
|||||||
- rastreamento
|
- rastreamento
|
||||||
- transportadora
|
- transportadora
|
||||||
- previsão
|
- previsão
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
solicitar_troca:
|
solicitar_troca:
|
||||||
description: Simula abertura de solicitação de troca.
|
description: Simula abertura de solicitação de troca.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -10,6 +10,9 @@ tools:
|
|||||||
- fatura
|
- fatura
|
||||||
- conta
|
- conta
|
||||||
- boleto
|
- boleto
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
consultar_pagamentos:
|
consultar_pagamentos:
|
||||||
description: Consulta histórico de pagamentos do cliente.
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -28,6 +31,9 @@ tools:
|
|||||||
asset_id: string
|
asset_id: string
|
||||||
selection_keywords:
|
selection_keywords:
|
||||||
- plano
|
- plano
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
listar_servicos:
|
listar_servicos:
|
||||||
description: Lista serviços ativos e adicionais VAS.
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -49,6 +55,9 @@ tools:
|
|||||||
- consultar pedido
|
- consultar pedido
|
||||||
- status do pedido
|
- status do pedido
|
||||||
- pedido
|
- pedido
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
consultar_entrega:
|
consultar_entrega:
|
||||||
description: Consulta entrega e rastreamento do pedido.
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
@@ -61,6 +70,9 @@ tools:
|
|||||||
- rastreamento
|
- rastreamento
|
||||||
- transportadora
|
- transportadora
|
||||||
- previsão
|
- previsão
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
solicitar_troca:
|
solicitar_troca:
|
||||||
description: Simula abertura de solicitação de troca.
|
description: Simula abertura de solicitação de troca.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Domain Requested LLM Composition
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Permitir que uma tool/workflow de domínio informe que o resultado operacional não deve ser devolvido diretamente ao usuário e precisa ser redigido pelo LLM oficial do agente, sem criar um gateway LLM dentro do domínio.
|
||||||
|
|
||||||
|
## Contrato
|
||||||
|
|
||||||
|
A tool pode devolver, em qualquer nível do resultado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_llm_composition": true,
|
||||||
|
"response_instruction": "Explique ao cliente a forma de devolução usando apenas os dados do workflow."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Também é aceito `response_instructions` como lista.
|
||||||
|
|
||||||
|
O `AgentRuntimeMixin` percorre recursivamente o resultado MCP. Quando a flag está ativa, `build_direct_mcp_answer()` retorna `None`; a resposta segue pelo LLM configurado no framework e recebe as evidências MCP no contexto normal do agente.
|
||||||
|
|
||||||
|
## Por que isso existe
|
||||||
|
|
||||||
|
Algumas operações, como pró-rata, possuem resultado determinístico e efeitos já concluídos, mas precisam de linguagem natural adequada ao canal. Antes, o domínio Contas possuía um gateway LLM próprio. Agora o domínio só declara a necessidade e a instrução; execução, credenciais, profiles, tracing e custos continuam no `agent_framework_oci`.
|
||||||
|
|
||||||
|
## Regras
|
||||||
|
|
||||||
|
- Não usar para decidir se uma transação deve ocorrer.
|
||||||
|
- Não usar para inventar valores ou protocolos.
|
||||||
|
- A instrução deve exigir que o LLM use somente as evidências retornadas pela tool/workflow.
|
||||||
|
- Pode coexistir com `requires_rag`; nesse caso o framework também executa RAG antes da composição.
|
||||||
51
Tuning-Performance/Domain_Requested_RAG/README.md
Normal file
51
Tuning-Performance/Domain_Requested_RAG/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Domain-Requested RAG
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Permitir que uma tool/workflow de domínio declare que o resultado MCP **não é suficiente** para produzir a resposta final e que o agente deve recuperar conhecimento usando o `RagService` oficial do `agent_framework_oci`.
|
||||||
|
|
||||||
|
O domínio não instancia banco vetorial, embeddings, LLM ou cliente RAG. Ele apenas devolve no resultado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_rag": true,
|
||||||
|
"rag_queries": [
|
||||||
|
"Como cancelar o serviço Paramount+ no parceiro? Procedimento oficial de cancelamento."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fluxo
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow/tool de domínio
|
||||||
|
|
|
||||||
|
| requires_rag + rag_queries
|
||||||
|
v
|
||||||
|
AgentRuntimeMixin
|
||||||
|
|
|
||||||
|
+-- impede direct MCP answer
|
||||||
|
+-- ignora SKIP_RAG_WHEN_MCP_SUFFICIENT para este resultado
|
||||||
|
+-- usa rag_query/rag_queries como query override
|
||||||
|
v
|
||||||
|
RagService
|
||||||
|
v
|
||||||
|
Vector/Graph store configurado no framework
|
||||||
|
v
|
||||||
|
LLM do agente com MCP evidence + RAG evidence
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regras
|
||||||
|
|
||||||
|
1. `requires_rag=false` ou ausente mantém o comportamento padrão.
|
||||||
|
2. `SKIP_RAG_WHEN_MCP_SUFFICIENT=true` continua válido para tools normais.
|
||||||
|
3. Quando `requires_rag=true`, o framework não usa `build_direct_mcp_answer()`.
|
||||||
|
4. `rag_query` aceita uma query; `rag_queries` aceita várias queries, preservadas e deduplicadas em ordem.
|
||||||
|
5. O domínio nunca executa `RagService` diretamente.
|
||||||
|
6. Guardrails de retrieval (`RAGSEC`, `RET_REL` etc.) continuam executando normalmente sobre o contexto recuperado.
|
||||||
|
|
||||||
|
## Exemplo Contas
|
||||||
|
|
||||||
|
No fluxo VAS Estratégico, após o cliente rejeitar a explicação e pedir cancelamento, a action devolve queries específicas por parceiro. O `VasAgent` usa o `RagService` do framework para recuperar o procedimento oficial e compor a resposta.
|
||||||
|
|
||||||
|
Isso substitui o comportamento legado em que o backend Contas possuía uma busca RAG própria dentro de `vas_strategic()`.
|
||||||
@@ -3,5 +3,8 @@ from __future__ import annotations
|
|||||||
# Compatibilidade local do template/backend.
|
# Compatibilidade local do template/backend.
|
||||||
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
from app.presentation import register_tool_renderers
|
||||||
|
|
||||||
|
register_tool_renderers()
|
||||||
|
|
||||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -10,6 +10,9 @@ tools:
|
|||||||
- fatura
|
- fatura
|
||||||
- conta
|
- conta
|
||||||
- boleto
|
- boleto
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
consultar_pagamentos:
|
consultar_pagamentos:
|
||||||
description: Consulta histórico de pagamentos do cliente.
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -28,6 +31,9 @@ tools:
|
|||||||
asset_id: string
|
asset_id: string
|
||||||
selection_keywords:
|
selection_keywords:
|
||||||
- plano
|
- plano
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
listar_servicos:
|
listar_servicos:
|
||||||
description: Lista serviços ativos e adicionais VAS.
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -49,6 +55,9 @@ tools:
|
|||||||
- consultar pedido
|
- consultar pedido
|
||||||
- status do pedido
|
- status do pedido
|
||||||
- pedido
|
- pedido
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
consultar_entrega:
|
consultar_entrega:
|
||||||
description: Consulta entrega e rastreamento do pedido.
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
@@ -61,6 +70,9 @@ tools:
|
|||||||
- rastreamento
|
- rastreamento
|
||||||
- transportadora
|
- transportadora
|
||||||
- previsão
|
- previsão
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
solicitar_troca:
|
solicitar_troca:
|
||||||
description: Simula abertura de solicitação de troca.
|
description: Simula abertura de solicitação de troca.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
|
|||||||
@@ -3,5 +3,8 @@ from __future__ import annotations
|
|||||||
# Compatibilidade local do template/backend.
|
# Compatibilidade local do template/backend.
|
||||||
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
from app.presentation import register_tool_renderers
|
||||||
|
|
||||||
|
register_tool_renderers()
|
||||||
|
|
||||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -10,6 +10,9 @@ tools:
|
|||||||
- fatura
|
- fatura
|
||||||
- conta
|
- conta
|
||||||
- boleto
|
- boleto
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
consultar_pagamentos:
|
consultar_pagamentos:
|
||||||
description: Consulta histórico de pagamentos do cliente.
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -28,6 +31,9 @@ tools:
|
|||||||
asset_id: string
|
asset_id: string
|
||||||
selection_keywords:
|
selection_keywords:
|
||||||
- plano
|
- plano
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
listar_servicos:
|
listar_servicos:
|
||||||
description: Lista serviços ativos e adicionais VAS.
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -49,6 +55,9 @@ tools:
|
|||||||
- consultar pedido
|
- consultar pedido
|
||||||
- status do pedido
|
- status do pedido
|
||||||
- pedido
|
- pedido
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
consultar_entrega:
|
consultar_entrega:
|
||||||
description: Consulta entrega e rastreamento do pedido.
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
@@ -61,6 +70,9 @@ tools:
|
|||||||
- rastreamento
|
- rastreamento
|
||||||
- transportadora
|
- transportadora
|
||||||
- previsão
|
- previsão
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
solicitar_troca:
|
solicitar_troca:
|
||||||
description: Simula abertura de solicitação de troca.
|
description: Simula abertura de solicitação de troca.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
|
|||||||
21
Tuning-Performance/Offline_Workflow_Regression/README.md
Normal file
21
Tuning-Performance/Offline_Workflow_Regression/README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Offline Workflow Regression
|
||||||
|
|
||||||
|
O backend de produção de `WorkflowRuntime` continua sendo **LangGraph**. A ausência do pacote `langgraph` em produção é erro de configuração.
|
||||||
|
|
||||||
|
Para builders restritos/offline, o runtime aceita `allow_deterministic_fallback=True`. Esse modo é deliberadamente opt-in e existe somente para exercitar a DSL do framework (actions, edges, condições, pause/resume e trace) quando a dependência externa não pode ser instalada. Ele não é selecionado automaticamente.
|
||||||
|
|
||||||
|
Exemplo de teste:
|
||||||
|
|
||||||
|
```python
|
||||||
|
runtime = WorkflowRuntime(
|
||||||
|
repository,
|
||||||
|
actions=registry,
|
||||||
|
allow_deterministic_fallback=True,
|
||||||
|
)
|
||||||
|
first = await runtime.arun("workflow", payload)
|
||||||
|
assert first.status == "PAUSED"
|
||||||
|
final = await runtime.aresume("workflow", first.execution_id, {"resposta": "SIM"})
|
||||||
|
assert final.status == "COMPLETED"
|
||||||
|
```
|
||||||
|
|
||||||
|
O objetivo é não transformar indisponibilidade de rede/PyPI em `pytest.skip`, sem mascarar o requisito de LangGraph do runtime de produção.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
# Implementação no framework
|
||||||
|
|
||||||
|
A evolução adiciona `WorkflowPause` e `WorkflowExpectedInput` ao modelo de workflow e mantém o LangGraph como detalhe interno de `WorkflowRuntime`.
|
||||||
|
|
||||||
|
O runtime aceita condições declarativas `all`, `any`, `not`, `eq`, `neq`, `exists`, `path/equals`, `path/not_equals` e `path/in`. Em um nó com `pause`, a action é executada primeiro e a interrupção ocorre em um nó técnico separado. Na retomada, `langgraph.types.Command(resume=...)` injeta o valor esperado e segue para `resume_from` sem repetir a action que precedeu o pause.
|
||||||
|
|
||||||
|
`FrameworkStateGraph` é a facade para aplicações que ainda precisam compor um grafo de agentes; templates oficiais devem usar a facade e não importar LangGraph diretamente.
|
||||||
|
|
||||||
|
## Preservação de estado em falhas posteriores
|
||||||
|
|
||||||
|
O `WorkflowRuntime` também preserva o último snapshot persistido do LangGraph quando uma action posterior falha. O resultado `FAILED` contém `output`, `state` e `trace` dos nodes que já terminaram com sucesso.
|
||||||
|
|
||||||
|
Isso é necessário para workflows transacionais: por exemplo, se um protocolo foi criado e uma chamada posterior falha, o chamador ainda recebe o `protocol_number` persistido e pode executar recuperação/idempotência sem repetir o primeiro side effect.
|
||||||
|
|
||||||
|
O runtime não transforma falha em sucesso e não reexecuta automaticamente a action; ele apenas preserva a evidência durável já existente no checkpointer.
|
||||||
34
Tuning-Performance/Pause_Resume_Workflow/README.md
Normal file
34
Tuning-Performance/Pause_Resume_Workflow/README.md
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
# Pause/Resume Workflow — LangGraph encapsulado pelo Agent Framework OCI
|
||||||
|
|
||||||
|
Este exemplo demonstra uma capability genérica do `agent_framework_oci`: workflows determinísticos podem interromper a execução para obter uma resposta do cliente e retomar posteriormente pelo mesmo `execution_id`, sem que o domínio importe ou monte um `StateGraph`.
|
||||||
|
|
||||||
|
## Conceito
|
||||||
|
|
||||||
|
A aplicação declara o workflow em YAML. `WorkflowRuntime` transforma a definição em LangGraph internamente, utiliza o checkpointer configurado pelo framework e expõe somente:
|
||||||
|
|
||||||
|
- `arun(name, payload)` — inicia ou executa o workflow;
|
||||||
|
- `aresume(name, execution_id, value)` — retoma o workflow pausado;
|
||||||
|
- `WorkflowRunResult.status` — `PAUSED`, `COMPLETED` ou `FAILED`.
|
||||||
|
|
||||||
|
O `pause` é compilado em um nó separado da action anterior. Isto impede que uma action com efeito colateral seja executada novamente quando o cliente responde.
|
||||||
|
|
||||||
|
## Executar
|
||||||
|
|
||||||
|
A partir da raiz do exemplo, com as dependências do framework instaladas:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m app.demo
|
||||||
|
pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## YAML
|
||||||
|
|
||||||
|
`workflows/confirmacao.v1.yaml` mostra `expected_input`, normalização, valores permitidos e `resume_from`.
|
||||||
|
|
||||||
|
## Persistência
|
||||||
|
|
||||||
|
O exemplo usa `MemorySaver` apenas para ser autocontido. Em aplicações reais use `create_langgraph_checkpointer(settings)`. Assim `execution_id` é o `thread_id` do LangGraph e a retomada sobrevive a processos/replicas conforme o provider configurado (por exemplo Autonomous Database).
|
||||||
|
|
||||||
|
## Regra arquitetural
|
||||||
|
|
||||||
|
Código de domínio não deve importar `langgraph.graph.StateGraph`. Para grafos de agentes use `FrameworkStateGraph`; para workflows determinísticos de negócio use `WorkflowRuntime`.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from agent_framework.workflows import FileWorkflowRepository, WorkflowActionRegistry, WorkflowRuntime
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
|
||||||
|
def build_runtime(*, offline_test_fallback: bool = False) -> WorkflowRuntime:
|
||||||
|
actions = WorkflowActionRegistry()
|
||||||
|
|
||||||
|
async def preparar(params, state):
|
||||||
|
return {"assunto": params.get("assunto") or "operação"}
|
||||||
|
|
||||||
|
async def perguntar(params, state):
|
||||||
|
return {"mensagem": f"Deseja confirmar {params['assunto']}?"}
|
||||||
|
|
||||||
|
async def decidir(params, state):
|
||||||
|
return {
|
||||||
|
"mensagem": "Operação confirmada." if params["resposta"] == "SIM" else "Operação cancelada.",
|
||||||
|
"confirmado": params["resposta"] == "SIM",
|
||||||
|
}
|
||||||
|
|
||||||
|
actions.register("preparar_operacao", preparar)
|
||||||
|
actions.register("montar_pergunta", perguntar)
|
||||||
|
actions.register("registrar_decisao", decidir)
|
||||||
|
|
||||||
|
checkpointer = None
|
||||||
|
if not offline_test_fallback:
|
||||||
|
# Produção/exemplo real continua usando LangGraph + checkpointer. O import
|
||||||
|
# fica aqui para que a regressão offline do repositório não dependa de rede.
|
||||||
|
from langgraph.checkpoint.memory import MemorySaver
|
||||||
|
checkpointer = MemorySaver()
|
||||||
|
|
||||||
|
return WorkflowRuntime(
|
||||||
|
FileWorkflowRepository(ROOT / "workflows"),
|
||||||
|
actions=actions,
|
||||||
|
checkpointer=checkpointer,
|
||||||
|
allow_deterministic_fallback=offline_test_fallback,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
runtime = build_runtime()
|
||||||
|
first = await runtime.arun("confirmacao", {"assunto": "a alteração do plano"})
|
||||||
|
print(first.model_dump(mode="json"))
|
||||||
|
assert first.status == "PAUSED"
|
||||||
|
resumed = await runtime.aresume("confirmacao", first.execution_id, {"resposta_usuario": "sim"})
|
||||||
|
print(resumed.model_dump(mode="json"))
|
||||||
|
assert resumed.status == "COMPLETED"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.demo import build_runtime
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_pause_resume_does_not_repeat_previous_action():
|
||||||
|
# Regressão offline: exercita a mesma DSL/WorkflowRuntime sem exigir download
|
||||||
|
# de LangGraph no builder. Produção continua usando build_runtime() default.
|
||||||
|
runtime = build_runtime(offline_test_fallback=True)
|
||||||
|
first = await runtime.arun("confirmacao", {"assunto": "o cancelamento"})
|
||||||
|
assert first.status == "PAUSED"
|
||||||
|
assert first.pause["expected_input"]["key"] == "resposta_usuario"
|
||||||
|
before = [item for item in first.trace if item.get("action") == "preparar_operacao"]
|
||||||
|
assert len(before) == 1
|
||||||
|
resumed = await runtime.aresume("confirmacao", first.execution_id, {"resposta_usuario": "SIM"})
|
||||||
|
assert resumed.status == "COMPLETED"
|
||||||
|
after = [item for item in resumed.trace if item.get("action") == "preparar_operacao"]
|
||||||
|
assert len(after) == 1
|
||||||
|
assert resumed.state["vars"]["decidir"]["confirmado"] is True
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
version: 1
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
name: confirmacao
|
||||||
|
version: 1
|
||||||
|
start: preparar
|
||||||
|
nodes:
|
||||||
|
- id: preparar
|
||||||
|
action: preparar_operacao
|
||||||
|
input:
|
||||||
|
assunto: $.input.assunto
|
||||||
|
- id: perguntar
|
||||||
|
action: montar_pergunta
|
||||||
|
input:
|
||||||
|
assunto: $.vars.preparar.assunto
|
||||||
|
pause:
|
||||||
|
enabled: true
|
||||||
|
return_from: $.output.mensagem
|
||||||
|
expected_input:
|
||||||
|
key: resposta_usuario
|
||||||
|
allowed_values: [SIM, NAO]
|
||||||
|
normalize: upper_strip
|
||||||
|
resume_from: decidir
|
||||||
|
- id: decidir
|
||||||
|
action: registrar_decisao
|
||||||
|
input:
|
||||||
|
resposta: $.input.resposta_usuario
|
||||||
|
assunto: $.vars.preparar.assunto
|
||||||
|
edges:
|
||||||
|
- from: preparar
|
||||||
|
to: perguntar
|
||||||
|
- from: perguntar
|
||||||
|
to: END
|
||||||
|
- from: decidir
|
||||||
|
to: END
|
||||||
@@ -3,5 +3,8 @@ from __future__ import annotations
|
|||||||
# Compatibilidade local do template/backend.
|
# Compatibilidade local do template/backend.
|
||||||
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
from app.presentation import register_tool_renderers
|
||||||
|
|
||||||
|
register_tool_renderers()
|
||||||
|
|
||||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -10,6 +10,9 @@ tools:
|
|||||||
- fatura
|
- fatura
|
||||||
- conta
|
- conta
|
||||||
- boleto
|
- boleto
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
consultar_pagamentos:
|
consultar_pagamentos:
|
||||||
description: Consulta histórico de pagamentos do cliente.
|
description: Consulta histórico de pagamentos do cliente.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -28,6 +31,9 @@ tools:
|
|||||||
asset_id: string
|
asset_id: string
|
||||||
selection_keywords:
|
selection_keywords:
|
||||||
- plano
|
- plano
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
listar_servicos:
|
listar_servicos:
|
||||||
description: Lista serviços ativos e adicionais VAS.
|
description: Lista serviços ativos e adicionais VAS.
|
||||||
mcp_server: telecom
|
mcp_server: telecom
|
||||||
@@ -49,6 +55,9 @@ tools:
|
|||||||
- consultar pedido
|
- consultar pedido
|
||||||
- status do pedido
|
- status do pedido
|
||||||
- pedido
|
- pedido
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
consultar_entrega:
|
consultar_entrega:
|
||||||
description: Consulta entrega e rastreamento do pedido.
|
description: Consulta entrega e rastreamento do pedido.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
@@ -61,6 +70,9 @@ tools:
|
|||||||
- rastreamento
|
- rastreamento
|
||||||
- transportadora
|
- transportadora
|
||||||
- previsão
|
- previsão
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
solicitar_troca:
|
solicitar_troca:
|
||||||
description: Simula abertura de solicitação de troca.
|
description: Simula abertura de solicitação de troca.
|
||||||
mcp_server: retail
|
mcp_server: retail
|
||||||
|
|||||||
47
Tuning-Performance/Voice_Interruption_Replay/README.md
Normal file
47
Tuning-Performance/Voice_Interruption_Replay/README.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# Voice Interruption / Replay — framework-native
|
||||||
|
|
||||||
|
Esta melhoria move para `agent_framework.channels.interruption` comportamentos que antes costumavam ser implementados dentro de agentes de voz específicos.
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Evitar que `idle_nudge`, barge-in e fala residual pós-finalização reabram desnecessariamente o LangGraph principal, executem tools novamente ou confundam uma resposta de continuidade com uma nova intenção.
|
||||||
|
|
||||||
|
## Ordem de decisão
|
||||||
|
|
||||||
|
1. **Sessão encerrada**: replay da última fala terminal (ou fallback), preservando `terminal_status`. Não chama LangGraph, tools ou guardrails.
|
||||||
|
2. **Idle nudge**: replay da última fala real do assistente. Não chama LangGraph, tools ou guardrails.
|
||||||
|
3. **Fala não interrompível**: replay literal.
|
||||||
|
4. **Fala interrompível com contexto anterior**: executa `processing_interruption_classifier` pelo `LLMProvider` do framework.
|
||||||
|
- `1`: reprocessa o complemento;
|
||||||
|
- `0`, erro ou resposta inválida: replay fail-safe.
|
||||||
|
5. **Sem fala anterior suficiente**: processa normalmente.
|
||||||
|
|
||||||
|
## Classificador
|
||||||
|
|
||||||
|
O classificador usa o profile `processing_interruption_classifier` e solicita resposta binária `1/0`. Ele não possui gateway LLM próprio e não depende do domínio do agente.
|
||||||
|
|
||||||
|
## Telemetria
|
||||||
|
|
||||||
|
O backend emite `channel.processing_interruption.classified` com `regenerate=true|false`. Replays retornam metadata:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"replay": true,
|
||||||
|
"replay_reason": "post_finalize|idle_nudge|non_interruptible_speech|classifier_result_0",
|
||||||
|
"framework_short_circuit": true,
|
||||||
|
"llm_called": false,
|
||||||
|
"tools_called": false,
|
||||||
|
"guardrails_called": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
No caso `classifier_result_0`, o LLM chamado é apenas o classificador leve; o LangGraph conversacional e o LLM do agente não são executados.
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
A funcionalidade foi aplicada em:
|
||||||
|
|
||||||
|
- `templates/agent_template_backend/app/main.py`
|
||||||
|
- `templates/agent_template_backend_day_zero/app/main.py`
|
||||||
|
|
||||||
|
Portanto novos agentes herdam o comportamento sem copiar código de domínio.
|
||||||
16
Tuning-Performance/Workflow_Error_Recovery/README.md
Normal file
16
Tuning-Performance/Workflow_Error_Recovery/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Workflow Error Recovery
|
||||||
|
|
||||||
|
O `WorkflowRuntime` preserva o último snapshot válido do LangGraph e, nesta versão, também expõe `error_details` estruturado quando a exceção externa oferece campos como `status_code`, `body` e `attempts`.
|
||||||
|
|
||||||
|
Isso permite que o domínio diferencie erro técnico de erro de negócio sem acoplar o framework ao provider. O framework continua responsável por runtime/checkpoint/trace; o domínio interpreta apenas o contrato do seu provider.
|
||||||
|
|
||||||
|
Exemplo conceitual:
|
||||||
|
|
||||||
|
```python
|
||||||
|
result = await runtime.arun("workflow_transacional", payload)
|
||||||
|
if result.status == "FAILED":
|
||||||
|
print(result.output) # nodes concluídos antes da falha
|
||||||
|
print(result.trace) # trace parcial
|
||||||
|
print(result.error) # mensagem humana/técnica
|
||||||
|
print(result.error_details) # status/body/attempts se disponíveis
|
||||||
|
```
|
||||||
1
deploy/oke/load-test/.backend-image
Normal file
1
deploy/oke/load-test/.backend-image
Normal file
@@ -0,0 +1 @@
|
|||||||
|
gru.ocir.io/idi1o0a010nx/agent-framework-loadtest/agent-template-backend:loadtest
|
||||||
30
deploy/oke/load-test/.env.langfuse
Normal file
30
deploy/oke/load-test/.env.langfuse
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Generated by prepare_env.py. Do not commit.
|
||||||
|
CLICKHOUSE_PASSWORD=YRDQrFHp9i_g_H5orAc3-CJ19_hqBwJR
|
||||||
|
DATABASE_URL=postgresql://postgres:z8LsI4acQoIKZ_63Uqv1Lb0pLxiaMGWM@postgres.langfuse.svc.cluster.local:5432/postgres
|
||||||
|
DIRECT_URL=postgresql://postgres:z8LsI4acQoIKZ_63Uqv1Lb0pLxiaMGWM@postgres.langfuse.svc.cluster.local:5432/postgres
|
||||||
|
ENCRYPTION_KEY=1207a026e5cb136511f304e49eb6e487216b17828bfd90ab404fb4be4d442aef
|
||||||
|
LANGFUSE_CLICKHOUSE_PASSWORD=YRDQrFHp9i_g_H5orAc3-CJ19_hqBwJR
|
||||||
|
LANGFUSE_ENCRYPTION_KEY=1207a026e5cb136511f304e49eb6e487216b17828bfd90ab404fb4be4d442aef
|
||||||
|
LANGFUSE_INIT_ORG_ID=agent-framework-org
|
||||||
|
LANGFUSE_INIT_ORG_NAME=Agent Framework OCI
|
||||||
|
LANGFUSE_INIT_PROJECT_ID=agent-framework-loadtest
|
||||||
|
LANGFUSE_INIT_PROJECT_NAME=Agent Framework Load Test
|
||||||
|
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-2c82b991104f3f38f41878baecaaffd0
|
||||||
|
LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-defd21d2c6da3d4a9f301d993236958f9e56d94f9050a00f79067267b78a00a0
|
||||||
|
LANGFUSE_INIT_USER_EMAIL=admin@loadtest.local
|
||||||
|
LANGFUSE_INIT_USER_NAME=Load Test Admin
|
||||||
|
LANGFUSE_INIT_USER_PASSWORD=HQmgpnBW3zVcG-TJMzrRgwejEUaLD7nb
|
||||||
|
LANGFUSE_MINIO_PASSWORD=Idj1YOugvY8vYhT08O60ShpLFWOVRAfy
|
||||||
|
LANGFUSE_NEXTAUTH_SECRET=54f5a8f462097696a512e81fa0141bb0739877648a63695eedd02b7475471d97
|
||||||
|
LANGFUSE_POSTGRES_PASSWORD=z8LsI4acQoIKZ_63Uqv1Lb0pLxiaMGWM
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-2c82b991104f3f38f41878baecaaffd0
|
||||||
|
LANGFUSE_REDIS_PASSWORD=gqpWg7Ylawd3Xd-Jf496htDZyeSNWLHJ
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY=Idj1YOugvY8vYhT08O60ShpLFWOVRAfy
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY=Idj1YOugvY8vYhT08O60ShpLFWOVRAfy
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY=Idj1YOugvY8vYhT08O60ShpLFWOVRAfy
|
||||||
|
LANGFUSE_SALT=8fdc487e68e7bff89f510e7cdac9c103
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-defd21d2c6da3d4a9f301d993236958f9e56d94f9050a00f79067267b78a00a0
|
||||||
|
NEXTAUTH_SECRET=54f5a8f462097696a512e81fa0141bb0739877648a63695eedd02b7475471d97
|
||||||
|
POSTGRES_PASSWORD=z8LsI4acQoIKZ_63Uqv1Lb0pLxiaMGWM
|
||||||
|
REDIS_AUTH=gqpWg7Ylawd3Xd-Jf496htDZyeSNWLHJ
|
||||||
|
SALT=8fdc487e68e7bff89f510e7cdac9c103
|
||||||
93
deploy/oke/load-test/.env.loadtest.example
Normal file
93
deploy/oke/load-test/.env.loadtest.example
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# OKE architecture/load validation overlay
|
||||||
|
# This file provides DEFAULTS for keys absent from templates/agent_template_backend/.env.
|
||||||
|
# It does NOT override backend values. Use .env.loadtest for explicit overrides.
|
||||||
|
# ============================================================================
|
||||||
|
APP_ENV=oke-load-test
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# OCI/OKE
|
||||||
|
K8S_NAMESPACE=agent-load-test
|
||||||
|
OKE_CLUSTER_OCID=ocid1.cluster.oc1..replace-me
|
||||||
|
OCI_CLI_PROFILE=DEFAULT
|
||||||
|
OCI_REGION=sa-saopaulo-1
|
||||||
|
OCI_REGION_KEY=gru
|
||||||
|
OCI_TENANCY_NAMESPACE=replace-me
|
||||||
|
OCIR_REPOSITORY_PREFIX=agent-framework-loadtest
|
||||||
|
IMAGE_TAG=loadtest
|
||||||
|
OCIR_USERNAME=<tenancy-namespace>/<usuario>
|
||||||
|
OCIR_AUTH_TOKEN=<auth-token>
|
||||||
|
OCIR_EMAIL=<email>
|
||||||
|
OCIR_PULL_SECRET_NAME=ocir-secret
|
||||||
|
|
||||||
|
|
||||||
|
# Existing OCI Load Balancer reuse. The Kubernetes service is NodePort; no new LB is created.
|
||||||
|
EXISTING_LB_OCID=ocid1.loadbalancer.oc1..replace-me
|
||||||
|
BACKEND_SET_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_PORT=8000
|
||||||
|
BACKEND_LISTENER_PROTOCOL=HTTP
|
||||||
|
BACKEND_HEALTH_PATH=/health
|
||||||
|
BACKEND_NODE_PORT=32116
|
||||||
|
LOADTEST_TARGET_MODE=external
|
||||||
|
# Optional: set this to bypass OCI CLI IP discovery.
|
||||||
|
# LOADTEST_EXTERNAL_URL=http://203.0.113.10:8000
|
||||||
|
|
||||||
|
# OCI GenAI. For OKE prefer workload identity or instance principal when available.
|
||||||
|
# Keep oci_sdk for the real end-to-end profile.
|
||||||
|
LLM_PROVIDER=oci_sdk
|
||||||
|
OCI_AUTH_MODE=oke_workload_identity
|
||||||
|
|
||||||
|
# Shared persistence: required for horizontal-scale validation.
|
||||||
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
USAGE_REPOSITORY_PROVIDER=autonomous
|
||||||
|
VECTOR_STORE_PROVIDER=autonomous
|
||||||
|
GRAPH_STORE_PROVIDER=autonomous
|
||||||
|
|
||||||
|
# Wallet is mounted by Kubernetes Secret at this fixed in-container path.
|
||||||
|
ADB_WALLET_LOCATION=/app/wallet
|
||||||
|
|
||||||
|
# Mongo-compatible endpoint / sequence store. Use the private VCN endpoint.
|
||||||
|
# IMPORTANT: standard port restored to 27017 (not 37017).
|
||||||
|
# Example only; replace host/user/password with the Autonomous/Mongo endpoint used in OCI.
|
||||||
|
MONGODB_URI=mongodb://user:password@autonomous-private-endpoint:27017/?tls=true
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
PUBSUB_SEQUENCE_PROVIDER=mongodb
|
||||||
|
PUBSUB_SEQUENCE_MONGODB_URI=mongodb://user:password@autonomous-private-endpoint:27017/?tls=true
|
||||||
|
PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
|
||||||
|
PUBSUB_SEQUENCE_MEMORY_FALLBACK=false
|
||||||
|
|
||||||
|
# Langfuse is installed in the same OKE cluster by deploy_langfuse.sh.
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000
|
||||||
|
LANGFUSE_TRACE_MODE=compact
|
||||||
|
LANGFUSE_IGNORE_HEALTHCHECKS=true
|
||||||
|
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
|
||||||
|
|
||||||
|
# Keep analytics disabled unless Pub/Sub is part of the test objective.
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
|
||||||
|
# MCP can be enabled in a second pass if you also deploy mcp-gateway.
|
||||||
|
ENABLE_MCP_TOOLS=false
|
||||||
|
MCP_GATEWAY_ENABLED=false
|
||||||
|
|
||||||
|
# Load-test deployment sizing
|
||||||
|
LOADTEST_MIN_REPLICAS=4
|
||||||
|
LOADTEST_MAX_REPLICAS=30
|
||||||
|
LOADTEST_CPU_REQUEST=500m
|
||||||
|
LOADTEST_CPU_LIMIT=2000m
|
||||||
|
LOADTEST_MEMORY_REQUEST=768Mi
|
||||||
|
LOADTEST_MEMORY_LIMIT=2Gi
|
||||||
|
|
||||||
|
# Load generator defaults
|
||||||
|
LOADTEST_TARGET=http://agent-template-backend-lb.agent-load-test.svc.cluster.local:8000
|
||||||
|
LOADTEST_AGENT_ID=telecom_contas
|
||||||
|
LOADTEST_TENANT_ID=loadtest
|
||||||
|
LOADTEST_DURATION=10m
|
||||||
|
LOADTEST_VUS=250
|
||||||
|
LOADTEST_MAX_VUS=1000
|
||||||
|
LOADTEST_RPS=100
|
||||||
|
LOADTEST_SCENARIO=unique_sessions
|
||||||
|
LOADTEST_MESSAGE=Explique em uma frase o que é uma fatura de telecom.
|
||||||
13
deploy/oke/load-test/.env.loadtest.override.example
Normal file
13
deploy/oke/load-test/.env.loadtest.override.example
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Optional explicit overrides for OKE/load testing.
|
||||||
|
# Copy to .env.loadtest ONLY when you intentionally want to replace values
|
||||||
|
# inherited from templates/agent_template_backend/.env.
|
||||||
|
#
|
||||||
|
# cp deploy/oke/load-test/.env.loadtest.override.example \
|
||||||
|
# deploy/oke/load-test/.env.loadtest
|
||||||
|
|
||||||
|
# Example:
|
||||||
|
# OCI_REGION=sa-saopaulo-1
|
||||||
|
# OCI_AUTH_MODE=oke_workload_identity
|
||||||
|
# ENABLE_ANALYTICS=true
|
||||||
|
# ENABLE_MCP_TOOLS=true
|
||||||
|
# MONGODB_URI=mongodb://...
|
||||||
159
deploy/oke/load-test/.env.runtime
Normal file
159
deploy/oke/load-test/.env.runtime
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Generated by prepare_env.py. Do not commit.
|
||||||
|
ADB_DSN=oradb23ai_high
|
||||||
|
ADB_PASSWORD=Moniquinha19721972
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
ADB_USER=admin
|
||||||
|
ADB_WALLET_LOCATION=/app/wallet
|
||||||
|
ADB_WALLET_PASSWORD=Moniquinha1972
|
||||||
|
AGENT_PUBSUB_TOPIC=teste_pubsub
|
||||||
|
ANALYTICS_PROVIDERS=pubsub
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
APP_ENV=oke-load-test
|
||||||
|
APP_NAME=ai-agent-template
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
|
DEFAULT_CHANNEL=web
|
||||||
|
EMBEDDING_PROVIDER=oci
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||||
|
ENABLE_INPUT_GUARDRAILS=true
|
||||||
|
ENABLE_JUDGES=true
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
ENABLE_MCP_TOOLS=false
|
||||||
|
ENABLE_OCI_STREAMING=false
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
ENABLE_OUTPUT_GUARDRAILS=true
|
||||||
|
ENABLE_OUTPUT_SUPERVISOR=true
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
ENABLE_REDIS_CACHE=false
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ENABLE_SUPERVISOR=true
|
||||||
|
ENABLE_TEXT_ADAPTER=true
|
||||||
|
ENABLE_VOICE_ADAPTER=true
|
||||||
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
|
END_SESSION_MESSAGE='Atendimento encerrado. Obrigado pelo contato.'
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
GCP_PROJECT_ID=project-7f829b68-bf43-442c-ad3
|
||||||
|
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||||
|
GCP_PUBSUB_TOPIC=teste_pubsub
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
|
||||||
|
GOOGLE_APPLICATION_CREDENTIALS=/mnt/d/Dropbox/ORACLE/TIM/FY27/Testes/sa-hoshikawa.json
|
||||||
|
GRAPH_STORE_PROVIDER=autonomous
|
||||||
|
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
HUMAN_HANDOFF_MESSAGE='Vou encaminhar seu atendimento para uma pessoa.'
|
||||||
|
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||||
|
IMAGE_TAG=loadtest
|
||||||
|
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||||
|
K8S_NAMESPACE=agent-load-test
|
||||||
|
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
|
||||||
|
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES='AGA.,NOC., IC.'
|
||||||
|
LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000
|
||||||
|
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
|
||||||
|
LANGFUSE_IGNORE_HEALTHCHECKS=true
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-2c82b991104f3f38f41878baecaaffd0
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-defd21d2c6da3d4a9f301d993236958f9e56d94f9050a00f79067267b78a00a0
|
||||||
|
LANGFUSE_TRACE_MODE=compact
|
||||||
|
LLM_MAX_TOKENS=2048
|
||||||
|
LLM_PROVIDER=oci_sdk
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_TIMEOUT_SECONDS=120
|
||||||
|
LOADTEST_AGENT_ID=telecom_contas
|
||||||
|
LOADTEST_CPU_LIMIT=2000m
|
||||||
|
LOADTEST_CPU_REQUEST=500m
|
||||||
|
LOADTEST_DURATION=10m
|
||||||
|
LOADTEST_MAX_REPLICAS=30
|
||||||
|
LOADTEST_MAX_VUS=1000
|
||||||
|
LOADTEST_MEMORY_LIMIT=2Gi
|
||||||
|
LOADTEST_MEMORY_REQUEST=768Mi
|
||||||
|
LOADTEST_MESSAGE='Explique em uma frase o que é uma fatura de telecom.'
|
||||||
|
LOADTEST_MIN_REPLICAS=4
|
||||||
|
LOADTEST_RPS=100
|
||||||
|
LOADTEST_SCENARIO=unique_sessions
|
||||||
|
LOADTEST_TARGET=http://agent-template-backend-lb.agent-load-test.svc.cluster.local:8000
|
||||||
|
LOADTEST_TENANT_ID=loadtest
|
||||||
|
LOADTEST_VUS=250
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
MCP_GATEWAY_ENABLED=false
|
||||||
|
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||||
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
MEMORY_CONTEXT_STRATEGY=summary
|
||||||
|
MEMORY_HISTORY_LIMIT=80
|
||||||
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||||
|
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
|
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||||
|
MEMORY_SUMMARY_USE_LLM=true
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
MONGODB_URI='mongodb://user:password@autonomous-private-endpoint:27017/?tls=true'
|
||||||
|
OCIR_REPOSITORY_PREFIX=agent-framework-loadtest
|
||||||
|
OCI_AUTH_MODE=oke_workload_identity
|
||||||
|
OCI_CLI_PROFILE=LATINOAMERICA-SaoPaulo
|
||||||
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
|
||||||
|
OCI_CONFIG_FILE='~/.oci/config'
|
||||||
|
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||||
|
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_PROJECT_OCID=''
|
||||||
|
OCI_PROFILE=LATINOAMERICA-Chicago
|
||||||
|
OCI_REGION=sa-saopaulo-1
|
||||||
|
OCI_REGION_KEY=gru
|
||||||
|
OCIR_USERNAME=idi1o0a010nx/oracleidentitycloudservice/cristiano.hoshikawa@oracle.com
|
||||||
|
OCIR_AUTH_TOKEN="A5XBm>8gvA2oDy;it1lq"
|
||||||
|
OCIR_EMAIL=cristiano.hoshikawa@oracle.com
|
||||||
|
OCIR_PULL_SECRET_NAME=ocir-secret
|
||||||
|
OCI_STREAM_ENDPOINT=''
|
||||||
|
OCI_STREAM_OCID=''
|
||||||
|
OCI_STREAM_PARTITION_KEY=agent-events
|
||||||
|
OCI_TENANCY_NAMESPACE=idi1o0a010nx
|
||||||
|
OKE_CLUSTER_OCID=ocid1.cluster.oc1.sa-saopaulo-1.aaaaaaaabpismmmj6kwo4idomzso5fr32xlnbjcm74pfa7bfhc52plqe64oq
|
||||||
|
EXISTING_LB_OCID=ocid1.loadbalancer.oc1.sa-saopaulo-1.aaaaaaaaklbmmriyabvxradqkchy5cobelzxj37rn27p2rsfef5uez54jfqa
|
||||||
|
BACKEND_SET_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_PORT=8000
|
||||||
|
BACKEND_LISTENER_PROTOCOL=HTTP
|
||||||
|
BACKEND_HEALTH_PATH=/health
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=''
|
||||||
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||||
|
PROJECT_ID=project-7f829b68-bf43-442c-ad3
|
||||||
|
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||||
|
PUBSUB_SEQUENCE_ENABLED=true
|
||||||
|
PUBSUB_SEQUENCE_KEY_PREFIX=observer:sequence
|
||||||
|
PUBSUB_SEQUENCE_MEMORY_FALLBACK=false
|
||||||
|
PUBSUB_SEQUENCE_MONGODB_COLLECTION=observer_event_counters
|
||||||
|
PUBSUB_SEQUENCE_MONGODB_DATABASE=agent_platform
|
||||||
|
PUBSUB_SEQUENCE_MONGODB_URI='mongodb://user:password@autonomous-private-endpoint:27017/?tls=true'
|
||||||
|
PUBSUB_SEQUENCE_PROVIDER=mongodb
|
||||||
|
PUBSUB_SEQUENCE_TTL_SECONDS=86400
|
||||||
|
PUBSUB_TOPIC_PATH=projects/project-7f829b68-bf43-442c-ad3/topics/teste_pubsub
|
||||||
|
RAG_FILE_GLOBS='*.md,*.txt,*.yaml,*.yml,*.json'
|
||||||
|
RAG_TOP_K=5
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
ROUTING_MODE=router
|
||||||
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
|
SIMULATE_AGA_MISSING_TRANSACTION_ID=true
|
||||||
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
USAGE_REPOSITORY_PROVIDER=autonomous
|
||||||
|
VECTOR_STORE_PROVIDER=autonomous
|
||||||
36
deploy/oke/load-test/CHANGES_2026-08-17.md
Normal file
36
deploy/oke/load-test/CHANGES_2026-08-17.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Alterações do pacote OKE load test — 2026-08-17
|
||||||
|
|
||||||
|
- substituído o fluxo de criação de novo OCI Load Balancer pelo reaproveitamento de `EXISTING_LB_OCID`;
|
||||||
|
- Service externo alterado para `NodePort`, com `BACKEND_NODE_PORT` configurável;
|
||||||
|
- `configure_existing_lb.sh` cria/reutiliza backend set, backends dos workers e listener;
|
||||||
|
- smoke/load test externo resolvem o IP do LB existente ou usam `LOADTEST_EXTERNAL_URL`;
|
||||||
|
- bootstrap headless do Langfuse passa a ser validado com API autenticada;
|
||||||
|
- criado `sync_langfuse_client_secret.sh` para copiar as keys do projeto Langfuse para o namespace do backend;
|
||||||
|
- backend recebe `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY` e `LANGFUSE_HOST` explicitamente do Secret `langfuse-client`;
|
||||||
|
- checksum de runtime no pod template força rollout quando env/keys mudam;
|
||||||
|
- `validate_dependencies.sh` agora valida Oracle, Mongo, health do Langfuse e API autenticada;
|
||||||
|
- smoke test confirma ingestão do trace no Langfuse (`LANGFUSE_TRACE_OK`);
|
||||||
|
- `01_deploy_all.sh` executa configuração do LB existente antes das validações;
|
||||||
|
- manual reescrito com fluxo completo, troubleshooting e critérios de aprovação.
|
||||||
|
|
||||||
|
## v4 - Langfuse Secret synchronization fix
|
||||||
|
|
||||||
|
- `sync_langfuse_client_secret.sh` no longer sources `.env.runtime`, preventing backend OCI variables from altering the OCI CLI exec-plugin environment used by `kubectl`.
|
||||||
|
- A failed `kubectl get secret` is no longer mislabeled as `Secret not found`; the actual Kubernetes authentication/context error is printed.
|
||||||
|
- `deploy_langfuse.sh` verifies `Secret/langfuse-runtime` immediately before synchronization and explicitly passes Langfuse/backend namespaces to the child script.
|
||||||
|
- Synchronization can fall back to `.env.langfuse` only for key values after Kubernetes Secret access has been validated.
|
||||||
|
|
||||||
|
## Documentação — inicialização manual do Langfuse
|
||||||
|
|
||||||
|
- README atualizado para documentar explicitamente o fluxo pela UI após subir o Langfuse.
|
||||||
|
- Passos incluídos: criação de usuário, organização/empresa, projeto e API keys.
|
||||||
|
- Documentado onde copiar `pk-lf-*` e `sk-lf-*` em `.env.langfuse` e `.env.runtime`.
|
||||||
|
- Incluída atualização de `Secret/langfuse-runtime`, sincronização para `Secret/langfuse-client` e validação obrigatória com `LANGFUSE_AUTH_OK` antes do deploy do backend.
|
||||||
|
- O bootstrap headless continua existente no código, mas o procedimento operacional recomendado para esta validação passa a confirmar/criar o projeto e as chaves pela UI.
|
||||||
|
|
||||||
|
|
||||||
|
## v6 - Langfuse validation fix
|
||||||
|
- Replaced `kubectl run --overrides` JSON with a regular Pod YAML manifest.
|
||||||
|
- Fixed `Invalid JSON Patch` caused by invalid JSON escaping in the validator.
|
||||||
|
- Validator no longer sources `.env.runtime`; it reads only required keys.
|
||||||
|
- Added Kubernetes authentication preflight and explicit Langfuse HTTP auth result.
|
||||||
728
deploy/oke/load-test/README.md
Normal file
728
deploy/oke/load-test/README.md
Normal file
@@ -0,0 +1,728 @@
|
|||||||
|
# Validação arquitetural de carga — Agent Framework OCI / OKE
|
||||||
|
|
||||||
|
Este pacote executa validação arquitetural e teste de carga do `agent_template_backend` em OCI OKE usando **persistência compartilhada**, **OCI Generative AI**, **Langfuse v3** e um **OCI Load Balancer preexistente**. O pacote não cria um novo OCI Load Balancer: o acesso externo é feito por `NodePort` nos workers e por um listener/backend set criado no LB informado em `EXISTING_LB_OCID`.
|
||||||
|
|
||||||
|
## Arquitetura do teste
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cliente / k6 externo
|
||||||
|
|
|
||||||
|
v
|
||||||
|
OCI Load Balancer existente :BACKEND_LISTENER_PORT
|
||||||
|
|
|
||||||
|
v
|
||||||
|
backend set agent-framework-loadtest
|
||||||
|
|
|
||||||
|
+--> worker-1:BACKEND_NODE_PORT
|
||||||
|
+--> worker-2:BACKEND_NODE_PORT
|
||||||
|
+--> worker-N:BACKEND_NODE_PORT
|
||||||
|
|
|
||||||
|
v
|
||||||
|
Kubernetes Service NodePort
|
||||||
|
|
|
||||||
|
v
|
||||||
|
agent-template-backend (HPA)
|
||||||
|
|
|
||||||
|
+--> Autonomous Database / wallet
|
||||||
|
+--> Mongo-compatible endpoint
|
||||||
|
+--> OCI Generative AI
|
||||||
|
+--> Langfuse v3
|
||||||
|
```
|
||||||
|
|
||||||
|
O Service `agent-template-backend` continua `ClusterIP` para testes internos. O Service `agent-template-backend-lb` é `NodePort`; ele **não** solicita um novo LB à OCI.
|
||||||
|
|
||||||
|
## Estrutura principal
|
||||||
|
|
||||||
|
```text
|
||||||
|
deploy/oke/load-test/
|
||||||
|
.env.loadtest.example
|
||||||
|
.env.runtime # gerado; não commitar
|
||||||
|
.env.langfuse # gerado; não commitar
|
||||||
|
README.md
|
||||||
|
k8s/
|
||||||
|
agent-backend.yaml
|
||||||
|
k6-job.yaml
|
||||||
|
langfuse/
|
||||||
|
langfuse-k8s.yaml
|
||||||
|
loadgen/
|
||||||
|
loadtest.js
|
||||||
|
scripts/
|
||||||
|
prepare_env.sh
|
||||||
|
configure_kubeconfig.sh
|
||||||
|
deploy_langfuse.sh
|
||||||
|
sync_langfuse_client_secret.sh
|
||||||
|
validate_langfuse_integration.sh
|
||||||
|
build_push_backend.sh
|
||||||
|
deploy_backend.sh
|
||||||
|
configure_existing_lb.sh
|
||||||
|
validate_dependencies.sh
|
||||||
|
smoke_test.sh
|
||||||
|
run_load_test.sh
|
||||||
|
run_architecture_suite.sh
|
||||||
|
watch_test.sh
|
||||||
|
collect_results.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Preparar `.env.runtime`
|
||||||
|
|
||||||
|
Execute da raiz do repositório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/prepare_env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script usa `templates/agent_template_backend/.env` como configuração principal, completa somente as chaves ausentes com `deploy/oke/load-test/.env.loadtest.example` e gera:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deploy/oke/load-test/.env.runtime
|
||||||
|
deploy/oke/load-test/.env.langfuse
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Importante sobre Langfuse:** o script pode gerar valores de inicialização (`LANGFUSE_INIT_*`) para bootstrap headless, mas, para esta validação, o procedimento recomendado é confirmar o ambiente pela UI do Langfuse e criar manualmente o usuário, organização/empresa, projeto e API keys. As chaves criadas na UI devem ser copiadas para os arquivos de configuração antes do deploy do backend.
|
||||||
|
|
||||||
|
Preencha no `.env.runtime` os parâmetros reais de OCI/DB/OCIR e o LB existente. Para o LB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
EXISTING_LB_OCID=ocid1.loadbalancer.oc1.sa-saopaulo-1....
|
||||||
|
BACKEND_SET_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_NAME=agent-framework-loadtest
|
||||||
|
BACKEND_LISTENER_PORT=8000
|
||||||
|
BACKEND_LISTENER_PROTOCOL=HTTP
|
||||||
|
BACKEND_HEALTH_PATH=/health
|
||||||
|
BACKEND_NODE_PORT=32116
|
||||||
|
LOADTEST_TARGET_MODE=external
|
||||||
|
```
|
||||||
|
|
||||||
|
Opcionalmente, fixe diretamente a URL externa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LOADTEST_EXTERNAL_URL=http://<IP_DO_LB>:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Wallet e acesso ao OKE
|
||||||
|
|
||||||
|
Copie a wallet real para:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/agent_template_backend/wallet/
|
||||||
|
```
|
||||||
|
|
||||||
|
Ela será criada como Kubernetes Secret e montada em `/app/wallet`.
|
||||||
|
|
||||||
|
Configure kubeconfig:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/configure_kubeconfig.sh
|
||||||
|
kubectl get nodes -o wide
|
||||||
|
kubectl top nodes
|
||||||
|
```
|
||||||
|
|
||||||
|
`kubectl top nodes` deve funcionar para o HPA baseado em CPU/memória.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Subir o Langfuse v3 e criar manualmente usuário, organização, projeto e API keys
|
||||||
|
|
||||||
|
## 3.1 Subir a infraestrutura do Langfuse
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/deploy_langfuse.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O deploy cria os componentes Kubernetes do Langfuse v3, incluindo web, worker, Postgres, Redis, ClickHouse, MinIO e os Secrets de runtime.
|
||||||
|
|
||||||
|
Se a etapa de validação autenticada falhar porque ainda não existem API keys válidas, prossiga com a inicialização manual abaixo. A infraestrutura do Langfuse já pode estar operacional mesmo que a validação das chaves ainda não tenha passado.
|
||||||
|
|
||||||
|
Confirme:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n langfuse get pods
|
||||||
|
kubectl -n langfuse get svc langfuse-web
|
||||||
|
```
|
||||||
|
|
||||||
|
Os pods principais devem estar `Running`/`Ready`.
|
||||||
|
|
||||||
|
## 3.2 Abrir a interface do Langfuse
|
||||||
|
|
||||||
|
Abra um port-forward em outro terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n langfuse port-forward --address 127.0.0.1 svc/langfuse-web 3005:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Acesse no navegador:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:3005
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.3 Criar o usuário
|
||||||
|
|
||||||
|
Na primeira abertura do Langfuse:
|
||||||
|
|
||||||
|
1. crie o usuário administrador;
|
||||||
|
2. faça login com esse usuário;
|
||||||
|
3. confirme que a interface principal do Langfuse foi carregada.
|
||||||
|
|
||||||
|
Para ambiente de teste, pode ser utilizado um usuário dedicado ao load test. Não use credenciais pessoais de produção no arquivo do projeto.
|
||||||
|
|
||||||
|
## 3.4 Criar a organização/empresa
|
||||||
|
|
||||||
|
Dentro do Langfuse, crie ou selecione a organização que será usada pelo teste.
|
||||||
|
|
||||||
|
Sugestão de nome:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Framework OCI
|
||||||
|
```
|
||||||
|
|
||||||
|
A UI do Langfuse pode usar o termo **Organization**. Neste manual, organização/empresa representam a mesma entidade de agrupamento do projeto.
|
||||||
|
|
||||||
|
## 3.5 Criar o projeto
|
||||||
|
|
||||||
|
Dentro dessa organização, crie o projeto que receberá os traces do teste.
|
||||||
|
|
||||||
|
Sugestão:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Framework Load Test
|
||||||
|
```
|
||||||
|
|
||||||
|
Não prossiga para o backend enquanto não conseguir abrir esse projeto na UI.
|
||||||
|
|
||||||
|
## 3.6 Criar as API keys do projeto
|
||||||
|
|
||||||
|
No projeto criado, abra a área de configuração/API Keys e crie um novo par de credenciais.
|
||||||
|
|
||||||
|
Você receberá duas chaves:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Public Key: pk-lf-...
|
||||||
|
Secret Key: sk-lf-...
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Atenção:** copie a `Secret Key` no momento da criação. Dependendo da tela/versão, ela pode não ser exibida novamente.
|
||||||
|
|
||||||
|
Essas são as credenciais que o `agent_template_backend` usará para enviar traces ao Langfuse.
|
||||||
|
|
||||||
|
## 3.7 Copiar as API keys para `.env.langfuse`
|
||||||
|
|
||||||
|
Edite:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deploy/oke/load-test/.env.langfuse
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua/ajuste estas quatro linhas com o par criado na UI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-COLE_A_PUBLIC_KEY_AQUI
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-COLE_A_SECRET_KEY_AQUI
|
||||||
|
LANGFUSE_INIT_PROJECT_PUBLIC_KEY=pk-lf-COLE_A_MESMA_PUBLIC_KEY_AQUI
|
||||||
|
LANGFUSE_INIT_PROJECT_SECRET_KEY=sk-lf-COLE_A_MESMA_SECRET_KEY_AQUI
|
||||||
|
```
|
||||||
|
|
||||||
|
Para um projeto já criado manualmente na UI, os campos `LANGFUSE_INIT_PROJECT_*` são mantidos com os mesmos valores para que os scripts de sincronização do pacote encontrem um único par de chaves consistente.
|
||||||
|
|
||||||
|
Não exiba nem versione a `LANGFUSE_SECRET_KEY`.
|
||||||
|
|
||||||
|
## 3.8 Copiar as API keys para `.env.runtime`
|
||||||
|
|
||||||
|
Edite também:
|
||||||
|
|
||||||
|
```text
|
||||||
|
deploy/oke/load-test/.env.runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Garanta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_HOST=http://langfuse-web.langfuse.svc.cluster.local:3000
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-COLE_A_PUBLIC_KEY_AQUI
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-COLE_A_SECRET_KEY_AQUI
|
||||||
|
```
|
||||||
|
|
||||||
|
As chaves em `.env.runtime` devem ser exatamente as mesmas criadas no projeto pela UI.
|
||||||
|
|
||||||
|
## 3.9 Atualizar o Secret Kubernetes do Langfuse
|
||||||
|
|
||||||
|
Depois de colar as chaves nos arquivos, atualize `Secret/langfuse-runtime` sem imprimir os valores:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
set -a
|
||||||
|
source deploy/oke/load-test/.env.langfuse
|
||||||
|
set +a
|
||||||
|
|
||||||
|
kubectl -n langfuse create secret generic langfuse-runtime \
|
||||||
|
--from-env-file=deploy/oke/load-test/.env.langfuse \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
```
|
||||||
|
|
||||||
|
Para um projeto criado manualmente, não é necessário recriar o banco do Langfuse. O objetivo desta etapa é manter o Secret usado pelos scripts sincronizado com as credenciais reais do projeto.
|
||||||
|
|
||||||
|
## 3.10 Sincronizar as chaves para o namespace do backend
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LANGFUSE_NAMESPACE=langfuse \
|
||||||
|
K8S_NAMESPACE=agent-load-test \
|
||||||
|
./deploy/oke/load-test/scripts/sync_langfuse_client_secret.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script cria/atualiza:
|
||||||
|
|
||||||
|
```text
|
||||||
|
namespace: agent-load-test
|
||||||
|
Secret: langfuse-client
|
||||||
|
```
|
||||||
|
|
||||||
|
com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LANGFUSE_PUBLIC_KEY
|
||||||
|
LANGFUSE_SECRET_KEY
|
||||||
|
LANGFUSE_HOST
|
||||||
|
```
|
||||||
|
|
||||||
|
Confira apenas a existência do Secret:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-load-test get secret langfuse-client
|
||||||
|
```
|
||||||
|
|
||||||
|
Para conferir somente a Public Key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-load-test get secret langfuse-client \
|
||||||
|
-o jsonpath='{.data.LANGFUSE_PUBLIC_KEY}' | base64 -d
|
||||||
|
|
||||||
|
echo
|
||||||
|
```
|
||||||
|
|
||||||
|
Ela deve começar com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pk-lf-
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.11 Validar autenticação no Langfuse antes de prosseguir
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/validate_langfuse_integration.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LANGFUSE_AUTH_OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse teste chama o Langfuse pela rede interna do OKE e usa `LANGFUSE_PUBLIC_KEY` + `LANGFUSE_SECRET_KEY`. Portanto, `LANGFUSE_AUTH_OK` comprova que o par de chaves é aceito pelo projeto.
|
||||||
|
|
||||||
|
Se receber `401`/`403`, não prossiga para o backend. Revise:
|
||||||
|
|
||||||
|
```text
|
||||||
|
.env.langfuse
|
||||||
|
.env.runtime
|
||||||
|
Secret/langfuse-runtime no namespace langfuse
|
||||||
|
Secret/langfuse-client no namespace agent-load-test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.12 Ordem obrigatória antes de continuar
|
||||||
|
|
||||||
|
A sequência esperada é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Langfuse Kubernetes operacional
|
||||||
|
↓
|
||||||
|
criar usuário
|
||||||
|
↓
|
||||||
|
criar/selecionar organização (empresa)
|
||||||
|
↓
|
||||||
|
criar projeto
|
||||||
|
↓
|
||||||
|
criar Public Key + Secret Key
|
||||||
|
↓
|
||||||
|
colar keys em .env.langfuse
|
||||||
|
↓
|
||||||
|
colar keys em .env.runtime
|
||||||
|
↓
|
||||||
|
atualizar Secret/langfuse-runtime
|
||||||
|
↓
|
||||||
|
sync_langfuse_client_secret.sh
|
||||||
|
↓
|
||||||
|
validate_langfuse_integration.sh
|
||||||
|
↓
|
||||||
|
LANGFUSE_AUTH_OK
|
||||||
|
↓
|
||||||
|
somente então fazer deploy do backend
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Build e push do backend
|
||||||
|
|
||||||
|
Garanta que o repositório OCIR já exista e faça login:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login gru.ocir.io
|
||||||
|
./deploy/oke/load-test/scripts/build_push_backend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
A imagem utilizada fica registrada em `deploy/oke/load-test/.backend-image`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Deploy do backend com NodePort
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/deploy_backend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O deploy cria/atualiza:
|
||||||
|
|
||||||
|
- `agent-backend-runtime`;
|
||||||
|
- `agent-backend-wallet`;
|
||||||
|
- `langfuse-client`;
|
||||||
|
- Deployment do backend;
|
||||||
|
- `ClusterIP` interno;
|
||||||
|
- `NodePort` externo (`BACKEND_NODE_PORT`, default `32116`);
|
||||||
|
- HPA;
|
||||||
|
- PDB.
|
||||||
|
|
||||||
|
Confira:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-load-test get pods -o wide
|
||||||
|
kubectl -n agent-load-test get svc
|
||||||
|
kubectl -n agent-load-test get hpa
|
||||||
|
```
|
||||||
|
|
||||||
|
O Service externo deve aparecer como `NodePort`, e não `LoadBalancer`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Reutilizar o OCI Load Balancer existente
|
||||||
|
|
||||||
|
Depois que o NodePort existir:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/configure_existing_lb.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O script é idempotente. Ele:
|
||||||
|
|
||||||
|
1. valida `EXISTING_LB_OCID`;
|
||||||
|
2. descobre `BACKEND_NODE_PORT` no Service;
|
||||||
|
3. verifica o backend set;
|
||||||
|
4. cria `BACKEND_SET_NAME` se não existir;
|
||||||
|
5. aguarda a operação OCI concluir;
|
||||||
|
6. descobre os `InternalIP` dos workers OKE;
|
||||||
|
7. adiciona os workers ainda não cadastrados como `<worker-ip>:<node-port>`;
|
||||||
|
8. verifica/cria o listener `BACKEND_LISTENER_NAME`;
|
||||||
|
9. aponta o listener para o backend set;
|
||||||
|
10. mostra o health do backend set.
|
||||||
|
|
||||||
|
Arquitetura resultante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OCI LB existente
|
||||||
|
-> listener :8000
|
||||||
|
-> backend set
|
||||||
|
-> worker InternalIP:32116
|
||||||
|
-> Service NodePort
|
||||||
|
-> pods do backend
|
||||||
|
```
|
||||||
|
|
||||||
|
As Security Lists/NSGs precisam permitir tráfego do LB para os workers na porta NodePort.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Validar dependências antes do smoke/carga
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/validate_dependencies.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse teste é executado **de dentro do OKE** usando a mesma imagem, env e wallet do backend. Ele valida:
|
||||||
|
|
||||||
|
- conexão real com Oracle/Autonomous usando `/app/wallet`;
|
||||||
|
- `ping` real no Mongo-compatible endpoint;
|
||||||
|
- `/api/public/health` do Langfuse;
|
||||||
|
- presença das API keys Langfuse;
|
||||||
|
- autenticação real em `/api/public/projects` usando `LANGFUSE_PUBLIC_KEY` + `LANGFUSE_SECRET_KEY`.
|
||||||
|
|
||||||
|
Resultado esperado termina com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dependencies: ALL_DEPENDENCIES_OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse passo valida conectividade e autenticação. Ele não faz uma chamada de negócio ao LLM; isso é responsabilidade do smoke test.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Smoke test end-to-end
|
||||||
|
|
||||||
|
## Externo — caminho real pelo OCI LB existente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/smoke_test.sh external
|
||||||
|
```
|
||||||
|
|
||||||
|
Caminho validado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cliente
|
||||||
|
-> OCI LB existente
|
||||||
|
-> listener
|
||||||
|
-> backend set
|
||||||
|
-> NodePort
|
||||||
|
-> pod
|
||||||
|
-> LangGraph/framework
|
||||||
|
-> RAG/memória conforme configuração
|
||||||
|
-> OCI Generative AI (quando LLM_PROVIDER=oci_sdk)
|
||||||
|
-> persistência
|
||||||
|
-> Langfuse
|
||||||
|
```
|
||||||
|
|
||||||
|
O smoke executa `/health`, depois `POST /gateway/message`. Quando Langfuse está habilitado, ele também consulta a Public API autenticada e aguarda o trace da sessão `smoke-*`. O sucesso final inclui:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LANGFUSE_TRACE_OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Assim, um `HTTP 200` sozinho não é mais considerado evidência suficiente de observabilidade.
|
||||||
|
|
||||||
|
## Interno — sem OCI LB
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/smoke_test.sh internal
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse modo usa o `ClusterIP` e serve para separar problemas do backend/Kubernetes de problemas do LB externo.
|
||||||
|
|
||||||
|
Para provar chamadas reais de OCI Generative AI, mantenha:
|
||||||
|
|
||||||
|
```text
|
||||||
|
LLM_PROVIDER=oci_sdk
|
||||||
|
OCI_AUTH_MODE=oke_workload_identity
|
||||||
|
```
|
||||||
|
|
||||||
|
Nos logs devem aparecer `OCI SDK GenAI client`, `OnDemandServingMode` e eventos `llm.*` com o modelo configurado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Teste de carga
|
||||||
|
|
||||||
|
## Interno
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/run_load_test.sh internal
|
||||||
|
```
|
||||||
|
|
||||||
|
Alvo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://agent-template-backend.agent-load-test.svc.cluster.local:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Externo pelo LB preexistente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/run_load_test.sh external
|
||||||
|
```
|
||||||
|
|
||||||
|
O script resolve o endereço do LB usando `EXISTING_LB_OCID` via OCI CLI, ou usa `LOADTEST_EXTERNAL_URL` se definida.
|
||||||
|
|
||||||
|
Acompanhe:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-load-test logs -f job/agent-load-generator
|
||||||
|
./deploy/oke/load-test/scripts/watch_test.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Distribuição pelo LB:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/check_load_balancing.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Suite arquitetural
|
||||||
|
|
||||||
|
Externa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/run_architecture_suite.sh external
|
||||||
|
```
|
||||||
|
|
||||||
|
Interna:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/run_architecture_suite.sh internal
|
||||||
|
```
|
||||||
|
|
||||||
|
Cenários padrão:
|
||||||
|
|
||||||
|
```text
|
||||||
|
warmup 5 rps 2 min unique_sessions
|
||||||
|
baseline 25 rps 5 min unique_sessions
|
||||||
|
scale 100 rps 10 min unique_sessions
|
||||||
|
shared-state 50 rps 10 min shared_sessions
|
||||||
|
```
|
||||||
|
|
||||||
|
`unique_sessions` estressa criação de estado/checkpoints/DB/Langfuse/LLM. `shared_sessions` reutiliza sessões entre chamadas e é importante para provar que a aplicação não depende de memória local do pod.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Execução completa automatizada
|
||||||
|
|
||||||
|
Depois de preencher `.env.runtime` e colocar a wallet:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/01_deploy_all.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Ordem:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Langfuse + API keys
|
||||||
|
-> build/push backend
|
||||||
|
-> deploy backend NodePort
|
||||||
|
-> configurar LB existente
|
||||||
|
-> validar dependências
|
||||||
|
-> smoke externo + confirmação do trace Langfuse
|
||||||
|
```
|
||||||
|
|
||||||
|
Só depois desse fluxo passar execute carga sustentada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Critérios de aprovação
|
||||||
|
|
||||||
|
Antes da carga:
|
||||||
|
|
||||||
|
```text
|
||||||
|
kubectl nodes Ready
|
||||||
|
metrics-server OK
|
||||||
|
Langfuse AUTH OK
|
||||||
|
Oracle connection OK
|
||||||
|
Mongo ping OK
|
||||||
|
backend rollout OK
|
||||||
|
OCI LB backend-set healthy
|
||||||
|
smoke HTTP 200
|
||||||
|
OCI GenAI real nos logs (perfil end-to-end)
|
||||||
|
LANGFUSE_TRACE_OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Durante a carga, procure:
|
||||||
|
|
||||||
|
- nenhum `CrashLoopBackOff`/`OOMKilled`;
|
||||||
|
- ausência de deadlock/cross-event-loop errors;
|
||||||
|
- sessão consistente entre pods;
|
||||||
|
- checkpoints recuperáveis por qualquer réplica;
|
||||||
|
- HPA escalando quando as métricas atingirem os targets;
|
||||||
|
- 5xx próximos de zero;
|
||||||
|
- 429/timeouts do LLM separados de erros internos;
|
||||||
|
- traces chegando ao Langfuse sem criar falha em cascata.
|
||||||
|
|
||||||
|
O backend é I/O-bound em chamadas de LLM/DB; CPU/memória não são métricas perfeitas de autoscaling. Para produção, considere métricas de aplicação como `in_flight_requests`, `request_queue_depth` e p95 de latência via KEDA/Prometheus Adapter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Coletar evidências
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/collect_results.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Os arquivos ficam em `deploy/oke/load-test/results/`. Use em conjunto com Langfuse para correlacionar request/session/trace.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 14. Resiliência
|
||||||
|
|
||||||
|
Com carga ativa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n agent-load-test delete pod <pod>
|
||||||
|
kubectl -n agent-load-test rollout restart deployment/agent-template-backend
|
||||||
|
kubectl -n agent-load-test rollout status deployment/agent-template-backend
|
||||||
|
```
|
||||||
|
|
||||||
|
A perda de uma réplica não deve perder estado compartilhado nem interromper o serviço enquanto houver capacidade saudável.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 15. Troubleshooting de credenciais Langfuse em instalação já existente
|
||||||
|
|
||||||
|
O bootstrap headless é idempotente e foi desenhado para criar recursos que ainda não existem. Em um ambiente de teste que já possua PVCs do Langfuse inicializados anteriormente com outro projeto ou outro par de API keys, `validate_langfuse_integration.sh` pode retornar erro de autenticação mesmo que `/api/public/health` esteja `200`.
|
||||||
|
|
||||||
|
Primeiro confirme sem revelar a secret key:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n langfuse get secret langfuse-runtime \
|
||||||
|
-o jsonpath='{.data.LANGFUSE_INIT_PROJECT_PUBLIC_KEY}' | base64 -d; echo
|
||||||
|
|
||||||
|
kubectl -n agent-load-test get secret langfuse-client \
|
||||||
|
-o jsonpath='{.data.LANGFUSE_PUBLIC_KEY}' | base64 -d; echo
|
||||||
|
```
|
||||||
|
|
||||||
|
As public keys precisam ser iguais. Depois execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/validate_langfuse_integration.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Se as keys estiverem sincronizadas no Kubernetes mas a API autenticada falhar, o banco persistente do Langfuse provavelmente já contém credenciais diferentes. Para um ambiente descartável de load test, a opção mais limpa é recriar a stack/PVCs do Langfuse e executar `deploy_langfuse.sh` novamente. Não apague PVCs de um Langfuse que contenha dados que precisem ser preservados.
|
||||||
|
|
||||||
|
Depois de qualquer alteração de keys, execute novamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/deploy_backend.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
O checksum de runtime força a criação de pods novos, garantindo que as novas credenciais entrem no environment do processo Python.
|
||||||
|
|
||||||
|
|
||||||
|
## Precedência das variáveis de ambiente
|
||||||
|
|
||||||
|
`prepare_env.sh` usa o `.env` real do backend como fonte principal. A ordem é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/agent_template_backend/.env (fonte principal)
|
||||||
|
↓
|
||||||
|
.env.loadtest.example (somente defaults ausentes)
|
||||||
|
↓
|
||||||
|
.env.loadtest (override explícito opcional)
|
||||||
|
↓
|
||||||
|
overrides obrigatórios OKE/container (wallet e Langfuse interno)
|
||||||
|
↓
|
||||||
|
.env.runtime
|
||||||
|
```
|
||||||
|
|
||||||
|
Portanto, `ENABLE_MCP_TOOLS`, `ENABLE_ANALYTICS`, endpoints, banco, modelos e demais configurações do agente são preservados por padrão. Para substituir deliberadamente alguma delas no teste, copie `.env.loadtest.override.example` para `.env.loadtest` e altere somente as chaves desejadas.
|
||||||
|
|
||||||
|
Depois de executar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./deploy/oke/load-test/scripts/prepare_env.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
o script informa quantas variáveis do `.env` do backend foram preservadas e lista apenas as que precisaram mudar por compatibilidade com OKE/container.
|
||||||
190
deploy/oke/load-test/k8s/agent-backend.yaml
Normal file
190
deploy/oke/load-test/k8s/agent-backend.yaml
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: agent-load-test
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: agent-template-backend
|
||||||
|
namespace: agent-load-test
|
||||||
|
spec:
|
||||||
|
replicas: 4
|
||||||
|
strategy:
|
||||||
|
type: RollingUpdate
|
||||||
|
rollingUpdate:
|
||||||
|
maxUnavailable: 0
|
||||||
|
maxSurge: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: agent-template-backend
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: agent-template-backend
|
||||||
|
annotations:
|
||||||
|
loadtest.runtime-checksum: "RUNTIME_CHECKSUM_PLACEHOLDER"
|
||||||
|
spec:
|
||||||
|
terminationGracePeriodSeconds: 60
|
||||||
|
topologySpreadConstraints:
|
||||||
|
- maxSkew: 1
|
||||||
|
topologyKey: kubernetes.io/hostname
|
||||||
|
whenUnsatisfiable: ScheduleAnyway
|
||||||
|
labelSelector:
|
||||||
|
matchLabels:
|
||||||
|
app: agent-template-backend
|
||||||
|
containers:
|
||||||
|
- name: agent-template-backend
|
||||||
|
image: IMAGE_PLACEHOLDER
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 8000
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: agent-backend-runtime
|
||||||
|
env:
|
||||||
|
- name: LANGFUSE_PUBLIC_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_PUBLIC_KEY
|
||||||
|
- name: LANGFUSE_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_SECRET_KEY
|
||||||
|
- name: LANGFUSE_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_HOST
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 10
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8000
|
||||||
|
initialDelaySeconds: 45
|
||||||
|
periodSeconds: 20
|
||||||
|
timeoutSeconds: 5
|
||||||
|
failureThreshold: 6
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /health
|
||||||
|
port: 8000
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 36
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: "500m"
|
||||||
|
memory: "768Mi"
|
||||||
|
limits:
|
||||||
|
cpu: "2000m"
|
||||||
|
memory: "2Gi"
|
||||||
|
volumeMounts:
|
||||||
|
- name: wallet
|
||||||
|
mountPath: /app/wallet
|
||||||
|
readOnly: true
|
||||||
|
volumes:
|
||||||
|
- name: wallet
|
||||||
|
secret:
|
||||||
|
secretName: agent-backend-wallet
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: agent-template-backend
|
||||||
|
namespace: agent-load-test
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
sessionAffinity: None
|
||||||
|
selector:
|
||||||
|
app: agent-template-backend
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: agent-template-backend-lb
|
||||||
|
namespace: agent-load-test
|
||||||
|
#spec:
|
||||||
|
# type: LoadBalancer
|
||||||
|
# sessionAffinity: None
|
||||||
|
# selector:
|
||||||
|
# app: agent-template-backend
|
||||||
|
# ports:
|
||||||
|
# - name: http
|
||||||
|
# port: 8000
|
||||||
|
# targetPort: 8000
|
||||||
|
spec:
|
||||||
|
type: NodePort
|
||||||
|
selector:
|
||||||
|
app: agent-template-backend
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8000
|
||||||
|
targetPort: 8000
|
||||||
|
nodePort: NODE_PORT_PLACEHOLDER
|
||||||
|
---
|
||||||
|
apiVersion: autoscaling/v2
|
||||||
|
kind: HorizontalPodAutoscaler
|
||||||
|
metadata:
|
||||||
|
name: agent-template-backend
|
||||||
|
namespace: agent-load-test
|
||||||
|
spec:
|
||||||
|
scaleTargetRef:
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
name: agent-template-backend
|
||||||
|
minReplicas: 4
|
||||||
|
maxReplicas: 30
|
||||||
|
behavior:
|
||||||
|
scaleUp:
|
||||||
|
stabilizationWindowSeconds: 0
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 100
|
||||||
|
periodSeconds: 60
|
||||||
|
- type: Pods
|
||||||
|
value: 8
|
||||||
|
periodSeconds: 60
|
||||||
|
selectPolicy: Max
|
||||||
|
scaleDown:
|
||||||
|
stabilizationWindowSeconds: 300
|
||||||
|
policies:
|
||||||
|
- type: Percent
|
||||||
|
value: 25
|
||||||
|
periodSeconds: 60
|
||||||
|
metrics:
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: cpu
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 65
|
||||||
|
- type: Resource
|
||||||
|
resource:
|
||||||
|
name: memory
|
||||||
|
target:
|
||||||
|
type: Utilization
|
||||||
|
averageUtilization: 75
|
||||||
|
---
|
||||||
|
apiVersion: policy/v1
|
||||||
|
kind: PodDisruptionBudget
|
||||||
|
metadata:
|
||||||
|
name: agent-template-backend
|
||||||
|
namespace: agent-load-test
|
||||||
|
spec:
|
||||||
|
minAvailable: 3
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: agent-template-backend
|
||||||
28
deploy/oke/load-test/k8s/k6-job.yaml
Normal file
28
deploy/oke/load-test/k8s/k6-job.yaml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
apiVersion: batch/v1
|
||||||
|
kind: Job
|
||||||
|
metadata:
|
||||||
|
name: agent-load-generator
|
||||||
|
namespace: agent-load-test
|
||||||
|
spec:
|
||||||
|
backoffLimit: 0
|
||||||
|
ttlSecondsAfterFinished: 3600
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: agent-load-generator
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: k6
|
||||||
|
image: grafana/k6:latest
|
||||||
|
args: ["run", "/scripts/loadtest.js"]
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: agent-backend-runtime
|
||||||
|
volumeMounts:
|
||||||
|
- name: script
|
||||||
|
mountPath: /scripts
|
||||||
|
volumes:
|
||||||
|
- name: script
|
||||||
|
configMap:
|
||||||
|
name: k6-load-script
|
||||||
633
deploy/oke/load-test/langfuse/langfuse-k8s.yaml
Normal file
633
deploy/oke/load-test/langfuse/langfuse-k8s.yaml
Normal file
@@ -0,0 +1,633 @@
|
|||||||
|
# OKE load-test Langfuse manifest v11 - web HOSTNAME bind fix
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: langfuse
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: mongo-data
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: postgres-data
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: clickhouse-data
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 30Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: clickhouse-logs
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 10Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: PersistentVolumeClaim
|
||||||
|
metadata:
|
||||||
|
name: minio-data
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
accessModes:
|
||||||
|
- ReadWriteOnce
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
storage: 20Gi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ConfigMap
|
||||||
|
metadata:
|
||||||
|
name: langfuse-common
|
||||||
|
namespace: langfuse
|
||||||
|
data:
|
||||||
|
NEXTAUTH_URL: http://localhost:3005
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
TELEMETRY_ENABLED: 'false'
|
||||||
|
LANGFUSE_ENABLE_EXPERIMENTAL_FEATURES: 'true'
|
||||||
|
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse.langfuse.svc.cluster.local:9000
|
||||||
|
CLICKHOUSE_URL: http://clickhouse.langfuse.svc.cluster.local:8123
|
||||||
|
CLICKHOUSE_USER: clickhouse
|
||||||
|
CLICKHOUSE_CLUSTER_ENABLED: 'false'
|
||||||
|
REDIS_HOST: redis.langfuse.svc.cluster.local
|
||||||
|
REDIS_PORT: '6379'
|
||||||
|
REDIS_TLS_ENABLED: 'false'
|
||||||
|
LANGFUSE_USE_AZURE_BLOB: 'false'
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio.langfuse.svc.cluster.local:9000
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: 'true'
|
||||||
|
LANGFUSE_S3_EVENT_UPLOAD_PREFIX: events/
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio.langfuse.svc.cluster.local:9000
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: 'true'
|
||||||
|
LANGFUSE_S3_MEDIA_UPLOAD_PREFIX: media/
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_ENABLED: 'false'
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_BUCKET: langfuse
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_PREFIX: exports/
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_REGION: auto
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_ENDPOINT: http://minio.langfuse.svc.cluster.local:9000
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_EXTERNAL_ENDPOINT: http://localhost:9090
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_ACCESS_KEY_ID: minio
|
||||||
|
LANGFUSE_S3_BATCH_EXPORT_FORCE_PATH_STYLE: 'true'
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: mongo
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: mongo
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: mongo
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: mongo
|
||||||
|
image: docker.io/library/mongo:8.0
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: MONGO_INITDB_ROOT_USERNAME
|
||||||
|
value: mongo
|
||||||
|
- name: MONGO_INITDB_ROOT_PASSWORD
|
||||||
|
value: mongopassword
|
||||||
|
- name: MONGO_INITDB_DATABASE
|
||||||
|
value: agent_memory
|
||||||
|
ports:
|
||||||
|
- name: mongo
|
||||||
|
containerPort: 27017
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data/db
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- 'mongosh --quiet --host 127.0.0.1 --port 27017 -u mongo -p mongopassword
|
||||||
|
--authenticationDatabase admin --eval ''db.adminCommand({ ping: 1 }).ok''
|
||||||
|
| grep 1'
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 60
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- 'mongosh --quiet --host 127.0.0.1 --port 27017 -u mongo -p mongopassword
|
||||||
|
--authenticationDatabase admin --eval ''db.adminCommand({ ping: 1 }).ok''
|
||||||
|
| grep 1'
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 1Gi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: mongo-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: mongo
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: mongo
|
||||||
|
ports:
|
||||||
|
- name: mongo
|
||||||
|
port: 27017
|
||||||
|
targetPort: 27017
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: postgres
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: postgres
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: postgres
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: postgres
|
||||||
|
image: docker.io/library/postgres:17
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: POSTGRES_USER
|
||||||
|
value: postgres
|
||||||
|
- name: POSTGRES_DB
|
||||||
|
value: postgres
|
||||||
|
- name: TZ
|
||||||
|
value: UTC
|
||||||
|
- name: PGTZ
|
||||||
|
value: UTC
|
||||||
|
- name: PGDATA
|
||||||
|
value: /var/lib/postgresql/data/pgdata
|
||||||
|
- name: POSTGRES_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
key: LANGFUSE_POSTGRES_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
containerPort: 5432
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/postgresql/data
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- pg_isready -U postgres
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 40
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- pg_isready -U postgres
|
||||||
|
periodSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 1Gi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: postgres-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: postgres
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: postgres
|
||||||
|
ports:
|
||||||
|
- name: postgres
|
||||||
|
port: 5432
|
||||||
|
targetPort: 5432
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: redis
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: redis
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: redis
|
||||||
|
image: docker.io/library/redis:7
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- exec redis-server --requirepass "$LANGFUSE_REDIS_PASSWORD" --maxmemory-policy
|
||||||
|
noeviction
|
||||||
|
env:
|
||||||
|
- name: LANGFUSE_REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
key: LANGFUSE_REDIS_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
containerPort: 6379
|
||||||
|
startupProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- redis-cli -a "$LANGFUSE_REDIS_PASSWORD" ping | grep PONG
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 30
|
||||||
|
readinessProbe:
|
||||||
|
exec:
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- redis-cli -a "$LANGFUSE_REDIS_PASSWORD" ping | grep PONG
|
||||||
|
periodSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: redis
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: redis
|
||||||
|
ports:
|
||||||
|
- name: redis
|
||||||
|
port: 6379
|
||||||
|
targetPort: 6379
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: clickhouse
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: clickhouse
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: clickhouse
|
||||||
|
spec:
|
||||||
|
securityContext:
|
||||||
|
fsGroup: 101
|
||||||
|
fsGroupChangePolicy: OnRootMismatch
|
||||||
|
containers:
|
||||||
|
- name: clickhouse
|
||||||
|
image: docker.io/clickhouse/clickhouse-server:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
securityContext:
|
||||||
|
runAsUser: 101
|
||||||
|
runAsGroup: 101
|
||||||
|
env:
|
||||||
|
- name: CLICKHOUSE_DB
|
||||||
|
value: default
|
||||||
|
- name: CLICKHOUSE_USER
|
||||||
|
value: clickhouse
|
||||||
|
- name: CLICKHOUSE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
key: LANGFUSE_CLICKHOUSE_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8123
|
||||||
|
- name: native
|
||||||
|
containerPort: 9000
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /var/lib/clickhouse
|
||||||
|
- name: logs
|
||||||
|
mountPath: /var/log/clickhouse-server
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ping
|
||||||
|
port: 8123
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 40
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /ping
|
||||||
|
port: 8123
|
||||||
|
periodSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 512Mi
|
||||||
|
limits:
|
||||||
|
cpu: '2'
|
||||||
|
memory: 2Gi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: clickhouse-data
|
||||||
|
- name: logs
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: clickhouse-logs
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: clickhouse
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: clickhouse
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 8123
|
||||||
|
targetPort: 8123
|
||||||
|
- name: native
|
||||||
|
port: 9000
|
||||||
|
targetPort: 9000
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: minio
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: minio
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: minio
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: minio
|
||||||
|
image: docker.io/minio/minio:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- mkdir -p /data/langfuse && exec minio server --address ":9000" --console-address
|
||||||
|
":9001" /data
|
||||||
|
env:
|
||||||
|
- name: MINIO_ROOT_USER
|
||||||
|
value: minio
|
||||||
|
- name: MINIO_ROOT_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
key: LANGFUSE_MINIO_PASSWORD
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
containerPort: 9000
|
||||||
|
- name: console
|
||||||
|
containerPort: 9001
|
||||||
|
volumeMounts:
|
||||||
|
- name: data
|
||||||
|
mountPath: /data
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /minio/health/live
|
||||||
|
port: 9000
|
||||||
|
periodSeconds: 3
|
||||||
|
failureThreshold: 40
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /minio/health/ready
|
||||||
|
port: 9000
|
||||||
|
periodSeconds: 5
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: '1'
|
||||||
|
memory: 1Gi
|
||||||
|
volumes:
|
||||||
|
- name: data
|
||||||
|
persistentVolumeClaim:
|
||||||
|
claimName: minio-data
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: minio
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: minio
|
||||||
|
ports:
|
||||||
|
- name: api
|
||||||
|
port: 9000
|
||||||
|
targetPort: 9000
|
||||||
|
- name: console
|
||||||
|
port: 9001
|
||||||
|
targetPort: 9001
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langfuse-worker
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langfuse-worker
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langfuse-worker
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: wait-dependencies
|
||||||
|
image: docker.io/library/busybox:1.36
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- until nc -z postgres.langfuse.svc.cluster.local 5432 && nc -z redis.langfuse.svc.cluster.local
|
||||||
|
6379 && nc -z clickhouse.langfuse.svc.cluster.local 8123 && nc -z minio.langfuse.svc.cluster.local
|
||||||
|
9000; do echo 'waiting for Langfuse dependencies via FQDN'; sleep 3; done
|
||||||
|
containers:
|
||||||
|
- name: worker
|
||||||
|
image: docker.io/langfuse/langfuse-worker:3
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: langfuse-common
|
||||||
|
- secretRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 2Gi
|
||||||
|
limits:
|
||||||
|
cpu: '2'
|
||||||
|
memory: 4Gi
|
||||||
|
env:
|
||||||
|
- name: NODE_OPTIONS
|
||||||
|
value: --max-old-space-size=3072
|
||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: langfuse-web
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: langfuse-web
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: langfuse-web
|
||||||
|
spec:
|
||||||
|
initContainers:
|
||||||
|
- name: wait-dependencies
|
||||||
|
image: docker.io/library/busybox:1.36
|
||||||
|
command:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
args:
|
||||||
|
- until nc -z postgres.langfuse.svc.cluster.local 5432 && nc -z redis.langfuse.svc.cluster.local
|
||||||
|
6379 && nc -z clickhouse.langfuse.svc.cluster.local 8123 && nc -z minio.langfuse.svc.cluster.local
|
||||||
|
9000; do echo 'waiting for Langfuse dependencies via FQDN'; sleep 3; done
|
||||||
|
containers:
|
||||||
|
- name: web
|
||||||
|
image: docker.io/langfuse/langfuse:3
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 3000
|
||||||
|
envFrom:
|
||||||
|
- configMapRef:
|
||||||
|
name: langfuse-common
|
||||||
|
- secretRef:
|
||||||
|
name: langfuse-runtime
|
||||||
|
startupProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/public/health
|
||||||
|
port: 3000
|
||||||
|
initialDelaySeconds: 10
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 60
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /api/public/health
|
||||||
|
port: 3000
|
||||||
|
periodSeconds: 10
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 2Gi
|
||||||
|
limits:
|
||||||
|
cpu: '2'
|
||||||
|
memory: 4Gi
|
||||||
|
env:
|
||||||
|
- name: HOSTNAME
|
||||||
|
value: 0.0.0.0
|
||||||
|
- name: NODE_OPTIONS
|
||||||
|
value: --max-old-space-size=3072
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langfuse-web
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: langfuse-web
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3000
|
||||||
|
targetPort: 3000
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: langfuse-ui
|
||||||
|
namespace: langfuse
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: langfuse-web
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: 3005
|
||||||
|
targetPort: 3000
|
||||||
75
deploy/oke/load-test/loadgen/loadtest.js
Normal file
75
deploy/oke/load-test/loadgen/loadtest.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import http from 'k6/http';
|
||||||
|
import { check, sleep } from 'k6';
|
||||||
|
import exec from 'k6/execution';
|
||||||
|
import { Trend, Counter } from 'k6/metrics';
|
||||||
|
|
||||||
|
const target = __ENV.LOADTEST_TARGET || 'http://agent-template-backend.agent-load-test.svc.cluster.local:8000';
|
||||||
|
const agentId = __ENV.LOADTEST_AGENT_ID || 'telecom_contas';
|
||||||
|
const tenantId = __ENV.LOADTEST_TENANT_ID || 'loadtest';
|
||||||
|
const scenario = __ENV.LOADTEST_SCENARIO || 'unique_sessions';
|
||||||
|
const message = __ENV.LOADTEST_MESSAGE || 'Explique em uma frase o que é uma fatura de telecom.';
|
||||||
|
const rps = Number(__ENV.LOADTEST_RPS || 100);
|
||||||
|
const duration = __ENV.LOADTEST_DURATION || '10m';
|
||||||
|
const maxVUs = Number(__ENV.LOADTEST_MAX_VUS || 1000);
|
||||||
|
|
||||||
|
const backendLatency = new Trend('agent_backend_latency', true);
|
||||||
|
const errors = new Counter('agent_backend_errors');
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
discardResponseBodies: false,
|
||||||
|
scenarios: {
|
||||||
|
requests: {
|
||||||
|
executor: 'constant-arrival-rate',
|
||||||
|
rate: rps,
|
||||||
|
timeUnit: '1s',
|
||||||
|
duration,
|
||||||
|
preAllocatedVUs: Math.min(maxVUs, Math.max(50, rps * 2)),
|
||||||
|
maxVUs,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thresholds: {
|
||||||
|
http_req_failed: ['rate<0.05'],
|
||||||
|
checks: ['rate>0.95'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function sessionId() {
|
||||||
|
if (scenario === 'shared_sessions') {
|
||||||
|
return `shared-${exec.vu.idInTest % 100}`;
|
||||||
|
}
|
||||||
|
return `load-${exec.scenario.iterationInTest}-${exec.vu.idInTest}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function () {
|
||||||
|
const sid = sessionId();
|
||||||
|
const mid = `msg-${exec.scenario.iterationInTest}-${Date.now()}`;
|
||||||
|
const payload = JSON.stringify({
|
||||||
|
channel: 'web',
|
||||||
|
agent_id: agentId,
|
||||||
|
tenant_id: tenantId,
|
||||||
|
payload: {
|
||||||
|
text: message,
|
||||||
|
session_id: sid,
|
||||||
|
user_id: `user-${exec.vu.idInTest}`,
|
||||||
|
customer_id: `cust-${exec.vu.idInTest}`,
|
||||||
|
message_id: mid,
|
||||||
|
metadata: { load_test: true, scenario },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const started = Date.now();
|
||||||
|
const res = http.post(`${target}/gateway/message`, payload, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-Request-ID': mid,
|
||||||
|
'X-Load-Test': 'true',
|
||||||
|
},
|
||||||
|
timeout: __ENV.LOADTEST_HTTP_TIMEOUT || '180s',
|
||||||
|
});
|
||||||
|
backendLatency.add(Date.now() - started);
|
||||||
|
const ok = check(res, {
|
||||||
|
'status is 200': (r) => r.status === 200,
|
||||||
|
'response has body': (r) => !!r.body && r.body.length > 0,
|
||||||
|
});
|
||||||
|
if (!ok) errors.add(1);
|
||||||
|
sleep(Number(__ENV.LOADTEST_THINK_TIME || 0));
|
||||||
|
}
|
||||||
0
deploy/oke/load-test/results/.gitkeep
Normal file
0
deploy/oke/load-test/results/.gitkeep
Normal file
11
deploy/oke/load-test/scripts/00_prepare.sh
Normal file
11
deploy/oke/load-test/scripts/00_prepare.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
"$DIR/prepare_env.sh"
|
||||||
|
echo
|
||||||
|
cat <<'TXT'
|
||||||
|
Next:
|
||||||
|
1) edit deploy/oke/load-test/.env.runtime
|
||||||
|
2) copy the real Autonomous wallet into templates/agent_template_backend/wallet/
|
||||||
|
3) run configure_kubeconfig.sh
|
||||||
|
TXT
|
||||||
9
deploy/oke/load-test/scripts/01_deploy_all.sh
Normal file
9
deploy/oke/load-test/scripts/01_deploy_all.sh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
"$DIR/deploy_langfuse.sh"
|
||||||
|
"$DIR/build_push_backend.sh"
|
||||||
|
"$DIR/deploy_backend.sh"
|
||||||
|
"$DIR/configure_existing_lb.sh"
|
||||||
|
"$DIR/validate_dependencies.sh"
|
||||||
|
"$DIR/smoke_test.sh" external
|
||||||
Binary file not shown.
10
deploy/oke/load-test/scripts/build_push_backend.sh
Normal file
10
deploy/oke/load-test/scripts/build_push_backend.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
IMAGE="${OCI_REGION_KEY}.ocir.io/${OCI_TENANCY_NAMESPACE}/${OCIR_REPOSITORY_PREFIX}/agent-template-backend:${IMAGE_TAG}"
|
||||||
|
echo "Building $IMAGE"
|
||||||
|
docker build -f "$ROOT/deploy/oke/dockerfiles/Dockerfile.agent-template-backend" -t "$IMAGE" "$ROOT"
|
||||||
|
docker push "$IMAGE"
|
||||||
|
echo "$IMAGE" > "$ROOT/deploy/oke/load-test/.backend-image"
|
||||||
12
deploy/oke/load-test/scripts/check_load_balancing.sh
Normal file
12
deploy/oke/load-test/scripts/check_load_balancing.sh
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
set -a; source "$ROOT/deploy/oke/load-test/.env.runtime"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
IP="$(kubectl -n "$NS" get svc agent-template-backend-lb -o jsonpath='{.status.loadBalancer.ingress[0].ip}')"
|
||||||
|
[[ -n "$IP" ]] || { echo "LoadBalancer IP not assigned"; exit 3; }
|
||||||
|
URL="http://${IP}:8000/health"
|
||||||
|
echo "Sampling 30 requests from $URL"
|
||||||
|
for i in $(seq 1 30); do
|
||||||
|
curl -fsS -D - -o /dev/null "$URL" 2>/dev/null | awk -F': ' 'tolower($1)=="x-agent-pod" {gsub("\r", "", $2); print $2}'
|
||||||
|
done | sort | uniq -c | sort -nr
|
||||||
19
deploy/oke/load-test/scripts/collect_results.sh
Normal file
19
deploy/oke/load-test/scripts/collect_results.sh
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
set -a; source "$LT/.env.runtime"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
STAMP="$(date +%Y%m%d-%H%M%S)"
|
||||||
|
OUT="$LT/results/$STAMP"
|
||||||
|
mkdir -p "$OUT"
|
||||||
|
kubectl -n "$NS" get pods -o wide > "$OUT/pods.txt"
|
||||||
|
kubectl -n "$NS" get hpa -o yaml > "$OUT/hpa.yaml"
|
||||||
|
kubectl -n "$NS" get deploy agent-template-backend -o yaml > "$OUT/deployment.yaml"
|
||||||
|
kubectl -n "$NS" get svc -o wide > "$OUT/services.txt"
|
||||||
|
kubectl -n "$NS" top pods > "$OUT/top-pods.txt" 2>&1 || true
|
||||||
|
kubectl top nodes > "$OUT/top-nodes.txt" 2>&1 || true
|
||||||
|
kubectl -n "$NS" logs job/agent-load-generator > "$OUT/k6.log" 2>&1 || true
|
||||||
|
kubectl -n "$NS" logs -l app=agent-template-backend --prefix --tail=5000 > "$OUT/backend.log" 2>&1 || true
|
||||||
|
kubectl -n "$NS" get events --sort-by='.lastTimestamp' > "$OUT/events.txt" 2>&1 || true
|
||||||
|
echo "$OUT"
|
||||||
122
deploy/oke/load-test/scripts/configure_existing_lb.sh
Normal file
122
deploy/oke/load-test/scripts/configure_existing_lb.sh
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
ENV="$LT/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] && source "$ENV"
|
||||||
|
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
SERVICE_NAME="${BACKEND_SERVICE_NAME:-agent-template-backend-lb}"
|
||||||
|
LB_OCID="${EXISTING_LB_OCID:?EXISTING_LB_OCID must be set}"
|
||||||
|
BACKEND_SET="${BACKEND_SET_NAME:-agent-framework-loadtest}"
|
||||||
|
LISTENER_NAME="${BACKEND_LISTENER_NAME:-agent-framework-loadtest}"
|
||||||
|
LISTENER_PORT="${BACKEND_LISTENER_PORT:-8000}"
|
||||||
|
LISTENER_PROTOCOL="${BACKEND_LISTENER_PROTOCOL:-HTTP}"
|
||||||
|
BACKEND_POLICY="${BACKEND_POLICY:-ROUND_ROBIN}"
|
||||||
|
HEALTH_PATH="${BACKEND_HEALTH_PATH:-/health}"
|
||||||
|
OCI_PROFILE="${OCI_CLI_PROFILE:-}"
|
||||||
|
|
||||||
|
log(){ printf '[existing-lb] %s\n' "$*"; }
|
||||||
|
die(){ printf '[existing-lb] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
OCI_ARGS=()
|
||||||
|
[[ -n "$OCI_PROFILE" ]] && OCI_ARGS+=(--profile "$OCI_PROFILE")
|
||||||
|
oci_lb(){ oci lb "$@" "${OCI_ARGS[@]}"; }
|
||||||
|
|
||||||
|
command -v oci >/dev/null || die "oci CLI not found"
|
||||||
|
command -v kubectl >/dev/null || die "kubectl not found"
|
||||||
|
|
||||||
|
log "Validating Load Balancer..."
|
||||||
|
oci_lb load-balancer get --load-balancer-id "$LB_OCID" >/dev/null
|
||||||
|
|
||||||
|
NODE_PORT="$(kubectl -n "$NS" get svc "$SERVICE_NAME" -o jsonpath='{.spec.ports[0].nodePort}')"
|
||||||
|
[[ -n "$NODE_PORT" && "$NODE_PORT" != "0" ]] || die "Service/$SERVICE_NAME has no NodePort"
|
||||||
|
log "NodePort=$NODE_PORT"
|
||||||
|
|
||||||
|
if oci_lb backend-set get --load-balancer-id "$LB_OCID" --backend-set-name "$BACKEND_SET" >/dev/null 2>&1; then
|
||||||
|
log "Backend set '$BACKEND_SET' already exists"
|
||||||
|
else
|
||||||
|
log "Creating backend set '$BACKEND_SET'..."
|
||||||
|
oci_lb backend-set create \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--name "$BACKEND_SET" \
|
||||||
|
--policy "$BACKEND_POLICY" \
|
||||||
|
--health-checker-protocol HTTP \
|
||||||
|
--health-checker-port "$NODE_PORT" \
|
||||||
|
--health-checker-url-path "$HEALTH_PATH" \
|
||||||
|
--health-checker-return-code 200 \
|
||||||
|
--health-checker-retries 3 \
|
||||||
|
--health-checker-timeout-in-ms 3000 \
|
||||||
|
--health-checker-interval-in-ms 10000 \
|
||||||
|
--wait-for-state SUCCEEDED \
|
||||||
|
--max-wait-seconds 1200 >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
for i in $(seq 1 20); do
|
||||||
|
oci_lb backend-set get --load-balancer-id "$LB_OCID" --backend-set-name "$BACKEND_SET" >/dev/null 2>&1 && break
|
||||||
|
[[ "$i" == "20" ]] && die "backend set still unavailable"
|
||||||
|
sleep 3
|
||||||
|
done
|
||||||
|
|
||||||
|
mapfile -t NODE_IPS < <(
|
||||||
|
kubectl get nodes -o jsonpath='{range .items[*]}{.status.addresses[?(@.type=="InternalIP")].address}{"\n"}{end}' |
|
||||||
|
sed '/^$/d' | sort -u
|
||||||
|
)
|
||||||
|
|
||||||
|
EXISTING="$(oci_lb backend list \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--backend-set-name "$BACKEND_SET" \
|
||||||
|
--query 'data[].name' --raw-output 2>/dev/null || true)"
|
||||||
|
|
||||||
|
for ip in "${NODE_IPS[@]}"; do
|
||||||
|
name="${ip}:${NODE_PORT}"
|
||||||
|
if grep -Fxq "$name" <<<"$EXISTING"; then
|
||||||
|
log "Backend $name already exists"
|
||||||
|
else
|
||||||
|
log "Creating backend $name..."
|
||||||
|
oci_lb backend create \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--backend-set-name "$BACKEND_SET" \
|
||||||
|
--ip-address "$ip" \
|
||||||
|
--port "$NODE_PORT" \
|
||||||
|
--wait-for-state SUCCEEDED \
|
||||||
|
--max-wait-seconds 1200 >/dev/null
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
listener_found="$(
|
||||||
|
oci_lb load-balancer get \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--query "data.listeners.\"${LISTENER_NAME}\".name" \
|
||||||
|
--raw-output 2>/dev/null || true
|
||||||
|
)"
|
||||||
|
|
||||||
|
if [[ "$listener_found" == "$LISTENER_NAME" ]]; then
|
||||||
|
log "Listener '$LISTENER_NAME' already exists"
|
||||||
|
else
|
||||||
|
log "Creating listener '$LISTENER_NAME'..."
|
||||||
|
oci_lb listener create \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--name "$LISTENER_NAME" \
|
||||||
|
--default-backend-set-name "$BACKEND_SET" \
|
||||||
|
--port "$LISTENER_PORT" \
|
||||||
|
--protocol "$LISTENER_PROTOCOL" \
|
||||||
|
--wait-for-state SUCCEEDED \
|
||||||
|
--max-wait-seconds 1200 >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Backends:"
|
||||||
|
oci_lb backend list \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--backend-set-name "$BACKEND_SET" \
|
||||||
|
--query 'data[].{name:name,ip:"ip-address",port:port}' \
|
||||||
|
--output table
|
||||||
|
|
||||||
|
log "Backend set health:"
|
||||||
|
oci_lb backend-set-health get \
|
||||||
|
--load-balancer-id "$LB_OCID" \
|
||||||
|
--backend-set-name "$BACKEND_SET" \
|
||||||
|
--output table || true
|
||||||
|
|
||||||
|
log "Done"
|
||||||
15
deploy/oke/load-test/scripts/configure_kubeconfig.sh
Normal file
15
deploy/oke/load-test/scripts/configure_kubeconfig.sh
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || { echo "Run prepare_env.sh first"; exit 1; }
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
oci ce cluster create-kubeconfig \
|
||||||
|
--cluster-id "$OKE_CLUSTER_OCID" \
|
||||||
|
--file "$HOME/.kube/config" \
|
||||||
|
--region "$OCI_REGION" \
|
||||||
|
--token-version 2.0.0 \
|
||||||
|
--kube-endpoint PUBLIC_ENDPOINT \
|
||||||
|
--profile "${OCI_CLI_PROFILE:-DEFAULT}"
|
||||||
|
kubectl cluster-info
|
||||||
|
kubectl get nodes -o wide
|
||||||
10
deploy/oke/load-test/scripts/create_runtime_secret.sh
Normal file
10
deploy/oke/load-test/scripts/create_runtime_secret.sh
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || { echo "Run prepare_env.sh first"; exit 1; }
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n "$NS" create secret generic agent-backend-runtime --from-env-file="$ENV" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
echo "Runtime env loaded into secret agent-backend-runtime."
|
||||||
18
deploy/oke/load-test/scripts/create_wallet_secret.sh
Normal file
18
deploy/oke/load-test/scripts/create_wallet_secret.sh
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
WALLET="$ROOT/templates/agent_template_backend/wallet"
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
mapfile -t wallet_files < <(find "$WALLET" -maxdepth 1 -type f ! -name 'README_PLACE_WALLET_HERE.txt' ! -name '.gitkeep')
|
||||||
|
if (( ${#wallet_files[@]} == 0 )); then
|
||||||
|
echo "No real wallet files found in $WALLET"
|
||||||
|
echo "Copy the Autonomous wallet files there, then rerun."
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
args=()
|
||||||
|
for f in "${wallet_files[@]}"; do args+=("--from-file=$(basename "$f")=$f"); done
|
||||||
|
kubectl -n "$NS" create secret generic agent-backend-wallet "${args[@]}" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
echo "Wallet secret agent-backend-wallet created/updated in namespace $NS"
|
||||||
91
deploy/oke/load-test/scripts/deploy_backend.sh
Normal file
91
deploy/oke/load-test/scripts/deploy_backend.sh
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
ENV="$LT/.env.runtime"
|
||||||
|
|
||||||
|
[[ -f "$ENV" ]] || { echo "ERROR: runtime env not found: $ENV" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Keep variables local to this shell; do not export everything to kubectl/OCI CLI.
|
||||||
|
source "$ENV"
|
||||||
|
|
||||||
|
log() { printf '[deploy-backend] %s\n' "$*"; }
|
||||||
|
die() { printf '[deploy-backend] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v kubectl >/dev/null 2>&1 || die "kubectl not found in PATH"
|
||||||
|
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
OCIR_SECRET_NAME="${OCIR_PULL_SECRET_NAME:-ocir-secret}"
|
||||||
|
|
||||||
|
IMAGE="$(cat "$LT/.backend-image" 2>/dev/null || true)"
|
||||||
|
[[ -n "$IMAGE" ]] || IMAGE="${OCI_REGION_KEY}.ocir.io/${OCI_TENANCY_NAMESPACE}/${OCIR_REPOSITORY_PREFIX}/agent-template-backend:${IMAGE_TAG}"
|
||||||
|
|
||||||
|
log "Namespace: $NS"
|
||||||
|
log "Backend image: $IMAGE"
|
||||||
|
|
||||||
|
log "Checking Kubernetes API authentication..."
|
||||||
|
kubectl get --raw='/readyz' >/dev/null 2>&1 || die "Kubernetes API authentication failed"
|
||||||
|
kubectl get --raw='/openapi/v2' >/dev/null 2>&1 || die "Kubernetes OpenAPI access failed"
|
||||||
|
|
||||||
|
log "Ensuring namespace exists..."
|
||||||
|
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
|
||||||
|
: "${OCI_REGION_KEY:?OCI_REGION_KEY must be set in $ENV}"
|
||||||
|
: "${OCIR_USERNAME:?OCIR_USERNAME must be set in $ENV}"
|
||||||
|
: "${OCIR_AUTH_TOKEN:?OCIR_AUTH_TOKEN must be set in $ENV}"
|
||||||
|
|
||||||
|
OCIR_EMAIL="${OCIR_EMAIL:-unused@example.invalid}"
|
||||||
|
OCIR_SERVER="${OCI_REGION_KEY}.ocir.io"
|
||||||
|
|
||||||
|
log "Creating/updating OCIR pull secret '$OCIR_SECRET_NAME'..."
|
||||||
|
kubectl create secret docker-registry "$OCIR_SECRET_NAME" -n "$NS" --docker-server="$OCIR_SERVER" --docker-username="$OCIR_USERNAME" --docker-password="$OCIR_AUTH_TOKEN" --docker-email="$OCIR_EMAIL" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
|
||||||
|
log "Associating OCIR pull secret with ServiceAccount/default..."
|
||||||
|
kubectl patch serviceaccount default -n "$NS" --type=merge -p "{\"imagePullSecrets\":[{\"name\":\"${OCIR_SECRET_NAME}\"}]}" >/dev/null
|
||||||
|
|
||||||
|
pull_secret="$(kubectl get serviceaccount default -n "$NS" -o jsonpath='{.imagePullSecrets[*].name}')"
|
||||||
|
[[ " $pull_secret " == *" $OCIR_SECRET_NAME "* ]] || die "ServiceAccount/default does not reference $OCIR_SECRET_NAME"
|
||||||
|
|
||||||
|
log "Creating/updating backend runtime secret..."
|
||||||
|
"$LT/scripts/create_runtime_secret.sh"
|
||||||
|
|
||||||
|
if [[ "${ENABLE_LANGFUSE:-true}" == "true" ]]; then
|
||||||
|
log "Synchronizing Langfuse client credentials..."
|
||||||
|
"$LT/scripts/sync_langfuse_client_secret.sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Creating/updating backend wallet secret..."
|
||||||
|
"$LT/scripts/create_wallet_secret.sh"
|
||||||
|
|
||||||
|
RUNTIME_CHECKSUM="$(cat "$LT/.env.runtime" "$LT/.env.langfuse" 2>/dev/null | sha256sum | awk '{print $1}')"
|
||||||
|
NODE_PORT="${BACKEND_NODE_PORT:-32116}"
|
||||||
|
|
||||||
|
log "Applying backend Kubernetes manifest..."
|
||||||
|
sed -e "s#namespace: agent-load-test#namespace: $NS#g" -e "s#name: agent-load-test#name: $NS#g" -e "s#IMAGE_PLACEHOLDER#$IMAGE#g" -e "s#replicas: 4#replicas: ${LOADTEST_MIN_REPLICAS:-4}#" -e "s#minReplicas: 4#minReplicas: ${LOADTEST_MIN_REPLICAS:-4}#" -e "s#maxReplicas: 30#maxReplicas: ${LOADTEST_MAX_REPLICAS:-30}#" -e "s#cpu: \"500m\"#cpu: \"${LOADTEST_CPU_REQUEST:-500m}\"#" -e "s#cpu: \"2000m\"#cpu: \"${LOADTEST_CPU_LIMIT:-2000m}\"#" -e "s#memory: \"768Mi\"#memory: \"${LOADTEST_MEMORY_REQUEST:-768Mi}\"#" -e "s#memory: \"2Gi\"#memory: \"${LOADTEST_MEMORY_LIMIT:-2Gi}\"#" -e "s#RUNTIME_CHECKSUM_PLACEHOLDER#$RUNTIME_CHECKSUM#g" -e "s#NODE_PORT_PLACEHOLDER#$NODE_PORT#g" "$LT/k8s/agent-backend.yaml" | kubectl apply -f -
|
||||||
|
|
||||||
|
log "Waiting for backend rollout..."
|
||||||
|
if ! kubectl -n "$NS" rollout status deployment/agent-template-backend --timeout=10m; then
|
||||||
|
echo "================ BACKEND ROLLOUT FAILED ================" >&2
|
||||||
|
kubectl -n "$NS" get pods -o wide >&2 || true
|
||||||
|
bad_pod="$(kubectl -n "$NS" get pods -l app=agent-template-backend --sort-by=.metadata.creationTimestamp -o jsonpath='{.items[-1].metadata.name}' 2>/dev/null || true)"
|
||||||
|
if [[ -n "$bad_pod" ]]; then
|
||||||
|
kubectl -n "$NS" describe pod "$bad_pod" >&2 || true
|
||||||
|
kubectl -n "$NS" logs "$bad_pod" -c agent-template-backend --tail=200 >&2 || true
|
||||||
|
fi
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Final backend pods:"
|
||||||
|
kubectl -n "$NS" get pods -o wide
|
||||||
|
|
||||||
|
log "Backend NodePort service (fronted by existing OCI LB):"
|
||||||
|
kubectl -n "$NS" get svc agent-template-backend-lb
|
||||||
|
|
||||||
|
log "Backend HPA:"
|
||||||
|
kubectl -n "$NS" get hpa
|
||||||
|
|
||||||
|
log "Default ServiceAccount imagePullSecrets:"
|
||||||
|
kubectl -n "$NS" get serviceaccount default -o jsonpath='{.imagePullSecrets[*].name}{"\n"}'
|
||||||
|
|
||||||
|
log "Backend deployment completed successfully."
|
||||||
193
deploy/oke/load-test/scripts/deploy_langfuse.sh
Normal file
193
deploy/oke/load-test/scripts/deploy_langfuse.sh
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy_langfuse_v11.sh
|
||||||
|
# OKE-safe Langfuse deployment with DNS preflight.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
MANIFEST="$LT/langfuse/langfuse-k8s.yaml"
|
||||||
|
LF_ENV="$LT/.env.langfuse"
|
||||||
|
|
||||||
|
NS="${LANGFUSE_NAMESPACE:-langfuse}"
|
||||||
|
SECRET="langfuse-runtime"
|
||||||
|
DNS_TEST_NS="${DNS_TEST_NAMESPACE:-default}"
|
||||||
|
|
||||||
|
PG_FQDN="postgres.${NS}.svc.cluster.local"
|
||||||
|
REDIS_FQDN="redis.${NS}.svc.cluster.local"
|
||||||
|
CH_FQDN="clickhouse.${NS}.svc.cluster.local"
|
||||||
|
MINIO_FQDN="minio.${NS}.svc.cluster.local"
|
||||||
|
MONGO_FQDN="mongo.${NS}.svc.cluster.local"
|
||||||
|
|
||||||
|
log() { printf '[langfuse-deploy] %s\n' "$*"; }
|
||||||
|
die() { printf '[langfuse-deploy] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
cleanup_probe() {
|
||||||
|
local ns="${1:-$DNS_TEST_NS}"
|
||||||
|
local name="${2:-}"
|
||||||
|
[[ -n "$name" ]] && kubectl -n "$ns" delete pod "$name" --ignore-not-found --wait=false >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
trap 'rc=$?; if (( rc != 0 )); then
|
||||||
|
echo >&2
|
||||||
|
echo "[langfuse-deploy] Failure detected." >&2
|
||||||
|
kubectl get nodes -o wide 2>/dev/null >&2 || true
|
||||||
|
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide 2>/dev/null >&2 || true
|
||||||
|
kubectl -n "$NS" get pods,svc,pvc 2>/dev/null >&2 || true
|
||||||
|
fi
|
||||||
|
exit $rc' EXIT
|
||||||
|
|
||||||
|
command -v kubectl >/dev/null 2>&1 || die "kubectl not found in PATH"
|
||||||
|
[[ -f "$MANIFEST" ]] || die "manifest not found: $MANIFEST"
|
||||||
|
|
||||||
|
log "Checking Kubernetes API..."
|
||||||
|
kubectl version --request-timeout=15s >/dev/null || die "cannot reach Kubernetes API"
|
||||||
|
|
||||||
|
log "Checking worker nodes..."
|
||||||
|
not_ready="$(kubectl get nodes --no-headers 2>/dev/null | awk '$2 != "Ready" {print $1 ":" $2}')"
|
||||||
|
[[ -z "$not_ready" ]] || { printf '%s\n' "$not_ready" >&2; die "one or more OKE nodes are not Ready"; }
|
||||||
|
|
||||||
|
log "Checking CoreDNS..."
|
||||||
|
kubectl -n kube-system get svc kube-dns >/dev/null 2>&1 || die "kube-dns Service not found"
|
||||||
|
dns_ready="$(kubectl -n kube-system get pods -l k8s-app=kube-dns --no-headers 2>/dev/null | awk '$2 ~ /^[0-9]+\/[0-9]+$/ {split($2,a,"/"); if(a[1]==a[2]) n++} END{print n+0}')"
|
||||||
|
(( dns_ready > 0 )) || die "no Ready CoreDNS pod found"
|
||||||
|
|
||||||
|
DNS_IP="$(kubectl -n kube-system get svc kube-dns -o jsonpath='{.spec.clusterIP}')"
|
||||||
|
[[ -n "$DNS_IP" ]] || die "kube-dns ClusterIP is empty"
|
||||||
|
log "kube-dns ClusterIP: $DNS_IP"
|
||||||
|
|
||||||
|
probe="oke-dns-preflight-$(date +%s)"
|
||||||
|
log "Running DNS preflight BEFORE Langfuse deployment..."
|
||||||
|
kubectl -n "$DNS_TEST_NS" run "$probe" --restart=Never --image=docker.io/library/busybox:1.36 --command -- sh -c "echo '--- /etc/resolv.conf ---';
|
||||||
|
cat /etc/resolv.conf;
|
||||||
|
echo '--- default resolver ---';
|
||||||
|
nslookup kubernetes.default.svc.cluster.local;
|
||||||
|
echo '--- direct kube-dns ---';
|
||||||
|
nslookup kubernetes.default.svc.cluster.local ${DNS_IP}" >/dev/null
|
||||||
|
|
||||||
|
if ! kubectl -n "$DNS_TEST_NS" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$probe" --timeout=90s >/dev/null 2>&1; then
|
||||||
|
echo >&2
|
||||||
|
echo "================ DNS PREFLIGHT FAILED ================" >&2
|
||||||
|
kubectl -n "$DNS_TEST_NS" logs "$probe" >&2 || true
|
||||||
|
kubectl -n "$DNS_TEST_NS" describe pod "$probe" >&2 || true
|
||||||
|
cleanup_probe "$DNS_TEST_NS" "$probe"
|
||||||
|
die "cluster DNS is unavailable; fix worker/CNI networking before deploying Langfuse"
|
||||||
|
fi
|
||||||
|
kubectl -n "$DNS_TEST_NS" logs "$probe" || true
|
||||||
|
cleanup_probe "$DNS_TEST_NS" "$probe"
|
||||||
|
log "Cluster DNS preflight PASSED."
|
||||||
|
|
||||||
|
log "Generating Langfuse runtime environment..."
|
||||||
|
"$LT/scripts/prepare_env.sh"
|
||||||
|
[[ -s "$LF_ENV" ]] || die "Langfuse env was not generated: $LF_ENV"
|
||||||
|
|
||||||
|
required_keys=(
|
||||||
|
LANGFUSE_PUBLIC_KEY LANGFUSE_SECRET_KEY
|
||||||
|
LANGFUSE_INIT_PROJECT_PUBLIC_KEY LANGFUSE_INIT_PROJECT_SECRET_KEY
|
||||||
|
LANGFUSE_INIT_ORG_ID LANGFUSE_INIT_ORG_NAME
|
||||||
|
LANGFUSE_INIT_PROJECT_ID LANGFUSE_INIT_PROJECT_NAME
|
||||||
|
LANGFUSE_INIT_USER_EMAIL LANGFUSE_INIT_USER_NAME
|
||||||
|
LANGFUSE_INIT_USER_PASSWORD LANGFUSE_NEXTAUTH_SECRET
|
||||||
|
LANGFUSE_SALT LANGFUSE_ENCRYPTION_KEY
|
||||||
|
LANGFUSE_POSTGRES_PASSWORD LANGFUSE_REDIS_PASSWORD
|
||||||
|
LANGFUSE_CLICKHOUSE_PASSWORD LANGFUSE_MINIO_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in "${required_keys[@]}"; do
|
||||||
|
grep -qE "^${key}=.+" "$LF_ENV" || die "required key '$key' missing/empty in $LF_ENV"
|
||||||
|
done
|
||||||
|
|
||||||
|
kubectl create namespace "$NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
|
||||||
|
pg_password="$(grep '^LANGFUSE_POSTGRES_PASSWORD=' "$LF_ENV" | cut -d= -f2-)"
|
||||||
|
[[ -n "$pg_password" ]] || die "LANGFUSE_POSTGRES_PASSWORD is empty"
|
||||||
|
db_url="postgresql://postgres:${pg_password}@${PG_FQDN}:5432/postgres"
|
||||||
|
|
||||||
|
tmp_env="$(mktemp)"
|
||||||
|
grep -vE '^(DATABASE_URL|DIRECT_URL)=' "$LF_ENV" > "$tmp_env"
|
||||||
|
printf 'DATABASE_URL=%s\n' "$db_url" >> "$tmp_env"
|
||||||
|
printf 'DIRECT_URL=%s\n' "$db_url" >> "$tmp_env"
|
||||||
|
|
||||||
|
log "Creating/updating Secret/$SECRET..."
|
||||||
|
kubectl -n "$NS" create secret generic "$SECRET" --from-env-file="$tmp_env" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
rm -f "$tmp_env"
|
||||||
|
|
||||||
|
log "Applying Langfuse manifest..."
|
||||||
|
kubectl apply -f "$MANIFEST"
|
||||||
|
|
||||||
|
for dep in mongo postgres redis clickhouse minio langfuse-worker langfuse-web; do
|
||||||
|
kubectl -n "$NS" get deployment "$dep" >/dev/null 2>&1 || die "deployment/$dep is missing"
|
||||||
|
done
|
||||||
|
|
||||||
|
pgdata="$(kubectl -n "$NS" get deployment postgres -o jsonpath='{.spec.template.spec.containers[?(@.name=="postgres")].env[?(@.name=="PGDATA")].value}')"
|
||||||
|
[[ "$pgdata" == "/var/lib/postgresql/data/pgdata" ]] || die "Postgres PGDATA is incorrect: '$pgdata'"
|
||||||
|
|
||||||
|
log "Scaling web/worker to zero until infrastructure is healthy..."
|
||||||
|
kubectl -n "$NS" scale deployment/langfuse-web --replicas=0 >/dev/null
|
||||||
|
kubectl -n "$NS" scale deployment/langfuse-worker --replicas=0 >/dev/null
|
||||||
|
|
||||||
|
for dep in mongo postgres redis clickhouse minio; do
|
||||||
|
log "Waiting for deployment/$dep ..."
|
||||||
|
kubectl -n "$NS" rollout status "deployment/$dep" --timeout=10m
|
||||||
|
done
|
||||||
|
|
||||||
|
log "Checking service endpoints..."
|
||||||
|
for svc in mongo postgres redis clickhouse minio; do
|
||||||
|
ip="$(kubectl -n "$NS" get endpoints "$svc" -o jsonpath='{.subsets[0].addresses[0].ip}' 2>/dev/null || true)"
|
||||||
|
[[ -n "$ip" ]] || die "service/$svc has no ready endpoint"
|
||||||
|
log "$svc endpoint: $ip"
|
||||||
|
done
|
||||||
|
|
||||||
|
probe="langfuse-infra-probe-$(date +%s)"
|
||||||
|
log "Running Langfuse infrastructure DNS/TCP probe..."
|
||||||
|
kubectl -n "$NS" run "$probe" --restart=Never --image=docker.io/library/busybox:1.36 --command -- sh -c "set -e;
|
||||||
|
nslookup ${PG_FQDN};
|
||||||
|
nslookup ${REDIS_FQDN};
|
||||||
|
nslookup ${CH_FQDN};
|
||||||
|
nslookup ${MINIO_FQDN};
|
||||||
|
nslookup ${MONGO_FQDN};
|
||||||
|
nc -zvw5 ${PG_FQDN} 5432;
|
||||||
|
nc -zvw5 ${REDIS_FQDN} 6379;
|
||||||
|
nc -zvw5 ${CH_FQDN} 8123;
|
||||||
|
nc -zvw5 ${MINIO_FQDN} 9000;
|
||||||
|
nc -zvw5 ${MONGO_FQDN} 27017;
|
||||||
|
echo 'ALL LANGFUSE INFRA CHECKS PASSED'" >/dev/null
|
||||||
|
|
||||||
|
if ! kubectl -n "$NS" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$probe" --timeout=120s >/dev/null 2>&1; then
|
||||||
|
kubectl -n "$NS" logs "$probe" >&2 || true
|
||||||
|
kubectl -n "$NS" describe pod "$probe" >&2 || true
|
||||||
|
cleanup_probe "$NS" "$probe"
|
||||||
|
die "Langfuse infrastructure probe failed; web/worker remain scaled to zero"
|
||||||
|
fi
|
||||||
|
kubectl -n "$NS" logs "$probe" || true
|
||||||
|
cleanup_probe "$NS" "$probe"
|
||||||
|
|
||||||
|
log "Infrastructure healthy. Applying Langfuse web/worker memory sizing..."
|
||||||
|
kubectl -n "$NS" set resources deployment/langfuse-worker --requests=cpu=500m,memory=2Gi --limits=cpu=2,memory=4Gi >/dev/null
|
||||||
|
kubectl -n "$NS" set resources deployment/langfuse-web --requests=cpu=500m,memory=2Gi --limits=cpu=2,memory=4Gi >/dev/null
|
||||||
|
kubectl -n "$NS" set env deployment/langfuse-worker NODE_OPTIONS=--max-old-space-size=3072 >/dev/null
|
||||||
|
kubectl -n "$NS" set env deployment/langfuse-web NODE_OPTIONS=--max-old-space-size=3072 HOSTNAME=0.0.0.0 >/dev/null
|
||||||
|
|
||||||
|
log "Scaling worker/web to 1..."
|
||||||
|
kubectl -n "$NS" scale deployment/langfuse-worker --replicas=1 >/dev/null
|
||||||
|
kubectl -n "$NS" scale deployment/langfuse-web --replicas=1 >/dev/null
|
||||||
|
|
||||||
|
kubectl -n "$NS" rollout status deployment/langfuse-worker --timeout=15m
|
||||||
|
kubectl -n "$NS" rollout status deployment/langfuse-web --timeout=15m
|
||||||
|
|
||||||
|
log "Verifying Secret/$SECRET before synchronization..."
|
||||||
|
kubectl -n "$NS" get secret "$SECRET" >/dev/null || die "Secret/$SECRET became unavailable in namespace $NS"
|
||||||
|
|
||||||
|
APP_NS="$(sed -n -E 's/^K8S_NAMESPACE=(.*)$/\1/p' "$LT/.env.runtime" | tail -n 1 | tr -d '\r' | sed -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
|
||||||
|
APP_NS="${APP_NS:-agent-load-test}"
|
||||||
|
|
||||||
|
log "Synchronizing Langfuse project keys to backend namespace $APP_NS..."
|
||||||
|
LANGFUSE_NAMESPACE="$NS" K8S_NAMESPACE="$APP_NS" \
|
||||||
|
"$LT/scripts/sync_langfuse_client_secret.sh"
|
||||||
|
|
||||||
|
log "Validating Langfuse health and authenticated Public API..."
|
||||||
|
"$LT/scripts/validate_langfuse_integration.sh"
|
||||||
|
|
||||||
|
log "Final state:"
|
||||||
|
kubectl -n "$NS" get pods,svc,pvc -o wide
|
||||||
|
printf '\nUI: kubectl -n %s port-forward --address 127.0.0.1 svc/langfuse-web 3005:3000\n' "$NS"
|
||||||
|
log "Langfuse deployment completed successfully."
|
||||||
39
deploy/oke/load-test/scripts/fix_oke_worker_network_v1.sh
Normal file
39
deploy/oke/load-test/scripts/fix_oke_worker_network_v1.sh
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# fix_oke_worker_network_v1.sh
|
||||||
|
# Run ON EACH OKE WORKER NODE as root or with sudo.
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
log() { printf '[oke-worker-network] %s\n' "$*"; }
|
||||||
|
|
||||||
|
if [[ "$(id -u)" -ne 0 ]]; then
|
||||||
|
echo "ERROR: run this script with sudo/root." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Loading br_netfilter..."
|
||||||
|
modprobe br_netfilter
|
||||||
|
|
||||||
|
log "Persisting br_netfilter..."
|
||||||
|
cat >/etc/modules-load.d/br_netfilter.conf <<'EOF'
|
||||||
|
br_netfilter
|
||||||
|
EOF
|
||||||
|
|
||||||
|
log "Applying Kubernetes networking sysctls..."
|
||||||
|
cat >/etc/sysctl.d/99-kubernetes-network.conf <<'EOF'
|
||||||
|
net.bridge.bridge-nf-call-iptables=1
|
||||||
|
net.bridge.bridge-nf-call-ip6tables=1
|
||||||
|
net.ipv4.ip_forward=1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
sysctl --system >/dev/null
|
||||||
|
|
||||||
|
log "Validating kernel/module settings..."
|
||||||
|
lsmod | grep -q '^br_netfilter' || { echo "ERROR: br_netfilter is not loaded." >&2; exit 1; }
|
||||||
|
[[ "$(sysctl -n net.bridge.bridge-nf-call-iptables)" == "1" ]] || { echo "ERROR: bridge-nf-call-iptables != 1" >&2; exit 1; }
|
||||||
|
[[ "$(sysctl -n net.ipv4.ip_forward)" == "1" ]] || { echo "ERROR: ip_forward != 1" >&2; exit 1; }
|
||||||
|
|
||||||
|
log "Restarting CRI-O and kubelet..."
|
||||||
|
systemctl restart crio
|
||||||
|
systemctl restart kubelet
|
||||||
|
|
||||||
|
log "Worker network correction completed."
|
||||||
151
deploy/oke/load-test/scripts/prepare_env.py
Normal file
151
deploy/oke/load-test/scripts/prepare_env.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
from pathlib import Path
|
||||||
|
import secrets
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
|
||||||
|
def parse_env(path: Path) -> dict[str, str]:
|
||||||
|
out: dict[str, str] = {}
|
||||||
|
if not path.exists():
|
||||||
|
return out
|
||||||
|
for raw in path.read_text(encoding="utf-8").splitlines():
|
||||||
|
line = raw.strip()
|
||||||
|
if not line or line.startswith("#") or "=" not in line:
|
||||||
|
continue
|
||||||
|
# Support the common `export KEY=value` form too.
|
||||||
|
if line.startswith("export "):
|
||||||
|
line = line[7:].lstrip()
|
||||||
|
k, v = line.split("=", 1)
|
||||||
|
k = k.strip()
|
||||||
|
if not k:
|
||||||
|
continue
|
||||||
|
out[k] = v.strip()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def write_env(path: Path, values: dict[str, str], *, shell_safe: bool = False):
|
||||||
|
lines = ["# Generated by prepare_env.py. Do not commit."]
|
||||||
|
for k in sorted(values):
|
||||||
|
value = values[k]
|
||||||
|
if shell_safe:
|
||||||
|
value = shlex.quote(value)
|
||||||
|
lines.append(f"{k}={value}")
|
||||||
|
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def merge_missing(target: dict[str, str], defaults: dict[str, str]) -> None:
|
||||||
|
"""Add only keys that are missing from target; never replace backend values."""
|
||||||
|
for k, v in defaults.items():
|
||||||
|
target.setdefault(k, v)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--base", required=True,
|
||||||
|
help="Main application env. Existing values are preserved.")
|
||||||
|
p.add_argument("--overlay", required=True,
|
||||||
|
help="Load-test defaults. Only fills keys absent from --base.")
|
||||||
|
p.add_argument("--override", required=False,
|
||||||
|
help="Optional explicit load-test overrides. These replace base/default values.")
|
||||||
|
p.add_argument("--output", required=True)
|
||||||
|
p.add_argument("--langfuse-env", required=True)
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
base_path = Path(args.base)
|
||||||
|
defaults_path = Path(args.overlay)
|
||||||
|
override_path = Path(args.override) if args.override else None
|
||||||
|
|
||||||
|
base = parse_env(base_path)
|
||||||
|
defaults = parse_env(defaults_path)
|
||||||
|
explicit = parse_env(override_path) if override_path and override_path.exists() else {}
|
||||||
|
|
||||||
|
# Backend .env is authoritative. The example file provides only missing load-test keys.
|
||||||
|
merged = dict(base)
|
||||||
|
merge_missing(merged, defaults)
|
||||||
|
|
||||||
|
# .env.loadtest, when present, is the deliberate operator override layer.
|
||||||
|
merged.update(explicit)
|
||||||
|
|
||||||
|
# Container/OKE invariants. These are location/routing details that cannot use
|
||||||
|
# workstation-local values from templates/agent_template_backend/.env.
|
||||||
|
merged["APP_ENV"] = "oke-load-test"
|
||||||
|
merged["ADB_WALLET_LOCATION"] = "/app/wallet"
|
||||||
|
merged["ENABLE_LANGFUSE"] = "true"
|
||||||
|
merged["LANGFUSE_HOST"] = "http://langfuse-web.langfuse.svc.cluster.local:3000"
|
||||||
|
|
||||||
|
# OKE authentication should normally use workload identity. Allow an explicit
|
||||||
|
# .env.loadtest override if the operator intentionally selected another mode.
|
||||||
|
if "OCI_AUTH_MODE" not in explicit:
|
||||||
|
merged["OCI_AUTH_MODE"] = "oke_workload_identity"
|
||||||
|
|
||||||
|
# Correct only the historical typo in Mongo-compatible ports; preserve host,
|
||||||
|
# credentials, query params and database values from the backend .env.
|
||||||
|
for key in ("MONGODB_URI", "PUBSUB_SEQUENCE_MONGODB_URI"):
|
||||||
|
if key in merged:
|
||||||
|
merged[key] = merged[key].replace(":37017", ":27017")
|
||||||
|
|
||||||
|
# Langfuse bootstrap secrets are stable across reruns.
|
||||||
|
lf_path = Path(args.langfuse_env)
|
||||||
|
lf = parse_env(lf_path)
|
||||||
|
public_key = lf.get("LANGFUSE_PUBLIC_KEY") or "pk-lf-" + secrets.token_hex(16)
|
||||||
|
secret_key = lf.get("LANGFUSE_SECRET_KEY") or "sk-lf-" + secrets.token_hex(32)
|
||||||
|
admin_password = lf.get("LANGFUSE_INIT_USER_PASSWORD") or secrets.token_urlsafe(24)
|
||||||
|
|
||||||
|
lf.update({
|
||||||
|
"LANGFUSE_INIT_ORG_ID": lf.get("LANGFUSE_INIT_ORG_ID", "agent-framework-org"),
|
||||||
|
"LANGFUSE_INIT_ORG_NAME": lf.get("LANGFUSE_INIT_ORG_NAME", "Agent Framework OCI"),
|
||||||
|
"LANGFUSE_INIT_PROJECT_ID": lf.get("LANGFUSE_INIT_PROJECT_ID", "agent-framework-loadtest"),
|
||||||
|
"LANGFUSE_INIT_PROJECT_NAME": lf.get("LANGFUSE_INIT_PROJECT_NAME", "Agent Framework Load Test"),
|
||||||
|
"LANGFUSE_PUBLIC_KEY": public_key,
|
||||||
|
"LANGFUSE_SECRET_KEY": secret_key,
|
||||||
|
"LANGFUSE_INIT_USER_EMAIL": lf.get("LANGFUSE_INIT_USER_EMAIL", "admin@loadtest.local"),
|
||||||
|
"LANGFUSE_INIT_USER_NAME": lf.get("LANGFUSE_INIT_USER_NAME", "Load Test Admin"),
|
||||||
|
"LANGFUSE_INIT_USER_PASSWORD": admin_password,
|
||||||
|
"LANGFUSE_NEXTAUTH_SECRET": lf.get("LANGFUSE_NEXTAUTH_SECRET") or secrets.token_hex(32),
|
||||||
|
"LANGFUSE_SALT": lf.get("LANGFUSE_SALT") or secrets.token_hex(16),
|
||||||
|
"LANGFUSE_ENCRYPTION_KEY": lf.get("LANGFUSE_ENCRYPTION_KEY") or secrets.token_hex(32),
|
||||||
|
"LANGFUSE_POSTGRES_PASSWORD": lf.get("LANGFUSE_POSTGRES_PASSWORD") or secrets.token_urlsafe(24),
|
||||||
|
"LANGFUSE_REDIS_PASSWORD": lf.get("LANGFUSE_REDIS_PASSWORD") or secrets.token_urlsafe(24),
|
||||||
|
"LANGFUSE_CLICKHOUSE_PASSWORD": lf.get("LANGFUSE_CLICKHOUSE_PASSWORD") or secrets.token_urlsafe(24),
|
||||||
|
"LANGFUSE_MINIO_PASSWORD": lf.get("LANGFUSE_MINIO_PASSWORD") or secrets.token_urlsafe(24),
|
||||||
|
})
|
||||||
|
lf.update({
|
||||||
|
"DATABASE_URL": f"postgresql://postgres:{lf['LANGFUSE_POSTGRES_PASSWORD']}@postgres.langfuse.svc.cluster.local:5432/postgres",
|
||||||
|
"DIRECT_URL": f"postgresql://postgres:{lf['LANGFUSE_POSTGRES_PASSWORD']}@postgres.langfuse.svc.cluster.local:5432/postgres",
|
||||||
|
"POSTGRES_PASSWORD": lf["LANGFUSE_POSTGRES_PASSWORD"],
|
||||||
|
"SALT": lf["LANGFUSE_SALT"],
|
||||||
|
"ENCRYPTION_KEY": lf["LANGFUSE_ENCRYPTION_KEY"],
|
||||||
|
"NEXTAUTH_SECRET": lf["LANGFUSE_NEXTAUTH_SECRET"],
|
||||||
|
"CLICKHOUSE_PASSWORD": lf["LANGFUSE_CLICKHOUSE_PASSWORD"],
|
||||||
|
"REDIS_AUTH": lf["LANGFUSE_REDIS_PASSWORD"],
|
||||||
|
"LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY": lf["LANGFUSE_MINIO_PASSWORD"],
|
||||||
|
"LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY": lf["LANGFUSE_MINIO_PASSWORD"],
|
||||||
|
"LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY": lf["LANGFUSE_MINIO_PASSWORD"],
|
||||||
|
"LANGFUSE_INIT_PROJECT_PUBLIC_KEY": public_key,
|
||||||
|
"LANGFUSE_INIT_PROJECT_SECRET_KEY": secret_key,
|
||||||
|
})
|
||||||
|
write_env(lf_path, lf, shell_safe=False)
|
||||||
|
|
||||||
|
merged["LANGFUSE_PUBLIC_KEY"] = public_key
|
||||||
|
merged["LANGFUSE_SECRET_KEY"] = secret_key
|
||||||
|
write_env(Path(args.output), merged, shell_safe=True)
|
||||||
|
|
||||||
|
preserved = sum(1 for k, v in base.items() if merged.get(k) == v)
|
||||||
|
changed = sorted(k for k, v in base.items() if merged.get(k) != v)
|
||||||
|
print(f"Base env: {base_path} ({len(base)} variables)")
|
||||||
|
print(f"Defaults: {defaults_path} ({len(defaults)} variables)")
|
||||||
|
if override_path:
|
||||||
|
print(f"Explicit override: {override_path} ({len(explicit)} variables)")
|
||||||
|
print(f"Runtime env written: {args.output} ({len(merged)} variables)")
|
||||||
|
print(f"Backend values preserved unchanged: {preserved}/{len(base)}")
|
||||||
|
if changed:
|
||||||
|
print("Backend values intentionally changed for OKE/container compatibility:")
|
||||||
|
for k in changed:
|
||||||
|
print(f" - {k}")
|
||||||
|
print(f"Langfuse env written: {args.langfuse_env}")
|
||||||
|
print("LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY synchronized automatically.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
31
deploy/oke/load-test/scripts/prepare_env.sh
Normal file
31
deploy/oke/load-test/scripts/prepare_env.sh
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
BASE="$ROOT/templates/agent_template_backend/.env"
|
||||||
|
DEFAULTS="$LT/.env.loadtest.example"
|
||||||
|
OVERRIDE="$LT/.env.loadtest"
|
||||||
|
|
||||||
|
[[ -f "$BASE" ]] || { echo "ERROR: backend env not found: $BASE" >&2; exit 1; }
|
||||||
|
[[ -f "$DEFAULTS" ]] || { echo "ERROR: load-test defaults not found: $DEFAULTS" >&2; exit 1; }
|
||||||
|
|
||||||
|
args=(
|
||||||
|
--base "$BASE"
|
||||||
|
--overlay "$DEFAULTS"
|
||||||
|
--output "$LT/.env.runtime"
|
||||||
|
--langfuse-env "$LT/.env.langfuse"
|
||||||
|
)
|
||||||
|
|
||||||
|
if [[ -f "$OVERRIDE" ]]; then
|
||||||
|
args+=(--override "$OVERRIDE")
|
||||||
|
echo "Using explicit load-test overrides: $OVERRIDE"
|
||||||
|
else
|
||||||
|
echo "No $OVERRIDE found; backend .env values will be preserved and example defaults only fill missing keys."
|
||||||
|
fi
|
||||||
|
|
||||||
|
python3 "$LT/scripts/prepare_env.py" "${args[@]}"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Runtime environment prepared: $LT/.env.runtime"
|
||||||
|
echo "Source of application configuration: $BASE"
|
||||||
|
echo "Optional explicit overrides: $OVERRIDE"
|
||||||
37
deploy/oke/load-test/scripts/run_architecture_suite.sh
Normal file
37
deploy/oke/load-test/scripts/run_architecture_suite.sh
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
ENV="$LT/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || "$LT/scripts/prepare_env.sh"
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
|
||||||
|
run_case() {
|
||||||
|
local name="$1" rps="$2" dur="$3" scenario="$4"
|
||||||
|
echo "=== $name: ${rps} rps / ${dur} / ${scenario} ==="
|
||||||
|
python3 - "$ENV" "$rps" "$dur" "$scenario" <<'PY'
|
||||||
|
import sys
|
||||||
|
p,rps,dur,scenario=sys.argv[1:]
|
||||||
|
kv={}
|
||||||
|
order=[]
|
||||||
|
for line in open(p):
|
||||||
|
if '=' in line and not line.startswith('#'):
|
||||||
|
k,v=line.rstrip('\n').split('=',1); kv[k]=v; order.append(k)
|
||||||
|
kv['LOADTEST_RPS']=rps; kv['LOADTEST_DURATION']=dur; kv['LOADTEST_SCENARIO']=scenario
|
||||||
|
for k in ('LOADTEST_RPS','LOADTEST_DURATION','LOADTEST_SCENARIO'):
|
||||||
|
if k not in order: order.append(k)
|
||||||
|
with open(p,'w') as f:
|
||||||
|
f.write('# Generated/updated load-test runtime env. Do not commit.\n')
|
||||||
|
for k in order: f.write(f'{k}={kv[k]}\n')
|
||||||
|
PY
|
||||||
|
"$LT/scripts/run_load_test.sh" internal
|
||||||
|
kubectl -n "${K8S_NAMESPACE:-agent-load-test}" logs -f job/agent-load-generator || true
|
||||||
|
"$LT/scripts/collect_results.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
run_case warmup 5 2m unique_sessions
|
||||||
|
run_case baseline 25 5m unique_sessions
|
||||||
|
run_case scale 100 10m unique_sessions
|
||||||
|
run_case shared-state 50 10m shared_sessions
|
||||||
|
|
||||||
|
echo "Suite completed. Review deploy/oke/load-test/results and Langfuse."
|
||||||
111
deploy/oke/load-test/scripts/run_load_test.sh
Normal file
111
deploy/oke/load-test/scripts/run_load_test.sh
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
ENV="$LT/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || { echo "ERROR: $ENV not found" >&2; exit 1; }
|
||||||
|
source "$ENV"
|
||||||
|
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
TARGET_MODE="${1:-${LOADTEST_TARGET_MODE:-internal}}"
|
||||||
|
|
||||||
|
resolve_external_url() {
|
||||||
|
if [[ -n "${LOADTEST_EXTERNAL_URL:-}" ]]; then
|
||||||
|
printf '%s\n' "${LOADTEST_EXTERNAL_URL%/}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${EXISTING_LB_OCID:?Set EXISTING_LB_OCID or LOADTEST_EXTERNAL_URL in .env.runtime}"
|
||||||
|
|
||||||
|
command -v oci >/dev/null 2>&1 || {
|
||||||
|
echo "ERROR: oci CLI not found and LOADTEST_EXTERNAL_URL is not set" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local oci_args=()
|
||||||
|
[[ -n "${OCI_CLI_PROFILE:-}" ]] && oci_args+=(--profile "$OCI_CLI_PROFILE")
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip="$(
|
||||||
|
oci lb load-balancer get \
|
||||||
|
--load-balancer-id "$EXISTING_LB_OCID" \
|
||||||
|
"${oci_args[@]}" \
|
||||||
|
--query 'data."ip-addresses"[0]."ip-address"' \
|
||||||
|
--raw-output
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ -n "$ip" && "$ip" != "null" ]] || {
|
||||||
|
echo "ERROR: could not resolve IP from existing LB $EXISTING_LB_OCID" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local proto="${BACKEND_LISTENER_PROTOCOL:-HTTP}"
|
||||||
|
local scheme
|
||||||
|
scheme="$(printf '%s' "$proto" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
[[ "$scheme" == "http" || "$scheme" == "https" ]] || scheme="http"
|
||||||
|
|
||||||
|
local port="${BACKEND_LISTENER_PORT:-8000}"
|
||||||
|
printf '%s://%s:%s\n' "$scheme" "$ip" "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$TARGET_MODE" in
|
||||||
|
external)
|
||||||
|
LOADTEST_TARGET="$(resolve_external_url)"
|
||||||
|
;;
|
||||||
|
internal)
|
||||||
|
LOADTEST_TARGET="http://agent-template-backend.${NS}.svc.cluster.local:8000"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: $0 [internal|external]" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo "Load-test mode: $TARGET_MODE"
|
||||||
|
echo "Load-test target: $LOADTEST_TARGET"
|
||||||
|
|
||||||
|
TMP_ENV="$(mktemp)"
|
||||||
|
trap 'rm -f "$TMP_ENV"' EXIT
|
||||||
|
cp "$ENV" "$TMP_ENV"
|
||||||
|
|
||||||
|
python3 - "$TMP_ENV" "$LOADTEST_TARGET" "$TARGET_MODE" <<'PY'
|
||||||
|
import sys
|
||||||
|
p,target,mode=sys.argv[1:]
|
||||||
|
lines=[]
|
||||||
|
seen_target=False
|
||||||
|
seen_mode=False
|
||||||
|
for line in open(p):
|
||||||
|
if line.startswith("LOADTEST_TARGET="):
|
||||||
|
lines.append(f"LOADTEST_TARGET={target}\n")
|
||||||
|
seen_target=True
|
||||||
|
elif line.startswith("LOADTEST_TARGET_MODE="):
|
||||||
|
lines.append(f"LOADTEST_TARGET_MODE={mode}\n")
|
||||||
|
seen_mode=True
|
||||||
|
else:
|
||||||
|
lines.append(line)
|
||||||
|
if not seen_target:
|
||||||
|
lines.append(f"LOADTEST_TARGET={target}\n")
|
||||||
|
if not seen_mode:
|
||||||
|
lines.append(f"LOADTEST_TARGET_MODE={mode}\n")
|
||||||
|
open(p,"w").writelines(lines)
|
||||||
|
PY
|
||||||
|
|
||||||
|
kubectl -n "$NS" create secret generic agent-backend-runtime \
|
||||||
|
--from-env-file="$TMP_ENV" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
kubectl -n "$NS" create configmap k6-load-script \
|
||||||
|
--from-file=loadtest.js="$LT/loadgen/loadtest.js" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
|
kubectl -n "$NS" delete job agent-load-generator \
|
||||||
|
--ignore-not-found --wait=true
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s#namespace: agent-load-test#namespace: $NS#g" \
|
||||||
|
"$LT/k8s/k6-job.yaml" | kubectl apply -f -
|
||||||
|
|
||||||
|
echo "Load test started against $LOADTEST_TARGET"
|
||||||
|
echo "Follow: kubectl -n $NS logs -f job/agent-load-generator"
|
||||||
|
echo "Watch: watch kubectl -n $NS get pods,hpa"
|
||||||
27
deploy/oke/load-test/scripts/set_llm_mode.sh
Normal file
27
deploy/oke/load-test/scripts/set_llm_mode.sh
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
MODE="${1:-}"
|
||||||
|
case "$MODE" in
|
||||||
|
mock) VALUE=mock ;;
|
||||||
|
real|oci) VALUE=oci_sdk ;;
|
||||||
|
*) echo "Usage: $0 mock|real"; exit 2 ;;
|
||||||
|
esac
|
||||||
|
python3 - "$ENV" "$VALUE" <<'PY'
|
||||||
|
import sys
|
||||||
|
p,v=sys.argv[1:]
|
||||||
|
lines=open(p).read().splitlines(); out=[]; found=False
|
||||||
|
for line in lines:
|
||||||
|
if line.startswith('LLM_PROVIDER='):
|
||||||
|
out.append('LLM_PROVIDER='+v); found=True
|
||||||
|
else: out.append(line)
|
||||||
|
if not found: out.append('LLM_PROVIDER='+v)
|
||||||
|
open(p,'w').write('\n'.join(out)+'\n')
|
||||||
|
PY
|
||||||
|
set -a; source "$ENV"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
kubectl -n "$NS" create secret generic agent-backend-runtime --from-env-file="$ENV" --dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
kubectl -n "$NS" rollout restart deployment/agent-template-backend
|
||||||
|
kubectl -n "$NS" rollout status deployment/agent-template-backend --timeout=10m
|
||||||
|
echo "LLM_PROVIDER=$VALUE"
|
||||||
131
deploy/oke/load-test/scripts/smoke_test.sh
Normal file
131
deploy/oke/load-test/scripts/smoke_test.sh
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
ENV="$ROOT/deploy/oke/load-test/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || { echo "ERROR: $ENV not found" >&2; exit 1; }
|
||||||
|
source "$ENV"
|
||||||
|
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
MODE="${1:-external}"
|
||||||
|
|
||||||
|
resolve_external_url() {
|
||||||
|
if [[ -n "${LOADTEST_EXTERNAL_URL:-}" ]]; then
|
||||||
|
printf '%s\n' "${LOADTEST_EXTERNAL_URL%/}"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
: "${EXISTING_LB_OCID:?Set EXISTING_LB_OCID or LOADTEST_EXTERNAL_URL in .env.runtime}"
|
||||||
|
|
||||||
|
command -v oci >/dev/null 2>&1 || {
|
||||||
|
echo "ERROR: oci CLI not found and LOADTEST_EXTERNAL_URL is not set" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local oci_args=()
|
||||||
|
[[ -n "${OCI_CLI_PROFILE:-}" ]] && oci_args+=(--profile "$OCI_CLI_PROFILE")
|
||||||
|
|
||||||
|
local ip
|
||||||
|
ip="$(
|
||||||
|
oci lb load-balancer get \
|
||||||
|
--load-balancer-id "$EXISTING_LB_OCID" \
|
||||||
|
"${oci_args[@]}" \
|
||||||
|
--query 'data."ip-addresses"[0]."ip-address"' \
|
||||||
|
--raw-output
|
||||||
|
)"
|
||||||
|
|
||||||
|
[[ -n "$ip" && "$ip" != "null" ]] || {
|
||||||
|
echo "ERROR: could not resolve IP from existing LB $EXISTING_LB_OCID" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local proto="${BACKEND_LISTENER_PROTOCOL:-HTTP}"
|
||||||
|
local scheme
|
||||||
|
scheme="$(printf '%s' "$proto" | tr '[:upper:]' '[:lower:]')"
|
||||||
|
[[ "$scheme" == "http" || "$scheme" == "https" ]] || scheme="http"
|
||||||
|
|
||||||
|
local port="${BACKEND_LISTENER_PORT:-8000}"
|
||||||
|
printf '%s://%s:%s\n' "$scheme" "$ip" "$port"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ "$MODE" == "external" ]]; then
|
||||||
|
URL="$(resolve_external_url)"
|
||||||
|
elif [[ "$MODE" == "internal" ]]; then
|
||||||
|
# Execute curl inside the cluster because *.svc.cluster.local is not resolvable
|
||||||
|
# from the developer workstation.
|
||||||
|
URL="http://agent-template-backend.${NS}.svc.cluster.local:8000"
|
||||||
|
else
|
||||||
|
echo "Usage: $0 [external|internal]" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
SID="smoke-$(date +%s)"
|
||||||
|
PAYLOAD="{\"channel\":\"web\",\"agent_id\":\"${LOADTEST_AGENT_ID:-telecom_contas}\",\"tenant_id\":\"loadtest\",\"payload\":{\"text\":\"Olá. Responda apenas OK.\",\"session_id\":\"$SID\",\"user_id\":\"smoke\",\"customer_id\":\"smoke\",\"message_id\":\"smoke-1\"}}"
|
||||||
|
|
||||||
|
|
||||||
|
verify_langfuse_trace() {
|
||||||
|
[[ "${ENABLE_LANGFUSE:-true}" == "true" ]] || return 0
|
||||||
|
local checkpod="langfuse-trace-check-$(date +%s)"
|
||||||
|
local needle="$SID"
|
||||||
|
echo "Verifying trace ingestion in Langfuse for session $needle ..."
|
||||||
|
kubectl -n "$NS" delete pod "$checkpod" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
|
kubectl -n "$NS" run "$checkpod" --restart=Never --image=docker.io/curlimages/curl:latest \
|
||||||
|
--overrides="$(cat <<JSON
|
||||||
|
{"spec":{"containers":[{"name":"$checkpod","image":"docker.io/curlimages/curl:latest","env":[
|
||||||
|
{"name":"LANGFUSE_HOST","valueFrom":{"secretKeyRef":{"name":"langfuse-client","key":"LANGFUSE_HOST"}}},
|
||||||
|
{"name":"LANGFUSE_PUBLIC_KEY","valueFrom":{"secretKeyRef":{"name":"langfuse-client","key":"LANGFUSE_PUBLIC_KEY"}}},
|
||||||
|
{"name":"LANGFUSE_SECRET_KEY","valueFrom":{"secretKeyRef":{"name":"langfuse-client","key":"LANGFUSE_SECRET_KEY"}}}
|
||||||
|
],"command":["sh","-c"],"args":["set -eu; i=0; while [ \$i -lt 12 ]; do body=\$(curl -fsS -u \"\$LANGFUSE_PUBLIC_KEY:\$LANGFUSE_SECRET_KEY\" \"\$LANGFUSE_HOST/api/public/traces?limit=100\" || true); echo \"\$body\" | grep -F '$needle' >/dev/null && { echo LANGFUSE_TRACE_OK; exit 0; }; i=\$((i+1)); sleep 5; done; echo LANGFUSE_TRACE_NOT_FOUND >&2; exit 1"]}]}}
|
||||||
|
JSON
|
||||||
|
)" >/dev/null
|
||||||
|
if ! kubectl -n "$NS" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$checkpod" --timeout=90s >/dev/null 2>&1; then
|
||||||
|
kubectl -n "$NS" logs "$checkpod" || true
|
||||||
|
kubectl -n "$NS" delete pod "$checkpod" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
kubectl -n "$NS" logs "$checkpod"
|
||||||
|
kubectl -n "$NS" delete pod "$checkpod" --ignore-not-found >/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Smoke target: $URL ($MODE)"
|
||||||
|
|
||||||
|
if [[ "$MODE" == "external" ]]; then
|
||||||
|
echo "Health:"
|
||||||
|
curl -fsS --connect-timeout 10 --max-time 30 "$URL/health"
|
||||||
|
echo
|
||||||
|
echo "Gateway message:"
|
||||||
|
curl -fsS --connect-timeout 10 --max-time 120 \
|
||||||
|
-X POST "$URL/gateway/message" \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H "X-Request-ID: smoke-$SID" \
|
||||||
|
-d "$PAYLOAD"
|
||||||
|
echo
|
||||||
|
else
|
||||||
|
pod="backend-smoke-$(date +%s)"
|
||||||
|
kubectl -n "$NS" run "$pod" \
|
||||||
|
--restart=Never \
|
||||||
|
--image=docker.io/curlimages/curl:latest \
|
||||||
|
--command -- sh -c \
|
||||||
|
"set -e;
|
||||||
|
echo 'Health:';
|
||||||
|
curl -fsS --connect-timeout 10 --max-time 30 '$URL/health';
|
||||||
|
echo;
|
||||||
|
echo 'Gateway message:';
|
||||||
|
curl -fsS --connect-timeout 10 --max-time 120 -X POST '$URL/gateway/message' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'X-Request-ID: smoke-$SID' \
|
||||||
|
-d '$PAYLOAD';
|
||||||
|
echo" >/dev/null
|
||||||
|
|
||||||
|
if ! kubectl -n "$NS" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$pod" --timeout=180s >/dev/null 2>&1; then
|
||||||
|
kubectl -n "$NS" logs "$pod" || true
|
||||||
|
kubectl -n "$NS" describe pod "$pod" || true
|
||||||
|
kubectl -n "$NS" delete pod "$pod" --ignore-not-found >/dev/null 2>&1 || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl -n "$NS" logs "$pod"
|
||||||
|
kubectl -n "$NS" delete pod "$pod" --ignore-not-found >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
verify_langfuse_trace
|
||||||
11
deploy/oke/load-test/scripts/start_langfuse_compose.sh
Normal file
11
deploy/oke/load-test/scripts/start_langfuse_compose.sh
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
[[ -f "$LT/.env.langfuse" ]] || "$LT/scripts/prepare_env.sh"
|
||||||
|
docker compose \
|
||||||
|
--env-file "$LT/.env.langfuse" \
|
||||||
|
-f "$ROOT/libs/agent_framework/Infrastructure_Langfuse/docker-compose.yml" \
|
||||||
|
up -d
|
||||||
|
echo "Local Langfuse: http://localhost:3005"
|
||||||
|
echo "Keys are synchronized in $LT/.env.langfuse and $LT/.env.runtime"
|
||||||
91
deploy/oke/load-test/scripts/sync_langfuse_client_secret.sh
Normal file
91
deploy/oke/load-test/scripts/sync_langfuse_client_secret.sh
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
RUNTIME_ENV="$LT/.env.runtime"
|
||||||
|
LF_ENV="$LT/.env.langfuse"
|
||||||
|
|
||||||
|
log(){ printf '[langfuse-sync] %s\n' "$*"; }
|
||||||
|
die(){ printf '[langfuse-sync] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# IMPORTANT: do not `source .env.runtime` here. It contains backend OCI settings
|
||||||
|
# (OCI_AUTH_MODE, OCI_PROFILE, OCI_CLI_PROFILE, etc.) that can change the OCI CLI
|
||||||
|
# exec-plugin environment used by kubectl/kubeconfig.
|
||||||
|
read_env_value() {
|
||||||
|
local file="$1" key="$2" default_value="${3:-}"
|
||||||
|
local value=""
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
value="$(sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | tail -n 1)"
|
||||||
|
value="${value%$'\r'}"
|
||||||
|
if [[ "$value" == \"*\" && "$value" == *\" ]]; then value="${value:1:${#value}-2}"; fi
|
||||||
|
if [[ "$value" == \'*\' && "$value" == *\' ]]; then value="${value:1:${#value}-2}"; fi
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${value:-$default_value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
LF_NS="${LANGFUSE_NAMESPACE:-$(read_env_value "$RUNTIME_ENV" LANGFUSE_NAMESPACE langfuse)}"
|
||||||
|
APP_NS="${K8S_NAMESPACE:-$(read_env_value "$RUNTIME_ENV" K8S_NAMESPACE agent-load-test)}"
|
||||||
|
LF_SECRET="${LANGFUSE_RUNTIME_SECRET:-langfuse-runtime}"
|
||||||
|
CLIENT_SECRET="${LANGFUSE_CLIENT_SECRET:-langfuse-client}"
|
||||||
|
LF_HOST="${LANGFUSE_HOST:-$(read_env_value "$RUNTIME_ENV" LANGFUSE_HOST "http://langfuse-web.${LF_NS}.svc.cluster.local:3000")}"
|
||||||
|
|
||||||
|
command -v kubectl >/dev/null 2>&1 || die "kubectl not found"
|
||||||
|
|
||||||
|
log "Kubernetes context: $(kubectl config current-context 2>/dev/null || echo '<unknown>')"
|
||||||
|
log "Langfuse namespace: $LF_NS"
|
||||||
|
log "Backend namespace: $APP_NS"
|
||||||
|
|
||||||
|
# Do not hide kubectl errors. A credentials/context problem is not a missing Secret.
|
||||||
|
set +e
|
||||||
|
secret_err="$(mktemp)"
|
||||||
|
kubectl -n "$LF_NS" get secret "$LF_SECRET" >/dev/null 2>"$secret_err"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
if (( rc != 0 )); then
|
||||||
|
msg="$(cat "$secret_err")"
|
||||||
|
rm -f "$secret_err"
|
||||||
|
echo "$msg" >&2
|
||||||
|
echo >&2
|
||||||
|
log "Secrets visible in namespace '$LF_NS':"
|
||||||
|
kubectl -n "$LF_NS" get secrets 2>&1 || true
|
||||||
|
echo >&2
|
||||||
|
log "Langfuse web Secret references:"
|
||||||
|
kubectl -n "$LF_NS" get deployment langfuse-web \
|
||||||
|
-o jsonpath='{.spec.template.spec.containers[0].envFrom[*].secretRef.name}{"\n"}' 2>&1 || true
|
||||||
|
die "cannot read Secret/$LF_SECRET in namespace $LF_NS (kubectl rc=$rc). See the kubectl error above; this may be authentication/context rather than a missing Secret."
|
||||||
|
fi
|
||||||
|
rm -f "$secret_err"
|
||||||
|
|
||||||
|
get_secret(){
|
||||||
|
local key="$1"
|
||||||
|
local encoded
|
||||||
|
encoded="$(kubectl -n "$LF_NS" get secret "$LF_SECRET" -o "jsonpath={.data.${key}}")"
|
||||||
|
[[ -n "$encoded" ]] && printf '%s' "$encoded" | base64 -d
|
||||||
|
}
|
||||||
|
|
||||||
|
PUBLIC_KEY="$(get_secret LANGFUSE_INIT_PROJECT_PUBLIC_KEY || true)"
|
||||||
|
SECRET_KEY="$(get_secret LANGFUSE_INIT_PROJECT_SECRET_KEY || true)"
|
||||||
|
[[ -n "$PUBLIC_KEY" ]] || PUBLIC_KEY="$(get_secret LANGFUSE_PUBLIC_KEY || true)"
|
||||||
|
[[ -n "$SECRET_KEY" ]] || SECRET_KEY="$(get_secret LANGFUSE_SECRET_KEY || true)"
|
||||||
|
|
||||||
|
# Last-resort local fallback: the same values generated by prepare_env.py.
|
||||||
|
if [[ -z "$PUBLIC_KEY" && -f "$LF_ENV" ]]; then
|
||||||
|
PUBLIC_KEY="$(read_env_value "$LF_ENV" LANGFUSE_INIT_PROJECT_PUBLIC_KEY '')"
|
||||||
|
fi
|
||||||
|
if [[ -z "$SECRET_KEY" && -f "$LF_ENV" ]]; then
|
||||||
|
SECRET_KEY="$(read_env_value "$LF_ENV" LANGFUSE_INIT_PROJECT_SECRET_KEY '')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[[ -n "$PUBLIC_KEY" ]] || die "Langfuse public key is empty"
|
||||||
|
[[ -n "$SECRET_KEY" ]] || die "Langfuse secret key is empty"
|
||||||
|
|
||||||
|
kubectl create namespace "$APP_NS" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
kubectl -n "$APP_NS" create secret generic "$CLIENT_SECRET" \
|
||||||
|
--from-literal=LANGFUSE_PUBLIC_KEY="$PUBLIC_KEY" \
|
||||||
|
--from-literal=LANGFUSE_SECRET_KEY="$SECRET_KEY" \
|
||||||
|
--from-literal=LANGFUSE_HOST="$LF_HOST" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
|
||||||
|
log "Secret/$CLIENT_SECRET synchronized into namespace $APP_NS."
|
||||||
|
log "Public key: ${PUBLIC_KEY:0:10}... (secret key not printed)"
|
||||||
103
deploy/oke/load-test/scripts/validate_dependencies.sh
Normal file
103
deploy/oke/load-test/scripts/validate_dependencies.sh
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
ENV="$LT/.env.runtime"
|
||||||
|
[[ -f "$ENV" ]] || { echo "Run prepare_env.sh first" >&2; exit 1; }
|
||||||
|
source "$ENV"
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
IMAGE="$(cat "$LT/.backend-image" 2>/dev/null || true)"
|
||||||
|
[[ -n "$IMAGE" ]] || IMAGE="${OCI_REGION_KEY}.ocir.io/${OCI_TENANCY_NAMESPACE}/${OCIR_REPOSITORY_PREFIX}/agent-template-backend:${IMAGE_TAG}"
|
||||||
|
|
||||||
|
"$LT/scripts/sync_langfuse_client_secret.sh"
|
||||||
|
|
||||||
|
kubectl -n "$NS" delete pod dependency-check --ignore-not-found >/dev/null 2>&1 || true
|
||||||
|
cat <<YAML | kubectl apply -f -
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: dependency-check
|
||||||
|
namespace: $NS
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: dependency-check
|
||||||
|
image: $IMAGE
|
||||||
|
envFrom:
|
||||||
|
- secretRef:
|
||||||
|
name: agent-backend-runtime
|
||||||
|
env:
|
||||||
|
- name: LANGFUSE_PUBLIC_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_PUBLIC_KEY
|
||||||
|
- name: LANGFUSE_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_SECRET_KEY
|
||||||
|
- name: LANGFUSE_HOST
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: langfuse-client
|
||||||
|
key: LANGFUSE_HOST
|
||||||
|
volumeMounts:
|
||||||
|
- name: wallet
|
||||||
|
mountPath: /app/wallet
|
||||||
|
readOnly: true
|
||||||
|
command: ["python", "-c"]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
import os, json, base64, urllib.request
|
||||||
|
import oracledb
|
||||||
|
from pymongo import MongoClient
|
||||||
|
|
||||||
|
def ok(name, detail="OK"):
|
||||||
|
print(f"{name}: {detail}", flush=True)
|
||||||
|
|
||||||
|
required = ["LANGFUSE_PUBLIC_KEY", "LANGFUSE_SECRET_KEY", "LANGFUSE_HOST"]
|
||||||
|
missing = [k for k in required if not os.getenv(k)]
|
||||||
|
if missing:
|
||||||
|
raise RuntimeError("missing Langfuse variables: " + ", ".join(missing))
|
||||||
|
|
||||||
|
ok("wallet", os.getenv("ADB_WALLET_LOCATION", "<unset>"))
|
||||||
|
c = oracledb.connect(
|
||||||
|
user=os.environ["ADB_USER"], password=os.environ["ADB_PASSWORD"],
|
||||||
|
dsn=os.environ["ADB_DSN"], config_dir=os.environ["ADB_WALLET_LOCATION"],
|
||||||
|
wallet_location=os.environ["ADB_WALLET_LOCATION"],
|
||||||
|
wallet_password=os.getenv("ADB_WALLET_PASSWORD"),
|
||||||
|
)
|
||||||
|
ok("oracle", c.version)
|
||||||
|
c.close()
|
||||||
|
|
||||||
|
m = MongoClient(os.environ["MONGODB_URI"], serverSelectionTimeoutMS=10000)
|
||||||
|
ok("mongo", str(m.admin.command("ping")))
|
||||||
|
m.close()
|
||||||
|
|
||||||
|
host = os.environ["LANGFUSE_HOST"].rstrip("/")
|
||||||
|
health = urllib.request.urlopen(host + "/api/public/health", timeout=10)
|
||||||
|
ok("langfuse_health", str(health.status))
|
||||||
|
|
||||||
|
token = base64.b64encode((os.environ["LANGFUSE_PUBLIC_KEY"] + ":" + os.environ["LANGFUSE_SECRET_KEY"]).encode()).decode()
|
||||||
|
req = urllib.request.Request(host + "/api/public/projects", headers={"Authorization": "Basic " + token})
|
||||||
|
with urllib.request.urlopen(req, timeout=15) as r:
|
||||||
|
body = r.read().decode("utf-8", errors="replace")
|
||||||
|
if r.status != 200:
|
||||||
|
raise RuntimeError(f"Langfuse authenticated API returned {r.status}")
|
||||||
|
json.loads(body)
|
||||||
|
ok("langfuse_authenticated_api", "200")
|
||||||
|
|
||||||
|
ok("dependencies", "ALL_DEPENDENCIES_OK")
|
||||||
|
volumes:
|
||||||
|
- name: wallet
|
||||||
|
secret:
|
||||||
|
secretName: agent-backend-wallet
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if ! kubectl -n "$NS" wait --for=jsonpath='{.status.phase}'=Succeeded pod/dependency-check --timeout=180s >/dev/null 2>&1; then
|
||||||
|
kubectl -n "$NS" describe pod dependency-check || true
|
||||||
|
kubectl -n "$NS" logs dependency-check || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
kubectl -n "$NS" logs dependency-check
|
||||||
105
deploy/oke/load-test/scripts/validate_langfuse_integration.sh
Normal file
105
deploy/oke/load-test/scripts/validate_langfuse_integration.sh
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -Eeuo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
LT="$ROOT/deploy/oke/load-test"
|
||||||
|
RUNTIME_ENV="$LT/.env.runtime"
|
||||||
|
|
||||||
|
log(){ printf '[langfuse-validate] %s\n' "$*"; }
|
||||||
|
die(){ printf '[langfuse-validate] ERROR: %s\n' "$*" >&2; exit 1; }
|
||||||
|
|
||||||
|
# Do not source .env.runtime: OCI_* values can affect the OCI CLI exec plugin
|
||||||
|
# used by kubectl. Read only the values required by this validator.
|
||||||
|
read_env_value() {
|
||||||
|
local file="$1" key="$2" default_value="${3:-}"
|
||||||
|
local value=""
|
||||||
|
if [[ -f "$file" ]]; then
|
||||||
|
value="$(sed -n -E "s/^${key}=(.*)$/\\1/p" "$file" | tail -n 1)"
|
||||||
|
value="${value%$'\r'}"
|
||||||
|
if [[ "$value" == \"*\" && "$value" == *\" ]]; then value="${value:1:${#value}-2}"; fi
|
||||||
|
if [[ "$value" == \'*\' && "$value" == *\' ]]; then value="${value:1:${#value}-2}"; fi
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${value:-$default_value}"
|
||||||
|
}
|
||||||
|
|
||||||
|
LF_NS="${LANGFUSE_NAMESPACE:-$(read_env_value "$RUNTIME_ENV" LANGFUSE_NAMESPACE langfuse)}"
|
||||||
|
APP_NS="${K8S_NAMESPACE:-$(read_env_value "$RUNTIME_ENV" K8S_NAMESPACE agent-load-test)}"
|
||||||
|
CLIENT_SECRET="${LANGFUSE_CLIENT_SECRET:-langfuse-client}"
|
||||||
|
LF_HOST="${LANGFUSE_HOST:-$(read_env_value "$RUNTIME_ENV" LANGFUSE_HOST "http://langfuse-web.${LF_NS}.svc.cluster.local:3000")}"
|
||||||
|
POD="langfuse-auth-check-$(date +%s)"
|
||||||
|
|
||||||
|
command -v kubectl >/dev/null 2>&1 || die "kubectl not found"
|
||||||
|
|
||||||
|
log "Kubernetes context: $(kubectl config current-context 2>/dev/null || echo '<unknown>')"
|
||||||
|
|
||||||
|
# Fail early with a Kubernetes-auth specific message.
|
||||||
|
if ! kubectl auth can-i get secrets -n "$LF_NS" >/dev/null 2>&1; then
|
||||||
|
die "Kubernetes authentication/authorization failed. Confirm 'kubectl get nodes' works before validating Langfuse."
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$LT/scripts/sync_langfuse_client_secret.sh"
|
||||||
|
|
||||||
|
cleanup(){
|
||||||
|
kubectl -n "$APP_NS" delete pod "$POD" --ignore-not-found --wait=false >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Use a normal Pod manifest instead of `kubectl run --overrides`.
|
||||||
|
# This avoids JSON-patch escaping issues and keeps secretKeyRef explicit.
|
||||||
|
cat <<YAML | kubectl apply -f - >/dev/null
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Pod
|
||||||
|
metadata:
|
||||||
|
name: ${POD}
|
||||||
|
namespace: ${APP_NS}
|
||||||
|
labels:
|
||||||
|
app: langfuse-auth-check
|
||||||
|
spec:
|
||||||
|
restartPolicy: Never
|
||||||
|
containers:
|
||||||
|
- name: check
|
||||||
|
image: docker.io/curlimages/curl:latest
|
||||||
|
env:
|
||||||
|
- name: LANGFUSE_HOST
|
||||||
|
value: "${LF_HOST}"
|
||||||
|
- name: LANGFUSE_PUBLIC_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ${CLIENT_SECRET}
|
||||||
|
key: LANGFUSE_PUBLIC_KEY
|
||||||
|
- name: LANGFUSE_SECRET_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: ${CLIENT_SECRET}
|
||||||
|
key: LANGFUSE_SECRET_KEY
|
||||||
|
command: ["sh", "-c"]
|
||||||
|
args:
|
||||||
|
- |
|
||||||
|
set -eu
|
||||||
|
test -n "\$LANGFUSE_PUBLIC_KEY"
|
||||||
|
test -n "\$LANGFUSE_SECRET_KEY"
|
||||||
|
|
||||||
|
echo "Checking Langfuse health..."
|
||||||
|
curl -fsS "\$LANGFUSE_HOST/api/public/health" >/dev/null
|
||||||
|
|
||||||
|
echo "Checking authenticated Langfuse Public API..."
|
||||||
|
code=\$(curl -sS -o /tmp/projects.json -w "%{http_code}" \
|
||||||
|
-u "\$LANGFUSE_PUBLIC_KEY:\$LANGFUSE_SECRET_KEY" \
|
||||||
|
"\$LANGFUSE_HOST/api/public/projects")
|
||||||
|
|
||||||
|
if [ "\$code" != "200" ]; then
|
||||||
|
echo "LANGFUSE_AUTH_FAILED HTTP \$code"
|
||||||
|
cat /tmp/projects.json || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "LANGFUSE_AUTH_OK"
|
||||||
|
YAML
|
||||||
|
|
||||||
|
if ! kubectl -n "$APP_NS" wait --for=jsonpath='{.status.phase}'=Succeeded "pod/$POD" --timeout=120s >/dev/null 2>&1; then
|
||||||
|
kubectl -n "$APP_NS" logs "$POD" || true
|
||||||
|
kubectl -n "$APP_NS" describe pod "$POD" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
kubectl -n "$APP_NS" logs "$POD"
|
||||||
16
deploy/oke/load-test/scripts/watch_test.sh
Normal file
16
deploy/oke/load-test/scripts/watch_test.sh
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||||
|
set -a; source "$ROOT/deploy/oke/load-test/.env.runtime"; set +a
|
||||||
|
NS="${K8S_NAMESPACE:-agent-load-test}"
|
||||||
|
while true; do
|
||||||
|
clear
|
||||||
|
date
|
||||||
|
echo '=== Pods ==='
|
||||||
|
kubectl -n "$NS" get pods -o wide
|
||||||
|
echo '=== HPA ==='
|
||||||
|
kubectl -n "$NS" get hpa
|
||||||
|
echo '=== Resource usage ==='
|
||||||
|
kubectl -n "$NS" top pods 2>/dev/null || echo 'metrics-server not available'
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
165
docs/features/01_authentication.md
Normal file
165
docs/features/01_authentication.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Autenticação / Authentication
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `security/authentication.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Verifica quem pode acessar APIs, gateways e serviços protegidos antes que a requisição chegue ao agente.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cliente/Sistema
|
||||||
|
↓
|
||||||
|
Authentication Provider
|
||||||
|
↓
|
||||||
|
credencial válida?
|
||||||
|
├─ não → 401/nega acesso
|
||||||
|
└─ sim → principal autenticado → agente
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O framework contém uma abstração `AuthenticationProvider` e implementações para cenários diferentes. Entre as implementações atuais estão `NoAuthenticationProvider`, `DenyAuthenticationProvider`, `BasicAuthenticationProvider`, `ApiKeyAuthenticationProvider`, `StaticBearerAuthenticationProvider`, `JwtAuthenticationProvider`, `OAuth2IntrospectionAuthenticationProvider` e `TrustedProxyAuthenticationProvider`.
|
||||||
|
|
||||||
|
A autenticação produz um `AuthenticatedPrincipal` com `subject`, `scheme` e, quando aplicável, `claims`. A regra de negócio do agente não deve validar senha/token diretamente.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.security.authentication import BasicAuthenticationProvider
|
||||||
|
|
||||||
|
provider = BasicAuthenticationProvider(
|
||||||
|
client_id="client-a",
|
||||||
|
secret_hash="pbkdf2_sha256:...",
|
||||||
|
)
|
||||||
|
result = await provider.authenticate(request)
|
||||||
|
if not result.authenticated:
|
||||||
|
# negar acesso
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Segredos podem ser verificados em formato simples, SHA-256 ou PBKDF2; em produção, prefira hashes fortes e secret stores.
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Basic auth retornando 401: validar `Authorization: Basic ...` e o secret configurado.
|
||||||
|
- Confundir autenticação do usuário com `OCI_AUTH_MODE`: são problemas diferentes.
|
||||||
|
- Usar `NoAuthenticationProvider` em produção sem decisão explícita de arquitetura.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/security/authentication.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Checks who may access protected APIs, gateways, and services before the request reaches the agent.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Client/System
|
||||||
|
↓
|
||||||
|
Authentication Provider
|
||||||
|
↓
|
||||||
|
valid credential?
|
||||||
|
├─ no → 401/deny
|
||||||
|
└─ yes → authenticated principal → agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The framework exposes an `AuthenticationProvider` abstraction with multiple implementations. Current providers include `NoAuthenticationProvider`, `DenyAuthenticationProvider`, `BasicAuthenticationProvider`, `ApiKeyAuthenticationProvider`, `StaticBearerAuthenticationProvider`, `JwtAuthenticationProvider`, `OAuth2IntrospectionAuthenticationProvider`, and `TrustedProxyAuthenticationProvider`.
|
||||||
|
|
||||||
|
Authentication produces an `AuthenticatedPrincipal` containing `subject`, `scheme`, and optional `claims`. Domain code should not validate credentials directly.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.security.authentication import BasicAuthenticationProvider
|
||||||
|
|
||||||
|
provider = BasicAuthenticationProvider(
|
||||||
|
client_id="client-a",
|
||||||
|
secret_hash="pbkdf2_sha256:...",
|
||||||
|
)
|
||||||
|
result = await provider.authenticate(request)
|
||||||
|
if not result.authenticated:
|
||||||
|
# deny access
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets may be verified as plain, SHA-256, or PBKDF2 values; for production, prefer strong hashes and managed secret stores.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Basic auth returns 401: validate the `Authorization: Basic ...` header and configured secret.
|
||||||
|
- Do not confuse API authentication with `OCI_AUTH_MODE`; they solve different problems.
|
||||||
|
- Avoid `NoAuthenticationProvider` in production unless explicitly accepted by architecture.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/security/authentication.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
183
docs/features/02_deterministic_transactional_workflow.md
Normal file
183
docs/features/02_deterministic_transactional_workflow.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# Workflow Transacional Determinístico / Deterministic Transactional Workflow
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `workflows/runtime.py + mcp/tool_policy.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Garante que operações que alteram estado sigam passos previsíveis, com confirmação e controle de execução, em vez de depender da criatividade do LLM.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mensagem do cliente
|
||||||
|
↓
|
||||||
|
LLM entende intenção
|
||||||
|
↓
|
||||||
|
Tool policy = transactional
|
||||||
|
↓
|
||||||
|
Workflow determinístico
|
||||||
|
↓
|
||||||
|
confirmação
|
||||||
|
↓
|
||||||
|
execução controlada
|
||||||
|
↓
|
||||||
|
resultado
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O LLM pode ajudar a interpretar a intenção e extrair parâmetros, mas não deve decidir a sequência crítica de uma transação. O `ToolPolicyRegistry` classifica tools, e `operation_type: transactional` ativa a política transacional. O `WorkflowRuntime` executa o workflow, mantém estado e integra pause/resume e recuperação de erro.
|
||||||
|
|
||||||
|
A configuração `ENABLE_TRANSACTIONAL_WORKFLOWS` controla a capability global, e `WORKFLOWS_PATH` aponta para os YAMLs.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tools:
|
||||||
|
cancelar_servico:
|
||||||
|
operation_type: transactional
|
||||||
|
requires_confirmation: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. localizar serviço
|
||||||
|
2. validar elegibilidade
|
||||||
|
3. pedir confirmação
|
||||||
|
4. PAUSE
|
||||||
|
5. receber confirmação
|
||||||
|
6. RESUME
|
||||||
|
7. executar side effect
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Marcar uma tool de escrita como `read_only` elimina proteções transacionais.
|
||||||
|
- Reexecutar steps anteriores ao pause pode duplicar side effects; use o runtime oficial.
|
||||||
|
- Não use prompt como única garantia de confirmação.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/mcp/tool_policy.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Ensures state-changing operations follow predictable steps with confirmation and execution control instead of depending on LLM creativity.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Customer message
|
||||||
|
↓
|
||||||
|
LLM understands intent
|
||||||
|
↓
|
||||||
|
Tool policy = transactional
|
||||||
|
↓
|
||||||
|
Deterministic workflow
|
||||||
|
↓
|
||||||
|
confirmation
|
||||||
|
↓
|
||||||
|
controlled execution
|
||||||
|
↓
|
||||||
|
result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The LLM may help interpret intent and extract parameters, but it should not decide the critical sequence of a transaction. `ToolPolicyRegistry` classifies tools, and `operation_type: transactional` activates transactional behavior. `WorkflowRuntime` executes the workflow, preserves state, and integrates pause/resume and error recovery.
|
||||||
|
|
||||||
|
`ENABLE_TRANSACTIONAL_WORKFLOWS` controls the capability globally, while `WORKFLOWS_PATH` points to workflow YAML files.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tools:
|
||||||
|
cancel_service:
|
||||||
|
operation_type: transactional
|
||||||
|
requires_confirmation: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. locate service
|
||||||
|
2. validate eligibility
|
||||||
|
3. ask for confirmation
|
||||||
|
4. PAUSE
|
||||||
|
5. receive confirmation
|
||||||
|
6. RESUME
|
||||||
|
7. execute side effect
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Marking a write tool as `read_only` bypasses transactional protections.
|
||||||
|
- Re-running steps before a pause can duplicate side effects; use the official runtime.
|
||||||
|
- Do not use prompts as the only confirmation guarantee.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/mcp/tool_policy.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
157
docs/features/03_domain_requested_llm_composition.md
Normal file
157
docs/features/03_domain_requested_llm_composition.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Composição por LLM Solicitada pelo Domínio / Domain Requested LLM Composition
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite que a regra de negócio calcule o resultado e peça ao LLM apenas para redigir a resposta final.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Regra de negócio calcula
|
||||||
|
↓
|
||||||
|
requires_llm_composition=true
|
||||||
|
↓
|
||||||
|
framework impede resposta MCP direta
|
||||||
|
↓
|
||||||
|
LLMProvider oficial
|
||||||
|
↓
|
||||||
|
redação natural
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O domínio retorna dados confiáveis e uma instrução de composição. O `AgentRuntimeMixin` detecta `requires_llm_composition` de forma recursiva no resultado da tool/workflow e não encerra a resposta pelo caminho direto de MCP. A composição segue pelo LLM oficial do agente, preservando profiles, tracing, usage e políticas do framework.
|
||||||
|
|
||||||
|
O LLM deve redigir; ele não deve recalcular valores nem decidir regras de negócio já resolvidas.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"refund_amount": "38,00",
|
||||||
|
"requires_llm_composition": true,
|
||||||
|
"response_instruction": "Explique a devolução usando somente os valores calculados."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Instrução muito aberta pode fazer o LLM adicionar conteúdo não autorizado.
|
||||||
|
- Não envie ao LLM a responsabilidade de recalcular valores determinísticos.
|
||||||
|
- Se não houver necessidade de redação livre, prefira resposta determinística direta.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Lets domain logic compute the authoritative result and ask the LLM only to compose the final user-facing response.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Domain logic computes
|
||||||
|
↓
|
||||||
|
requires_llm_composition=true
|
||||||
|
↓
|
||||||
|
framework prevents direct MCP answer
|
||||||
|
↓
|
||||||
|
official LLMProvider
|
||||||
|
↓
|
||||||
|
natural-language response
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The domain returns authoritative data plus a composition instruction. `AgentRuntimeMixin` recursively detects `requires_llm_composition` in tool/workflow results and avoids terminating through the direct MCP-answer path. Composition then uses the agent's official LLM provider, preserving profiles, tracing, usage accounting, and framework policies.
|
||||||
|
|
||||||
|
The LLM should compose language, not recalculate values or override already-resolved business rules.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"refund_amount": "38.00",
|
||||||
|
"requires_llm_composition": true,
|
||||||
|
"response_instruction": "Explain the refund using only the computed values."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- An overly broad instruction may let the LLM add unauthorized content.
|
||||||
|
- Do not delegate deterministic calculations back to the LLM.
|
||||||
|
- If free-form wording is unnecessary, prefer a deterministic direct response.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
167
docs/features/04_domain_requested_rag.md
Normal file
167
docs/features/04_domain_requested_rag.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# RAG Solicitado pelo Domínio / Domain Requested RAG
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite que uma tool ou workflow declare que a resposta precisa consultar conhecimento externo, mesmo quando já existe resultado MCP.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tool/Workflow
|
||||||
|
↓
|
||||||
|
requires_rag=true
|
||||||
|
↓
|
||||||
|
rag_query / rag_queries
|
||||||
|
↓
|
||||||
|
RagService do framework
|
||||||
|
↓
|
||||||
|
Retrieval Guardrails
|
||||||
|
↓
|
||||||
|
LLM/resposta
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
Normalmente o framework pode pular RAG quando MCP já trouxe informação suficiente (`SKIP_RAG_WHEN_MCP_SUFFICIENT`). Esta feature permite que o domínio substitua essa decisão para um caso específico. O resultado pode declarar `requires_rag`, `rag_query` ou `rag_queries`; o runtime usa essas queries como override e executa o `RagService`.
|
||||||
|
|
||||||
|
O domínio informa **o que precisa saber**. Ele não implementa cliente de vetor, retriever ou prompt RAG paralelo.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_rag": true,
|
||||||
|
"rag_queries": [
|
||||||
|
"Como cancelar YouTube Premium?",
|
||||||
|
"Como cancelar Aya Books?"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurações relacionadas incluem `RAG_TOP_K` e `SKIP_RAG_WHEN_MCP_SUFFICIENT`.
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Declarar RAG para fatos transacionais já resolvidos pela API pode aumentar custo e latência.
|
||||||
|
- Query genérica demais reduz relevância.
|
||||||
|
- Nunca confie no retrieval sem `Retrieval Guardrails` quando o dado influencia resposta crítica.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows a tool or workflow to declare that external knowledge retrieval is required even when an MCP result already exists.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tool/Workflow
|
||||||
|
↓
|
||||||
|
requires_rag=true
|
||||||
|
↓
|
||||||
|
rag_query / rag_queries
|
||||||
|
↓
|
||||||
|
framework RagService
|
||||||
|
↓
|
||||||
|
Retrieval Guardrails
|
||||||
|
↓
|
||||||
|
LLM/response
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Normally the framework may skip RAG when MCP already provides sufficient data (`SKIP_RAG_WHEN_MCP_SUFFICIENT`). This feature lets the domain override that decision for a specific case. A result may declare `requires_rag`, `rag_query`, or `rag_queries`; the runtime uses those queries as overrides and invokes `RagService`.
|
||||||
|
|
||||||
|
The domain declares **what knowledge is needed**. It does not implement its own vector client, retriever, or parallel RAG prompt stack.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_rag": true,
|
||||||
|
"rag_queries": [
|
||||||
|
"How to cancel YouTube Premium?",
|
||||||
|
"How to cancel Aya Books?"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Related settings include `RAG_TOP_K` and `SKIP_RAG_WHEN_MCP_SUFFICIENT`.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Requesting RAG for transactional facts already resolved by an API adds unnecessary cost and latency.
|
||||||
|
- Queries that are too broad reduce relevance.
|
||||||
|
- Do not trust retrieved content for critical responses without Retrieval Guardrails.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
167
docs/features/05_long_term_memory.md
Normal file
167
docs/features/05_long_term_memory.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Memória de Longo Prazo / Long Term Memory
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `memory/long_term_memory.py + memory/long_term_store.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite lembrar informações úteis entre sessões diferentes, sem depender do histórico completo de uma conversa.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Sessão A
|
||||||
|
↓
|
||||||
|
extração de memória relevante
|
||||||
|
↓
|
||||||
|
Long Term Memory Store
|
||||||
|
↓
|
||||||
|
... dias depois ...
|
||||||
|
↓
|
||||||
|
Sessão B
|
||||||
|
↓
|
||||||
|
recupera contexto relevante
|
||||||
|
↓
|
||||||
|
agente
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
A memória de longo prazo é diferente de histórico de mensagens e de checkpoint. Ela persiste fatos/preferências úteis e os recupera como contexto de uma nova sessão. O framework oferece providers `memory`, `sqlite`, `autonomous` e `oracle`.
|
||||||
|
|
||||||
|
Configurações importantes: `ENABLE_LONG_TERM_MEMORY`, `LONG_TERM_MEMORY_PROVIDER`, `LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS`, `LONG_TERM_MEMORY_MIN_CONFIDENCE`, `LONG_TERM_MEMORY_AUTO_EXTRACT` e `LONG_TERM_MEMORY_INJECT_CONTEXT`.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=oracle
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Não confundir LTM com replay de toda conversa.
|
||||||
|
- Memória irrelevante ou de baixa confiança não deveria ser injetada.
|
||||||
|
- Em múltiplas réplicas, prefira storage durável compartilhado em vez de memória local.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_memory.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_store.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows useful information to persist across different sessions without depending on the full transcript of a previous conversation.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Session A
|
||||||
|
↓
|
||||||
|
extract relevant memory
|
||||||
|
↓
|
||||||
|
Long Term Memory Store
|
||||||
|
↓
|
||||||
|
... days later ...
|
||||||
|
↓
|
||||||
|
Session B
|
||||||
|
↓
|
||||||
|
retrieve relevant context
|
||||||
|
↓
|
||||||
|
agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Long-term memory is different from message history and checkpoints. It persists useful facts/preferences and retrieves them as context for a future session. The framework supports `memory`, `sqlite`, `autonomous`, and `oracle` providers.
|
||||||
|
|
||||||
|
Important settings include `ENABLE_LONG_TERM_MEMORY`, `LONG_TERM_MEMORY_PROVIDER`, `LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS`, `LONG_TERM_MEMORY_MIN_CONFIDENCE`, `LONG_TERM_MEMORY_AUTO_EXTRACT`, and `LONG_TERM_MEMORY_INJECT_CONTEXT`.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=oracle
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Do not confuse LTM with replaying the entire transcript.
|
||||||
|
- Irrelevant or low-confidence memories should not be injected.
|
||||||
|
- For multiple replicas, prefer shared durable storage over local memory.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_memory.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_store.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
163
docs/features/06_offline_workflow_regression.md
Normal file
163
docs/features/06_offline_workflow_regression.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Regressão Offline de Workflow / Offline Workflow Regression
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `workflows/runtime.py + Tuning-Performance/Offline_Workflow_Regression`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite testar a lógica de workflows sem exigir toda a infraestrutura de produção.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Teste
|
||||||
|
↓
|
||||||
|
backend determinístico explicitamente habilitado
|
||||||
|
↓
|
||||||
|
run → PAUSED
|
||||||
|
↓
|
||||||
|
resume → COMPLETED
|
||||||
|
↓
|
||||||
|
asserts de estado/side effects
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O `WorkflowRuntime` possui um caminho determinístico/offline **explicitamente opt-in para testes**. Ele permite validar DSL, condições, pause/resume e proteção contra reexecução sem exigir LangGraph, banco, OCI ou APIs externas.
|
||||||
|
|
||||||
|
O comportamento de produção continua usando LangGraph. O modo offline não deve virar fallback silencioso quando LangGraph falha ou está ausente em produção.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
run(workflow)
|
||||||
|
action_a = 1 execução
|
||||||
|
status = PAUSED
|
||||||
|
|
||||||
|
resume(workflow)
|
||||||
|
action_a continua com 1 execução
|
||||||
|
action_b = 1 execução
|
||||||
|
status = COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Usar o backend offline em produção mascara problemas reais.
|
||||||
|
- Mockar tanto que o teste deixa de validar a DSL real.
|
||||||
|
- Não verificar side effects anteriores ao pause pode esconder duplicações.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/Tuning-Performance/Offline_Workflow_Regression`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows workflow logic to be regression-tested without requiring the full production infrastructure.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test
|
||||||
|
↓
|
||||||
|
explicit deterministic test backend
|
||||||
|
↓
|
||||||
|
run → PAUSED
|
||||||
|
↓
|
||||||
|
resume → COMPLETED
|
||||||
|
↓
|
||||||
|
state/side-effect assertions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
`WorkflowRuntime` includes an **explicitly opt-in deterministic/offline test backend**. It can validate DSL rules, conditions, pause/resume behavior, and duplicate-execution protection without requiring LangGraph, a database, OCI, or external APIs.
|
||||||
|
|
||||||
|
Production behavior still uses LangGraph. Offline mode must never become a silent fallback when LangGraph fails or is unavailable in production.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
run(workflow)
|
||||||
|
action_a = executed once
|
||||||
|
status = PAUSED
|
||||||
|
|
||||||
|
resume(workflow)
|
||||||
|
action_a remains executed once
|
||||||
|
action_b = executed once
|
||||||
|
status = COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Using the offline backend in production hides real issues.
|
||||||
|
- Over-mocking can stop the test from validating real DSL behavior.
|
||||||
|
- Failing to assert pre-pause side effects may hide duplicate execution.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/Tuning-Performance/Offline_Workflow_Regression`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
165
docs/features/07_pause_resume_workflow.md
Normal file
165
docs/features/07_pause_resume_workflow.md
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
# Pause / Resume de Workflow / Pause / Resume Workflow
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `workflows/runtime.py + workflows/graph.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite interromper um workflow em um ponto seguro, persistir o estado e continuar depois com a resposta do usuário ou outro evento.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow
|
||||||
|
↓
|
||||||
|
ações prévias
|
||||||
|
↓
|
||||||
|
PAUSE
|
||||||
|
↓
|
||||||
|
checkpoint/estado
|
||||||
|
↓
|
||||||
|
nova mensagem
|
||||||
|
↓
|
||||||
|
RESUME
|
||||||
|
↓
|
||||||
|
ações seguintes
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
`WorkflowRuntime` expõe `arun(...)` e `aresume(...)`. O nó de pause é separado da action anterior para evitar reexecutar side effects quando o workflow retoma. O mesmo `execution_id/thread_id` identifica a execução pausada e retomada.
|
||||||
|
|
||||||
|
O runtime suporta condições declarativas como `all`, `any`, `not`, `eq`, `neq` e `exists`, permitindo definir quando pausar ou continuar sem colocar lógica conversacional no prompt.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
status = await runtime.arun(...)
|
||||||
|
# status == PAUSED
|
||||||
|
|
||||||
|
status = await runtime.aresume(execution_id, input={"confirmed": true})
|
||||||
|
# status == COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Perder o `execution_id` impede retomar a execução correta.
|
||||||
|
- Reexecutar o workflow do zero após confirmação pode repetir side effects.
|
||||||
|
- Pause sem storage/checkpoint compartilhado é frágil em múltiplas réplicas.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/graph.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows a workflow to stop at a safe point, persist state, and continue later using user input or another event.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow
|
||||||
|
↓
|
||||||
|
pre-pause actions
|
||||||
|
↓
|
||||||
|
PAUSE
|
||||||
|
↓
|
||||||
|
checkpoint/state
|
||||||
|
↓
|
||||||
|
new message
|
||||||
|
↓
|
||||||
|
RESUME
|
||||||
|
↓
|
||||||
|
remaining actions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
`WorkflowRuntime` exposes `arun(...)` and `aresume(...)`. The pause node is separated from the preceding action so previous side effects are not executed again on resume. The same `execution_id/thread_id` identifies the paused and resumed execution.
|
||||||
|
|
||||||
|
The runtime supports declarative conditions such as `all`, `any`, `not`, `eq`, `neq`, and `exists`, so pause/continue decisions do not need to live in the prompt.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
status = await runtime.arun(...)
|
||||||
|
# status == PAUSED
|
||||||
|
|
||||||
|
status = await runtime.aresume(execution_id, input={"confirmed": true})
|
||||||
|
# status == COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Losing the `execution_id` prevents resuming the right execution.
|
||||||
|
- Restarting the workflow from scratch after confirmation may duplicate side effects.
|
||||||
|
- Pause without shared checkpoint/state storage is fragile across multiple replicas.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/graph.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
151
docs/features/08_route_stickiness.md
Normal file
151
docs/features/08_route_stickiness.md
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
# Aderência de Rota / Route Stickiness
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `routing/enterprise_router.py + runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Evita que pequenas mensagens de continuação façam a conversa trocar de agente sem necessidade.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
mensagem atual
|
||||||
|
+ histórico curto
|
||||||
|
+ rota anterior
|
||||||
|
↓
|
||||||
|
continuidade semântica
|
||||||
|
↓
|
||||||
|
manter rota ou handoff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
Route Stickiness avalia se a nova mensagem continua semanticamente ligada ao assunto/agente atual. Isso reduz ping-pong de agentes em mensagens como “e esse valor?”, “sim”, “o segundo” ou “e no mês passado?”.
|
||||||
|
|
||||||
|
Configurações existentes incluem `ENABLE_ROUTE_STICKINESS`, `ROUTE_STICKINESS_LLM_PROFILE`, `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`, `ROUTE_STICKINESS_HISTORY_TURNS` e `ROUTE_STICKINESS_MAX_TOKENS`. A decisão pode permitir handoff quando há evidência suficiente de mudança de assunto.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Threshold muito baixo pode prender o cliente no agente errado.
|
||||||
|
- Threshold alto demais perde continuidade em mensagens curtas.
|
||||||
|
- Stickiness não deve bloquear handoff explícito quando a intenção realmente mudou.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Prevents short follow-up messages from unnecessarily switching the conversation to another agent.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
current message
|
||||||
|
+ short history
|
||||||
|
+ previous route
|
||||||
|
↓
|
||||||
|
semantic continuity
|
||||||
|
↓
|
||||||
|
keep route or handoff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Route Stickiness evaluates whether a new message is semantically continuous with the current subject/agent. It reduces agent ping-pong for messages such as “what about that amount?”, “yes”, “the second one”, or “and last month?”.
|
||||||
|
|
||||||
|
Existing settings include `ENABLE_ROUTE_STICKINESS`, `ROUTE_STICKINESS_LLM_PROFILE`, `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`, `ROUTE_STICKINESS_HISTORY_TURNS`, and `ROUTE_STICKINESS_MAX_TOKENS`. The decision may still allow handoff when there is enough evidence of a topic change.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- A threshold that is too low may trap the user on the wrong agent.
|
||||||
|
- A threshold that is too high may lose continuity on short follow-ups.
|
||||||
|
- Stickiness should not block explicit handoff when the user clearly changes intent.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
153
docs/features/09_voice_interruption_replay.md
Normal file
153
docs/features/09_voice_interruption_replay.md
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
# Replay em Interrupções de Voz / Voice Interruption Replay
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `channels/interruption.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Decide se um áudio recebido durante a fala do agente representa uma nova intenção, um ruído/backchannel ou algo que deve apenas repetir/continuar a última fala.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
áudio durante fala
|
||||||
|
↓
|
||||||
|
InterruptionPolicy
|
||||||
|
├─ process → nova mensagem
|
||||||
|
├─ classify → classificador leve
|
||||||
|
└─ replay → última fala
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
A política fica no framework, não no domínio. Ela diferencia sessão terminal, `idle_nudge`, fala não interrompível e fala potencialmente interrompível. Quando necessário, pode usar um classificador leve baseado no `LLMProvider`; quando a classificação falha, a política é conservadora e pode optar por replay.
|
||||||
|
|
||||||
|
O objetivo é evitar que “aham”, ruído, eco ou fragmentos residuais sejam tratados como uma nova intenção completa.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agente: "Sua fatura possui..."
|
||||||
|
Cliente: "aham"
|
||||||
|
→ replay/continua
|
||||||
|
|
||||||
|
Agente: "Sua fatura possui..."
|
||||||
|
Cliente: "espera, quero falar de outra coisa"
|
||||||
|
→ processa nova intenção
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Classificar todo ruído com LLM aumenta latência e custo.
|
||||||
|
- Permitir interrupção em fala transacional não interrompível pode corromper UX/estado.
|
||||||
|
- Replay deve usar uma fala real anterior, não um envelope técnico.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/channels/interruption.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Decides whether audio received while the agent is speaking represents a new intent, a backchannel/noise event, or something that should simply replay/continue the previous speech.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
audio during speech
|
||||||
|
↓
|
||||||
|
InterruptionPolicy
|
||||||
|
├─ process → new message
|
||||||
|
├─ classify → lightweight classifier
|
||||||
|
└─ replay → previous speech
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The policy lives in the framework rather than domain code. It distinguishes terminal sessions, `idle_nudge`, non-interruptible speech, and potentially interruptible speech. When needed, it may use a lightweight classifier backed by `LLMProvider`; on classification failure, it can fail safely to replay.
|
||||||
|
|
||||||
|
The goal is to prevent “uh-huh”, noise, echo, or residual audio fragments from being interpreted as a full new intent.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent: "Your invoice contains..."
|
||||||
|
User: "uh-huh"
|
||||||
|
→ replay/continue
|
||||||
|
|
||||||
|
Agent: "Your invoice contains..."
|
||||||
|
User: "wait, I want to ask something else"
|
||||||
|
→ process new intent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Sending every noise fragment to an LLM increases latency and cost.
|
||||||
|
- Allowing interruption during non-interruptible transactional speech may corrupt UX/state.
|
||||||
|
- Replay should use a real previous utterance, not a technical envelope.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/channels/interruption.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
167
docs/features/10_workflow_error_recovery.md
Normal file
167
docs/features/10_workflow_error_recovery.md
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
# Recuperação de Erro em Workflow / Workflow Error Recovery
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `workflows/runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Preserva o estado parcial de uma execução quando um passo posterior falha, permitindo entender o que já aconteceu e evitar repetir side effects.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
passo A ✅
|
||||||
|
passo B ✅
|
||||||
|
passo C ❌
|
||||||
|
↓
|
||||||
|
FAILED + snapshot parcial
|
||||||
|
↓
|
||||||
|
recovery decide o que pode continuar/repetir
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O runtime preserva o snapshot parcial do LangGraph quando uma etapa posterior falha e produz `error_details` genérico. Quando a exceção externa possui informações estruturadas, podem ser preservados status HTTP, body, número de tentativas, code e metadata.
|
||||||
|
|
||||||
|
A feature não significa “tentar tudo de novo”. Recuperação segura depende de conhecer o estado já executado, a idempotência e a natureza do erro.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "FAILED",
|
||||||
|
"error_details": {
|
||||||
|
"status": 503,
|
||||||
|
"attempts": 3,
|
||||||
|
"code": "UPSTREAM_UNAVAILABLE"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"protocol_created": true,
|
||||||
|
"operation_completed": true,
|
||||||
|
"sms_sent": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Retry indiscriminado pode repetir transações.
|
||||||
|
- Se a exceção externa perde metadata, a recuperação fica menos precisa.
|
||||||
|
- Combine sempre com Durable Idempotency em side effects críticos.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Preserves partial execution state when a later step fails, making it possible to know what already happened and avoid repeating side effects.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
step A ✅
|
||||||
|
step B ✅
|
||||||
|
step C ❌
|
||||||
|
↓
|
||||||
|
FAILED + partial snapshot
|
||||||
|
↓
|
||||||
|
recovery decides what may continue/retry
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The runtime preserves the partial LangGraph snapshot when a later step fails and produces generic `error_details`. When an external exception provides structured information, HTTP status, body, attempt count, code, and metadata may be preserved.
|
||||||
|
|
||||||
|
This feature does not mean “retry everything”. Safe recovery depends on knowing what already executed, idempotency guarantees, and the nature of the failure.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "FAILED",
|
||||||
|
"error_details": {
|
||||||
|
"status": 503,
|
||||||
|
"attempts": 3,
|
||||||
|
"code": "UPSTREAM_UNAVAILABLE"
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"protocol_created": true,
|
||||||
|
"operation_completed": true,
|
||||||
|
"sms_sent": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Blind retries may repeat transactions.
|
||||||
|
- If external exceptions discard metadata, recovery becomes less precise.
|
||||||
|
- Always combine with Durable Idempotency for critical side effects.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
169
docs/features/11_clarification.md
Normal file
169
docs/features/11_clarification.md
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
# Clarificação / Clarification
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Quando faltam dados ou uma tool encontra múltiplas opções, o framework pergunta ao usuário em vez de adivinhar.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
pedido ambíguo
|
||||||
|
↓
|
||||||
|
NEEDS_CLARIFICATION
|
||||||
|
↓
|
||||||
|
pergunta + opções
|
||||||
|
↓
|
||||||
|
usuário responde
|
||||||
|
↓
|
||||||
|
framework resolve
|
||||||
|
↓
|
||||||
|
retoma mesma tool/workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O runtime suporta clarificação tanto de parâmetros faltantes quanto de resultados de tools. Para tool-result clarification, um resultado com `status: NEEDS_CLARIFICATION` pode trazer opções; o runtime persiste `pending_tool_clarification`, entra em `TOOL_RESULT_CLARIFICATION` e consegue resolver respostas por ordinal ou nome.
|
||||||
|
|
||||||
|
Depois da escolha, o framework reutiliza a mesma tool e injeta os argumentos resolvidos, evitando que o roteador trate a resposta curta como uma intenção nova.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "NEEDS_CLARIFICATION",
|
||||||
|
"question": "Qual serviço?",
|
||||||
|
"options": [
|
||||||
|
{"id": "tim_music", "label": "TIM Music"},
|
||||||
|
{"id": "hbo_max", "label": "HBO Max"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Usuário: `o segundo` → `hbo_max`.
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Não descarte `pending_tool_clarification` entre turns.
|
||||||
|
- Uma resposta curta deve ser resolvida contra as opções antes do roteamento normal.
|
||||||
|
- Opções sem identificador/label consistente pioram a resolução.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
When required information is missing or a tool finds multiple options, the framework asks the user instead of guessing.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
ambiguous request
|
||||||
|
↓
|
||||||
|
NEEDS_CLARIFICATION
|
||||||
|
↓
|
||||||
|
question + options
|
||||||
|
↓
|
||||||
|
user answers
|
||||||
|
↓
|
||||||
|
framework resolves
|
||||||
|
↓
|
||||||
|
resume same tool/workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The runtime supports clarification for both missing parameters and ambiguous tool results. For tool-result clarification, a result with `status: NEEDS_CLARIFICATION` may include options; the runtime persists `pending_tool_clarification`, moves to `TOOL_RESULT_CLARIFICATION`, and can resolve responses by ordinal or name.
|
||||||
|
|
||||||
|
After selection, the framework reuses the same tool and injects resolved arguments, preventing the router from treating a short reply as a brand-new intent.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "NEEDS_CLARIFICATION",
|
||||||
|
"question": "Which service?",
|
||||||
|
"options": [
|
||||||
|
{"id": "tim_music", "label": "TIM Music"},
|
||||||
|
{"id": "hbo_max", "label": "HBO Max"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
User: `the second one` → `hbo_max`.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Do not discard `pending_tool_clarification` between turns.
|
||||||
|
- A short answer should be resolved against pending options before normal routing.
|
||||||
|
- Options without stable identifiers/labels reduce resolution quality.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
163
docs/features/12_durable_idempotency.md
Normal file
163
docs/features/12_durable_idempotency.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Idempotência Durável / Durable Idempotency
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `idempotency.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Impede que a mesma operação crítica seja executada duas vezes, inclusive quando outra réplica/pod recebe a repetição.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
requisição
|
||||||
|
↓
|
||||||
|
idempotency key
|
||||||
|
↓
|
||||||
|
store durável
|
||||||
|
├─ existe → retorna resultado anterior
|
||||||
|
└─ não existe → executa → persiste resultado
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
`create_idempotency_store(settings, ...)` escolhe o backend conforme configuração/plataforma. O framework possui `IdempotencyStore` e `InMemoryIdempotencyStore`, mas produção distribuída deve preferir storage compartilhado. As configurações incluem `IDEMPOTENCY_PROVIDER`, `IDEMPOTENCY_REQUIRE_DURABLE` e `IDEMPOTENCY_TTL_SECONDS`.
|
||||||
|
|
||||||
|
Idempotência é diferente de retry: retry repete a tentativa; idempotência garante que a repetição não produza um novo side effect.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pod A recebe cancelamento
|
||||||
|
→ key=cliente:servico:operacao
|
||||||
|
→ executa
|
||||||
|
→ grava resultado
|
||||||
|
|
||||||
|
Pod A cai
|
||||||
|
|
||||||
|
Pod B recebe retry
|
||||||
|
→ mesma key
|
||||||
|
→ encontra resultado
|
||||||
|
→ NÃO cancela de novo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Usar store em memória com múltiplos pods não é idempotência durável.
|
||||||
|
- Chave ampla demais pode bloquear operações legítimas; estreita demais permite duplicidade.
|
||||||
|
- TTL deve ser compatível com a janela real de retry/replay.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/idempotency.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Prevents the same critical operation from executing twice, including when a retry lands on another replica/pod.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
request
|
||||||
|
↓
|
||||||
|
idempotency key
|
||||||
|
↓
|
||||||
|
durable store
|
||||||
|
├─ exists → return previous result
|
||||||
|
└─ missing → execute → persist result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
`create_idempotency_store(settings, ...)` chooses a backend according to configuration/platform. The framework provides `IdempotencyStore` and `InMemoryIdempotencyStore`, but distributed production should prefer shared storage. Settings include `IDEMPOTENCY_PROVIDER`, `IDEMPOTENCY_REQUIRE_DURABLE`, and `IDEMPOTENCY_TTL_SECONDS`.
|
||||||
|
|
||||||
|
Idempotency is different from retry: retry repeats an attempt; idempotency guarantees that repetition does not create another side effect.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
Pod A receives cancellation
|
||||||
|
→ key=customer:service:operation
|
||||||
|
→ executes
|
||||||
|
→ stores result
|
||||||
|
|
||||||
|
Pod A crashes
|
||||||
|
|
||||||
|
Pod B receives retry
|
||||||
|
→ same key
|
||||||
|
→ finds stored result
|
||||||
|
→ DOES NOT cancel again
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- An in-memory store across multiple pods is not durable idempotency.
|
||||||
|
- A key that is too broad may block legitimate operations; too narrow may allow duplicates.
|
||||||
|
- TTL should match the real retry/replay window.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/idempotency.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
157
docs/features/13_dynamic_transaction_states.md
Normal file
157
docs/features/13_dynamic_transaction_states.md
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
# Estados Transacionais Dinâmicos / Dynamic Transaction States
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `runtime/agent_runtime.py + mcp/tool_policy.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Permite criar estados de confirmação baseados no agente/domínio atual sem hardcode de todos os domínios dentro do framework.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
tool transactional
|
||||||
|
↓
|
||||||
|
agente/domínio atual
|
||||||
|
↓
|
||||||
|
WAITING_<PREFIX>_CONFIRMATION
|
||||||
|
↓
|
||||||
|
confirmação/rejeição
|
||||||
|
↓
|
||||||
|
estado seguinte
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
Em vez de manter estados fixos como `WAITING_BILLING_CONFIRMATION`, `WAITING_PRODUCT_CONFIRMATION` etc. para cada domínio conhecido, o runtime deriva o prefixo do agente atual e gera o estado dinamicamente. A função interna de estado transacional mantém o framework genérico.
|
||||||
|
|
||||||
|
A classificação `operation_type` aceita `read_only`, `transactional`, `conversational` e `internal`; somente `transactional` entra no caminho de confirmação transacional.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
VasAgent + cancelar_vas
|
||||||
|
→ WAITING_VAS_CONFIRMATION
|
||||||
|
|
||||||
|
AddressAgent + alterar_endereco
|
||||||
|
→ WAITING_ADDRESS_CONFIRMATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Hardcode de estados no domínio reduz reutilização.
|
||||||
|
- Classificar uma tool como `conversational` não deve ativar confirmação transacional.
|
||||||
|
- Mudanças no identificador do agente podem mudar o prefixo; mantenha IDs estáveis.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/mcp/tool_policy.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows confirmation states to be derived from the current agent/domain instead of hardcoding every business domain into the framework.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
transactional tool
|
||||||
|
↓
|
||||||
|
current agent/domain
|
||||||
|
↓
|
||||||
|
WAITING_<PREFIX>_CONFIRMATION
|
||||||
|
↓
|
||||||
|
confirm/reject
|
||||||
|
↓
|
||||||
|
next state
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Instead of maintaining fixed states such as `WAITING_BILLING_CONFIRMATION`, `WAITING_PRODUCT_CONFIRMATION`, and so on for every known domain, the runtime derives a prefix from the current agent and builds the confirmation state dynamically. This keeps the framework generic.
|
||||||
|
|
||||||
|
`operation_type` accepts `read_only`, `transactional`, `conversational`, and `internal`; only `transactional` enters the transactional confirmation path.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
VasAgent + cancel_vas
|
||||||
|
→ WAITING_VAS_CONFIRMATION
|
||||||
|
|
||||||
|
AddressAgent + change_address
|
||||||
|
→ WAITING_ADDRESS_CONFIRMATION
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Hardcoding states in domain code reduces reuse.
|
||||||
|
- Classifying a tool as `conversational` should not trigger transactional confirmation.
|
||||||
|
- Changing agent identifiers may change state prefixes; keep IDs stable.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/mcp/tool_policy.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
159
docs/features/14_post_finalization_replay.md
Normal file
159
docs/features/14_post_finalization_replay.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# Replay Após Finalização / Post Finalization Replay
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `channels/interruption.py + config/settings.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Evita que áudio residual ou mensagens tardias reabram uma sessão já finalizada.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
sessão terminal
|
||||||
|
↓
|
||||||
|
entrada residual
|
||||||
|
↓
|
||||||
|
policy detecta finalização
|
||||||
|
↓
|
||||||
|
replay última fala/fallback
|
||||||
|
↓
|
||||||
|
NÃO reabre LangGraph
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
A política de interrupção verifica metadata de sessão terminal antes de tratar uma entrada como nova intenção. Quando há texto terminal disponível, usa `last_assistant_text`/`terminal_replay_text`; caso contrário, pode usar a mensagem configurada em `POST_FINALIZE_REPLAY_MESSAGE`.
|
||||||
|
|
||||||
|
O objetivo é proteger o fechamento lógico da sessão, especialmente em canais de voz onde pacotes de áudio podem chegar depois do evento de finalização.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agente: "Atendimento concluído."
|
||||||
|
→ sessão finalizada
|
||||||
|
|
||||||
|
chega fragmento: "ã..."
|
||||||
|
→ replay "Atendimento concluído."
|
||||||
|
→ nenhum routing / tool / LLM novo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Se o estado terminal não for persistido, outra réplica pode reabrir a jornada.
|
||||||
|
- Não use replay técnico/JSON como fala do cliente.
|
||||||
|
- Essa feature não substitui política de nova sessão intencional.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/channels/interruption.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/config/settings.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Prevents residual audio or late messages from reopening a session that has already been finalized.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
terminal session
|
||||||
|
↓
|
||||||
|
residual input
|
||||||
|
↓
|
||||||
|
policy detects finalization
|
||||||
|
↓
|
||||||
|
replay last utterance/fallback
|
||||||
|
↓
|
||||||
|
DO NOT reopen LangGraph
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The interruption policy checks terminal-session metadata before treating an input as a new intent. When terminal speech is available, it uses `last_assistant_text`/`terminal_replay_text`; otherwise it may use `POST_FINALIZE_REPLAY_MESSAGE`.
|
||||||
|
|
||||||
|
The purpose is to protect the logical end of a session, especially on voice channels where audio packets may arrive after the finalization event.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent: "The interaction is complete."
|
||||||
|
→ session finalized
|
||||||
|
|
||||||
|
late fragment arrives: "uh..."
|
||||||
|
→ replay "The interaction is complete."
|
||||||
|
→ no new routing / tool / LLM call
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- If terminal state is not persisted, another replica may reopen the journey.
|
||||||
|
- Do not replay technical/JSON envelopes as user-facing speech.
|
||||||
|
- This feature does not replace an intentional new-session policy.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/channels/interruption.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/config/settings.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
171
docs/features/15_retrieval_tool_guardrails.md
Normal file
171
docs/features/15_retrieval_tool_guardrails.md
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
# Guardrails de Retrieval e Tools / Retrieval / Tool Guardrails
|
||||||
|
|
||||||
|
> Feature do `agent_framework_oci` — guia bilíngue PT-BR / EN.
|
||||||
|
|
||||||
|
**Implementação principal / Main implementation:** `guardrails/pipeline.py + guardrails/rails.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Português (PT-BR)
|
||||||
|
|
||||||
|
### 1. O que é
|
||||||
|
|
||||||
|
Aplica proteção não apenas na mensagem do usuário e na resposta final, mas também no conhecimento recuperado por RAG e nos argumentos/resultados de ferramentas.
|
||||||
|
|
||||||
|
### 2. Problema que resolve
|
||||||
|
|
||||||
|
Em agentes de produção, não é suficiente pedir ao LLM que “faça a coisa certa”. Esta feature move uma responsabilidade específica para uma camada controlada do framework, reduzindo comportamento imprevisível e código duplicado nos agentes de domínio.
|
||||||
|
|
||||||
|
### 3. Fluxo simplificado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário
|
||||||
|
↓
|
||||||
|
Input Guardrails
|
||||||
|
↓
|
||||||
|
RAG → Retrieval Guardrails
|
||||||
|
↓
|
||||||
|
LLM/Tool call → Tool Guardrails
|
||||||
|
↓
|
||||||
|
API
|
||||||
|
↓
|
||||||
|
Output Guardrails
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Como funciona internamente
|
||||||
|
|
||||||
|
O framework possui stages distintos de guardrails. Para retrieval, rails como `RAGSEC` e `RET_REL` podem validar segurança e relevância do conteúdo recuperado. Para tools, `TOOL_VAL` valida o uso/argumentos antes ou ao redor da execução.
|
||||||
|
|
||||||
|
As configurações globais incluem `ENABLE_INPUT_GUARDRAILS`, `ENABLE_OUTPUT_GUARDRAILS`, `ENABLE_PARALLEL_GUARDRAILS`, `GUARDRAILS_FAIL_FAST` e `GUARDRAILS_CONFIG_PATH`. O YAML é a fonte de verdade dos rails ativados por agente.
|
||||||
|
|
||||||
|
### 5. Como ativar/configurar
|
||||||
|
|
||||||
|
A ativação exata depende do template/agente. Verifique o arquivo de settings, YAMLs de configuração e o template usado pelo serviço. Nem toda feature precisa de uma flag global: algumas são ativadas pelo contrato retornado por uma tool/workflow.
|
||||||
|
|
||||||
|
### 6. Exemplo
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
retrieval:
|
||||||
|
rails:
|
||||||
|
- RAGSEC
|
||||||
|
- RET_REL
|
||||||
|
|
||||||
|
tool:
|
||||||
|
rails:
|
||||||
|
- TOOL_VAL
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplo: a pergunta é sobre cancelamento de um serviço, mas o RAG retorna documentação de modem. `RET_REL` pode rejeitar o contexto antes que ele seja usado na resposta.
|
||||||
|
|
||||||
|
### 7. Telemetria e observabilidade
|
||||||
|
|
||||||
|
Quando a feature participa de uma execução de agente, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id` e demais chaves de correlação no estado/eventos. Isso permite acompanhar a decisão no Langfuse/Observer sem colocar lógica de observabilidade dentro do domínio.
|
||||||
|
|
||||||
|
### 8. Como testar
|
||||||
|
|
||||||
|
1. Crie um teste unitário do comportamento principal.
|
||||||
|
2. Crie um teste de integração do runtime quando houver estado entre turns.
|
||||||
|
3. Verifique o caso feliz e pelo menos um caso de falha/negação.
|
||||||
|
4. Confirme que não há side effects duplicados em retry/replay quando a feature toca transações.
|
||||||
|
5. Em produção, valide também telemetria e correlação de IDs.
|
||||||
|
|
||||||
|
### 9. Erros comuns
|
||||||
|
|
||||||
|
- Ter a implementação do rail não significa que ele está ativo: confira `guardrails.yaml`.
|
||||||
|
- Fail-fast deve ser escolhido conscientemente para cada stage.
|
||||||
|
- Tool guardrail não substitui validação de negócio dentro da própria API/action.
|
||||||
|
|
||||||
|
### 10. Relação com outras features
|
||||||
|
|
||||||
|
Esta feature deve ser usada junto das demais capacidades horizontais do framework, em vez de criar uma implementação paralela no agente de domínio. Em fluxos transacionais, considere especialmente **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery** e **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Referências no repositório
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/guardrails/pipeline.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/guardrails/rails.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English (EN)
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Applies safety and validation not only to user input and final output, but also to RAG-retrieved knowledge and tool arguments/results.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
User
|
||||||
|
↓
|
||||||
|
Input Guardrails
|
||||||
|
↓
|
||||||
|
RAG → Retrieval Guardrails
|
||||||
|
↓
|
||||||
|
LLM/Tool call → Tool Guardrails
|
||||||
|
↓
|
||||||
|
API
|
||||||
|
↓
|
||||||
|
Output Guardrails
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The framework has distinct guardrail stages. For retrieval, rails such as `RAGSEC` and `RET_REL` can validate retrieved-content safety and relevance. For tools, `TOOL_VAL` validates usage/arguments before or around execution.
|
||||||
|
|
||||||
|
Global settings include `ENABLE_INPUT_GUARDRAILS`, `ENABLE_OUTPUT_GUARDRAILS`, `ENABLE_PARALLEL_GUARDRAILS`, `GUARDRAILS_FAIL_FAST`, and `GUARDRAILS_CONFIG_PATH`. The agent YAML is the source of truth for enabled rails.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
retrieval:
|
||||||
|
rails:
|
||||||
|
- RAGSEC
|
||||||
|
- RET_REL
|
||||||
|
|
||||||
|
tool:
|
||||||
|
rails:
|
||||||
|
- TOOL_VAL
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: the question concerns canceling a service, but RAG retrieves modem documentation. `RET_REL` can reject that context before it is used in the answer.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Having a rail implementation does not mean it is enabled: check `guardrails.yaml`.
|
||||||
|
- Fail-fast behavior should be chosen intentionally for each stage.
|
||||||
|
- Tool guardrails do not replace business validation inside the API/action itself.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/guardrails/pipeline.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/guardrails/rails.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
62
docs/features/README.md
Normal file
62
docs/features/README.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Feature Guide — agent_framework_oci
|
||||||
|
|
||||||
|
Guia consolidado e bilíngue das principais capabilities horizontais do framework. Cada página contém **Português (PT-BR)** e **English (EN)** no mesmo arquivo.
|
||||||
|
|
||||||
|
Consolidated bilingual guide for the framework's main horizontal capabilities. Each page contains **Portuguese (PT-BR)** and **English (EN)** in the same file.
|
||||||
|
|
||||||
|
| # | Feature |
|
||||||
|
|---:|---|
|
||||||
|
| 01 | [Autenticação / Authentication](01_authentication.md) |
|
||||||
|
| 02 | [Workflow Transacional Determinístico / Deterministic Transactional Workflow](02_deterministic_transactional_workflow.md) |
|
||||||
|
| 03 | [Composição por LLM Solicitada pelo Domínio / Domain Requested LLM Composition](03_domain_requested_llm_composition.md) |
|
||||||
|
| 04 | [RAG Solicitado pelo Domínio / Domain Requested RAG](04_domain_requested_rag.md) |
|
||||||
|
| 05 | [Memória de Longo Prazo / Long Term Memory](05_long_term_memory.md) |
|
||||||
|
| 06 | [Regressão Offline de Workflow / Offline Workflow Regression](06_offline_workflow_regression.md) |
|
||||||
|
| 07 | [Pause / Resume de Workflow / Pause / Resume Workflow](07_pause_resume_workflow.md) |
|
||||||
|
| 08 | [Aderência de Rota / Route Stickiness](08_route_stickiness.md) |
|
||||||
|
| 09 | [Replay em Interrupções de Voz / Voice Interruption Replay](09_voice_interruption_replay.md) |
|
||||||
|
| 10 | [Recuperação de Erro em Workflow / Workflow Error Recovery](10_workflow_error_recovery.md) |
|
||||||
|
| 11 | [Clarificação / Clarification](11_clarification.md) |
|
||||||
|
| 12 | [Idempotência Durável / Durable Idempotency](12_durable_idempotency.md) |
|
||||||
|
| 13 | [Estados Transacionais Dinâmicos / Dynamic Transaction States](13_dynamic_transaction_states.md) |
|
||||||
|
| 14 | [Replay Após Finalização / Post Finalization Replay](14_post_finalization_replay.md) |
|
||||||
|
| 15 | [Guardrails de Retrieval e Tools / Retrieval / Tool Guardrails](15_retrieval_tool_guardrails.md) |
|
||||||
|
|
||||||
|
## Mapa conceitual / Conceptual map
|
||||||
|
|
||||||
|
```text
|
||||||
|
LLM
|
||||||
|
│
|
||||||
|
├── Conversation
|
||||||
|
│ ├── Clarification
|
||||||
|
│ ├── Route Stickiness
|
||||||
|
│ └── Long Term Memory
|
||||||
|
│
|
||||||
|
├── Knowledge
|
||||||
|
│ ├── Domain Requested RAG
|
||||||
|
│ └── Retrieval Guardrails
|
||||||
|
│
|
||||||
|
├── Transactions
|
||||||
|
│ ├── Deterministic Transactional Workflow
|
||||||
|
│ ├── Pause / Resume
|
||||||
|
│ ├── Dynamic Transaction States
|
||||||
|
│ ├── Durable Idempotency
|
||||||
|
│ └── Workflow Error Recovery
|
||||||
|
│
|
||||||
|
├── Response
|
||||||
|
│ └── Domain Requested LLM Composition
|
||||||
|
│
|
||||||
|
├── Voice
|
||||||
|
│ ├── Voice Interruption Replay
|
||||||
|
│ └── Post Finalization Replay
|
||||||
|
│
|
||||||
|
└── Platform
|
||||||
|
├── Authentication
|
||||||
|
└── Offline Workflow Regression
|
||||||
|
```
|
||||||
|
|
||||||
|
## Princípio de arquitetura / Architecture principle
|
||||||
|
|
||||||
|
**PT-BR:** o LLM entende e redige; o framework controla estado, segurança, memória, roteamento e transações; o domínio contém regras específicas de negócio.
|
||||||
|
|
||||||
|
**EN:** the LLM understands and writes; the framework controls state, security, memory, routing, and transactions; the domain contains business-specific rules.
|
||||||
83
docs/features/en/01_authentication.md
Normal file
83
docs/features/en/01_authentication.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `security/authentication.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Checks who may access protected APIs, gateways, and services before the request reaches the agent.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Client/System
|
||||||
|
↓
|
||||||
|
Authentication Provider
|
||||||
|
↓
|
||||||
|
valid credential?
|
||||||
|
├─ no → 401/deny
|
||||||
|
└─ yes → authenticated principal → agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The framework exposes an `AuthenticationProvider` abstraction with multiple implementations. Current providers include `NoAuthenticationProvider`, `DenyAuthenticationProvider`, `BasicAuthenticationProvider`, `ApiKeyAuthenticationProvider`, `StaticBearerAuthenticationProvider`, `JwtAuthenticationProvider`, `OAuth2IntrospectionAuthenticationProvider`, and `TrustedProxyAuthenticationProvider`.
|
||||||
|
|
||||||
|
Authentication produces an `AuthenticatedPrincipal` containing `subject`, `scheme`, and optional `claims`. Domain code should not validate credentials directly.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.security.authentication import BasicAuthenticationProvider
|
||||||
|
|
||||||
|
provider = BasicAuthenticationProvider(
|
||||||
|
client_id="client-a",
|
||||||
|
secret_hash="pbkdf2_sha256:...",
|
||||||
|
)
|
||||||
|
result = await provider.authenticate(request)
|
||||||
|
if not result.authenticated:
|
||||||
|
# deny access
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
Secrets may be verified as plain, SHA-256, or PBKDF2 values; for production, prefer strong hashes and managed secret stores.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Basic auth returns 401: validate the `Authorization: Basic ...` header and configured secret.
|
||||||
|
- Do not confuse API authentication with `OCI_AUTH_MODE`; they solve different problems.
|
||||||
|
- Avoid `NoAuthenticationProvider` in production unless explicitly accepted by architecture.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/security/authentication.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
92
docs/features/en/02_deterministic_transactional_workflow.md
Normal file
92
docs/features/en/02_deterministic_transactional_workflow.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Deterministic Transactional Workflow
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `workflows/runtime.py + mcp/tool_policy.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Ensures state-changing operations follow predictable steps with confirmation and execution control instead of depending on LLM creativity.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Customer message
|
||||||
|
↓
|
||||||
|
LLM understands intent
|
||||||
|
↓
|
||||||
|
Tool policy = transactional
|
||||||
|
↓
|
||||||
|
Deterministic workflow
|
||||||
|
↓
|
||||||
|
confirmation
|
||||||
|
↓
|
||||||
|
controlled execution
|
||||||
|
↓
|
||||||
|
result
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The LLM may help interpret intent and extract parameters, but it should not decide the critical sequence of a transaction. `ToolPolicyRegistry` classifies tools, and `operation_type: transactional` activates transactional behavior. `WorkflowRuntime` executes the workflow, preserves state, and integrates pause/resume and error recovery.
|
||||||
|
|
||||||
|
`ENABLE_TRANSACTIONAL_WORKFLOWS` controls the capability globally, while `WORKFLOWS_PATH` points to workflow YAML files.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
tools:
|
||||||
|
cancel_service:
|
||||||
|
operation_type: transactional
|
||||||
|
requires_confirmation: true
|
||||||
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
1. locate service
|
||||||
|
2. validate eligibility
|
||||||
|
3. ask for confirmation
|
||||||
|
4. PAUSE
|
||||||
|
5. receive confirmation
|
||||||
|
6. RESUME
|
||||||
|
7. execute side effect
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Marking a write tool as `read_only` bypasses transactional protections.
|
||||||
|
- Re-running steps before a pause can duplicate side effects; use the official runtime.
|
||||||
|
- Do not use prompts as the only confirmation guarantee.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/mcp/tool_policy.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
79
docs/features/en/03_domain_requested_llm_composition.md
Normal file
79
docs/features/en/03_domain_requested_llm_composition.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Domain Requested LLM Composition
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Lets domain logic compute the authoritative result and ask the LLM only to compose the final user-facing response.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Domain logic computes
|
||||||
|
↓
|
||||||
|
requires_llm_composition=true
|
||||||
|
↓
|
||||||
|
framework prevents direct MCP answer
|
||||||
|
↓
|
||||||
|
official LLMProvider
|
||||||
|
↓
|
||||||
|
natural-language response
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The domain returns authoritative data plus a composition instruction. `AgentRuntimeMixin` recursively detects `requires_llm_composition` in tool/workflow results and avoids terminating through the direct MCP-answer path. Composition then uses the agent's official LLM provider, preserving profiles, tracing, usage accounting, and framework policies.
|
||||||
|
|
||||||
|
The LLM should compose language, not recalculate values or override already-resolved business rules.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"refund_amount": "38.00",
|
||||||
|
"requires_llm_composition": true,
|
||||||
|
"response_instruction": "Explain the refund using only the computed values."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- An overly broad instruction may let the LLM add unauthorized content.
|
||||||
|
- Do not delegate deterministic calculations back to the LLM.
|
||||||
|
- If free-form wording is unnecessary, prefer a deterministic direct response.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
84
docs/features/en/04_domain_requested_rag.md
Normal file
84
docs/features/en/04_domain_requested_rag.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Domain Requested RAG
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows a tool or workflow to declare that external knowledge retrieval is required even when an MCP result already exists.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tool/Workflow
|
||||||
|
↓
|
||||||
|
requires_rag=true
|
||||||
|
↓
|
||||||
|
rag_query / rag_queries
|
||||||
|
↓
|
||||||
|
framework RagService
|
||||||
|
↓
|
||||||
|
Retrieval Guardrails
|
||||||
|
↓
|
||||||
|
LLM/response
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Normally the framework may skip RAG when MCP already provides sufficient data (`SKIP_RAG_WHEN_MCP_SUFFICIENT`). This feature lets the domain override that decision for a specific case. A result may declare `requires_rag`, `rag_query`, or `rag_queries`; the runtime uses those queries as overrides and invokes `RagService`.
|
||||||
|
|
||||||
|
The domain declares **what knowledge is needed**. It does not implement its own vector client, retriever, or parallel RAG prompt stack.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_rag": true,
|
||||||
|
"rag_queries": [
|
||||||
|
"How to cancel YouTube Premium?",
|
||||||
|
"How to cancel Aya Books?"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Related settings include `RAG_TOP_K` and `SKIP_RAG_WHEN_MCP_SUFFICIENT`.
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Requesting RAG for transactional facts already resolved by an API adds unnecessary cost and latency.
|
||||||
|
- Queries that are too broad reduce relevance.
|
||||||
|
- Do not trust retrieved content for critical responses without Retrieval Guardrails.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
84
docs/features/en/05_long_term_memory.md
Normal file
84
docs/features/en/05_long_term_memory.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Long Term Memory
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `memory/long_term_memory.py + memory/long_term_store.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows useful information to persist across different sessions without depending on the full transcript of a previous conversation.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Session A
|
||||||
|
↓
|
||||||
|
extract relevant memory
|
||||||
|
↓
|
||||||
|
Long Term Memory Store
|
||||||
|
↓
|
||||||
|
... days later ...
|
||||||
|
↓
|
||||||
|
Session B
|
||||||
|
↓
|
||||||
|
retrieve relevant context
|
||||||
|
↓
|
||||||
|
agent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Long-term memory is different from message history and checkpoints. It persists useful facts/preferences and retrieves them as context for a future session. The framework supports `memory`, `sqlite`, `autonomous`, and `oracle` providers.
|
||||||
|
|
||||||
|
Important settings include `ENABLE_LONG_TERM_MEMORY`, `LONG_TERM_MEMORY_PROVIDER`, `LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS`, `LONG_TERM_MEMORY_MIN_CONFIDENCE`, `LONG_TERM_MEMORY_AUTO_EXTRACT`, and `LONG_TERM_MEMORY_INJECT_CONTEXT`.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=oracle
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Do not confuse LTM with replaying the entire transcript.
|
||||||
|
- Irrelevant or low-confidence memories should not be injected.
|
||||||
|
- For multiple replicas, prefer shared durable storage over local memory.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_memory.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/memory/long_term_store.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
82
docs/features/en/06_offline_workflow_regression.md
Normal file
82
docs/features/en/06_offline_workflow_regression.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Offline Workflow Regression
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `workflows/runtime.py + Tuning-Performance/Offline_Workflow_Regression`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows workflow logic to be regression-tested without requiring the full production infrastructure.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Test
|
||||||
|
↓
|
||||||
|
explicit deterministic test backend
|
||||||
|
↓
|
||||||
|
run → PAUSED
|
||||||
|
↓
|
||||||
|
resume → COMPLETED
|
||||||
|
↓
|
||||||
|
state/side-effect assertions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
`WorkflowRuntime` includes an **explicitly opt-in deterministic/offline test backend**. When `allow_deterministic_fallback=True`, this backend is explicitly selected even if LangGraph is installed, keeping regression results reproducible across developer machines and CI. It can validate DSL rules, conditions, pause/resume behavior, and duplicate-execution protection without depending on LangGraph internals, a database, OCI, or external APIs.
|
||||||
|
|
||||||
|
Production behavior still uses LangGraph. Offline mode must never become a silent fallback when LangGraph fails or is unavailable in production.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
run(workflow)
|
||||||
|
action_a = executed once
|
||||||
|
status = PAUSED
|
||||||
|
|
||||||
|
resume(workflow)
|
||||||
|
action_a remains executed once
|
||||||
|
action_b = executed once
|
||||||
|
status = COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Using the offline backend in production hides real issues.
|
||||||
|
- Over-mocking can stop the test from validating real DSL behavior.
|
||||||
|
- Failing to assert pre-pause side effects may hide duplicate execution.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/Tuning-Performance/Offline_Workflow_Regression`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
83
docs/features/en/07_pause_resume_workflow.md
Normal file
83
docs/features/en/07_pause_resume_workflow.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Resume de Workflow / Pause / Resume Workflow
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `workflows/runtime.py + workflows/graph.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Allows a workflow to stop at a safe point, persist state, and continue later using user input or another event.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow
|
||||||
|
↓
|
||||||
|
pre-pause actions
|
||||||
|
↓
|
||||||
|
PAUSE
|
||||||
|
↓
|
||||||
|
checkpoint/state
|
||||||
|
↓
|
||||||
|
new message
|
||||||
|
↓
|
||||||
|
RESUME
|
||||||
|
↓
|
||||||
|
remaining actions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
`WorkflowRuntime` exposes `arun(...)` and `aresume(...)`. The pause node is separated from the preceding action so previous side effects are not executed again on resume. The same `execution_id/thread_id` identifies the paused and resumed execution.
|
||||||
|
|
||||||
|
The runtime supports declarative conditions such as `all`, `any`, `not`, `eq`, `neq`, and `exists`, so pause/continue decisions do not need to live in the prompt.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
status = await runtime.arun(...)
|
||||||
|
# status == PAUSED
|
||||||
|
|
||||||
|
status = await runtime.aresume(execution_id, input={"confirmed": true})
|
||||||
|
# status == COMPLETED
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Losing the `execution_id` prevents resuming the right execution.
|
||||||
|
- Restarting the workflow from scratch after confirmation may duplicate side effects.
|
||||||
|
- Pause without shared checkpoint/state storage is fragile across multiple replicas.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/runtime.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/workflows/graph.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
76
docs/features/en/08_route_stickiness.md
Normal file
76
docs/features/en/08_route_stickiness.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
# Route Stickiness
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `routing/enterprise_router.py + runtime/agent_runtime.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Prevents short follow-up messages from unnecessarily switching the conversation to another agent.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
current message
|
||||||
|
+ short history
|
||||||
|
+ previous route
|
||||||
|
↓
|
||||||
|
semantic continuity
|
||||||
|
↓
|
||||||
|
keep route or handoff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
Route Stickiness evaluates whether a new message is semantically continuous with the current subject/agent. It reduces agent ping-pong for messages such as “what about that amount?”, “yes”, “the second one”, or “and last month?”.
|
||||||
|
|
||||||
|
Existing settings include `ENABLE_ROUTE_STICKINESS`, `ROUTE_STICKINESS_LLM_PROFILE`, `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`, `ROUTE_STICKINESS_HISTORY_TURNS`, and `ROUTE_STICKINESS_MAX_TOKENS`. The decision may still allow handoff when there is enough evidence of a topic change.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- A threshold that is too low may trap the user on the wrong agent.
|
||||||
|
- A threshold that is too high may lose continuity on short follow-ups.
|
||||||
|
- Stickiness should not block explicit handoff when the user clearly changes intent.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/routing/enterprise_router.py`
|
||||||
|
- `libs/agent_framework/src/agent_framework/runtime/agent_runtime.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
77
docs/features/en/09_voice_interruption_replay.md
Normal file
77
docs/features/en/09_voice_interruption_replay.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Voice Interruption Replay
|
||||||
|
|
||||||
|
> `agent_framework_oci` feature — English guide.
|
||||||
|
|
||||||
|
**Main implementation:** `channels/interruption.py`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. What it is
|
||||||
|
|
||||||
|
Decides whether audio received while the agent is speaking represents a new intent, a backchannel/noise event, or something that should simply replay/continue the previous speech.
|
||||||
|
|
||||||
|
### 2. Problem it solves
|
||||||
|
|
||||||
|
Production agents should not rely on prompts alone to “do the right thing”. This feature moves a specific responsibility into a controlled framework layer, reducing unpredictable behavior and duplicate domain-agent code.
|
||||||
|
|
||||||
|
### 3. Simplified flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
audio during speech
|
||||||
|
↓
|
||||||
|
InterruptionPolicy
|
||||||
|
├─ process → new message
|
||||||
|
├─ classify → lightweight classifier
|
||||||
|
└─ replay → previous speech
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. How it works internally
|
||||||
|
|
||||||
|
The policy lives in the framework rather than domain code. It distinguishes terminal sessions, `idle_nudge`, non-interruptible speech, and potentially interruptible speech. When needed, it may use a lightweight classifier backed by `LLMProvider`; on classification failure, it can fail safely to replay.
|
||||||
|
|
||||||
|
The goal is to prevent “uh-huh”, noise, echo, or residual audio fragments from being interpreted as a full new intent.
|
||||||
|
|
||||||
|
### 5. How to enable/configure
|
||||||
|
|
||||||
|
Exact activation depends on the template/agent. Check framework settings, YAML configuration, and the service template. Not every feature requires a global flag: some are activated by the contract returned from a tool/workflow.
|
||||||
|
|
||||||
|
### 6. Example
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent: "Your invoice contains..."
|
||||||
|
User: "uh-huh"
|
||||||
|
→ replay/continue
|
||||||
|
|
||||||
|
Agent: "Your invoice contains..."
|
||||||
|
User: "wait, I want to ask something else"
|
||||||
|
→ process new intent
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. Telemetry and observability
|
||||||
|
|
||||||
|
When the feature participates in an agent execution, preserve `request_id`, `trace_id`, `session_id`, `agent_id`, `message_id`, and other correlation keys in state/events. This makes the decision observable through Langfuse/Observer without embedding observability logic in the domain.
|
||||||
|
|
||||||
|
### 8. How to test
|
||||||
|
|
||||||
|
1. Add a unit test for the core behavior.
|
||||||
|
2. Add a runtime integration test when state spans multiple turns.
|
||||||
|
3. Test the happy path and at least one failure/rejection path.
|
||||||
|
4. Confirm retries/replays do not duplicate side effects for transactional features.
|
||||||
|
5. In production, also validate telemetry and ID correlation.
|
||||||
|
|
||||||
|
### 9. Common mistakes
|
||||||
|
|
||||||
|
- Sending every noise fragment to an LLM increases latency and cost.
|
||||||
|
- Allowing interruption during non-interruptible transactional speech may corrupt UX/state.
|
||||||
|
- Replay should use a real previous utterance, not a technical envelope.
|
||||||
|
|
||||||
|
### 10. Relationship with other features
|
||||||
|
|
||||||
|
Use this feature together with the framework's horizontal capabilities rather than creating a parallel implementation in domain-agent code. For transactional journeys, pay special attention to **Clarification**, **Pause/Resume**, **Durable Idempotency**, **Workflow Error Recovery**, and **Guardrails**.
|
||||||
|
|
||||||
|
### 11. Repository references
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/channels/interruption.py`
|
||||||
|
- `Tuning-Performance/`
|
||||||
|
- `Documentacao/`
|
||||||
|
- `libs/agent_framework/docs/`
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user