Compare commits

...

45 Commits

Author SHA1 Message Date
7a22b7af11 new feature: frontend with separated responses 2026-09-11 11:59:46 -03:00
e882afde25 new feature: frontend with separated responses 2026-09-11 10:45:49 -03:00
f00c2f721a new feature: frontend with separated responses 2026-09-11 09:33:50 -03:00
f62c7993bb new feature: Multi-Intent 2026-09-11 00:34:56 -03:00
3f884d028a new feature: Multi-Intent 2026-09-10 22:55:50 -03:00
05410adf09 new feature: Multi-Intent 2026-09-10 16:12:04 -03:00
ffe47e8d49 ajustes no manual 2026-09-10 09:48:25 -03:00
ae5887ce50 adjustment: load-tester 2026-09-09 13:51:05 -03:00
82636ec15b bugfix: extractor cases 8 and 16 2026-09-08 10:13:03 -03:00
1711575abe bugfix reconciliation extractor 2026-09-07 22:57:36 -03:00
2c3aa58b59 bugfix reconciliation extractor 2026-09-07 20:16:57 -03:00
d84d833694 nova funcionalidade: reconciliacao temporal 2026-09-02 13:04:56 -03:00
fe8c18093f Ajustes no template para definicao de campos para extracao 2026-09-01 20:58:51 -03:00
T3782834
f4638e287a ajuste no contas 2026-09-01 11:23:22 -03:00
T3782834
e24963abe1 ajuste no guardraild COE 2026-08-31 21:11:51 -03:00
10a6f357dd Conferencia de parametros coletados na transacao 2026-08-31 17:27:35 -03:00
c8c83981c4 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 21:04:31 -03:00
83bf961185 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 11:22:10 -03:00
81f24d7357 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 10:03:42 -03:00
1fd18531c0 Documentation organization 2026-08-27 21:11:01 -03:00
df2a1cbe1b bugfix: oci_openai provider 2026-08-27 20:30:27 -03:00
42ab000c7b Documentation organization 2026-08-27 10:28:23 -03:00
472d44074c Documentation organization 2026-08-27 09:41:46 -03:00
T3782834
faf5ca55ba new feature: Integration with kbdb Autonomous 2026-08-26 10:15:51 -03:00
ac18d68eaf new feature: External guardrails/judges 2026-08-24 11:55:07 -03:00
63d0fb51c4 new feature: External guardrails/judges 2026-08-24 11:26:08 -03:00
T3782834
a472daa1e4 adjustments: transaction parameter extraction 2026-08-21 23:06:54 -03:00
T3782834
d93efd8972 adjustments: transaction parameter extraction 2026-08-21 22:44:36 -03:00
727997aa41 Adding transaction state 2026-08-21 17:59:06 -03:00
2496e831a1 Adding transaction state 2026-08-21 16:47:13 -03:00
b5d2a12953 bugfixes: router stickness vs transaction workflow parameters 2026-08-21 14:34:30 -03:00
bdc44b15d6 bugfix: enviar subject para contexto do guardrail 2026-08-21 11:01:48 -03:00
05077a91bc bugfix: aofertas 2026-08-21 10:47:19 -03:00
19c4024693 new feature: reasoning_content for compatible models in ainvoke_response() 2026-08-21 07:40:21 -03:00
22e8f56146 bugfixes: router stickness vs transaction workflow parameters 2026-08-20 23:29:56 -03:00
0a72afb94e bugfixes: router stickness vs transaction workflow parameters 2026-08-20 22:58:25 -03:00
57df723a24 new feature: mcp pre-validation 2026-08-20 15:13:20 -03:00
6e43cc0158 adjustments: route stickness usefull tokens only 2026-08-20 13:14:57 -03:00
d271031198 adjustments: route stickness usefull tokens only 2026-08-20 11:44:24 -03:00
34d33350ae new feature: transaction evidence 2026-08-20 09:52:22 -03:00
ad778f6889 bugfix: route stickness precedences (transaction in the same intent) 2026-08-20 09:18:58 -03:00
762f6fb024 bugfixes: transaction parameter collector, generic formatting messages, guardrails, prompts. Testing contas 2026-08-20 00:23:50 -03:00
f9c66b4792 Melhorias no guardrails de FRASEOLOGY. Melhorias no Transaction Workflow 2026-08-19 16:08:03 -03:00
560e79d21b New features: Domain_Requested_LLM_Composition, Domain_Requested_RAG, Offline_Workflow_Regression, Pause_Resume_Workflow, Voice_Interruption_Replay, Workflow_Error_Recovery, Durable Idempotency, Workflow_Pause_Resume, Dynamic_Transaction_States, Post_Finalization_Replay, Retrieval_Tool_Guardrails 2026-08-19 09:25:38 -03:00
23d32bbcfc Testes de Carga: /deploy/oke/load-test 2026-08-17 18:59:14 -03:00
1148 changed files with 108525 additions and 3478 deletions

View File

@@ -207,3 +207,30 @@ LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70 LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
LONG_TERM_MEMORY_AUTO_EXTRACT=true LONG_TERM_MEMORY_AUTO_EXTRACT=true
LONG_TERM_MEMORY_INJECT_CONTEXT=true LONG_TERM_MEMORY_INJECT_CONTEXT=true
# Optional agent/deployment observability contract mapping.
# Keep disabled in the generic framework; agents may enable their own YAML mapping.
OBSERVABILITY_CODE_MAPPING_ENABLED=false
OBSERVABILITY_CODE_MAPPING_PATH=
###############################################################################
# RAG provider selection (mutually exclusive at runtime)
###############################################################################
# standard = RAG original do agent_framework_oci (default, backward compatible)
# kbdb = KBDB enterprise via PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE
RAG_PROVIDER=standard
# Somente usados quando RAG_PROVIDER=kbdb. Se vazios, credenciais caem para ADB_*.
KBDB_DB_USER=
KBDB_DB_PASSWORD=
KBDB_DB_DSN=
KBDB_DB_WALLET_LOCATION=
KBDB_DB_WALLET_PASSWORD=
KBDB_SEARCH_TYPE=hybrid
KBDB_NODE_EXPANSION=true
KBDB_NODE_MAX_RELATED=8
KBDB_GRAPH_CROSS_REF=false
KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON=
KBDB_MIN_SCORE=

92
.idea/workspace.xml generated
View File

@@ -50,21 +50,102 @@
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true", "ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
"ModuleVcsDetector.initialDetectionPerformed": "true", "ModuleVcsDetector.initialDetectionPerformed": "true",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager": "true",
"RunOnceActivity.git.unshallow": "true", "RunOnceActivity.git.unshallow": "true",
"RunOnceActivity.typescript.service.memoryLimit.init": "true", "RunOnceActivity.typescript.service.memoryLimit.init": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true", "SHARE_PROJECT_CONFIGURATION_FILES": "true",
"Shell Script.agent_frontend.executor": "Run",
"Shell Script.agent_template_backend.executor": "Run",
"Shell Script.mcp_gateway.executor": "Run",
"Shell Script.mcp_server.executor": "Run",
"git-widget-placeholder": "master", "git-widget-placeholder": "master",
"kotlin-language-version-configured": "true", "kotlin-language-version-configured": "true",
"last_opened_file_path": "D:/Dropbox/ORACLE/TIM/FY27/Wave_2/Ajuste para manter o agente atual ou voltar ao roteamento/agent_framework_oci", "last_opened_file_path": "C:/Asus_Projects/kbdb/agent_framework_oci/templates/agent_template_backend",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)", "node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm", "nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "configurable.group.appearance", "project.structure.last.edited": "Project",
"project.structure.proportion": "0.15",
"project.structure.side.proportion": "0.2",
"settings.editor.selected.configurable": "advanced.settings",
"vue.rearranger.settings.migration": "true" "vue.rearranger.settings.migration": "true"
} }
}]]></component> }]]></component>
<component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS">
<recent name="C:\Asus_Projects\kbdb\agent_framework_oci\templates\agent_template_backend" />
</key>
</component>
<component name="RunManager" selected="Shell Script.agent_template_backend">
<configuration name="agent_frontend" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; cd apps/agent_frontend &amp;&amp; python -m http.server 5173&quot;" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="powershell.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
<configuration name="agent_template_backend" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; cd templates/agent_template_backend &amp;&amp; python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload&quot;" />
<option name="INDEPENDENT_SCRIPT_PATH" value="false" />
<option name="SCRIPT_PATH" value="$PROJECT_DIR$/templates/agent_template_backend/app" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="false" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="powershell.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
<configuration name="mcp_gateway" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; cd scripts &amp;&amp; bash run_mcp_gateway.sh&quot;" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="powershell.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
<configuration name="mcp_server" type="ShConfigurationType">
<option name="SCRIPT_TEXT" value="wsl bash -lc &quot;source .venv/bin/activate &amp;&amp; cd scripts &amp;&amp; bash run_mcp_servers.sh&quot;" />
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
<option name="SCRIPT_PATH" value="" />
<option name="SCRIPT_OPTIONS" value="" />
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
<option name="INTERPRETER_PATH" value="powershell.exe" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="EXECUTE_IN_TERMINAL" value="true" />
<option name="EXECUTE_SCRIPT_FILE" value="false" />
<envs />
<method v="2" />
</configuration>
<list>
<item itemvalue="Shell Script.agent_template_backend" />
<item itemvalue="Shell Script.mcp_gateway" />
<item itemvalue="Shell Script.mcp_server" />
<item itemvalue="Shell Script.agent_frontend" />
</list>
</component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
@@ -89,9 +170,10 @@
<workItem from="1785245181314" duration="79000" /> <workItem from="1785245181314" duration="79000" />
<workItem from="1785245608367" duration="670000" /> <workItem from="1785245608367" duration="670000" />
<workItem from="1785326458404" duration="86000" /> <workItem from="1785326458404" duration="86000" />
<workItem from="1785414225783" duration="148000" /> <workItem from="1785405876905" duration="1300000" />
<workItem from="1785414447653" duration="704000" /> <workItem from="1785409583147" duration="1561000" />
<workItem from="1785630146329" duration="316000" /> <workItem from="1785452072692" duration="784000" />
<workItem from="1785453511270" duration="1006000" />
</task> </task>
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders"> <task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
<option name="closed" value="true" /> <option name="closed" value="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

@@ -0,0 +1,47 @@
# Release Notes — Generic Deterministic Intent Shift v15
## Problema corrigido
A Route Stickiness podia preservar a intent anterior quando a nova mensagem correspondia a uma intent configurada no `routing.yaml`, mas a frase do usuário omitia conectores curtos presentes na keyword configurada.
Exemplo real de configuração:
- keyword: `qual é o meu plano`
- mensagem: `qual o meu plano`
A classificação determinística não reconhecia a nova intent e a continuity acabava mantendo a intent anterior.
## Correção
O `EnterpriseRouter` continua usando, nesta ordem:
1. match exato;
2. sequência completa de tokens com palavras inseridas (`ordered_tokens`);
3. sequência de tokens informativos tolerando a omissão de conectores curtos presentes na keyword (`ordered_content_tokens`).
A terceira estratégia ignora, somente no lado da keyword, tokens de até dois caracteres e exige pelo menos dois tokens informativos. Não há nomes de intents, agentes, domínios ou verbos de negócio hardcoded.
Assim, a solução é dirigida integralmente pelas intents carregadas do `routing.yaml` da aplicação.
## Precedência sobre Route Stickiness
Quando o candidato determinístico encontrado é diferente da intent ativa, ele preempta a stickiness e retorna:
- `route_stickiness_preempted: true`
- `previous_agent`
- `previous_intent`
- `keyword_match_strategy`
A continuity LLM não é chamada nesse caminho.
## Casos cobertos
### Mesmo agente, nova intent
`retail_order_tracking` -> `quero cancelar meu pedido` -> `retail_order_cancel`
### Mesmo agente, tools diferentes
`contas_invoice_query` -> `qual o meu plano` -> `contas_plan_information`
Mesmo que ambas as intents usem `faturas_agent`, as tools mudam de `consultar_faturas` para `consultar_plano`.

View File

@@ -0,0 +1,41 @@
# Route Stickiness — deterministic intent shift (v14)
## Problema corrigido
Uma keyword multi-token como `cancelar pedido` não era reconhecida em frases como `quero cancelar meu pedido`. O match legado usava substring literal; assim, a keyword genérica `pedido` podia manter `retail_order_tracking` e a continuidade reutilizava a intent anterior.
## Correção
O `EnterpriseRouter` agora possui um segundo estágio determinístico para keywords multi-token: ordered-token matching com até três tokens intermediários. Não há chamada adicional de LLM.
Exemplos reconhecidos pela keyword configurada `cancelar pedido`:
- `quero cancelar meu pedido`
- `quero cancelar o meu pedido`
- `pode cancelar esse pedido`
- `gostaria de cancelar meu pedido`
Quando esse match identifica uma intent diferente da ativa, ele preempta a route stickiness antes do LLM de continuidade.
Metadados de auditoria esperados:
```json
{
"method": "keyword",
"intent": "retail_order_cancel",
"metadata": {
"matched_keyword": "cancelar pedido",
"keyword_match_strategy": "ordered_tokens",
"route_stickiness_preempted": true,
"previous_intent": "retail_order_tracking"
}
}
```
## Custo de LLM
Para mudança explícita reconhecida deterministicamente, o classificador LLM de continuity não é chamado. Para mensagens sem sinal explícito, a Route Stickiness continua com o comportamento configurado.
## Regressão
Testes cobrem mudança `retail_order_tracking -> retail_order_cancel` no mesmo `orders_agent`, inclusive com palavras intermediárias. A suíte relacionada passou com 18 testes.

View File

@@ -0,0 +1,30 @@
# Correção: escape de estado transacional / mudança de intenção
Correção aplicada em 2026-08-20 para impedir que uma sessão fique presa em `COLLECTING_PARAMETERS` ou `AWAITING_CONFIRMATION` quando o usuário muda explicitamente de assunto.
## Comportamento corrigido
Antes:
1. uma transação entrava em `COLLECTING_PARAMETERS`;
2. `next_state` forçava o mesmo agente via `state_policies`;
3. toda mensagem seguinte era tratada como tentativa de preencher o parâmetro faltante;
4. uma nova intenção como `quais sao meus servicos` permanecia presa no fluxo anterior.
Agora:
- o `EnterpriseRouter` verifica mudança explícita de intenção antes de aplicar o lock de estado;
- keyword explícita tem prioridade;
- quando necessário, o LLM router pode detectar mudança com confiança >= `router.confidence_threshold`;
- a decisão recebe `metadata.transaction_interruption=intent_shift`;
- o runtime encerra a transação pendente como `CANCELLED`, limpa `next_state`, parâmetros e latches, e prossegue com a nova intent;
- cancelamentos explícitos como `cancele essa operação anterior` funcionam também durante `COLLECTING_PARAMETERS`.
## Testes adicionados
- mudança de intent durante `COLLECTING_PARAMETERS`;
- resposta curta/baixa confiança permanece na transação;
- cancelamento explícito durante coleta de parâmetros;
- limpeza do estado transacional antes de executar a nova intent.
Testes focados: 19 passed.

View File

@@ -0,0 +1,102 @@
# Precedência transacional + extração LLM de parâmetros
Esta correção remove a extração textual hardcoded de parâmetros transacionais e faz a coleta de `policy.requires` por um extrator LLM genérico.
## Regra de precedência
Enquanto existir uma transação ativa, o framework trata o turno nesta ordem:
```text
ACTIVE_TRANSACTION
|
+-- COLLECTING_PARAMETERS
| |
| +-- LLM tenta extrair SOMENTE os parâmetros ainda pendentes
| |
| +-- extraiu >= 1 ?
| |
| +-- SIM -> continua a transação; NÃO avalia intent_shift
| |
| +-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
+-- AWAITING_CONFIRMATION
|
+-- reconhece confirmação/rejeição explícita
|
+-- reconheceu ?
|
+-- SIM -> continua/cancela a transação; NÃO avalia intent_shift
|
+-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
```
## TransactionParameterExtractor
Novo componente:
`libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py`
A extração textual dos parâmetros de negócio é feita exclusivamente por LLM. O componente recebe:
- nome da tool/transação ativa;
- parâmetros atualmente pendentes;
- argumentos já conhecidos;
- schema/tipos declarados em `tools.yaml` quando disponíveis;
- descrição da tool;
- mensagem atual do usuário.
Ele não conhece nomes de domínio como `order_id`, `reason`, `subject`, `valor`, TIM ou retail. Não há regex de entidades de negócio.
A LLM pode interpretar, por exemplo:
- `PED-1001` quando só há um parâmetro compatível pendente;
- `o pedido é PED-1001`;
- `PED-1001, desisti da compra` preenchendo dois parâmetros no mesmo turno;
- respostas com o nome do parâmetro seguido do valor;
- respostas apenas com o valor, quando semanticamente inequívocas.
Em caso de dúvida, o prompt manda retornar `null`. Uma nova solicitação não deve ser transformada em valor de parâmetro.
## Separação de responsabilidades
`tool_policies.yaml` continua sendo a fonte de verdade para `requires`.
`tools.yaml` pode fornecer tipos via `args_schema` e descrição da tool para melhorar a interpretação sem introduzir código específico de domínio.
`mcp_parameter_mapping.yaml` continua responsável pelos parâmetros auxiliares/contrato MCP. As strategies do mapper são explicitamente excluídas dos campos presentes em `policy.requires`, para não misturar extração MCP com coleta transacional.
O `EnterpriseRouter` usa o mesmo extrator LLM apenas como *probe* de precedência. Se pelo menos um parâmetro pendente for encontrado, o turno permanece no estado transacional. Os valores extraídos são colocados no metadata da decisão e reutilizados pelo runtime, evitando uma segunda chamada LLM no mesmo turno.
## Profile LLM
Foi adicionado aos templates:
```yaml
transaction_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 500
timeout_seconds: 8
```
Generation/component:
- `llm.transaction_parameter_extraction`
- `transaction_parameter_extraction`
## Limpeza de estado
Em `intent_shift`, `transaction_pre_validation` da transação abandonada é removido para não contaminar a nova transação. O resultado de pre-validation continua preservado enquanto pertence à própria transação para auditoria.
## Testes adicionados
`tests/test_transaction_parameter_llm_precedence.py`
Cobertura:
1. dois parâmetros extraídos no mesmo turno;
2. um parâmetro preenchido ganha precedência sobre keyword que indicaria outra intent;
3. nenhum parâmetro encontrado libera `intent_shift`;
4. ausência do antigo `_extract_action_arguments()` hardcoded;
5. confirmação `sim` ganha precedência sobre intent shift.

532
README.md
View File

