Compare commits

..

25 Commits

Author SHA1 Message Date
d84d833694 nova funcionalidade: reconciliacao temporal 2026-09-02 13:04:56 -03:00
fe8c18093f Ajustes no template para definicao de campos para extracao 2026-09-01 20:58:51 -03:00
T3782834
f4638e287a ajuste no contas 2026-09-01 11:23:22 -03:00
T3782834
e24963abe1 ajuste no guardraild COE 2026-08-31 21:11:51 -03:00
10a6f357dd Conferencia de parametros coletados na transacao 2026-08-31 17:27:35 -03:00
c8c83981c4 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 21:04:31 -03:00
83bf961185 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 11:22:10 -03:00
81f24d7357 Ajustes conforme relatorio de testes 2026-08-27 2026-08-29 10:03:42 -03:00
1fd18531c0 Documentation organization 2026-08-27 21:11:01 -03:00
df2a1cbe1b bugfix: oci_openai provider 2026-08-27 20:30:27 -03:00
42ab000c7b Documentation organization 2026-08-27 10:28:23 -03:00
472d44074c Documentation organization 2026-08-27 09:41:46 -03:00
T3782834
faf5ca55ba new feature: Integration with kbdb Autonomous 2026-08-26 10:15:51 -03:00
ac18d68eaf new feature: External guardrails/judges 2026-08-24 11:55:07 -03:00
63d0fb51c4 new feature: External guardrails/judges 2026-08-24 11:26:08 -03:00
T3782834
a472daa1e4 adjustments: transaction parameter extraction 2026-08-21 23:06:54 -03:00
T3782834
d93efd8972 adjustments: transaction parameter extraction 2026-08-21 22:44:36 -03:00
727997aa41 Adding transaction state 2026-08-21 17:59:06 -03:00
2496e831a1 Adding transaction state 2026-08-21 16:47:13 -03:00
b5d2a12953 bugfixes: router stickness vs transaction workflow parameters 2026-08-21 14:34:30 -03:00
bdc44b15d6 bugfix: enviar subject para contexto do guardrail 2026-08-21 11:01:48 -03:00
05077a91bc bugfix: aofertas 2026-08-21 10:47:19 -03:00
19c4024693 new feature: reasoning_content for compatible models in ainvoke_response() 2026-08-21 07:40:21 -03:00
22e8f56146 bugfixes: router stickness vs transaction workflow parameters 2026-08-20 23:29:56 -03:00
0a72afb94e bugfixes: router stickness vs transaction workflow parameters 2026-08-20 22:58:25 -03:00
1380 changed files with 71349 additions and 2224 deletions

View File

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

14
.idea/workspace.xml generated
View File

@@ -4,7 +4,9 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders" /> <list default="true" id="30a0e1d8-9d7d-469b-b241-f300911cee8a" name="Changes" comment="Ajustes na documentação e remanejamento dos folders">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -50,12 +52,13 @@
"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.252": "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",
"git-widget-placeholder": "master", "git-widget-placeholder": "main",
"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": "D:/Dropbox/ORACLE/TIM/FY27/Commits/agent_platform_oci",
"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)",
@@ -68,8 +71,8 @@
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.29188.36" /> <option value="bundled-jdk-30f59d01ecdd-cffe25b9f5b3-intellij.indexing.shared.core-IU-253.28294.334" />
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-IU-251.29188.36" /> <option value="bundled-js-predefined-d6986cc7102b-c7e53b3be11b-JavaScript-IU-253.28294.334" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -92,6 +95,7 @@
<workItem from="1785414225783" duration="148000" /> <workItem from="1785414225783" duration="148000" />
<workItem from="1785414447653" duration="704000" /> <workItem from="1785414447653" duration="704000" />
<workItem from="1785630146329" duration="316000" /> <workItem from="1785630146329" duration="316000" />
<workItem from="1787832640995" duration="4662000" />
</task> </task>
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders"> <task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

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

View File

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

212
README.md
View File

