Projeto do Agent Contas ORACLE
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
# Route Stickiness, Handoff, Encerramento e Políticas MCP
|
||||
|
||||
## 1. Objetivo
|
||||
|
||||
Este material consolida o desenho de continuidade semântica de rota, transferência para atendimento humano, encerramento de sessão e proteção mínima de ferramentas MCP de consulta e transação no Agent Framework OCI.
|
||||
|
||||
As capacidades são complementares:
|
||||
|
||||
- **Route stickiness** decide se o turno permanece com o agente ativo ou volta ao Enterprise Router.
|
||||
- **Handoff e encerramento** tratam ações globais de sessão sem delegá-las a agentes de domínio.
|
||||
- **Políticas MCP** decidem se uma ferramenta já selecionada pode executar, com diferenciação entre `read_only` e `transactional`.
|
||||
|
||||
Todas são opcionais e preservam o comportamento anterior quando desabilitadas ou não configuradas.
|
||||
|
||||
## 2. Visão arquitetural
|
||||
|
||||
```text
|
||||
Nova mensagem
|
||||
|
|
||||
+-- sessão já encerrada? -- sim --> rejeitar reutilização da sessão
|
||||
|
|
||||
+-- route stickiness habilitada --> classificador semântico leve
|
||||
| |
|
||||
| +-- CONTINUE + agente ativo --> agente ativo
|
||||
| +-- ROUTE/baixa confiança/erro --> Enterprise Router
|
||||
| +-- HUMAN_HANDOFF --> nó global human_handoff
|
||||
| +-- END_SESSION --> nó global end_session
|
||||
|
|
||||
+-- route stickiness desabilitada --> Enterprise Router
|
||||
|
|
||||
v
|
||||
agente de domínio
|
||||
|
|
||||
v
|
||||
ferramenta MCP selecionada
|
||||
|
|
||||
v
|
||||
política read-only/transacional
|
||||
| |
|
||||
permitida bloqueada
|
||||
| |
|
||||
v v
|
||||
MCP Gateway/Server resposta segura
|
||||
```
|
||||
|
||||
O classificador de continuidade não responde ao usuário, não escolhe outro agente, não executa ferramentas e não interpreta regras de negócio. O MCP Server continua sendo a autoridade final para autenticação, autorização, idempotência, validação e atomicidade.
|
||||
|
||||
## 3. Decisões de continuidade e sessão
|
||||
|
||||
| Decisão | Condição | Destino | Executa agente/MCP? |
|
||||
|---|---|---|---|
|
||||
| `CONTINUE` | A mensagem permanece no domínio do agente ativo e supera o threshold | agente ativo | sim, conforme o fluxo do agente |
|
||||
| `ROUTE` | Mudança de assunto, dúvida, baixa confiança ou falha | Enterprise Router | somente após nova rota |
|
||||
| `HUMAN_HANDOFF` | Solicitação de atendimento humano | nó global `human_handoff` | não |
|
||||
| `END_SESSION` | Solicitação ou confirmação de encerramento | nó global `end_session` | não |
|
||||
|
||||
No primeiro turno, `CONTINUE` é normalizado para `ROUTE`, pois ainda não existe agente ativo. `HUMAN_HANDOFF` e `END_SESSION` podem ser identificados mesmo no primeiro turno.
|
||||
|
||||
### 3.1 Por que a decisão é semântica
|
||||
|
||||
Não são usadas regexes, listas de frases, pronomes ou palavras-chave específicas de idioma. O código mantém somente decisões técnicas:
|
||||
|
||||
- feature flag;
|
||||
- existência de agente ativo;
|
||||
- threshold de confiança;
|
||||
- validação do contrato de saída;
|
||||
- fallback em timeout, erro ou JSON inválido.
|
||||
|
||||
Isso evita manutenção de padrões linguísticos por domínio e mantém o comportamento multilíngue no perfil LLM.
|
||||
|
||||
## 4. Contratos globais de sessão
|
||||
|
||||
### 4.1 Handoff humano
|
||||
|
||||
O router produz:
|
||||
|
||||
```json
|
||||
{
|
||||
"route": "human_handoff",
|
||||
"intent": "human_handoff",
|
||||
"method": "continuity",
|
||||
"handoff": true,
|
||||
"metadata": {
|
||||
"session_control": "HUMAN_HANDOFF",
|
||||
"route_bypassed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
O nó global define:
|
||||
|
||||
- `session_control=HUMAN_HANDOFF`;
|
||||
- `human_handoff_requested=true`;
|
||||
- `session_ended=false`;
|
||||
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||
|
||||
O evento `session.human_handoff.requested` permite que a integração escolha fila, fornecedor e protocolo. O framework não presume uma plataforma humana específica.
|
||||
|
||||
### 4.2 Encerramento
|
||||
|
||||
O router produz:
|
||||
|
||||
```json
|
||||
{
|
||||
"route": "end_session",
|
||||
"intent": "end_session",
|
||||
"method": "continuity",
|
||||
"metadata": {
|
||||
"session_control": "END_SESSION",
|
||||
"route_bypassed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
O nó global define:
|
||||
|
||||
- `session_control=END_SESSION`;
|
||||
- `session_ended=true`;
|
||||
- `human_handoff_requested=false`;
|
||||
- `next_state=SESSION_ENDED`.
|
||||
|
||||
O evento `session.end.requested` deve ser emitido antes da persistência. Para encerramento definitivo, `session_ended=true` precisa ser persistido e novas mensagens com a mesma `session_id` devem ser bloqueadas antes de guardrails, roteamento, agentes, RAG, judges ou MCP.
|
||||
|
||||
## 5. Políticas MCP read-only e transacionais
|
||||
|
||||
### 5.1 Responsabilidade
|
||||
|
||||
Depois que a rota e o agente selecionam uma ferramenta, o `MCPToolRouter` aplica a política imediatamente antes da chamada externa:
|
||||
|
||||
- `read_only`: consulta sem alteração de estado; por padrão não exige confirmação.
|
||||
- `transactional`: operação que altera estado; pode exigir confirmação explícita e campos obrigatórios.
|
||||
|
||||
A classificação não cria outro roteador LLM e não substitui a allowlist atual de ferramentas por agente/intenção.
|
||||
|
||||
### 5.2 Configuração no backend
|
||||
|
||||
```text
|
||||
templates/agent_template_backend/config/tool_policies.yaml
|
||||
```
|
||||
|
||||
```dotenv
|
||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
defaults:
|
||||
operation_type: read_only
|
||||
require_confirmation: false
|
||||
|
||||
tool_policies:
|
||||
consultar_plano:
|
||||
operation_type: read_only
|
||||
|
||||
alterar_plano:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
requires: [new_plan_id]
|
||||
|
||||
cancelar_servico:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
```
|
||||
|
||||
Uma transação confirmada deve receber um booleano literal:
|
||||
|
||||
```json
|
||||
{
|
||||
"new_plan_id": "CONTROLE_100",
|
||||
"confirmed": true
|
||||
}
|
||||
```
|
||||
|
||||
Também é aceito `"confirmation": true`. Strings como `"true"` não confirmam a operação.
|
||||
|
||||
### 5.3 Compatibilidade
|
||||
|
||||
- Se `tool_policies.yaml` não existir, o framework preserva `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
|
||||
- Tools antigas sem política executam como antes.
|
||||
- Uma política explícita no arquivo novo prevalece para tipo e confirmação daquela tool.
|
||||
- `tools.yaml` continua sendo a fonte de endpoint, schema, habilitação e cache.
|
||||
- A política fica no backend, e não em `libs/agent_framework`, porque varia por aplicação e domínio.
|
||||
|
||||
## 6. Interação entre continuidade e transações
|
||||
|
||||
Route stickiness não autoriza transações. Mesmo quando `CONTINUE` mantém o agente ativo, toda ferramenta passa novamente pelo controle MCP.
|
||||
|
||||
Exemplo:
|
||||
|
||||
```text
|
||||
Usuário: Quero mudar para o plano Controle 100.
|
||||
Agente: Confirma a alteração para o Controle 100?
|
||||
Usuário: Sim.
|
||||
-> CONTINUE mantém product_agent
|
||||
-> agente recupera a ação pendente
|
||||
-> MCPToolRouter valida confirmation=true
|
||||
-> alterar_plano executa
|
||||
```
|
||||
|
||||
Em `HUMAN_HANDOFF` ou `END_SESSION`, nenhum agente de domínio ou MCP deve executar. Uma transação pendente deve ser invalidada ou mantida suspensa conforme política explícita da aplicação; nunca deve executar implicitamente após handoff ou encerramento.
|
||||
|
||||
## 7. Configuração da continuidade
|
||||
|
||||
```dotenv
|
||||
ENABLE_ROUTE_STICKINESS=true
|
||||
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||
```
|
||||
|
||||
```yaml
|
||||
profiles:
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
```
|
||||
|
||||
Use o menor modelo aprovado no ambiente OCI. O classificador recebe apenas agente ativo, capacidades derivadas das intents, intent/domínio anteriores, histórico recente limitado e mensagem atual. Não recebe RAG completo, resultados MCP integrais, prompt do agente ou regras de negócio.
|
||||
|
||||
## 8. Segurança e fallback
|
||||
|
||||
- Apenas decisões acima do threshold são aceitas.
|
||||
- `CONTINUE` sem agente ativo vira `ROUTE`.
|
||||
- Baixa confiança, timeout, erro ou JSON inválido voltam ao Enterprise Router.
|
||||
- Handoff e encerramento não executam tools.
|
||||
- Confirmação transacional exige booleano literal.
|
||||
- A validação conversacional não substitui controles do MCP Server.
|
||||
- Sessões encerradas devem ser bloqueadas na entrada.
|
||||
- Retries de transações devem usar idempotência no serviço de destino.
|
||||
|
||||
## 9. Telemetria
|
||||
|
||||
Evento de continuidade:
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "CONTINUE",
|
||||
"confidence": 0.97,
|
||||
"active_agent": "product_agent",
|
||||
"route_bypassed": true,
|
||||
"profile_name": "route_continuity"
|
||||
}
|
||||
```
|
||||
|
||||
Campos recomendados:
|
||||
|
||||
- `route_decision.method`;
|
||||
- `active_agent`;
|
||||
- `route_bypassed`;
|
||||
- `continuity_signal`;
|
||||
- `session_control`;
|
||||
- `human_handoff_requested`;
|
||||
- `session_ended`;
|
||||
- `tool_name`;
|
||||
- `operation_type`;
|
||||
- `policy_source`;
|
||||
- `blocked_by_policy`.
|
||||
|
||||
## 10. Testes e benchmark
|
||||
|
||||
Casos mínimos:
|
||||
|
||||
1. continuidade com bypass;
|
||||
2. mudança de domínio com retorno ao router;
|
||||
3. baixa confiança, timeout e JSON inválido;
|
||||
4. `CONTINUE` sem agente ativo;
|
||||
5. handoff e encerramento no primeiro turno e em turnos posteriores;
|
||||
6. bloqueio de mensagem após sessão encerrada;
|
||||
7. consulta sem confirmação;
|
||||
8. transação sem confirmação, com string e com booleano válido;
|
||||
9. campo obrigatório ausente;
|
||||
10. ausência de `tool_policies.yaml` usando comportamento legado.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
|
||||
```
|
||||
|
||||
Para benchmark, compare a mesma conversa com a funcionalidade habilitada e desabilitada. Registre `route_bypassed`, chamadas ao `llm.router`, latência de `llm.route_continuity`, tokens por perfil e latência total p50/p95/p99. Só atribua ganho à stickiness quando houver `route_bypassed=true` e nenhuma geração do Enterprise Router no mesmo turno.
|
||||
|
||||
## 11. Critério de adoção
|
||||
|
||||
Adote route stickiness quando houver conversas multi-turno com retornos frequentes ao mesmo agente. Cadastre políticas apenas para ferramentas que precisem de comportamento adicional, começando pelas transações que exigem confirmação. Mantenha autorização, idempotência e regras de negócio no MCP Server para evitar duas fontes de verdade.
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
# Route Stickiness, Handoff, Session End, and MCP Policies
|
||||
|
||||
## 1. Purpose
|
||||
|
||||
This document consolidates semantic route continuity, human handoff, session ending, and minimal protection for read-only and transactional MCP tools in the OCI Agent Framework.
|
||||
|
||||
The capabilities are complementary:
|
||||
|
||||
- **Route stickiness** decides whether a turn stays with the active agent or returns to the Enterprise Router.
|
||||
- **Handoff and session end** handle global session actions outside domain agents.
|
||||
- **MCP policies** decide whether an already selected tool may execute, distinguishing `read_only` from `transactional` operations.
|
||||
|
||||
All capabilities are optional and preserve previous behavior when disabled or not configured.
|
||||
|
||||
## 2. Architecture overview
|
||||
|
||||
```text
|
||||
Incoming message
|
||||
|
|
||||
+-- session already ended? -- yes --> reject session reuse
|
||||
|
|
||||
+-- route stickiness enabled --> lightweight semantic classifier
|
||||
| |
|
||||
| +-- CONTINUE + active agent --> active agent
|
||||
| +-- ROUTE/low confidence/error --> Enterprise Router
|
||||
| +-- HUMAN_HANDOFF --> global human_handoff node
|
||||
| +-- END_SESSION --> global end_session node
|
||||
|
|
||||
+-- route stickiness disabled --> Enterprise Router
|
||||
|
|
||||
v
|
||||
domain agent
|
||||
|
|
||||
v
|
||||
selected MCP tool
|
||||
|
|
||||
v
|
||||
read-only/transactional policy
|
||||
| |
|
||||
allowed blocked
|
||||
| |
|
||||
v v
|
||||
MCP Gateway/Server safe response
|
||||
```
|
||||
|
||||
The continuity classifier does not answer users, choose another agent, execute tools, or interpret business rules. The MCP Server remains the final authority for authentication, authorization, idempotency, validation, and atomicity.
|
||||
|
||||
## 3. Continuity and session decisions
|
||||
|
||||
| Decision | Condition | Destination | Runs agent/MCP? |
|
||||
|---|---|---|---|
|
||||
| `CONTINUE` | The message remains in the active agent's domain and exceeds the threshold | active agent | yes, according to the agent flow |
|
||||
| `ROUTE` | Topic change, uncertainty, low confidence, or failure | Enterprise Router | only after routing |
|
||||
| `HUMAN_HANDOFF` | Human assistance requested | global `human_handoff` node | no |
|
||||
| `END_SESSION` | Session ending requested or confirmed | global `end_session` node | no |
|
||||
|
||||
On the first turn, `CONTINUE` is normalized to `ROUTE` because no active agent exists. `HUMAN_HANDOFF` and `END_SESSION` may be detected on the first turn.
|
||||
|
||||
### 3.1 Why the decision is semantic
|
||||
|
||||
The implementation uses no regexes, phrase lists, pronoun lists, or language-specific keywords. Code retains only technical decisions:
|
||||
|
||||
- feature flag;
|
||||
- presence of an active agent;
|
||||
- confidence threshold;
|
||||
- output-contract validation;
|
||||
- fallback on timeout, error, or invalid JSON.
|
||||
|
||||
This avoids domain-specific language-pattern maintenance and keeps multilingual behavior in the LLM profile.
|
||||
|
||||
## 4. Global session contracts
|
||||
|
||||
### 4.1 Human handoff
|
||||
|
||||
The router returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"route": "human_handoff",
|
||||
"intent": "human_handoff",
|
||||
"method": "continuity",
|
||||
"handoff": true,
|
||||
"metadata": {
|
||||
"session_control": "HUMAN_HANDOFF",
|
||||
"route_bypassed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The global node sets:
|
||||
|
||||
- `session_control=HUMAN_HANDOFF`;
|
||||
- `human_handoff_requested=true`;
|
||||
- `session_ended=false`;
|
||||
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||
|
||||
The `session.human_handoff.requested` event lets the integration select the queue, provider, and protocol. The framework assumes no specific human-service platform.
|
||||
|
||||
### 4.2 Session end
|
||||
|
||||
The router returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"route": "end_session",
|
||||
"intent": "end_session",
|
||||
"method": "continuity",
|
||||
"metadata": {
|
||||
"session_control": "END_SESSION",
|
||||
"route_bypassed": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The global node sets:
|
||||
|
||||
- `session_control=END_SESSION`;
|
||||
- `session_ended=true`;
|
||||
- `human_handoff_requested=false`;
|
||||
- `next_state=SESSION_ENDED`.
|
||||
|
||||
The `session.end.requested` event should be emitted before persistence. For definitive closure, `session_ended=true` must be persisted, and new messages using the same `session_id` must be blocked before guardrails, routing, agents, RAG, judges, or MCP.
|
||||
|
||||
## 5. Read-only and transactional MCP policies
|
||||
|
||||
### 5.1 Responsibility
|
||||
|
||||
After routing and agent selection identify a tool, `MCPToolRouter` applies policy immediately before the external call:
|
||||
|
||||
- `read_only`: retrieves data without changing state and does not require confirmation by default.
|
||||
- `transactional`: changes state and may require explicit confirmation and mandatory fields.
|
||||
|
||||
This classification adds no LLM router and does not replace the existing per-agent/per-intent tool allowlist.
|
||||
|
||||
### 5.2 Backend configuration
|
||||
|
||||
```text
|
||||
templates/agent_template_backend/config/tool_policies.yaml
|
||||
```
|
||||
|
||||
```dotenv
|
||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||
```
|
||||
|
||||
```yaml
|
||||
version: 1
|
||||
|
||||
defaults:
|
||||
operation_type: read_only
|
||||
require_confirmation: false
|
||||
|
||||
tool_policies:
|
||||
consultar_plano:
|
||||
operation_type: read_only
|
||||
|
||||
alterar_plano:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
requires: [new_plan_id]
|
||||
|
||||
cancelar_servico:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
```
|
||||
|
||||
A confirmed transaction must receive a literal boolean:
|
||||
|
||||
```json
|
||||
{
|
||||
"new_plan_id": "CONTROLE_100",
|
||||
"confirmed": true
|
||||
}
|
||||
```
|
||||
|
||||
`"confirmation": true` is also accepted. Strings such as `"true"` do not confirm an operation.
|
||||
|
||||
### 5.3 Compatibility
|
||||
|
||||
- If `tool_policies.yaml` is absent, the framework preserves `tool_type`, `requires`, `confirmation_required`, and `execution_policy` from `tools.yaml`.
|
||||
- Legacy tools without policy execute as before.
|
||||
- An explicit entry in the new file takes precedence for that tool's type and confirmation behavior.
|
||||
- `tools.yaml` remains the source for endpoint, schema, enablement, and cache.
|
||||
- Policy belongs in the backend rather than `libs/agent_framework` because it varies by application and domain.
|
||||
|
||||
## 6. Interaction between continuity and transactions
|
||||
|
||||
Route stickiness does not authorize transactions. Even when `CONTINUE` retains the active agent, every tool passes through MCP policy again.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
User: I want to switch to the Control 100 plan.
|
||||
Agent: Do you confirm the change to Control 100?
|
||||
User: Yes.
|
||||
-> CONTINUE retains product_agent
|
||||
-> agent retrieves the pending action
|
||||
-> MCPToolRouter validates confirmation=true
|
||||
-> alterar_plano executes
|
||||
```
|
||||
|
||||
For `HUMAN_HANDOFF` or `END_SESSION`, no domain agent or MCP tool should execute. A pending transaction must be invalidated or remain suspended according to an explicit application policy; it must never execute implicitly after handoff or session end.
|
||||
|
||||
## 7. Continuity configuration
|
||||
|
||||
```dotenv
|
||||
ENABLE_ROUTE_STICKINESS=true
|
||||
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||
HUMAN_HANDOFF_MESSAGE=I will transfer your request to a person.
|
||||
END_SESSION_MESSAGE=The session has ended. Thank you for contacting us.
|
||||
```
|
||||
|
||||
```yaml
|
||||
profiles:
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
```
|
||||
|
||||
Use the smallest approved model available in the OCI environment. The classifier receives only the active agent, capabilities derived from intents, previous intent/domain, limited recent history, and the current message. It receives no complete RAG context, full MCP results, agent prompt, or business rules.
|
||||
|
||||
## 8. Safety and fallback
|
||||
|
||||
- Only decisions above the configured threshold are accepted.
|
||||
- `CONTINUE` without an active agent becomes `ROUTE`.
|
||||
- Low confidence, timeout, error, or invalid JSON falls back to the Enterprise Router.
|
||||
- Handoff and session ending execute no tools.
|
||||
- Transaction confirmation requires a literal boolean.
|
||||
- Conversational validation does not replace MCP Server controls.
|
||||
- Ended sessions must be blocked at request entry.
|
||||
- Transaction retries require idempotency in the destination service.
|
||||
|
||||
## 9. Telemetry
|
||||
|
||||
Continuity event:
|
||||
|
||||
```json
|
||||
{
|
||||
"decision": "CONTINUE",
|
||||
"confidence": 0.97,
|
||||
"active_agent": "product_agent",
|
||||
"route_bypassed": true,
|
||||
"profile_name": "route_continuity"
|
||||
}
|
||||
```
|
||||
|
||||
Recommended fields:
|
||||
|
||||
- `route_decision.method`;
|
||||
- `active_agent`;
|
||||
- `route_bypassed`;
|
||||
- `continuity_signal`;
|
||||
- `session_control`;
|
||||
- `human_handoff_requested`;
|
||||
- `session_ended`;
|
||||
- `tool_name`;
|
||||
- `operation_type`;
|
||||
- `policy_source`;
|
||||
- `blocked_by_policy`.
|
||||
|
||||
## 10. Tests and benchmark
|
||||
|
||||
Minimum scenarios:
|
||||
|
||||
1. continuity with router bypass;
|
||||
2. domain change with router fallback;
|
||||
3. low confidence, timeout, and invalid JSON;
|
||||
4. `CONTINUE` without an active agent;
|
||||
5. handoff and session end on first and subsequent turns;
|
||||
6. rejection of messages after session end;
|
||||
7. read-only call without confirmation;
|
||||
8. transaction without confirmation, with a string, and with a valid boolean;
|
||||
9. missing mandatory field;
|
||||
10. absent `tool_policies.yaml` using legacy behavior.
|
||||
|
||||
```bash
|
||||
PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
|
||||
```
|
||||
|
||||
For benchmarking, compare the same conversation with the feature enabled and disabled. Record `route_bypassed`, calls to `llm.router`, `llm.route_continuity` latency, tokens per profile, and total p50/p95/p99 latency. Attribute gains to stickiness only when `route_bypassed=true` and no Enterprise Router generation occurs in the same turn.
|
||||
|
||||
## 11. Adoption criteria
|
||||
|
||||
Adopt route stickiness for multi-turn conversations that frequently remain with the same agent. Register policies only for tools requiring additional behavior, starting with transactions that require confirmation. Keep authorization, idempotency, and business rules in the MCP Server to avoid competing sources of truth.
|
||||
@@ -0,0 +1,207 @@
|
||||
###############################################################################
|
||||
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
||||
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
||||
###############################################################################
|
||||
|
||||
APP_NAME=ai-agent-template
|
||||
APP_ENV=local
|
||||
LOG_LEVEL=INFO
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
|
||||
###############################################################################
|
||||
# LLM - OCI Generative AI como provider principal
|
||||
###############################################################################
|
||||
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||
LLM_PROVIDER=oci_openai
|
||||
LLM_TEMPERATURE=0.2
|
||||
LLM_MAX_TOKENS=2048
|
||||
LLM_TIMEOUT_SECONDS=120
|
||||
|
||||
# OCI OpenAI-compatible endpoint
|
||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||
OCI_GENAI_PROJECT_OCID=
|
||||
|
||||
# OCI SDK / signer / profiles
|
||||
OCI_CONFIG_FILE=~/.oci/config
|
||||
OCI_PROFILE=DEFAULT
|
||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
OCI_REGION=us-chicago-1
|
||||
|
||||
###############################################################################
|
||||
# Persistência
|
||||
###############################################################################
|
||||
# Opções: memory, autonomous, mongodb
|
||||
SESSION_REPOSITORY_PROVIDER=sqlite
|
||||
MEMORY_REPOSITORY_PROVIDER=sqlite
|
||||
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||
SQLITE_DB_PATH=./data/agent_framework.db
|
||||
|
||||
# Autonomous Database
|
||||
ADB_USER=admin
|
||||
ADB_PASSWORD=fjhsdf04954hf
|
||||
ADB_DSN=oradb23aidev_high
|
||||
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||
ADB_TABLE_PREFIX=AGENTFW
|
||||
|
||||
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||
MONGODB_DATABASE=agent_platform
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
ENABLE_REDIS_CACHE=false
|
||||
|
||||
###############################################################################
|
||||
# RAG / Vector / Graph
|
||||
###############################################################################
|
||||
VECTOR_STORE_PROVIDER=sqlite
|
||||
GRAPH_STORE_PROVIDER=sqlite
|
||||
RAG_TOP_K=5
|
||||
EMBEDDING_PROVIDER=mock
|
||||
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||
|
||||
###############################################################################
|
||||
# Observabilidade
|
||||
###############################################################################
|
||||
ENABLE_LANGFUSE=true
|
||||
LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
|
||||
LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
|
||||
LANGFUSE_HOST=http://localhost:3005
|
||||
ENABLE_OTEL=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
OTEL_SERVICE_NAME=ai-agent-template
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||
|
||||
###############################################################################
|
||||
# Analytics / Observer corporativo
|
||||
###############################################################################
|
||||
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||
ENABLE_ANALYTICS=false
|
||||
# Providers aceitos: oci_streaming,pubsub,noop
|
||||
ANALYTICS_PROVIDERS=pubsub
|
||||
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||
AGENT_PUBSUB_TOPIC=
|
||||
GCP_PUBSUB_TOPIC_PATH=
|
||||
GCP_PROJECT_ID=
|
||||
GCP_PUBSUB_TOPIC=
|
||||
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||
# Credencial GCP segue padrão Google:
|
||||
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||
|
||||
###############################################################################
|
||||
# OCI Streaming
|
||||
###############################################################################
|
||||
ENABLE_OCI_STREAMING=false
|
||||
OCI_STREAM_ENDPOINT=
|
||||
OCI_STREAM_OCID=
|
||||
OCI_STREAM_PARTITION_KEY=agent-events
|
||||
|
||||
###############################################################################
|
||||
# Guardrails, Judges, Supervisor
|
||||
###############################################################################
|
||||
ENABLE_INPUT_GUARDRAILS=true
|
||||
ENABLE_OUTPUT_GUARDRAILS=true
|
||||
ENABLE_JUDGES=true
|
||||
ENABLE_SUPERVISOR=true
|
||||
ENABLE_OUTPUT_SUPERVISOR=true
|
||||
ENABLE_PARALLEL_GUARDRAILS=true
|
||||
GUARDRAILS_FAIL_FAST=true
|
||||
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||
|
||||
###############################################################################
|
||||
# Gateway de canais
|
||||
###############################################################################
|
||||
DEFAULT_CHANNEL=web
|
||||
# embedded = backend may parse simple/native channel payloads.
|
||||
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||
ENABLE_VOICE_ADAPTER=true
|
||||
ENABLE_WHATSAPP_ADAPTER=true
|
||||
ENABLE_TEXT_ADAPTER=true
|
||||
|
||||
#################################################
|
||||
# ENTERPRISE ROUTING
|
||||
#################################################
|
||||
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||
ENABLE_LLM_ROUTER=true
|
||||
|
||||
# Semantic route stickiness (optional).
|
||||
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||
# There are no regexes or deterministic language rules.
|
||||
ENABLE_ROUTE_STICKINESS=true
|
||||
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||
SESSION_ALREADY_ENDED_MESSAGE=Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.
|
||||
|
||||
###############################################################################
|
||||
# MCP / Tools
|
||||
###############################################################################
|
||||
ENABLE_MCP_TOOLS=true
|
||||
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||
|
||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||
ROUTING_MODE=router
|
||||
|
||||
# Usage/cost accounting
|
||||
USAGE_REPOSITORY_PROVIDER=sqlite
|
||||
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||
# -----------------------------------------------------------------------------
|
||||
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||
MEMORY_CONTEXT_STRATEGY=summary
|
||||
MEMORY_HISTORY_LIMIT=80
|
||||
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||
MEMORY_SUMMARY_USE_LLM=true
|
||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||
MEMORY_INJECT_SUMMARY=true
|
||||
|
||||
###############################################################################
|
||||
# MCP Gateway
|
||||
###############################################################################
|
||||
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||
MCP_GATEWAY_ENABLED=true
|
||||
MCP_GATEWAY_URL=http://localhost:8300
|
||||
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||
# MCP_GATEWAY_TOKEN=
|
||||
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||
MCP_GATEWAY_TENANT_ID=default
|
||||
|
||||
###############################################################################
|
||||
# LONG-TERM MEMORY
|
||||
###############################################################################
|
||||
ENABLE_LONG_TERM_MEMORY=true
|
||||
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY agent_framework /agent_framework
|
||||
COPY agent_template_backend /app
|
||||
RUN pip install --no-cache-dir -e /agent_framework -r requirements.txt
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,54 @@
|
||||
# Agent Template Backend Enterprise
|
||||
|
||||
Este folder é uma cópia completa do `agent_template_backend`, sem cortes de
|
||||
arquitetura. Ele mantém workflow, router, output supervisor, guardrails,
|
||||
analytics, observer, MCP, memória, checkpoints e configurações.
|
||||
|
||||
A diferença é que a lógica de negócio dos agentes de exemplo foi removida da
|
||||
execução e preservada comentada nos próprios arquivos:
|
||||
|
||||
- `app/agents/billing_agent.py`
|
||||
- `app/agents/product_agent.py`
|
||||
- `app/agents/orders_agent.py`
|
||||
- `app/agents/support_agent.py`
|
||||
|
||||
## O que o desenvolvedor deve alterar
|
||||
|
||||
1. Escolher ou criar um agente em `app/agents/`.
|
||||
2. Implementar o método `run()`.
|
||||
3. Ajustar prompts e tools, se necessário.
|
||||
4. Emitir ICs de negócio relevantes para a jornada.
|
||||
5. Manter NOC/GRL nos pontos operacionais e de guardrails.
|
||||
|
||||
## O que já está integrado
|
||||
|
||||
- `AgentObserver`
|
||||
- `observer.emit_ic()`
|
||||
- `observer.emit_noc()`
|
||||
- `observer.emit_grl()`
|
||||
- `AnalyticsPublisher`
|
||||
- OCI Streaming
|
||||
- GCP Pub/Sub
|
||||
- OutputSupervisor
|
||||
- GuardrailPipeline com suporte a execução paralela/fail-fast no framework
|
||||
- MCP Tool Router
|
||||
- LangGraph
|
||||
- Memory
|
||||
- Checkpoint
|
||||
- Langfuse / OpenTelemetry
|
||||
|
||||
## Exemplos adicionados
|
||||
|
||||
Veja `app/examples/`:
|
||||
|
||||
- `ic_examples.py`
|
||||
- `noc_examples.py`
|
||||
- `grl_examples.py`
|
||||
- `mcp_examples.py`
|
||||
- `observer_examples.py`
|
||||
|
||||
## Convenção rápida
|
||||
|
||||
- IC = evento de negócio / curadoria / informacional.
|
||||
- NOC = evento operacional / saúde técnica.
|
||||
- GRL = evento de guardrail / segurança / validação.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Agentes do Template Backend Enterprise
|
||||
|
||||
Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas
|
||||
não executam lógica de negócio pronta.
|
||||
|
||||
Cada agente mostra:
|
||||
|
||||
- como emitir IC;
|
||||
- como emitir NOC;
|
||||
- como emitir GRL;
|
||||
- como coletar MCP via `_collect_tool_context()`;
|
||||
- como recuperar RAG via `_retrieve_rag_context()`;
|
||||
- onde chamar LLM/cache.
|
||||
|
||||
A implementação original do exemplo está comentada no fim de cada arquivo.
|
||||
@@ -0,0 +1,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,7 @@
|
||||
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
|
||||
|
||||
__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."""
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Exemplos de GRL.
|
||||
|
||||
GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos
|
||||
pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão
|
||||
manual apenas para validações customizadas do agente.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||
await observer.emit_grl(
|
||||
"OBSERVE",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"rail_code": rail_code,
|
||||
"reason": reason,
|
||||
},
|
||||
component="examples.grl",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||
await observer.emit_grl(
|
||||
"004",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"rail_code": rail_code,
|
||||
"reason": reason,
|
||||
"action": "block",
|
||||
},
|
||||
component="examples.grl",
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Exemplos de IC - Item de Controle.
|
||||
|
||||
ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria,
|
||||
analytics, BigQuery ou qualquer publisher configurado no framework.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None:
|
||||
await observer.emit_ic(
|
||||
"IC.FATURA_CONSULTADA",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"invoice_id": invoice_id,
|
||||
},
|
||||
component="examples.ic",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None:
|
||||
await observer.emit_ic(
|
||||
"IC.ACAO_CONCLUIDA",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"action_name": action_name,
|
||||
"ok": ok,
|
||||
},
|
||||
component="examples.ic",
|
||||
)
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Exemplos de MCP + IC.
|
||||
|
||||
O AgentRuntimeMixin já possui _collect_mcp_context(), mas este arquivo mostra o
|
||||
padrão para chamadas explícitas ao tool_router quando necessário.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_chamada_mcp(tool_router: Any, observer: Any, state: dict[str, Any], tool_name: str, payload: dict[str, Any]) -> Any:
|
||||
session_id = state.get("conversation_key") or state.get("session_id")
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.MCP_TOOL_CALLED",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"tool_name": tool_name,
|
||||
},
|
||||
component="examples.mcp",
|
||||
)
|
||||
|
||||
result = await tool_router.call(
|
||||
tool_name,
|
||||
payload,
|
||||
business_context=(state.get("context") or {}).get("business_context") or {},
|
||||
original_context=state.get("context") or {},
|
||||
)
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.TOOL_CALLED",
|
||||
{
|
||||
"session_id": session_id,
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"tool_name": tool_name,
|
||||
"ok": getattr(result, "ok", None),
|
||||
},
|
||||
component="examples.mcp",
|
||||
)
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Exemplos de NOC.
|
||||
|
||||
NOC representa telemetria operacional. O workflow do template já emite NOC.001,
|
||||
NOC.005 e NOC.006. Estes exemplos mostram eventos adicionais que a squad pode
|
||||
emitir em pontos críticos.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def exemplo_api_invalida(observer: Any, state: dict[str, Any], api_url: str, status_code: int, latency_ms: int) -> None:
|
||||
await observer.emit_noc(
|
||||
"002",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"apiUrl": api_url,
|
||||
"statusCode": status_code,
|
||||
"latencyMs": latency_ms,
|
||||
},
|
||||
component="examples.noc",
|
||||
)
|
||||
|
||||
|
||||
async def exemplo_latencia_banco(observer: Any, state: dict[str, Any], resource_name: str, latency_ms: int) -> None:
|
||||
await observer.emit_noc(
|
||||
"003",
|
||||
{
|
||||
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||
"tenant_id": state.get("tenant_id"),
|
||||
"agent_id": state.get("agent_id"),
|
||||
"resourceName": resource_name,
|
||||
"latencyMs": latency_ms,
|
||||
},
|
||||
component="examples.noc",
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
"""Resumo prático do Observer corporativo.
|
||||
|
||||
Use este arquivo como cola rápida para IC, NOC e GRL.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
|
||||
async def emitir_eventos_basicos(observer: Any, state: dict[str, Any]) -> None:
|
||||
session_id = state.get("conversation_key") or state.get("session_id")
|
||||
|
||||
await observer.emit_ic(
|
||||
"IC.EXEMPLO_NEGOCIO",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id")},
|
||||
component="examples.observer",
|
||||
)
|
||||
|
||||
await observer.emit_noc(
|
||||
"EXEMPLO_OPERACIONAL",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id")},
|
||||
component="examples.observer",
|
||||
)
|
||||
|
||||
await observer.emit_grl(
|
||||
"OBSERVE",
|
||||
{"session_id": session_id, "agent_id": state.get("agent_id"), "rail_code": "CUSTOM"},
|
||||
component="examples.observer",
|
||||
)
|
||||
@@ -0,0 +1,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")),
|
||||
)
|
||||
@@ -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,51 @@
|
||||
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]
|
||||
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]
|
||||
@@ -0,0 +1,816 @@
|
||||
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": "persist", "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"
|
||||
|
||||
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):
|
||||
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.",
|
||||
"guardrail_decisions": [d.model_dump() for d in decisions],
|
||||
"route": "blocked",
|
||||
"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.
|
||||
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
@@ -0,0 +1,20 @@
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
profile: judge
|
||||
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
|
||||
- name: sentiment
|
||||
enabled: true
|
||||
fail_on_negative: false
|
||||
|
||||
- name: tone
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
@@ -0,0 +1,6 @@
|
||||
id: telecom_contas_prompt_policy
|
||||
version: 1
|
||||
description: Prompt base isolado do agente de telecom/contas.
|
||||
system_prefix: |
|
||||
Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte.
|
||||
Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida.
|
||||
@@ -0,0 +1,12 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
- code: PINJ
|
||||
enabled: true
|
||||
- code: DLEX_OUT
|
||||
enabled: true
|
||||
@@ -0,0 +1,55 @@
|
||||
identity:
|
||||
version: "2"
|
||||
required:
|
||||
- session_key
|
||||
keys:
|
||||
customer_key:
|
||||
description: Cliente/assinante/consumidor canônico.
|
||||
sources:
|
||||
- business_context.customer_key
|
||||
- customer_key
|
||||
- msisdn
|
||||
- customer_id
|
||||
- user_id
|
||||
- ani
|
||||
- from
|
||||
contract_key:
|
||||
description: Contrato, conta, fatura, pedido ou asset principal.
|
||||
sources:
|
||||
- business_context.contract_key
|
||||
- contract_key
|
||||
- invoice_id
|
||||
- current_invoice_number
|
||||
- order_id
|
||||
- pedido_id
|
||||
- asset_id
|
||||
interaction_key:
|
||||
description: Chave externa da interação/call/chat vinda do canal.
|
||||
sources:
|
||||
- business_context.interaction_key
|
||||
- interaction_key
|
||||
- ura_call_id
|
||||
- call_id
|
||||
- message_id
|
||||
account_key:
|
||||
description: Conta de cobrança/conta comercial.
|
||||
sources:
|
||||
- business_context.account_key
|
||||
- account_key
|
||||
- account_id
|
||||
- billing_account_id
|
||||
resource_key:
|
||||
description: Recurso/linha/produto/asset específico.
|
||||
sources:
|
||||
- business_context.resource_key
|
||||
- resource_key
|
||||
- asset_id
|
||||
- product_id
|
||||
- sku
|
||||
session_key:
|
||||
description: Sessão técnica estável já escopada por tenant e agente.
|
||||
sources:
|
||||
- business_context.session_key
|
||||
- session_key
|
||||
- conversation_key
|
||||
- session_id
|
||||
@@ -0,0 +1,18 @@
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
profile: judge
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
- name: sentiment
|
||||
enabled: true
|
||||
fail_on_negative: false
|
||||
- name: tone
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
sample_rate: 0.25
|
||||
always_run_for_transactional: true
|
||||
@@ -0,0 +1,92 @@
|
||||
mcp_parameter_mapping:
|
||||
defaults:
|
||||
use_mock: true
|
||||
tools:
|
||||
consultar_fatura:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
contract_key: invoice_id
|
||||
interaction_key: ura_call_id
|
||||
session_key: session_id
|
||||
extract:
|
||||
mes_referencia:
|
||||
from: message
|
||||
type: int
|
||||
strategy: month_name_pt
|
||||
description: 'Extrair mês citado na mensagem. janeiro=1, fevereiro=2, março=3,
|
||||
abril=4, maio=5, junho=6, julho=7, agosto=8, setembro=9, outubro=10, novembro=11,
|
||||
dezembro=12.
|
||||
|
||||
'
|
||||
consultar_pagamentos:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
interaction_key: ura_call_id
|
||||
session_key: session_id
|
||||
consultar_plano:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
resource_key: asset_id
|
||||
contract_key: asset_id
|
||||
session_key: session_id
|
||||
listar_servicos:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
session_key: session_id
|
||||
consultar_pedido:
|
||||
map:
|
||||
customer_key: customer_id
|
||||
session_key: session_id
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
consultar_entrega:
|
||||
map:
|
||||
session_key: session_id
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
solicitar_troca:
|
||||
map:
|
||||
session_key: session_id
|
||||
defaults:
|
||||
reason: Solicitação aberta pelo atendimento conversacional.
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
solicitar_devolucao:
|
||||
map:
|
||||
session_key: session_id
|
||||
defaults:
|
||||
reason: Solicitação aberta pelo atendimento conversacional.
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
@@ -0,0 +1,12 @@
|
||||
servers:
|
||||
telecom:
|
||||
transport: http
|
||||
endpoint: http://telecom-mcp:8100/mcp
|
||||
enabled: true
|
||||
description: MCP Server Telecom via docker-compose.
|
||||
|
||||
retail:
|
||||
transport: http
|
||||
endpoint: http://retail-mcp:8200/mcp
|
||||
enabled: true
|
||||
description: MCP Server Retail via docker-compose.
|
||||
@@ -0,0 +1,30 @@
|
||||
# MCP servers registry.
|
||||
# transport=http keeps the legacy framework mock contract:
|
||||
# GET <endpoint>/tools/list
|
||||
# POST <endpoint>/tools/call
|
||||
# transport=fastmcp uses official MCP Streamable HTTP, typically endpoint http://host:port/mcp
|
||||
# transport=sse uses official MCP SSE, typically endpoint http://host:port/sse
|
||||
servers:
|
||||
# telecom:
|
||||
# enabled: true
|
||||
# transport: fastmcp
|
||||
# endpoint: http://localhost:8001/mcp
|
||||
# description: Telecom FastMCP server using official MCP protocol
|
||||
#
|
||||
# retail:
|
||||
# enabled: true
|
||||
# transport: fastmcp
|
||||
# endpoint: http://localhost:8002/mcp
|
||||
# description: Retail FastMCP server using official MCP protocol
|
||||
|
||||
telecom:
|
||||
enabled: true
|
||||
transport: http
|
||||
endpoint: http://localhost:8100/mcp
|
||||
description: Telecom legacy HTTP mock MCP server
|
||||
|
||||
retail:
|
||||
enabled: true
|
||||
transport: http
|
||||
endpoint: http://localhost:8200/mcp
|
||||
description: Retail legacy HTTP mock MCP server
|
||||
@@ -0,0 +1,19 @@
|
||||
tone:
|
||||
style: "claro, objetivo, empático"
|
||||
forbidden_phrases:
|
||||
- "procure atendimento humano"
|
||||
vocabulary:
|
||||
preferred:
|
||||
fatura: "fatura"
|
||||
contestacao: "contestação"
|
||||
intents:
|
||||
billing_agent:
|
||||
- fatura
|
||||
- boleto
|
||||
- cobrança
|
||||
- segunda via
|
||||
product_agent:
|
||||
- plano
|
||||
- produto
|
||||
- oferta
|
||||
- serviço
|
||||
@@ -0,0 +1,128 @@
|
||||
# Roteamento enterprise configurável com MCP-aware intents.
|
||||
router:
|
||||
# mode também pode ser definido por variável de ambiente ROUTING_MODE.
|
||||
# Valores: router | supervisor
|
||||
mode: router
|
||||
fallback_agent: billing_agent
|
||||
confidence_threshold: 0.65
|
||||
allow_handoff: true
|
||||
|
||||
state_policies:
|
||||
- state: WAITING_BILLING_CONFIRMATION
|
||||
agent: billing_agent
|
||||
description: Mantém mensagens curtas como "sim" ou "não" no fluxo de fatura.
|
||||
- state: WAITING_PRODUCT_CONFIRMATION
|
||||
agent: product_agent
|
||||
description: Mantém confirmações no fluxo de produtos/serviços.
|
||||
- state: WAITING_ORDER_CONFIRMATION
|
||||
agent: orders_agent
|
||||
description: Mantém confirmações no fluxo de pedidos.
|
||||
- state: WAITING_SUPPORT_CONFIRMATION
|
||||
agent: support_agent
|
||||
description: Mantém confirmações no fluxo de suporte retail.
|
||||
- state: COLLECTING_BILLING_PARAMETERS
|
||||
agent: billing_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de faturamento.
|
||||
- state: COLLECTING_PRODUCT_PARAMETERS
|
||||
agent: product_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de produtos e serviços.
|
||||
- state: COLLECTING_ORDER_PARAMETERS
|
||||
agent: orders_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de pedidos.
|
||||
- state: COLLECTING_SUPPORT_PARAMETERS
|
||||
agent: support_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo transacional de suporte retail.
|
||||
|
||||
intents:
|
||||
- name: billing_invoice_explanation
|
||||
domain: telecom
|
||||
agent: billing_agent
|
||||
description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
|
||||
priority: 10
|
||||
mcp_tools:
|
||||
- consultar_fatura
|
||||
- consultar_pagamentos
|
||||
keywords:
|
||||
- fatura
|
||||
- conta
|
||||
- cobrança
|
||||
- boleto
|
||||
- vencimento
|
||||
- segunda via
|
||||
- contestar
|
||||
- valor alto
|
||||
- invoice
|
||||
examples:
|
||||
- Minha fatura veio alta.
|
||||
- Quero entender uma cobrança.
|
||||
- Preciso da segunda via da conta.
|
||||
|
||||
- name: product_services_information
|
||||
domain: telecom
|
||||
agent: product_agent
|
||||
description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
|
||||
priority: 20
|
||||
mcp_tools:
|
||||
- consultar_plano
|
||||
- listar_servicos
|
||||
keywords:
|
||||
- plano
|
||||
- serviço
|
||||
- pacote
|
||||
- internet
|
||||
- roaming
|
||||
- vas
|
||||
- benefício
|
||||
- assinatura
|
||||
examples:
|
||||
- Quais serviços estão ativos no meu plano?
|
||||
- Quero saber sobre meu pacote de internet.
|
||||
- Tenho roaming internacional?
|
||||
|
||||
- name: retail_order_tracking
|
||||
domain: retail
|
||||
agent: orders_agent
|
||||
description: Consulta de pedido, entrega, rastreamento, atraso e status de compra.
|
||||
priority: 30
|
||||
mcp_tools:
|
||||
- consultar_pedido
|
||||
- consultar_entrega
|
||||
keywords:
|
||||
- pedido
|
||||
- entrega
|
||||
- rastreio
|
||||
- rastreamento
|
||||
- encomenda
|
||||
- compra
|
||||
- atraso
|
||||
- correios
|
||||
examples:
|
||||
- Meu pedido não chegou.
|
||||
- Quero rastrear minha entrega.
|
||||
- Qual é o status da minha compra?
|
||||
|
||||
- name: retail_support_exchange_return
|
||||
domain: retail
|
||||
agent: support_agent
|
||||
description: Suporte, troca, devolução, garantia e problema com produto.
|
||||
priority: 25
|
||||
mcp_tools:
|
||||
- consultar_pedido
|
||||
- solicitar_troca
|
||||
- solicitar_devolucao
|
||||
keywords:
|
||||
- solicitar devolução
|
||||
- devolver pedido
|
||||
- solicitar troca
|
||||
- troca
|
||||
- devolução
|
||||
- devolver
|
||||
- garantia
|
||||
- defeito
|
||||
- produto quebrado
|
||||
- suporte
|
||||
- arrependimento
|
||||
examples:
|
||||
- Quero trocar um produto.
|
||||
- Meu produto veio com defeito.
|
||||
- Como faço uma devolução?
|
||||
@@ -0,0 +1,21 @@
|
||||
version: 1
|
||||
|
||||
# Arquivo opcional da aplicação. A ausência mantém o comportamento dos
|
||||
# templates anteriores e as políticas legadas declaradas em tools.yaml.
|
||||
defaults:
|
||||
operation_type: read_only
|
||||
require_confirmation: false
|
||||
|
||||
tool_policies:
|
||||
solicitar_troca:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
|
||||
solicitar_devolucao:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
|
||||
# Exemplo para uma operação real que só pode executar após confirmação:
|
||||
# cancelar_servico:
|
||||
# operation_type: transactional
|
||||
# require_confirmation: true
|
||||
@@ -0,0 +1,101 @@
|
||||
tools:
|
||||
consultar_fatura:
|
||||
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
invoice_id: string
|
||||
selection_keywords:
|
||||
- fatura
|
||||
- conta
|
||||
- boleto
|
||||
consultar_pagamentos:
|
||||
description: Consulta histórico de pagamentos do cliente.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
selection_keywords:
|
||||
- pagamento
|
||||
- pagamentos
|
||||
consultar_plano:
|
||||
description: Consulta plano ativo e atributos comerciais.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
asset_id: string
|
||||
selection_keywords:
|
||||
- plano
|
||||
listar_servicos:
|
||||
description: Lista serviços ativos e adicionais VAS.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
selection_keywords:
|
||||
- serviços
|
||||
- servicos
|
||||
- vas
|
||||
consultar_pedido:
|
||||
description: Consulta pedido de varejo por order_id/customer_id.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
customer_id: string
|
||||
selection_keywords:
|
||||
- consultar pedido
|
||||
- status do pedido
|
||||
- pedido
|
||||
consultar_entrega:
|
||||
description: Consulta entrega e rastreamento do pedido.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
selection_keywords:
|
||||
- entrega
|
||||
- rastreio
|
||||
- rastreamento
|
||||
- transportadora
|
||||
- previsão
|
||||
solicitar_troca:
|
||||
description: Simula abertura de solicitação de troca.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
tool_type: action
|
||||
requires:
|
||||
- order_id
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
selection_keywords:
|
||||
- solicitar troca
|
||||
- trocar
|
||||
- troca
|
||||
- defeito
|
||||
- quebrado
|
||||
solicitar_devolucao:
|
||||
description: Simula abertura de solicitação de devolução.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
tool_type: action
|
||||
requires:
|
||||
- order_id
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
selection_keywords:
|
||||
- solicitar devolução
|
||||
- solicitar devolucao
|
||||
- devolver pedido
|
||||
- devolver
|
||||
- devolução
|
||||
- devolucao
|
||||
- arrependimento
|
||||
@@ -0,0 +1,95 @@
|
||||
# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor
|
||||
|
||||
Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`.
|
||||
|
||||
## 1. Analytics e Pub/Sub
|
||||
|
||||
O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`:
|
||||
|
||||
```python
|
||||
from agent_framework.analytics.factory import create_analytics_publisher
|
||||
from agent_framework.observability.observer import AgentObserver
|
||||
|
||||
analytics = create_analytics_publisher(settings)
|
||||
observer = AgentObserver(analytics=analytics)
|
||||
```
|
||||
|
||||
Com isso, o mesmo backend pode publicar em:
|
||||
|
||||
- OCI Streaming
|
||||
- GCP Pub/Sub
|
||||
- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub`
|
||||
- Noop, quando analytics estiver desligado
|
||||
|
||||
## 2. Configuração mínima
|
||||
|
||||
```env
|
||||
ENABLE_ANALYTICS=true
|
||||
ANALYTICS_PROVIDERS=pubsub
|
||||
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||
```
|
||||
|
||||
Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub:
|
||||
|
||||
```env
|
||||
ENABLE_ANALYTICS=true
|
||||
ANALYTICS_PROVIDERS=oci_streaming,pubsub
|
||||
ENABLE_OCI_STREAMING=true
|
||||
OCI_STREAM_ENDPOINT=<endpoint>
|
||||
OCI_STREAM_OCID=<stream-ocid>
|
||||
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||
```
|
||||
|
||||
## 3. Observer corporativo
|
||||
|
||||
O workflow recebeu emissão automática dos principais eventos corporativos:
|
||||
|
||||
- `NOC.001`: início do workflow
|
||||
- `NOC.005`: exceção fatal no workflow
|
||||
- `NOC.006`: fim do workflow antes da resposta final
|
||||
- `IC.AGENT_COMPLETED`: evento informacional de conclusão
|
||||
- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor`
|
||||
|
||||
## 4. OutputSupervisor
|
||||
|
||||
Foi inserido um novo nó LangGraph:
|
||||
|
||||
```text
|
||||
agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist
|
||||
```
|
||||
|
||||
O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo:
|
||||
|
||||
- `allow`
|
||||
- `sanitize`
|
||||
- `retry`
|
||||
- `block`
|
||||
- `handover`
|
||||
- `observe`
|
||||
|
||||
Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`.
|
||||
|
||||
## 5. Campos adicionados ao AgentState
|
||||
|
||||
```python
|
||||
supervisor_action: str
|
||||
supervisor_guidance: str
|
||||
supervisor_attempt: int
|
||||
supervisor_handover_reason: str
|
||||
output_supervisor_results: list[dict]
|
||||
output_guardrails_already_applied: bool
|
||||
```
|
||||
|
||||
## 6. Arquivos alterados
|
||||
|
||||
- `agent_template_backend/app/main.py`
|
||||
- `agent_template_backend/app/workflows/agent_graph.py`
|
||||
- `agent_template_backend/app/state.py`
|
||||
- `agent_template_backend/.env`
|
||||
- `agent_template_backend/requirements.txt`
|
||||
- `agent_framework/src/agent_framework/config/settings.py`
|
||||
|
||||
## 7. Observação importante
|
||||
|
||||
O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Como usar IC, NOC e GRL no Template Backend
|
||||
|
||||
## IC — Item de Controle
|
||||
|
||||
Use IC para registrar eventos de negócio relevantes.
|
||||
|
||||
```python
|
||||
await observer.emit_ic(
|
||||
"IC.FATURA_CONSULTADA",
|
||||
{"session_id": session_id, "invoice_id": invoice_id},
|
||||
component="billing_agent",
|
||||
)
|
||||
```
|
||||
|
||||
## NOC — Evento operacional
|
||||
|
||||
Use NOC para saúde técnica, latência, erros e checkpoints operacionais.
|
||||
|
||||
```python
|
||||
await observer.emit_noc(
|
||||
"003",
|
||||
{"session_id": session_id, "resourceName": "ADB", "latencyMs": 120},
|
||||
component="repository",
|
||||
)
|
||||
```
|
||||
|
||||
## GRL — Evento de guardrail
|
||||
|
||||
Normalmente o framework emite GRL automaticamente. Use manualmente apenas para
|
||||
rails customizados dentro do agente.
|
||||
|
||||
```python
|
||||
await observer.emit_grl(
|
||||
"OBSERVE",
|
||||
{"session_id": session_id, "rail_code": "CUSTOM_POLICY"},
|
||||
component="custom_rail",
|
||||
)
|
||||
```
|
||||
|
||||
## Onde já existe no template
|
||||
|
||||
- `app/workflows/agent_graph.py` emite IC/NOC no ciclo do workflow.
|
||||
- `app/agents/runtime.py` emite IC para MCP/tools.
|
||||
- `app/agents/*_agent.py` contém exemplos dentro do método `run()`.
|
||||
- `app/examples/` contém exemplos isolados.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Backends atualizados para ConversationSummaryMemory
|
||||
|
||||
Esta versão dos backends foi compatibilizada com a versão do framework que adiciona `ConversationSummaryMemory`.
|
||||
|
||||
## O que mudou
|
||||
|
||||
- `app/main.py` agora inicializa `create_conversation_summary_memory(...)` junto com `create_memory(...)`.
|
||||
- `AgentWorkflow` recebe `summary_memory` e repassa para os agentes.
|
||||
- Os agentes não montam mais prompts manuais para o LLM; agora usam `build_messages()` do framework.
|
||||
- Antes da chamada ao LLM, os agentes executam `await self.prepare_memory_context(state)`.
|
||||
- Quando habilitado por `.env`, o prompt passa a receber:
|
||||
- resumo acumulado da conversa;
|
||||
- últimas mensagens completas;
|
||||
- mensagem atual;
|
||||
- BusinessContext;
|
||||
- MCP results;
|
||||
- RAG context e metadata.
|
||||
|
||||
## Configuração
|
||||
|
||||
```env
|
||||
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||
MEMORY_CONTEXT_STRATEGY=summary
|
||||
MEMORY_HISTORY_LIMIT=80
|
||||
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||
MEMORY_SUMMARY_USE_LLM=true
|
||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||
MEMORY_INJECT_SUMMARY=true
|
||||
```
|
||||
|
||||
## Backends alterados
|
||||
|
||||
- `backoffice_convertido_framework`
|
||||
- `agent_template_backend`
|
||||
- `agent_template_backend_day_zero`
|
||||
|
||||
## Observação importante
|
||||
|
||||
Estes backends esperam que o pacote `agent_framework` instalado/conectado seja a versão com os módulos:
|
||||
|
||||
- `agent_framework.memory.summary_memory`
|
||||
- `agent_framework.memory.summary_store`
|
||||
- `AgentRuntimeMixin.prepare_memory_context()`
|
||||
- `AgentRuntimeMixin.build_messages()` com injeção de memória
|
||||
|
||||
Use junto com o ZIP `agent_framework_conversation_summary_memory.zip`.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Exemplos implementados no template
|
||||
|
||||
Este projeto entrega as capacidades transversais habilitadas como referência:
|
||||
|
||||
- route stickiness semântica com o perfil `route_continuity`;
|
||||
- decisões `CONTINUE`, `ROUTE`, `HUMAN_HANDOFF` e `END_SESSION`;
|
||||
- nós globais `human_handoff` e `end_session`;
|
||||
- persistência de `active_agent`, `route_bypassed`, `continuity_signal` e controle de sessão;
|
||||
- rejeição de novas mensagens depois de `session_ended=true`;
|
||||
- políticas MCP `read_only` e `transactional` no backend;
|
||||
- exemplo `solicitar_devolucao` com `require_confirmation: true`.
|
||||
|
||||
Para confirmar a transação, envie `confirmed: true` ou `confirmation: true` como booleano. Handoff e encerramento não chamam agentes de domínio nem MCP.
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# FRAMEWORK_CHANNEL_INPUT_MODE
|
||||
|
||||
This backend setting controls what kind of channel input the Agent Framework backend accepts.
|
||||
|
||||
It replaces the ambiguous use of `CHANNEL_GATEWAY_MODE` inside the backend.
|
||||
|
||||
## Values
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||
```
|
||||
|
||||
The backend may use internal channel adapters to interpret simple/native channel payloads. This is useful for demos, labs, local frontend, curl tests, and simple environments.
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
```
|
||||
|
||||
The backend accepts only a normalized `GatewayRequest` produced by an external Channel Gateway. It does not parse native WhatsApp, Voice, Teams, or other channel payloads.
|
||||
|
||||
## Recommended enterprise setup
|
||||
|
||||
In the external channel gateway service:
|
||||
|
||||
```env
|
||||
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||
```
|
||||
|
||||
In this backend:
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
```
|
||||
|
||||
Flow:
|
||||
|
||||
```text
|
||||
External channel / browser / customer adapter
|
||||
↓
|
||||
channel_gateway:7000
|
||||
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||
↓ GatewayRequest
|
||||
agent_template_backend:8000
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
↓
|
||||
LangGraph / Agents / MCP / Guardrails
|
||||
```
|
||||
|
||||
## Valid direct request to backend in external mode
|
||||
|
||||
```bash
|
||||
curl -s -X POST "http://localhost:8000/gateway/message" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel": "web",
|
||||
"tenant_id": "default",
|
||||
"agent_id": "telecom_contas",
|
||||
"payload": {
|
||||
"message": "Quero consultar minha fatura",
|
||||
"session_id": "backend-external-ok-001"
|
||||
}
|
||||
}' | jq
|
||||
```
|
||||
|
||||
## Invalid direct request to backend in external mode
|
||||
|
||||
```bash
|
||||
curl -i -s -X POST "http://localhost:8000/gateway/message" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "Quero consultar minha fatura",
|
||||
"session_id": "raw-payload-error-001"
|
||||
}'
|
||||
```
|
||||
|
||||
Expected result: HTTP 422.
|
||||
|
||||
## Legacy compatibility
|
||||
|
||||
`CHANNEL_GATEWAY_MODE` is still present as a legacy alias for older environments, but new deployments should use:
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded|external
|
||||
```
|
||||
@@ -0,0 +1,127 @@
|
||||
# Guardrails paralelos fail-fast e Observer IC
|
||||
|
||||
## O que foi implementado
|
||||
|
||||
### 1. ParallelRailExecutor
|
||||
|
||||
Arquivo principal:
|
||||
|
||||
```text
|
||||
agent_framework/src/agent_framework/guardrails/parallel_executor.py
|
||||
```
|
||||
|
||||
Também foi criado um alias de compatibilidade:
|
||||
|
||||
```text
|
||||
agent_framework/src/agent_framework/guardrails/executor.py
|
||||
```
|
||||
|
||||
Esse alias evita erro quando algum código antigo importar:
|
||||
|
||||
```python
|
||||
from agent_framework.guardrails.executor import ParallelRailExecutor
|
||||
```
|
||||
|
||||
### 2. Execução paralela no GuardrailPipeline
|
||||
|
||||
Arquivo alterado:
|
||||
|
||||
```text
|
||||
agent_framework/src/agent_framework/guardrails/pipeline.py
|
||||
```
|
||||
|
||||
O pipeline continua retornando o contrato antigo:
|
||||
|
||||
```python
|
||||
(texto_final, list[RailDecision])
|
||||
```
|
||||
|
||||
mas internamente pode executar rails em paralelo com fail-fast.
|
||||
|
||||
### 3. Execução paralela no OutputSupervisor
|
||||
|
||||
Arquivo alterado:
|
||||
|
||||
```text
|
||||
agent_framework/src/agent_framework/guardrails/output_supervisor.py
|
||||
```
|
||||
|
||||
O `OutputSupervisor` agora usa `ParallelRailExecutor` quando habilitado.
|
||||
|
||||
### 4. Configuração
|
||||
|
||||
Novas configurações:
|
||||
|
||||
```env
|
||||
ENABLE_PARALLEL_GUARDRAILS=true
|
||||
GUARDRAILS_FAIL_FAST=true
|
||||
```
|
||||
|
||||
Também foram adicionadas em:
|
||||
|
||||
```text
|
||||
agent_framework/src/agent_framework/config/settings.py
|
||||
.env
|
||||
.env.example
|
||||
agent_template_backend/.env
|
||||
agent_template_backend_day_zero/.env
|
||||
```
|
||||
|
||||
### 5. Observer IC
|
||||
|
||||
O `AgentObserver` já tinha `emit_ic()`.
|
||||
|
||||
Foi complementada a API global compatível com FIRST/TIM:
|
||||
|
||||
```python
|
||||
from agent_framework.observer import ic, aic, noc, anoc, grl, agrl
|
||||
```
|
||||
|
||||
Exemplos:
|
||||
|
||||
```python
|
||||
ic("AGENT_COMPLETED", data={"session_id": "..."})
|
||||
await aic("MCP_TOOL_CALLED", data={"tool_name": "consultar_fatura"})
|
||||
```
|
||||
|
||||
### 6. ICs automáticos no template backend
|
||||
|
||||
O backend emite agora:
|
||||
|
||||
```text
|
||||
IC.AGENT_STARTED
|
||||
IC.ROUTE_SELECTED
|
||||
IC.MCP_TOOL_CALLED
|
||||
IC.TOOL_CALLED
|
||||
IC.AGENT_COMPLETED
|
||||
```
|
||||
|
||||
Além dos eventos já existentes:
|
||||
|
||||
```text
|
||||
NOC.001
|
||||
NOC.005
|
||||
NOC.006
|
||||
GRL.001 ... GRL.009
|
||||
```
|
||||
|
||||
## Validações executadas
|
||||
|
||||
Foram executadas validações locais com `PYTHONPATH=agent_framework/src`:
|
||||
|
||||
```bash
|
||||
python3 -m compileall -q agent_framework/src/agent_framework agent_template_backend/app agent_template_backend_day_zero/app
|
||||
```
|
||||
|
||||
Smoke tests executados:
|
||||
|
||||
```text
|
||||
1. Import de ParallelRailExecutor via agent_framework.guardrails
|
||||
2. Import de ParallelRailExecutor via agent_framework.guardrails.executor
|
||||
3. Execução fail-fast: FastBlock cancela SlowAllow
|
||||
4. GuardrailPipeline paralelo retorna RailDecision legado
|
||||
5. OutputSupervisor paralelo retorna RailAction.BLOCK
|
||||
6. API global observer.ic/noc/grl/aic/anoc/agrl
|
||||
```
|
||||
|
||||
Observação: o import completo do `agent_template_backend.app.workflows.agent_graph` depende de `langgraph`, que não está instalado no sandbox de validação. O arquivo foi validado por `compileall`, e a dependência já consta em `agent_template_backend/requirements.txt`.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Implementação IC/NOC/GRL preservando lógica existente
|
||||
|
||||
Esta versão mantém a lógica original dos agentes do `agent_template_backend` e adiciona observabilidade corporativa.
|
||||
|
||||
## IC adicionados nos agentes
|
||||
|
||||
Cada agente agora emite eventos de negócio sem alterar a resposta final:
|
||||
|
||||
- `IC.BILLING_AGENT_STARTED` / `IC.BILLING_AGENT_COMPLETED`
|
||||
- `IC.ORDERS_AGENT_STARTED` / `IC.ORDERS_AGENT_COMPLETED`
|
||||
- `IC.PRODUCT_AGENT_STARTED` / `IC.PRODUCT_AGENT_COMPLETED`
|
||||
- `IC.SUPPORT_AGENT_STARTED` / `IC.SUPPORT_AGENT_COMPLETED`
|
||||
- `IC.<AGENT>_MCP_CONTEXT_COLLECTED` quando houver dados MCP
|
||||
- `IC.<AGENT>_RAG_CONTEXT_RETRIEVED` quando RAG estiver habilitado
|
||||
|
||||
O mixin `AgentRuntimeMixin` também emite:
|
||||
|
||||
- `IC.MCP_TOOL_CALLED` antes da chamada MCP
|
||||
- `IC.TOOL_CALLED` após a chamada MCP
|
||||
|
||||
## NOC
|
||||
|
||||
O workflow já emite eventos operacionais principais:
|
||||
|
||||
- `NOC.001` no início da execução
|
||||
- `NOC.005` em exceção fatal
|
||||
- `NOC.006` na persistência/finalização
|
||||
|
||||
## GRL
|
||||
|
||||
O backend agora também exemplifica emissão GRL no workflow:
|
||||
|
||||
- `GRL.001` início do pipeline de guardrails
|
||||
- `GRL.002` decisão allow
|
||||
- `GRL.004` decisão block
|
||||
- `GRL.009` decisão final agregada
|
||||
|
||||
Quando `OutputSupervisor` está habilitado, ele continua sendo o principal mecanismo corporativo de supervisão de saída.
|
||||
|
||||
## Garantia
|
||||
|
||||
A lógica original dos agentes não foi substituída por stubs. As chamadas LLM, MCP, RAG, cache e os retornos originais foram preservados.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Langfuse single trace observer fix
|
||||
|
||||
This backend now uses `TelemetryBackedAgentObserver` instead of publishing IC/NOC/GRL through `AgentObserver(analytics=...)`.
|
||||
|
||||
Why: when analytics includes the Langfuse provider, observer events such as `IC.AGENT_COMPLETED` and `NOC.006` may create a second root trace with little detail. Emitting those events through `Telemetry.event(...)` keeps them inside the active request/workflow trace.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Validação da versão com IC/NOC/GRL
|
||||
|
||||
Validações executadas nesta geração:
|
||||
|
||||
1. `python -m compileall -q agent_template_backend/app`
|
||||
- Resultado: OK.
|
||||
|
||||
2. Smoke test dos agentes com LLM fake e Observer fake:
|
||||
- `BillingAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||
- `OrdersAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||
- `ProductAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||
- `SupportAgent`: preservou resposta gerada pelo LLM e emitiu IC de início/fim.
|
||||
|
||||
3. Verificação de regressão:
|
||||
- Nenhum agente retorna `Template Enterprise ativo`.
|
||||
- A lógica LLM/MCP/RAG/cache existente foi preservada.
|
||||
|
||||
## Eventos adicionados
|
||||
|
||||
### IC
|
||||
|
||||
Nos agentes:
|
||||
|
||||
- `IC.BILLING_AGENT_STARTED`
|
||||
- `IC.BILLING_MCP_CONTEXT_COLLECTED`
|
||||
- `IC.BILLING_RAG_CONTEXT_RETRIEVED`
|
||||
- `IC.BILLING_AGENT_COMPLETED`
|
||||
- `IC.ORDERS_AGENT_STARTED`
|
||||
- `IC.ORDERS_MCP_CONTEXT_COLLECTED`
|
||||
- `IC.ORDERS_RAG_CONTEXT_RETRIEVED`
|
||||
- `IC.ORDERS_AGENT_COMPLETED`
|
||||
- `IC.PRODUCT_AGENT_STARTED`
|
||||
- `IC.PRODUCT_MCP_CONTEXT_COLLECTED`
|
||||
- `IC.PRODUCT_RAG_CONTEXT_RETRIEVED`
|
||||
- `IC.PRODUCT_AGENT_COMPLETED`
|
||||
- `IC.SUPPORT_AGENT_STARTED`
|
||||
- `IC.SUPPORT_MCP_CONTEXT_COLLECTED`
|
||||
- `IC.SUPPORT_RAG_CONTEXT_RETRIEVED`
|
||||
- `IC.SUPPORT_AGENT_COMPLETED`
|
||||
|
||||
No runtime MCP:
|
||||
|
||||
- `IC.MCP_TOOL_CALLED`
|
||||
- `IC.TOOL_CALLED`
|
||||
|
||||
### NOC
|
||||
|
||||
Já integrados no workflow:
|
||||
|
||||
- `NOC.001` início da execução
|
||||
- `NOC.005` erro fatal
|
||||
- `NOC.006` finalização/persistência
|
||||
|
||||
### GRL
|
||||
|
||||
No workflow de guardrails:
|
||||
|
||||
- `GRL.001` início da avaliação
|
||||
- `GRL.002` allow
|
||||
- `GRL.004` block
|
||||
- `GRL.009` decisão final
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
compileall app: OK
|
||||
Arquivos de exemplos IC/NOC/GRL adicionados.
|
||||
Agentes preservam implementação original comentada.
|
||||
@@ -0,0 +1,80 @@
|
||||
profiles:
|
||||
default:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
max_tokens: 2048
|
||||
supervisor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
router:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
guardrail:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 600
|
||||
grl:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
judge:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 800
|
||||
rag_rewriter:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 300
|
||||
rag_compressor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 1200
|
||||
rag_generation:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1800
|
||||
summary_memory:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1200
|
||||
noc:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
billing_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
product_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
backoffice_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
mcp_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
@@ -0,0 +1,23 @@
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.30.0
|
||||
pydantic>=2.8.0
|
||||
pydantic-settings>=2.4.0
|
||||
python-dotenv>=1.0.1
|
||||
langgraph>=0.2.60
|
||||
langchain-core>=0.3.0
|
||||
openai>=1.60.0
|
||||
oci>=2.130.0
|
||||
oracledb>=2.4.0
|
||||
pymongo>=4.8.0
|
||||
redis>=5.0.0
|
||||
PyYAML>=6.0.2
|
||||
|
||||
langfuse>=3.0.0
|
||||
httpx>=0.27.0
|
||||
opentelemetry-api>=1.27.0
|
||||
opentelemetry-sdk>=1.27.0
|
||||
opentelemetry-exporter-otlp-proto-http>=1.27.0
|
||||
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.23.0
|
||||
google-cloud-pubsub>=2.28.0
|
||||
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
|
||||
|
||||
async def main():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
settings = SimpleNamespace(
|
||||
ENABLE_LONG_TERM_MEMORY=True,
|
||||
LONG_TERM_MEMORY_PROVIDER='sqlite',
|
||||
LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db',
|
||||
LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory',
|
||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20,
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70,
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=True,
|
||||
)
|
||||
manager = create_long_term_memory_manager(settings)
|
||||
first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||
assert (await manager.persist_turn(first))['saved'] >= 3
|
||||
second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||
values = {item.key:item.value for item in await manager.load(second)}
|
||||
assert values['preferred_name'].lower() == 'cris'
|
||||
assert values['preferred_language'].lower() == 'python'
|
||||
assert values['current_project'].lower() == 'atlas'
|
||||
isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}}
|
||||
assert await manager.load(isolated) == []
|
||||
print('OK: persistência, recuperação entre sessões e isolamento validados')
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,202 @@
|
||||
###############################################################################
|
||||
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
||||
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
||||
###############################################################################
|
||||
|
||||
APP_NAME=ai-agent-template
|
||||
APP_ENV=local
|
||||
LOG_LEVEL=INFO
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
|
||||
###############################################################################
|
||||
# LLM - OCI Generative AI como provider principal
|
||||
###############################################################################
|
||||
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||
LLM_PROVIDER=oci_openai
|
||||
LLM_TEMPERATURE=0.2
|
||||
LLM_MAX_TOKENS=2048
|
||||
LLM_TIMEOUT_SECONDS=120
|
||||
|
||||
# OCI OpenAI-compatible endpoint
|
||||
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||
OCI_GENAI_PROJECT_OCID=
|
||||
|
||||
# OCI SDK / signer / profiles
|
||||
OCI_CONFIG_FILE=~/.oci/config
|
||||
OCI_PROFILE=DEFAULT
|
||||
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||
OCI_REGION=us-chicago-1
|
||||
|
||||
###############################################################################
|
||||
# Persistência
|
||||
###############################################################################
|
||||
# Opções: memory, autonomous, mongodb
|
||||
SESSION_REPOSITORY_PROVIDER=sqlite
|
||||
MEMORY_REPOSITORY_PROVIDER=sqlite
|
||||
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||
SQLITE_DB_PATH=./data/agent_framework.db
|
||||
|
||||
# Autonomous Database
|
||||
ADB_USER=admin
|
||||
ADB_PASSWORD=fjhsdf04954hf
|
||||
ADB_DSN=oradb23aidev_high
|
||||
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||
ADB_TABLE_PREFIX=AGENTFW
|
||||
|
||||
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||
MONGODB_DATABASE=agent_platform
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379/0
|
||||
ENABLE_REDIS_CACHE=false
|
||||
|
||||
###############################################################################
|
||||
# RAG / Vector / Graph
|
||||
###############################################################################
|
||||
VECTOR_STORE_PROVIDER=sqlite
|
||||
GRAPH_STORE_PROVIDER=sqlite
|
||||
RAG_TOP_K=5
|
||||
EMBEDDING_PROVIDER=mock
|
||||
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||
|
||||
###############################################################################
|
||||
# Observabilidade
|
||||
###############################################################################
|
||||
ENABLE_LANGFUSE=true
|
||||
LANGFUSE_TRACE_MODE=compact # Opcional: verbose, compact
|
||||
LANGFUSE_PUBLIC_KEY=pk-lf-2f9da109-5b0f-4c78-b61d-9598ed787eba
|
||||
LANGFUSE_SECRET_KEY=sk-lf-a4cb0cdd-f2ea-4468-9911-cebeb91ba944
|
||||
LANGFUSE_HOST=http://localhost:3005
|
||||
ENABLE_OTEL=false
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||
OTEL_SERVICE_NAME=ai-agent-template
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||
|
||||
###############################################################################
|
||||
# Analytics / Observer corporativo
|
||||
###############################################################################
|
||||
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||
ENABLE_ANALYTICS=false
|
||||
# Providers aceitos: oci_streaming,pubsub,noop
|
||||
ANALYTICS_PROVIDERS=pubsub
|
||||
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||
AGENT_PUBSUB_TOPIC=
|
||||
GCP_PUBSUB_TOPIC_PATH=
|
||||
GCP_PROJECT_ID=
|
||||
GCP_PUBSUB_TOPIC=
|
||||
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||
# Credencial GCP segue padrão Google:
|
||||
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||
|
||||
###############################################################################
|
||||
# OCI Streaming
|
||||
###############################################################################
|
||||
ENABLE_OCI_STREAMING=false
|
||||
OCI_STREAM_ENDPOINT=
|
||||
OCI_STREAM_OCID=
|
||||
OCI_STREAM_PARTITION_KEY=agent-events
|
||||
|
||||
###############################################################################
|
||||
# Guardrails, Judges, Supervisor
|
||||
###############################################################################
|
||||
ENABLE_INPUT_GUARDRAILS=true
|
||||
ENABLE_OUTPUT_GUARDRAILS=true
|
||||
ENABLE_JUDGES=true
|
||||
ENABLE_SUPERVISOR=true
|
||||
ENABLE_OUTPUT_SUPERVISOR=true
|
||||
ENABLE_PARALLEL_GUARDRAILS=true
|
||||
GUARDRAILS_FAIL_FAST=true
|
||||
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||
|
||||
###############################################################################
|
||||
# Gateway de canais
|
||||
###############################################################################
|
||||
DEFAULT_CHANNEL=web
|
||||
ENABLE_VOICE_ADAPTER=true
|
||||
ENABLE_WHATSAPP_ADAPTER=true
|
||||
ENABLE_TEXT_ADAPTER=true
|
||||
|
||||
#################################################
|
||||
# ENTERPRISE ROUTING
|
||||
#################################################
|
||||
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||
ENABLE_LLM_ROUTER=true
|
||||
|
||||
# Continuidade semântica, handoff humano e encerramento global.
|
||||
ENABLE_ROUTE_STICKINESS=true
|
||||
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||
SESSION_ALREADY_ENDED_MESSAGE=Este atendimento já foi encerrado. Inicie uma nova sessão para continuar.
|
||||
|
||||
###############################################################################
|
||||
# MCP / Tools
|
||||
###############################################################################
|
||||
ENABLE_MCP_TOOLS=true
|
||||
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||
|
||||
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||
ROUTING_MODE=router
|
||||
|
||||
# Usage/cost accounting
|
||||
USAGE_REPOSITORY_PROVIDER=sqlite
|
||||
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||
# -----------------------------------------------------------------------------
|
||||
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||
MEMORY_CONTEXT_STRATEGY=summary
|
||||
MEMORY_HISTORY_LIMIT=80
|
||||
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||
MEMORY_SUMMARY_USE_LLM=true
|
||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||
MEMORY_INJECT_SUMMARY=true
|
||||
|
||||
###############################################################################
|
||||
# MCP Gateway
|
||||
###############################################################################
|
||||
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||
MCP_GATEWAY_ENABLED=true
|
||||
MCP_GATEWAY_URL=http://localhost:8300
|
||||
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||
# MCP_GATEWAY_TOKEN=
|
||||
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||
MCP_GATEWAY_TENANT_ID=default
|
||||
|
||||
###############################################################################
|
||||
# LONG-TERM MEMORY
|
||||
###############################################################################
|
||||
ENABLE_LONG_TERM_MEMORY=true
|
||||
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||
@@ -0,0 +1,6 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY agent_framework /agent_framework
|
||||
COPY agent_template_backend_day_zero /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"]
|
||||
@@ -0,0 +1,89 @@
|
||||
# agent_template_backend_day_zero
|
||||
|
||||
Este folder é uma cópia do `agent_template_backend`, porém transformada em um template **Day Zero**.
|
||||
|
||||
A ideia é o desenvolvedor começar um agente novo sem apagar manualmente exemplos de negócio.
|
||||
|
||||
## O que foi mantido
|
||||
|
||||
Foi mantida a estrutura original do backend:
|
||||
|
||||
- `app/main.py`
|
||||
- `app/workflows/agent_graph.py`
|
||||
- `app/state.py`
|
||||
- `app/agents/runtime.py`
|
||||
- `app/agents/prompting.py`
|
||||
- configurações em `config/`
|
||||
- integração com `agent_framework`
|
||||
- Analytics / Observer
|
||||
- NOC / GRL
|
||||
- OutputSupervisor
|
||||
- MCP Router
|
||||
- RAG
|
||||
- cache
|
||||
- memória
|
||||
- checkpoints
|
||||
- Langfuse / OTEL
|
||||
|
||||
## O que foi comentado
|
||||
|
||||
As implementações de exemplo dos agentes foram comentadas nos arquivos:
|
||||
|
||||
- `app/agents/billing_agent.py`
|
||||
- `app/agents/product_agent.py`
|
||||
- `app/agents/orders_agent.py`
|
||||
- `app/agents/support_agent.py`
|
||||
|
||||
Cada arquivo contém:
|
||||
|
||||
1. um esqueleto funcional mínimo;
|
||||
2. comentários `TODO` para o desenvolvedor;
|
||||
3. a implementação original comentada no final do arquivo.
|
||||
|
||||
## Como desenvolver um novo agente
|
||||
|
||||
1. Escolha qual classe vai reutilizar inicialmente, por exemplo `BillingAgent`.
|
||||
2. Edite o método `run()`.
|
||||
3. Ajuste o prompt em `apply_agent_profile_prompt(...)`.
|
||||
4. Descomente MCP se precisar de tools:
|
||||
|
||||
```python
|
||||
# tool_context = await self._collect_tool_context(state)
|
||||
```
|
||||
|
||||
5. Descomente RAG se precisar de base de conhecimento:
|
||||
|
||||
```python
|
||||
# rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||
```
|
||||
|
||||
6. Ajuste o retorno:
|
||||
|
||||
```python
|
||||
return {
|
||||
"answer": answer,
|
||||
"next_state": "MEU_ESTADO",
|
||||
}
|
||||
```
|
||||
|
||||
## O que o desenvolvedor normalmente altera
|
||||
|
||||
- `app/agents/*.py`
|
||||
- `config/routing.yaml`
|
||||
- `config/agents.yaml`
|
||||
- `config/tools.yaml`
|
||||
- `config/mcp_servers.yaml`
|
||||
- `config/mcp_parameter_mapping.yaml`
|
||||
- `.env`
|
||||
|
||||
## O que normalmente não deve ser alterado no início
|
||||
|
||||
- `app/main.py`
|
||||
- `app/workflows/agent_graph.py`
|
||||
- `app/state.py`
|
||||
- `app/agents/runtime.py`
|
||||
|
||||
Esses arquivos são o esqueleto de execução usando o framework.
|
||||
# Política opcional de tools
|
||||
|
||||
O arquivo `config/tool_policies.yaml` classifica tools como `read_only` ou `transactional`. Para uma transação real, ative `require_confirmation: true`; chamadas sem `confirmed: true` ou `confirmation: true` serão bloqueadas antes do MCP. A ausência do arquivo preserva o comportamento de templates anteriores.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Desenvolvimento de agentes
|
||||
|
||||
Os arquivos `billing_agent.py`, `product_agent.py`, `orders_agent.py` e `support_agent.py` foram mantidos com os mesmos nomes do template completo para o workflow continuar compatível.
|
||||
|
||||
A implementação de negócio original está comentada no final de cada arquivo.
|
||||
|
||||
Para criar seu agente:
|
||||
|
||||
1. Edite o método `run()` da classe desejada.
|
||||
2. Use o bloco comentado como referência.
|
||||
3. Depois, ajuste o roteamento em `config/routing.yaml`.
|
||||
4. Se quiser renomear classes/arquivos, atualize também os imports em `app/workflows/agent_graph.py`.
|
||||
@@ -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,7 @@
|
||||
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
|
||||
|
||||
__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,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")),
|
||||
)
|
||||
@@ -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,51 @@
|
||||
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]
|
||||
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]
|
||||
@@ -0,0 +1,816 @@
|
||||
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": "persist", "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"
|
||||
|
||||
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):
|
||||
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.",
|
||||
"guardrail_decisions": [d.model_dump() for d in decisions],
|
||||
"route": "blocked",
|
||||
"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,38 @@
|
||||
# ============================================================================
|
||||
# DAY ZERO
|
||||
# Este arquivo foi copiado do agent_template_backend original.
|
||||
# Ajuste os exemplos abaixo para o domínio do seu novo agente.
|
||||
# ============================================================================
|
||||
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.
|
||||
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
@@ -0,0 +1,20 @@
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
profile: judge
|
||||
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
|
||||
- name: sentiment
|
||||
enabled: true
|
||||
fail_on_negative: false
|
||||
|
||||
- name: tone
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
@@ -0,0 +1,6 @@
|
||||
id: telecom_contas_prompt_policy
|
||||
version: 1
|
||||
description: Prompt base isolado do agente de telecom/contas.
|
||||
system_prefix: |
|
||||
Você é um agente corporativo de atendimento telecom especializado em faturas, produtos, VAS e suporte.
|
||||
Seja claro, objetivo e não prometa execução operacional sem ferramenta ou confirmação válida.
|
||||
@@ -0,0 +1,8 @@
|
||||
input:
|
||||
- code: MSK
|
||||
enabled: true
|
||||
- code: VLOOP
|
||||
enabled: true
|
||||
output:
|
||||
- code: REVPREC
|
||||
enabled: true
|
||||
@@ -0,0 +1,55 @@
|
||||
identity:
|
||||
version: "2"
|
||||
required:
|
||||
- session_key
|
||||
keys:
|
||||
customer_key:
|
||||
description: Cliente/assinante/consumidor canônico.
|
||||
sources:
|
||||
- business_context.customer_key
|
||||
- customer_key
|
||||
- msisdn
|
||||
- customer_id
|
||||
- user_id
|
||||
- ani
|
||||
- from
|
||||
contract_key:
|
||||
description: Contrato, conta, fatura, pedido ou asset principal.
|
||||
sources:
|
||||
- business_context.contract_key
|
||||
- contract_key
|
||||
- invoice_id
|
||||
- current_invoice_number
|
||||
- order_id
|
||||
- pedido_id
|
||||
- asset_id
|
||||
interaction_key:
|
||||
description: Chave externa da interação/call/chat vinda do canal.
|
||||
sources:
|
||||
- business_context.interaction_key
|
||||
- interaction_key
|
||||
- ura_call_id
|
||||
- call_id
|
||||
- message_id
|
||||
account_key:
|
||||
description: Conta de cobrança/conta comercial.
|
||||
sources:
|
||||
- business_context.account_key
|
||||
- account_key
|
||||
- account_id
|
||||
- billing_account_id
|
||||
resource_key:
|
||||
description: Recurso/linha/produto/asset específico.
|
||||
sources:
|
||||
- business_context.resource_key
|
||||
- resource_key
|
||||
- asset_id
|
||||
- product_id
|
||||
- sku
|
||||
session_key:
|
||||
description: Sessão técnica estável já escopada por tenant e agente.
|
||||
sources:
|
||||
- business_context.session_key
|
||||
- session_key
|
||||
- conversation_key
|
||||
- session_id
|
||||
@@ -0,0 +1,18 @@
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
profile: judge
|
||||
judges:
|
||||
- name: response_quality
|
||||
enabled: true
|
||||
threshold: 0.7
|
||||
- name: groundedness
|
||||
enabled: true
|
||||
threshold: 0.6
|
||||
- name: sentiment
|
||||
enabled: true
|
||||
fail_on_negative: false
|
||||
- name: tone
|
||||
enabled: true
|
||||
fail_closed: true
|
||||
sample_rate: 0.25
|
||||
always_run_for_transactional: true
|
||||
@@ -0,0 +1,92 @@
|
||||
mcp_parameter_mapping:
|
||||
defaults:
|
||||
use_mock: true
|
||||
tools:
|
||||
consultar_fatura:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
contract_key: invoice_id
|
||||
interaction_key: ura_call_id
|
||||
session_key: session_id
|
||||
extract:
|
||||
mes_referencia:
|
||||
from: message
|
||||
type: int
|
||||
strategy: month_name_pt
|
||||
description: 'Extrair mês citado na mensagem. janeiro=1, fevereiro=2, março=3,
|
||||
abril=4, maio=5, junho=6, julho=7, agosto=8, setembro=9, outubro=10, novembro=11,
|
||||
dezembro=12.
|
||||
|
||||
'
|
||||
consultar_pagamentos:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
interaction_key: ura_call_id
|
||||
session_key: session_id
|
||||
consultar_plano:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
resource_key: asset_id
|
||||
contract_key: asset_id
|
||||
session_key: session_id
|
||||
listar_servicos:
|
||||
map:
|
||||
customer_key: msisdn
|
||||
session_key: session_id
|
||||
consultar_pedido:
|
||||
map:
|
||||
customer_key: customer_id
|
||||
session_key: session_id
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
consultar_entrega:
|
||||
map:
|
||||
session_key: session_id
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
solicitar_troca:
|
||||
map:
|
||||
session_key: session_id
|
||||
defaults:
|
||||
reason: Solicitação aberta pelo atendimento conversacional.
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
solicitar_devolucao:
|
||||
map:
|
||||
session_key: session_id
|
||||
defaults:
|
||||
reason: Solicitação aberta pelo atendimento conversacional.
|
||||
extract:
|
||||
order_id:
|
||||
from: message
|
||||
type: string
|
||||
strategy: hybrid
|
||||
description: Extraia somente o identificador do pedido informado explicitamente
|
||||
pelo usuário. Retorne null quando não houver identificador de pedido na
|
||||
mensagem.
|
||||
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||
group: 1
|
||||
@@ -0,0 +1,12 @@
|
||||
servers:
|
||||
telecom:
|
||||
transport: http
|
||||
endpoint: http://telecom-mcp:8100/mcp
|
||||
enabled: true
|
||||
description: MCP Server Telecom via docker-compose.
|
||||
|
||||
retail:
|
||||
transport: http
|
||||
endpoint: http://retail-mcp:8200/mcp
|
||||
enabled: true
|
||||
description: MCP Server Retail via docker-compose.
|
||||
@@ -0,0 +1,30 @@
|
||||
# MCP servers registry.
|
||||
# transport=http keeps the legacy framework mock contract:
|
||||
# GET <endpoint>/tools/list
|
||||
# POST <endpoint>/tools/call
|
||||
# transport=fastmcp uses official MCP Streamable HTTP, typically endpoint http://host:port/mcp
|
||||
# transport=sse uses official MCP SSE, typically endpoint http://host:port/sse
|
||||
servers:
|
||||
# telecom:
|
||||
# enabled: true
|
||||
# transport: fastmcp
|
||||
# endpoint: http://localhost:8001/mcp
|
||||
# description: Telecom FastMCP server using official MCP protocol
|
||||
#
|
||||
# retail:
|
||||
# enabled: true
|
||||
# transport: fastmcp
|
||||
# endpoint: http://localhost:8002/mcp
|
||||
# description: Retail FastMCP server using official MCP protocol
|
||||
|
||||
telecom:
|
||||
enabled: true
|
||||
transport: http
|
||||
endpoint: http://localhost:8100/mcp
|
||||
description: Telecom legacy HTTP mock MCP server
|
||||
|
||||
retail:
|
||||
enabled: true
|
||||
transport: http
|
||||
endpoint: http://localhost:8200/mcp
|
||||
description: Retail legacy HTTP mock MCP server
|
||||
@@ -0,0 +1,19 @@
|
||||
tone:
|
||||
style: "claro, objetivo, empático"
|
||||
forbidden_phrases:
|
||||
- "procure atendimento humano"
|
||||
vocabulary:
|
||||
preferred:
|
||||
fatura: "fatura"
|
||||
contestacao: "contestação"
|
||||
intents:
|
||||
billing_agent:
|
||||
- fatura
|
||||
- boleto
|
||||
- cobrança
|
||||
- segunda via
|
||||
product_agent:
|
||||
- plano
|
||||
- produto
|
||||
- oferta
|
||||
- serviço
|
||||
@@ -0,0 +1,128 @@
|
||||
# Roteamento enterprise configurável com MCP-aware intents.
|
||||
router:
|
||||
# mode também pode ser definido por variável de ambiente ROUTING_MODE.
|
||||
# Valores: router | supervisor
|
||||
mode: router
|
||||
fallback_agent: billing_agent
|
||||
confidence_threshold: 0.65
|
||||
allow_handoff: true
|
||||
|
||||
state_policies:
|
||||
- state: WAITING_BILLING_CONFIRMATION
|
||||
agent: billing_agent
|
||||
description: Mantém mensagens curtas como "sim" ou "não" no fluxo de fatura.
|
||||
- state: WAITING_PRODUCT_CONFIRMATION
|
||||
agent: product_agent
|
||||
description: Mantém confirmações no fluxo de produtos/serviços.
|
||||
- state: WAITING_ORDER_CONFIRMATION
|
||||
agent: orders_agent
|
||||
description: Mantém confirmações no fluxo de pedidos.
|
||||
- state: WAITING_SUPPORT_CONFIRMATION
|
||||
agent: support_agent
|
||||
description: Mantém confirmações no fluxo de suporte retail.
|
||||
- state: COLLECTING_BILLING_PARAMETERS
|
||||
agent: billing_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de faturamento.
|
||||
- state: COLLECTING_PRODUCT_PARAMETERS
|
||||
agent: product_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de produtos e serviços.
|
||||
- state: COLLECTING_ORDER_PARAMETERS
|
||||
agent: orders_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo de pedidos.
|
||||
- state: COLLECTING_SUPPORT_PARAMETERS
|
||||
agent: support_agent
|
||||
description: Mantém a coleta de parâmetros no fluxo transacional de suporte retail.
|
||||
|
||||
intents:
|
||||
- name: billing_invoice_explanation
|
||||
domain: telecom
|
||||
agent: billing_agent
|
||||
description: Dúvidas sobre fatura, cobrança, vencimento, segunda via, contestação e valores.
|
||||
priority: 10
|
||||
mcp_tools:
|
||||
- consultar_fatura
|
||||
- consultar_pagamentos
|
||||
keywords:
|
||||
- fatura
|
||||
- conta
|
||||
- cobrança
|
||||
- boleto
|
||||
- vencimento
|
||||
- segunda via
|
||||
- contestar
|
||||
- valor alto
|
||||
- invoice
|
||||
examples:
|
||||
- Minha fatura veio alta.
|
||||
- Quero entender uma cobrança.
|
||||
- Preciso da segunda via da conta.
|
||||
|
||||
- name: product_services_information
|
||||
domain: telecom
|
||||
agent: product_agent
|
||||
description: Dúvidas sobre plano, pacote, produto, serviço, VAS, internet, roaming e benefícios.
|
||||
priority: 20
|
||||
mcp_tools:
|
||||
- consultar_plano
|
||||
- listar_servicos
|
||||
keywords:
|
||||
- plano
|
||||
- serviço
|
||||
- pacote
|
||||
- internet
|
||||
- roaming
|
||||
- vas
|
||||
- benefício
|
||||
- assinatura
|
||||
examples:
|
||||
- Quais serviços estão ativos no meu plano?
|
||||
- Quero saber sobre meu pacote de internet.
|
||||
- Tenho roaming internacional?
|
||||
|
||||
- name: retail_order_tracking
|
||||
domain: retail
|
||||
agent: orders_agent
|
||||
description: Consulta de pedido, entrega, rastreamento, atraso e status de compra.
|
||||
priority: 30
|
||||
mcp_tools:
|
||||
- consultar_pedido
|
||||
- consultar_entrega
|
||||
keywords:
|
||||
- pedido
|
||||
- entrega
|
||||
- rastreio
|
||||
- rastreamento
|
||||
- encomenda
|
||||
- compra
|
||||
- atraso
|
||||
- correios
|
||||
examples:
|
||||
- Meu pedido não chegou.
|
||||
- Quero rastrear minha entrega.
|
||||
- Qual é o status da minha compra?
|
||||
|
||||
- name: retail_support_exchange_return
|
||||
domain: retail
|
||||
agent: support_agent
|
||||
description: Suporte, troca, devolução, garantia e problema com produto.
|
||||
priority: 25
|
||||
mcp_tools:
|
||||
- consultar_pedido
|
||||
- solicitar_troca
|
||||
- solicitar_devolucao
|
||||
keywords:
|
||||
- solicitar devolução
|
||||
- devolver pedido
|
||||
- solicitar troca
|
||||
- troca
|
||||
- devolução
|
||||
- devolver
|
||||
- garantia
|
||||
- defeito
|
||||
- produto quebrado
|
||||
- suporte
|
||||
- arrependimento
|
||||
examples:
|
||||
- Quero trocar um produto.
|
||||
- Meu produto veio com defeito.
|
||||
- Como faço uma devolução?
|
||||
@@ -0,0 +1,21 @@
|
||||
version: 1
|
||||
|
||||
# Arquivo opcional da aplicação. A ausência mantém o comportamento dos
|
||||
# templates anteriores e as políticas legadas declaradas em tools.yaml.
|
||||
defaults:
|
||||
operation_type: read_only
|
||||
require_confirmation: false
|
||||
|
||||
tool_policies:
|
||||
solicitar_troca:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
|
||||
solicitar_devolucao:
|
||||
operation_type: transactional
|
||||
require_confirmation: true
|
||||
|
||||
# Exemplo para uma operação real que só pode executar após confirmação:
|
||||
# cancelar_servico:
|
||||
# operation_type: transactional
|
||||
# require_confirmation: true
|
||||
@@ -0,0 +1,101 @@
|
||||
tools:
|
||||
consultar_fatura:
|
||||
description: Consulta dados resumidos de fatura por msisdn/invoice_id.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
invoice_id: string
|
||||
selection_keywords:
|
||||
- fatura
|
||||
- conta
|
||||
- boleto
|
||||
consultar_pagamentos:
|
||||
description: Consulta histórico de pagamentos do cliente.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
selection_keywords:
|
||||
- pagamento
|
||||
- pagamentos
|
||||
consultar_plano:
|
||||
description: Consulta plano ativo e atributos comerciais.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
asset_id: string
|
||||
selection_keywords:
|
||||
- plano
|
||||
listar_servicos:
|
||||
description: Lista serviços ativos e adicionais VAS.
|
||||
mcp_server: telecom
|
||||
enabled: true
|
||||
args_schema:
|
||||
msisdn: string
|
||||
selection_keywords:
|
||||
- serviços
|
||||
- servicos
|
||||
- vas
|
||||
consultar_pedido:
|
||||
description: Consulta pedido de varejo por order_id/customer_id.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
customer_id: string
|
||||
selection_keywords:
|
||||
- consultar pedido
|
||||
- status do pedido
|
||||
- pedido
|
||||
consultar_entrega:
|
||||
description: Consulta entrega e rastreamento do pedido.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
selection_keywords:
|
||||
- entrega
|
||||
- rastreio
|
||||
- rastreamento
|
||||
- transportadora
|
||||
- previsão
|
||||
solicitar_troca:
|
||||
description: Simula abertura de solicitação de troca.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
tool_type: action
|
||||
requires:
|
||||
- order_id
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
selection_keywords:
|
||||
- solicitar troca
|
||||
- trocar
|
||||
- troca
|
||||
- defeito
|
||||
- quebrado
|
||||
solicitar_devolucao:
|
||||
description: Simula abertura de solicitação de devolução.
|
||||
mcp_server: retail
|
||||
enabled: true
|
||||
tool_type: action
|
||||
requires:
|
||||
- order_id
|
||||
- reason
|
||||
confirmation_required: true
|
||||
args_schema:
|
||||
order_id: string
|
||||
reason: string
|
||||
selection_keywords:
|
||||
- solicitar devolução
|
||||
- solicitar devolucao
|
||||
- devolver pedido
|
||||
- devolver
|
||||
- devolução
|
||||
- devolucao
|
||||
- arrependimento
|
||||
@@ -0,0 +1,95 @@
|
||||
# Atualização do Template Backend — Analytics, Observer, NOC/GRL e OutputSupervisor
|
||||
|
||||
Esta versão do `agent_template_backend` foi atualizada para consumir as novidades transportadas para o `agent_framework`.
|
||||
|
||||
## 1. Analytics e Pub/Sub
|
||||
|
||||
O backend não chama mais diretamente apenas o publisher antigo de eventos. Agora ele cria um `AnalyticsPublisher`:
|
||||
|
||||
```python
|
||||
from agent_framework.analytics.factory import create_analytics_publisher
|
||||
from agent_framework.observability.observer import AgentObserver
|
||||
|
||||
analytics = create_analytics_publisher(settings)
|
||||
observer = AgentObserver(analytics=analytics)
|
||||
```
|
||||
|
||||
Com isso, o mesmo backend pode publicar em:
|
||||
|
||||
- OCI Streaming
|
||||
- GCP Pub/Sub
|
||||
- CompositePublisher, quando `ANALYTICS_PROVIDERS=oci_streaming,pubsub`
|
||||
- Noop, quando analytics estiver desligado
|
||||
|
||||
## 2. Configuração mínima
|
||||
|
||||
```env
|
||||
ENABLE_ANALYTICS=true
|
||||
ANALYTICS_PROVIDERS=pubsub
|
||||
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||
GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||
```
|
||||
|
||||
Para publicar simultaneamente em OCI Streaming e GCP Pub/Sub:
|
||||
|
||||
```env
|
||||
ENABLE_ANALYTICS=true
|
||||
ANALYTICS_PROVIDERS=oci_streaming,pubsub
|
||||
ENABLE_OCI_STREAMING=true
|
||||
OCI_STREAM_ENDPOINT=<endpoint>
|
||||
OCI_STREAM_OCID=<stream-ocid>
|
||||
GCP_PUBSUB_TOPIC_PATH=projects/<project-id>/topics/<topic-name>
|
||||
```
|
||||
|
||||
## 3. Observer corporativo
|
||||
|
||||
O workflow recebeu emissão automática dos principais eventos corporativos:
|
||||
|
||||
- `NOC.001`: início do workflow
|
||||
- `NOC.005`: exceção fatal no workflow
|
||||
- `NOC.006`: fim do workflow antes da resposta final
|
||||
- `IC.AGENT_COMPLETED`: evento informacional de conclusão
|
||||
- `GRL.001` a `GRL.009`: emitidos pelo `OutputSupervisor`
|
||||
|
||||
## 4. OutputSupervisor
|
||||
|
||||
Foi inserido um novo nó LangGraph:
|
||||
|
||||
```text
|
||||
agent -> output_supervisor -> output_guardrails -> judge -> supervisor_review -> persist
|
||||
```
|
||||
|
||||
O `OutputSupervisor` não substitui o supervisor de roteamento. Ele valida a saída candidata do agente usando o contrato corporativo:
|
||||
|
||||
- `allow`
|
||||
- `sanitize`
|
||||
- `retry`
|
||||
- `block`
|
||||
- `handover`
|
||||
- `observe`
|
||||
|
||||
Para compatibilidade com os guardrails já existentes, o template inclui o adapter `LegacyOutputGuardrailRail`, que converte decisões antigas `allowed=True/False` para `RailAction`.
|
||||
|
||||
## 5. Campos adicionados ao AgentState
|
||||
|
||||
```python
|
||||
supervisor_action: str
|
||||
supervisor_guidance: str
|
||||
supervisor_attempt: int
|
||||
supervisor_handover_reason: str
|
||||
output_supervisor_results: list[dict]
|
||||
output_guardrails_already_applied: bool
|
||||
```
|
||||
|
||||
## 6. Arquivos alterados
|
||||
|
||||
- `agent_template_backend/app/main.py`
|
||||
- `agent_template_backend/app/workflows/agent_graph.py`
|
||||
- `agent_template_backend/app/state.py`
|
||||
- `agent_template_backend/.env`
|
||||
- `agent_template_backend/requirements.txt`
|
||||
- `agent_framework/src/agent_framework/config/settings.py`
|
||||
|
||||
## 7. Observação importante
|
||||
|
||||
O `OutputSupervisor` roda os guardrails de saída por meio do adapter legado e marca `output_guardrails_already_applied=True`. Assim o nó `output_guardrails` permanece no grafo para compatibilidade, mas evita reexecutar a mesma validação quando o supervisor já aplicou os rails.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Backends atualizados para ConversationSummaryMemory
|
||||
|
||||
Esta versão dos backends foi compatibilizada com a versão do framework que adiciona `ConversationSummaryMemory`.
|
||||
|
||||
## O que mudou
|
||||
|
||||
- `app/main.py` agora inicializa `create_conversation_summary_memory(...)` junto com `create_memory(...)`.
|
||||
- `AgentWorkflow` recebe `summary_memory` e repassa para os agentes.
|
||||
- Os agentes não montam mais prompts manuais para o LLM; agora usam `build_messages()` do framework.
|
||||
- Antes da chamada ao LLM, os agentes executam `await self.prepare_memory_context(state)`.
|
||||
- Quando habilitado por `.env`, o prompt passa a receber:
|
||||
- resumo acumulado da conversa;
|
||||
- últimas mensagens completas;
|
||||
- mensagem atual;
|
||||
- BusinessContext;
|
||||
- MCP results;
|
||||
- RAG context e metadata.
|
||||
|
||||
## Configuração
|
||||
|
||||
```env
|
||||
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||
MEMORY_CONTEXT_STRATEGY=summary
|
||||
MEMORY_HISTORY_LIMIT=80
|
||||
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||
MEMORY_SUMMARY_USE_LLM=true
|
||||
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||
MEMORY_INJECT_SUMMARY=true
|
||||
```
|
||||
|
||||
## Backends alterados
|
||||
|
||||
- `backoffice_convertido_framework`
|
||||
- `agent_template_backend`
|
||||
- `agent_template_backend_day_zero`
|
||||
|
||||
## Observação importante
|
||||
|
||||
Estes backends esperam que o pacote `agent_framework` instalado/conectado seja a versão com os módulos:
|
||||
|
||||
- `agent_framework.memory.summary_memory`
|
||||
- `agent_framework.memory.summary_store`
|
||||
- `AgentRuntimeMixin.prepare_memory_context()`
|
||||
- `AgentRuntimeMixin.build_messages()` com injeção de memória
|
||||
|
||||
Use junto com o ZIP `agent_framework_conversation_summary_memory.zip`.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Como usar o `agent_template_backend_day_zero`
|
||||
|
||||
Este template é uma cópia do backend completo, mas com a lógica dos agentes de exemplo comentada.
|
||||
|
||||
## Fluxo mantido
|
||||
|
||||
```text
|
||||
Gateway / Canal
|
||||
-> AgentWorkflow
|
||||
-> Input Guardrails
|
||||
-> Router / Supervisor Router
|
||||
-> Agente
|
||||
-> OutputSupervisor
|
||||
-> Output Guardrails
|
||||
-> Judges
|
||||
-> Persistência
|
||||
```
|
||||
|
||||
## Onde escrever código
|
||||
|
||||
O ponto principal é o método `run()` dos agentes em `app/agents/`.
|
||||
|
||||
A estrutura esperada pelo workflow é:
|
||||
|
||||
```python
|
||||
async def run(self, state):
|
||||
...
|
||||
return {
|
||||
"answer": answer,
|
||||
"next_state": "MEU_ESTADO"
|
||||
}
|
||||
```
|
||||
|
||||
## Como usar MCP
|
||||
|
||||
Dentro de `run()`:
|
||||
|
||||
```python
|
||||
tool_context = await self._collect_tool_context(state)
|
||||
```
|
||||
|
||||
## Como usar RAG
|
||||
|
||||
Dentro de `run()`:
|
||||
|
||||
```python
|
||||
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||
```
|
||||
|
||||
## Como chamar o LLM com cache/telemetria
|
||||
|
||||
```python
|
||||
answer = await self._invoke_llm_cached(state, "MeuAgente", messages)
|
||||
```
|
||||
|
||||
## Como ajustar roteamento
|
||||
|
||||
Edite `config/routing.yaml`.
|
||||
|
||||
O arquivo original foi mantido para servir de referência, mas as intents devem ser adaptadas para o domínio do novo agente.
|
||||
@@ -0,0 +1,13 @@
|
||||
# Exemplos implementados no template Day Zero
|
||||
|
||||
O Day Zero preserva seu conteúdo simplificado, mas possui o mesmo conjunto transversal do template completo:
|
||||
|
||||
- route stickiness semântica com o perfil `route_continuity`;
|
||||
- decisões `CONTINUE`, `ROUTE`, `HUMAN_HANDOFF` e `END_SESSION`;
|
||||
- nós globais `human_handoff` e `end_session`;
|
||||
- persistência de `active_agent`, `route_bypassed`, `continuity_signal` e controle de sessão;
|
||||
- rejeição de novas mensagens depois de `session_ended=true`;
|
||||
- políticas MCP `read_only` e `transactional` no backend;
|
||||
- exemplo `solicitar_devolucao` com `require_confirmation: true`.
|
||||
|
||||
Para confirmar a transação, envie `confirmed: true` ou `confirmation: true` como booleano. Substitua os agentes e ferramentas de exemplo sem remover os controles transversais.
|
||||
@@ -0,0 +1,84 @@
|
||||
# FRAMEWORK_CHANNEL_INPUT_MODE
|
||||
|
||||
This backend setting controls what kind of channel input the Agent Framework backend accepts.
|
||||
|
||||
It replaces the ambiguous use of `CHANNEL_GATEWAY_MODE` inside the backend.
|
||||
|
||||
## Values
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||
```
|
||||
|
||||
The backend may use internal channel adapters to interpret simple/native channel payloads. This is useful for demos, labs, local frontend, curl tests, and simple environments.
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
```
|
||||
|
||||
The backend accepts only a normalized `GatewayRequest` produced by an external Channel Gateway. It does not parse native WhatsApp, Voice, Teams, or other channel payloads.
|
||||
|
||||
## Recommended enterprise setup
|
||||
|
||||
In the external channel gateway service:
|
||||
|
||||
```env
|
||||
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||
```
|
||||
|
||||
In this backend:
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
```
|
||||
|
||||
Flow:
|
||||
|
||||
```text
|
||||
External channel / browser / customer adapter
|
||||
↓
|
||||
channel_gateway:7000
|
||||
CHANNEL_GATEWAY_RUNTIME_MODE=adapter
|
||||
↓ GatewayRequest
|
||||
agent_template_backend:8000
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=external
|
||||
↓
|
||||
LangGraph / Agents / MCP / Guardrails
|
||||
```
|
||||
|
||||
## Valid direct request to backend in external mode
|
||||
|
||||
```bash
|
||||
curl -s -X POST "http://localhost:8000/gateway/message" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"channel": "web",
|
||||
"tenant_id": "default",
|
||||
"agent_id": "telecom_contas",
|
||||
"payload": {
|
||||
"message": "Quero consultar minha fatura",
|
||||
"session_id": "backend-external-ok-001"
|
||||
}
|
||||
}' | jq
|
||||
```
|
||||
|
||||
## Invalid direct request to backend in external mode
|
||||
|
||||
```bash
|
||||
curl -i -s -X POST "http://localhost:8000/gateway/message" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"message": "Quero consultar minha fatura",
|
||||
"session_id": "raw-payload-error-001"
|
||||
}'
|
||||
```
|
||||
|
||||
Expected result: HTTP 422.
|
||||
|
||||
## Legacy compatibility
|
||||
|
||||
`CHANNEL_GATEWAY_MODE` is still present as a legacy alias for older environments, but new deployments should use:
|
||||
|
||||
```env
|
||||
FRAMEWORK_CHANNEL_INPUT_MODE=embedded|external
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
# Langfuse single trace observer fix
|
||||
|
||||
This backend now uses `TelemetryBackedAgentObserver` instead of publishing IC/NOC/GRL through `AgentObserver(analytics=...)`.
|
||||
|
||||
Why: when analytics includes the Langfuse provider, observer events such as `IC.AGENT_COMPLETED` and `NOC.006` may create a second root trace with little detail. Emitting those events through `Telemetry.event(...)` keeps them inside the active request/workflow trace.
|
||||
@@ -0,0 +1,80 @@
|
||||
profiles:
|
||||
default:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
max_tokens: 2048
|
||||
supervisor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
route_continuity:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
router:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 500
|
||||
guardrail:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 600
|
||||
grl:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
judge:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 800
|
||||
rag_rewriter:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 300
|
||||
rag_compressor:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 1200
|
||||
rag_generation:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1800
|
||||
summary_memory:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.1
|
||||
max_tokens: 1200
|
||||
noc:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0
|
||||
max_tokens: 700
|
||||
billing_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
product_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
backoffice_agent:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1
|
||||
temperature: 0.2
|
||||
mcp_parameter_extraction:
|
||||
provider: oci_openai
|
||||
model: openai.gpt-4.1-mini
|
||||
temperature: 0
|
||||
max_tokens: 80
|
||||
timeout_seconds: 5
|
||||
@@ -0,0 +1,23 @@
|
||||
fastapi>=0.115.0
|
||||
uvicorn[standard]>=0.30.0
|
||||
pydantic>=2.8.0
|
||||
pydantic-settings>=2.4.0
|
||||
python-dotenv>=1.0.1
|
||||
langgraph>=0.2.60
|
||||
langchain-core>=0.3.0
|
||||
openai>=1.60.0
|
||||
oci>=2.130.0
|
||||
oracledb>=2.4.0
|
||||
pymongo>=4.8.0
|
||||
redis>=5.0.0
|
||||
PyYAML>=6.0.2
|
||||
|
||||
langfuse>=3.0.0
|
||||
httpx>=0.27.0
|
||||
opentelemetry-api>=1.27.0
|
||||
opentelemetry-sdk>=1.27.0
|
||||
opentelemetry-exporter-otlp-proto-http>=1.27.0
|
||||
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.23.0
|
||||
google-cloud-pubsub>=2.28.0
|
||||
@@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
import tempfile
|
||||
from types import SimpleNamespace
|
||||
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
|
||||
|
||||
async def main():
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
settings = SimpleNamespace(
|
||||
ENABLE_LONG_TERM_MEMORY=True,
|
||||
LONG_TERM_MEMORY_PROVIDER='sqlite',
|
||||
LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db',
|
||||
LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory',
|
||||
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20,
|
||||
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70,
|
||||
LONG_TERM_MEMORY_AUTO_EXTRACT=True,
|
||||
)
|
||||
manager = create_long_term_memory_manager(settings)
|
||||
first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||
assert (await manager.persist_turn(first))['saved'] >= 3
|
||||
second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}}
|
||||
values = {item.key:item.value for item in await manager.load(second)}
|
||||
assert values['preferred_name'].lower() == 'cris'
|
||||
assert values['preferred_language'].lower() == 'python'
|
||||
assert values['current_project'].lower() == 'atlas'
|
||||
isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}}
|
||||
assert await manager.load(isolated) == []
|
||||
print('OK: persistência, recuperação entre sessões e isolamento validados')
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,15 @@
|
||||
# Template 2 — Retail/E-commerce: Pedidos + Suporte
|
||||
|
||||
Este template demonstra outro uso do mesmo framework, com dois agentes diferentes:
|
||||
|
||||
- `OrdersAgent`: status de pedido, entrega, troca, devolução e rastreamento.
|
||||
- `SupportAgent`: problemas de acesso, cadastro, pagamento, cupom e atendimento geral.
|
||||
|
||||
A ideia é mostrar que o framework não é dependente de telecom. O desenvolvedor troca apenas:
|
||||
|
||||
- intents em `routing.yaml`;
|
||||
- prompts dos agentes;
|
||||
- tools de negócio;
|
||||
- estados do workflow.
|
||||
|
||||
O core de LangGraph, guardrails, judges, supervisor, Langfuse, OCI Generative AI e sessão permanece igual.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user