adjustments: manual

This commit is contained in:
2026-09-12 07:11:07 -03:00
parent 7a22b7af11
commit 2f9b053098
3 changed files with 940 additions and 231 deletions

483
README.md
View File

@@ -1049,6 +1049,73 @@ Com llm_profiles.yaml:
## 5. Criando um novo agente ## 5. Criando um novo agente
### 5.0. Contrato atual de implementação (leia antes de copiar código)
O template executável em `templates/agent_template_backend` é a referência de código. Um novo agente deve reutilizar o runtime público do framework; não copie uma versão de `AgentRuntimeMixin` para dentro do domínio. O arquivo local `app/agents/runtime.py` existe apenas como adaptador e também registra os renderizadores de resposta:
```python
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
```
Por que essas declarações importam:
| Declaração | Obrigatória? | Motivo |
| --- | --- | --- |
| `AgentRuntimeMixin` | Sim | Centraliza MCP, transações, RAG, memória, cache, mensagens e telemetria; evita motores paralelos dentro dos agentes. |
| `register_tool_renderers()` | Sim no template atual | Registra a apresentação de resultados MCP antes da primeira execução. Sem isso, uma resposta direta pode cair no formato genérico. |
| `apply_agent_profile_prompt` | Sim para agentes do template | Combina o prompt do código com a política isolada selecionada por `agent_id`. |
| `name` na classe | Sim | Identifica a especialidade em RAG, prompts e telemetria. `route` é o nome do nó e `agent_id` é o perfil/backend; são conceitos diferentes. Para agentes novos, use o mesmo identificador quando não houver motivo de compatibilidade para divergir. |
| Dependências no `__init__` | Sim, quando fornecidas pelo workflow | O mixin usa `llm`, `tool_router`, `rag_service`, `cache`, `settings`, `observer`, `memory` e `summary_memory`; não as instancie novamente no agente. |
| `transaction_state_patch(state)` | Sim em todo retorno que possa participar de transação | Preserva coleta, confirmação, evidência, workflow e limpeza de estado entre turnos. |
| `prepare_memory_context(state)` | Sim antes de `build_messages()` quando memória estiver habilitada | Injeta resumo e janela recente sem duplicar todo o histórico. |
Importe pelo adaptador do template:
```python
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
```
Não importe módulos internos como `agent_framework.runtime.agent_runtime`: esse caminho é detalhe de implementação e pode mudar. Também não recrie cliente LLM, roteador MCP, memória ou telemetria dentro da classe; o workflow injeta instâncias já configuradas.
O fluxo mínimo correto de `run()` é:
1. emitir início;
2. coletar/executar contexto MCP;
3. gravar `state["mcp_results"]`;
4. devolver primeiro uma eventual pergunta de parâmetros;
5. devolver depois uma eventual confirmação transacional;
6. usar resposta MCP direta somente quando a política de apresentação declarar que ela é suficiente;
7. recuperar RAG quando aplicável;
8. preparar memória, montar mensagens e chamar o LLM;
9. incluir `transaction_state_patch(state)` em todos os retornos relevantes;
10. emitir conclusão.
Essa ordem é funcional, não apenas estética: consultar RAG ou chamar o LLM antes de tratar uma confirmação pendente pode responder à pergunta errada ou executar novamente uma operação.
### 5.0.1. O que realmente deve ser copiado
Neste capítulo, somente os blocos marcados como **COPIAR** formam a implementação. Os demais fragmentos explicam uma API ou mostram um anti-pattern e não constituem, isoladamente, um agente executável.
| Ordem | Arquivo | Declaração necessária |
| --- | --- | --- |
| 1 | `app/agents/financeiro_agent.py` | Classe completa da seção 5.3. |
| 2 | `app/workflows/agent_graph.py` | Import, instanciação com `agent_kwargs`, wrapper, nó, rota e handler do supervisor; capítulo 6. |
| 3 | `config/agents.yaml` | Cadastro do `agent_id` e paths; capítulo 8. |
| 4 | `config/agents/financeiro_agent/*.yaml` | Prompt, guardrails e judges isolados; capítulo 9. |
| 5 | `config/routing.yaml` | Intent, estado, agente, tools, examples, keywords e prioridade; capítulo 10. |
| 6 | `config/tools.yaml` | Catálogo, servidor, schema semântico e apresentação; capítulo 11. |
| 7 | `config/tool_policies.yaml` | Tipo, confirmação, parâmetros, pré-validação e workflow; seção 11.3.1. |
| 8 | `config/mcp_parameter_mapping.yaml` | Tradução das chaves canônicas para cada MCP; capítulo 13. |
| 9 | `.env` e `llm_profiles.yaml` | Paths e profiles de router, agente, RAG, guardrails e judges; capítulos 4 e 4.2. |
Criar apenas o arquivo Python não registra o agente nem autoriza suas tools. A configuração é parte do contrato executável.
Neste exemplo, vamos criar um agente chamado `financeiro_agent` para atendimento financeiro genérico. Neste exemplo, vamos criar um agente chamado `financeiro_agent` para atendimento financeiro genérico.
### 5.1. Antes do código: o que é um agente neste framework? ### 5.1. Antes do código: o que é um agente neste framework?
@@ -2360,15 +2427,15 @@ Raciocínio e execução pertencem ao Agent Framework.
Esse arquivo deve conter a lógica específica do agente financeiro. Ele deve: Esse arquivo deve conter a lógica específica do agente financeiro. Ele deve:
1. Receber o `state`. 1. Receber o `state`.
2. Separar `context`, `session`, `business_context` e `tool_arguments`. 2. Ler `context`, `session`, `business_context` e `tool_arguments` pelos helpers do mixin, sem duplicar a precedência.
3. Emitir IC de início usando `AgentRuntimeMixin`. 3. Emitir IC de início usando `AgentRuntimeMixin`.
4. Coletar contexto de tools MCP, se houver, usando o MCP Tool Router do framework. 4. Coletar contexto de tools MCP, se houver, usando o MCP Tool Router do framework.
5. Coletar contexto RAG, se houver, usando o RAG genérico do framework. 5. Interromper para coleta ou confirmação transacional quando o runtime solicitar.
6. Montar um prompt de domínio. 6. Entregar resposta MCP direta somente quando houver política explícita de apresentação.
7. Chamar o LLM pelo runtime comum, com cache e telemetria. 7. Coletar RAG quando aplicável e preparar a memória antes das mensagens.
8. Montar uma resposta padronizada. 8. Montar o prompt e chamar o LLM pelo runtime comum.
9. Emitir IC de conclusão. 9. Retornar `rag_results`, metadados de memória e `transaction_state_patch(state)`.
10. Retornar dados para o workflow. 10. Emitir IC de conclusão.
### 5.2.1. Entendendo `state`, `context`, `session`, `business_context` e `tool_arguments` ### 5.2.1. Entendendo `state`, `context`, `session`, `business_context` e `tool_arguments`
@@ -2474,6 +2541,12 @@ No template, o `AgentRuntimeMixin` concentra métodos utilitários como:
| `_emit_grl()` | Emite evento de governança customizado | regra de domínio bloqueou ou sanitizou algo | | `_emit_grl()` | Emite evento de governança customizado | regra de domínio bloqueou ou sanitizou algo |
| `_retrieve_rag_context()` | Consulta o RAG genérico do framework | agente precisa de contexto documental | | `_retrieve_rag_context()` | Consulta o RAG genérico do framework | agente precisa de contexto documental |
| `_collect_mcp_context()` | Chama as tools MCP declaradas no `state.mcp_tools` | agente precisa consultar sistemas externos | | `_collect_mcp_context()` | Chama as tools MCP declaradas no `state.mcp_tools` | agente precisa consultar sistemas externos |
| `transaction_clarification_message()` | Obtém a pergunta canônica do próximo parâmetro | depois do MCP e antes de RAG/LLM |
| `transaction_confirmation_message()` | Obtém a confirmação pendente montada pelo runtime | depois da coleta e antes da execução final |
| `transaction_state_patch()` | Preserva ou limpa corretamente o estado transacional | em todo retorno que possa participar de transação |
| `build_direct_mcp_answer()` | Renderiza MCP somente quando resposta direta foi autorizada | antes de RAG/LLM |
| `prepare_memory_context()` | Prepara resumo e janela recente | imediatamente antes de `build_messages()` |
| `build_messages()` | Monta contexto, MCP, RAG e memória canônicos | antes de `_invoke_llm_cached()` |
| `_cache_get()` | Lê cache genérico | uso avançado, normalmente indireto | | `_cache_get()` | Lê cache genérico | uso avançado, normalmente indireto |
| `_cache_set()` | Grava cache genérico | uso avançado, normalmente indireto | | `_cache_set()` | Grava cache genérico | uso avançado, normalmente indireto |
| `_llm_cache_key()` | Monta chave estável de cache do LLM | normalmente usado internamente | | `_llm_cache_key()` | Monta chave estável de cache do LLM | normalmente usado internamente |
@@ -2637,7 +2710,7 @@ tool_context = await self._collect_mcp_context(state)
O desenvolvedor deve usar esse método quando basta chamar as tools definidas pela intent. O desenvolvedor deve usar esse método quando basta chamar as tools definidas pela intent.
Se o agente precisar escolher argumentos especiais por tool, pular tools perigosas, exigir confirmação ou montar parâmetros adicionais, ele pode implementar um método próprio no agente e chamar o router de forma mais controlada, como no exemplo do `BackofficeAgent`. Se uma tool exigir argumentos especiais, bloqueio, confirmação, pré-validação ou workflow, declare isso em `tools.yaml`, `tool_policies.yaml` e `mcp_parameter_mapping.yaml`. Não implemente um segundo executor no agente. Customização Python só cabe para regra de domínio não representada pelos contratos e deve continuar chamando `execute_tools_for_intent()` ou `_collect_mcp_context()`.
#### 5.2.2.6. Como `_retrieve_rag_context()` funciona #### 5.2.2.6. Como `_retrieve_rag_context()` funciona
@@ -2703,18 +2776,18 @@ Isso evita que cada agente implemente cache de forma diferente.
O desenvolvedor deve entender que o cache é útil para prompts determinísticos ou consultas repetidas, mas deve ser usado com cuidado em ações sensíveis. O agente não deve confirmar operação externa apenas porque uma resposta de LLM veio de cache. Confirmações operacionais devem depender de retorno real da tool. O desenvolvedor deve entender que o cache é útil para prompts determinísticos ou consultas repetidas, mas deve ser usado com cuidado em ações sensíveis. O agente não deve confirmar operação externa apenas porque uma resposta de LLM veio de cache. Confirmações operacionais devem depender de retorno real da tool.
#### 5.2.2.8. Quando usar `_collect_mcp_context()` e quando criar lógica própria #### 5.2.2.8. Quando usar `_collect_mcp_context()` e quando declarar políticas
Use `_collect_mcp_context()` quando: Use `_collect_mcp_context()` para as tools da intent. O próprio runtime aplica o catálogo, o schema e as políticas declaradas, inclusive em operações sensíveis.
```text ```text
a intent já definiu as tools corretas a intent já definiu as tools corretas
os parâmetros canônicos já estão no business_context os parâmetros canônicos já estão no business_context
a execução pode chamar todas as tools da lista a execução pode chamar todas as tools da lista
nenhuma tool representa ação sensível as políticas transacionais estão declaradas em tool_policies.yaml
``` ```
Crie lógica própria no agente quando: Declare configuração, e não `if` manual no agente, quando:
```text ```text
uma tool só pode ser chamada após confirmação explícita uma tool só pode ser chamada após confirmação explícita
@@ -2724,14 +2797,17 @@ uma tool de registro/alteração não pode rodar automaticamente
uma sequência de tools depende do resultado anterior uma sequência de tools depende do resultado anterior
``` ```
Exemplo de regra segura: Exemplo de política segura:
```python ```yaml
if tool.startswith("registrar_") and not action_text: tool_policies:
return {"ok": False, "skipped": True, "reason": "ação sem confirmação explícita"} registrar_acao:
operation_type: transactional
require_confirmation: true
requires: [action_text]
``` ```
Isso é regra de domínio e deve ficar no agente, não no mixin. O significado de `action_text` pertence ao domínio, mas coleta, bloqueio e confirmação pertencem ao runtime configurável.
#### 5.2.2.9. Como o dev deve ler o `run()` de um agente que herda o mixin #### 5.2.2.9. Como o dev deve ler o `run()` de um agente que herda o mixin
@@ -2751,78 +2827,25 @@ Ao abrir um agente, o desenvolvedor deve procurar esta estrutura mental:
Se o agente faz isso, ele está usando o framework corretamente. Se o agente faz isso, ele está usando o framework corretamente.
#### 5.2.2.10. Exemplo mínimo de uso correto do mixin #### 5.2.2.10. Ordem mínima correta do mixin
```python Este é um mapa conceitual, **não um segundo bloco para copiar**:
async def run(self, state):
await self._emit_ic(
"IC.FINANCEIRO_AGENT_STARTED",
state,
{"business_component": "financeiro"},
component="agent.financeiro.start",
)
tool_context = await self._collect_tool_context(state) ```text
if tool_context: emitir início
await self._emit_ic( → coletar MCP e salvar state.mcp_results
"IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", → tratar transaction_clarification_message
state, → tratar transaction_confirmation_message
{"tool_result_count": len(tool_context)}, → tentar build_direct_mcp_answer
component="agent.financeiro.mcp", → recuperar RAG
) → prepare_memory_context
→ build_messages
rag_context, rag_metadata = await self._retrieve_rag_context(state) → _invoke_llm_cached
if rag_metadata.get("enabled"): → retornar rag_results + transaction_state_patch
await self._emit_ic( → emitir conclusão
"IC.FINANCEIRO_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.financeiro.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 financeiro. Responda com clareza, usando dados das ferramentas quando disponíveis. Não confirme ações financeiras sem evidência e confirmação explícita."
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages)
result = {
"answer": f"[FinanceiroAgent] {answer}",
"next_state": "FINANCEIRO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
await self._emit_ic(
"IC.FINANCEIRO_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.financeiro.completed",
)
return result
``` ```
Esse exemplo mostra a intenção do mixin: o desenvolvedor escreve o raciocínio do agente, mas delega infraestrutura para métodos padronizados. Use exclusivamente o arquivo completo da seção 5.3 para copiar e colar.
#### 5.2.2.11. Erros comuns ao usar o `AgentRuntimeMixin` #### 5.2.2.11. Erros comuns ao usar o `AgentRuntimeMixin`
@@ -2831,7 +2854,7 @@ Herdar de AgentRuntimeMixin, mas chamar REST diretamente dentro do agente.
Criar outro cache manual em vez de usar _invoke_llm_cached(). Criar outro cache manual em vez de usar _invoke_llm_cached().
Emitir eventos diretamente em formatos diferentes do observer. Emitir eventos diretamente em formatos diferentes do observer.
Colocar regra de domínio dentro do runtime.py. Colocar regra de domínio dentro do runtime.py.
Usar _collect_mcp_context() para tool de ação sem confirmação. Chamar tool de ação fora de _collect_mcp_context()/execute_tools_for_intent() e contornar a política de confirmação.
Ignorar business_context e pegar parâmetros soltos do payload. Ignorar business_context e pegar parâmetros soltos do payload.
Tratar session_id global e backend_session_id como se fossem a mesma coisa. Tratar session_id global e backend_session_id como se fossem a mesma coisa.
Sobrescrever métodos internos do mixin sem necessidade. Sobrescrever métodos internos do mixin sem necessidade.
@@ -3372,7 +3395,7 @@ mcp_parameter_mapping.yaml traduz para o nome esperado por cada MCP Server.
Nem toda tool é apenas consulta. Algumas tools executam ações, como registrar parecer, abrir solicitação, cancelar serviço ou criar protocolo. Nem toda tool é apenas consulta. Algumas tools executam ações, como registrar parecer, abrir solicitação, cancelar serviço ou criar protocolo.
Essas tools devem ser declaradas com política em `config/tools.yaml`: O contrato é dividido: descrição e schema ficam em `config/tools.yaml`; autorização de execução fica em `config/tool_policies.yaml`:
```yaml ```yaml
tools: tools:
@@ -3380,13 +3403,25 @@ tools:
description: Registra ação operacional no backoffice. description: Registra ação operacional no backoffice.
mcp_server: backoffice mcp_server: backoffice
enabled: true enabled: true
tool_type: action
requires: [protocol_id, action_text, operator_session]
confirmation_required: false
args_schema: args_schema:
protocol_id: string protocol_id:
action_text: string type: string
operator_session: string description: Protocolo da interação.
action_text:
type: string
description: Texto da ação autorizada pelo operador.
operator_session:
type: string
description: Sessão autenticada do operador.
```
```yaml
# config/tool_policies.yaml
tool_policies:
registrar_acao_backoffice:
operation_type: transactional
require_confirmation: true
requires: [protocol_id, action_text, operator_session]
``` ```
Com isso, o framework consegue bloquear a chamada antes de chegar ao MCP quando falta campo obrigatório: Com isso, o framework consegue bloquear a chamada antes de chegar ao MCP quando falta campo obrigatório:
@@ -3508,7 +3543,7 @@ Crie:
app/agents/financeiro_agent.py app/agents/financeiro_agent.py
``` ```
Código-base comentado: **COPIAR — arquivo completo canônico:**
```python ```python
from app.agents.prompting import apply_agent_profile_prompt from app.agents.prompting import apply_agent_profile_prompt
@@ -3549,6 +3584,7 @@ class FinanceiroAgent(AgentRuntimeMixin):
) )
tool_context = await self._collect_tool_context(state) tool_context = await self._collect_tool_context(state)
state["mcp_results"] = tool_context
if tool_context: if tool_context:
await self._emit_ic( await self._emit_ic(
"IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", "IC.FINANCEIRO_MCP_CONTEXT_COLLECTED",
@@ -3557,16 +3593,59 @@ class FinanceiroAgent(AgentRuntimeMixin):
component="agent.financeiro.mcp", component="agent.financeiro.mcp",
) )
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[FinanceiroAgent] {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:
return {
"answer": f"[FinanceiroAgent] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="FinanceiroAgent")
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) rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"): rag_event_payload = {
await self._emit_ic( "provider": rag_metadata.get("provider"),
"IC.FINANCEIRO_RAG_CONTEXT_RETRIEVED", "status": rag_metadata.get("status"),
state, "attempted": rag_metadata.get("attempted"),
{ "enabled": rag_metadata.get("enabled"),
"document_count": rag_metadata.get("document_count"), "document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"), "graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"), "latency_ms": rag_metadata.get("latency_ms"),
}, "reason": rag_metadata.get("reason"),
"error": rag_metadata.get("error"),
"query": rag_metadata.get("query"),
"namespace": rag_metadata.get("namespace"),
}
await self._emit_ic(
"IC.FINANCEIRO_RAG_CONTEXT_EVALUATED",
state,
rag_event_payload,
component="agent.financeiro.rag",
)
if rag_metadata.get("enabled") and rag_metadata.get("status") == "executed":
await self._emit_ic(
"IC.FINANCEIRO_RAG_CONTEXT_RETRIEVED",
state,
rag_event_payload,
component="agent.financeiro.rag", component="agent.financeiro.rag",
) )
@@ -3591,7 +3670,16 @@ class FinanceiroAgent(AgentRuntimeMixin):
"next_state": "FINANCEIRO_ACTIVE", "next_state": "FINANCEIRO_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
} }
await self._emit_ic( await self._emit_ic(
@@ -3613,6 +3701,77 @@ class FinanceiroAgent(AgentRuntimeMixin):
### 5.3.1. Como adaptar esse exemplo para um agente real ### 5.3.1. Como adaptar esse exemplo para um agente real
#### 5.3.1.1. Capacidades que o agente herda sem novas declarações Python
O bloco da seção 5.3 parece pequeno porque `_collect_mcp_context()` chama `execute_tools_for_intent()` e materializa `relevant_transaction_evidence`. Não copie os internals abaixo para `FinanceiroAgent`:
| Capacidade automática | Fonte de verdade |
| --- | --- |
| Allowlist e seleção de tools read-only/transacionais | `routing.yaml` + `tools.yaml` + `tool_policies.yaml` |
| Extração semântica dos parâmetros obrigatórios | `requires` + `args_schema` (`type`, `label`, `description`, `user_prompt`) |
| Continuidade de coleta e confirmação | `active_transaction`, snapshot de confirmação e `state_policies` |
| Cancelamento por mudança real de intent | decisão `transaction_interruption=intent_shift` do `EnterpriseRouter` |
| Pré-validação e eventual troca da operação efetiva | `pre_validation` e resposta da tool validora |
| Retomada de workflow pausado | `pending_domain_workflow` + workflow declarativo |
| Clarificação de um resultado MCP ambíguo | `pending_tool_clarification` devolvido pela tool |
| Cache e deduplicação MCP | configuração de cache da tool e runtime MCP |
| Evidência operacional para supervisor/judges | `relevant_transaction_evidence` materializada pelo mixin |
| Encerramento antecipado por `BLOCKED`/resultado terminal | envelope MCP e política de terminação |
| RAG solicitado pela própria tool | `requires_rag` e eventual `rag_query` no resultado MCP |
| Composição obrigatória pelo LLM | `requires_llm_composition` e instruções estruturadas do MCP |
| Resposta direta segura | `response.direct`/renderer declarado, nunca apenas a existência de payload |
O agente deve apenas respeitar a ordem dos helpers e devolver os patches. Se uma dessas capacidades não funcionar, corrija YAML, contrato MCP ou runtime; não acrescente uma segunda máquina de estado ao agente financeiro.
#### 5.3.1.2. O que deve ser específico do agente financeiro
Personalize no agente somente identidade da classe, códigos IC, `business_component`, prompt de domínio, `agent_label` e estado ativo. Regras permanentes mais extensas devem ficar em `config/agents/financeiro_agent/prompt_policy.yaml`; schemas, confirmação e execução permanecem nos YAMLs próprios.
#### 5.3.1.3. Leitura comentada do `run()`, etapa por etapa
1. **Evento de início.** `_emit_ic()` acrescenta correlação de sessão, tenant, agent, route e intent. A ausência do observer não interrompe o atendimento.
2. **Coleta MCP.** `_collect_tool_context()` é apenas um nome legível no domínio; ele delega a `_collect_mcp_context()`. O runtime escolhe e executa as tools permitidas, aplica políticas e grava evidência operacional.
3. **Materialização dos resultados.** `state["mcp_results"] = tool_context` precisa ocorrer antes de clarificação, RAG e messages. RAG, supervisor e judges devem observar o mesmo conjunto de evidências.
4. **Clarificação de parâmetro.** `transaction_clarification_message()` detecta `COLLECTING_PARAMETERS` e usa `label`, `description` e `user_prompt` do schema. O retorno antecipado impede RAG/LLM de responder no lugar da pergunta transacional.
5. **Confirmação.** `transaction_confirmation_message()` apresenta a operação e os argumentos congelados. A ação ainda não é executada nesse retorno; o aceite será tratado no turno seguinte pelo runtime/router.
6. **Resposta direta.** `build_direct_mcp_answer()` só encerra cedo quando existe resultado terminal, workflow final ou `response.direct: true` com renderer/template válido. `requires_rag` e `requires_llm_composition` impedem esse atalho.
7. **Avaliação do RAG.** `_retrieve_rag_context()` pode retornar `executed`, `empty`, `error`, `blocked`, `no_service` ou skipped. Por isso o exemplo emite `RAG_CONTEXT_EVALUATED` sempre e `RETRIEVED` somente para busca realmente executada.
8. **Memória.** `prepare_memory_context()` é assíncrono e deve vir antes de `build_messages()`. Ele prepara resumo/janela recente; o builder apenas consome o resultado.
9. **Prompt.** `apply_agent_profile_prompt()` combina o prefixo do agente registrado com a regra local. `build_messages()` acrescenta pergunta sanitizada, contexto necessário, MCP, RAG e memória sem despejar o state bruto.
10. **LLM.** `_invoke_llm_cached()` usa o profile `FinanceiroAgent`, telemetria, chave contextual de cache e retry compactado em erro de janela de contexto.
11. **Contrato de saída.** `rag`, `rag_context`, `rag_results` e `memory_context_metadata` permitem que os próximos nós auditem a origem da resposta.
12. **Patch transacional.** `transaction_state_patch(state)` deve estar no retorno final e em todos os retornos antecipados transacionais; omiti-lo perde continuidade ou mantém latches antigos.
13. **Evento de conclusão.** Deve ser emitido depois que o resultado estiver montado. Retornos antecipados representam espera/resposta direta e, portanto, não passam por esse evento final de composição LLM.
#### 5.3.1.4. Estado esperado nos caminhos principais
| Caminho | LLM chamado? | RAG chamado? | Estado/saída essencial |
| --- | --- | --- | --- |
| Consulta comum sem resposta direta | Sim | Conforme política | `answer`, `mcp_results`, `rag_results`, patch transacional |
| Falta parâmetro | Não | Não | `COLLECTING_PARAMETERS`, `missing_parameters`, `active_transaction` |
| Aguarda aceite | Não | Não | `AWAITING_CONFIRMATION`, `pending_tool_call`, snapshot imutável |
| Confirmação aceita | Depende do resultado | Depende das diretivas MCP | resultado real da tool e evidência da execução |
| Confirmação recusada | Não | Não | transação `CANCELLED` e latches limpos |
| Resposta MCP direta | Não | Não | texto do renderer/template e `reason=direct_mcp_answer` |
| Tool exige RAG | Sim, após retrieval | Sim | `required_by_tool=true`, query efetiva e documentos recuperados |
| Tool exige composição | Sim | Conforme diretiva | instruções MCP incluídas em `messages` |
| RAG falha tecnicamente | Sim, com outras evidências | Tentado | `status=error`; a jornada conversacional continua |
| Workflow pausado | Não até receber a entrada | Não | prompt declarativo e `pending_domain_workflow` |
| Resultado terminal/handoff | Não | Não | mensagem terminal; cadeia de tools encerrada |
#### 5.3.1.5. Por que não há `try/except` genérico no agente
MCP, RAG, observabilidade, cache e compactação já possuem tratamento na camada apropriada. Um `except Exception` no `FinanceiroAgent` esconderia falhas contratuais e poderia converter uma operação bloqueada em resposta aparentemente bem-sucedida. Capture exceções no agente somente quando houver uma recuperação específica de negócio; caso contrário, deixe o workflow e a telemetria registrarem a falha.
#### 5.3.1.6. Como validar o exemplo copiável
O repositório inclui `tests/test_readme_financeiro_agent_example.py`. O teste extrai o código diretamente de `README.md` e `README_en.md`, compila, instancia e exercita composição normal, clarificação, confirmação, resposta direta e avaliação de RAG. Assim, o exemplo deixa de ser documentação passiva: mudanças incompatíveis falham no teste.
```bash
PYTHONPATH="libs/agent_framework/src:templates/agent_template_backend" \
pytest -q tests/test_readme_financeiro_agent_example.py
```
No exemplo acima, `session`, `business_context` e `tool_arguments` aparecem no prompt para fins didáticos. Em produção, o desenvolvedor deve evitar jogar objetos enormes diretamente no prompt. O ideal é selecionar apenas os campos necessários. No exemplo acima, `session`, `business_context` e `tool_arguments` aparecem no prompt para fins didáticos. Em produção, o desenvolvedor deve evitar jogar objetos enormes diretamente no prompt. O ideal é selecionar apenas os campos necessários.
Exemplo de raciocínio para um agente financeiro: Exemplo de raciocínio para um agente financeiro:
@@ -3626,24 +3785,14 @@ business_context.interaction_key → útil para rastrear protocolo/chamado/inter
tool_arguments → útil quando o Gateway ou Identity Resolver já preparou parâmetros exatos. tool_arguments → útil quando o Gateway ou Identity Resolver já preparou parâmetros exatos.
``` ```
Uma função utilitária comum dentro do agente é um `pick()` com ordem de precedência explícita: Não copie uma função `pick()` para cada agente. A precedência já é contrato de `RuntimeContext`:
```python ```python
def pick(name: str, *, tool_arguments, business_context, ctx, session, session_metadata, state): runtime = self.get_runtime_context(state)
if name in tool_arguments: customer_key = runtime.pick("customer_key", "cpf", "cnpj", "msisdn")
return tool_arguments.get(name)
if isinstance(business_context, dict) and name in business_context:
return business_context.get(name)
if name in ctx:
return ctx.get(name)
if name in session:
return session.get(name)
if name in session_metadata:
return session_metadata.get(name)
return state.get(name)
``` ```
Essa função deixa claro que o agente não está “adivinhando” de onde vem o dado. Ele está seguindo uma política de confiança. Isso mantém uma única política de confiança no framework e evita divergência entre agentes.
### 5.3.2. Onde entra o Agent Gateway nesse código? ### 5.3.2. Onde entra o Agent Gateway nesse código?
@@ -4637,6 +4786,71 @@ Um supervisor pode decidir a sequência de agentes, handoff ou combinação de r
Use router quando o domínio for bem mapeado. Use supervisor quando a conversa exigir decomposição, múltiplos agentes ou decisão mais flexível. Use router quando o domínio for bem mapeado. Use supervisor quando a conversa exigir decomposição, múltiplos agentes ou decisão mais flexível.
### 10.6. Contrato completo e inteligência de roteamento
O roteamento atual não se limita a keywords. Ele aplica política de estado/continuidade, reconhecimento determinístico, fallback semântico por LLM quando habilitado e fallback seguro. A LLM propõe uma intent; o framework valida nome, agente, habilitação e confiança contra o catálogo YAML. Ela não pode inventar uma rota.
```yaml
router:
mode: router # router | supervisor; ROUTING_MODE pode sobrescrever
fallback_agent: financeiro_agent
confidence_threshold: 0.65 # confiança mínima do roteador/continuidade
allow_handoff: true
transaction_confirmation:
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: |
Classifique somente a confirmação pendente.
multi_intent:
enabled: true
llm_confidence_threshold: 0.65
max_operations: 4
state_policies:
- state: COLLECTING_FINANCEIRO_PARAMETERS
agent: financeiro_agent
description: Preserva a coleta quando a próxima fala contém apenas um valor.
- state: WAITING_FINANCEIRO_CONFIRMATION
agent: financeiro_agent
description: Preserva respostas curtas à confirmação.
intents:
- name: financeiro_pagamentos
enabled: true
domain: financeiro
agent: financeiro_agent
description: Consulta e explicação de pagamentos.
priority: 15
mcp_tools: [consultar_titulo_financeiro, consultar_pagamentos_financeiro]
keywords: [pagamento, boleto, segunda via]
examples:
- Meu pagamento ainda não foi baixado.
```
| Atributo | Efeito |
| --- | --- |
| `description` | Dá contexto de negócio ao fallback semântico; escreva limites positivos e negativos da intent. |
| `examples` | Frases representativas para classificação semântica; varie formulações, sem transformar a lista em keywords. |
| `keywords` | Evidência determinística. Prefira expressões específicas; termos genéricos aumentam colisões. |
| `priority` | Desempata candidatos: número menor tem precedência. Não é confiança. |
| `enabled` | Desativa a intent sem remover seu contrato. Intents desabilitadas não são aceitas da LLM. |
| `domain` | Isola vocabulário e ajuda composição multiagente/multi-intent. |
| `mcp_tools` | Lista permitida e ordenada de tools para a intent; os nomes devem existir em `tools.yaml`. |
| `state_policies` | Garante continuidade para estados de coleta e confirmação antes de reclassificar texto curto. |
| `confidence_threshold` | Evita aceitar classificação semântica fraca. Calibre com regressão offline. |
| `allow_handoff` | Permite troca controlada de agente quando a decisão válida aponta outra rota. |
Para múltiplos pedidos na mesma frase, habilite `multi_intent` e mantenha cada operação ligada a uma intent cadastrada. Operações transacionais continuam sujeitas a coleta e confirmação individual; o planejador não elimina as políticas de tool.
Use o profile `router` em `llm_profiles.yaml` e habilite o roteador LLM pela configuração/variável prevista no ambiente. Se o provedor estiver indisponível ou a confiança ficar abaixo do limiar, o comportamento continua determinístico e fail-safe. Teste: keyword inequívoca, frase sem keyword, colisão, intent desabilitada, resposta curta pendente, `intent_shift` e duas operações na mesma fala.
--- ---
## 11. Configurando tools em `config/tools.yaml` ## 11. Configurando tools em `config/tools.yaml`
@@ -4709,6 +4923,47 @@ O resultado tem dados sensíveis que precisam ser mascarados?
O backend não deve chamar diretamente HTTP/SOAP/DB de sistemas de negócio quando essa chamada puder ser padronizada via MCP Tool Router. O backend não deve chamar diretamente HTTP/SOAP/DB de sistemas de negócio quando essa chamada puder ser padronizada via MCP Tool Router.
### 11.3.1. Separação obrigatória: catálogo versus política
`tools.yaml` descreve **o que a tool é e como chamá-la**. `tool_policies.yaml` descreve **quando e sob quais condições ela pode executar**. Novos agentes devem usar essa separação; campos legados em `tools.yaml` permanecem somente para compatibilidade.
```yaml
# config/tool_policies.yaml
version: 1
defaults:
operation_type: read_only
require_confirmation: false
tool_policies:
cancelar_acordo:
operation_type: transactional
require_confirmation: true
requires: [customer_key, agreement_id]
pre_validation:
enabled: true
tool: validar_cancelamento_acordo
fail_open: false
execution:
mode: workflow
workflow: cancelamento_acordo
version: active
```
| Campo | Regra de uso |
| --- | --- |
| `operation_type` | `read_only` pode executar sem confirmação; `transactional` participa de coleta, confirmação, evidência e idempotência. |
| `require_confirmation` | Exige aceite explícito. Não infira confirmação no código do agente. |
| `requires` | Fonte canônica dos parâmetros obrigatórios. O runtime pergunta um campo semântico por vez e preserva valores coletados. |
| `pre_validation.enabled` | Executa uma consulta de elegibilidade antes da confirmação/ação. |
| `pre_validation.tool` | Tool read-only que valida a operação efetiva; deve existir em `tools.yaml`. |
| `pre_validation.fail_open` | Em `false`, indisponibilidade ou reprovação bloqueia a ação; recomendado para operações sensíveis. |
| `execution.mode` | `direct_tool` (padrão) chama MCP; `workflow` entrega ao workflow declarativo; `agent` delega ao agente. |
| `execution.workflow` / `version` | Nome em `workflows/` e versão resolvida (`active` ou explícita). |
Não duplique `requires` em Python. Declare tipo, descrição e valores esperados, quando aplicável, no `args_schema` de `tools.yaml`: esses metadados alimentam o extrator transacional único, que recebe a fala atual, valores conhecidos e contexto conversacional delimitado da própria transação, além das perguntas de coleta. “Valor” é insuficiente; prefira “valor monetário da cobrança contestada, em reais”.
O MCP também pode declarar `requires_rag`, `requires_llm_composition` ou resposta `direct` com política de apresentação registrada. `direct` deve ser explícito: existir payload MCP não significa que ele já seja uma resposta adequada ao cliente.
### 11.4. Cache do MCP ### 11.4. Cache do MCP
### 11.4.1. Visão geral ### 11.4.1. Visão geral

View File

@@ -1046,6 +1046,73 @@ With llm_profiles.yaml:
## 5. Creating a new agent ## 5. Creating a new agent
### 5.0. Current implementation contract (read before copying code)
The executable template under `templates/agent_template_backend` is the code reference. A new agent must reuse the framework public runtime; do not copy an `AgentRuntimeMixin` implementation into the domain. The local `app/agents/runtime.py` file is only an adapter and also registers response renderers:
```python
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
from app.presentation import register_tool_renderers
register_tool_renderers()
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
```
Why these declarations matter:
| Declaration | Required? | Reason |
| --- | --- | --- |
| `AgentRuntimeMixin` | Yes | Centralizes MCP, transactions, RAG, memory, cache, messages, and telemetry; prevents parallel engines inside agents. |
| `register_tool_renderers()` | Yes in the current template | Registers MCP result presentation before the first execution. Without it, a direct response may use the generic format. |
| `apply_agent_profile_prompt` | Yes for template agents | Combines the code prompt with the isolated policy selected by `agent_id`. |
| Class `name` | Yes | Identifies the specialty in RAG, prompts, and telemetry. `route` is the node name and `agent_id` is the profile/backend; they are different concepts. For new agents, use one identifier unless compatibility requires divergence. |
| Constructor dependencies | Yes, when supplied by the workflow | The mixin uses `llm`, `tool_router`, `rag_service`, `cache`, `settings`, `observer`, `memory`, and `summary_memory`; do not instantiate them again in the agent. |
| `transaction_state_patch(state)` | Yes for every return that can participate in a transaction | Preserves collection, confirmation, evidence, workflow, and state cleanup across turns. |
| `prepare_memory_context(state)` | Yes before `build_messages()` when memory is enabled | Injects the summary and recent window without duplicating the entire history. |
Import through the template adapter:
```python
from app.agents.prompting import apply_agent_profile_prompt
from app.agents.runtime import AgentRuntimeMixin
```
Do not import internal modules such as `agent_framework.runtime.agent_runtime`: that path is an implementation detail and may change. Do not recreate the LLM client, MCP router, memory, or telemetry inside the class; the workflow injects configured instances.
The correct minimum `run()` flow is:
1. emit start;
2. collect/execute MCP context;
3. write `state["mcp_results"]`;
4. return a parameter clarification first, when present;
5. return a transactional confirmation next, when present;
6. use a direct MCP answer only when its presentation policy declares it sufficient;
7. retrieve RAG when applicable;
8. prepare memory, build messages, and call the LLM;
9. include `transaction_state_patch(state)` in every relevant return;
10. emit completion.
This order is functional, not merely stylistic: retrieving RAG or calling the LLM before handling a pending confirmation can answer the wrong question or replay an operation.
### 5.0.1. What must actually be copied
Only blocks marked **COPY** form the implementation. Other fragments explain an API or show an anti-pattern and do not constitute an executable agent by themselves.
| Order | File | Required declaration |
| --- | --- | --- |
| 1 | `app/agents/financeiro_agent.py` | Complete class from section 5.3. |
| 2 | `app/workflows/agent_graph.py` | Import, `agent_kwargs` instantiation, wrapper, node, route, and supervisor handler; chapter 6. |
| 3 | `config/agents.yaml` | `agent_id` registration and paths; chapter 8. |
| 4 | `config/agents/financeiro_agent/*.yaml` | Isolated prompt, guardrails, and judges; chapter 9. |
| 5 | `config/routing.yaml` | Intent, state, agent, tools, examples, keywords, and priority; chapter 10. |
| 6 | `config/tools.yaml` | Catalog, server, semantic schema, and presentation; chapter 11. |
| 7 | `config/tool_policies.yaml` | Type, confirmation, parameters, pre-validation, and workflow; section 11.3.1. |
| 8 | `config/mcp_parameter_mapping.yaml` | Canonical-key translation for each MCP; chapter 13. |
| 9 | `.env` and `llm_profiles.yaml` | Router, agent, RAG, guardrail, and judge paths/profiles; chapters 4 and 4.2. |
Creating only the Python file neither registers the agent nor authorizes its tools. Configuration is part of the executable contract.
In this example, we will create an agent called `finance_agent` for generic financial service. In this example, we will create an agent called `finance_agent` for generic financial service.
### 5.1. Before the code: what is an agent in this framework? ### 5.1. Before the code: what is an agent in this framework?
@@ -2356,15 +2423,15 @@ Reasoning and execution belong to the Agent Framework.
This file must contain the specific logic of the financial agent. It must: This file must contain the specific logic of the financial agent. It must:
1. Receive the `state`. 1. Receive the `state`.
2. Separate `context`, `session`, `business_context` and `tool_arguments`. 2. Read `context`, `session`, `business_context`, and `tool_arguments` through mixin helpers without duplicating precedence.
3. Issue start IC using `AgentRuntimeMixin`. 3. Issue start IC using `AgentRuntimeMixin`.
4. Collect context from MCP tools, if any, using the framework's MCP Tool Router. 4. Collect context from MCP tools, if any, using the framework's MCP Tool Router.
5. Collect RAG context, if any, using the framework's generic RAG. 5. Stop for parameter collection or transactional confirmation when runtime requests it.
6. Set up a domain prompt. 6. Return a direct MCP response only when an explicit presentation policy allows it.
7. Call the LLM through the common runtime, with cache and telemetry. 7. Collect RAG when applicable and prepare memory before building messages.
8. Assemble a standardized response. 8. Build the prompt and call the LLM through the common runtime.
9. Issue completion IC. 9. Return `rag_results`, memory metadata, and `transaction_state_patch(state)`.
10. Return data to the workflow. 10. Emit completion IC.
### 5.2.1. Understanding `state`, `context`, `session`, `business_context` and `tool_arguments` ### 5.2.1. Understanding `state`, `context`, `session`, `business_context` and `tool_arguments`
@@ -2470,6 +2537,12 @@ In the template, `AgentRuntimeMixin` concentrates utility methods such as:
| `_emit_grl()` | Issues custom governance event | domain rule blocked or sanitized something | | `_emit_grl()` | Issues custom governance event | domain rule blocked or sanitized something |
| `_retrieve_rag_context()` | Queries the framework's generic RAG | agent needs document context | | `_retrieve_rag_context()` | Queries the framework's generic RAG | agent needs document context |
| `_collect_mcp_context()` | Calls the MCP tools declared in `state.mcp_tools` | agent needs to consult external systems | | `_collect_mcp_context()` | Calls the MCP tools declared in `state.mcp_tools` | agent needs to consult external systems |
| `transaction_clarification_message()` | Returns the canonical question for the next parameter | after MCP and before RAG/LLM |
| `transaction_confirmation_message()` | Returns the pending confirmation prepared by runtime | after collection and before final execution |
| `transaction_state_patch()` | Correctly preserves or clears transactional state | in every return that may participate in a transaction |
| `build_direct_mcp_answer()` | Renders MCP only when direct response is authorized | before RAG/LLM |
| `prepare_memory_context()` | Prepares the summary and recent window | immediately before `build_messages()` |
| `build_messages()` | Builds canonical context, MCP, RAG, and memory messages | before `_invoke_llm_cached()` |
| `_cache_get()` | Reads generic cache | advanced use, usually indirect | | `_cache_get()` | Reads generic cache | advanced use, usually indirect |
| `_cache_set()` | Writes generic cache | advanced use, usually indirect | | `_cache_set()` | Writes generic cache | advanced use, usually indirect |
| `_llm_cache_key()` | Creates a stable LLM cache key | normally used internally | | `_llm_cache_key()` | Creates a stable LLM cache key | normally used internally |
@@ -2520,7 +2593,18 @@ Therefore, this pattern appears in the agent:
class FinanceiroAgent(AgentRuntimeMixin): class FinanceiroAgent(AgentRuntimeMixin):
name = "financeiro_agent" name = "financeiro_agent"
def __init__(self, llm, telemetry=None, tool_router=None, rag_service=None, cache=None, settings=None, observer=None): 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.llm = llm
self.telemetry = telemetry self.telemetry = telemetry
self.tool_router = tool_router self.tool_router = tool_router
@@ -2528,6 +2612,8 @@ name = "financeiro_agent"
self.cache = cache self.cache = cache
self.settings = settings self.settings = settings
self.observer = observer self.observer = observer
self.memory = memory
self.summary_memory = summary_memory
``` ```
This means: This means:
@@ -2620,7 +2706,7 @@ tool_context = await self._collect_mcp_context(state)
The developer should use this method when it is sufficient to call the tools defined by the intent. The developer should use this method when it is sufficient to call the tools defined by the intent.
If the agent needs to choose special arguments per tool, skip dangerous tools, require confirmation, or set up additional parameters, it can implement its own method in the agent and call the router in a more controlled way, as in the `BackofficeAgent` example. If a tool requires special arguments, blocking, confirmation, pre-validation, or a workflow, declare it in `tools.yaml`, `tool_policies.yaml`, and `mcp_parameter_mapping.yaml`. Do not implement a second executor inside the agent. Python customization belongs only to domain behavior not represented by these contracts and must still call `execute_tools_for_intent()` or `_collect_mcp_context()`.
#### 5.2.2.6. How`_retrieve_rag_context()` works #### 5.2.2.6. How`_retrieve_rag_context()` works
@@ -2686,18 +2772,18 @@ This prevents each agent from implementing caching differently.
The developer should understand that caching is useful for deterministic prompts or repeated queries, but should be used with caution for sensitive actions. The agent should not confirm an external operation just because an LLM response came from the cache. Operational confirmations must depend on the actual return from the tool. The developer should understand that caching is useful for deterministic prompts or repeated queries, but should be used with caution for sensitive actions. The agent should not confirm an external operation just because an LLM response came from the cache. Operational confirmations must depend on the actual return from the tool.
#### 5.2.2.8. When to use`_collect_mcp_context()` and when to create your own logic #### 5.2.2.8. When to use `_collect_mcp_context()` and when to declare policies
Use `_collect_mcp_context()` when: Use `_collect_mcp_context()` for intent tools. Runtime applies catalog, schema, and declared policies, including sensitive operations.
```text ```text
the intent has already defined the correct tools the intent has already defined the correct tools
the canonical parameters are already in the business_context the canonical parameters are already in the business_context
the execution can call all the tools in the list the execution can call all the tools in the list
no tool represents a sensitive action transactional policies are declared in tool_policies.yaml
``` ```
Create your own logic in the agent when: Declare configuration, rather than a manual agent `if`, when:
```text ```text
a tool can only be called after explicit confirmation a tool can only be called after explicit confirmation
@@ -2707,14 +2793,17 @@ a registration/change tool cannot run automatically
a sequence of tools depends on the previous result a sequence of tools depends on the previous result
``` ```
Example of a safe rule: Example of a safe policy:
```python ```yaml
if tool.startswith("registrar_") and not action_text: tool_policies:
return {"ok": False, "skipped": True, "reason": "action without explicit confirmation"} registrar_acao:
operation_type: transactional
require_confirmation: true
requires: [action_text]
``` ```
This is a domain rule and should stay in the agent, not in the mixin. The meaning of `action_text` belongs to the domain, but collection, blocking, and confirmation belong to configurable runtime.
#### 5.2.2.9. How the dev should read the `run()` of an agent that inherits the mixin #### 5.2.2.9. How the dev should read the `run()` of an agent that inherits the mixin
@@ -2734,78 +2823,25 @@ When opening an agent, the developer should look for this mental structure:
If the agent does this, it is using the framework correctly. If the agent does this, it is using the framework correctly.
#### 5.2.2.10. Minimum example of correct use of the mixin #### 5.2.2.10. Correct minimum mixin order
```python This is a conceptual map, **not a second copy block**:
async def run(self, state):
await self._emit_ic(
"IC.FINANCEIRO_AGENT_STARTED",
state,
{"business_component": "financeiro"},
component="agent.financeiro.start",
)
tool_context = await self._collect_tool_context(state) ```text
if tool_context: emit start
await self._emit_ic( → collect MCP and save state.mcp_results
"IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", → handle transaction_clarification_message
state, → handle transaction_confirmation_message
{"tool_result_count": len(tool_context)}, → try build_direct_mcp_answer
component="agent.financeiro.mcp", → retrieve RAG
) → prepare_memory_context
→ build_messages
rag_context, rag_metadata = await self._retrieve_rag_context(state) → _invoke_llm_cached
if rag_metadata.get("enabled"): → return rag_results + transaction_state_patch
await self._emit_ic( → emit completion
"IC.FINANCEIRO_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.financeiro.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,
"You are a financial agent. Respond clearly, using data from available tools. Do not confirm financial actions without evidence and explicit confirmation."
),
mcp_results=tool_context,
rag_context=rag_context,
rag_metadata=rag_metadata,
)
answer = await self._invoke_llm_cached(state, "FinanceiroAgent", messages)
result = {
"answer": f"[FinanceiroAgent] {answer}",
"next_state": "FINANCEIRO_ACTIVE",
"mcp_results": tool_context,
"rag": rag_metadata,
"memory_context_metadata": state.get("memory_context_metadata"),
}
await self._emit_ic(
"IC.FINANCEIRO_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.financeiro.completed",
)
return result
``` ```
This example shows the intention of the mixin: the developer writes the agent's reasoning, but delegates infrastructure to standardized methods. Use only the complete file in section 5.3 for copy and paste.
#### 5.2.2.11. Common mistakes when using `AgentRuntimeMixin` #### 5.2.2.11. Common mistakes when using `AgentRuntimeMixin`
@@ -2814,7 +2850,7 @@ Inherit from AgentRuntimeMixin, but call REST directly inside the agent.
Create another manual cache instead of using _invoke_llm_cached(). Create another manual cache instead of using _invoke_llm_cached().
Emit events directly in formats other than the observer. Emit events directly in formats other than the observer.
Placing a domain rule inside runtime.py. Placing a domain rule inside runtime.py.
Using _collect_mcp_context() for an action tool without confirmation. Calling an action tool outside _collect_mcp_context()/execute_tools_for_intent() and bypassing confirmation policy.
Ignore business_context and get loose parameters from the payload. Ignore business_context and get loose parameters from the payload.
Treat global session_id and backend_session_id as if they were the same thing. Treat global session_id and backend_session_id as if they were the same thing.
Overwrite internal mixin methods unnecessarily. Overwrite internal mixin methods unnecessarily.
@@ -3353,22 +3389,33 @@ mcp_parameter_mapping.yaml translates to the name expected by each MCP Server.
Not every tool is just for consultation. Some tools perform actions, such as registering an opinion, opening a request, canceling a service, or creating a protocol. Not every tool is just for consultation. Some tools perform actions, such as registering an opinion, opening a request, canceling a service, or creating a protocol.
These tools must be declared with a policy in `config/tools.yaml`: The contract is split: description and schema belong in `config/tools.yaml`; execution authorization belongs in `config/tool_policies.yaml`:
```yaml ```yaml
tools:
tools: tools:
registrar_acao_backoffice: registrar_acao_backoffice:
description: Registers operational action in the backoffice. description: Registers operational action in the backoffice.
mcp_server: backoffice mcp_server: backoffice
enabled: true enabled: true
tool_type: action
requires: [protocol_id, action_text, operator_session]
confirmation_required: false
args_schema: args_schema:
protocol_id: string protocol_id:
action_text: string type: string
operator_session: string description: Interaction protocol.
action_text:
type: string
description: Action text authorized by the operator.
operator_session:
type: string
description: Authenticated operator session.
```
```yaml
# config/tool_policies.yaml
tool_policies:
registrar_acao_backoffice:
operation_type: transactional
require_confirmation: true
requires: [protocol_id, action_text, operator_session]
``` ```
With this, the framework is able to block the call before it reaches the MCP when a required field is missing: With this, the framework is able to block the call before it reaches the MCP when a required field is missing:
@@ -3490,7 +3537,7 @@ Create:
app/agents/financeiro_agent.py app/agents/financeiro_agent.py
``` ```
Annotated base code: **COPY — complete canonical file:**
```python ```python
from app.agents.prompting import apply_agent_profile_prompt from app.agents.prompting import apply_agent_profile_prompt
@@ -3531,6 +3578,7 @@ class FinanceiroAgent(AgentRuntimeMixin):
) )
tool_context = await self._collect_tool_context(state) tool_context = await self._collect_tool_context(state)
state["mcp_results"] = tool_context
if tool_context: if tool_context:
await self._emit_ic( await self._emit_ic(
"IC.FINANCEIRO_MCP_CONTEXT_COLLECTED", "IC.FINANCEIRO_MCP_CONTEXT_COLLECTED",
@@ -3539,16 +3587,59 @@ class FinanceiroAgent(AgentRuntimeMixin):
component="agent.financeiro.mcp", component="agent.financeiro.mcp",
) )
clarification_message = self.transaction_clarification_message(state)
if clarification_message:
return {
"answer": f"[FinanceiroAgent] {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:
return {
"answer": f"[FinanceiroAgent] {confirmation_message}",
"next_state": state.get("next_state"),
"mcp_results": tool_context,
**self.transaction_state_patch(state),
}
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="FinanceiroAgent")
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) rag_context, rag_metadata = await self._retrieve_rag_context(state)
if rag_metadata.get("enabled"): rag_event_payload = {
await self._emit_ic( "provider": rag_metadata.get("provider"),
"IC.FINANCEIRO_RAG_CONTEXT_RETRIEVED", "status": rag_metadata.get("status"),
state, "attempted": rag_metadata.get("attempted"),
{ "enabled": rag_metadata.get("enabled"),
"document_count": rag_metadata.get("document_count"), "document_count": rag_metadata.get("document_count"),
"graph_neighbors": rag_metadata.get("graph_neighbors"), "graph_neighbors": rag_metadata.get("graph_neighbors"),
"latency_ms": rag_metadata.get("latency_ms"), "latency_ms": rag_metadata.get("latency_ms"),
}, "reason": rag_metadata.get("reason"),
"error": rag_metadata.get("error"),
"query": rag_metadata.get("query"),
"namespace": rag_metadata.get("namespace"),
}
await self._emit_ic(
"IC.FINANCEIRO_RAG_CONTEXT_EVALUATED",
state,
rag_event_payload,
component="agent.financeiro.rag",
)
if rag_metadata.get("enabled") and rag_metadata.get("status") == "executed":
await self._emit_ic(
"IC.FINANCEIRO_RAG_CONTEXT_RETRIEVED",
state,
rag_event_payload,
component="agent.financeiro.rag", component="agent.financeiro.rag",
) )
@@ -3573,7 +3664,16 @@ class FinanceiroAgent(AgentRuntimeMixin):
"next_state": "FINANCEIRO_ACTIVE", "next_state": "FINANCEIRO_ACTIVE",
"mcp_results": tool_context, "mcp_results": tool_context,
"rag": rag_metadata, "rag": rag_metadata,
"rag_context": rag_context,
"rag_results": [{
"intent": state.get("intent"),
"agent": self.name,
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
"metadata": rag_metadata,
**({"context": rag_context} if rag_context else {}),
}],
"memory_context_metadata": state.get("memory_context_metadata"), "memory_context_metadata": state.get("memory_context_metadata"),
**self.transaction_state_patch(state),
} }
await self._emit_ic( await self._emit_ic(
@@ -3595,6 +3695,77 @@ class FinanceiroAgent(AgentRuntimeMixin):
### 5.3.1. How to adapt this example to a real agent ### 5.3.1. How to adapt this example to a real agent
#### 5.3.1.1. Capabilities inherited without new Python declarations
The section 5.3 block stays small because `_collect_mcp_context()` calls `execute_tools_for_intent()` and materializes `relevant_transaction_evidence`. Do not copy these internals into `FinanceiroAgent`:
| Automatic capability | Source of truth |
| --- | --- |
| Read-only/transactional tool allowlist and selection | `routing.yaml` + `tools.yaml` + `tool_policies.yaml` |
| Semantic extraction of required parameters | `requires` + `args_schema` (`type`, `label`, `description`, `user_prompt`) |
| Collection and confirmation continuity | `active_transaction`, confirmation snapshot, and `state_policies` |
| Cancellation on a real intent change | `transaction_interruption=intent_shift` decision from `EnterpriseRouter` |
| Pre-validation and possible effective-operation replacement | `pre_validation` and validator-tool response |
| Paused workflow resume | `pending_domain_workflow` + declarative workflow |
| Ambiguous MCP-result clarification | `pending_tool_clarification` returned by the tool |
| MCP cache and deduplication | tool cache configuration and MCP runtime |
| Operational evidence for supervisor/judges | `relevant_transaction_evidence` materialized by the mixin |
| Early termination on `BLOCKED`/terminal result | MCP envelope and termination policy |
| Tool-requested RAG | `requires_rag` and optional `rag_query` in MCP output |
| Required LLM composition | `requires_llm_composition` and structured MCP instructions |
| Safe direct response | declared `response.direct`/renderer, never payload presence alone |
The agent only needs to preserve helper order and return the patches. If a capability fails, fix YAML, the MCP contract, or runtime; do not add a second state machine to the financial agent.
#### 5.3.1.2. What must be specific to the financial agent
Customize only the class identity, IC codes, `business_component`, domain prompt, `agent_label`, and active state in the agent. Longer permanent rules belong in `config/agents/financeiro_agent/prompt_policy.yaml`; schemas, confirmation, and execution remain in their dedicated YAML files.
#### 5.3.1.3. Commented walkthrough of `run()`
1. **Start event.** `_emit_ic()` adds session, tenant, agent, route, and intent correlation. A missing observer does not stop service.
2. **MCP collection.** `_collect_tool_context()` is only a domain-readable name delegating to `_collect_mcp_context()`. Runtime selects allowed tools, applies policies, executes them, and records operational evidence.
3. **Result materialization.** `state["mcp_results"] = tool_context` must happen before clarification, RAG, and messages so RAG, supervisor, and judges consume the same evidence.
4. **Parameter clarification.** `transaction_clarification_message()` detects `COLLECTING_PARAMETERS` and uses schema `label`, `description`, and `user_prompt`. Early return prevents RAG/LLM from answering instead of asking the transactional question.
5. **Confirmation.** `transaction_confirmation_message()` presents the frozen operation and arguments. The action is not executed in this return; acceptance is processed by runtime/router on the next turn.
6. **Direct response.** `build_direct_mcp_answer()` exits early only for a terminal result, final workflow, or `response.direct: true` with a valid renderer/template. `requires_rag` and `requires_llm_composition` disable this shortcut.
7. **RAG evaluation.** `_retrieve_rag_context()` may return `executed`, `empty`, `error`, `blocked`, `no_service`, or skipped. Therefore the example always emits `RAG_CONTEXT_EVALUATED` and emits `RETRIEVED` only for an actual execution.
8. **Memory.** `prepare_memory_context()` is asynchronous and must precede `build_messages()`. It prepares the summary/recent window; the builder only consumes it.
9. **Prompt.** `apply_agent_profile_prompt()` combines the registered-agent prefix with the local rule. `build_messages()` adds sanitized input, required context, MCP, RAG, and memory without dumping raw state.
10. **LLM.** `_invoke_llm_cached()` uses the `FinanceiroAgent` profile, telemetry, contextual cache key, and compacted retry on context-window errors.
11. **Output contract.** `rag`, `rag_context`, `rag_results`, and `memory_context_metadata` let downstream nodes audit the response source.
12. **Transactional patch.** `transaction_state_patch(state)` belongs in the final return and every transactional early return; omitting it loses continuity or retains stale latches.
13. **Completion event.** Emit it after assembling the result. Early returns represent waiting/direct-response branches and do not pass through the final LLM-composition event.
#### 5.3.1.4. Expected state for primary paths
| Path | LLM called? | RAG called? | Essential state/output |
| --- | --- | --- | --- |
| Ordinary query without direct response | Yes | According to policy | `answer`, `mcp_results`, `rag_results`, transactional patch |
| Missing parameter | No | No | `COLLECTING_PARAMETERS`, `missing_parameters`, `active_transaction` |
| Awaiting acceptance | No | No | `AWAITING_CONFIRMATION`, `pending_tool_call`, immutable snapshot |
| Accepted confirmation | Depends on result | Depends on MCP directives | actual tool result and execution evidence |
| Rejected confirmation | No | No | `CANCELLED` transaction and cleared latches |
| Direct MCP response | No | No | renderer/template text and `reason=direct_mcp_answer` |
| Tool requires RAG | Yes, after retrieval | Yes | `required_by_tool=true`, effective query, and retrieved documents |
| Tool requires composition | Yes | According to directive | MCP instructions included in `messages` |
| Technical RAG failure | Yes, with other evidence | Attempted | `status=error`; conversation continues |
| Paused workflow | Not until input arrives | No | declarative prompt and `pending_domain_workflow` |
| Terminal result/handoff | No | No | terminal message; tool chain stopped |
#### 5.3.1.5. Why the agent has no generic `try/except`
MCP, RAG, observability, cache, and compaction are handled in their proper layers. An `except Exception` in `FinanceiroAgent` would hide contract failures and might turn a blocked operation into an apparently successful answer. Catch exceptions in the agent only for a specific business recovery; otherwise let the workflow and telemetry record the failure.
#### 5.3.1.6. How to validate the copyable example
The repository includes `tests/test_readme_financeiro_agent_example.py`. It extracts code directly from `README.md` and `README_en.md`, compiles, instantiates, and exercises normal composition, clarification, confirmation, direct response, and RAG evaluation. Documentation is therefore executable: incompatible changes fail the test.
```bash
PYTHONPATH="libs/agent_framework/src:templates/agent_template_backend" \
pytest -q tests/test_readme_financeiro_agent_example.py
```
In the example above, `session`, `business_context`, and `tool_arguments` appear in the prompt for educational purposes. In production, the developer should avoid throwing huge objects directly into the prompt. Ideally, only the necessary fields should be selected. In the example above, `session`, `business_context`, and `tool_arguments` appear in the prompt for educational purposes. In production, the developer should avoid throwing huge objects directly into the prompt. Ideally, only the necessary fields should be selected.
Example of reasoning for a financial agent: Example of reasoning for a financial agent:
@@ -3608,24 +3779,14 @@ business_context.interaction_key → useful for tracking protocol/call/interacti
tool_arguments → useful when the Gateway or Identity Resolver has already prepared exact parameters. tool_arguments → useful when the Gateway or Identity Resolver has already prepared exact parameters.
``` ```
A common utility function within the agent is a `pick()` with an explicit order of precedence: Do not copy a `pick()` function into every agent. Precedence is already a `RuntimeContext` contract:
```python ```python
def pick(name: str, *, tool_arguments, business_context, ctx, session, session_metadata, state): runtime = self.get_runtime_context(state)
if name in tool_arguments: customer_key = runtime.pick("customer_key", "cpf", "cnpj", "msisdn")
return tool_arguments.get(name)
if isinstance(business_context, dict) and name in business_context:
return business_context.get(name)
if name in ctx:
return ctx.get(name)
if name in session:
return session.get(name)
if name in session_metadata:
return session_metadata.get(name)
return state.get(name)
``` ```
This function makes it clear that the agent is not “guessing” where the data comes from. It is following a trust policy. This keeps one trust policy in the framework and prevents divergence among agents.
### 5.3.2. Where does the Agent Gateway come in in this code? ### 5.3.2. Where does the Agent Gateway come in in this code?
@@ -4619,6 +4780,68 @@ A supervisor can decide the sequence of agents, handoff, or combination of respo
Use router when the domain is well mapped. Use supervisor when the conversation requires decomposition, multiple agents, or more flexible decision-making. Use router when the domain is well mapped. Use supervisor when the conversation requires decomposition, multiple agents, or more flexible decision-making.
### 10.6. Complete contract and routing intelligence
Current routing is not limited to keywords. It applies state/continuity policy, deterministic recognition, semantic LLM fallback when enabled, and a safe fallback. The LLM proposes an intent; the framework validates its name, agent, enabled status, and confidence against the YAML catalog. It cannot invent a route.
```yaml
router:
mode: router # router | supervisor; ROUTING_MODE may override
fallback_agent: financeiro_agent
confidence_threshold: 0.65
allow_handoff: true
transaction_confirmation:
semantic_fallback:
enabled: true
allowed_values: [YES, NO, CONTINUE]
confirm_values: [YES]
reject_values: [NO]
continue_values: [CONTINUE]
include_relevant_context: true
profile_name: router
prompt: |
Classify only the pending confirmation.
multi_intent:
enabled: true
llm_confidence_threshold: 0.65
max_operations: 4
state_policies:
- state: COLLECTING_FINANCEIRO_PARAMETERS
agent: financeiro_agent
- state: WAITING_FINANCEIRO_CONFIRMATION
agent: financeiro_agent
intents:
- name: financeiro_pagamentos
enabled: true
domain: financeiro
agent: financeiro_agent
description: Payment lookup and explanation.
priority: 15
mcp_tools: [consultar_titulo_financeiro, consultar_pagamentos_financeiro]
keywords: [payment, invoice, duplicate]
examples: [My payment has not cleared yet.]
```
| Attribute | Effect |
| --- | --- |
| `description` | Gives business context to semantic fallback; document positive and negative intent boundaries. |
| `examples` | Representative utterances for semantic classification; vary phrasing rather than duplicating keywords. |
| `keywords` | Deterministic evidence. Prefer specific phrases; generic terms increase collisions. |
| `priority` | Breaks ties: a lower number wins. It is not confidence. |
| `enabled` | Disables an intent without deleting its contract. Disabled intents are rejected from LLM output. |
| `domain` | Isolates vocabulary and supports multi-agent/multi-intent composition. |
| `mcp_tools` | Ordered allowlist for the intent; names must exist in `tools.yaml`. |
| `state_policies` | Preserves collection and confirmation before reclassifying short text. |
| `confidence_threshold` | Rejects weak semantic classifications. Calibrate with offline regression. |
| `allow_handoff` | Allows a controlled agent change when a valid decision targets another route. |
For several requests in one utterance, enable `multi_intent` and link every operation to a registered intent. Transactional operations still undergo individual collection and confirmation; the planner does not bypass tool policies.
Use the `router` profile in `llm_profiles.yaml` and enable the LLM router through the supported environment setting. If the provider is unavailable or confidence falls below the threshold, behavior remains deterministic and fail-safe. Test unambiguous keywords, no-keyword phrases, collisions, disabled intents, short pending replies, `intent_shift`, and two operations in one utterance.
--- ---
## 11. Configuring tools in `config/tools.yaml` ## 11. Configuring tools in `config/tools.yaml`
@@ -4691,6 +4914,47 @@ Does the result have sensitive data that needs to be masked?
The backend should not directly call HTTP/SOAP/DB from business systems when this call can be standardized via the MCP Tool Router. The backend should not directly call HTTP/SOAP/DB from business systems when this call can be standardized via the MCP Tool Router.
### 11.3.1. Required separation: catalog versus policy
`tools.yaml` describes **what a tool is and how to call it**. `tool_policies.yaml` describes **when and under which conditions it may execute**. New agents must use this separation; legacy fields in `tools.yaml` remain for compatibility only.
```yaml
# config/tool_policies.yaml
version: 1
defaults:
operation_type: read_only
require_confirmation: false
tool_policies:
cancelar_acordo:
operation_type: transactional
require_confirmation: true
requires: [customer_key, agreement_id]
pre_validation:
enabled: true
tool: validar_cancelamento_acordo
fail_open: false
execution:
mode: workflow
workflow: cancelamento_acordo
version: active
```
| Field | Usage rule |
| --- | --- |
| `operation_type` | `read_only` may run without confirmation; `transactional` enters collection, confirmation, evidence, and idempotency. |
| `require_confirmation` | Requires explicit acceptance. Do not infer confirmation in agent code. |
| `requires` | Canonical required parameters. Runtime asks for one semantic field at a time and preserves collected values. |
| `pre_validation.enabled` | Runs an eligibility lookup before confirmation/action. |
| `pre_validation.tool` | Read-only tool that validates the operation; it must exist in `tools.yaml`. |
| `pre_validation.fail_open` | When `false`, unavailability or rejection blocks the action; recommended for sensitive operations. |
| `execution.mode` | `direct_tool` (default) calls MCP; `workflow` delegates to a declarative workflow; `agent` delegates to the agent. |
| `execution.workflow` / `version` | Name under `workflows/` and resolved version (`active` or explicit). |
Do not duplicate `requires` in Python. Declare type, description, and expected values when applicable in the `tools.yaml` `args_schema`: this metadata feeds the single transactional extractor, which receives the current utterance, known values, and bounded conversational context for the same transaction, as well as collection questions. “Value” is insufficient; prefer “monetary amount of the disputed charge, in BRL.”
MCP may also declare `requires_rag`, `requires_llm_composition`, or a `direct` response with a registered presentation policy. `direct` must be explicit: the presence of an MCP payload does not mean it is already an adequate customer-facing response.
### 11.4. MCP Cache ### 11.4. MCP Cache
### 11.4.1. Overview ### 11.4.1. Overview

View File

@@ -0,0 +1,190 @@
from __future__ import annotations
import ast
import re
import sys
from pathlib import Path
from types import MethodType, SimpleNamespace
import pytest
ROOT = Path(__file__).resolve().parents[1]
FRAMEWORK_SRC = ROOT / "libs" / "agent_framework" / "src"
TEMPLATE_ROOT = ROOT / "templates" / "agent_template_backend"
for path in (FRAMEWORK_SRC, TEMPLATE_ROOT):
if str(path) not in sys.path:
sys.path.insert(0, str(path))
EXAMPLES = (
("README.md", "**COPIAR — arquivo completo canônico:**"),
("README_en.md", "**COPY — complete canonical file:**"),
)
class FakeLLM:
def __init__(self):
self.calls = 0
async def ainvoke(self, messages, **kwargs):
self.calls += 1
assert messages
assert kwargs["profile_name"] == "FinanceiroAgent"
return "resposta documentada"
class CaptureObserver:
def __init__(self):
self.events: list[tuple[str, dict, str | None]] = []
async def emit_ic(self, code, payload, component=None):
self.events.append((code, payload, component))
def extract_documented_class(readme_name: str, marker: str):
text = (ROOT / readme_name).read_text(encoding="utf-8")
marked = text[text.index(marker) :]
match = re.search(r"```python\n(.*?)\n```", marked, re.DOTALL)
assert match, f"Bloco Python canônico ausente em {readme_name}"
source = match.group(1)
ast.parse(source, filename=readme_name)
namespace: dict[str, object] = {}
exec(compile(source, readme_name, "exec"), namespace)
return namespace["FinanceiroAgent"], source
def make_agent(agent_class, *, observer=None):
llm = FakeLLM()
agent = agent_class(
llm,
settings=SimpleNamespace(CACHE_TTL_SECONDS=0, RAG_PROVIDER="standard"),
observer=observer,
)
return agent, llm
def base_state():
return {
"user_text": "Qual é a situação do meu pagamento?",
"sanitized_input": "Qual é a situação do meu pagamento?",
"session_id": "readme-test",
"tenant_id": "default",
"agent_id": "financeiro_agent",
"route": "financeiro_agent",
"intent": "financeiro_pagamentos",
"mcp_tools": [],
"context": {},
}
@pytest.mark.parametrize(("readme_name", "marker"), EXAMPLES)
def test_documented_financeiro_agent_compiles_and_uses_current_contract(readme_name, marker):
_, source = extract_documented_class(readme_name, marker)
required = {
"transaction_clarification_message",
"transaction_confirmation_message",
"build_direct_mcp_answer",
"prepare_memory_context",
"build_messages",
"transaction_state_patch",
"rag_results",
"IC.FINANCEIRO_RAG_CONTEXT_EVALUATED",
}
assert all(item in source for item in required)
@pytest.mark.asyncio
@pytest.mark.parametrize(("readme_name", "marker"), EXAMPLES)
async def test_documented_financeiro_agent_runs_normal_composition(readme_name, marker):
agent_class, _ = extract_documented_class(readme_name, marker)
observer = CaptureObserver()
agent, llm = make_agent(agent_class, observer=observer)
result = await agent.run(base_state())
assert result["answer"] == "[FinanceiroAgent] resposta documentada"
assert result["next_state"] == "FINANCEIRO_ACTIVE"
assert result["mcp_results"] == []
assert result["rag"]["status"] == "no_service"
assert result["rag_results"][0]["agent"] == "financeiro_agent"
assert llm.calls == 1
codes = [event[0] for event in observer.events]
assert "IC.FINANCEIRO_AGENT_STARTED" in codes
assert "IC.FINANCEIRO_RAG_CONTEXT_EVALUATED" in codes
assert "IC.FINANCEIRO_AGENT_COMPLETED" in codes
@pytest.mark.asyncio
@pytest.mark.parametrize(("readme_name", "marker"), EXAMPLES)
@pytest.mark.parametrize(
("method_name", "message", "expected_state"),
(
("transaction_clarification_message", "Informe o contrato.", "COLLECTING_PARAMETERS"),
("transaction_confirmation_message", "Confirma a operação?", "AWAITING_CONFIRMATION"),
),
)
async def test_documented_financeiro_agent_returns_before_llm_for_transaction_prompts(
readme_name, marker, method_name, message, expected_state
):
agent_class, _ = extract_documented_class(readme_name, marker)
agent, llm = make_agent(agent_class)
state = base_state()
state["next_state"] = expected_state
setattr(agent, method_name, MethodType(lambda self, state: message, agent))
result = await agent.run(state)
assert result["answer"] == f"[FinanceiroAgent] {message}"
assert result["next_state"] == expected_state
assert llm.calls == 0
@pytest.mark.asyncio
@pytest.mark.parametrize(("readme_name", "marker"), EXAMPLES)
async def test_documented_financeiro_agent_honors_direct_mcp_response(readme_name, marker):
agent_class, _ = extract_documented_class(readme_name, marker)
agent, llm = make_agent(agent_class)
async def collect(self, state):
return [{"ok": True, "tool_name": "consultar_pagamento", "result": {"status": "PAGO"}}]
agent._collect_tool_context = MethodType(collect, agent)
agent.build_direct_mcp_answer = MethodType(
lambda self, state, results, agent_label: "[FinanceiroAgent] Pagamento confirmado.", agent
)
result = await agent.run(base_state())
assert result["answer"] == "[FinanceiroAgent] Pagamento confirmado."
assert result["rag"]["reason"] == "direct_mcp_answer"
assert llm.calls == 0
@pytest.mark.asyncio
@pytest.mark.parametrize(("readme_name", "marker"), EXAMPLES)
async def test_documented_financeiro_agent_emits_complete_rag_evaluation(readme_name, marker):
agent_class, _ = extract_documented_class(readme_name, marker)
observer = CaptureObserver()
agent, llm = make_agent(agent_class, observer=observer)
async def retrieve(self, state):
return "contexto financeiro", {
"provider": "test",
"status": "executed",
"attempted": True,
"enabled": True,
"document_count": 1,
"graph_neighbors": 0,
"latency_ms": 1,
"query": state["sanitized_input"],
"namespace": "financeiro_agent",
}
agent._retrieve_rag_context = MethodType(retrieve, agent)
result = await agent.run(base_state())
codes = [event[0] for event in observer.events]
assert "IC.FINANCEIRO_RAG_CONTEXT_EVALUATED" in codes
assert "IC.FINANCEIRO_RAG_CONTEXT_RETRIEVED" in codes
assert result["rag_context"] == "contexto financeiro"
assert llm.calls == 1