@@ -18,6 +18,27 @@ O objetivo é que cada novo agente implemente apenas sua lógica de domínio —
>**Note: Se deseja ir direto e testar a DEMO, vá até a Seção 17 e 18.** >**Note: Se deseja ir direto e testar a DEMO, vá até a Seção 17 e 18.**
## Índice de Desenvolvimento — Agent Framework OCI
### Outros idiomas
- [Developer documentation in English](README_en.md)
- [Índice técnico detalhado em Português](docs/developer/pt/INDEX_DEVELOPER_GUIDE.md)
- [Detailed technical index in English](docs/developer/en/INDEX_DEVELOPER_GUIDE.md)
### Como usar esta documentação
A documentação possui três níveis:
1. **Tutorial principal:** este [`README.md`](README.md) — criação, configuração, execução e teste de um agente do início ao fim.
2. **Arquitetura:** [01 — Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) — componentes, responsabilidades e onde implementar cada coisa.
3. **Referências especializadas:** manuais `02` a `12` — implementação profunda e troubleshooting por capacidade.
Se você está começando um novo agente, siga este `README.md` desde o início. Para aprofundamento ou troubleshooting, use os links abaixo.
Se algo não está funcionando ou se deseja entender melhor funcionalidades da arquitetura do Agent Framework OCI, vá até [34. Funcionalidades Avançadas](#34-funcionalidades-avançadas). Você vai encontrar detalhamento sobre funcionalidades avançadas, como conceitos, exemplos e manuais de utilização.
## SPECs / SDDs da Agent Platform OCI ## SPECs / SDDs da Agent Platform OCI
@@ -164,6 +185,7 @@ O framework deve concentrar os motores reutilizáveis:
- Telemetria Langfuse/OpenTelemetry. - Telemetria Langfuse/OpenTelemetry.
- Analytics IC/NOC/GRL. - Analytics IC/NOC/GRL.
- MCP Tool Router. - MCP Tool Router.
- Planejamento multi-intent e fila persistente de `pending_topics`.
- Cache. - Cache.
- RAG genérico. - RAG genérico.
@@ -209,6 +231,8 @@ Input Guardrails
↓ ↓
Enterprise Router ou Supervisor Enterprise Router ou Supervisor
↓ ↓
Planejamento multi-intent / pending_topics
↓
Agente especializado Agente especializado
↓ ↓
MCP Tool Router / RAG / Cache / LLM MCP Tool Router / RAG / Cache / LLM
@@ -2150,71 +2174,30 @@ trace_id
#### 5.1.1.21.3. Instrumentação automática do cliente OpenAI pelo Langfuse #### 5.1.1.21.3. Instrumentação automática do cliente OpenAI pelo Langfuse
O padrão oficial do framework é:
```python ```env
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
```
habilita a instrumentação automática do cliente OpenAI pelo Langfuse.
Quando habilitada, todas as chamadas realizadas através do cliente OpenAI instrumentado passam a gerar automaticamente spans e generations detalhadas no Langfuse.
Benefícios
Com a instrumentação automática ativada, o Langfuse passa a registrar informações como:
* OpenAI-generation
* Prompt enviado ao modelo
* Resposta retornada pelo modelo
* Modelo utilizado
* Quantidade de tokens
* Custos estimados
* Latência da chamada
* Erros de execução
Essas informações ficam associadas ao trace principal da conversa, facilitando análise, troubleshooting e auditoria.
Comportamento quando desabilitado
Quando:
```python
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
``` ```
ou a variável não está definida: O framework já instrumenta as chamadas LLM por meio de `Telemetry.generation(...)`, preservando `trace_id`, `session_id`, `user_id`, metadados, tokens, custos, latência e o relacionamento pai/filho dentro do trace de negócio. Por esse motivo, a auto-instrumentação do cliente OpenAI não é necessária no fluxo normal do framework.
* As chamadas LLM continuam funcionando normalmente. Quando `false`:
* Os spans customizados do framework continuam sendo emitidos.
* O Langfuse deixa de criar automaticamente as entradas OpenAI-generation.
* Menos detalhes ficam disponíveis para análise das chamadas ao modelo.
Quando utilizar * as chamadas LLM continuam funcionando normalmente;
* prompts, respostas, modelo, tokens, custos e latência continuam disponíveis pela telemetria explícita do framework;
* as generations permanecem correlacionadas ao trace principal da requisição;
* evita-se dupla instrumentação e `OpenAI-generation` como trace raiz separado.
Recomenda-se habilitar em: A opção `true` existe apenas para compatibilidade ou diagnóstico de código que chama diretamente o SDK OpenAI/OpenAI-compatible fora da camada de `Telemetry` do framework. Nesses casos, o wrapper `langfuse.openai` pode capturar automaticamente essas chamadas. Entretanto, em uma aplicação que já usa a instrumentação nativa do framework, mantê-la habilitada pode gerar duplicidade de observations, contagem duplicada de tokens/custos ou traces independentes quando não houver um parent Langfuse ativo.
* Ambientes de desenvolvimento. ```env
* Ambientes de homologação. # Padrão recomendado para todos os templates e ambientes do framework
* Ambientes de produção que necessitem observabilidade detalhada das chamadas LLM. ENABLE_LANGFUSE=true
* Cenários de troubleshooting, tuning de prompts e análise de custos. ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
```
Observação Todos os arquivos `.env.example` distribuídos pelo projeto mantêm essa opção explicitamente em `false`. Se um componente externo precisar de captura automática, habilite-a somente naquele deployment e valide a árvore de traces no Langfuse.
Esta configuração afeta apenas a telemetria automática do Langfuse.
Ela não altera:
* O comportamento dos agentes.
* O roteamento do Supervisor.
* Guardrails.
* Judges.
* MCP Tool Router.
* Fluxos LangGraph.
Seu único objetivo é enriquecer a observabilidade das chamadas realizadas ao modelo de linguagem.
---
### 5.1.1.22. Recomendações de arquitetura
#### 5.1.1.22.1. Para demos e desenvolvimento #### 5.1.1.22.1. Para demos e desenvolvimento
@@ -7973,6 +7956,14 @@ Windows PowerShell:
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
``` ```
### 17.2.1 Subir a estrutura de Langfuse localmente
É possível subir uma estrutura Langfuse localmente através de um Docker (docker-compose).
Com a integração habilitada, o `agent_template_backend` pode registrar traces da execução, spans do workflow, entradas e saídas, chamadas de LLM, latência, consumo de tokens, roteamento, uso de tools e informações úteis para investigar guardrails, judges e falhas. O conteúdo exato depende do modo de trace e dos componentes executados em cada requisição.
Siga a seção [31.1. Langfuse local com agent_template_backend](#311-langfuse-local-com-agent_template_backend) para maiores detalhes.
### 17.3. Validações imediatas ### 17.3. Validações imediatas
Verifique saúde: Verifique saúde:
@@ -10742,7 +10733,7 @@ group by namespace;
``` ```
--- ---
---
## 31. Conclusão ## 31. Conclusão
O `agent_template_backend` fornece a espinha dorsal corporativa para novos agentes. A implementação de um agente novo deve se limitar ao domínio: prompts, regras, tools, clients, schemas e decisões específicas. O `agent_template_backend` fornece a espinha dorsal corporativa para novos agentes. A implementação de um agente novo deve se limitar ao domínio: prompts, regras, tools, clients, schemas e decisões específicas.
@@ -10774,6 +10765,289 @@ Testar gateway → valida o fluxo real fim a fim.
Seguindo esse modelo, novos agentes podem ser criados com padronização, escalabilidade, rastreabilidade e manutenção mais simples. Seguindo esse modelo, novos agentes podem ser criados com padronização, escalabilidade, rastreabilidade e manutenção mais simples.
---
### 31.1. Langfuse local com `agent_template_backend`
Este guia explica como iniciar o Langfuse local fornecido pelo projeto `agent_framework_oci`, conectá-lo ao `agent_template_backend` e validar o envio de traces. O ambiente usa o arquivo:
```text
agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse/docker-compose.yml
```
O Compose desta versão inicia Langfuse v3 e suas dependências: `langfuse-web`, `langfuse-worker`, PostgreSQL, ClickHouse, Redis e MinIO. Ele também inicia MongoDB para os recursos locais de memória do framework.
#### O que o Langfuse permite observar
Com a integração habilitada, o `agent_template_backend` pode registrar traces da execução, spans do workflow, entradas e saídas, chamadas de LLM, latência, consumo de tokens, roteamento, uso de tools e informações úteis para investigar guardrails, judges e falhas. O conteúdo exato depende do modo de trace e dos componentes executados em cada requisição.
#### Pré-requisitos
- Docker Desktop recente, com Docker Compose v2 (`docker compose`).
- Docker Desktop em execução antes de iniciar o ambiente.
- No Windows com WSL 2, habilite a integração da distribuição Linux em **Docker Desktop > Settings > Resources > WSL Integration**.
- Portas locais livres: `3005`, `9090`, `27017` e, no endereço `127.0.0.1`, `5433`, `6379`, `8124`, `9002` e `9091`.
- Memória suficiente para vários containers. Recomenda-se reservar pelo menos 8 GB para o Docker durante o desenvolvimento.
- O projeto `agent_framework_oci` disponível localmente.
- O ambiente Python e as dependências do `agent_template_backend` já instalados.
Valide o Docker e o Compose:
```bash
docker --version
docker compose version
docker info
```
Se `docker info` não responder, inicie o Docker Desktop. No WSL, se `docker` não existir ou não acessar o daemon, revise a integração WSL do Docker Desktop e reabra o terminal.
#### Portas usadas pelo ambiente
| Componente | Endereço no host | Finalidade |
| --- | --- | --- |
| Langfuse Web/API | `http://localhost:3005` | Interface e ingestão de traces |
| MinIO S3 | `http://localhost:9090` | Armazenamento de eventos e mídia |
| MinIO Console | `http://127.0.0.1:9091` | Administração local do MinIO |
| MongoDB | `localhost:27017` | Memória local do framework |
| PostgreSQL | `127.0.0.1:5433` | Banco transacional do Langfuse |
| Redis | `127.0.0.1:6379` | Filas/cache do Langfuse |
| ClickHouse HTTP | `127.0.0.1:8124` | Banco analítico |
| ClickHouse nativo | `127.0.0.1:9002` | Protocolo nativo do ClickHouse |
Se alguma porta estiver ocupada, altere somente o lado esquerdo do mapeamento em `ports:` e ajuste as URLs usadas no host quando aplicável. Para manter a configuração do template sem alterações, preserve a porta `3005` do Langfuse.
#### Iniciar o Langfuse
A partir da raiz do projeto:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose pull
docker compose up -d
```
Na primeira execução, o download das imagens e as migrações podem levar alguns minutos. Acompanhe o estado:
```bash
docker compose ps
docker compose logs -f langfuse-web langfuse-worker
```
Encerre `docker compose logs -f` com `Ctrl+C`; isso não derruba os containers.
Espere até que os serviços estejam em execução e as dependências com health check estejam saudáveis. Valide a interface:
```bash
curl -I http://localhost:3005
```
Depois abra [http://localhost:3005](http://localhost:3005) no navegador.
#### Primeiro acesso e criação das chaves
1. Abra `http://localhost:3005`.
2. Crie o primeiro usuário local e entre no Langfuse.
3. Crie uma organização, se a interface solicitar.
4. Crie um projeto, por exemplo `agent-template-backend-local`.
5. No projeto, abra **Settings > API Keys** e crie um par de chaves.
6. Guarde a **Public Key** (`pk-lf-...`) e a **Secret Key** (`sk-lf-...`).
As chaves devem pertencer ao mesmo projeto no qual os traces serão consultados. Não use chaves ilustrativas ou antigas presentes em arquivos de exemplo.
#### Configurar o `agent_template_backend`
Entre no template e crie o `.env` caso ele ainda não exista:
```bash
cd agent_framework_oci/templates/agent_template_backend
cp .env.example .env
```
No Windows PowerShell, o equivalente é:
```powershell
Set-Location agent_framework_oci/templates/agent_template_backend
Copy-Item .env.example .env
```
Edite a seção de observabilidade do `.env`:
```env
ENABLE_LANGFUSE=true
LANGFUSE_TRACE_MODE=verbose
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
LANGFUSE_LEGACY_IO_FALLBACK=true
LANGFUSE_PUBLIC_KEY=pk-lf-SUBSTITUA_PELA_CHAVE_DO_PROJETO
LANGFUSE_SECRET_KEY=sk-lf-SUBSTITUA_PELA_CHAVE_DO_PROJETO
LANGFUSE_HOST=http://localhost:3005
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
# Mantenha desabilitado se não houver um coletor OTEL configurado.
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
```
`LANGFUSE_TRACE_MODE=verbose` oferece o maior detalhamento durante o desenvolvimento. Use `compact` quando quiser reduzir o volume e o nível de detalhe.
Se o backend também estiver dentro de um container, `localhost` apontará para o próprio container, não para o host. Nesse caso, conecte-o à mesma rede do Compose e use o nome do serviço, normalmente `http://langfuse-web:3000`, ou use `http://host.docker.internal:3005` quando essa resolução estiver disponível.
#### Iniciar o `agent_template_backend`
No diretório `agent_framework_oci/templates/agent_template_backend`, ative o ambiente virtual e inicie a API:
```bash
source .venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
No Windows PowerShell:
```powershell
.\.venv\Scripts\Activate.ps1
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
Valide o backend em outro terminal:
```bash
curl http://localhost:8000/health
curl http://localhost:8000/agents
curl http://localhost:8000/debug/env
```
Em `/debug/env`, confirme que o Langfuse está habilitado e que o host efetivo é `http://localhost:3005`. O endpoint não deve expor o valor da secret key.
#### Gerar um trace de teste
Envie uma requisição real ao template:
```bash
curl -X POST http://localhost:8000/gateway/message \
-H "Content-Type: application/json" \
-d '{
"channel": "web",
"tenant_id": "default",
"payload": {
"text": "Meu pagamento ainda não foi baixado",
"session_id": "teste-langfuse-001",
"user_id": "user-001",
"customer_id": "12345",
"contract_id": "ABC-999",
"message_id": "msg-langfuse-001"
}
}'
```
Uma resposta do backend confirma o processamento da mensagem, mas a validação completa exige conferir o trace no Langfuse:
1. Abra `http://localhost:3005`.
2. Entre no projeto associado às chaves do `.env`.
3. Abra **Tracing/Traces**.
4. Localize a execução recente, normalmente com o span raiz `agent.gateway_message`.
5. Confirme horário, entrada, saída, duração e spans filhos.
6. Se disponível, filtre ou procure por `teste-langfuse-001`, `user-001` ou pelo nome do trace.
Para testar melhor a observabilidade, envie mensagens que ativem caminhos diferentes: roteamento, uma tool MCP, guardrail, judge, RAG e uma segunda mensagem usando o mesmo `session_id`. Isso permite comparar a árvore de spans e verificar a continuidade da sessão.
#### Checklist de validação
- [ ] `docker compose ps` mostra `langfuse-web` e `langfuse-worker` em execução.
- [ ] PostgreSQL, ClickHouse, Redis e MinIO estão saudáveis.
- [ ] `http://localhost:3005` abre no navegador.
- [ ] O projeto e suas API keys foram criados no Langfuse local.
- [ ] O `.env` do template usa as chaves desse mesmo projeto.
- [ ] `ENABLE_LANGFUSE=true` e `LANGFUSE_HOST=http://localhost:3005`.
- [ ] `curl http://localhost:8000/health` retorna o backend saudável.
- [ ] Uma chamada a `/gateway/message` retorna uma resposta.
- [ ] O trace aparece no projeto correto no Langfuse.
- [ ] O trace contém o span raiz e os spans filhos esperados.
#### Comandos operacionais úteis
Ver o estado:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose ps
```
Ver todos os logs recentes:
```bash
docker compose logs --tail=200
```
Ver somente Langfuse:
```bash
docker compose logs --tail=200 langfuse-web langfuse-worker
```
Reiniciar:
```bash
docker compose restart
```
Parar sem apagar os dados:
```bash
docker compose stop
```
Subir novamente:
```bash
docker compose start
```
Parar e remover containers e rede, preservando os volumes:
```bash
docker compose down
```
Atualizar as imagens:
```bash
docker compose pull
docker compose up -d
```
#### Apagar completamente os dados locais
O comando abaixo remove também os volumes, incluindo usuários, projetos, chaves, traces, banco analítico e objetos armazenados. Use somente quando quiser recriar o ambiente do zero:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose down -v
```
Essa operação não é reversível, a menos que exista backup dos volumes.
#### Solução de problemas
| Sintoma | Verificação e correção |
| --- | --- |
| `docker: command not found` no WSL | Instale/inicie o Docker Desktop, habilite WSL 2 e a integração da distribuição; reabra o terminal. |
| Não é possível acessar `localhost:3005` | Execute `docker compose ps` e examine `docker compose logs langfuse-web`. Confirme que a porta 3005 está livre. |
| Containers reiniciam continuamente | Examine os logs do serviço e das dependências. Confirme memória e espaço em disco disponíveis ao Docker. |
| Langfuse abre, mas não recebe traces | Confirme `ENABLE_LANGFUSE`, host e o par de chaves. Reinicie o backend após alterar o `.env`. |
| `401 Unauthorized` no envio ao Langfuse | A public key e a secret key não correspondem ao mesmo projeto, estão incorretas ou foram revogadas. Gere um novo par no projeto local. |
| O trace aparece em outro projeto | O backend está usando outro par de API keys. Confira o `.env` carregado pelo processo. |
| Há trace, mas faltam spans/detalhes | Use `LANGFUSE_TRACE_MODE=verbose`, habilite a autoinstrumentação e execute um fluxo que realmente acione LLM/tools/guardrails. |
| Backend em Docker não alcança Langfuse | Não use `localhost:3005` dentro do container. Use a rede e o nome do serviço ou `host.docker.internal`, conforme a topologia. |
| Porta já está em uso | Identifique o processo ou altere o mapeamento de porta no Compose. Se alterar a porta 3005, atualize `LANGFUSE_HOST` e `NEXTAUTH_URL`. |
| Traces demoram a aparecer | Aguarde alguns segundos, confira `langfuse-worker` e procure erros de fila, Redis, ClickHouse ou autenticação. |
#### Cuidados de segurança
O Compose fornecido contém credenciais simples e secrets voltados ao desenvolvimento local. Ele não deve ser publicado diretamente na internet nem usado em produção sem substituir senhas, secrets, chaves de criptografia, controles de acesso, TLS, backups e políticas de rede.
Não versione o `.env` com API keys reais. Embora as chaves pertençam a uma instância local, elas permitem gravar e consultar dados no projeto correspondente.
---
## 32. Entrega final com Agent Gateway ## 32. Entrega final com Agent Gateway
@@ -11176,3 +11450,147 @@ A adoção das funcionalidades do `Tuning-Performance` pode proporcionar:
* comportamento consistente entre diferentes agentes e projetos. * comportamento consistente entre diferentes agentes e projetos.
O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção. O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção.
### Buscar pelo problema
| Problema / dúvida | O que normalmente está envolvido | Onde procurar |
|---|---|---|
| O framework não encontra o agente/intenção correta | routing, intents, threshold, modo determinístico/LLM | [Routing e Stickiness](docs/developer/pt/02_routing_stickiness_and_intent_shift.md) |
| O agente fica preso no mesmo assunto e não troca de intent | route stickiness, intent shift, handoff | [Routing e Stickiness](docs/developer/pt/02_routing_stickiness_and_intent_shift.md) |
| Uma resposta que deveria preencher parâmetro é interpretada como novo intent | precedência transacional, parameter extraction | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) |
| A transação fica pedindo o mesmo parâmetro | estado transacional, extractor, schema | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| A confirmação “sim/não” não continua o fluxo | confirmation state, transaction state | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) |
| Uma fala inválida durante um `expected_input` vira `CONTINUAR` em vez de pedir esclarecimento | `semantic_classifier.unmatched_value`, `reprompt`, `contextual_reentry`, COER delegado | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| Uma transação encerrada reaparece | checkpoint antigo versus estado transacional ativo | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [LTM/Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| O sistema diz que executou algo, mas não existe evidência | MCP result, estado `COMPLETED`, judges transacionais | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [Guardrails/Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Uma tool não aparece ou não é encontrada | `tools.yaml`, catálogo MCP, discovery | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| MCP Server não aparece no catálogo | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) e [Gateways](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md) |
| Parâmetros enviados à tool estão errados | schema, mapping, BusinessContext, extractor | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Uma operação transacional executa sem confirmação | tool policy, `require_confirmation` | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Uma busca por nome exige correspondência exata demais | extração/mapeamento de parâmetros e lógica do agente | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Recebo 401 entre gateway/backend/MCP | Basic Auth, credenciais por hop | [Gateways e Auth](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md) |
| Preciso decidir se algo pertence ao framework ou ao agente | boundary core/agente | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) |
| Guardrail específico de um agente está quebrando outro | extensibilidade, imports de domínio no core | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Uma frase incompleta recebe mensagem genérica de “regra de segurança” | feedback de input guardrail, `COER`, blocked-turn state | [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| `route=blocked` aparece junto com tools/resultados de outro turno | limpeza de estado do turno bloqueado | [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| Workflow conclui e gera protocolo, mas a resposta final vira mensagem de segurança | `expected_protocols`, `CMP`, `DLEX_OUT`, ordem de `output_guardrails` | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Judge não roda em uma transação | sampling, `always_run_for_transactional`, sinais transacionais | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Groundedness está avaliando sem contexto correto | RAG context, MCP evidence, judge inputs | [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| RAG não encontra conteúdo | provider, ingestão, embeddings, configuração | [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| Não sei se usar RAG, memória ou tool | separação de responsabilidades | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) e [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| Memória desaparece ao trocar de sessão | LTM versus conversation memory | [LTM e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| Memória de um cliente/agente aparece em outro | identity key, tenant/agent/customer isolation | [LTM e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| Preciso recuperar `reasoning_content` | `ainvoke_response()` | [LLM Rich Response](docs/developer/pt/09_llm_rich_response_reasoning.md) |
| `reasoning_content` vem `None` | provider/model não expõe o campo | [LLM Rich Response](docs/developer/pt/09_llm_rich_response_reasoning.md) |
| Há chamadas LLM desnecessárias | routing determinístico, concorrência, cache | [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Há deadlock ou espera entre event loops | cross-loop sequence/runtime | [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Logs/traces não correlacionam o mesmo agente | labels, IDs e mapeamento de observabilidade | [Observabilidade](docs/developer/pt/11_observability_persistence_and_operational_readiness.md) |
| Sequence está interferindo no processamento | implementação assíncrona de sequência | [Observabilidade](docs/developer/pt/11_observability_persistence_and_operational_readiness.md) e [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Um exemplo antigo não compila | documentação histórica versus API atual | [Validação README x Código](docs/developer/pt/VALIDATION_README_ALIGNMENT.md) |
| Preciso criar um agente novo do zero | fluxo completo | [`README.md`](README.md) |
| Preciso saber onde colocar uma nova feature | arquitetura e boundaries | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) |
| Duas solicitações na mesma mensagem acionam apenas uma intent | decomposição multi-intent, `pending_topics`, allowlist de tools | [Planejamento Multi-Intent](docs/features/pt-BR/16_multi_intent_planning.md) |
| O agent secundário repete a transação ou confirmação primária | isolamento de `active_transaction`, `mcp_tools` e estado operacional | [Planejamento Multi-Intent](docs/features/pt-BR/16_multi_intent_planning.md) |
### 34. Funcionalidades Avançadas
### [01 — Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md)
**O que é:** visão dos componentes, contratos e limites de responsabilidade.
**Use quando:** precisar entender a plataforma, decidir onde implementar algo ou evitar acoplamento entre core e agente.
### [02 — Routing, Route Stickiness e Intent Shift](docs/developer/pt/02_routing_stickiness_and_intent_shift.md)
**O que é:** referência completa de descoberta de agente/intent, stickiness, handoff e mudança de intenção.
**Use quando:** a mensagem cai no agente errado, não troca de intent ou perde continuidade.
### [03 — Workflows Transacionais e Estado](docs/developer/pt/03_transaction_workflows_and_state.md)
**O que é:** ciclo transacional multi-turno, estados, confirmação, pausa/retomada, `expected_input`, `semantic_classifier`, `unmatched_value`/`reprompt` e evidência operacional.
**Use quando:** há loops, confirmações incorretas, retomadas erradas, `CONTINUAR`/`contextual_reentry` indevido, `reprompt` ausente ou operações críticas.
### [04 — MCP, Tools, Policies e Extração de Parâmetros](docs/developer/pt/04_mcp_integration_tools_and_policies.md)
**O que é:** referência de tools, MCP Servers, mappings, policies e parameter extraction.
**Use quando:** integração/execução de tool está incorreta ou precisa ser criada.
### [05 — Agent Gateway, MCP Gateway e Autenticação](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md)
**O que é:** responsabilidades dos gateways, governança e autenticação entre componentes.
**Use quando:** houver problema de entrada, catálogo, autorização, 401 ou deployment dos gateways.
### [06 — Guardrails, Judges e Avaliação Transacional](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md)
**O que é:** validações nativas/externas, judges, grounding e regras para turnos transacionais.
**Use quando:** uma validação bloqueia, não roda ou produz avaliação incorreta.
### [07 — RAG, BusinessContext e Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md)
**O que é:** providers de RAG, contexto recuperado, BusinessContext e grounding.
**Use quando:** conhecimento recuperado não chega corretamente ao agente/judge.
### [08 — Long-Term Memory e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md)
**O que é:** memória durável, memória conversacional, identidade e snapshots de estado.
**Use quando:** contexto some, vaza ou workflow retoma do lugar errado.
### [09 — LLM Rich Response e reasoning_content](docs/developer/pt/09_llm_rich_response_reasoning.md)
**O que é:** resposta estruturada de inferência além do `str` retornado por `ainvoke()`.
**Use quando:** consumidores precisam de metadados, usage ou reasoning disponibilizado pelo provider.
### [10 — Performance, Cache e Runtime Assíncrono](docs/developer/pt/10_performance_cache_and_async_runtime.md)
**O que é:** otimizações de concorrência, cache, LLM e event loops.
**Use quando:** houver latência evitável, processamento serial ou deadlock.
### [11 — Observabilidade, Persistência e Prontidão Operacional](docs/developer/pt/11_observability_persistence_and_operational_readiness.md)
**O que é:** correlação, eventos, labels, sequence, persistência e diagnóstico.
**Use quando:** for necessário provar o caminho executado ou diagnosticar produção.
### [12 — Feedback de Guardrails de Entrada e Turnos Bloqueados](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md)
**O que é:** semântica de mensagens públicas para bloqueios de input, limpeza do estado do turno e passagem da resposta pelos guardrails de saída.
**Use quando:** um `COER`/guardrail de entrada gera mensagem genérica, `route=blocked` carrega resultados antigos ou há dúvida sobre a precedência entre input guardrails, routing e tools.
### Guias de features implementadas
Os manuais `01` a `12` acima organizam os assuntos por área de desenvolvimento. Os guias abaixo documentam cada feature executável individualmente:
1. [Autenticação](docs/features/pt-BR/01_authentication.md)
2. [Workflow Transacional Determinístico](docs/features/pt-BR/02_deterministic_transactional_workflow.md)
3. [Composição por LLM Solicitada pelo Domínio](docs/features/pt-BR/03_domain_requested_llm_composition.md)
4. [RAG Solicitado pelo Domínio](docs/features/pt-BR/04_domain_requested_rag.md)
5. [Memória de Longo Prazo](docs/features/pt-BR/05_long_term_memory.md)
6. [Regressão Offline de Workflow](docs/features/pt-BR/06_offline_workflow_regression.md)
7. [Pause e Resume de Workflow](docs/features/pt-BR/07_pause_resume_workflow.md)
8. [Route Stickiness](docs/features/pt-BR/08_route_stickiness.md)
9. [Replay em Interrupções de Voz](docs/features/pt-BR/09_voice_interruption_replay.md)
10. [Recuperação de Erro em Workflow](docs/features/pt-BR/10_workflow_error_recovery.md)
11. [Clarificação](docs/features/pt-BR/11_clarification.md)
12. [Idempotência Durável](docs/features/pt-BR/12_durable_idempotency.md)
13. [Estados Transacionais Dinâmicos](docs/features/pt-BR/13_dynamic_transaction_states.md)
14. [Replay Após Finalização](docs/features/pt-BR/14_post_finalization_replay.md)
15. [Guardrails de Retrieval e Tools](docs/features/pt-BR/15_retrieval_tool_guardrails.md)
16. [Planejamento Multi-Intent e Pending Topics](docs/features/pt-BR/16_multi_intent_planning.md)
O [índice de features em português](docs/features/pt-BR/README.md) deve ser atualizado sempre que um novo guia for adicionado.
### Tutorial principal
[`README.md`](README.md) continua sendo a referência para o passo a passo completo:
`arquitetura → configuração → criação do agente → registro → estado → routing → tools → MCP → identidade → execução → testes → gateways → memória → RAG`.

View File

@@ -18,6 +18,28 @@ The goal is for each new agent to implement only its domain logic — prompts, b
>**Note: If you want to test the DEMO, go to the Section 17 and 18.** >**Note: If you want to test the DEMO, go to the Section 17 and 18.**
## Developer Index — Agent Framework OCI
### Other languages
- [Documentação de desenvolvimento em Português](README.md)
- [Detailed technical index in English](docs/developer/en/INDEX_DEVELOPER_GUIDE.md)
- [Índice técnico detalhado em Português](docs/developer/pt/INDEX_DEVELOPER_GUIDE.md)
### How to use this documentation
The documentation has three clear levels:
1. **Main tutorial:** this [`README_en.md`](README_en.md) — build, configure, run and test an agent end to end.
2. **Architecture:** [01 — Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) — components, boundaries and implementation placement.
3. **Specialized references:** manuals `02` through `12` — deep implementation and troubleshooting by capability.
If you are creating a new agent, follow this `README_en.md` from the beginning. For deeper implementation details or troubleshooting, use the links below.
If something is not working or if you want to understand the features of the Agent Framework OCI architecture, go to [34. Advanced Features](#34-advanced-features)
## SPECs / SDDs of the Agent Platform OCI ## SPECs / SDDs of the Agent Platform OCI
The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI. The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI.
@@ -104,7 +126,7 @@ Details the evaluation and certification framework, including evaluation archite
Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists. Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists.
### [IMPORTANT: Disclaimer Auth and Security](specs/Disclaimer%20Auth%20and%20Security_EN.md) ### [IMPORTANT: Disclaimer Auth and Security](specs/Disclaimer_Auth_and_Security_EN.md)
Oracle Cloud Infrastructure Security Best Practice Recommendations. Oracle Cloud Infrastructure Security Best Practice Recommendations.
@@ -164,6 +186,7 @@ The framework should concentrate the reusable engines:
- Langfuse/OpenTelemetry Telemetry. - Langfuse/OpenTelemetry Telemetry.
- Analytics IC/NOC/GRL. - Analytics IC/NOC/GRL.
- MCP Tool Router. - MCP Tool Router.
- Multi-intent planning and the persistent `pending_topics` queue.
- Cache. - Cache.
- Generic RAG. - Generic RAG.
@@ -209,6 +232,8 @@ Input Guardrails
↓ ↓
Enterprise Router or Supervisor Enterprise Router or Supervisor
↓ ↓
Multi-intent planning / pending_topics
↓
Specialized agent Specialized agent
↓ ↓
MCP Tool Router / RAG / Cache / LLM MCP Tool Router / RAG / Cache / LLM
@@ -2145,71 +2170,30 @@ trace_id
#### 5.1.1.21.3. Automatic Langfuse instrumentation for the OpenAI client #### 5.1.1.21.3. Automatic Langfuse instrumentation for the OpenAI client
```python The framework's official default is:
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
```
enables automatic Langfuse instrumentation for the OpenAI client. ```env
When enabled, every request executed through the Langfuse-instrumented OpenAI client automatically generates detailed spans and generations within Langfuse.
Benefits
With automatic instrumentation enabled, Langfuse can automatically capture and display information such as:
* OpenAI-generation
* Prompt sent to the model
* Model response
* Model name used
* Token consumption
* Estimated costs
* Request latency
* Execution errors
All of this information is linked to the main conversation trace, making troubleshooting, auditing, and performance analysis significantly easier.
Behavior When Disabled
When:
```python
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
``` ```
or when the variable is not defined: The framework already instruments LLM calls through `Telemetry.generation(...)`, preserving `trace_id`, `session_id`, `user_id`, metadata, token usage, cost, latency, and the parent/child relationship inside the business trace. Therefore, OpenAI client auto-instrumentation is not required in the normal framework path.
* LLM calls continue to function normally. When set to `false`:
* Custom framework spans are still emitted.
* Langfuse no longer automatically creates OpenAI-generation entries.
* Less detailed information is available for analyzing model interactions.
Recommended Usage * LLM calls continue to work normally;
* prompts, responses, model, tokens, costs, and latency remain available through the framework's explicit telemetry;
* generations remain correlated with the main request trace;
* duplicate instrumentation and standalone `OpenAI-generation` root traces are avoided.
It is recommended to enable this setting in: The `true` option exists only for compatibility or diagnostics for code that calls the OpenAI/OpenAI-compatible SDK directly outside the framework `Telemetry` layer. In such cases, the `langfuse.openai` wrapper can automatically capture those calls. In an application already using the framework's native instrumentation, keeping it enabled may create duplicate observations, duplicate token/cost accounting, or independent traces when no active Langfuse parent exists.
* Development environments ```env
* Testing and staging environments # Recommended default for every framework template and environment
* Production environments that require detailed LLM observability ENABLE_LANGFUSE=true
* Prompt engineering, troubleshooting, and cost analysis scenarios ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
```
Important Note Every `.env.example` distributed with the project explicitly keeps this option set to `false`. If an external component requires automatic capture, enable it only for that deployment and validate the trace tree in Langfuse.
This setting only affects Langfuse automatic telemetry and observability.
It does not change:
* Agent behavior
* Supervisor routing
* Guardrails
* Judges
* MCP Tool Router
* LangGraph workflows
Its sole purpose is to enrich the observability of language model interactions and provide more detailed execution insights within Langfuse.
---
### 5.1.1.22. Architecture recommendations
#### 5.1.1.22.1. For demos and development #### 5.1.1.22.1. For demos and development
@@ -7881,6 +7865,15 @@ Windows PowerShell:
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
``` ```
### 17.2.1 Deploy a local Langfuse structure
It's possible to deploy a local Langfuse through Docker (docker-compose).
Once enabled, the `agent_template_backend` integration can record execution traces, workflow spans, inputs and outputs, LLM calls, latency, token usage, routing, tool usage, and information useful for investigating guardrails, judges, and failures. The exact content depends on the trace mode and the components used by each request.
Follow the section [31.1. Local Langfuse with agent_template_backend](#311-local-langfuse-with-agent_template_backend) for more details.
### 17.3. Immediate Validations ### 17.3. Immediate Validations
Check health: Check health:
@@ -10680,6 +10673,289 @@ Test gateway → validates the actual end-to-end flow.
Following this model, new agents can be created with simpler standardization, scalability, traceability, and maintenance. Following this model, new agents can be created with simpler standardization, scalability, traceability, and maintenance.
---
### 31.1. Local Langfuse with `agent_template_backend`
This guide explains how to start the local Langfuse environment supplied with `agent_framework_oci`, connect it to `agent_template_backend`, and validate trace ingestion. The environment is defined in:
```text
agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse/docker-compose.yml
```
This version of the Compose stack starts Langfuse v3 and its dependencies: `langfuse-web`, `langfuse-worker`, PostgreSQL, ClickHouse, Redis, and MinIO. It also starts MongoDB for local framework memory features.
#### What Langfuse helps you observe
Once enabled, the `agent_template_backend` integration can record execution traces, workflow spans, inputs and outputs, LLM calls, latency, token usage, routing, tool usage, and information useful for investigating guardrails, judges, and failures. The exact content depends on the trace mode and the components used by each request.
#### Prerequisites
- A recent Docker Desktop installation with Docker Compose v2 (`docker compose`).
- Docker Desktop running before the environment is started.
- On Windows with WSL 2, enable your Linux distribution under **Docker Desktop > Settings > Resources > WSL Integration**.
- Available local ports: `3005`, `9090`, `27017`, and, on `127.0.0.1`, `5433`, `6379`, `8124`, `9002`, and `9091`.
- Enough memory for several containers. At least 8 GB allocated to Docker is recommended for development.
- A local copy of the `agent_framework_oci` project.
- The Python environment and `agent_template_backend` dependencies already installed.
Validate Docker and Compose:
```bash
docker --version
docker compose version
docker info
```
If `docker info` fails, start Docker Desktop. In WSL, if `docker` is missing or cannot reach the daemon, review Docker Desktop's WSL integration and reopen the terminal.
#### Ports used by the environment
| Component | Host address | Purpose |
| --- | --- | --- |
| Langfuse Web/API | `http://localhost:3005` | UI and trace ingestion |
| MinIO S3 | `http://localhost:9090` | Event and media storage |
| MinIO Console | `http://127.0.0.1:9091` | Local MinIO administration |
| MongoDB | `localhost:27017` | Framework local memory |
| PostgreSQL | `127.0.0.1:5433` | Langfuse transactional database |
| Redis | `127.0.0.1:6379` | Langfuse queues/cache |
| ClickHouse HTTP | `127.0.0.1:8124` | Analytics database |
| ClickHouse native | `127.0.0.1:9002` | ClickHouse native protocol |
If a port is already in use, change only the left side of its `ports:` mapping and update host-facing URLs where applicable. Keep Langfuse on port `3005` to use the template configuration unchanged.
#### Start Langfuse
From the project root:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose pull
docker compose up -d
```
On the first run, downloading images and applying migrations may take a few minutes. Monitor the stack:
```bash
docker compose ps
docker compose logs -f langfuse-web langfuse-worker
```
Stop `docker compose logs -f` with `Ctrl+C`; this does not stop the containers.
Wait for the services to run and for dependencies with health checks to become healthy. Validate the UI:
```bash
curl -I http://localhost:3005
```
Then open [http://localhost:3005](http://localhost:3005) in a browser.
#### First login and API key creation
1. Open `http://localhost:3005`.
2. Create the first local user and sign in.
3. Create an organization if prompted.
4. Create a project such as `agent-template-backend-local`.
5. In the project, open **Settings > API Keys** and create a key pair.
6. Save the **Public Key** (`pk-lf-...`) and **Secret Key** (`sk-lf-...`).
Both keys must belong to the same project where you will inspect traces. Do not use illustrative or stale keys found in example files.
#### Configure `agent_template_backend`
Enter the template directory and create `.env` if it does not exist:
```bash
cd agent_framework_oci/templates/agent_template_backend
cp .env.example .env
```
Windows PowerShell equivalent:
```powershell
Set-Location agent_framework_oci/templates/agent_template_backend
Copy-Item .env.example .env
```
Edit the observability section in `.env`:
```env
ENABLE_LANGFUSE=true
LANGFUSE_TRACE_MODE=verbose
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
LANGFUSE_LEGACY_IO_FALLBACK=true
LANGFUSE_PUBLIC_KEY=pk-lf-REPLACE_WITH_PROJECT_PUBLIC_KEY
LANGFUSE_SECRET_KEY=sk-lf-REPLACE_WITH_PROJECT_SECRET_KEY
LANGFUSE_HOST=http://localhost:3005
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
# Keep disabled unless an OTEL collector has been configured.
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
```
`LANGFUSE_TRACE_MODE=verbose` provides the most detail during development. Use `compact` when you want to reduce volume and detail.
If the backend also runs inside a container, `localhost` points to that container, not to the host. Connect it to the same Compose network and use the service name, normally `http://langfuse-web:3000`, or use `http://host.docker.internal:3005` where that hostname is supported.
#### Start `agent_template_backend`
From `agent_framework_oci/templates/agent_template_backend`, activate the virtual environment and start the API:
```bash
source .venv/bin/activate
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
On Windows PowerShell:
```powershell
.\.venv\Scripts\Activate.ps1
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
```
Validate the backend from another terminal:
```bash
curl http://localhost:8000/health
curl http://localhost:8000/agents
curl http://localhost:8000/debug/env
```
In `/debug/env`, confirm that Langfuse is enabled and the effective host is `http://localhost:3005`. The endpoint must not expose the secret key value.
#### Generate a test trace
Send a real request to the template:
```bash
curl -X POST http://localhost:8000/gateway/message \
-H "Content-Type: application/json" \
-d '{
"channel": "web",
"tenant_id": "default",
"payload": {
"text": "My payment has not been posted yet",
"session_id": "langfuse-test-001",
"user_id": "user-001",
"customer_id": "12345",
"contract_id": "ABC-999",
"message_id": "msg-langfuse-001"
}
}'
```
A backend response confirms message processing, but full validation requires checking the trace in Langfuse:
1. Open `http://localhost:3005`.
2. Enter the project associated with the keys in `.env`.
3. Open **Tracing/Traces**.
4. Find the latest execution, normally using `agent.gateway_message` as its root span.
5. Confirm timestamp, input, output, duration, and child spans.
6. If available, filter or search for `langfuse-test-001`, `user-001`, or the trace name.
For a stronger observability test, send messages that activate different paths: routing, an MCP tool, a guardrail, a judge, RAG, and a second message with the same `session_id`. This lets you compare the span tree and verify session continuity.
#### Validation checklist
- [ ] `docker compose ps` shows `langfuse-web` and `langfuse-worker` running.
- [ ] PostgreSQL, ClickHouse, Redis, and MinIO are healthy.
- [ ] `http://localhost:3005` opens in a browser.
- [ ] A project and API keys were created in local Langfuse.
- [ ] The template `.env` uses the keys from that same project.
- [ ] `ENABLE_LANGFUSE=true` and `LANGFUSE_HOST=http://localhost:3005`.
- [ ] `curl http://localhost:8000/health` reports a healthy backend.
- [ ] A `/gateway/message` request returns a response.
- [ ] The trace appears in the correct Langfuse project.
- [ ] The trace contains the expected root and child spans.
#### Useful operational commands
Check status:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose ps
```
View recent logs from all services:
```bash
docker compose logs --tail=200
```
View only Langfuse logs:
```bash
docker compose logs --tail=200 langfuse-web langfuse-worker
```
Restart:
```bash
docker compose restart
```
Stop without deleting data:
```bash
docker compose stop
```
Start again:
```bash
docker compose start
```
Stop and remove containers and the network while preserving volumes:
```bash
docker compose down
```
Update images:
```bash
docker compose pull
docker compose up -d
```
#### Completely delete local data
The command below also removes volumes, including users, projects, keys, traces, analytics data, and stored objects. Use it only when you intend to recreate the environment from scratch:
```bash
cd agent_framework_oci/libs/agent_framework/Infrastructure_Langfuse
docker compose down -v
```
This operation cannot be undone unless the volumes have been backed up.
#### Troubleshooting
| Symptom | Check and resolution |
| --- | --- |
| `docker: command not found` in WSL | Install/start Docker Desktop, enable WSL 2 and distribution integration, then reopen the terminal. |
| Cannot access `localhost:3005` | Run `docker compose ps` and inspect `docker compose logs langfuse-web`. Confirm port 3005 is available. |
| Containers keep restarting | Inspect service and dependency logs. Confirm that Docker has enough memory and disk space. |
| Langfuse opens but receives no traces | Confirm `ENABLE_LANGFUSE`, host, and the key pair. Restart the backend after changing `.env`. |
| `401 Unauthorized` while sending to Langfuse | The public and secret keys do not belong to the same project, are incorrect, or were revoked. Create a new pair in the local project. |
| The trace appears in another project | The backend is using another API key pair. Check the `.env` loaded by the process. |
| A trace exists but spans/details are missing | Use `LANGFUSE_TRACE_MODE=verbose`, enable auto-instrumentation, and run a flow that actually invokes LLMs/tools/guardrails. |
| A containerized backend cannot reach Langfuse | Do not use `localhost:3005` inside the container. Use the Compose network and service name or `host.docker.internal`, depending on the topology. |
| A port is already in use | Identify the process or change the host-side Compose port. If port 3005 changes, update `LANGFUSE_HOST` and `NEXTAUTH_URL`. |
| Traces take time to appear | Wait a few seconds, inspect `langfuse-worker`, and look for queue, Redis, ClickHouse, or authentication errors. |
#### Security notes
The supplied Compose file contains simple credentials and secrets intended for local development. Do not expose it directly to the internet or use it in production without replacing passwords, secrets, encryption keys, access controls, TLS, backups, and network policies.
Do not commit `.env` with real API keys. Even local-instance keys allow access to data in the corresponding project.
---
## 32. Final delivery with Agent Gateway ## 32. Final delivery with Agent Gateway
@@ -11082,3 +11358,141 @@ Adopting the `Tuning-Performance` capabilities can provide:
* consistent behavior across agents and projects. * consistent behavior across agents and projects.
The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment. The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment.
### Search by problem
| Problem / question | Usually involves | Go to |
|---|---|---|
| Framework selects the wrong agent/intent | routing, intents, thresholds, deterministic/LLM mode | [Routing and Stickiness](docs/developer/en/02_routing_stickiness_and_intent_shift.md) |
| Agent stays stuck on the same subject | route stickiness, intent shift, handoff | [Routing and Stickiness](docs/developer/en/02_routing_stickiness_and_intent_shift.md) |
| A parameter answer is mistaken for a new intent | transaction precedence, parameter extraction | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) |
| Transaction keeps asking for the same parameter | transaction state, extractor, schema | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| “yes/no” confirmation does not continue the flow | confirmation state | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) |
| Invalid input during `expected_input` becomes `CONTINUAR` instead of asking for clarification | `semantic_classifier.unmatched_value`, `reprompt`, `contextual_reentry`, delegated COER | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [Input Guardrail Feedback](docs/developer/en/12_input_guardrail_feedback_and_blocked_turns.md) |
| A closed transaction reappears | old checkpoint vs active transaction | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [LTM/Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| System claims an operation ran but there is no evidence | MCP results, `COMPLETED`, transaction judges | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [Guardrails/Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| A tool is missing | tools config, MCP catalog/discovery | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| MCP Server is missing from catalog | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) and [Gateways](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
| Tool parameters are wrong | schema, mapping, BusinessContext, extraction | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| Transactional tool executes without confirmation | policy, `require_confirmation` | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| 401 between gateway/backend/MCP | Basic Auth, hop credentials | [Gateways and Auth](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
| Need to decide framework vs agent ownership | core/agent boundary | [Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) |
| Agent-specific guardrail breaks another agent | extension model, domain imports | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| Judge does not run for a transaction | sampling, transaction signals | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| Groundedness gets the wrong context | RAG context, MCP evidence, judge inputs | [RAG/Grounding](docs/developer/en/07_rag_business_context_and_grounding.md) |
| RAG returns no useful content | provider, ingestion, embeddings | [RAG/Grounding](docs/developer/en/07_rag_business_context_and_grounding.md) |
| Unsure whether to use RAG, memory or a tool | responsibility separation | [Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) |
| Memory disappears across sessions | LTM vs conversation memory | [LTM and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| Memory leaks across customer/agent | identity isolation | [LTM and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| Need `reasoning_content` | `ainvoke_response()` | [LLM Rich Response](docs/developer/en/09_llm_rich_response_reasoning.md) |
| `reasoning_content` is `None` | provider/model does not expose it | [LLM Rich Response](docs/developer/en/09_llm_rich_response_reasoning.md) |
| Too many LLM calls | deterministic routing, concurrency, cache | [Performance](docs/developer/en/10_performance_cache_and_async_runtime.md) |
| Deadlock across event loops | cross-loop runtime/sequence | [Performance](docs/developer/en/10_performance_cache_and_async_runtime.md) |
| Logs/traces do not correlate the same agent | labels, IDs, observability mapping | [Observability](docs/developer/en/11_observability_persistence_and_operational_readiness.md) |
| Historical example no longer compiles | stale docs vs current API | [README Alignment Validation](docs/developer/en/VALIDATION_README_ALIGNMENT.md) |
| Need to create a new agent from scratch | complete flow | [`README_en.md`](README_en.md) |
| Two requests in one message trigger only one intent | multi-intent decomposition, `pending_topics`, tool allowlist | [Multi-Intent Planning](docs/features/en/16_multi_intent_planning.md) |
| A secondary agent repeats the primary transaction or confirmation | isolation of `active_transaction`, `mcp_tools`, and operational state | [Multi-Intent Planning](docs/features/en/16_multi_intent_planning.md) |
### 34. Advanced Features
### [01 — Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md)
**What it is:** component, contract and responsibility-boundary reference.
**Use it when:** understanding the platform or deciding where a feature belongs.
### [02 — Routing, Route Stickiness and Intent Shift](docs/developer/en/02_routing_stickiness_and_intent_shift.md)
**What it is:** agent/intent discovery, stickiness, handoff and intent-shift reference.
**Use it when:** routing is wrong or session continuity behaves incorrectly.
### [03 — Transactional Workflows and State](docs/developer/en/03_transaction_workflows_and_state.md)
**What it is:** multi-turn transaction lifecycle, states, confirmation, resume and execution evidence.
**Use it when:** transactions loop, resume incorrectly or perform critical operations.
### [04 — MCP, Tools, Policies and Parameter Extraction](docs/developer/en/04_mcp_integration_tools_and_policies.md)
**What it is:** tools, MCP Servers, mappings, policies and extraction reference.
**Use it when:** building or troubleshooting tool integration.
### [05 — Agent Gateway, MCP Gateway and Authentication](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md)
**What it is:** gateway responsibilities, governance and component authentication.
**Use it when:** troubleshooting ingress, catalog, authorization or gateway deployment.
### [06 — Guardrails, Judges and Transaction Evaluation](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md)
**What it is:** native/external validation, judges, grounding and transaction evaluation.
**Use it when:** validation blocks, skips or evaluates incorrectly.
### [07 — RAG, BusinessContext and Grounding](docs/developer/en/07_rag_business_context_and_grounding.md)
**What it is:** RAG providers, retrieved context, BusinessContext and grounding.
**Use it when:** retrieved knowledge does not reach the runtime/judge correctly.
### [08 — Long-Term Memory and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md)
**What it is:** durable memory, conversational memory, identity and state snapshots.
**Use it when:** context disappears, leaks or resumes incorrectly.
### [09 — LLM Rich Response and reasoning_content](docs/developer/en/09_llm_rich_response_reasoning.md)
**What it is:** structured inference output beyond the `str` returned by `ainvoke()`.
**Use it when:** consumers require provider metadata, usage or reasoning exposed by the provider.
### [10 — Performance, Cache and Async Runtime](docs/developer/en/10_performance_cache_and_async_runtime.md)
**What it is:** concurrency, caching, LLM and event-loop optimization reference.
**Use it when:** reducing avoidable latency or diagnosing deadlocks.
### [11 — Observability, Persistence and Operational Readiness](docs/developer/en/11_observability_persistence_and_operational_readiness.md)
**What it is:** correlation, events, labels, sequencing, persistence and production diagnostics.
**Use it when:** proving execution paths or diagnosing production behavior.
### [12 — Input Guardrail Feedback and Blocked-Turn Semantics](docs/developer/en/12_input_guardrail_feedback_and_blocked_turns.md)
**What it is:** user-facing semantics for input blocks, blocked-turn state cleanup, and output-guardrail validation of the generated feedback.
**Use it when:** `COER`/input guardrails generate generic messages, `route=blocked` carries stale results, or you need to reason about precedence between input guardrails, routing, and tools.
### Implemented feature guides
The `01` through `12` manuals above organize documentation by development area. The guides below document each executable feature individually:
1. [Authentication](docs/features/en/01_authentication.md)
2. [Deterministic Transactional Workflow](docs/features/en/02_deterministic_transactional_workflow.md)
3. [Domain-Requested LLM Composition](docs/features/en/03_domain_requested_llm_composition.md)
4. [Domain-Requested RAG](docs/features/en/04_domain_requested_rag.md)
5. [Long-Term Memory](docs/features/en/05_long_term_memory.md)
6. [Offline Workflow Regression](docs/features/en/06_offline_workflow_regression.md)
7. [Pause and Resume Workflow](docs/features/en/07_pause_resume_workflow.md)
8. [Route Stickiness](docs/features/en/08_route_stickiness.md)
9. [Voice Interruption Replay](docs/features/en/09_voice_interruption_replay.md)
10. [Workflow Error Recovery](docs/features/en/10_workflow_error_recovery.md)
11. [Clarification](docs/features/en/11_clarification.md)
12. [Durable Idempotency](docs/features/en/12_durable_idempotency.md)
13. [Dynamic Transaction States](docs/features/en/13_dynamic_transaction_states.md)
14. [Post-Finalization Replay](docs/features/en/14_post_finalization_replay.md)
15. [Retrieval and Tool Guardrails](docs/features/en/15_retrieval_tool_guardrails.md)
16. [Multi-Intent Planning and Pending Topics](docs/features/en/16_multi_intent_planning.md)
The [English feature index](docs/features/en/README.md) must be updated whenever a new feature guide is added.
### Main tutorial
[`README_en.md`](README_en.md) remains the complete step-by-step guide.
| Workflow completes and generates a protocol, but the final response becomes a safety message | `expected_protocols`, `CMP`, `DLEX_OUT`, `output_guardrails` ordering | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |

View File

@@ -37,3 +37,27 @@ AGENT_AUTH_BASIC_REALM=agent-contas
# AGENT_AUTH_PROXY_SUBJECT_HEADER=x-authenticated-subject # AGENT_AUTH_PROXY_SUBJECT_HEADER=x-authenticated-subject
# AGENT_AUTH_PROXY_SHARED_SECRET_HEADER=x-internal-auth # AGENT_AUTH_PROXY_SHARED_SECRET_HEADER=x-internal-auth
# AGENT_AUTH_PROXY_SHARED_SECRET_HASH=sha256:replace-hex # AGENT_AUTH_PROXY_SHARED_SECRET_HASH=sha256:replace-hex
# RAG / KBDB
# RAG provider: standard (local) ou kbdb (PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE)
RAG_PROVIDER=standard
# KBDB usa credenciais próprias; não há fallback para ADB_*.
KBDB_DB_USER=
KBDB_DB_PASSWORD=
KBDB_DB_DSN=
KBDB_DB_WALLET_LOCATION=
KBDB_DB_WALLET_PASSWORD=
KBDB_SEARCH_TYPE=hybrid
KBDB_NODE_EXPANSION=true
KBDB_NODE_MAX_RELATED=8
KBDB_GRAPH_CROSS_REF=false
KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON=
KBDB_MIN_SCORE=
KBDB_IDENTIFY_DOCUMENT=true
KBDB_STORE_QUERY=true
KBDB_IDENTIFY_TOP_N=3
RAG_GROUNDED_ONLY=false
KBDB_GROUNDED_ONLY=true

View File

@@ -108,6 +108,14 @@ class BillingAgent(AgentRuntimeMixin):
"next_state": "BILLING_ACTIVE", "next_state": "BILLING_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -108,6 +108,14 @@ class OrdersAgent(AgentRuntimeMixin):
"next_state": "ORDER_ACTIVE", "next_state": "ORDER_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -108,6 +108,14 @@ class ProductAgent(AgentRuntimeMixin):
"next_state": "PRODUCT_ACTIVE", "next_state": "PRODUCT_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

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

View File

@@ -108,6 +108,14 @@ class SupportAgent(AgentRuntimeMixin):
"next_state": "SUPPORT_ACTIVE", "next_state": "SUPPORT_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -369,9 +369,12 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),
"rag_results": result.get("rag_results"),
"business_context": business_context.model_dump(), "business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys, "identity_missing": missing_identity_keys,
"judges": result.get("judge_results"), "judges": result.get("judge_results"),

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

@@ -23,9 +23,12 @@ class AgentState(TypedDict, total=False):
domain: str domain: str
mcp_tools: list[str] mcp_tools: list[str]
mcp_results: list[dict[str, Any]] mcp_results: list[dict[str, Any]]
rag_results: list[dict[str, Any]]
available_mcp_tools: list[str] available_mcp_tools: list[str]
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str transaction_status: str
confirmation_required: bool confirmation_required: bool
confirmation_received: bool confirmation_received: bool
@@ -33,6 +36,9 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str
route_bypassed: bool route_bypassed: bool
continuity_signal: dict[str, Any] continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -160,7 +161,7 @@ class AgentWorkflow:
builder.add_conditional_edges( builder.add_conditional_edges(
"input_guardrails", "input_guardrails",
self._after_input_guardrails, self._after_input_guardrails,
{"blocked": "persist", "continue": "load_long_term_memory"}, {"blocked": "output_guardrails", "continue": "load_long_term_memory"},
) )
builder.add_edge("load_long_term_memory", "routing_decision") builder.add_edge("load_long_term_memory", "routing_decision")
builder.add_conditional_edges( builder.add_conditional_edges(
@@ -197,6 +198,31 @@ class AgentWorkflow:
def _after_input_guardrails(self, state): def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue" return "blocked" if state.get("blocked") else "continue"
@staticmethod
def _input_guardrail_user_message(decisions, state, sanitized_text):
# Keep the technical guardrail reason in telemetry, but expose only a
# safe, actionable message to the end user. The message is intentionally
# routed through output_guardrails before persistence/delivery.
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
first = blocked[0] if blocked else None
code = str(getattr(first, "code", "") or "").upper()
if code == "COER":
return (
"Não consegui entender sua última mensagem porque ela parece "
"incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
)
if code == "INPUT_SIZE":
return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
if code == "DLEX_IN":
return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
if code == "PINJ":
return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
if code == "TOX":
return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
if code == "CMP":
return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."
return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"
async def input_guardrails(self, state): async def input_guardrails(self, state):
if state.get("session_ended") is True: if state.get("session_ended") is True:
answer = str(getattr( answer = str(getattr(
@@ -222,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -240,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -281,12 +333,33 @@ class AgentWorkflow:
component="workflow.input_guardrails.final", component="workflow.input_guardrails.final",
) )
if any(not d.allowed for d in decisions): if any(not d.allowed for d in decisions):
# A blocking input guardrail stops the turn before routing/tools.
# Clear turn-local routing/tool state so stale data from a prior
# turn cannot appear as if it was executed after the block.
user_message = self._input_guardrail_user_message(decisions, state, sanitized)
return { return {
"sanitized_input": sanitized, "sanitized_input": sanitized,
"answer": "Não consegui seguir com essa mensagem por regra de segurança.", "answer": user_message,
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.", "final_answer": None,
"guardrail_decisions": [d.model_dump() for d in decisions], "guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked", "route": "blocked",
"intent": "input_guardrail_blocked",
"route_decision": {
"route": "blocked",
"agent": None,
"intent": "input_guardrail_blocked",
"confidence": 1.0,
"reason": "Entrada interrompida por guardrail antes do roteamento.",
"method": "guardrail",
"next_state": state.get("next_state"),
"handoff": False,
"metadata": {},
"domain": state.get("domain"),
"mcp_tools": [],
},
"mcp_tools": [],
"mcp_results": [],
"judge_results": [],
"blocked": True, "blocked": True,
} }
return { return {
@@ -363,6 +436,11 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None, "reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"), "profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
),
} }
async def billing_agent(self, state): async def billing_agent(self, state):
@@ -439,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -501,16 +580,50 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver. supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
""" """
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)): if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return { return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
candidate = state.get("answer") or ""
context = { context = {
**(state.get("context") or {}), **(state.get("context") or {}),
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"), "session_id": state.get("conversation_key") or state.get("session_id"),
@@ -577,6 +690,12 @@ class AgentWorkflow:
for r in decision.results for r in decision.results
], ],
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }
@@ -803,6 +922,7 @@ class AgentWorkflow:
"judges": state.get("judge_results", []), "judges": state.get("judge_results", []),
"mcp_tools": state.get("mcp_tools", []), "mcp_tools": state.get("mcp_tools", []),
"mcp_results": state.get("mcp_results", []), "mcp_results": state.get("mcp_results", []),
"rag_results": state.get("rag_results", []),
}, },
) )

View File

@@ -60,6 +60,18 @@ mcp_parameter_mapping:
mensagem. mensagem.
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
group: 1 group: 1
cancelar_pedido:
map:
session_key: session_id
extract:
order_id:
from: message
type: string
strategy: hybrid
description: Extraia somente o identificador do pedido informado explicitamente pelo usuário. Retorne null quando não houver identificador de pedido na mensagem.
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
group: 1
solicitar_troca: solicitar_troca:
map: map:
session_key: session_id session_key: session_id

View File

@@ -7,6 +7,35 @@ router:
confidence_threshold: 0.65 confidence_threshold: 0.65
allow_handoff: true allow_handoff: true
transaction_confirmation:
# Explicit yes/no stays deterministic. Only inconclusive replies use this LLM fallback.
semantic_fallback:
enabled: true
allowed_values: [SIM, NAO, CONTINUAR]
confirm_values: [SIM]
reject_values: [NAO]
continue_values: [CONTINUAR]
include_relevant_context: true
profile_name: router
prompt: |
Você classifica a resposta do cliente a uma confirmação transacional pendente.
Considere a pergunta pendente, somente o histórico recente relacionado ao mesmo tema e a fala atual.
Não execute a ação e não invente fatos.
Classes permitidas: {{ allowed_values }}
- SIM: confirmação/aceite inequívoco, inclusive equivalentes como "isso mesmo", "pode confirmar", "é isso" quando o contexto tornar o aceite claro.
- NAO: recusa/cancelamento inequívoco da ação pendente.
- CONTINUAR: qualquer resposta que não confirme nem rejeite inequivocamente, incluindo pergunta adicional, correção, novo dado, ambiguidade ou possível mudança de assunto.
Pergunta pendente:
{{ pending_prompt }}
Histórico relevante:
{{ relevant_conversation_context }}
Resposta atual do cliente:
{{ user_input }}
state_policies: state_policies:
- state: WAITING_BILLING_CONFIRMATION - state: WAITING_BILLING_CONFIRMATION
agent: billing_agent agent: billing_agent
@@ -79,6 +108,25 @@ intents:
- Quero saber sobre meu pacote de internet. - Quero saber sobre meu pacote de internet.
- Tenho roaming internacional? - Tenho roaming internacional?
- name: retail_order_cancel
domain: retail
agent: orders_agent
description: Cancelamento explícito de pedido ou compra.
priority: 20
mcp_tools:
- consultar_pedido
- cancelar_pedido
keywords:
- cancelar pedido
- cancelamento do pedido
- cancelar a compra
- cancelar compra
examples:
- Quero cancelar meu pedido.
- Cancele o pedido.
- Quero cancelar a compra.
- name: retail_order_tracking - name: retail_order_tracking
domain: retail domain: retail
agent: orders_agent agent: orders_agent

View File

@@ -7,6 +7,12 @@ defaults:
require_confirmation: false require_confirmation: false
tool_policies: tool_policies:
cancelar_pedido:
operation_type: transactional
require_confirmation: true
requires: [order_id]
solicitar_troca: solicitar_troca:
operation_type: transactional operation_type: transactional
require_confirmation: true require_confirmation: true

View File

@@ -4,18 +4,33 @@ tools:
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
invoice_id: string type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
invoice_id:
type: string
label: o identificador da fatura
description: Identificador da fatura que o cliente deseja consultar.
user_prompt: Informe o identificador da fatura que deseja consultar.
selection_keywords: selection_keywords:
- fatura - fatura
- conta - conta
- boleto - boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos: consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente. description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
selection_keywords: selection_keywords:
- pagamento - pagamento
- pagamentos - pagamentos
@@ -24,16 +39,31 @@ tools:
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
asset_id: string type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
asset_id:
type: string
label: o identificador do plano ou ativo
description: Identificador do plano ou ativo comercial associado ao cliente.
user_prompt: Informe o identificador do plano ou ativo que deseja consultar.
selection_keywords: selection_keywords:
- plano - plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos: listar_servicos:
description: Lista serviços ativos e adicionais VAS. description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
selection_keywords: selection_keywords:
- serviços - serviços
- servicos - servicos
@@ -43,24 +73,63 @@ tools:
mcp_server: retail mcp_server: retail
enabled: true enabled: true
args_schema: args_schema:
order_id: string order_id:
customer_id: string type: string
label: o número do pedido
description: Identificador do pedido que o cliente deseja consultar.
user_prompt: Informe o número do pedido que deseja consultar.
customer_id:
type: string
label: a identificação do cliente
description: Identificador do cliente associado ao pedido de varejo.
user_prompt: Informe a identificação do cliente.
selection_keywords: selection_keywords:
- consultar pedido - consultar pedido
- status do pedido - status do pedido
- pedido - pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega: consultar_entrega:
description: Consulta entrega e rastreamento do pedido. description: Consulta entrega e rastreamento do pedido.
mcp_server: retail mcp_server: retail
enabled: true enabled: true
args_schema: args_schema:
order_id: string order_id:
type: string
label: o número do pedido
description: Identificador do pedido cuja entrega ou rastreamento será consultado.
user_prompt: Informe o número do pedido que deseja rastrear.
selection_keywords: selection_keywords:
- entrega - entrega
- rastreio - rastreio
- rastreamento - rastreamento
- transportadora - transportadora
- previsão - previsão
response:
mode: renderer
renderer: retail.delivery
cancelar_pedido:
description: Simula o cancelamento de um pedido de varejo.
mcp_server: retail
enabled: true
tool_type: action
requires:
- order_id
confirmation_required: true
args_schema:
order_id:
type: string
label: o número do pedido
description: Identificador do pedido que o cliente deseja cancelar.
user_prompt: Informe o número do pedido que deseja cancelar.
selection_keywords:
- cancelar pedido
- cancelamento do pedido
- cancelar compra
- cancelar a compra
solicitar_troca: solicitar_troca:
description: Simula abertura de solicitação de troca. description: Simula abertura de solicitação de troca.
mcp_server: retail mcp_server: retail
@@ -71,8 +140,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar troca.
user_prompt: Informe o número do pedido que deseja trocar.
reason:
type: string
label: o motivo da troca
description: Motivo informado pelo cliente para solicitar a troca do pedido.
user_prompt: Qual é o motivo da troca?
selection_keywords: selection_keywords:
- solicitar troca - solicitar troca
- trocar - trocar
@@ -89,8 +166,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar devolução.
user_prompt: Informe o número do pedido que deseja devolver.
reason:
type: string
label: o motivo da devolução
description: Motivo informado pelo cliente para solicitar a devolução do pedido.
user_prompt: Qual é o motivo da devolução?
selection_keywords: selection_keywords:
- solicitar devolução - solicitar devolução
- solicitar devolucao - solicitar devolucao

View File

@@ -0,0 +1,11 @@
# Confirmação Transacional Semântica
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.

View File

@@ -78,3 +78,10 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
transaction_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 500
timeout_seconds: 8

View File

@@ -1,211 +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_sdk
LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048
LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
OCI_GENAI_PROJECT_OCID=
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
#OCI_GENAI_MODEL=openai.gpt-4.1
#OCI_GENAI_API_KEY=
#OCI_GENAI_PROJECT_OCID=
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
OCI_AUTH_MODE=config_file
# OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=LATINOAMERICA-Chicago
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
OCI_REGION=us-chicago-1
###############################################################################
# Persistência
###############################################################################
# Opções: memory, autonomous, mongodb
SESSION_REPOSITORY_PROVIDER=autonomous
MEMORY_REPOSITORY_PROVIDER=autonomous
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database
ADB_USER=admin
ADB_PASSWORD=Moniquinha19721972
ADB_DSN=oradb23ai_high
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
ADB_WALLET_PASSWORD=Moniquinha1972
ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
MONGODB_DATABASE=agent_platform
# Redis
REDIS_URL=redis://localhost:6379/0
ENABLE_REDIS_CACHE=false
###############################################################################
# RAG / Vector / Graph
###############################################################################
VECTOR_STORE_PROVIDER=autonomous
GRAPH_STORE_PROVIDER=autonomous
RAG_TOP_K=5
EMBEDDING_PROVIDER=oci
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
###############################################################################
# Observabilidade
###############################################################################
ENABLE_LANGFUSE=true
# Opcional: verbose, compact
LANGFUSE_TRACE_MODE=compact
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
LANGFUSE_IGNORE_HEALTHCHECKS=true
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
###############################################################################
# Analytics / Observer corporativo
###############################################################################
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=oci_streaming
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH=
GCP_PROJECT_ID=
GCP_PUBSUB_TOPIC=
GCP_PUBSUB_TIMEOUT_SECONDS=30
# Credencial GCP segue padrão Google:
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
###############################################################################
# OCI Streaming
###############################################################################
ENABLE_OCI_STREAMING=false
OCI_STREAM_ENDPOINT=
OCI_STREAM_OCID=
OCI_STREAM_PARTITION_KEY=agent-events
###############################################################################
# Guardrails, Judges, Supervisor
###############################################################################
ENABLE_INPUT_GUARDRAILS=true
ENABLE_OUTPUT_GUARDRAILS=true
ENABLE_JUDGES=true
ENABLE_SUPERVISOR=true
ENABLE_OUTPUT_SUPERVISOR=true
ENABLE_PARALLEL_GUARDRAILS=true
GUARDRAILS_FAIL_FAST=true
OUTPUT_SUPERVISOR_MAX_RETRIES=3
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
JUDGES_CONFIG_PATH=./config/judges.yaml
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
###############################################################################
# Gateway de canais
###############################################################################
DEFAULT_CHANNEL=web
# embedded = backend may parse simple/native channel payloads.
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
ENABLE_VOICE_ADAPTER=true
ENABLE_WHATSAPP_ADAPTER=true
ENABLE_TEXT_ADAPTER=true
#################################################
# ENTERPRISE ROUTING
#################################################
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
ROUTING_CONFIG_PATH=./config/routing.yaml
# true = usa LLM para classificar quando keywords/estado não resolverem.
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
ENABLE_LLM_ROUTER=true
# Semantic route stickiness (optional).
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
# There are no regexes or deterministic language rules.
ENABLE_ROUTE_STICKINESS=true
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
ROUTE_STICKINESS_HISTORY_TURNS=2
ROUTE_STICKINESS_MAX_TOKENS=80
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
ENABLE_TRANSACTIONAL_WORKFLOWS=true
WORKFLOWS_PATH=./workflows
###############################################################################
# MCP / Tools
###############################################################################
ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml
MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
ROUTING_MODE=router
# Usage/cost accounting
USAGE_REPOSITORY_PROVIDER=autonomous
IDENTITY_CONFIG_PATH=./config/identity.yaml
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
# -----------------------------------------------------------------------------
# ConversationSummaryMemory / compressão de contexto conversacional
# -----------------------------------------------------------------------------
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
MEMORY_CONTEXT_STRATEGY=summary
MEMORY_HISTORY_LIMIT=80
MEMORY_RECENT_MESSAGES_LIMIT=8
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
###############################################################################
# LONG-TERM MEMORY
###############################################################################
ENABLE_LONG_TERM_MEMORY=true
LONG_TERM_MEMORY_PROVIDER=sqlite
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
LONG_TERM_MEMORY_AUTO_EXTRACT=true
LONG_TERM_MEMORY_INJECT_CONTEXT=true
ENABLE_TRANSACTIONAL_WORKFLOWS=true
WORKFLOWS_PATH=./workflows

View File

@@ -0,0 +1,235 @@
###############################################################################
# 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
# RAG / KBDB
# RAG provider: standard (local) ou kbdb (PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE)
RAG_PROVIDER=standard
# KBDB usa credenciais próprias; não há fallback para ADB_*.
KBDB_DB_USER=
KBDB_DB_PASSWORD=
KBDB_DB_DSN=
KBDB_DB_WALLET_LOCATION=
KBDB_DB_WALLET_PASSWORD=
KBDB_SEARCH_TYPE=hybrid
KBDB_NODE_EXPANSION=true
KBDB_NODE_MAX_RELATED=8
KBDB_GRAPH_CROSS_REF=false
KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON=
KBDB_MIN_SCORE=
KBDB_IDENTIFY_DOCUMENT=true
KBDB_STORE_QUERY=true
KBDB_IDENTIFY_TOP_N=3
RAG_GROUNDED_ONLY=false
KBDB_GROUNDED_ONLY=true
###############################################################################
# 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=true
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
ROUTE_STICKINESS_HISTORY_TURNS=2
ROUTE_STICKINESS_MAX_TOKENS=80
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
ENABLE_TRANSACTIONAL_WORKFLOWS=true
WORKFLOWS_PATH=./workflows
###############################################################################
# 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

@@ -108,6 +108,14 @@ class BillingAgent(AgentRuntimeMixin):
"next_state": "BILLING_ACTIVE", "next_state": "BILLING_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -108,6 +108,14 @@ class OrdersAgent(AgentRuntimeMixin):
"next_state": "ORDER_ACTIVE", "next_state": "ORDER_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -108,6 +108,14 @@ class ProductAgent(AgentRuntimeMixin):
"next_state": "PRODUCT_ACTIVE", "next_state": "PRODUCT_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -108,6 +108,14 @@ class SupportAgent(AgentRuntimeMixin):
"next_state": "SUPPORT_ACTIVE", "next_state": "SUPPORT_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state), **self.transaction_state_patch(state),
} }

View File

@@ -363,9 +363,12 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
"route": result.get("route"), "route": result.get("route"),
"intent": result.get("intent"), "intent": result.get("intent"),
"route_decision": result.get("route_decision"), "route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"), "domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"), "mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"), "mcp_results": result.get("mcp_results"),
"rag_results": result.get("rag_results"),
"business_context": business_context.model_dump(), "business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys, "identity_missing": missing_identity_keys,
"judges": result.get("judge_results"), "judges": result.get("judge_results"),

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

@@ -23,9 +23,12 @@ class AgentState(TypedDict, total=False):
domain: str domain: str
mcp_tools: list[str] mcp_tools: list[str]
mcp_results: list[dict[str, Any]] mcp_results: list[dict[str, Any]]
rag_results: list[dict[str, Any]]
available_mcp_tools: list[str] available_mcp_tools: list[str]
selected_tool_call: dict[str, Any] selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any] pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str transaction_status: str
confirmation_required: bool confirmation_required: bool
confirmation_received: bool confirmation_received: bool
@@ -33,6 +36,9 @@ class AgentState(TypedDict, total=False):
missing_parameters: list[str] missing_parameters: list[str]
supervisor_plan: dict[str, Any] supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]] supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str active_agent: str
route_bypassed: bool route_bypassed: bool
continuity_signal: dict[str, Any] continuity_signal: dict[str, Any]

View File

@@ -7,6 +7,7 @@ from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry from agent_framework.observability.guardrail_events import GuardrailTelemetry
@@ -160,7 +161,7 @@ class AgentWorkflow:
builder.add_conditional_edges( builder.add_conditional_edges(
"input_guardrails", "input_guardrails",
self._after_input_guardrails, self._after_input_guardrails,
{"blocked": "persist", "continue": "load_long_term_memory"}, {"blocked": "output_guardrails", "continue": "load_long_term_memory"},
) )
builder.add_edge("load_long_term_memory", "routing_decision") builder.add_edge("load_long_term_memory", "routing_decision")
builder.add_conditional_edges( builder.add_conditional_edges(
@@ -197,6 +198,31 @@ class AgentWorkflow:
def _after_input_guardrails(self, state): def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue" return "blocked" if state.get("blocked") else "continue"
@staticmethod
def _input_guardrail_user_message(decisions, state, sanitized_text):
# Keep the technical guardrail reason in telemetry, but expose only a
# safe, actionable message to the end user. The message is intentionally
# routed through output_guardrails before persistence/delivery.
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
first = blocked[0] if blocked else None
code = str(getattr(first, "code", "") or "").upper()
if code == "COER":
return (
"Não consegui entender sua última mensagem porque ela parece "
"incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
)
if code == "INPUT_SIZE":
return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
if code == "DLEX_IN":
return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
if code == "PINJ":
return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
if code == "TOX":
return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
if code == "CMP":
return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."
return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"
async def input_guardrails(self, state): async def input_guardrails(self, state):
if state.get("session_ended") is True: if state.get("session_ended") is True:
answer = str(getattr( answer = str(getattr(
@@ -222,6 +248,28 @@ class AgentWorkflow:
input=state.get("user_text"), input=state.get("user_text"),
): ):
history_texts = [m.get("content", "") for m in state.get("history", [])] history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl( await self.observer.emit_grl(
"001", "001",
{ {
@@ -240,6 +288,10 @@ class AgentWorkflow:
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {}, "agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
}, },
) )
for _decision in decisions: for _decision in decisions:
@@ -281,12 +333,33 @@ class AgentWorkflow:
component="workflow.input_guardrails.final", component="workflow.input_guardrails.final",
) )
if any(not d.allowed for d in decisions): if any(not d.allowed for d in decisions):
# A blocking input guardrail stops the turn before routing/tools.
# Clear turn-local routing/tool state so stale data from a prior
# turn cannot appear as if it was executed after the block.
user_message = self._input_guardrail_user_message(decisions, state, sanitized)
return { return {
"sanitized_input": sanitized, "sanitized_input": sanitized,
"answer": "Não consegui seguir com essa mensagem por regra de segurança.", "answer": user_message,
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.", "final_answer": None,
"guardrail_decisions": [d.model_dump() for d in decisions], "guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked", "route": "blocked",
"intent": "input_guardrail_blocked",
"route_decision": {
"route": "blocked",
"agent": None,
"intent": "input_guardrail_blocked",
"confidence": 1.0,
"reason": "Entrada interrompida por guardrail antes do roteamento.",
"method": "guardrail",
"next_state": state.get("next_state"),
"handoff": False,
"metadata": {},
"domain": state.get("domain"),
"mcp_tools": [],
},
"mcp_tools": [],
"mcp_results": [],
"judge_results": [],
"blocked": True, "blocked": True,
} }
return { return {
@@ -363,6 +436,11 @@ class AgentWorkflow:
"reason": decision.reason if decision.method == "continuity" else None, "reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"), "profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {}, } if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
),
} }
async def billing_agent(self, state): async def billing_agent(self, state):
@@ -439,6 +517,7 @@ class AgentWorkflow:
return { return {
"answer": answer, "answer": answer,
"supervisor_results": partials, "supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results, "mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE", "next_state": "SUPERVISOR_ACTIVE",
} }
@@ -501,16 +580,50 @@ class AgentWorkflow:
agente gerar `answer` e antes dos judges/persistência, produzindo campos agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver. supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
""" """
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)): if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return { return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
"output_guardrails_already_applied": False, "output_guardrails_already_applied": False,
"supervisor_action": "disabled", "supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)), "supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
} }
candidate = state.get("answer") or ""
context = { context = {
**(state.get("context") or {}), **(state.get("context") or {}),
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"tenant_id": state.get("tenant_id"), "tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"), "agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"), "session_id": state.get("conversation_key") or state.get("session_id"),
@@ -577,6 +690,12 @@ class AgentWorkflow:
for r in decision.results for r in decision.results
], ],
"output_guardrails_already_applied": True, "output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", []) "guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])], + [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
} }
@@ -803,6 +922,7 @@ class AgentWorkflow:
"judges": state.get("judge_results", []), "judges": state.get("judge_results", []),
"mcp_tools": state.get("mcp_tools", []), "mcp_tools": state.get("mcp_tools", []),
"mcp_results": state.get("mcp_results", []), "mcp_results": state.get("mcp_results", []),
"rag_results": state.get("rag_results", []),
}, },
) )

View File

@@ -60,6 +60,18 @@ mcp_parameter_mapping:
mensagem. mensagem.
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
group: 1 group: 1
cancelar_pedido:
map:
session_key: session_id
extract:
order_id:
from: message
type: string
strategy: hybrid
description: Extraia somente o identificador do pedido informado explicitamente pelo usuário. Retorne null quando não houver identificador de pedido na mensagem.
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
group: 1
solicitar_troca: solicitar_troca:
map: map:
session_key: session_id session_key: session_id

View File

@@ -7,6 +7,35 @@ router:
confidence_threshold: 0.65 confidence_threshold: 0.65
allow_handoff: true allow_handoff: true
transaction_confirmation:
# Explicit yes/no stays deterministic. Only inconclusive replies use this LLM fallback.
semantic_fallback:
enabled: true
allowed_values: [SIM, NAO, CONTINUAR]
confirm_values: [SIM]
reject_values: [NAO]
continue_values: [CONTINUAR]
include_relevant_context: true
profile_name: router
prompt: |
Você classifica a resposta do cliente a uma confirmação transacional pendente.
Considere a pergunta pendente, somente o histórico recente relacionado ao mesmo tema e a fala atual.
Não execute a ação e não invente fatos.
Classes permitidas: {{ allowed_values }}
- SIM: confirmação/aceite inequívoco, inclusive equivalentes como "isso mesmo", "pode confirmar", "é isso" quando o contexto tornar o aceite claro.
- NAO: recusa/cancelamento inequívoco da ação pendente.
- CONTINUAR: qualquer resposta que não confirme nem rejeite inequivocamente, incluindo pergunta adicional, correção, novo dado, ambiguidade ou possível mudança de assunto.
Pergunta pendente:
{{ pending_prompt }}
Histórico relevante:
{{ relevant_conversation_context }}
Resposta atual do cliente:
{{ user_input }}
state_policies: state_policies:
- state: WAITING_BILLING_CONFIRMATION - state: WAITING_BILLING_CONFIRMATION
agent: billing_agent agent: billing_agent
@@ -79,6 +108,25 @@ intents:
- Quero saber sobre meu pacote de internet. - Quero saber sobre meu pacote de internet.
- Tenho roaming internacional? - Tenho roaming internacional?
- name: retail_order_cancel
domain: retail
agent: orders_agent
description: Cancelamento explícito de pedido ou compra.
priority: 20
mcp_tools:
- consultar_pedido
- cancelar_pedido
keywords:
- cancelar pedido
- cancelamento do pedido
- cancelar a compra
- cancelar compra
examples:
- Quero cancelar meu pedido.
- Cancele o pedido.
- Quero cancelar a compra.
- name: retail_order_tracking - name: retail_order_tracking
domain: retail domain: retail
agent: orders_agent agent: orders_agent

View File

@@ -7,6 +7,12 @@ defaults:
require_confirmation: false require_confirmation: false
tool_policies: tool_policies:
cancelar_pedido:
operation_type: transactional
require_confirmation: true
requires: [order_id]
solicitar_troca: solicitar_troca:
operation_type: transactional operation_type: transactional
require_confirmation: true require_confirmation: true

View File

@@ -4,18 +4,33 @@ tools:
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
invoice_id: string type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
invoice_id:
type: string
label: o identificador da fatura
description: Identificador da fatura que o cliente deseja consultar.
user_prompt: Informe o identificador da fatura que deseja consultar.
selection_keywords: selection_keywords:
- fatura - fatura
- conta - conta
- boleto - boleto
response:
mode: renderer
renderer: telecom.invoice
consultar_pagamentos: consultar_pagamentos:
description: Consulta histórico de pagamentos do cliente. description: Consulta histórico de pagamentos do cliente.
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
selection_keywords: selection_keywords:
- pagamento - pagamento
- pagamentos - pagamentos
@@ -24,16 +39,31 @@ tools:
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
asset_id: string type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
asset_id:
type: string
label: o identificador do plano ou ativo
description: Identificador do plano ou ativo comercial associado ao cliente.
user_prompt: Informe o identificador do plano ou ativo que deseja consultar.
selection_keywords: selection_keywords:
- plano - plano
response:
mode: renderer
renderer: telecom.plan
listar_servicos: listar_servicos:
description: Lista serviços ativos e adicionais VAS. description: Lista serviços ativos e adicionais VAS.
mcp_server: telecom mcp_server: telecom
enabled: true enabled: true
args_schema: args_schema:
msisdn: string msisdn:
type: string
label: o número da linha
description: Número da linha do cliente (MSISDN) usado para consultar informações de telecom.
user_prompt: Informe o número da linha que deseja consultar.
selection_keywords: selection_keywords:
- serviços - serviços
- servicos - servicos
@@ -43,24 +73,63 @@ tools:
mcp_server: retail mcp_server: retail
enabled: true enabled: true
args_schema: args_schema:
order_id: string order_id:
customer_id: string type: string
label: o número do pedido
description: Identificador do pedido que o cliente deseja consultar.
user_prompt: Informe o número do pedido que deseja consultar.
customer_id:
type: string
label: a identificação do cliente
description: Identificador do cliente associado ao pedido de varejo.
user_prompt: Informe a identificação do cliente.
selection_keywords: selection_keywords:
- consultar pedido - consultar pedido
- status do pedido - status do pedido
- pedido - pedido
response:
mode: renderer
renderer: retail.order
consultar_entrega: consultar_entrega:
description: Consulta entrega e rastreamento do pedido. description: Consulta entrega e rastreamento do pedido.
mcp_server: retail mcp_server: retail
enabled: true enabled: true
args_schema: args_schema:
order_id: string order_id:
type: string
label: o número do pedido
description: Identificador do pedido cuja entrega ou rastreamento será consultado.
user_prompt: Informe o número do pedido que deseja rastrear.
selection_keywords: selection_keywords:
- entrega - entrega
- rastreio - rastreio
- rastreamento - rastreamento
- transportadora - transportadora
- previsão - previsão
response:
mode: renderer
renderer: retail.delivery
cancelar_pedido:
description: Simula o cancelamento de um pedido de varejo.
mcp_server: retail
enabled: true
tool_type: action
requires:
- order_id
confirmation_required: true
args_schema:
order_id:
type: string
label: o número do pedido
description: Identificador do pedido que o cliente deseja cancelar.
user_prompt: Informe o número do pedido que deseja cancelar.
selection_keywords:
- cancelar pedido
- cancelamento do pedido
- cancelar compra
- cancelar a compra
solicitar_troca: solicitar_troca:
description: Simula abertura de solicitação de troca. description: Simula abertura de solicitação de troca.
mcp_server: retail mcp_server: retail
@@ -71,8 +140,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar troca.
user_prompt: Informe o número do pedido que deseja trocar.
reason:
type: string
label: o motivo da troca
description: Motivo informado pelo cliente para solicitar a troca do pedido.
user_prompt: Qual é o motivo da troca?
selection_keywords: selection_keywords:
- solicitar troca - solicitar troca
- trocar - trocar
@@ -89,8 +166,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar devolução.
user_prompt: Informe o número do pedido que deseja devolver.
reason:
type: string
label: o motivo da devolução
description: Motivo informado pelo cliente para solicitar a devolução do pedido.
user_prompt: Qual é o motivo da devolução?
selection_keywords: selection_keywords:
- solicitar devolução - solicitar devolução
- solicitar devolucao - solicitar devolucao

View File

@@ -0,0 +1,11 @@
# Confirmação Transacional Semântica
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.

View File

@@ -78,3 +78,10 @@ profiles:
temperature: 0 temperature: 0
max_tokens: 80 max_tokens: 80
timeout_seconds: 5 timeout_seconds: 5
transaction_parameter_extraction:
provider: oci_openai
model: openai.gpt-4.1-mini
temperature: 0
max_tokens: 500
timeout_seconds: 8

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

@@ -0,0 +1,11 @@
# External Guardrails / Judges
Este exemplo parte do `agent_template_backend` e demonstra a composição de componentes nativos com políticas pertencentes ao agente.
- `type: external` ativa import dinâmico somente para o componente declarado.
- Guardrail/judge síncrono roda em worker thread via `asyncio.to_thread`.
- Implementação `async` roda concorrente no event loop.
- O framework não importa `app.extensions.*` por padrão.
- Use códigos/names próprios do domínio; não sobrescreva semanticamente o genérico sem deixar a substituição explícita no YAML.
Veja também `agent_framework_oci/docs/EXTERNAL_GUARDRAILS_JUDGES.md` e `docs/EXTERNAL_GUARDRAILS_JUDGES.md` no Contas.

View File

@@ -66,6 +66,30 @@ EMBEDDING_PROVIDER=mock
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0 OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
# RAG / KBDB
# RAG provider: standard (local) ou kbdb (PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE)
RAG_PROVIDER=standard
# KBDB usa credenciais próprias; não há fallback para ADB_*.
KBDB_DB_USER=
KBDB_DB_PASSWORD=
KBDB_DB_DSN=
KBDB_DB_WALLET_LOCATION=
KBDB_DB_WALLET_PASSWORD=
KBDB_SEARCH_TYPE=hybrid
KBDB_NODE_EXPANSION=true
KBDB_NODE_MAX_RELATED=8
KBDB_GRAPH_CROSS_REF=false
KBDB_MAX_CROSS_REF_HOPS=1
KBDB_DOCUMENT_TYPE=customer_safe
KBDB_METADATA_JSON=
KBDB_MIN_SCORE=
KBDB_IDENTIFY_DOCUMENT=true
KBDB_STORE_QUERY=true
KBDB_IDENTIFY_TOP_N=3
RAG_GROUNDED_ONLY=false
KBDB_GROUNDED_ONLY=true
############################################################################### ###############################################################################
# Observabilidade # Observabilidade
############################################################################### ###############################################################################

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,54 @@
# Agent Template Backend Enterprise
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
analytics, observer, MCP, memória, checkpoints e configurações.
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
execução e preservada comentada nos próprios arquivos:
- `app/agents/billing_agent.py`
- `app/agents/product_agent.py`
- `app/agents/orders_agent.py`
- `app/agents/support_agent.py`
## O que o desenvolvedor deve alterar
1. Escolher ou criar um agente em `app/agents/`.
2. Implementar o método `run()`.
3. Ajustar prompts e tools, se necessário.
4. Emitir ICs de negócio relevantes para a jornada.
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
## O que já está integrado
- `AgentObserver`
- `observer.emit_ic()`
- `observer.emit_noc()`
- `observer.emit_grl()`
- `AnalyticsPublisher`
- OCI Streaming
- GCP Pub/Sub
- OutputSupervisor
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
- MCP Tool Router
- LangGraph
- Memory
- Checkpoint
- Langfuse / OpenTelemetry
## Exemplos adicionados
Veja `app/examples/`:
- `ic_examples.py`
- `noc_examples.py`
- `grl_examples.py`
- `mcp_examples.py`
- `observer_examples.py`
## Convenção rápida
- IC = evento de negócio / curadoria / informacional.
- NOC = evento operacional / saúde técnica.
- GRL = evento de guardrail / segurança / validação.

View File

@@ -0,0 +1,15 @@
# Agentes do Template Backend Enterprise
Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas
não executam lógica de negócio pronta.
Cada agente mostra:
- como emitir IC;
- como emitir NOC;
- como emitir GRL;
- como coletar MCP via `_collect_tool_context()`;
- como recuperar RAG via `_retrieve_rag_context()`;
- onde chamar LLM/cache.
A implementação original do exemplo está comentada no fim de cada arquivo.

View File

@@ -0,0 +1,137 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class BillingAgent(AgentRuntimeMixin):
name = "billingAgent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
await self._emit_ic(
"IC.BILLING_AGENT_STARTED",
state,
{"business_component": "faturas"},
component="agent.billing.start",
)
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.BILLING_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.billing.mcp",
)
state["mcp_results"] = tool_context
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[{self.__class__.__name__}] {clarification_message}",
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
confirmation_message = self.transaction_confirmation_message(state)
if confirmation_message:
result = {
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
return result
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="BillingAgent")
if direct_answer:
return {
"answer": direct_answer,
"next_state": state.get("next_state") or "ACTIVE",
"mcp_results": tool_context,
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
**self.transaction_state_patch(state),
}
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.BILLING_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.billing.rag",
)
# Prepara ConversationSummaryMemory antes de montar o prompt.
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente especialista em faturas. Responda com clareza, objetividade e sem sugerir ações não solicitadas. Use dados MCP quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "BillingAgent", messages)
result = {
"answer": f"[BillingAgent] {answer}",
"next_state": "BILLING_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
}
await self._emit_ic(
"IC.BILLING_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
"memory_context": state.get("memory_context_metadata"),
},
component="agent.billing.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,137 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class OrdersAgent(AgentRuntimeMixin):
name = "orders_agent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
await self._emit_ic(
"IC.ORDERS_AGENT_STARTED",
state,
{"business_component": "pedidos"},
component="agent.orders.start",
)
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.ORDERS_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.orders.mcp",
)
state["mcp_results"] = tool_context
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[{self.__class__.__name__}] {clarification_message}",
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
confirmation_message = self.transaction_confirmation_message(state)
if confirmation_message:
result = {
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
return result
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="OrdersAgent")
if direct_answer:
return {
"answer": direct_answer,
"next_state": state.get("next_state") or "ACTIVE",
"mcp_results": tool_context,
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
**self.transaction_state_patch(state),
}
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.ORDERS_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.orders.rag",
)
# Prepara ConversationSummaryMemory antes de montar o prompt.
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente de pedidos de varejo. Use dados de tools quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "OrdersAgent", messages)
result = {
"answer": f"[OrdersAgent] {answer}",
"next_state": "ORDER_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
}
await self._emit_ic(
"IC.ORDERS_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
"memory_context": state.get("memory_context_metadata"),
},
component="agent.orders.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1,137 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class ProductAgent(AgentRuntimeMixin):
name = "productAgent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
await self._emit_ic(
"IC.PRODUCT_AGENT_STARTED",
state,
{"business_component": "produtos"},
component="agent.product.start",
)
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.PRODUCT_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.product.mcp",
)
state["mcp_results"] = tool_context
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[{self.__class__.__name__}] {clarification_message}",
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
confirmation_message = self.transaction_confirmation_message(state)
if confirmation_message:
result = {
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
return result
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="ProductAgent")
if direct_answer:
return {
"answer": direct_answer,
"next_state": state.get("next_state") or "ACTIVE",
"mcp_results": tool_context,
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
**self.transaction_state_patch(state),
}
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.PRODUCT_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.product.rag",
)
# Prepara ConversationSummaryMemory antes de montar o prompt.
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente especialista em produtos, planos e serviços. Explique sem fazer oferta proativa e sem executar ações sem confirmação. Use dados MCP quando disponíveis.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "ProductAgent", messages)
result = {
"answer": f"[ProductAgent] {answer}",
"next_state": "PRODUCT_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
}
await self._emit_ic(
"IC.PRODUCT_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
"memory_context": state.get("memory_context_metadata"),
},
component="agent.product.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

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

View File

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

View File

@@ -0,0 +1,137 @@
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
class SupportAgent(AgentRuntimeMixin):
name = "support_agent"
def __init__(
self,
llm,
telemetry=None,
tool_router=None,
rag_service=None,
cache=None,
settings=None,
observer=None,
memory=None,
summary_memory=None,
):
self.llm = llm
self.telemetry = telemetry
self.tool_router = tool_router
self.rag_service = rag_service
self.cache = cache
self.settings = settings
self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
async def run(self, state):
await self._emit_ic(
"IC.SUPPORT_AGENT_STARTED",
state,
{"business_component": "suporte"},
component="agent.support.start",
)
tool_context = await self._collect_tool_context(state)
if tool_context:
await self._emit_ic(
"IC.SUPPORT_MCP_CONTEXT_COLLECTED",
state,
{"tool_result_count": len(tool_context)},
component="agent.support.mcp",
)
state["mcp_results"] = tool_context
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[{self.__class__.__name__}] {clarification_message}",
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
confirmation_message = self.transaction_confirmation_message(state)
if confirmation_message:
result = {
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
return result
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="SupportAgent")
if direct_answer:
return {
"answer": direct_answer,
"next_state": state.get("next_state") or "ACTIVE",
"mcp_results": tool_context,
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
**self.transaction_state_patch(state),
}
rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"):
await self._emit_ic(
"IC.SUPPORT_RAG_CONTEXT_RETRIEVED",
state,
{
"document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"),
},
component="agent.support.rag",
)
# Prepara ConversationSummaryMemory antes de montar o prompt.
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
await self.prepare_memory_context(state)
messages = self.build_messages(
state,
system_prompt=apply_agent_profile_prompt(
state,
"Você é um agente de suporte de varejo para troca, devolução e garantia.",
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
result = {
"answer": f"[SupportAgent] {answer}",
"next_state": "SUPPORT_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
}
await self._emit_ic(
"IC.SUPPORT_AGENT_COMPLETED",
state,
{
"answer_chars": len(result.get("answer") or ""),
"has_mcp_results": bool(tool_context),
"rag_enabled": bool(rag_metadata.get("enabled")),
"memory_context": state.get("memory_context_metadata"),
},
component="agent.support.completed",
)
return result
async def _collect_tool_context(self, state):
return await self._collect_mcp_context(state)

View File

@@ -0,0 +1 @@
"""Exemplos de uso do template backend enterprise."""

View File

@@ -0,0 +1,37 @@
"""Exemplos de GRL.
GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos
pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão
manual apenas para validações customizadas do agente.
"""
from typing import Any
async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
await observer.emit_grl(
"OBSERVE",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"rail_code": rail_code,
"reason": reason,
},
component="examples.grl",
)
async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
await observer.emit_grl(
"004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"rail_code": rail_code,
"reason": reason,
"action": "block",
},
component="examples.grl",
)

View File

@@ -0,0 +1,34 @@
"""Exemplos de IC - Item de Controle.
ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria,
analytics, BigQuery ou qualquer publisher configurado no framework.
"""
from typing import Any
async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None:
await observer.emit_ic(
"IC.FATURA_CONSULTADA",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"invoice_id": invoice_id,
},
component="examples.ic",
)
async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None:
await observer.emit_ic(
"IC.ACAO_CONCLUIDA",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"action_name": action_name,
"ok": ok,
},
component="examples.ic",
)

View File

@@ -0,0 +1,43 @@
"""Exemplos de MCP + IC.
O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o
padrão para chamadas explícitas ao tool_router quando necessário.
"""
from typing import Any
async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any:
session_id = state.get("conversation_key") or state.get("session_id")
await observer.emit_ic(
"IC.MCP_TOOL_CALLED",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"tool_name": tool_name,
},
component="examples.mcp",
)
result = await tool_router.call(
tool_name,
payload,
business_context=(state.get("context") or {}).get("business_context") or {},
original_context=state.get("context") or {},
)
await observer.emit_ic(
"IC.TOOL_CALLED",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"tool_name": tool_name,
"ok": getattr(result, "ok", None),
},
component="examples.mcp",
)
return result

View File

@@ -0,0 +1,37 @@
"""Exemplos de NOC.
NOC representa telemetria operacional. O workflow do template já emite NOC.001,
NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode
emitir em pontos críticos.
"""
from typing import Any
async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None:
await observer.emit_noc(
"002",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"apiUrl": api_url,
"statusCode": status_code,
"latencyMs": latency_ms,
},
component="examples.noc",
)
async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None:
await observer.emit_noc(
"003",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"resourceName": resource_name,
"latencyMs": latency_ms,
},
component="examples.noc",
)

View File

@@ -0,0 +1,28 @@
"""Resumo prático do Observer corporativo.
Use este arquivo como cola rápida para IC, NOC e GRL.
"""
from typing import Any
async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None:
session_id = state.get("conversation_key") or state.get("session_id")
await observer.emit_ic(
"IC.EXEMPLO_NEGOCIO",
{"session_id": session_id, "agent_id": state.get("agent_id")},
component="examples.observer",
)
await observer.emit_noc(
"EXEMPLO_OPERACIONAL",
{"session_id": session_id, "agent_id": state.get("agent_id")},
component="examples.observer",
)
await observer.emit_grl(
"OBSERVE",
{"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"},
component="examples.observer",
)

View File

@@ -0,0 +1,11 @@
from __future__ import annotations
from agent_framework.guardrails.base import Guardrail, RailDecision
class ExternalBusinessPolicyRail(Guardrail):
code = "EXTERNAL_BUSINESS_POLICY"
stage = "output"
def evaluate(self, text, context):
# Synchronous on purpose: framework executes this method in a worker thread.
blocked = bool((context or {}).get("example_block"))
return RailDecision(code=self.code, allowed=not blocked, reason="example business policy" if blocked else "", metadata={"external": True})

View File

@@ -0,0 +1,9 @@
from __future__ import annotations
from agent_framework.judges.judge import JudgeResult
class ExternalBusinessJudge:
name = "external_business_quality"
def __init__(self, threshold=0.5, **kwargs): self.threshold=float(threshold or 0.5)
def evaluate(self, question, answer, context):
score = 1.0 if answer and len(answer.strip()) >= 10 else 0.0
return JudgeResult(name=self.name, score=score, passed=score >= self.threshold, reason="example external judge", metadata={"external": True})

View File

@@ -0,0 +1,535 @@
from __future__ import annotations
import logging
from uuid import uuid4
import time
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from agent_framework.channels.base import ChannelResponse
from agent_framework.channels.gateway import ChannelGateway
from agent_framework.config.agent_registry import AgentProfileRegistry
from agent_framework.config.settings import settings
from agent_framework.analytics.factory import create_analytics_publisher
from agent_framework.observer import configure as configure_global_observer
from agent_framework.llm.providers import create_llm
from agent_framework.memory.message_history import create_memory
from agent_framework.memory.summary_memory import create_conversation_summary_memory
from agent_framework.mcp.tool_router import create_mcp_tool_router
from agent_framework.models.identity import AgentIdentity
from agent_framework.identity import IdentityResolver, BusinessContext
from agent_framework.models.session import ChatMessage, SessionContext
from agent_framework.observability.telemetry import Telemetry
from agent_framework.observability.context import set_observability_context, clear_observability_context
from agent_framework.repositories.session_repository import create_session_repository
from agent_framework.checkpoints.checkpoint_repository import create_checkpoint_repository
from agent_framework.cache.cache import create_cache
from agent_framework.billing.usage_repository import create_usage_repository
from agent_framework.sse.events import SSEHub
from app.workflows.agent_graph import AgentWorkflow
from app.observability.telemetry_observer import TelemetryBackedAgentObserver
logging.basicConfig(level=settings.LOG_LEVEL)
logger = logging.getLogger("agent_template_backend")
app = FastAPI(title="Agent Template Backend FIRST-ready")
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
telemetry = Telemetry(settings)
usage_repository = create_usage_repository(settings)
llm = create_llm(settings, telemetry=telemetry, usage_repository=usage_repository)
memory = create_memory(settings)
summary_memory = create_conversation_summary_memory(settings, message_history=memory, llm=llm, telemetry=telemetry)
sessions = create_session_repository(settings)
checkpoints = create_checkpoint_repository(settings)
cache = create_cache(settings, telemetry=telemetry)
gateway = ChannelGateway(input_mode=settings.FRAMEWORK_CHANNEL_INPUT_MODE)
analytics = create_analytics_publisher(settings)
observer = TelemetryBackedAgentObserver(telemetry=telemetry)
configure_global_observer({
"enabled": getattr(settings, "ENABLE_ANALYTICS", False),
"providers": getattr(settings, "ANALYTICS_PROVIDERS", "oci_streaming"),
"topic_path": getattr(settings, "GCP_PUBSUB_TOPIC_PATH", None) or getattr(settings, "AGENT_PUBSUB_TOPIC", None),
})
tool_router = create_mcp_tool_router(settings, telemetry=telemetry)
identity_resolver = IdentityResolver.from_yaml(settings.IDENTITY_CONFIG_PATH)
agent_profiles = AgentProfileRegistry(settings)
sse_hub = SSEHub(settings, telemetry=telemetry)
workflow = AgentWorkflow(llm, memory, telemetry, analytics, settings, observer=observer, tool_router=tool_router, summary_memory=summary_memory)
logger.info("LLM provider carregado: %s", llm.__class__.__name__)
logger.info("Langfuse habilitado: %s host=%s", telemetry.is_enabled(), settings.LANGFUSE_HOST)
logger.info("Analytics habilitado: %s providers=%s", getattr(settings, "ENABLE_ANALYTICS", False), getattr(settings, "ANALYTICS_PROVIDERS", ""))
logger.info("Agentes disponíveis: %s", [p.agent_id for p in agent_profiles.list_profiles()])
logger.info("Framework channel input mode: %s", gateway.input_mode)
@app.middleware("http")
async def observability_context_middleware(request: Request, call_next):
clear_observability_context()
request_id = request.headers.get("x-request-id") or str(uuid4())
set_observability_context(
request_id=request_id,
channel=request.headers.get("x-channel") or "http",
ura_call_id=request.headers.get("x-ura-call-id"),
)
started = time.time()
try:
response = await call_next(request)
response.headers["x-request-id"] = request_id
await telemetry.event("http.request.completed", {
"method": request.method,
"path": request.url.path,
"status_code": response.status_code,
"duration_ms": int((time.time() - started) * 1000),
}, kind="http")
return response
except Exception as exc:
await telemetry.event("http.request.failed", {
"method": request.method,
"path": request.url.path,
"error": str(exc),
"duration_ms": int((time.time() - started) * 1000),
}, kind="http")
raise
finally:
clear_observability_context()
class GatewayRequest(BaseModel):
channel: str = "web"
payload: dict
agent_id: str | None = None
tenant_id: str | None = None
def _metadata_value(payload: dict, key: str):
metadata = payload.get("metadata")
if isinstance(metadata, dict):
return metadata.get(key)
return None
def _extract_workflow_id(payload: dict) -> str | None:
return (
payload.get("workflow_id")
or payload.get("workflowId")
or _metadata_value(payload, "workflow_id")
or _metadata_value(payload, "workflowId")
)
def _format_root_span_name(template: str | None, values: dict) -> str:
template = template or "agent.gateway_message"
try:
return template.format(**{k: v or "unknown" for k, v in values.items()})
except Exception:
logger.warning("LANGFUSE_ROOT_SPAN_NAME inválido: %s", template)
return "agent.gateway_message"
def _resolve_identity(req: GatewayRequest, msg) -> tuple[AgentIdentity, dict, BusinessContext, list[str]]:
payload = req.payload or {}
context = dict(msg.context or {})
tenant_id = req.tenant_id or payload.get("tenant_id") or context.get("tenant_id") or "default"
agent_id = req.agent_id or payload.get("agent_id") or context.get("agent_id") or agent_profiles.default_agent_id
profile = agent_profiles.get(agent_id)
# 1) Identidade técnica do framework: isola tenant/agente/sessão.
context.update({"tenant_id": tenant_id, "agent_id": profile.agent_id, "agent_profile": profile.__dict__})
identity = AgentIdentity.from_context(context, session_id=msg.session_id)
# 2) Identidade de negócio: chaves canônicas vindas do front/canal.
# Estas chaves são estáveis na sessão e seguem até agentes e MCP Router.
previous_business_context = context.get("business_context") or context.get("identity") or {}
business_context = identity_resolver.resolve(
{**payload, **context},
session_id=identity.conversation_key(),
previous=previous_business_context,
)
missing_identity_keys = identity_resolver.validate(business_context)
context.update({
"business_context": business_context.model_dump(),
"business_keys": business_context.to_context_dict(),
"identity_missing": missing_identity_keys,
"conversation_key": identity.conversation_key(),
"original_session_id": msg.session_id,
})
return identity, context, business_context, missing_identity_keys
async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False) -> dict:
try:
msg = await gateway.normalize(req.channel, req.payload)
except ValueError as exc:
raise HTTPException(status_code=422, detail=str(exc)) from exc
payload = req.payload or {}
identity, normalized_context, business_context, missing_identity_keys = _resolve_identity(req, msg)
agent_session_id = identity.conversation_key()
message_id = payload.get("message_id") or str(uuid4())
workflow_id = _extract_workflow_id(payload)
set_observability_context(
session_id=agent_session_id,
user_id=msg.user_id,
tenant_id=identity.tenant_id,
agent_id=identity.agent_id,
channel=msg.channel,
message_id=message_id,
workflow_id=workflow_id,
ura_call_id=payload.get("ura_call_id") or normalized_context.get("ura_call_id") or business_context.interaction_key,
)
stream = sse_hub.stream_for(agent_session_id)
async with stream.lock:
await sse_hub.emit(agent_session_id, "flow.start", {"session_id": agent_session_id, "message_id": message_id, "agent_id": identity.agent_id}) if emit_sse else None
session = await sessions.get(agent_session_id)
if not session:
context_fields = {
k: v
for k, v in normalized_context.items()
if k in SessionContext.model_fields
and k not in {"tenant_id", "agent_id", "session_id", "user_id", "channel", "channel_id"}
}
session = SessionContext(
tenant_id=identity.tenant_id,
agent_id=identity.agent_id,
session_id=agent_session_id,
user_id=msg.user_id,
channel=msg.channel,
channel_id=msg.channel_id,
**context_fields,
)
session.tenant_id = identity.tenant_id
session.agent_id = identity.agent_id
session.channel = msg.channel
session.channel_id = msg.channel_id or session.channel_id
await sessions.upsert(session)
session.metadata = {
**(session.metadata or {}),
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"original_context": normalized_context,
}
await sse_hub.emit(agent_session_id, "session.upserted", {"session_id": agent_session_id, "business_context": business_context.model_dump()}) if emit_sse else None
await memory.append(
agent_session_id,
ChatMessage(
role="user",
content=msg.text,
metadata={
**normalized_context,
"agent_id": identity.agent_id,
"tenant_id": identity.tenant_id,
"message_id": message_id,
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
},
),
)
await sse_hub.emit(agent_session_id, "message.received", {"session_id": agent_session_id, "role": "user"}) if emit_sse else None
history = [m.model_dump(mode="json") for m in await memory.list(agent_session_id)]
cms_input = {
"channel": req.channel,
"tenant_id": req.tenant_id,
"agent_id": req.agent_id,
"payload": payload,
}
trace_context = {
"text": msg.text,
"channel": msg.channel,
"channel_id": msg.channel_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"conversation_key": agent_session_id,
"workflow_id": workflow_id,
"message_id": message_id,
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
}
root_span_name = _format_root_span_name(
getattr(settings, "LANGFUSE_ROOT_SPAN_NAME", "agent.gateway_message"),
{
"workflow_id": workflow_id,
"channel": msg.channel,
"agent_id": identity.agent_id,
"tenant_id": identity.tenant_id,
},
)
root_tags = ["agent-template", msg.channel, f"agent:{identity.agent_id}", f"tenant:{identity.tenant_id}"]
if workflow_id:
root_tags.append(f"workflow:{workflow_id}")
async with telemetry.span(
root_span_name,
session_id=agent_session_id,
user_id=session.user_id,
channel=msg.channel,
workflow_id=workflow_id,
input=cms_input,
tags=root_tags,
_root_span=True,
) as root_span:
await telemetry.event("gateway.message.received", trace_context)
await sse_hub.emit(agent_session_id, "workflow.started", trace_context) if emit_sse else None
result = await workflow.ainvoke(
{
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"session_id": agent_session_id,
"conversation_key": agent_session_id,
"workflow_id": workflow_id,
"agent_profile": normalized_context["agent_profile"],
"user_text": msg.text,
"history": history,
"context": {
**normalized_context,
"session": session.model_dump(mode="json"),
"original_session_id": msg.session_id,
"session_id": agent_session_id,
"conversation_key": agent_session_id,
"workflow_id": workflow_id,
"user_id": session.user_id,
"channel": msg.channel,
"message_id": message_id,
"business_context": business_context.model_dump(),
"business_keys": business_context.to_context_dict(),
"identity_missing": missing_identity_keys,
},
}
)
await checkpoints.put(agent_session_id, {"state": result, "message_id": message_id})
await sse_hub.emit(agent_session_id, "workflow.completed", {"session_id": agent_session_id, "route": result.get("route"), "intent": result.get("intent")}) if emit_sse else None
answer = result.get("final_answer") or result.get("answer") or ""
await memory.append(
agent_session_id,
ChatMessage(
role="assistant",
content=answer,
metadata={
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"message_id": f"assistant-{message_id}",
"route": result.get("route"),
"intent": result.get("intent"),
"route_decision": result.get("route_decision"),
"judges": result.get("judge_results"),
},
),
)
await telemetry.event(
"gateway.message.responded",
{
"session_id": agent_session_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"route": result.get("route"),
"intent": result.get("intent"),
"answer_chars": len(answer),
},
)
response = ChannelResponse(
channel=msg.channel,
session_id=agent_session_id,
text=answer,
metadata={
"channel_id": msg.channel_id,
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"original_session_id": msg.session_id,
"conversation_key": agent_session_id,
"workflow_id": workflow_id,
"message_id": message_id,
"route": result.get("route"),
"intent": result.get("intent"),
"route_decision": result.get("route_decision"),
"supervisor_results": result.get("supervisor_results") or [],
"agent_responses": result.get("agent_responses") or result.get("supervisor_results") or [],
"domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"),
"rag_results": result.get("rag_results"),
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"judges": result.get("judge_results"),
"guardrails": result.get("guardrail_decisions"),
},
)
rendered = await gateway.render(response)
root_span.set_output(rendered)
await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None
await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None
return rendered
@app.get("/health")
async def health():
return {
"status": "ok",
"llm_provider": settings.LLM_PROVIDER,
"llm_class": llm.__class__.__name__,
"langfuse_enabled": telemetry.is_enabled(),
"agents": [p.agent_id for p in agent_profiles.list_profiles()],
"default_agent_id": agent_profiles.default_agent_id,
"routing_mode": settings.ROUTING_MODE,
"sse_enabled": settings.ENABLE_SSE,
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"usage_repository": settings.USAGE_REPOSITORY_PROVIDER,
"identity_config_path": settings.IDENTITY_CONFIG_PATH,
"mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH,
"framework_channel_input_mode": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
"legacy_channel_gateway_mode": settings.CHANNEL_GATEWAY_MODE,
}
@app.get("/agents")
async def list_agents():
return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]}
@app.get("/debug/env")
async def debug_env():
return {
"APP_ENV": settings.APP_ENV,
"LLM_PROVIDER": settings.LLM_PROVIDER,
"ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE,
"LANGFUSE_HOST": settings.LANGFUSE_HOST,
"TELEMETRY_ENABLED": telemetry.is_enabled(),
"SQLITE_DB_PATH": settings.SQLITE_DB_PATH,
"SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER,
"MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER,
"CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH,
"ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH,
"ROUTING_MODE": settings.ROUTING_MODE,
"FRAMEWORK_CHANNEL_INPUT_MODE": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
"CHANNEL_GATEWAY_MODE": settings.CHANNEL_GATEWAY_MODE,
}
@app.get("/test-llm")
async def test_llm():
async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}):
answer = await llm.ainvoke([
{"role": "system", "content": "Responda de forma curta."},
{"role": "user", "content": "Diga apenas OK"},
])
telemetry.flush()
return {"provider": llm.__class__.__name__, "answer": answer}
@app.post("/debug/route")
async def debug_route(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
state = {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"session_id": msg.session_id or "debug-session",
"conversation_key": identity.conversation_key(),
"agent_profile": context["agent_profile"],
"user_text": msg.text,
"sanitized_input": msg.text,
"history": [],
"context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()},
}
if settings.ROUTING_MODE == "supervisor":
plan = await workflow.supervisor.route_plan(state)
return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata}
decision = await workflow.router.route(state)
data = decision.model_dump(mode="json")
data["mode"] = "router"
return data
@app.post("/debug/identity")
async def debug_identity(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
return {
"technical_identity": {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"conversation_key": identity.conversation_key(),
"original_session_id": msg.session_id,
},
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"context_keys": sorted(context.keys()),
}
@app.get("/debug/usage")
async def debug_usage(tenant_id: str | None = None, session_id: str | None = None):
return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id)
@app.get("/debug/mcp/tools")
async def debug_mcp_tools():
return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()}
@app.post("/debug/mcp/call/{tool_name}")
async def debug_mcp_call(tool_name: str, arguments: dict | None = None):
arguments = arguments or {}
ctx = arguments.get("business_context") or arguments.get("identity") or {}
result = await tool_router.call(
tool_name,
arguments,
business_context=ctx,
original_context=arguments,
)
return result.model_dump(mode="json")
@app.post("/gateway/message")
async def gateway_message(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=False)
@app.post("/gateway/message/sse")
async def gateway_message_sse(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=True)
@app.get("/gateway/events/{session_id}")
async def gateway_events(session_id: str, request: Request):
last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0"
return StreamingResponse(
sse_hub.subscribe(session_id, int(last)),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
)
@app.get("/sessions/{session_id}/messages")
async def get_session_messages(session_id: str, limit: int = 50):
return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]}
@app.get("/sessions/{session_id}/checkpoint")
async def get_session_checkpoint(session_id: str):
return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)}
@app.on_event("shutdown")
async def shutdown():
telemetry.shutdown()

View File

@@ -0,0 +1,16 @@
from __future__ import annotations
import os
from agent_framework.gateways import MCPGatewayClient
def build_mcp_gateway_client() -> MCPGatewayClient | None:
if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true":
return None
return MCPGatewayClient(
base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"),
token=os.getenv("MCP_GATEWAY_TOKEN") or None,
timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")),
)

View File

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

View File

@@ -0,0 +1,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

@@ -0,0 +1,57 @@
from typing import Any, TypedDict
class AgentState(TypedDict, total=False):
tenant_id: str
agent_id: str
session_id: str
conversation_key: str
workflow_id: str
agent_profile: dict[str, Any]
user_text: str
sanitized_input: str
route: str
intent: str
route_decision: dict[str, Any]
answer: str
final_answer: str
history: list[dict[str, Any]]
context: dict[str, Any]
guardrail_decisions: list[dict[str, Any]]
judge_results: list[dict[str, Any]]
next_state: str
domain: str
mcp_tools: list[str]
mcp_results: list[dict[str, Any]]
rag_results: list[dict[str, Any]]
available_mcp_tools: list[str]
selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any]
active_transaction: dict[str, Any]
last_transaction: dict[str, Any]
transaction_status: str
confirmation_required: bool
confirmation_received: bool
tool_policy_result: dict[str, Any]
missing_parameters: list[str]
supervisor_plan: dict[str, Any]
supervisor_results: list[dict[str, Any]]
agent_responses: list[dict[str, Any]]
pending_topics: list[dict[str, Any]]
handled_topics: list[dict[str, Any]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]
session_control: str
session_ended: bool
human_handoff_requested: bool
blocked: bool
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool
long_term_memories: list[dict[str, Any]]
long_term_memory_context: str
long_term_memory_write_result: dict[str, Any]

View File

@@ -0,0 +1,936 @@
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
from langgraph.graph import END, START, StateGraph
from agent_framework.guardrails.pipeline import GuardrailPipeline
from agent_framework.guardrails.output_supervisor import OutputSupervisor
from agent_framework.guardrails.rail_action import RailAction
from agent_framework.guardrails.rail_result import RailResult
from agent_framework.judges.judge import JudgePipeline
from agent_framework.routing.enterprise_router import EnterpriseRouter
from agent_framework.routing.pending_topics import drain_pending_topics
from agent_framework.supervisor.supervisor import Supervisor
from agent_framework.observability.workflow_events import WorkflowTelemetry
from agent_framework.observability.guardrail_events import GuardrailTelemetry
from agent_framework.observability.judge_events import JudgeTelemetry
from agent_framework.observability.langgraph_telemetry import LangGraphDeepTelemetry
from agent_framework.observability.observer import AgentObserver
from app.agents.billing_agent import BillingAgent
from app.agents.product_agent import ProductAgent
from app.agents.orders_agent import OrdersAgent
from app.agents.support_agent import SupportAgent
from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.rag.embedding_provider import create_embedding_provider
from agent_framework.cache.cache import create_cache
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
class LegacyOutputGuardrailRail:
"""Adapter: reutiliza GuardrailPipeline.run_output dentro do OutputSupervisor novo.
O framework antigo retornava decisões allowed=True/False. O OutputSupervisor
corporativo trabalha com RailAction (allow/sanitize/retry/block/handover).
Este adapter evita reescrever todos os rails agora e mantém compatibilidade.
"""
code = "LEGACY_OUTPUT_GUARDRAILS"
def __init__(self, pipeline: GuardrailPipeline):
self.pipeline = pipeline
async def evaluate(self, candidate: str, context: dict):
final, decisions = await self.pipeline.run_output(candidate, context)
serialized = [d.model_dump() for d in decisions]
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
if blocked:
first = blocked[0]
code = (getattr(first, "code", "") or "").upper()
action = RailAction.RETRY if code in {"REVPREC", "CMP", "SCO", "GND"} else RailAction.BLOCK
return RailResult(
code=code or self.code,
action=action,
reason=getattr(first, "reason", "Resposta bloqueada por guardrail de saída"),
guidance=getattr(first, "reason", "Regerar resposta seguindo as políticas de saída."),
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
if final != candidate:
return RailResult(
code=self.code,
action=RailAction.SANITIZE,
reason="Resposta sanitizada por guardrail de saída legado.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
return RailResult(
code=self.code,
action=RailAction.ALLOW,
reason="Resposta aprovada pelos guardrails de saída legados.",
sanitized_text=final,
metadata={"legacy_decisions": serialized},
)
class AgentWorkflow:
"""Workflow principal com dois modos de roteamento.
Modos suportados por configuração:
ROUTING_MODE=router
input_guardrails -> routing_decision/EnterpriseRouter -> 1 agente -> output_guardrails
ROUTING_MODE=supervisor
input_guardrails -> routing_decision/Supervisor -> supervisor_agent -> N agentes -> consolidação
Em ambos os modos, memória/checkpoint/session usam tenant_id:agent_id:session_id.
"""
def __init__(self, llm, memory, telemetry, analytics, settings, observer: AgentObserver | None = None, tool_router=None, summary_memory=None):
self.llm = llm
self.memory = memory
self.telemetry = telemetry
self.analytics = analytics
self.observer = observer or AgentObserver(analytics=analytics)
self.settings = settings
self.tool_router = tool_router
self.summary_memory = summary_memory
self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry)
self.guardrails = GuardrailPipeline(
observer=self.observer,
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.output_supervisor_engine = OutputSupervisor(
rails=[LegacyOutputGuardrailRail(self.guardrails)],
observer=self.observer,
max_retries=int(getattr(settings, "OUTPUT_SUPERVISOR_MAX_RETRIES", 3)),
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
)
self.judges = JudgePipeline()
self.supervisor = Supervisor()
self.workflow_telemetry = WorkflowTelemetry(telemetry)
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
self.judge_telemetry = JudgeTelemetry(telemetry)
self.langgraph_telemetry = LangGraphDeepTelemetry(telemetry)
self.cache = create_cache(settings)
self.embedding_provider = create_embedding_provider(settings)
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
agent_kwargs = {"telemetry": telemetry, "tool_router": getattr(self, "tool_router", None), "rag_service": self.rag_service, "cache": self.cache, "settings": settings, "observer": self.observer, "memory": memory, "summary_memory": summary_memory}
self.billing = BillingAgent(llm, **agent_kwargs)
self.product = ProductAgent(llm, **agent_kwargs)
self.orders = OrdersAgent(llm, **agent_kwargs)
self.support = SupportAgent(llm, **agent_kwargs)
# The existing agent constructors intentionally keep their stable API.
# Long-term memory is injected as a runtime capability after creation.
for agent in (self.billing, self.product, self.orders, self.support):
agent.long_term_memory_manager = self.long_term_memory_manager
self.graph = self._build_graph()
def _node(self, name, fn):
async def _wrapped(state):
async with self.langgraph_telemetry.node(name, state):
return await fn(state)
return _wrapped
def _build_graph(self):
builder = StateGraph(AgentState)
builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
builder.add_node("routing_decision", self._node("routing_decision", self.routing_decision))
builder.add_node("billing_agent", self._node("billing_agent", self.billing_agent))
builder.add_node("product_agent", self._node("product_agent", self.product_agent))
builder.add_node("orders_agent", self._node("orders_agent", self.orders_agent))
builder.add_node("support_agent", self._node("support_agent", self.support_agent))
builder.add_node("handoff", self._node("handoff", self.handoff))
builder.add_node("human_handoff", self._node("human_handoff", self.human_handoff))
builder.add_node("end_session", self._node("end_session", self.end_session))
builder.add_node("supervisor_agent", self._node("supervisor_agent", self.supervisor_agent))
builder.add_node("output_supervisor", self._node("output_supervisor", self.output_supervisor))
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
builder.add_node("judge", self._node("judge", self.judge))
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory))
builder.add_node("persist", self._node("persist", self.persist))
builder.add_edge(START, "input_guardrails")
builder.add_conditional_edges(
"input_guardrails",
self._after_input_guardrails,
{"blocked": "output_guardrails", "continue": "routing_decision"},
)
builder.add_conditional_edges(
"routing_decision",
lambda s: s.get("route", "billing_agent"),
{
"billing_agent": "billing_agent",
"product_agent": "product_agent",
"orders_agent": "orders_agent",
"support_agent": "support_agent",
"handoff": "handoff",
"human_handoff": "human_handoff",
"end_session": "end_session",
"supervisor_agent": "supervisor_agent",
},
)
builder.add_edge("billing_agent", "output_supervisor")
builder.add_edge("product_agent", "output_supervisor")
builder.add_edge("orders_agent", "output_supervisor")
builder.add_edge("support_agent", "output_supervisor")
builder.add_edge("handoff", "output_supervisor")
builder.add_edge("human_handoff", "output_supervisor")
builder.add_edge("end_session", "output_supervisor")
builder.add_edge("supervisor_agent", "output_supervisor")
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist_long_term_memory")
builder.add_edge("persist_long_term_memory", "persist")
builder.add_edge("persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
def _after_input_guardrails(self, state):
return "blocked" if state.get("blocked") else "continue"
@staticmethod
def _input_guardrail_user_message(decisions, state, sanitized_text):
# Keep the technical guardrail reason in telemetry, but expose only a
# safe, actionable message to the end user. The message is intentionally
# routed through output_guardrails before persistence/delivery.
blocked = [d for d in decisions if not getattr(d, "allowed", True)]
first = blocked[0] if blocked else None
code = str(getattr(first, "code", "") or "").upper()
if code == "COER":
return (
"Não consegui entender sua última mensagem porque ela parece "
"incompleta ou ambígua. Pode reformular ou completar o que você quis dizer?"
)
if code == "INPUT_SIZE":
return "Sua mensagem ficou muito longa para eu processar de uma vez. Pode resumir ou dividir em partes?"
if code == "DLEX_IN":
return "Não posso usar essa informação da forma solicitada. Reformule o pedido sem incluir dados ou conteúdo restrito."
if code == "PINJ":
return "Não posso seguir instruções que tentem alterar as regras do atendimento. Posso continuar ajudando com a sua solicitação."
if code == "TOX":
return "Não consegui prosseguir com essa mensagem. Pode reformular o pedido para continuarmos o atendimento?"
if code == "CMP":
return "Não posso prosseguir com essa solicitação dessa forma. Posso ajudar com uma alternativa permitida."
return "Não consegui processar essa mensagem. Pode reformular para eu continuar o atendimento?"
async def input_guardrails(self, state):
if state.get("session_ended") is True:
answer = str(getattr(
self.settings,
"SESSION_ALREADY_ENDED_MESSAGE",
"Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.",
))
await self.telemetry.event(
"session.message.rejected_after_end",
{"session_id": state.get("conversation_key") or state.get("session_id")},
)
return {
"answer": answer,
"final_answer": answer,
"blocked": True,
"session_control": "END_SESSION",
"session_ended": True,
"next_state": "SESSION_ENDED",
}
async with self.telemetry.span(
"workflow.input_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("user_text"),
):
history_texts = [m.get("content", "") for m in state.get("history", [])]
guardrail_active_transaction = (
state.get("active_transaction")
if isinstance(state.get("active_transaction"), dict)
else {}
)
guardrail_transaction_status = str(
guardrail_active_transaction.get("status")
or state.get("transaction_status")
or ""
).strip().upper()
pending_call = state.get("pending_tool_call") if isinstance(state.get("pending_tool_call"), dict) else {}
if state.get("confirmation_required") and pending_call.get("tool_name"):
guardrail_transaction_status = "AWAITING_CONFIRMATION"
elif state.get("missing_parameters") and pending_call.get("tool_name"):
guardrail_transaction_status = "COLLECTING_PARAMETERS"
for mcp_result in reversed(list(state.get("mcp_results") or [])):
if not isinstance(mcp_result, dict):
continue
mcp_status = str(mcp_result.get("transaction_status") or "").strip().upper()
if mcp_status in {"COLLECTING_PARAMETERS", "AWAITING_CONFIRMATION"}:
guardrail_transaction_status = mcp_status
break
await self.observer.emit_grl(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
},
component="workflow.input_guardrails.start",
)
sanitized, decisions = await self.guardrails.run_input(
state["user_text"],
{
**(state.get("context") or {}),
"history_texts": history_texts,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"agent_profile": state.get("agent_profile") or {},
"transaction_status": guardrail_transaction_status,
"missing_parameters": list(state.get("missing_parameters") or []),
"active_transaction": guardrail_active_transaction,
"mcp_results": list(state.get("mcp_results") or []),
},
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("input", _decision)
await self.observer.emit_grl(
"002" if _decision.allowed else "004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
"rail_code": getattr(_decision, "code", None),
"allowed": bool(_decision.allowed),
"reason": getattr(_decision, "reason", None),
},
component="workflow.input_guardrails.decision",
)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("input", _decision)
await self.telemetry.event(
"guardrails.input.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
await self.observer.emit_grl(
"009",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "input",
"blocked": any(not d.allowed for d in decisions),
"decision_count": len(decisions),
},
component="workflow.input_guardrails.final",
)
if any(not d.allowed for d in decisions):
# A blocking input guardrail stops the turn before routing/tools.
# Clear turn-local routing/tool state so stale data from a prior
# turn cannot appear as if it was executed after the block.
user_message = self._input_guardrail_user_message(decisions, state, sanitized)
return {
"sanitized_input": sanitized,
"answer": user_message,
"final_answer": None,
"guardrail_decisions": [d.model_dump() for d in decisions],
"route": "blocked",
"intent": "input_guardrail_blocked",
"route_decision": {
"route": "blocked",
"agent": None,
"intent": "input_guardrail_blocked",
"confidence": 1.0,
"reason": "Entrada interrompida por guardrail antes do roteamento.",
"method": "guardrail",
"next_state": state.get("next_state"),
"handoff": False,
"metadata": {},
"domain": state.get("domain"),
"mcp_tools": [],
},
"mcp_tools": [],
"mcp_results": [],
"judge_results": [],
"blocked": True,
}
return {
"sanitized_input": sanitized,
"guardrail_decisions": [d.model_dump() for d in decisions],
"blocked": False,
}
async def routing_decision(self, state):
mode = getattr(self.settings, "ROUTING_MODE", "router")
async with self.telemetry.span(
"workflow.routing_decision",
session_id=state.get("conversation_key") or state.get("session_id"),
input={
"mode": mode,
"text": state.get("sanitized_input") or state.get("user_text"),
"previous_state": state.get("next_state"),
},
):
if mode == "supervisor":
plan = await self.supervisor.route_plan(state)
await self.langgraph_telemetry.edge("routing_decision", "supervisor_agent", state, {"method": "supervisor", "intent": plan.intent, "confidence": plan.confidence})
return {
"route": "supervisor_agent",
"intent": plan.intent,
"supervisor_plan": {
"agents": plan.agents,
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"metadata": plan.metadata,
},
"route_decision": {
"route": "supervisor_agent",
"agent": "supervisor",
"intent": plan.intent,
"confidence": plan.confidence,
"reason": plan.reason,
"method": "supervisor",
"metadata": plan.metadata,
},
}
decision = await self.router.route(state)
await self.langgraph_telemetry.edge("routing_decision", decision.route, state, {"method": getattr(decision, "method", None), "intent": decision.intent, "confidence": decision.confidence})
await self.observer.emit_ic(
"ROUTE_SELECTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": decision.route,
"intent": decision.intent,
"confidence": decision.confidence,
"method": getattr(decision, "method", None),
},
component="workflow.routing_decision",
)
return {
"route": decision.route,
"intent": decision.intent,
"route_decision": decision.model_dump(mode="json"),
"domain": decision.domain,
"mcp_tools": decision.mcp_tools,
"next_state": decision.next_state,
"active_agent": decision.agent,
"route_bypassed": decision.method == "continuity",
"session_control": (decision.metadata or {}).get("session_control", ""),
"human_handoff_requested": (decision.metadata or {}).get("session_control") == "HUMAN_HANDOFF",
"session_ended": (decision.metadata or {}).get("session_control") == "END_SESSION",
"continuity_signal": {
"decision": (decision.metadata or {}).get("continuity_decision"),
"confidence": decision.confidence if decision.method == "continuity" else None,
"reason": decision.reason if decision.method == "continuity" else None,
"profile": (decision.metadata or {}).get("continuity_profile"),
} if decision.method == "continuity" else {},
"pending_topics": (
(decision.metadata or {}).get("multi_intent_plan", {}).get("operations", [])[1:]
if (decision.metadata or {}).get("multi_intent_plan")
else list(state.get("pending_topics") or [])
),
}
async def billing_agent(self, state):
async with self.telemetry.span(
"workflow.agent.billing",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.billing.run(state)
async def product_agent(self, state):
async with self.telemetry.span(
"workflow.agent.product",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.product.run(state)
async def orders_agent(self, state):
async with self.telemetry.span(
"workflow.agent.orders",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.orders.run(state)
async def support_agent(self, state):
async with self.telemetry.span(
"workflow.agent.support",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"intent": state.get("intent")},
):
return await self.support.run(state)
async def supervisor_agent(self, state):
"""Executa um ou mais agentes no modo supervisor e consolida a resposta.
Este nó mantém o desenho de supervisor sem obrigar o restante do workflow
a conhecer quantos agentes foram acionados. Cada execução especializada
recebe o mesmo estado, mas com route/active_agent atualizados.
"""
plan = state.get("supervisor_plan") or {}
agents = plan.get("agents") or ["billing_agent"]
handlers = {
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
}
partials = []
mcp_results = []
async with self.telemetry.span(
"workflow.supervisor_agent",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"agents": agents, "intent": state.get("intent")},
):
for agent_name in agents:
handler = handlers.get(agent_name)
if handler is None:
continue
child_state = {**state, "route": agent_name, "active_agent": agent_name}
result = await handler(child_state)
partials.append({"agent": agent_name, "answer": result.get("answer", "")})
mcp_results.extend(result.get("mcp_results") or [])
if len(partials) == 1:
answer = partials[0]["answer"]
else:
joined = "\n\n".join(f"{p['agent']}: {p['answer']}" for p in partials)
answer = (
"[Supervisor] Consolidação de múltiplos agentes acionados.\n"
f"{joined}"
)
return {
"answer": answer,
"supervisor_results": partials,
"agent_responses": partials,
"mcp_results": mcp_results,
"next_state": "SUPERVISOR_ACTIVE",
}
async def handoff(self, state):
async with self.telemetry.span("workflow.handoff", session_id=state.get("session_id")):
target = (state.get("route_decision") or {}).get("metadata", {}).get("target_agent")
answer = (
"Vou redirecionar sua solicitação para o especialista correto. "
f"Destino sugerido: {target or 'agente especializado'}."
)
return {"answer": answer}
async def human_handoff(self, state):
session_id = state.get("conversation_key") or state.get("session_id")
async with self.telemetry.span("workflow.human_handoff", session_id=session_id):
answer = str(getattr(self.settings, "HUMAN_HANDOFF_MESSAGE", "Vou encaminhar seu atendimento para uma pessoa."))
await self.telemetry.event(
"session.human_handoff.requested",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"reason": (state.get("route_decision") or {}).get("reason"),
},
)
return {
"answer": answer,
"session_control": "HUMAN_HANDOFF",
"human_handoff_requested": True,
"session_ended": False,
"next_state": "HUMAN_HANDOFF_REQUESTED",
}
async def end_session(self, state):
session_id = state.get("conversation_key") or state.get("session_id")
async with self.telemetry.span("workflow.end_session", session_id=session_id):
answer = str(getattr(self.settings, "END_SESSION_MESSAGE", "Atendimento encerrado. Obrigado pelo contato."))
await self.telemetry.event(
"session.end.requested",
{
"session_id": session_id,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"reason": (state.get("route_decision") or {}).get("reason"),
},
)
return {
"answer": answer,
"session_control": "END_SESSION",
"session_ended": True,
"human_handoff_requested": False,
"next_state": "SESSION_ENDED",
}
async def output_supervisor(self, state):
"""Valida a resposta candidata com o OutputSupervisor corporativo.
Este nó não substitui o roteador/supervisor multiagente. Ele roda após o
agente gerar `answer` e antes dos judges/persistência, produzindo campos
supervisor_* no state e eventos GRL.001..GRL.009 via AgentObserver.
"""
drained = await drain_pending_topics(
state,
str(state.get("answer") or ""),
{
"billing_agent": self.billing.run,
"product_agent": self.product.run,
"orders_agent": self.orders.run,
"support_agent": self.support.run,
},
)
candidate = drained.answer
if not bool(getattr(self.settings, "ENABLE_OUTPUT_SUPERVISOR", True)):
return {
"answer": candidate,
"final_answer": candidate,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
"output_guardrails_already_applied": False,
"supervisor_action": "disabled",
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
**drained.state_patch,
}
context = {
**(state.get("context") or {}),
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"evidence": (list(drained.mcp_results) + list(drained.rag_results)) or (state.get("context") or {}).get("evidence"),
"tool_result": drained.mcp_results or (state.get("context") or {}).get("tool_result"),
"tool_executed": any(isinstance(item, dict) and item.get("ok") for item in drained.mcp_results),
"mcp_results": drained.mcp_results,
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"session_id": state.get("conversation_key") or state.get("session_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"supervisor_attempt": int(state.get("supervisor_attempt", 0)),
}
async with self.telemetry.span(
"workflow.output_supervisor",
session_id=state.get("conversation_key") or state.get("session_id"),
input=candidate,
):
decision = await self.output_supervisor_engine.evaluate(candidate, context)
action = decision.action.value
await self.telemetry.event(
"output_supervisor.completed",
{
"session_id": context["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"action": action,
"approved": decision.approved,
"guidance": decision.guidance,
},
)
await self.observer.emit_ic(
"IC.OUTPUT_SUPERVISOR_COMPLETED",
{
"session_id": context["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"action": action,
"approved": decision.approved,
"result_count": len(decision.results),
},
component="workflow.output_supervisor",
)
if decision.action in {RailAction.ALLOW, RailAction.SANITIZE, RailAction.OBSERVE}:
final_answer = decision.candidate
elif decision.action == RailAction.HANDOVER:
final_answer = "Vou encaminhar seu atendimento para continuidade com um especialista."
else:
final_answer = decision.fallback_message
return {
"answer": final_answer,
"final_answer": final_answer,
"supervisor_action": action,
"supervisor_guidance": decision.guidance,
"supervisor_attempt": int(state.get("supervisor_attempt", 0)) + (1 if decision.action == RailAction.RETRY else 0),
"supervisor_handover_reason": decision.handover_reason,
"output_supervisor_results": [
{
"code": r.code,
"action": r.action.value,
"reason": r.reason,
"guidance": r.guidance,
"metadata": r.metadata,
}
for r in decision.results
],
"output_guardrails_already_applied": True,
"pending_topics": drained.pending_topics,
"handled_topics": drained.handled_topics,
"agent_responses": drained.agent_responses,
"mcp_results": drained.mcp_results,
"rag_results": drained.rag_results,
**drained.state_patch,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [item for r in decision.results for item in (r.metadata or {}).get("legacy_decisions", [])],
}
async def output_guardrails(self, state):
if state.get("output_guardrails_already_applied"):
return {"final_answer": state.get("final_answer") or state.get("answer") or ""}
async with self.telemetry.span(
"workflow.output_guardrails",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("answer"),
):
await self.observer.emit_grl(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"route": state.get("route"),
"intent": state.get("intent"),
},
component="workflow.output_guardrails.start",
)
final, decisions = await self.guardrails.run_output(
state["answer"], state.get("context", {})
)
for _decision in decisions:
await self.guardrail_telemetry.evaluated("output", _decision)
await self.observer.emit_grl(
"002" if _decision.allowed else "004",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"rail_code": getattr(_decision, "code", None),
"allowed": bool(_decision.allowed),
"reason": getattr(_decision, "reason", None),
},
component="workflow.output_guardrails.decision",
)
if not _decision.allowed:
await self.guardrail_telemetry.blocked("output", _decision)
await self.telemetry.event(
"guardrails.output.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"decisions": [d.model_dump() for d in decisions],
},
)
await self.observer.emit_grl(
"009",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"phase": "output",
"blocked": any(not d.allowed for d in decisions),
"decision_count": len(decisions),
},
component="workflow.output_guardrails.final",
)
return {
"final_answer": final,
"guardrail_decisions": state.get("guardrail_decisions", [])
+ [d.model_dump() for d in decisions],
}
async def judge(self, state):
async with self.telemetry.span(
"workflow.judge",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"question": state.get("user_text"), "answer": state.get("final_answer")},
):
judge_context = dict(state.get("context", {}) or {})
judge_context["mcp_results"] = state.get("mcp_results", [])
judge_context["evidence"] = state.get("mcp_results", []) or judge_context.get("evidence")
judge_context["route"] = state.get("route")
judge_context["intent"] = state.get("intent")
# Judge sampling must see the finalized transaction state. These
# fields are populated by the agent/tool runtime before this node.
for key in (
"transaction_status",
"confirmation_required",
"confirmation_received",
"tool_policy_result",
"selected_tool_call",
"pending_tool_call",
):
judge_context[key] = state.get(key)
judge_context["transactional_tools"] = [
result.get("tool_name")
for result in state.get("mcp_results", [])
if isinstance(result, dict)
and (
(result.get("metadata") or {}).get("operation_type") == "transactional"
or result.get("awaiting_confirmation")
or result.get("transaction_status")
)
]
results = await self.judges.evaluate_all(
state["user_text"], state["final_answer"], judge_context
)
for _result in results:
await self.judge_telemetry.evaluated(_result)
await self.telemetry.event(
"judges.completed",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"results": [r.model_dump() for r in results],
},
)
return {"judge_results": [r.model_dump() for r in results]}
async def supervisor_review(self, state):
async with self.telemetry.span(
"workflow.supervisor_review",
session_id=state.get("conversation_key") or state.get("session_id"),
input=state.get("final_answer"),
):
ok, answer = await self.supervisor.review(
state["final_answer"], state.get("context", {})
)
await self.telemetry.event(
"supervisor.review.completed",
{"session_id": state.get("session_id"), "approved": ok},
)
return {"final_answer": answer if ok else answer}
async def persist_long_term_memory(self, state):
result = await self.long_term_memory_manager.persist_turn(state)
return {"long_term_memory_write_result": result}
async def persist(self, state):
async with self.telemetry.span(
"workflow.persist",
session_id=state.get("conversation_key") or state.get("session_id"),
input={"route": state.get("route"), "intent": state.get("intent")},
):
await self.observer.emit_ic(
"AGENT_COMPLETED",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"route_decision": state.get("route_decision"),
"judges": state.get("judge_results", []),
"mcp_tools": state.get("mcp_tools", []),
"mcp_results": state.get("mcp_results", []),
"rag_results": state.get("rag_results", []),
},
)
await self.observer.emit_noc(
"006",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
component="workflow.persist",
)
await self.telemetry.event(
"agent.completed",
{
"session_id": state.get("conversation_key") or state["session_id"],
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"route": state.get("route"),
"intent": state.get("intent"),
"answer_chars": len(state.get("final_answer") or ""),
},
)
return state
async def ainvoke(self, state):
thread_id = state.get("conversation_key") or state["session_id"]
config = {"configurable": {"thread_id": thread_id}}
async with self.telemetry.span(
"workflow.langgraph.ainvoke",
session_id=state.get("conversation_key") or state.get("session_id"),
user_id=state.get("context", {}).get("user_id"),
input={"user_text": state.get("user_text")},
tags=["langgraph", "agent-workflow", f"routing-mode:{getattr(self.settings, 'ROUTING_MODE', 'router')}",],
):
await self.workflow_telemetry.started("agent_workflow", state)
await self.observer.emit_noc(
"001",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"ura_call_id": (state.get("context") or {}).get("ura_call_id"),
},
component="workflow.ainvoke",
)
await self.observer.emit_ic(
"AGENT_STARTED",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"channel_id": (state.get("context") or {}).get("channel"),
"message_id": (state.get("context") or {}).get("message_id"),
"user_text_chars": len(state.get("user_text") or ""),
},
component="workflow.ainvoke",
)
try:
result = await self.graph.ainvoke(state, config=config)
await self.workflow_telemetry.completed("agent_workflow", result)
return result
except Exception as exc:
await self.workflow_telemetry.failed("agent_workflow", exc)
await self.observer.emit_noc(
"005",
{
"session_id": state.get("conversation_key") or state.get("session_id"),
"tenant_id": state.get("tenant_id"),
"agent_id": state.get("agent_id"),
"error": str(exc),
"exception_type": exc.__class__.__name__,
},
component="workflow.ainvoke",
)
raise

View File

@@ -0,0 +1,33 @@
default_agent_id: telecom_contas
agents:
- agent_id: telecom_contas
name: Agente Telecom Contas
description: Template de atendimento para faturas, produtos e suporte de telecom.
prompt_policy_path: ./config/agents/telecom_contas/prompt_policy.yaml
routing_config_path: ./config/routing.yaml
guardrails_config_path: ./config/agents/telecom_contas/guardrails.yaml
judges_config_path: ./config/agents/telecom_contas/judges.yaml
mcp_servers_config_path: ./config/mcp_servers.yaml
tools_config_path: ./config/tools.yaml
metadata:
domain: telecom
system_prefix: |
Você está executando o agent_template telecom_contas.
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
Não misture histórico ou decisões de outros agentes.
- agent_id: retail_orders
name: Agente Retail Pedidos
description: Template de varejo para pedidos, produtos, troca/devolução e garantia.
prompt_policy_path: ./config/agents/retail_orders/prompt_policy.yaml
routing_config_path: ./config/routing.yaml
guardrails_config_path: ./config/agents/retail_orders/guardrails.yaml
judges_config_path: ./config/agents/retail_orders/judges.yaml
mcp_servers_config_path: ./config/mcp_servers.yaml
tools_config_path: ./config/tools.yaml
metadata:
domain: retail
system_prefix: |
Você está executando o agent_template retail_orders.
Use somente políticas, memória, checkpoints, guardrails e judges deste agent_id.
Não misture histórico ou decisões de outros agentes.

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More