Compare commits

...

4 Commits

351 changed files with 13596 additions and 1117 deletions

209
.env
View File

@@ -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

View 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.

View 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`.

View File

@@ -3,5 +3,8 @@ from __future__ import annotations
# Compatibilidade local do template/backend.
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]

View File

@@ -0,0 +1,3 @@
from .tool_renderers import register_tool_renderers
__all__ = ["register_tool_renderers"]

View File

@@ -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)

View File

@@ -10,6 +10,9 @@ tools:
- fatura
- conta
- boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
@@ -28,6 +31,9 @@ tools:
asset_id: string
selection_keywords:
- plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
@@ -49,6 +55,9 @@ tools:
- consultar pedido
- status do pedido
- pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
@@ -61,6 +70,9 @@ tools:
- rastreamento
- transportadora
- previsão
response:
mode: renderer
renderer: retail.delivery
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail

View File

@@ -0,0 +1,3 @@
from .tool_renderers import register_tool_renderers
__all__ = ["register_tool_renderers"]

View File

@@ -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)

View File

@@ -10,6 +10,9 @@ tools:
- fatura
- conta
- boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
@@ -28,6 +31,9 @@ tools:
asset_id: string
selection_keywords:
- plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
@@ -49,6 +55,9 @@ tools:
- consultar pedido
- status do pedido
- pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
@@ -61,6 +70,9 @@ tools:
- rastreamento
- transportadora
- previsão
response:
mode: renderer
renderer: retail.delivery
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail

View File

@@ -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.

View 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()`.

View File

@@ -3,5 +3,8 @@ from __future__ import annotations
# Compatibilidade local do template/backend.
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]

View File

@@ -0,0 +1,3 @@
from .tool_renderers import register_tool_renderers
__all__ = ["register_tool_renderers"]

View File

@@ -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)

View File

@@ -10,6 +10,9 @@ tools:
- fatura
- conta
- boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
@@ -28,6 +31,9 @@ tools:
asset_id: string
selection_keywords:
- plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
@@ -49,6 +55,9 @@ tools:
- consultar pedido
- status do pedido
- pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
@@ -61,6 +70,9 @@ tools:
- rastreamento
- transportadora
- previsão
response:
mode: renderer
renderer: retail.delivery
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail

View File

@@ -3,5 +3,8 @@ from __future__ import annotations
# Compatibilidade local do template/backend.
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]

View File

@@ -0,0 +1,3 @@
from .tool_renderers import register_tool_renderers
__all__ = ["register_tool_renderers"]

View File

@@ -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)

View File

@@ -10,6 +10,9 @@ tools:
- fatura
- conta
- boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
@@ -28,6 +31,9 @@ tools:
asset_id: string
selection_keywords:
- plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
@@ -49,6 +55,9 @@ tools:
- consultar pedido
- status do pedido
- pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
@@ -61,6 +70,9 @@ tools:
- rastreamento
- transportadora
- previsão
response:
mode: renderer
renderer: retail.delivery
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail

View 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.

View File

@@ -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.

View 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`.

View File

@@ -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())

View File

@@ -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

View File

@@ -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

View File

@@ -3,5 +3,8 @@ from __future__ import annotations
# Compatibilidade local do template/backend.
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]

View File

@@ -0,0 +1,3 @@
from .tool_renderers import register_tool_renderers
__all__ = ["register_tool_renderers"]

View File

@@ -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)

View File

@@ -10,6 +10,9 @@ tools:
- fatura
- conta
- boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom
@@ -28,6 +31,9 @@ tools:
asset_id: string
selection_keywords:
- plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos:
description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom
@@ -49,6 +55,9 @@ tools:
- consultar pedido
- status do pedido
- pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega:
description: Consulta entrega e rastreamento do pedido.
mcp_server: retail
@@ -61,6 +70,9 @@ tools:
- rastreamento
- transportadora
- previsão
response:
mode: renderer
renderer: retail.delivery
solicitar_troca:
description: Simula abertura de solicitação de troca.
mcp_server: retail

View 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.

View 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
```

View File

@@ -0,0 +1 @@
gru.ocir.io/idi1o0a010nx/agent-framework-loadtest/agent-template-backend:loadtest

View 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

View 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.

View 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://...

View 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

View 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.

View 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.

View 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

View 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

View 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

View 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));
}

View File

View 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

View 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

View 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"

View 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

View 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"

View 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"

View 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

View 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."

View 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"

View 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."

View 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."

View 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."

View 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()

View 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"

View 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."

View 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"

View 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"

View 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

View 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"

View 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)"

View 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

View 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"

View 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

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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
View 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.

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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/`

View 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