@@ -18,6 +18,27 @@ O objetivo é que cada novo agente implemente apenas sua lógica de domínio —
>**Note: Se deseja ir direto e testar a DEMO, vá até a Seção 17 e 18.** >**Note: Se deseja ir direto e testar a DEMO, vá até a Seção 17 e 18.**
## Índice de Desenvolvimento — Agent Framework OCI
### Outros idiomas
- [Developer documentation in English](README_en.md)
- [Índice técnico detalhado em Português](docs/developer/pt/INDEX_DEVELOPER_GUIDE.md)
- [Detailed technical index in English](docs/developer/en/INDEX_DEVELOPER_GUIDE.md)
### Como usar esta documentação
A documentação possui três níveis:
1. **Tutorial principal:** este [`README.md`](README.md) — criação, configuração, execução e teste de um agente do início ao fim.
2. **Arquitetura:** [01 — Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) — componentes, responsabilidades e onde implementar cada coisa.
3. **Referências especializadas:** manuais `02` a `12` — implementação profunda e troubleshooting por capacidade.
Se você está começando um novo agente, siga este `README.md` desde o início. Para aprofundamento ou troubleshooting, use os links abaixo.
Se algo não está funcionando ou se deseja entender melhor funcionalidades da arquitetura do Agent Framework OCI, vá até [34. Funcionalidades Avançadas](#34-funcionalidades-avançadas). Você vai encontrar detalhamento sobre funcionalidades avançadas, como conceitos, exemplos e manuais de utilização.
## SPECs / SDDs da Agent Platform OCI ## SPECs / SDDs da Agent Platform OCI
@@ -2150,71 +2171,30 @@ trace_id
#### 5.1.1.21.3. Instrumentação automática do cliente OpenAI pelo Langfuse #### 5.1.1.21.3. Instrumentação automática do cliente OpenAI pelo Langfuse
O padrão oficial do framework é:
```python ```env
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
```
habilita a instrumentação automática do cliente OpenAI pelo Langfuse.
Quando habilitada, todas as chamadas realizadas através do cliente OpenAI instrumentado passam a gerar automaticamente spans e generations detalhadas no Langfuse.
Benefícios
Com a instrumentação automática ativada, o Langfuse passa a registrar informações como:
* OpenAI-generation
* Prompt enviado ao modelo
* Resposta retornada pelo modelo
* Modelo utilizado
* Quantidade de tokens
* Custos estimados
* Latência da chamada
* Erros de execução
Essas informações ficam associadas ao trace principal da conversa, facilitando análise, troubleshooting e auditoria.
Comportamento quando desabilitado
Quando:
```python
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
``` ```
ou a variável não está definida: O framework já instrumenta as chamadas LLM por meio de `Telemetry.generation(...)`, preservando `trace_id`, `session_id`, `user_id`, metadados, tokens, custos, latência e o relacionamento pai/filho dentro do trace de negócio. Por esse motivo, a auto-instrumentação do cliente OpenAI não é necessária no fluxo normal do framework.
* As chamadas LLM continuam funcionando normalmente. Quando `false`:
* Os spans customizados do framework continuam sendo emitidos.
* O Langfuse deixa de criar automaticamente as entradas OpenAI-generation.
* Menos detalhes ficam disponíveis para análise das chamadas ao modelo.
Quando utilizar * as chamadas LLM continuam funcionando normalmente;
* prompts, respostas, modelo, tokens, custos e latência continuam disponíveis pela telemetria explícita do framework;
* as generations permanecem correlacionadas ao trace principal da requisição;
* evita-se dupla instrumentação e `OpenAI-generation` como trace raiz separado.
Recomenda-se habilitar em: A opção `true` existe apenas para compatibilidade ou diagnóstico de código que chama diretamente o SDK OpenAI/OpenAI-compatible fora da camada de `Telemetry` do framework. Nesses casos, o wrapper `langfuse.openai` pode capturar automaticamente essas chamadas. Entretanto, em uma aplicação que já usa a instrumentação nativa do framework, mantê-la habilitada pode gerar duplicidade de observations, contagem duplicada de tokens/custos ou traces independentes quando não houver um parent Langfuse ativo.
* Ambientes de desenvolvimento. ```env
* Ambientes de homologação. # Padrão recomendado para todos os templates e ambientes do framework
* Ambientes de produção que necessitem observabilidade detalhada das chamadas LLM. ENABLE_LANGFUSE=true
* Cenários de troubleshooting, tuning de prompts e análise de custos. ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
```
Observação Todos os arquivos `.env.example` distribuídos pelo projeto mantêm essa opção explicitamente em `false`. Se um componente externo precisar de captura automática, habilite-a somente naquele deployment e valide a árvore de traces no Langfuse.
Esta configuração afeta apenas a telemetria automática do Langfuse.
Ela não altera:
* O comportamento dos agentes.
* O roteamento do Supervisor.
* Guardrails.
* Judges.
* MCP Tool Router.
* Fluxos LangGraph.
Seu único objetivo é enriquecer a observabilidade das chamadas realizadas ao modelo de linguagem.
---
### 5.1.1.22. Recomendações de arquitetura
#### 5.1.1.22.1. Para demos e desenvolvimento #### 5.1.1.22.1. Para demos e desenvolvimento
@@ -11176,3 +11156,123 @@ A adoção das funcionalidades do `Tuning-Performance` pode proporcionar:
* comportamento consistente entre diferentes agentes e projetos. * comportamento consistente entre diferentes agentes e projetos.
O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção. O conteúdo desta pasta deve ser tratado como uma extensão adicional do framework. Sua utilização requer implementação, configuração, testes funcionais e validação das regras de negócio antes da implantação em produção.
### Buscar pelo problema
| Problema / dúvida | O que normalmente está envolvido | Onde procurar |
|---|---|---|
| O framework não encontra o agente/intenção correta | routing, intents, threshold, modo determinístico/LLM | [Routing e Stickiness](docs/developer/pt/02_routing_stickiness_and_intent_shift.md) |
| O agente fica preso no mesmo assunto e não troca de intent | route stickiness, intent shift, handoff | [Routing e Stickiness](docs/developer/pt/02_routing_stickiness_and_intent_shift.md) |
| Uma resposta que deveria preencher parâmetro é interpretada como novo intent | precedência transacional, parameter extraction | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) |
| A transação fica pedindo o mesmo parâmetro | estado transacional, extractor, schema | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| A confirmação “sim/não” não continua o fluxo | confirmation state, transaction state | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) |
| Uma fala inválida durante um `expected_input` vira `CONTINUAR` em vez de pedir esclarecimento | `semantic_classifier.unmatched_value`, `reprompt`, `contextual_reentry`, COER delegado | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| Uma transação encerrada reaparece | checkpoint antigo versus estado transacional ativo | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [LTM/Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| O sistema diz que executou algo, mas não existe evidência | MCP result, estado `COMPLETED`, judges transacionais | [Workflows Transacionais](docs/developer/pt/03_transaction_workflows_and_state.md) e [Guardrails/Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Uma tool não aparece ou não é encontrada | `tools.yaml`, catálogo MCP, discovery | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| MCP Server não aparece no catálogo | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) e [Gateways](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md) |
| Parâmetros enviados à tool estão errados | schema, mapping, BusinessContext, extractor | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Uma operação transacional executa sem confirmação | tool policy, `require_confirmation` | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Uma busca por nome exige correspondência exata demais | extração/mapeamento de parâmetros e lógica do agente | [MCP/Tools](docs/developer/pt/04_mcp_integration_tools_and_policies.md) |
| Recebo 401 entre gateway/backend/MCP | Basic Auth, credenciais por hop | [Gateways e Auth](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md) |
| Preciso decidir se algo pertence ao framework ou ao agente | boundary core/agente | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) |
| Guardrail específico de um agente está quebrando outro | extensibilidade, imports de domínio no core | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Uma frase incompleta recebe mensagem genérica de “regra de segurança” | feedback de input guardrail, `COER`, blocked-turn state | [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| `route=blocked` aparece junto com tools/resultados de outro turno | limpeza de estado do turno bloqueado | [Feedback de Guardrails de Entrada](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md) |
| Workflow conclui e gera protocolo, mas a resposta final vira mensagem de segurança | `expected_protocols`, `CMP`, `DLEX_OUT`, ordem de `output_guardrails` | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Judge não roda em uma transação | sampling, `always_run_for_transactional`, sinais transacionais | [Guardrails e Judges](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md) |
| Groundedness está avaliando sem contexto correto | RAG context, MCP evidence, judge inputs | [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| RAG não encontra conteúdo | provider, ingestão, embeddings, configuração | [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| Não sei se usar RAG, memória ou tool | separação de responsabilidades | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) e [RAG/Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md) |
| Memória desaparece ao trocar de sessão | LTM versus conversation memory | [LTM e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| Memória de um cliente/agente aparece em outro | identity key, tenant/agent/customer isolation | [LTM e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md) |
| Preciso recuperar `reasoning_content` | `ainvoke_response()` | [LLM Rich Response](docs/developer/pt/09_llm_rich_response_reasoning.md) |
| `reasoning_content` vem `None` | provider/model não expõe o campo | [LLM Rich Response](docs/developer/pt/09_llm_rich_response_reasoning.md) |
| Há chamadas LLM desnecessárias | routing determinístico, concorrência, cache | [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Há deadlock ou espera entre event loops | cross-loop sequence/runtime | [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Logs/traces não correlacionam o mesmo agente | labels, IDs e mapeamento de observabilidade | [Observabilidade](docs/developer/pt/11_observability_persistence_and_operational_readiness.md) |
| Sequence está interferindo no processamento | implementação assíncrona de sequência | [Observabilidade](docs/developer/pt/11_observability_persistence_and_operational_readiness.md) e [Performance](docs/developer/pt/10_performance_cache_and_async_runtime.md) |
| Um exemplo antigo não compila | documentação histórica versus API atual | [Validação README x Código](docs/developer/pt/VALIDATION_README_ALIGNMENT.md) |
| Preciso criar um agente novo do zero | fluxo completo | [`README.md`](README.md) |
| Preciso saber onde colocar uma nova feature | arquitetura e boundaries | [Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md) |
### 34. Funcionalidades Avançadas
### [01 — Arquitetura e Conceitos](docs/developer/pt/01_architecture_and_concepts.md)
**O que é:** visão dos componentes, contratos e limites de responsabilidade.
**Use quando:** precisar entender a plataforma, decidir onde implementar algo ou evitar acoplamento entre core e agente.
### [02 — Routing, Route Stickiness e Intent Shift](docs/developer/pt/02_routing_stickiness_and_intent_shift.md)
**O que é:** referência completa de descoberta de agente/intent, stickiness, handoff e mudança de intenção.
**Use quando:** a mensagem cai no agente errado, não troca de intent ou perde continuidade.
### [03 — Workflows Transacionais e Estado](docs/developer/pt/03_transaction_workflows_and_state.md)
**O que é:** ciclo transacional multi-turno, estados, confirmação, pausa/retomada, `expected_input`, `semantic_classifier`, `unmatched_value`/`reprompt` e evidência operacional.
**Use quando:** há loops, confirmações incorretas, retomadas erradas, `CONTINUAR`/`contextual_reentry` indevido, `reprompt` ausente ou operações críticas.
### [04 — MCP, Tools, Policies e Extração de Parâmetros](docs/developer/pt/04_mcp_integration_tools_and_policies.md)
**O que é:** referência de tools, MCP Servers, mappings, policies e parameter extraction.
**Use quando:** integração/execução de tool está incorreta ou precisa ser criada.
### [05 — Agent Gateway, MCP Gateway e Autenticação](docs/developer/pt/05_agent_gateway_mcp_gateway_and_auth.md)
**O que é:** responsabilidades dos gateways, governança e autenticação entre componentes.
**Use quando:** houver problema de entrada, catálogo, autorização, 401 ou deployment dos gateways.
### [06 — Guardrails, Judges e Avaliação Transacional](docs/developer/pt/06_guardrails_judges_and_transaction_evaluation.md)
**O que é:** validações nativas/externas, judges, grounding e regras para turnos transacionais.
**Use quando:** uma validação bloqueia, não roda ou produz avaliação incorreta.
### [07 — RAG, BusinessContext e Grounding](docs/developer/pt/07_rag_business_context_and_grounding.md)
**O que é:** providers de RAG, contexto recuperado, BusinessContext e grounding.
**Use quando:** conhecimento recuperado não chega corretamente ao agente/judge.
### [08 — Long-Term Memory e Checkpoint](docs/developer/pt/08_long_term_memory_and_checkpoint.md)
**O que é:** memória durável, memória conversacional, identidade e snapshots de estado.
**Use quando:** contexto some, vaza ou workflow retoma do lugar errado.
### [09 — LLM Rich Response e reasoning_content](docs/developer/pt/09_llm_rich_response_reasoning.md)
**O que é:** resposta estruturada de inferência além do `str` retornado por `ainvoke()`.
**Use quando:** consumidores precisam de metadados, usage ou reasoning disponibilizado pelo provider.
### [10 — Performance, Cache e Runtime Assíncrono](docs/developer/pt/10_performance_cache_and_async_runtime.md)
**O que é:** otimizações de concorrência, cache, LLM e event loops.
**Use quando:** houver latência evitável, processamento serial ou deadlock.
### [11 — Observabilidade, Persistência e Prontidão Operacional](docs/developer/pt/11_observability_persistence_and_operational_readiness.md)
**O que é:** correlação, eventos, labels, sequence, persistência e diagnóstico.
**Use quando:** for necessário provar o caminho executado ou diagnosticar produção.
### [12 — Feedback de Guardrails de Entrada e Turnos Bloqueados](docs/developer/pt/12_input_guardrail_feedback_and_blocked_turns.md)
**O que é:** semântica de mensagens públicas para bloqueios de input, limpeza do estado do turno e passagem da resposta pelos guardrails de saída.
**Use quando:** um `COER`/guardrail de entrada gera mensagem genérica, `route=blocked` carrega resultados antigos ou há dúvida sobre a precedência entre input guardrails, routing e tools.
### Tutorial principal
[`README.md`](README.md) continua sendo a referência para o passo a passo completo:
`arquitetura → configuração → criação do agente → registro → estado → routing → tools → MCP → identidade → execução → testes → gateways → memória → RAG`.

View File

@@ -18,6 +18,28 @@ The goal is for each new agent to implement only its domain logic — prompts, b
>**Note: If you want to test the DEMO, go to the Section 17 and 18.** >**Note: If you want to test the DEMO, go to the Section 17 and 18.**
## Developer Index — Agent Framework OCI
### Other languages
- [Documentação de desenvolvimento em Português](README.md)
- [Detailed technical index in English](docs/developer/en/INDEX_DEVELOPER_GUIDE.md)
- [Índice técnico detalhado em Português](docs/developer/pt/INDEX_DEVELOPER_GUIDE.md)
### How to use this documentation
The documentation has three clear levels:
1. **Main tutorial:** this [`README_en.md`](README_en.md) — build, configure, run and test an agent end to end.
2. **Architecture:** [01 — Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) — components, boundaries and implementation placement.
3. **Specialized references:** manuals `02` through `12` — deep implementation and troubleshooting by capability.
If you are creating a new agent, follow this `README_en.md` from the beginning. For deeper implementation details or troubleshooting, use the links below.
If something is not working or if you want to understand the features of the Agent Framework OCI architecture, go to [34. Advanced Features](#34-advanced-features)
## SPECs / SDDs of the Agent Platform OCI ## SPECs / SDDs of the Agent Platform OCI
The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI. The Agent Platform OCI documentation is organized into numbered SPECs/SDDs, each covering an architectural, operational, or governance area of the platform. The objective is to standardize the construction, evolution, operation, and certification of enterprise agents based on the Agent Framework OCI.
@@ -2145,71 +2167,30 @@ trace_id
#### 5.1.1.21.3. Automatic Langfuse instrumentation for the OpenAI client #### 5.1.1.21.3. Automatic Langfuse instrumentation for the OpenAI client
```python The framework's official default is:
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
```
enables automatic Langfuse instrumentation for the OpenAI client. ```env
When enabled, every request executed through the Langfuse-instrumented OpenAI client automatically generates detailed spans and generations within Langfuse.
Benefits
With automatic instrumentation enabled, Langfuse can automatically capture and display information such as:
* OpenAI-generation
* Prompt sent to the model
* Model response
* Model name used
* Token consumption
* Estimated costs
* Request latency
* Execution errors
All of this information is linked to the main conversation trace, making troubleshooting, auditing, and performance analysis significantly easier.
Behavior When Disabled
When:
```python
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
``` ```
or when the variable is not defined: The framework already instruments LLM calls through `Telemetry.generation(...)`, preserving `trace_id`, `session_id`, `user_id`, metadata, token usage, cost, latency, and the parent/child relationship inside the business trace. Therefore, OpenAI client auto-instrumentation is not required in the normal framework path.
* LLM calls continue to function normally. When set to `false`:
* Custom framework spans are still emitted.
* Langfuse no longer automatically creates OpenAI-generation entries.
* Less detailed information is available for analyzing model interactions.
Recommended Usage * LLM calls continue to work normally;
* prompts, responses, model, tokens, costs, and latency remain available through the framework's explicit telemetry;
* generations remain correlated with the main request trace;
* duplicate instrumentation and standalone `OpenAI-generation` root traces are avoided.
It is recommended to enable this setting in: The `true` option exists only for compatibility or diagnostics for code that calls the OpenAI/OpenAI-compatible SDK directly outside the framework `Telemetry` layer. In such cases, the `langfuse.openai` wrapper can automatically capture those calls. In an application already using the framework's native instrumentation, keeping it enabled may create duplicate observations, duplicate token/cost accounting, or independent traces when no active Langfuse parent exists.
* Development environments ```env
* Testing and staging environments # Recommended default for every framework template and environment
* Production environments that require detailed LLM observability ENABLE_LANGFUSE=true
* Prompt engineering, troubleshooting, and cost analysis scenarios ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
```
Important Note Every `.env.example` distributed with the project explicitly keeps this option set to `false`. If an external component requires automatic capture, enable it only for that deployment and validate the trace tree in Langfuse.
This setting only affects Langfuse automatic telemetry and observability.
It does not change:
* Agent behavior
* Supervisor routing
* Guardrails
* Judges
* MCP Tool Router
* LangGraph workflows
Its sole purpose is to enrich the observability of language model interactions and provide more detailed execution insights within Langfuse.
---
### 5.1.1.22. Architecture recommendations
#### 5.1.1.22.1. For demos and development #### 5.1.1.22.1. For demos and development
@@ -11082,3 +11063,116 @@ Adopting the `Tuning-Performance` capabilities can provide:
* consistent behavior across agents and projects. * consistent behavior across agents and projects.
The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment. The content of this folder should be treated as an additional framework extension. Its use requires implementation, configuration, functional testing, and business-rule validation before production deployment.
### Search by problem
| Problem / question | Usually involves | Go to |
|---|---|---|
| Framework selects the wrong agent/intent | routing, intents, thresholds, deterministic/LLM mode | [Routing and Stickiness](docs/developer/en/02_routing_stickiness_and_intent_shift.md) |
| Agent stays stuck on the same subject | route stickiness, intent shift, handoff | [Routing and Stickiness](docs/developer/en/02_routing_stickiness_and_intent_shift.md) |
| A parameter answer is mistaken for a new intent | transaction precedence, parameter extraction | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) |
| Transaction keeps asking for the same parameter | transaction state, extractor, schema | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| “yes/no” confirmation does not continue the flow | confirmation state | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) |
| Invalid input during `expected_input` becomes `CONTINUAR` instead of asking for clarification | `semantic_classifier.unmatched_value`, `reprompt`, `contextual_reentry`, delegated COER | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [Input Guardrail Feedback](docs/developer/en/12_input_guardrail_feedback_and_blocked_turns.md) |
| A closed transaction reappears | old checkpoint vs active transaction | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [LTM/Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| System claims an operation ran but there is no evidence | MCP results, `COMPLETED`, transaction judges | [Transactional Workflows](docs/developer/en/03_transaction_workflows_and_state.md) and [Guardrails/Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| A tool is missing | tools config, MCP catalog/discovery | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| MCP Server is missing from catalog | registration, manifest/discovery, MCP Gateway | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) and [Gateways](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
| Tool parameters are wrong | schema, mapping, BusinessContext, extraction | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| Transactional tool executes without confirmation | policy, `require_confirmation` | [MCP/Tools](docs/developer/en/04_mcp_integration_tools_and_policies.md) |
| 401 between gateway/backend/MCP | Basic Auth, hop credentials | [Gateways and Auth](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md) |
| Need to decide framework vs agent ownership | core/agent boundary | [Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) |
| Agent-specific guardrail breaks another agent | extension model, domain imports | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| Judge does not run for a transaction | sampling, transaction signals | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |
| Groundedness gets the wrong context | RAG context, MCP evidence, judge inputs | [RAG/Grounding](docs/developer/en/07_rag_business_context_and_grounding.md) |
| RAG returns no useful content | provider, ingestion, embeddings | [RAG/Grounding](docs/developer/en/07_rag_business_context_and_grounding.md) |
| Unsure whether to use RAG, memory or a tool | responsibility separation | [Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md) |
| Memory disappears across sessions | LTM vs conversation memory | [LTM and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| Memory leaks across customer/agent | identity isolation | [LTM and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md) |
| Need `reasoning_content` | `ainvoke_response()` | [LLM Rich Response](docs/developer/en/09_llm_rich_response_reasoning.md) |
| `reasoning_content` is `None` | provider/model does not expose it | [LLM Rich Response](docs/developer/en/09_llm_rich_response_reasoning.md) |
| Too many LLM calls | deterministic routing, concurrency, cache | [Performance](docs/developer/en/10_performance_cache_and_async_runtime.md) |
| Deadlock across event loops | cross-loop runtime/sequence | [Performance](docs/developer/en/10_performance_cache_and_async_runtime.md) |
| Logs/traces do not correlate the same agent | labels, IDs, observability mapping | [Observability](docs/developer/en/11_observability_persistence_and_operational_readiness.md) |
| Historical example no longer compiles | stale docs vs current API | [README Alignment Validation](docs/developer/en/VALIDATION_README_ALIGNMENT.md) |
| Need to create a new agent from scratch | complete flow | [`README_en.md`](README_en.md) |
### 34. Advanced Features
### [01 — Architecture and Concepts](docs/developer/en/01_architecture_and_concepts.md)
**What it is:** component, contract and responsibility-boundary reference.
**Use it when:** understanding the platform or deciding where a feature belongs.
### [02 — Routing, Route Stickiness and Intent Shift](docs/developer/en/02_routing_stickiness_and_intent_shift.md)
**What it is:** agent/intent discovery, stickiness, handoff and intent-shift reference.
**Use it when:** routing is wrong or session continuity behaves incorrectly.
### [03 — Transactional Workflows and State](docs/developer/en/03_transaction_workflows_and_state.md)
**What it is:** multi-turn transaction lifecycle, states, confirmation, resume and execution evidence.
**Use it when:** transactions loop, resume incorrectly or perform critical operations.
### [04 — MCP, Tools, Policies and Parameter Extraction](docs/developer/en/04_mcp_integration_tools_and_policies.md)
**What it is:** tools, MCP Servers, mappings, policies and extraction reference.
**Use it when:** building or troubleshooting tool integration.
### [05 — Agent Gateway, MCP Gateway and Authentication](docs/developer/en/05_agent_gateway_mcp_gateway_and_auth.md)
**What it is:** gateway responsibilities, governance and component authentication.
**Use it when:** troubleshooting ingress, catalog, authorization or gateway deployment.
### [06 — Guardrails, Judges and Transaction Evaluation](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md)
**What it is:** native/external validation, judges, grounding and transaction evaluation.
**Use it when:** validation blocks, skips or evaluates incorrectly.
### [07 — RAG, BusinessContext and Grounding](docs/developer/en/07_rag_business_context_and_grounding.md)
**What it is:** RAG providers, retrieved context, BusinessContext and grounding.
**Use it when:** retrieved knowledge does not reach the runtime/judge correctly.
### [08 — Long-Term Memory and Checkpoint](docs/developer/en/08_long_term_memory_and_checkpoint.md)
**What it is:** durable memory, conversational memory, identity and state snapshots.
**Use it when:** context disappears, leaks or resumes incorrectly.
### [09 — LLM Rich Response and reasoning_content](docs/developer/en/09_llm_rich_response_reasoning.md)
**What it is:** structured inference output beyond the `str` returned by `ainvoke()`.
**Use it when:** consumers require provider metadata, usage or reasoning exposed by the provider.
### [10 — Performance, Cache and Async Runtime](docs/developer/en/10_performance_cache_and_async_runtime.md)
**What it is:** concurrency, caching, LLM and event-loop optimization reference.
**Use it when:** reducing avoidable latency or diagnosing deadlocks.
### [11 — Observability, Persistence and Operational Readiness](docs/developer/en/11_observability_persistence_and_operational_readiness.md)
**What it is:** correlation, events, labels, sequencing, persistence and production diagnostics.
**Use it when:** proving execution paths or diagnosing production behavior.
### [12 — Input Guardrail Feedback and Blocked-Turn Semantics](docs/developer/en/12_input_guardrail_feedback_and_blocked_turns.md)
**What it is:** user-facing semantics for input blocks, blocked-turn state cleanup, and output-guardrail validation of the generated feedback.
**Use it when:** `COER`/input guardrails generate generic messages, `route=blocked` carries stale results, or you need to reason about precedence between input guardrails, routing, and tools.
### Main tutorial
[`README_en.md`](README_en.md) remains the complete step-by-step guide.
| Workflow completes and generates a protocol, but the final response becomes a safety message | `expected_protocols`, `CMP`, `DLEX_OUT`, `output_guardrails` ordering | [Guardrails and Judges](docs/developer/en/06_guardrails_judges_and_transaction_evaluation.md) |

View File

@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
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

View File

@@ -160,7 +160,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 +197,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(
@@ -281,12 +306,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 {

View File

@@ -7,6 +7,35 @@ router:
confidence_threshold: 0.65 confidence_threshold: 0.65
allow_handoff: true allow_handoff: true
transaction_confirmation:
# Explicit yes/no stays deterministic. Only inconclusive replies use this LLM fallback.
semantic_fallback:
enabled: true
allowed_values: [SIM, NAO, CONTINUAR]
confirm_values: [SIM]
reject_values: [NAO]
continue_values: [CONTINUAR]
include_relevant_context: true
profile_name: router
prompt: |
Você classifica a resposta do cliente a uma confirmação transacional pendente.
Considere a pergunta pendente, somente o histórico recente relacionado ao mesmo tema e a fala atual.
Não execute a ação e não invente fatos.
Classes permitidas: {{ allowed_values }}
- SIM: confirmação/aceite inequívoco, inclusive equivalentes como "isso mesmo", "pode confirmar", "é isso" quando o contexto tornar o aceite claro.
- NAO: recusa/cancelamento inequívoco da ação pendente.
- CONTINUAR: qualquer resposta que não confirme nem rejeite inequivocamente, incluindo pergunta adicional, correção, novo dado, ambiguidade ou possível mudança de assunto.
Pergunta pendente:
{{ pending_prompt }}
Histórico relevante:
{{ relevant_conversation_context }}
Resposta atual do cliente:
{{ user_input }}
state_policies: state_policies:
- state: WAITING_BILLING_CONFIRMATION - state: WAITING_BILLING_CONFIRMATION
agent: billing_agent agent: billing_agent

View File

@@ -4,8 +4,16 @@ 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
@@ -18,7 +26,11 @@ tools:
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
@@ -27,8 +39,16 @@ 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: response:
@@ -39,7 +59,11 @@ tools:
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
@@ -49,8 +73,16 @@ 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
@@ -63,7 +95,11 @@ tools:
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
@@ -82,7 +118,11 @@ tools:
- order_id - order_id
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string 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: selection_keywords:
- cancelar pedido - cancelar pedido
- cancelamento do pedido - cancelamento do pedido
@@ -100,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
@@ -118,8 +166,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar devolução.
user_prompt: Informe o número do pedido que deseja devolver.
reason:
type: string
label: o motivo da devolução
description: Motivo informado pelo cliente para solicitar a devolução do pedido.
user_prompt: Qual é o motivo da devolução?
selection_keywords: selection_keywords:
- solicitar devolução - solicitar devolução
- solicitar devolucao - solicitar devolucao

View File

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

View File

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

View File

@@ -14,29 +14,23 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
# LLM - OCI Generative AI como provider principal # LLM - OCI Generative AI como provider principal
############################################################################### ###############################################################################
# Opções: mock, oci_openai, oci_sdk, openai_compatible # Opções: mock, oci_openai, oci_sdk, openai_compatible
LLM_PROVIDER=oci_sdk LLM_PROVIDER=oci_openai
LLM_TEMPERATURE=0.2 LLM_TEMPERATURE=0.2
LLM_MAX_TOKENS=2048 LLM_MAX_TOKENS=2048
LLM_TIMEOUT_SECONDS=120 LLM_TIMEOUT_SECONDS=120
# OCI OpenAI-compatible endpoint # OCI OpenAI-compatible endpoint
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
OCI_GENAI_MODEL=openai.gpt-4.1 OCI_GENAI_MODEL=openai.gpt-4.1
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
OCI_GENAI_PROJECT_OCID= 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|instance_principal|resource_principal
OCI_AUTH_MODE=config_file OCI_AUTH_MODE=config_file
# OCI SDK / signer / profiles # OCI SDK / signer / profiles
OCI_CONFIG_FILE=~/.oci/config OCI_CONFIG_FILE=~/.oci/config
OCI_PROFILE=LATINOAMERICA-Chicago OCI_PROFILE=DEFAULT
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OCI_REGION=us-chicago-1 OCI_REGION=us-chicago-1
############################################################################### ###############################################################################
@@ -49,10 +43,10 @@ CHECKPOINT_REPOSITORY_PROVIDER=autonomous
# Autonomous Database # Autonomous Database
ADB_USER=admin ADB_USER=admin
ADB_PASSWORD=Moniquinha19721972 ADB_PASSWORD=fjhsdf04954hf
ADB_DSN=oradb23ai_high ADB_DSN=oradb23aidev_high
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
ADB_WALLET_PASSWORD=Moniquinha1972 ADB_WALLET_PASSWORD=fjhsdf04954hf
ADB_TABLE_PREFIX=AGENTFW ADB_TABLE_PREFIX=AGENTFW
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente # MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
@@ -66,10 +60,10 @@ ENABLE_REDIS_CACHE=false
############################################################################### ###############################################################################
# RAG / Vector / Graph # RAG / Vector / Graph
############################################################################### ###############################################################################
VECTOR_STORE_PROVIDER=autonomous VECTOR_STORE_PROVIDER=memory
GRAPH_STORE_PROVIDER=autonomous GRAPH_STORE_PROVIDER=memory
RAG_TOP_K=5 RAG_TOP_K=5
EMBEDDING_PROVIDER=oci 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
@@ -77,21 +71,16 @@ RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
# Observabilidade # Observabilidade
############################################################################### ###############################################################################
ENABLE_LANGFUSE=true ENABLE_LANGFUSE=true
# Opcional: verbose, compact LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
LANGFUSE_TRACE_MODE=compact LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow LANGFUSE_LEGACY_IO_FALLBACK=true
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC. LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
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 LANGFUSE_HOST=http://localhost:3005
ENABLE_OTEL=false ENABLE_OTEL=false
OTEL_EXPORTER_OTLP_ENDPOINT= OTEL_EXPORTER_OTLP_ENDPOINT=
OTEL_SERVICE_NAME=ai-agent-template OTEL_SERVICE_NAME=ai-agent-template
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
############################################################################### ###############################################################################
# Analytics / Observer corporativo # Analytics / Observer corporativo
@@ -99,7 +88,7 @@ ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo. # Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
ENABLE_ANALYTICS=false ENABLE_ANALYTICS=false
# Providers aceitos: oci_streaming,pubsub,noop # Providers aceitos: oci_streaming,pubsub,noop
ANALYTICS_PROVIDERS=oci_streaming ANALYTICS_PROVIDERS=pubsub
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente. # Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
AGENT_PUBSUB_TOPIC= AGENT_PUBSUB_TOPIC=
GCP_PUBSUB_TOPIC_PATH= GCP_PUBSUB_TOPIC_PATH=
@@ -171,6 +160,7 @@ WORKFLOWS_PATH=./workflows
ENABLE_MCP_TOOLS=true ENABLE_MCP_TOOLS=true
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
TOOLS_CONFIG_PATH=./config/tools.yaml TOOLS_CONFIG_PATH=./config/tools.yaml
TOOL_POLICIES_PATH=./config/tool_policies.yaml
MCP_TOOL_TIMEOUT_SECONDS=30 MCP_TOOL_TIMEOUT_SECONDS=30
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes # router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
@@ -194,6 +184,18 @@ MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=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 # LONG-TERM MEMORY
############################################################################### ###############################################################################
@@ -207,5 +209,3 @@ 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
ENABLE_TRANSACTIONAL_WORKFLOWS=true
WORKFLOWS_PATH=./workflows

View File

@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
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

View File

@@ -160,7 +160,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 +197,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(
@@ -281,12 +306,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 {

View File

@@ -7,6 +7,35 @@ router:
confidence_threshold: 0.65 confidence_threshold: 0.65
allow_handoff: true allow_handoff: true
transaction_confirmation:
# Explicit yes/no stays deterministic. Only inconclusive replies use this LLM fallback.
semantic_fallback:
enabled: true
allowed_values: [SIM, NAO, CONTINUAR]
confirm_values: [SIM]
reject_values: [NAO]
continue_values: [CONTINUAR]
include_relevant_context: true
profile_name: router
prompt: |
Você classifica a resposta do cliente a uma confirmação transacional pendente.
Considere a pergunta pendente, somente o histórico recente relacionado ao mesmo tema e a fala atual.
Não execute a ação e não invente fatos.
Classes permitidas: {{ allowed_values }}
- SIM: confirmação/aceite inequívoco, inclusive equivalentes como "isso mesmo", "pode confirmar", "é isso" quando o contexto tornar o aceite claro.
- NAO: recusa/cancelamento inequívoco da ação pendente.
- CONTINUAR: qualquer resposta que não confirme nem rejeite inequivocamente, incluindo pergunta adicional, correção, novo dado, ambiguidade ou possível mudança de assunto.
Pergunta pendente:
{{ pending_prompt }}
Histórico relevante:
{{ relevant_conversation_context }}
Resposta atual do cliente:
{{ user_input }}
state_policies: state_policies:
- state: WAITING_BILLING_CONFIRMATION - state: WAITING_BILLING_CONFIRMATION
agent: billing_agent agent: billing_agent

View File

@@ -4,8 +4,16 @@ 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
@@ -18,7 +26,11 @@ tools:
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
@@ -27,8 +39,16 @@ 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: response:
@@ -39,7 +59,11 @@ tools:
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
@@ -49,8 +73,16 @@ 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
@@ -63,7 +95,11 @@ tools:
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
@@ -82,7 +118,11 @@ tools:
- order_id - order_id
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string 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: selection_keywords:
- cancelar pedido - cancelar pedido
- cancelamento do pedido - cancelamento do pedido
@@ -100,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
@@ -118,8 +166,16 @@ tools:
- reason - reason
confirmation_required: true confirmation_required: true
args_schema: args_schema:
order_id: string order_id:
reason: string type: string
label: o número do pedido
description: Identificador do pedido para o qual o cliente deseja solicitar devolução.
user_prompt: Informe o número do pedido que deseja devolver.
reason:
type: string
label: o motivo da devolução
description: Motivo informado pelo cliente para solicitar a devolução do pedido.
user_prompt: Qual é o motivo da devolução?
selection_keywords: selection_keywords:
- solicitar devolução - solicitar devolução
- solicitar devolucao - solicitar devolucao

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,532 @@
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"),
"domain": result.get("domain"),
"mcp_tools": result.get("mcp_tools"),
"mcp_results": result.get("mcp_results"),
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"judges": result.get("judge_results"),
"guardrails": result.get("guardrail_decisions"),
},
)
rendered = await gateway.render(response)
root_span.set_output(rendered)
await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None
await sse_hub.emit(agent_session_id, "flow.end", {"session_id": agent_session_id, "message_id": message_id}) if emit_sse else None
return rendered
@app.get("/health")
async def health():
return {
"status": "ok",
"llm_provider": settings.LLM_PROVIDER,
"llm_class": llm.__class__.__name__,
"langfuse_enabled": telemetry.is_enabled(),
"agents": [p.agent_id for p in agent_profiles.list_profiles()],
"default_agent_id": agent_profiles.default_agent_id,
"routing_mode": settings.ROUTING_MODE,
"sse_enabled": settings.ENABLE_SSE,
"session_repository": settings.SESSION_REPOSITORY_PROVIDER,
"memory_repository": settings.MEMORY_REPOSITORY_PROVIDER,
"checkpoint_repository": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"usage_repository": settings.USAGE_REPOSITORY_PROVIDER,
"identity_config_path": settings.IDENTITY_CONFIG_PATH,
"mcp_parameter_mapping_path": settings.MCP_PARAMETER_MAPPING_PATH,
"framework_channel_input_mode": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
"legacy_channel_gateway_mode": settings.CHANNEL_GATEWAY_MODE,
}
@app.get("/agents")
async def list_agents():
return {"default_agent_id": agent_profiles.default_agent_id, "agents": [p.__dict__ for p in agent_profiles.list_profiles()]}
@app.get("/debug/env")
async def debug_env():
return {
"APP_ENV": settings.APP_ENV,
"LLM_PROVIDER": settings.LLM_PROVIDER,
"ENABLE_LANGFUSE": settings.ENABLE_LANGFUSE,
"LANGFUSE_HOST": settings.LANGFUSE_HOST,
"TELEMETRY_ENABLED": telemetry.is_enabled(),
"SQLITE_DB_PATH": settings.SQLITE_DB_PATH,
"SESSION_REPOSITORY_PROVIDER": settings.SESSION_REPOSITORY_PROVIDER,
"MEMORY_REPOSITORY_PROVIDER": settings.MEMORY_REPOSITORY_PROVIDER,
"CHECKPOINT_REPOSITORY_PROVIDER": settings.CHECKPOINT_REPOSITORY_PROVIDER,
"AGENTS_CONFIG_PATH": settings.AGENTS_CONFIG_PATH,
"ROUTING_CONFIG_PATH": settings.ROUTING_CONFIG_PATH,
"ROUTING_MODE": settings.ROUTING_MODE,
"FRAMEWORK_CHANNEL_INPUT_MODE": settings.FRAMEWORK_CHANNEL_INPUT_MODE,
"CHANNEL_GATEWAY_MODE": settings.CHANNEL_GATEWAY_MODE,
}
@app.get("/test-llm")
async def test_llm():
async with telemetry.span("debug.test_llm", input={"message": "Diga apenas OK"}):
answer = await llm.ainvoke([
{"role": "system", "content": "Responda de forma curta."},
{"role": "user", "content": "Diga apenas OK"},
])
telemetry.flush()
return {"provider": llm.__class__.__name__, "answer": answer}
@app.post("/debug/route")
async def debug_route(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
state = {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"session_id": msg.session_id or "debug-session",
"conversation_key": identity.conversation_key(),
"agent_profile": context["agent_profile"],
"user_text": msg.text,
"sanitized_input": msg.text,
"history": [],
"context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()},
}
if settings.ROUTING_MODE == "supervisor":
plan = await workflow.supervisor.route_plan(state)
return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata}
decision = await workflow.router.route(state)
data = decision.model_dump(mode="json")
data["mode"] = "router"
return data
@app.post("/debug/identity")
async def debug_identity(req: GatewayRequest):
msg = await gateway.normalize(req.channel, req.payload)
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
return {
"technical_identity": {
"tenant_id": identity.tenant_id,
"agent_id": identity.agent_id,
"conversation_key": identity.conversation_key(),
"original_session_id": msg.session_id,
},
"business_context": business_context.model_dump(),
"identity_missing": missing_identity_keys,
"context_keys": sorted(context.keys()),
}
@app.get("/debug/usage")
async def debug_usage(tenant_id: str | None = None, session_id: str | None = None):
return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id)
@app.get("/debug/mcp/tools")
async def debug_mcp_tools():
return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()}
@app.post("/debug/mcp/call/{tool_name}")
async def debug_mcp_call(tool_name: str, arguments: dict | None = None):
arguments = arguments or {}
ctx = arguments.get("business_context") or arguments.get("identity") or {}
result = await tool_router.call(
tool_name,
arguments,
business_context=ctx,
original_context=arguments,
)
return result.model_dump(mode="json")
@app.post("/gateway/message")
async def gateway_message(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=False)
@app.post("/gateway/message/sse")
async def gateway_message_sse(req: GatewayRequest):
return await _process_gateway_message(req, emit_sse=True)
@app.get("/gateway/events/{session_id}")
async def gateway_events(session_id: str, request: Request):
last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0"
return StreamingResponse(
sse_hub.subscribe(session_id, int(last)),
media_type="text/event-stream",
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
)
@app.get("/sessions/{session_id}/messages")
async def get_session_messages(session_id: str, limit: int = 50):
return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]}
@app.get("/sessions/{session_id}/checkpoint")
async def get_session_checkpoint(session_id: str):
return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)}
@app.on_event("shutdown")
async def shutdown():
telemetry.shutdown()

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,74 @@
from __future__ import annotations
from typing import Any
from agent_framework.presentation import register_tool_response_renderer
def _money_brl(value: Any) -> str:
try:
return f"{float(value):.2f}".replace(".", ",")
except (TypeError, ValueError):
return str(value)
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
return f"[{agent_label}] Fatura consultada: {result}."
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
plano = result.get("plano")
if plano is None:
return None
parts = [f"[{agent_label}] Seu plano é {plano}"]
internet_gb = result.get("internet_gb")
status = result.get("status")
if internet_gb is not None:
parts.append(f"com {internet_gb} GB")
if status is not None:
parts.append(f"status {status}")
return ", ".join(parts) + "."
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
order_id = result.get("order_id")
status = result.get("status")
if order_id is None or status is None:
return None
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
total = result.get("valor_total")
if total is not None:
lines.append(f"Valor total: R$ {_money_brl(total)}.")
items = result.get("itens") or []
rendered_items: list[str] = []
if isinstance(items, list):
for item in items:
if isinstance(item, dict):
value = item.get("descricao") or item.get("nome") or item.get("sku")
else:
value = item
if value not in (None, ""):
rendered_items.append(str(value))
if rendered_items:
lines.append("Itens: " + "; ".join(rendered_items) + ".")
return " ".join(lines)
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
order_id = result.get("order_id")
transportadora = result.get("transportadora")
codigo = result.get("codigo_rastreio")
previsao = result.get("previsao_entrega")
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
return None
return (
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
f"rastreio {codigo}, previsão {previsao}."
)
def register_tool_renderers() -> None:
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
register_tool_response_renderer("telecom.plan", render_telecom_plan)
register_tool_response_renderer("retail.order", render_retail_order)
register_tool_response_renderer("retail.delivery", render_retail_delivery)

View File

@@ -0,0 +1,53 @@
from typing import Any, TypedDict
class AgentState(TypedDict, total=False):
tenant_id: str
agent_id: str
session_id: str
conversation_key: str
workflow_id: str
agent_profile: dict[str, Any]
user_text: str
sanitized_input: str
route: str
intent: str
route_decision: dict[str, Any]
answer: str
final_answer: str
history: list[dict[str, Any]]
context: dict[str, Any]
guardrail_decisions: list[dict[str, Any]]
judge_results: list[dict[str, Any]]
next_state: str
domain: str
mcp_tools: list[str]
mcp_results: list[dict[str, Any]]
available_mcp_tools: list[str]
selected_tool_call: dict[str, Any]
pending_tool_call: dict[str, Any]
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]]
active_agent: str
route_bypassed: bool
continuity_signal: dict[str, Any]
session_control: str
session_ended: bool
human_handoff_requested: bool
blocked: bool
supervisor_action: str
supervisor_guidance: str
supervisor_attempt: int
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool
long_term_memories: list[dict[str, Any]]
long_term_memory_context: str
long_term_memory_write_result: dict[str, Any]

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
id: retail_orders_prompt_policy
version: 1
description: Prompt base isolado do agente de varejo/pedidos.
system_prefix: |
Você é um agente corporativo de varejo especializado em pedidos, entrega, troca, devolução e garantia.
Seja claro, objetivo e não use regras de negócio de telecom neste agente.

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