mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-11-06 03:14:12 +00:00
Compare commits
50 Commits
main
...
583d801d4e
| Author | SHA1 | Date | |
|---|---|---|---|
| 583d801d4e | |||
| 4712bcc0dc | |||
| e16d19083d | |||
| dd207b4078 | |||
| 2f9b053098 | |||
| 7a22b7af11 | |||
| e882afde25 | |||
| f00c2f721a | |||
| f62c7993bb | |||
| 3f884d028a | |||
| 05410adf09 | |||
| ffe47e8d49 | |||
| ae5887ce50 | |||
| 82636ec15b | |||
| 1711575abe | |||
| 2c3aa58b59 | |||
| d84d833694 | |||
| fe8c18093f | |||
|
|
f4638e287a | ||
|
|
e24963abe1 | ||
| 10a6f357dd | |||
| c8c83981c4 | |||
| 83bf961185 | |||
| 81f24d7357 | |||
| 1fd18531c0 | |||
| df2a1cbe1b | |||
| 42ab000c7b | |||
| 472d44074c | |||
|
|
faf5ca55ba | ||
| ac18d68eaf | |||
| 63d0fb51c4 | |||
|
|
a472daa1e4 | ||
|
|
d93efd8972 | ||
| 727997aa41 | |||
| 2496e831a1 | |||
| b5d2a12953 | |||
| bdc44b15d6 | |||
| 05077a91bc | |||
| 19c4024693 | |||
| 22e8f56146 | |||
| 0a72afb94e | |||
| 57df723a24 | |||
| 6e43cc0158 | |||
| d271031198 | |||
| 34d33350ae | |||
| ad778f6889 | |||
| 762f6fb024 | |||
| f9c66b4792 | |||
| 560e79d21b | |||
| 23d32bbcfc |
27
.env.example
27
.env.example
@@ -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
92
.idea/workspace.xml
generated
@@ -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 "source .venv/bin/activate && cd apps/agent_frontend && python -m http.server 5173"" />
|
||||||
|
<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 "source .venv/bin/activate && cd templates/agent_template_backend && python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"" />
|
||||||
|
<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 "source .venv/bin/activate && cd scripts && bash run_mcp_gateway.sh"" />
|
||||||
|
<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 "source .venv/bin/activate && cd scripts && bash run_mcp_servers.sh"" />
|
||||||
|
<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" />
|
||||||
|
|||||||
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Implementação — workflows transacionais determinísticos
|
||||||
|
|
||||||
|
## Entrega
|
||||||
|
|
||||||
|
Foi adicionada ao `agent_framework_oci` uma capacidade opcional para executar transações multi-etapas como workflows determinísticos compilados em LangGraph.
|
||||||
|
|
||||||
|
### Módulo novo
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/workflows/`
|
||||||
|
|
||||||
|
- `models.py`: contratos Pydantic e validação estrutural;
|
||||||
|
- `repository.py`: resolução de versão ativa e leitura de YAML imutável;
|
||||||
|
- `registry.py`: registro desacoplado de actions sync/async;
|
||||||
|
- `runtime.py`: compilação, cache e execução do StateGraph;
|
||||||
|
- `tool_executor.py`: integração com a política da tool;
|
||||||
|
- `__init__.py`: API pública.
|
||||||
|
|
||||||
|
### Política expandida
|
||||||
|
|
||||||
|
`ToolPolicy` agora aceita:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
execution:
|
||||||
|
mode: direct_tool | workflow | agent
|
||||||
|
workflow: nome_do_workflow
|
||||||
|
version: active | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
O default permanece `direct_tool`, preservando compatibilidade.
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Foram adicionados:
|
||||||
|
|
||||||
|
- `ENABLE_TRANSACTIONAL_WORKFLOWS=false`;
|
||||||
|
- `WORKFLOWS_PATH=./workflows`.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
Inclui um exemplo completo de devolução de pedido com:
|
||||||
|
|
||||||
|
- confirmação e campos obrigatórios pela política;
|
||||||
|
- workflow YAML versionado;
|
||||||
|
- actions de domínio no backend;
|
||||||
|
- bifurcação determinística baseada no resultado da validação.
|
||||||
|
|
||||||
|
## Validação realizada
|
||||||
|
|
||||||
|
- `tests/unit/test_tool_policies.py`: 4 testes aprovados;
|
||||||
|
- compilação Python de framework, template e novos testes: aprovada;
|
||||||
|
- o teste funcional novo do LangGraph foi criado, mas não pôde ser executado neste container porque `langgraph` não está instalado no ambiente. A dependência já está declarada no `pyproject.toml` do framework.
|
||||||
|
|
||||||
|
## Escopo e segurança
|
||||||
|
|
||||||
|
Esta entrega cria o motor e a integração de política. Para operações críticas em produção ainda é necessário conectar:
|
||||||
|
|
||||||
|
- execution store persistente;
|
||||||
|
- idempotência de negócio nas actions/APIs;
|
||||||
|
- autorização por escopo;
|
||||||
|
- telemetria IC/NOC específica de workflow;
|
||||||
|
- compensação/Saga quando aplicável;
|
||||||
|
- estratégia corporativa de timeout e retry.
|
||||||
|
|
||||||
|
Esses itens foram explicitamente documentados para evitar a falsa impressão de que retry por si só garante segurança transacional.
|
||||||
987
Documentacao/Implementando_Basic_Auth.md
Normal file
987
Documentacao/Implementando_Basic_Auth.md
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
# Implementando Basic Auth
|
||||||
|
|
||||||
|
Para validar **todo o circuito com Basic Auth**, você precisa configurar três relações distintas:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cliente de teste
|
||||||
|
└─ Basic Auth A ─► Agent Gateway :8010
|
||||||
|
└─ Basic Auth B ─► Agent Backend :8000
|
||||||
|
└─ Basic Auth C ─► MCP Gateway :8300
|
||||||
|
```
|
||||||
|
|
||||||
|
Há um detalhe importante: no pacote atual, a autenticação Basic já funciona para chamadas **de entrada**, mas os clientes internos ainda não enviam Basic Auth:
|
||||||
|
|
||||||
|
* `Agent Gateway → Agent Backend` não envia credencial;
|
||||||
|
* `Agent Backend → MCP Gateway` envia apenas Bearer Token.
|
||||||
|
|
||||||
|
Portanto, para testar o circuito inteiro com Basic Auth, faça os dois pequenos ajustes de código descritos abaixo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Preparar o ambiente
|
||||||
|
|
||||||
|
Considere que o ZIP foi extraído em:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci_authentication_v2_1
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie um único ambiente virtual para facilitar o teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Instale o framework e as dependências dos três componentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -U pip
|
||||||
|
|
||||||
|
pip install -e ./libs/agent_framework
|
||||||
|
|
||||||
|
pip install \
|
||||||
|
-r ./Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt \
|
||||||
|
-r ./apps/agent_gateway/requirements.txt \
|
||||||
|
-r ./apps/mcp_gateway/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirme a importação:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "from agent_framework.security import install_authentication; print('framework ok')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Criar três pares de Client ID e Secret
|
||||||
|
|
||||||
|
Use credenciais diferentes para cada trecho. Para teste local:
|
||||||
|
|
||||||
|
| Fluxo | Client ID | Secret de teste |
|
||||||
|
| ----------------------- | -------------------- | --------------------------- |
|
||||||
|
| Cliente → Agent Gateway | `tia-test` | `TiaGateway-Test-2026!` |
|
||||||
|
| Agent Gateway → Backend | `agent-gateway-test` | `GatewayBackend-Test-2026!` |
|
||||||
|
| Backend → MCP Gateway | `agent-backend-test` | `BackendMcp-Test-2026!` |
|
||||||
|
|
||||||
|
Esses valores são apenas para ambiente local. Não os reutilize em produção.
|
||||||
|
|
||||||
|
## Gerar os hashes
|
||||||
|
|
||||||
|
O script está em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
scripts/generate_secret_hash.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'TiaGateway-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'GatewayBackend-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
E:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'BackendMcp-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Você receberá três valores semelhantes a:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pbkdf2_sha256:310000:<salt>:<digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
Guarde-os temporariamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH_CLIENT_GATEWAY='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_GATEWAY_BACKEND='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_BACKEND_MCP='pbkdf2_sha256:310000:...'
|
||||||
|
```
|
||||||
|
|
||||||
|
O hash muda a cada execução porque o salt é aleatório. Isso é esperado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Configurar o Agent Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione ao final do `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: cliente/TIA -> Agent Gateway
|
||||||
|
AGENT_GATEWAY_AUTH_ENABLED=true
|
||||||
|
AGENT_GATEWAY_AUTH_MODE=basic
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_CLIENT_ID=tia-test
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_CLIENT_GATEWAY
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_REALM=agent-gateway
|
||||||
|
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
# Saída: Agent Gateway -> Agent Backend
|
||||||
|
BACKEND_AUTH_MODE=basic
|
||||||
|
BACKEND_AUTH_CLIENT_ID=agent-gateway-test
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Não coloque aspas no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo de backends já aponta o backend Contas para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
contas:
|
||||||
|
url: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/config/backends.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para este teste, mantenha apenas o backend `contas` ou force o backend no payload. Caso contrário, pedidos sobre ofertas e suporte podem ser roteados para portas em que nenhum backend está rodando.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Fazer o Agent Gateway enviar Basic Auth ao backend
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/global_supervisor/client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a classe `BackendClient` por uma versão que aceite autenticação Basic.
|
||||||
|
|
||||||
|
No início do arquivo, adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o construtor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BackendClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout_seconds: float = 120.0,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _auth(self) -> httpx.BasicAuth | None:
|
||||||
|
if self.basic_client_id and self.basic_secret:
|
||||||
|
return httpx.BasicAuth(
|
||||||
|
username=self.basic_client_id,
|
||||||
|
password=self.basic_secret,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `call_message`, troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(url, json=payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
auth=self._auth(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `health`, você pode manter `/health` público. Caso queira enviar autenticação também, use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.get(url, auth=self._auth())
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS,
|
||||||
|
basic_client_id=os.getenv("BACKEND_AUTH_CLIENT_ID"),
|
||||||
|
basic_secret=os.getenv("BACKEND_AUTH_SECRET"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso implementa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway → Agent Backend
|
||||||
|
Authorization: Basic base64(agent-gateway-test:GatewayBackend-Test-2026!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Configurar o Agent Backend autenticado
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste a seção de autenticação:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Gateway -> Agent Backend
|
||||||
|
AGENT_AUTH_ENABLED=true
|
||||||
|
AGENT_AUTH_MODE=basic
|
||||||
|
AGENT_AUTH_BASIC_CLIENT_ID=agent-gateway-test
|
||||||
|
AGENT_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_GATEWAY_BACKEND
|
||||||
|
AGENT_AUTH_BASIC_REALM=agent-contas
|
||||||
|
|
||||||
|
AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_AUTH_PUBLIC_PREFIXES=
|
||||||
|
```
|
||||||
|
|
||||||
|
Para usar o MCP Gateway:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
|
||||||
|
# Saída: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Para evitar dependências externas durante o primeiro teste, configure também:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
ENABLE_LANGFUSE=false
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
|
||||||
|
SESSION_REPOSITORY_PROVIDER=memory
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=memory
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=memory
|
||||||
|
CACHE_PROVIDER=memory
|
||||||
|
USAGE_REPOSITORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
Os nomes exatos de alguns providers podem depender do arquivo de configuração atual do framework. Caso o `.env.example` já contenha valores locais ou mock, preserve-os.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Fazer o Backend enviar Basic Auth ao MCP Gateway
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a implementação por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MCPGatewayClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
token: str | None = None,
|
||||||
|
timeout_seconds: int = 60,
|
||||||
|
auth_mode: str | None = None,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.auth_mode = (auth_mode or "").strip().lower()
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
if (
|
||||||
|
self.auth_mode == "basic"
|
||||||
|
and self.basic_client_id
|
||||||
|
and self.basic_secret
|
||||||
|
):
|
||||||
|
raw = f"{self.basic_client_id}:{self.basic_secret}".encode("utf-8")
|
||||||
|
encoded = base64.b64encode(raw).decode("ascii")
|
||||||
|
return {"Authorization": f"Basic {encoded}"}
|
||||||
|
|
||||||
|
if self.token:
|
||||||
|
return {"Authorization": f"Bearer {self.token}"}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def list_tools(self) -> dict[str, Any]:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/v1/tools",
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def invoke_tool(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
channel: str | None,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
business_context: dict[str, Any] | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": arguments or {},
|
||||||
|
"business_context": business_context or {},
|
||||||
|
"metadata": metadata or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/v1/tools/{tool_name}/invoke",
|
||||||
|
json=payload,
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/mcp/tool_router.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
auth_mode=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_AUTH_MODE",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_client_id=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_secret=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione estes campos em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/config/settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Próximo das configurações existentes de MCP Gateway:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCP_GATEWAY_AUTH_MODE: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_SECRET: str | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Há também uma factory local em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
app/mcp_gateway_client_factory.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from agent_framework.gateways import MCPGatewayClient
|
||||||
|
|
||||||
|
|
||||||
|
def build_mcp_gateway_client() -> MCPGatewayClient | None:
|
||||||
|
if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MCPGatewayClient(
|
||||||
|
base_url=os.getenv(
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=os.getenv("MCP_GATEWAY_TOKEN") or None,
|
||||||
|
timeout_seconds=int(
|
||||||
|
os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")
|
||||||
|
),
|
||||||
|
auth_mode=os.getenv("MCP_GATEWAY_AUTH_MODE"),
|
||||||
|
basic_client_id=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID"
|
||||||
|
),
|
||||||
|
basic_secret=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Configurar o MCP Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_ENABLED=true
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_BACKEND_MCP
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_REALM=mcp-gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PATHS=/health,/ready,/docs,/openapi.json,/redoc
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desabilitar o mecanismo Bearer legado
|
||||||
|
|
||||||
|
O MCP Gateway ainda possui um segundo mecanismo antigo, configurado dentro de:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize a seção:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso é necessário porque o novo middleware já faz a autenticação Basic. Caso o `auth_check()` legado continue habilitado, a requisição passará pelo Basic e depois será rejeitada por não possuir Bearer Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Subir os componentes
|
||||||
|
|
||||||
|
Use quatro terminais.
|
||||||
|
|
||||||
|
## Terminal 1 — MCP Servers
|
||||||
|
|
||||||
|
O MCP Gateway precisa ter pelo menos um servidor MCP disponível para demonstrar uma chamada real.
|
||||||
|
|
||||||
|
Na raiz do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba o servidor telecom:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.telecom_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8100 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Em outro terminal, caso queira também o retail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.retail_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8200 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Confira as URLs configuradas em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para execução local, devem apontar para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8100
|
||||||
|
```
|
||||||
|
|
||||||
|
e:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 2 — MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba usando `--env-file`. Isso é importante porque o middleware lê variáveis com `os.getenv()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8300 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste a saúde pública:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste um endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-backend-test:BackendMcp-Test-2026!' \
|
||||||
|
http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 3 — Agent Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
source ../../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8000 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial usada pelo Agent Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste mensagem diretamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "teste-backend-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 4 — Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8010 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8010/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial externa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Validar o circuito completo
|
||||||
|
|
||||||
|
Force o backend `contas` para evitar que o roteador selecione um backend não iniciado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8010/gateway/message \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"backend_id": "contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-circuito-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
O circuito esperado é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
curl
|
||||||
|
│ Basic tia-test
|
||||||
|
▼
|
||||||
|
Agent Gateway :8010
|
||||||
|
│ Basic agent-gateway-test
|
||||||
|
▼
|
||||||
|
Agent Backend :8000
|
||||||
|
│ Basic agent-backend-test
|
||||||
|
▼
|
||||||
|
MCP Gateway :8300
|
||||||
|
▼
|
||||||
|
MCP Server :8100 ou :8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Como comprovar cada autenticação
|
||||||
|
|
||||||
|
Faça testes negativos em cada trecho.
|
||||||
|
|
||||||
|
## Secret externo incorreto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:senha-errada' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secret do gateway para backend incorreto
|
||||||
|
|
||||||
|
Altere temporariamente no `apps/agent_gateway/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o Agent Gateway e envie uma mensagem.
|
||||||
|
|
||||||
|
O gateway deverá retornar erro de backend, normalmente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
502 Bad Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
O erro interno será originado por um:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
do Agent Backend.
|
||||||
|
|
||||||
|
## Secret do backend para MCP incorreto
|
||||||
|
|
||||||
|
Altere temporariamente:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o backend e execute uma frase que acione uma ferramenta MCP.
|
||||||
|
|
||||||
|
O backend deverá registrar falha na chamada ao MCP Gateway com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Verificação rápida de portas
|
||||||
|
|
||||||
|
No Linux ou WSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -lntp | grep -E ':8000|:8010|:8100|:8200|:8300'
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-NetTCPConnection -State Listen |
|
||||||
|
Where-Object LocalPort -in 8000,8010,8100,8200,8300 |
|
||||||
|
Sort-Object LocalPort
|
||||||
|
```
|
||||||
|
|
||||||
|
Você deverá ver:
|
||||||
|
|
||||||
|
```text
|
||||||
|
8000 Agent Backend
|
||||||
|
8010 Agent Gateway
|
||||||
|
8100 Telecom MCP Server
|
||||||
|
8200 Retail MCP Server
|
||||||
|
8300 MCP Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
O segredo original precisa existir no componente cliente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TIA ou curl:
|
||||||
|
TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Gateway:
|
||||||
|
GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Os componentes servidores armazenam apenas os hashes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway:
|
||||||
|
hash de TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
hash de GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
MCP Gateway:
|
||||||
|
hash de BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Em produção, os segredos originais e hashes devem vir de Vault ou Kubernetes Secret, não de arquivos `.env`.
|
||||||
@@ -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`.
|
||||||
@@ -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.
|
||||||
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal file
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal 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.
|
||||||
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal file
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal 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.
|
||||||
1887
MANUAL_REGRAS_AGENT_TEMPLATE_BACKEND.md
Normal file
1887
MANUAL_REGRAS_AGENT_TEMPLATE_BACKEND.md
Normal file
File diff suppressed because it is too large
Load Diff
1884
MANUAL_RULES_AGENT_TEMPLATE_BACKEND_EN.md
Normal file
1884
MANUAL_RULES_AGENT_TEMPLATE_BACKEND_EN.md
Normal file
File diff suppressed because it is too large
Load Diff
1120
README_en.md
1120
README_en.md
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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"),
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
@@ -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)
|
||||||
@@ -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]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
@@ -109,7 +110,6 @@ class AgentWorkflow:
|
|||||||
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
||||||
)
|
)
|
||||||
self.judges = JudgePipeline()
|
self.judges = JudgePipeline()
|
||||||
self.supervisor = Supervisor()
|
|
||||||
self.workflow_telemetry = WorkflowTelemetry(telemetry)
|
self.workflow_telemetry = WorkflowTelemetry(telemetry)
|
||||||
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
|
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
|
||||||
self.judge_telemetry = JudgeTelemetry(telemetry)
|
self.judge_telemetry = JudgeTelemetry(telemetry)
|
||||||
@@ -118,6 +118,14 @@ class AgentWorkflow:
|
|||||||
self.embedding_provider = create_embedding_provider(settings)
|
self.embedding_provider = create_embedding_provider(settings)
|
||||||
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
|
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
|
||||||
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
|
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
|
||||||
|
self.supervisor = Supervisor(
|
||||||
|
routing_rules=[
|
||||||
|
(intent.name, intent.agent, list(intent.keywords))
|
||||||
|
for intent in self.router.intents
|
||||||
|
if intent.enabled
|
||||||
|
],
|
||||||
|
fallback_agent=self.router.fallback_agent,
|
||||||
|
)
|
||||||
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}
|
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.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
self.product = ProductAgent(llm, **agent_kwargs)
|
self.product = ProductAgent(llm, **agent_kwargs)
|
||||||
@@ -160,7 +168,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 +205,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 +255,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 +295,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 +340,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 +443,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 +524,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 +587,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 +697,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 +929,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", []),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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
|
||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"),
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
@@ -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)
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -109,7 +110,6 @@ class AgentWorkflow:
|
|||||||
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
fail_fast=bool(getattr(settings, "GUARDRAILS_FAIL_FAST", True)),
|
||||||
)
|
)
|
||||||
self.judges = JudgePipeline()
|
self.judges = JudgePipeline()
|
||||||
self.supervisor = Supervisor()
|
|
||||||
self.workflow_telemetry = WorkflowTelemetry(telemetry)
|
self.workflow_telemetry = WorkflowTelemetry(telemetry)
|
||||||
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
|
self.guardrail_telemetry = GuardrailTelemetry(telemetry)
|
||||||
self.judge_telemetry = JudgeTelemetry(telemetry)
|
self.judge_telemetry = JudgeTelemetry(telemetry)
|
||||||
@@ -118,6 +118,14 @@ class AgentWorkflow:
|
|||||||
self.embedding_provider = create_embedding_provider(settings)
|
self.embedding_provider = create_embedding_provider(settings)
|
||||||
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
|
self.rag_service = RagService(settings, embedding_provider=self.embedding_provider, telemetry=telemetry)
|
||||||
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
|
self.router = EnterpriseRouter(settings, llm=llm, telemetry=telemetry)
|
||||||
|
self.supervisor = Supervisor(
|
||||||
|
routing_rules=[
|
||||||
|
(intent.name, intent.agent, list(intent.keywords))
|
||||||
|
for intent in self.router.intents
|
||||||
|
if intent.enabled
|
||||||
|
],
|
||||||
|
fallback_agent=self.router.fallback_agent,
|
||||||
|
)
|
||||||
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}
|
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.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
self.product = ProductAgent(llm, **agent_kwargs)
|
self.product = ProductAgent(llm, **agent_kwargs)
|
||||||
@@ -160,7 +168,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 +205,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 +255,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 +295,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 +340,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 +443,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 +524,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 +587,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 +697,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 +929,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", []),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
# Domain Requested LLM Composition
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Permitir que uma tool/workflow de domínio informe que o resultado operacional não deve ser devolvido diretamente ao usuário e precisa ser redigido pelo LLM oficial do agente, sem criar um gateway LLM dentro do domínio.
|
||||||
|
|
||||||
|
## Contrato
|
||||||
|
|
||||||
|
A tool pode devolver, em qualquer nível do resultado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_llm_composition": true,
|
||||||
|
"response_instruction": "Explique ao cliente a forma de devolução usando apenas os dados do workflow."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Também é aceito `response_instructions` como lista.
|
||||||
|
|
||||||
|
O `AgentRuntimeMixin` percorre recursivamente o resultado MCP. Quando a flag está ativa, `build_direct_mcp_answer()` retorna `None`; a resposta segue pelo LLM configurado no framework e recebe as evidências MCP no contexto normal do agente.
|
||||||
|
|
||||||
|
## Por que isso existe
|
||||||
|
|
||||||
|
Algumas operações, como pró-rata, possuem resultado determinístico e efeitos já concluídos, mas precisam de linguagem natural adequada ao canal. Antes, o domínio Contas possuía um gateway LLM próprio. Agora o domínio só declara a necessidade e a instrução; execução, credenciais, profiles, tracing e custos continuam no `agent_framework_oci`.
|
||||||
|
|
||||||
|
## Regras
|
||||||
|
|
||||||
|
- Não usar para decidir se uma transação deve ocorrer.
|
||||||
|
- Não usar para inventar valores ou protocolos.
|
||||||
|
- A instrução deve exigir que o LLM use somente as evidências retornadas pela tool/workflow.
|
||||||
|
- Pode coexistir com `requires_rag`; nesse caso o framework também executa RAG antes da composição.
|
||||||
51
Tuning-Performance/Domain_Requested_RAG/README.md
Normal file
51
Tuning-Performance/Domain_Requested_RAG/README.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
# Domain-Requested RAG
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Permitir que uma tool/workflow de domínio declare que o resultado MCP **não é suficiente** para produzir a resposta final e que o agente deve recuperar conhecimento usando o `RagService` oficial do `agent_framework_oci`.
|
||||||
|
|
||||||
|
O domínio não instancia banco vetorial, embeddings, LLM ou cliente RAG. Ele apenas devolve no resultado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"requires_rag": true,
|
||||||
|
"rag_queries": [
|
||||||
|
"Como cancelar o serviço Paramount+ no parceiro? Procedimento oficial de cancelamento."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fluxo
|
||||||
|
|
||||||
|
```text
|
||||||
|
Workflow/tool de domínio
|
||||||
|
|
|
||||||
|
| requires_rag + rag_queries
|
||||||
|
v
|
||||||
|
AgentRuntimeMixin
|
||||||
|
|
|
||||||
|
+-- impede direct MCP answer
|
||||||
|
+-- ignora SKIP_RAG_WHEN_MCP_SUFFICIENT para este resultado
|
||||||
|
+-- usa rag_query/rag_queries como query override
|
||||||
|
v
|
||||||
|
RagService
|
||||||
|
v
|
||||||
|
Vector/Graph store configurado no framework
|
||||||
|
v
|
||||||
|
LLM do agente com MCP evidence + RAG evidence
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regras
|
||||||
|
|
||||||
|
1. `requires_rag=false` ou ausente mantém o comportamento padrão.
|
||||||
|
2. `SKIP_RAG_WHEN_MCP_SUFFICIENT=true` continua válido para tools normais.
|
||||||
|
3. Quando `requires_rag=true`, o framework não usa `build_direct_mcp_answer()`.
|
||||||
|
4. `rag_query` aceita uma query; `rag_queries` aceita várias queries, preservadas e deduplicadas em ordem.
|
||||||
|
5. O domínio nunca executa `RagService` diretamente.
|
||||||
|
6. Guardrails de retrieval (`RAGSEC`, `RET_REL` etc.) continuam executando normalmente sobre o contexto recuperado.
|
||||||
|
|
||||||
|
## Exemplo Contas
|
||||||
|
|
||||||
|
No fluxo VAS Estratégico, após o cliente rejeitar a explicação e pedir cancelamento, a action devolve queries específicas por parceiro. O `VasAgent` usa o `RagService` do framework para recuperar o procedimento oficial e compor a resposta.
|
||||||
|
|
||||||
|
Isso substitui o comportamento legado em que o backend Contas possuía uma busca RAG própria dentro de `vas_strategic()`.
|
||||||
16
Tuning-Performance/External_Guardrails_Judges/README.md
Normal file
16
Tuning-Performance/External_Guardrails_Judges/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# External Guardrails / Judges
|
||||||
|
|
||||||
|
Este cenário de tuning mede extensões de política do agente. Antes de interpretar os resultados, implemente e valide o contrato descrito no [guia completo](../../docs/EXTERNAL_GUARDRAILS_JUDGES.md).
|
||||||
|
|
||||||
|
O cenário deve comparar, no mínimo, latência p50/p95/p99, taxa de allow/deny/sanitize, erros e timeouts, custo de judges com LLM e comportamento fail-open/fail-closed. Separe resultados por código/nome e estágio, sem registrar prompts, PII ou payloads financeiros.
|
||||||
|
|
||||||
|
Cuidados de leitura:
|
||||||
|
|
||||||
|
- `type: external` afeta apenas o componente declarado;
|
||||||
|
- implementação síncrona usa worker thread e implementação `async` usa o event loop;
|
||||||
|
- judges executam concorrentemente, então meça contenção do provedor e do pool;
|
||||||
|
- compare a mesma carga com extensão desligada para obter o overhead incremental;
|
||||||
|
- execute casos transacionais separadamente, pois `always_run_for_transactional` ignora a amostragem;
|
||||||
|
- no template corrente os pipelines são globais; caminhos por agente não implicam isolamento automático.
|
||||||
|
|
||||||
|
Antes do benchmark, rode `pytest -q tests/test_external_guardrails_judges_documentation.py` para provar que os exemplos do manual ainda carregam e executam.
|
||||||
@@ -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
|
||||||
###############################################################################
|
###############################################################################
|
||||||
@@ -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
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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}"
|
||||||
@@ -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"]
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Exemplos de uso do template backend enterprise."""
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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",
|
||||||
|
)
|
||||||
@@ -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})
|
||||||
@@ -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})
|
||||||
@@ -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()
|
||||||
@@ -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")),
|
||||||
|
)
|
||||||
@@ -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})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
@@ -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)
|
||||||
@@ -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]
|
||||||
@@ -0,0 +1,943 @@
|
|||||||
|
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.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)
|
||||||
|
self.supervisor = Supervisor(
|
||||||
|
routing_rules=[
|
||||||
|
(intent.name, intent.agent, list(intent.keywords))
|
||||||
|
for intent in self.router.intents
|
||||||
|
if intent.enabled
|
||||||
|
],
|
||||||
|
fallback_agent=self.router.fallback_agent,
|
||||||
|
)
|
||||||
|
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
|
||||||
@@ -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.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user