mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 18:23:46 +00:00
Compare commits
25 Commits
57df723a24
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d84d833694 | |||
| fe8c18093f | |||
|
|
f4638e287a | ||
|
|
e24963abe1 | ||
| 10a6f357dd | |||
| c8c83981c4 | |||
| 83bf961185 | |||
| 81f24d7357 | |||
| 1fd18531c0 | |||
| df2a1cbe1b | |||
| 42ab000c7b | |||
| 472d44074c | |||
|
|
faf5ca55ba | ||
| ac18d68eaf | |||
| 63d0fb51c4 | |||
|
|
a472daa1e4 | ||
|
|
d93efd8972 | ||
| 727997aa41 | |||
| 2496e831a1 | |||
| b5d2a12953 | |||
| bdc44b15d6 | |||
| 05077a91bc | |||
| 19c4024693 | |||
| 22e8f56146 | |||
| 0a72afb94e |
27
.env.example
27
.env.example
@@ -207,3 +207,30 @@ LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=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
14
.idea/workspace.xml
generated
@@ -4,7 +4,9 @@
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<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="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
@@ -50,12 +52,13 @@
|
||||
"ASKED_SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"RunOnceActivity.typescript.service.memoryLimit.init": "true",
|
||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"git-widget-placeholder": "main",
|
||||
"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.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
@@ -68,8 +71,8 @@
|
||||
<component name="SharedIndexes">
|
||||
<attachedChunks>
|
||||
<set>
|
||||
<option value="bundled-jdk-9823dce3aa75-fbdcb00ec9e3-intellij.indexing.shared.core-IU-251.29188.36" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-09060db00ec0-JavaScript-IU-251.29188.36" />
|
||||
<option value="bundled-jdk-30f59d01ecdd-cffe25b9f5b3-intellij.indexing.shared.core-IU-253.28294.334" />
|
||||
<option value="bundled-js-predefined-d6986cc7102b-c7e53b3be11b-JavaScript-IU-253.28294.334" />
|
||||
</set>
|
||||
</attachedChunks>
|
||||
</component>
|
||||
@@ -92,6 +95,7 @@
|
||||
<workItem from="1785414225783" duration="148000" />
|
||||
<workItem from="1785414447653" duration="704000" />
|
||||
<workItem from="1785630146329" duration="316000" />
|
||||
<workItem from="1787832640995" duration="4662000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Ajustes na documentação e remanejamento dos folders">
|
||||
<option name="closed" value="true" />
|
||||
|
||||
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal file
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Correção: escape de estado transacional / mudança de intenção
|
||||
|
||||
Correção aplicada em 2026-08-20 para impedir que uma sessão fique presa em `COLLECTING_PARAMETERS` ou `AWAITING_CONFIRMATION` quando o usuário muda explicitamente de assunto.
|
||||
|
||||
## Comportamento corrigido
|
||||
|
||||
Antes:
|
||||
|
||||
1. uma transação entrava em `COLLECTING_PARAMETERS`;
|
||||
2. `next_state` forçava o mesmo agente via `state_policies`;
|
||||
3. toda mensagem seguinte era tratada como tentativa de preencher o parâmetro faltante;
|
||||
4. uma nova intenção como `quais sao meus servicos` permanecia presa no fluxo anterior.
|
||||
|
||||
Agora:
|
||||
|
||||
- o `EnterpriseRouter` verifica mudança explícita de intenção antes de aplicar o lock de estado;
|
||||
- keyword explícita tem prioridade;
|
||||
- quando necessário, o LLM router pode detectar mudança com confiança >= `router.confidence_threshold`;
|
||||
- a decisão recebe `metadata.transaction_interruption=intent_shift`;
|
||||
- o runtime encerra a transação pendente como `CANCELLED`, limpa `next_state`, parâmetros e latches, e prossegue com a nova intent;
|
||||
- cancelamentos explícitos como `cancele essa operação anterior` funcionam também durante `COLLECTING_PARAMETERS`.
|
||||
|
||||
## Testes adicionados
|
||||
|
||||
- mudança de intent durante `COLLECTING_PARAMETERS`;
|
||||
- resposta curta/baixa confiança permanece na transação;
|
||||
- cancelamento explícito durante coleta de parâmetros;
|
||||
- limpeza do estado transacional antes de executar a nova intent.
|
||||
|
||||
Testes focados: 19 passed.
|
||||
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal file
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Precedência transacional + extração LLM de parâmetros
|
||||
|
||||
Esta correção remove a extração textual hardcoded de parâmetros transacionais e faz a coleta de `policy.requires` por um extrator LLM genérico.
|
||||
|
||||
## Regra de precedência
|
||||
|
||||
Enquanto existir uma transação ativa, o framework trata o turno nesta ordem:
|
||||
|
||||
```text
|
||||
ACTIVE_TRANSACTION
|
||||
|
|
||||
+-- COLLECTING_PARAMETERS
|
||||
| |
|
||||
| +-- LLM tenta extrair SOMENTE os parâmetros ainda pendentes
|
||||
| |
|
||||
| +-- extraiu >= 1 ?
|
||||
| |
|
||||
| +-- SIM -> continua a transação; NÃO avalia intent_shift
|
||||
| |
|
||||
| +-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||
|
|
||||
+-- AWAITING_CONFIRMATION
|
||||
|
|
||||
+-- reconhece confirmação/rejeição explícita
|
||||
|
|
||||
+-- reconheceu ?
|
||||
|
|
||||
+-- SIM -> continua/cancela a transação; NÃO avalia intent_shift
|
||||
|
|
||||
+-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||
```
|
||||
|
||||
## TransactionParameterExtractor
|
||||
|
||||
Novo componente:
|
||||
|
||||
`libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py`
|
||||
|
||||
A extração textual dos parâmetros de negócio é feita exclusivamente por LLM. O componente recebe:
|
||||
|
||||
- nome da tool/transação ativa;
|
||||
- parâmetros atualmente pendentes;
|
||||
- argumentos já conhecidos;
|
||||
- schema/tipos declarados em `tools.yaml` quando disponíveis;
|
||||
- descrição da tool;
|
||||
- mensagem atual do usuário.
|
||||
|
||||
Ele não conhece nomes de domínio como `order_id`, `reason`, `subject`, `valor`, TIM ou retail. Não há regex de entidades de negócio.
|
||||
|
||||
A LLM pode interpretar, por exemplo:
|
||||
|
||||
- `PED-1001` quando só há um parâmetro compatível pendente;
|
||||
- `o pedido é PED-1001`;
|
||||
- `PED-1001, desisti da compra` preenchendo dois parâmetros no mesmo turno;
|
||||
- respostas com o nome do parâmetro seguido do valor;
|
||||
- respostas apenas com o valor, quando semanticamente inequívocas.
|
||||
|
||||
Em caso de dúvida, o prompt manda retornar `null`. Uma nova solicitação não deve ser transformada em valor de parâmetro.
|
||||
|
||||
## Separação de responsabilidades
|
||||
|
||||
`tool_policies.yaml` continua sendo a fonte de verdade para `requires`.
|
||||
|
||||
`tools.yaml` pode fornecer tipos via `args_schema` e descrição da tool para melhorar a interpretação sem introduzir código específico de domínio.
|
||||
|
||||
`mcp_parameter_mapping.yaml` continua responsável pelos parâmetros auxiliares/contrato MCP. As strategies do mapper são explicitamente excluídas dos campos presentes em `policy.requires`, para não misturar extração MCP com coleta transacional.
|
||||
|
||||
O `EnterpriseRouter` usa o mesmo extrator LLM apenas como *probe* de precedência. Se pelo menos um parâmetro pendente for encontrado, o turno permanece no estado transacional. Os valores extraídos são colocados no metadata da decisão e reutilizados pelo runtime, evitando uma segunda chamada LLM no mesmo turno.
|
||||
|
||||
## Profile LLM
|
||||
|
||||
Foi adicionado aos templates:
|
||||
|
||||
```yaml
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
```
|
||||
|
||||
Generation/component:
|
||||
|
||||
- `llm.transaction_parameter_extraction`
|
||||
- `transaction_parameter_extraction`
|
||||
|
||||
## Limpeza de estado
|
||||
|
||||
Em `intent_shift`, `transaction_pre_validation` da transação abandonada é removido para não contaminar a nova transação. O resultado de pre-validation continua preservado enquanto pertence à própria transação para auditoria.
|
||||
|
||||
## Testes adicionados
|
||||
|
||||
`tests/test_transaction_parameter_llm_precedence.py`
|
||||
|
||||
Cobertura:
|
||||
|
||||
1. dois parâmetros extraídos no mesmo turno;
|
||||
2. um parâmetro preenchido ganha precedência sobre keyword que indicaria outra intent;
|
||||
3. nenhum parâmetro encontrado libera `intent_shift`;
|
||||
4. ausência do antigo `_extract_action_arguments()` hardcoded;
|
||||
5. confirmação `sim` ganha precedência sobre intent shift.
|
||||
212
README.md
212
README.md
@@ -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.**
|
||||
|
||||
## Í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
|
||||
|
||||
@@ -2150,71 +2171,30 @@ trace_id
|
||||
|
||||
#### 5.1.1.21.3. Instrumentação automática do cliente OpenAI pelo Langfuse
|
||||
|
||||
O padrão oficial do framework é:
|
||||
|
||||
```python
|
||||
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
|
||||
```env
|
||||
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.
|
||||
* 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 `false`:
|
||||
|
||||
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.
|
||||
* Ambientes de homologação.
|
||||
* Ambientes de produção que necessitem observabilidade detalhada das chamadas LLM.
|
||||
* Cenários de troubleshooting, tuning de prompts e análise de custos.
|
||||
```env
|
||||
# Padrão recomendado para todos os templates e ambientes do framework
|
||||
ENABLE_LANGFUSE=true
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
|
||||
```
|
||||
|
||||
Observação
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
#### 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.
|
||||
|
||||
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`.
|
||||
|
||||
|
||||
206
README_en.md
206
README_en.md
@@ -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.**
|
||||
|
||||
## 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
|
||||
|
||||
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
|
||||
|
||||
```python
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||
```
|
||||
The framework's official default is:
|
||||
|
||||
enables automatic Langfuse instrumentation for the OpenAI client.
|
||||
|
||||
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
|
||||
```env
|
||||
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.
|
||||
* Custom framework spans are still emitted.
|
||||
* Langfuse no longer automatically creates OpenAI-generation entries.
|
||||
* Less detailed information is available for analyzing model interactions.
|
||||
When set to `false`:
|
||||
|
||||
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
|
||||
* Testing and staging environments
|
||||
* Production environments that require detailed LLM observability
|
||||
* Prompt engineering, troubleshooting, and cost analysis scenarios
|
||||
```env
|
||||
# Recommended default for every framework template and environment
|
||||
ENABLE_LANGFUSE=true
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
|
||||
```
|
||||
|
||||
Important Note
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
#### 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.
|
||||
|
||||
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) |
|
||||
|
||||
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
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
|
||||
|
||||
Binary file not shown.
@@ -160,7 +160,7 @@ class AgentWorkflow:
|
||||
builder.add_conditional_edges(
|
||||
"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_conditional_edges(
|
||||
@@ -197,6 +197,31 @@ class AgentWorkflow:
|
||||
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(
|
||||
@@ -281,12 +306,33 @@ class AgentWorkflow:
|
||||
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": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"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 {
|
||||
|
||||
@@ -7,6 +7,35 @@ router:
|
||||
confidence_threshold: 0.65
|
||||
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: WAITING_BILLING_CONFIRMATION
|
||||
agent: billing_agent
|
||||
|
||||
@@ -4,8 +4,16 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
invoice_id: 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.
|
||||
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:
|
||||
- fatura
|
||||
- conta
|
||||
@@ -18,7 +26,11 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
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:
|
||||
- pagamento
|
||||
- pagamentos
|
||||
@@ -27,8 +39,16 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
asset_id: 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.
|
||||
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:
|
||||
- plano
|
||||
response:
|
||||
@@ -39,7 +59,11 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
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:
|
||||
- serviços
|
||||
- servicos
|
||||
@@ -49,8 +73,16 @@ tools:
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
customer_id: string
|
||||
order_id:
|
||||
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:
|
||||
- consultar pedido
|
||||
- status do pedido
|
||||
@@ -63,7 +95,11 @@ tools:
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
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:
|
||||
- entrega
|
||||
- rastreio
|
||||
@@ -82,7 +118,11 @@ tools:
|
||||
- order_id
|
||||
confirmation_required: true
|
||||
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:
|
||||
- cancelar pedido
|
||||
- cancelamento do pedido
|
||||
@@ -100,8 +140,16 @@ tools:
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
order_id:
|
||||
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:
|
||||
- solicitar troca
|
||||
- trocar
|
||||
@@ -118,8 +166,16 @@ tools:
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
order_id:
|
||||
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:
|
||||
- solicitar devolução
|
||||
- solicitar devolucao
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Confirmação Transacional Semântica
|
||||
|
||||
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
|
||||
|
||||
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
|
||||
|
||||
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
|
||||
|
||||
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
|
||||
|
||||
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
@@ -14,29 +14,23 @@ CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
# LLM - OCI Generative AI como provider principal
|
||||
###############################################################################
|
||||
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||
LLM_PROVIDER=oci_sdk
|
||||
LLM_PROVIDER=oci_openai
|
||||
LLM_TEMPERATURE=0.2
|
||||
LLM_MAX_TOKENS=2048
|
||||
LLM_TIMEOUT_SECONDS=120
|
||||
|
||||
# OCI OpenAI-compatible endpoint
|
||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
|
||||
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||
OCI_GENAI_PROJECT_OCID=
|
||||
|
||||
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
|
||||
#OCI_GENAI_MODEL=openai.gpt-4.1
|
||||
#OCI_GENAI_API_KEY=
|
||||
#OCI_GENAI_PROJECT_OCID=
|
||||
|
||||
|
||||
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
||||
OCI_AUTH_MODE=config_file
|
||||
# OCI SDK / signer / profiles
|
||||
OCI_CONFIG_FILE=~/.oci/config
|
||||
OCI_PROFILE=LATINOAMERICA-Chicago
|
||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
|
||||
OCI_PROFILE=DEFAULT
|
||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
OCI_REGION=us-chicago-1
|
||||
|
||||
###############################################################################
|
||||
@@ -49,10 +43,10 @@ CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||
|
||||
# Autonomous Database
|
||||
ADB_USER=admin
|
||||
ADB_PASSWORD=Moniquinha19721972
|
||||
ADB_DSN=oradb23ai_high
|
||||
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
|
||||
ADB_WALLET_PASSWORD=Moniquinha1972
|
||||
ADB_PASSWORD=fjhsdf04954hf
|
||||
ADB_DSN=oradb23aidev_high
|
||||
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||
ADB_TABLE_PREFIX=AGENTFW
|
||||
|
||||
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||
@@ -66,10 +60,10 @@ ENABLE_REDIS_CACHE=false
|
||||
###############################################################################
|
||||
# RAG / Vector / Graph
|
||||
###############################################################################
|
||||
VECTOR_STORE_PROVIDER=autonomous
|
||||
GRAPH_STORE_PROVIDER=autonomous
|
||||
VECTOR_STORE_PROVIDER=memory
|
||||
GRAPH_STORE_PROVIDER=memory
|
||||
RAG_TOP_K=5
|
||||
EMBEDDING_PROVIDER=oci
|
||||
EMBEDDING_PROVIDER=mock
|
||||
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||
|
||||
@@ -77,21 +71,16 @@ RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||
# Observabilidade
|
||||
###############################################################################
|
||||
ENABLE_LANGFUSE=true
|
||||
# Opcional: verbose, compact
|
||||
LANGFUSE_TRACE_MODE=compact
|
||||
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow
|
||||
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
|
||||
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
|
||||
LANGFUSE_IGNORE_HEALTHCHECKS=true
|
||||
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
|
||||
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
|
||||
LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
|
||||
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
|
||||
LANGFUSE_LEGACY_IO_FALLBACK=true
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
|
||||
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
|
||||
LANGFUSE_HOST=http://localhost:3005
|
||||
ENABLE_OTEL=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
OTEL_SERVICE_NAME=ai-agent-template
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
|
||||
|
||||
###############################################################################
|
||||
# Analytics / Observer corporativo
|
||||
@@ -99,7 +88,7 @@ ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
|
||||
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||
ENABLE_ANALYTICS=false
|
||||
# Providers aceitos: oci_streaming,pubsub,noop
|
||||
ANALYTICS_PROVIDERS=oci_streaming
|
||||
ANALYTICS_PROVIDERS=pubsub
|
||||
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||
AGENT_PUBSUB_TOPIC=
|
||||
GCP_PUBSUB_TOPIC_PATH=
|
||||
@@ -171,6 +160,7 @@ WORKFLOWS_PATH=./workflows
|
||||
ENABLE_MCP_TOOLS=true
|
||||
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||
|
||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||
@@ -194,6 +184,18 @@ MEMORY_SUMMARY_USE_LLM=true
|
||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||
MEMORY_INJECT_SUMMARY=true
|
||||
|
||||
###############################################################################
|
||||
# MCP Gateway
|
||||
###############################################################################
|
||||
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||
MCP_GATEWAY_ENABLED=true
|
||||
MCP_GATEWAY_URL=http://localhost:8300
|
||||
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||
# MCP_GATEWAY_TOKEN=
|
||||
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||
MCP_GATEWAY_TENANT_ID=default
|
||||
|
||||
###############################################################################
|
||||
# LONG-TERM MEMORY
|
||||
###############################################################################
|
||||
@@ -207,5 +209,3 @@ LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||
ENABLE_TRANSACTIONAL_WORKFLOWS=true
|
||||
WORKFLOWS_PATH=./workflows
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -26,6 +26,8 @@ class AgentState(TypedDict, total=False):
|
||||
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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -160,7 +160,7 @@ class AgentWorkflow:
|
||||
builder.add_conditional_edges(
|
||||
"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_conditional_edges(
|
||||
@@ -197,6 +197,31 @@ class AgentWorkflow:
|
||||
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(
|
||||
@@ -281,12 +306,33 @@ class AgentWorkflow:
|
||||
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": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"final_answer": "Não consegui seguir com essa mensagem por regra de segurança.",
|
||||
"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 {
|
||||
|
||||
@@ -7,6 +7,35 @@ router:
|
||||
confidence_threshold: 0.65
|
||||
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: WAITING_BILLING_CONFIRMATION
|
||||
agent: billing_agent
|
||||
|
||||
@@ -4,8 +4,16 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
invoice_id: 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.
|
||||
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:
|
||||
- fatura
|
||||
- conta
|
||||
@@ -18,7 +26,11 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
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:
|
||||
- pagamento
|
||||
- pagamentos
|
||||
@@ -27,8 +39,16 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
asset_id: 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.
|
||||
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:
|
||||
- plano
|
||||
response:
|
||||
@@ -39,7 +59,11 @@ tools:
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
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:
|
||||
- serviços
|
||||
- servicos
|
||||
@@ -49,8 +73,16 @@ tools:
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
customer_id: string
|
||||
order_id:
|
||||
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:
|
||||
- consultar pedido
|
||||
- status do pedido
|
||||
@@ -63,7 +95,11 @@ tools:
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
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:
|
||||
- entrega
|
||||
- rastreio
|
||||
@@ -82,7 +118,11 @@ tools:
|
||||
- order_id
|
||||
confirmation_required: true
|
||||
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:
|
||||
- cancelar pedido
|
||||
- cancelamento do pedido
|
||||
@@ -100,8 +140,16 @@ tools:
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
order_id:
|
||||
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:
|
||||
- solicitar troca
|
||||
- trocar
|
||||
@@ -118,8 +166,16 @@ tools:
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
order_id:
|
||||
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:
|
||||
- solicitar devolução
|
||||
- solicitar devolucao
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# Confirmação Transacional Semântica
|
||||
|
||||
Este template suporta confirmação transacional em duas camadas: primeiro um parser determinístico para `sim`/`não` e equivalentes explícitos; somente quando ele não consegue decidir, o framework usa um classificador semântico configurado em `config/routing.yaml`.
|
||||
|
||||
A configuração `router.transaction_confirmation.semantic_fallback` usa três classes: `SIM`, `NAO` e `CONTINUAR`. O prompt pode usar `{{ pending_prompt }}`, `{{ relevant_conversation_context }}`, `{{ user_input }}` e `{{ allowed_values }}`. O histórico injetado é apenas contexto de interpretação; não substitui validação de negócio ou evidência MCP.
|
||||
|
||||
Exemplo: após `Você confirma o cancelamento do serviço Tamboro Mensal?`, a frase `isso mesmo, pode confirmar` pode ser classificada como `SIM`. Já `mas qual é o valor?` deve ser `CONTINUAR`, portanto não executa a ação por confirmação.
|
||||
|
||||
Entradas explícitas já suportadas continuam no caminho determinístico e não geram custo adicional de LLM. Em observabilidade, o fallback usa `transaction.confirmation.semantic_classifier` e o `route_decision.metadata` informa `transaction_confirmation_source: semantic`.
|
||||
|
||||
Consulte `docs/developer/pt/03_transaction_workflows_and_state.md` do framework para o contrato completo e exemplos.
|
||||
@@ -78,3 +78,10 @@ profiles:
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
|
||||
transaction_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
timeout_seconds: 8
|
||||
|
||||
11
Tuning-Performance/External_Guardrails_Judges/README.md
Normal file
11
Tuning-Performance/External_Guardrails_Judges/README.md
Normal 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.
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY agent_framework /agent_framework
|
||||
COPY agent_template_backend /app
|
||||
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
# Agent Template Backend Enterprise
|
||||
|
||||
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
|
||||
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
|
||||
analytics, observer, MCP, memória, checkpoints e configurações.
|
||||
|
||||
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
|
||||
execução e preservada comentada nos próprios arquivos:
|
||||
|
||||
- `app/agents/billing_agent.py`
|
||||
- `app/agents/product_agent.py`
|
||||
- `app/agents/orders_agent.py`
|
||||
- `app/agents/support_agent.py`
|
||||
|
||||
## O que o desenvolvedor deve alterar
|
||||
|
||||
1. Escolher ou criar um agente em `app/agents/`.
|
||||
2. Implementar o método `run()`.
|
||||
3. Ajustar prompts e tools, se necessário.
|
||||
4. Emitir ICs de negócio relevantes para a jornada.
|
||||
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
|
||||
|
||||
## O que já está integrado
|
||||
|
||||
- `AgentObserver`
|
||||
- `observer.emit_ic()`
|
||||
- `observer.emit_noc()`
|
||||
- `observer.emit_grl()`
|
||||
- `AnalyticsPublisher`
|
||||
- OCI Streaming
|
||||
- GCP Pub/Sub
|
||||
- OutputSupervisor
|
||||
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
|
||||
- MCP Tool Router
|
||||
- LangGraph
|
||||
- Memory
|
||||
- Checkpoint
|
||||
- Langfuse / OpenTelemetry
|
||||
|
||||
## Exemplos adicionados
|
||||
|
||||
Veja `app/examples/`:
|
||||
|
||||
- `ic_examples.py`
|
||||
- `noc_examples.py`
|
||||
- `grl_examples.py`
|
||||
- `mcp_examples.py`
|
||||
- `observer_examples.py`
|
||||
|
||||
## Convenção rápida
|
||||
|
||||
- IC = evento de negócio / curadoria / informacional.
|
||||
- NOC = evento operacional / saúde técnica.
|
||||
- GRL = evento de guardrail / segurança / validação.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -0,0 +1,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def apply_agent_profile_prompt(state: dict, default_prompt: str) -> str:
|
||||
"""Adiciona o prefixo de prompt configurado para o agent_template selecionado.
|
||||
|
||||
Cada agent_id pode definir metadata.system_prefix em config/agents.yaml. Isso
|
||||
mantém prompts isolados sem duplicar o código dos agentes especializados.
|
||||
"""
|
||||
profile = state.get("agent_profile") or (state.get("context") or {}).get("agent_profile") or {}
|
||||
metadata = profile.get("metadata") or {}
|
||||
prefix = (metadata.get("system_prefix") or "").strip()
|
||||
if not prefix:
|
||||
return default_prompt
|
||||
return f"{prefix}\n\n{default_prompt}"
|
||||
@@ -0,0 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
# Compatibilidade local do template/backend.
|
||||
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||
from app.presentation import register_tool_renderers
|
||||
|
||||
register_tool_renderers()
|
||||
|
||||
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||
@@ -0,0 +1,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)
|
||||
@@ -0,0 +1 @@
|
||||
"""Exemplos de uso do template backend enterprise."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,37 @@
|
||||
"""Exemplos de GRL.
|
||||
|
||||
GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos
|
||||
pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão
|
||||
manual apenas para validações customizadas do agente.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||
await observer.emit_grl(
|
||||
"OBSERVE",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"rail_code": rail_code,
|
||||
"reason": reason,
|
||||
},
|
||||
component="examples.grl",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||
await observer.emit_grl(
|
||||
"004",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"rail_code": rail_code,
|
||||
"reason": reason,
|
||||
"action": "block",
|
||||
},
|
||||
component="examples.grl",
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Exemplos de IC - Item de Controle.
|
||||
|
||||
ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria,
|
||||
analytics, BigQuery ou qualquer publisher configurado no framework.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None:
|
||||
await observer.emit_ic(
|
||||
"IC.FATURA_CONSULTADA",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"invoice_id": invoice_id,
|
||||
},
|
||||
component="examples.ic",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None:
|
||||
await observer.emit_ic(
|
||||
"IC.ACAO_CONCLUIDA",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"action_name": action_name,
|
||||
"ok": ok,
|
||||
},
|
||||
component="examples.ic",
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Exemplos de MCP + IC.
|
||||
|
||||
O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o
|
||||
padrão para chamadas explícitas ao tool_router quando necessário.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any:
|
||||
session_id = state.get("conversation_key") or state.get("session_id")
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.MCP_TOOL_CALLED",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"tool_name": tool_name,
|
||||
},
|
||||
component="examples.mcp",
|
||||
)
|
||||
|
||||
result = await tool_router.call(
|
||||
tool_name,
|
||||
payload,
|
||||
business_context=(state.get("context") or {}).get("business_context") or {},
|
||||
original_context=state.get("context") or {},
|
||||
)
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.TOOL_CALLED",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"tool_name": tool_name,
|
||||
"ok": getattr(result, "ok", None),
|
||||
},
|
||||
component="examples.mcp",
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Exemplos de NOC.
|
||||
|
||||
NOC representa telemetria operacional. O workflow do template já emite NOC.001,
|
||||
NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode
|
||||
emitir em pontos críticos.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None:
|
||||
await observer.emit_noc(
|
||||
"002",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"apiUrl": api_url,
|
||||
"statusCode": status_code,
|
||||
"latencyMs": latency_ms,
|
||||
},
|
||||
component="examples.noc",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None:
|
||||
await observer.emit_noc(
|
||||
"003",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"resourceName": resource_name,
|
||||
"latencyMs": latency_ms,
|
||||
},
|
||||
component="examples.noc",
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Resumo prático do Observer corporativo.
|
||||
|
||||
Use este arquivo como cola rápida para IC, NOC e GRL.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None:
|
||||
session_id = state.get("conversation_key") or state.get("session_id")
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.EXEMPLO_NEGOCIO",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id")},
|
||||
component="examples.observer",
|
||||
)
|
||||
|
||||
await observer.emit_noc(
|
||||
"EXEMPLO_OPERACIONAL",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id")},
|
||||
component="examples.observer",
|
||||
)
|
||||
|
||||
await observer.emit_grl(
|
||||
"OBSERVE",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"},
|
||||
component="examples.observer",
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
||||
from __future__ import annotations
|
||||
from agent_framework.guardrails.base import Guardrail, RailDecision
|
||||
|
||||
class ExternalBusinessPolicyRail(Guardrail):
|
||||
code = "EXTERNAL_BUSINESS_POLICY"
|
||||
stage = "output"
|
||||
|
||||
def evaluate(self, text, context):
|
||||
# Synchronous on purpose: framework executes this method in a worker thread.
|
||||
blocked = bool((context or {}).get("example_block"))
|
||||
return RailDecision(code=self.code, allowed=not blocked, reason="example business policy" if blocked else "", metadata={"external": True})
|
||||
@@ -0,0 +1,9 @@
|
||||
from __future__ import annotations
|
||||
from agent_framework.judges.judge import JudgeResult
|
||||
|
||||
class ExternalBusinessJudge:
|
||||
name = "external_business_quality"
|
||||
def __init__(self, threshold=0.5, **kwargs): self.threshold=float(threshold or 0.5)
|
||||
def evaluate(self, question, answer, context):
|
||||
score = 1.0 if answer and len(answer.strip()) >= 10 else 0.0
|
||||
return JudgeResult(name=self.name, score=score, passed=score >= self.threshold, reason="example external judge", metadata={"external": True})
|
||||
@@ -0,0 +1,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()
|
||||
@@ -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")),
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Observer adapter that emits IC/NOC/GRL through framework Telemetry only.
|
||||
|
||||
This avoids a second Langfuse root trace created by AgentObserver ->
|
||||
AnalyticsPublisher while preserving the events inside the active request span.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _normalize_ic_code(code: str) -> str:
|
||||
code = str(code or "UNKNOWN").strip()
|
||||
return code if code.startswith(("IC.", "AGA.", "NOC.", "GRL.")) else f"IC.{code}"
|
||||
|
||||
|
||||
def _normalize_noc_code(code: str) -> str:
|
||||
code = str(code or "UNKNOWN").strip()
|
||||
return code if code.startswith("NOC.") else f"NOC.{code}"
|
||||
|
||||
|
||||
def _normalize_grl_code(code: str) -> str:
|
||||
code = str(code or "UNKNOWN").strip()
|
||||
return code if code.startswith("GRL.") else f"GRL.{code}"
|
||||
|
||||
|
||||
def _kind_for(event_type: str) -> str:
|
||||
if event_type.startswith(("IC.", "AGA.")):
|
||||
return "ic"
|
||||
if event_type.startswith("NOC."):
|
||||
return "noc"
|
||||
if event_type.startswith("GRL."):
|
||||
return "grl"
|
||||
return "event"
|
||||
|
||||
|
||||
class TelemetryBackedAgentObserver:
|
||||
"""Drop-in subset of AgentObserver backed by Telemetry.event.
|
||||
|
||||
Do not publish through AnalyticsPublisher here. Analytics publishing may be
|
||||
configured with a Langfuse provider, and that path creates an extra root
|
||||
trace for business events such as IC.AGENT_COMPLETED/NOC.006. Telemetry.event
|
||||
uses the active span/trace context, so these events appear inside the single
|
||||
request trace.
|
||||
"""
|
||||
|
||||
def __init__(self, telemetry: Any, *, source: str = "agent_framework") -> None:
|
||||
self.telemetry = telemetry
|
||||
self.source = source
|
||||
|
||||
async def emit(
|
||||
self,
|
||||
event_type: str,
|
||||
payload: dict[str, Any] | None = None,
|
||||
*,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
source: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
body = dict(payload or {})
|
||||
meta = dict(metadata or {})
|
||||
body.setdefault("tag", event_type)
|
||||
event = {
|
||||
"eventType": event_type,
|
||||
"source": source or self.source,
|
||||
"eventDate": datetime.now(timezone.utc).isoformat(),
|
||||
"body": body,
|
||||
"metadata": meta,
|
||||
}
|
||||
try:
|
||||
await self.telemetry.event(event_type, event, kind=_kind_for(event_type))
|
||||
except TypeError:
|
||||
# Compatibility with older Telemetry.event signatures.
|
||||
await self.telemetry.event(event_type, event)
|
||||
return event
|
||||
|
||||
async def emit_ic(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||
return await self.emit(_normalize_ic_code(code), payload, metadata={**metadata, "ic": True})
|
||||
|
||||
async def emit_noc(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||
return await self.emit(_normalize_noc_code(code), payload, metadata={**metadata, "noc": True})
|
||||
|
||||
async def emit_grl(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||
return await self.emit(_normalize_grl_code(code), payload, metadata={**metadata, "grl": True})
|
||||
@@ -0,0 +1,3 @@
|
||||
from .tool_renderers import register_tool_renderers
|
||||
|
||||
__all__ = ["register_tool_renderers"]
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from agent_framework.presentation import register_tool_response_renderer
|
||||
|
||||
|
||||
def _money_brl(value: Any) -> str:
|
||||
try:
|
||||
return f"{float(value):.2f}".replace(".", ",")
|
||||
except (TypeError, ValueError):
|
||||
return str(value)
|
||||
|
||||
|
||||
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
return f"[{agent_label}] Fatura consultada: {result}."
|
||||
|
||||
|
||||
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
plano = result.get("plano")
|
||||
if plano is None:
|
||||
return None
|
||||
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||
internet_gb = result.get("internet_gb")
|
||||
status = result.get("status")
|
||||
if internet_gb is not None:
|
||||
parts.append(f"com {internet_gb} GB")
|
||||
if status is not None:
|
||||
parts.append(f"status {status}")
|
||||
return ", ".join(parts) + "."
|
||||
|
||||
|
||||
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
order_id = result.get("order_id")
|
||||
status = result.get("status")
|
||||
if order_id is None or status is None:
|
||||
return None
|
||||
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||
total = result.get("valor_total")
|
||||
if total is not None:
|
||||
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||
items = result.get("itens") or []
|
||||
rendered_items: list[str] = []
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
if isinstance(item, dict):
|
||||
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||
else:
|
||||
value = item
|
||||
if value not in (None, ""):
|
||||
rendered_items.append(str(value))
|
||||
if rendered_items:
|
||||
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||
return " ".join(lines)
|
||||
|
||||
|
||||
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||
order_id = result.get("order_id")
|
||||
transportadora = result.get("transportadora")
|
||||
codigo = result.get("codigo_rastreio")
|
||||
previsao = result.get("previsao_entrega")
|
||||
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||
return None
|
||||
return (
|
||||
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||
f"rastreio {codigo}, previsão {previsao}."
|
||||
)
|
||||
|
||||
|
||||
def register_tool_renderers() -> None:
|
||||
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||
register_tool_response_renderer("retail.order", render_retail_order)
|
||||
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||
@@ -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]
|
||||
Binary file not shown.
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
@@ -0,0 +1,7 @@
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
@@ -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
Reference in New Issue
Block a user