mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 18:23:46 +00:00
new feature: transaction evidence
This commit is contained in:
@@ -6,3 +6,4 @@ Variantes e documentos de referência para comparar funcionalidades e impacto de
|
|||||||
- `Route_Stickness`: continuidade de rota, handoff e políticas transacionais conversacionais.
|
- `Route_Stickness`: continuidade de rota, handoff e políticas transacionais conversacionais.
|
||||||
- `Long_Term_Memory`: memória de longo prazo.
|
- `Long_Term_Memory`: memória de longo prazo.
|
||||||
- `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação.
|
- `Deterministic_Transactional_Workflow`: transações multi-etapas executadas por workflow LangGraph determinístico após clarification e confirmação.
|
||||||
|
- `Transaction_Evidence`: persistência e correlação de resultados transacionais como evidência operacional para turnos posteriores e groundedness.
|
||||||
|
|||||||
172
Tuning-Performance/Transaction_Evidence/README.md
Normal file
172
Tuning-Performance/Transaction_Evidence/README.md
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
# Transaction Evidence / Evidência Operacional de Transação
|
||||||
|
|
||||||
|
Esta variante demonstra a funcionalidade **Transaction Evidence** do `agent_framework_oci`.
|
||||||
|
Ela preserva o resultado estruturado de transações executadas no estado/checkpoint do
|
||||||
|
LangGraph e o reutiliza, quando relevante, em turnos posteriores como evidência
|
||||||
|
operacional para composição de resposta e groundedness.
|
||||||
|
|
||||||
|
> Transaction Evidence **não é Long Term Memory**. É estado operacional associado à
|
||||||
|
> sessão/workflow e aos recursos envolvidos na transação.
|
||||||
|
|
||||||
|
## Problema que resolve
|
||||||
|
|
||||||
|
Sem esta funcionalidade, uma transação pode ocorrer corretamente em um turno:
|
||||||
|
|
||||||
|
```text
|
||||||
|
quero cancelar pedido
|
||||||
|
PED-1001
|
||||||
|
sim
|
||||||
|
→ protocolo CANCEL-2026-001
|
||||||
|
```
|
||||||
|
|
||||||
|
mas, no turno seguinte:
|
||||||
|
|
||||||
|
```text
|
||||||
|
quero meu pedido
|
||||||
|
```
|
||||||
|
|
||||||
|
o runtime pode consultar apenas `consultar_pedido`. Se a resposta mencionar o
|
||||||
|
cancelamento/protocolo visto no turno anterior, o judge de groundedness pode tratar
|
||||||
|
isso como informação não evidenciada.
|
||||||
|
|
||||||
|
Com Transaction Evidence, o resultado transacional confirmado fica estruturado em
|
||||||
|
`transaction_evidence` e pode aparecer como `relevant_transaction_evidence` quando
|
||||||
|
correlacionado ao recurso atual.
|
||||||
|
|
||||||
|
## Fluxo demonstrado
|
||||||
|
|
||||||
|
```text
|
||||||
|
cancelar_pedido(PED-1001)
|
||||||
|
↓
|
||||||
|
transação executada
|
||||||
|
↓
|
||||||
|
transaction_evidence
|
||||||
|
- tool_name
|
||||||
|
- arguments
|
||||||
|
- transaction_id
|
||||||
|
- status
|
||||||
|
- result/protocolo
|
||||||
|
↓
|
||||||
|
checkpoint da sessão
|
||||||
|
↓
|
||||||
|
novo turno: "quero meu pedido"
|
||||||
|
↓
|
||||||
|
consultar_pedido(PED-1001)
|
||||||
|
+
|
||||||
|
relevant_transaction_evidence(PED-1001)
|
||||||
|
↓
|
||||||
|
resposta + groundedness com evidência operacional
|
||||||
|
```
|
||||||
|
|
||||||
|
## Regras de segurança e correlação
|
||||||
|
|
||||||
|
A implementação do framework:
|
||||||
|
|
||||||
|
- registra apenas resultados transacionais efetivamente executados;
|
||||||
|
- mantém histórico bounded das evidências mais recentes;
|
||||||
|
- correlaciona por identificadores de recurso, como `order_id`, `invoice_id`,
|
||||||
|
`asset_id`, `resource_key` e outros campos `*_id`;
|
||||||
|
- evita reutilizar evidência de um recurso diferente;
|
||||||
|
- disponibiliza somente a evidência relevante do turno em
|
||||||
|
`relevant_transaction_evidence`;
|
||||||
|
- expõe a evidência relevante no metadata da resposta para diagnóstico;
|
||||||
|
- inclui a evidência relevante no contexto usado pelos judges/groundedness.
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
Não existe uma variável obrigatória `ENABLE_TRANSACTION_EVIDENCE` nesta versão.
|
||||||
|
A funcionalidade faz parte do runtime transacional do framework e depende da
|
||||||
|
persistência de estado/checkpoint já usada pelo agente.
|
||||||
|
|
||||||
|
Os principais pontos do exemplo são:
|
||||||
|
|
||||||
|
- `app/state.py`: campos `transaction_evidence`, `last_transaction_evidence` e
|
||||||
|
`relevant_transaction_evidence`;
|
||||||
|
- `app/workflows/agent_graph.py`: injeta evidência relevante no contexto dos judges;
|
||||||
|
- `app/main.py`: publica `transaction_evidence` no metadata da resposta;
|
||||||
|
- framework: `runtime/agent_runtime.py`: grava, correlaciona e materializa a evidência.
|
||||||
|
|
||||||
|
## Executar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Transaction_Evidence/agent_template_backend
|
||||||
|
pip install -e ../../../libs/agent_framework
|
||||||
|
pip install -r requirements.txt
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Use o frontend normalmente apontando para `http://localhost:8000`.
|
||||||
|
|
||||||
|
## Cenário de teste recomendado
|
||||||
|
|
||||||
|
Execute na mesma sessão:
|
||||||
|
|
||||||
|
```text
|
||||||
|
quero meu pedido
|
||||||
|
quero cancelar pedido
|
||||||
|
PED-1001
|
||||||
|
sim
|
||||||
|
quero meu pedido
|
||||||
|
```
|
||||||
|
|
||||||
|
Na última rodada, valide no JSON de resposta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "orders_agent",
|
||||||
|
"intent": "retail_order_tracking",
|
||||||
|
"transaction_evidence": [
|
||||||
|
{
|
||||||
|
"tool_name": "cancelar_pedido",
|
||||||
|
"status": "COMPLETED"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
A estrutura exata do `result` depende da tool transacional, mas o protocolo retornado
|
||||||
|
pela transação deve estar presente na evidência relevante quando o recurso correlacionar.
|
||||||
|
|
||||||
|
## Resultado esperado
|
||||||
|
|
||||||
|
No último `quero meu pedido`:
|
||||||
|
|
||||||
|
1. a intent deve ser `retail_order_tracking`;
|
||||||
|
2. `consultar_pedido` deve ser executada normalmente;
|
||||||
|
3. a evidência da transação anterior de `PED-1001` deve ser recuperada;
|
||||||
|
4. a resposta pode mencionar o cancelamento/protocolo porque essa informação agora
|
||||||
|
está suportada por evidência operacional;
|
||||||
|
5. o judge de groundedness deve receber essa mesma evidência.
|
||||||
|
|
||||||
|
## Diferença para Long Term Memory
|
||||||
|
|
||||||
|
| Transaction Evidence | Long Term Memory |
|
||||||
|
|---|---|
|
||||||
|
| Estado operacional | Memória semântica/pessoal |
|
||||||
|
| Resultado de tools/transações | Preferências e fatos duráveis |
|
||||||
|
| Correlacionado a recursos | Correlacionado ao sujeito |
|
||||||
|
| Usado para grounding operacional | Usado para personalização/contexto |
|
||||||
|
| Curto/médio prazo de workflow | Longo prazo entre conversas |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## English summary
|
||||||
|
|
||||||
|
This variant demonstrates **Transaction Evidence**: executed transactional tool
|
||||||
|
outcomes are persisted as structured operational evidence in LangGraph state/checkpoint.
|
||||||
|
On later turns, the runtime correlates prior evidence with the current resource and
|
||||||
|
exposes it as `relevant_transaction_evidence` to both response composition and
|
||||||
|
judges/groundedness. It is operational workflow state, not Long Term Memory.
|
||||||
|
|
||||||
|
Recommended test sequence:
|
||||||
|
|
||||||
|
```text
|
||||||
|
show my order
|
||||||
|
cancel my order
|
||||||
|
PED-1001
|
||||||
|
yes
|
||||||
|
show my order
|
||||||
|
```
|
||||||
|
|
||||||
|
On the last turn, verify that the response metadata contains relevant transaction
|
||||||
|
evidence for the cancellation of `PED-1001`.
|
||||||
@@ -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_sdk
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2048
|
||||||
|
LLM_TIMEOUT_SECONDS=120
|
||||||
|
|
||||||
|
# OCI OpenAI-compatible endpoint
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_API_KEY=sk-ph3FgX6iP3fxAQCXb9IpPIDTadkeeYAWntUWhzcWysIM6zsS
|
||||||
|
OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
#OCI_GENAI_BASE_URL=https://pegruagntaiatenddev.pe.inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com
|
||||||
|
#OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
#OCI_GENAI_API_KEY=
|
||||||
|
#OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
|
||||||
|
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
||||||
|
OCI_AUTH_MODE=config_file
|
||||||
|
# OCI SDK / signer / profiles
|
||||||
|
OCI_CONFIG_FILE=~/.oci/config
|
||||||
|
OCI_PROFILE=LATINOAMERICA-Chicago
|
||||||
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaexpiw4a7dio64mkfv2t273s2hgdl6mgfvvyv7tycalnjlvpvfl3q
|
||||||
|
OCI_REGION=us-chicago-1
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Persistência
|
||||||
|
###############################################################################
|
||||||
|
# Opções: memory, autonomous, mongodb
|
||||||
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
|
||||||
|
# Autonomous Database
|
||||||
|
ADB_USER=admin
|
||||||
|
ADB_PASSWORD=Moniquinha19721972
|
||||||
|
ADB_DSN=oradb23ai_high
|
||||||
|
ADB_WALLET_LOCATION=/mnt/d/Dropbox/ORACLE/LatinoAmerica/Wallet_ORADB23ai
|
||||||
|
ADB_WALLET_PASSWORD=Moniquinha1972
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
|
||||||
|
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||||
|
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
ENABLE_REDIS_CACHE=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RAG / Vector / Graph
|
||||||
|
###############################################################################
|
||||||
|
VECTOR_STORE_PROVIDER=autonomous
|
||||||
|
GRAPH_STORE_PROVIDER=autonomous
|
||||||
|
RAG_TOP_K=5
|
||||||
|
EMBEDDING_PROVIDER=oci
|
||||||
|
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||||
|
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Observabilidade
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
# Opcional: verbose, compact
|
||||||
|
LANGFUSE_TRACE_MODE=compact
|
||||||
|
# Nome customizado do trace pai, ex.: backoffice.checklist.workflow ou backoffice.emulador.workflow
|
||||||
|
LANGFUSE_COMPACT_VISIBLE_EVENT_PREFIXES=AGA.,NOC., IC.
|
||||||
|
LANGFUSE_COMPACT_SUPPRESSED_PREFIXES=llm.chat_completion
|
||||||
|
LANGFUSE_IGNORE_HEALTHCHECKS=true
|
||||||
|
LANGFUSE_IGNORED_PATHS=/health,/ready,/metrics
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-4a1e3921-5158-4fd3-a16d-7a77549fb312
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-efc6fd59-c5ec-4858-b6ec-4aa129734915
|
||||||
|
LANGFUSE_HOST=http://localhost:3005
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
ENABLE_LANGFUSE_ANALYTICS_PUBLISHER=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Analytics / Observer corporativo
|
||||||
|
###############################################################################
|
||||||
|
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
# Providers aceitos: oci_streaming,pubsub,noop
|
||||||
|
ANALYTICS_PROVIDERS=oci_streaming
|
||||||
|
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||||
|
AGENT_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=
|
||||||
|
GCP_PROJECT_ID=
|
||||||
|
GCP_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||||
|
# Credencial GCP segue padrão Google:
|
||||||
|
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OCI Streaming
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_OCI_STREAMING=false
|
||||||
|
OCI_STREAM_ENDPOINT=
|
||||||
|
OCI_STREAM_OCID=
|
||||||
|
OCI_STREAM_PARTITION_KEY=agent-events
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Guardrails, Judges, Supervisor
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_INPUT_GUARDRAILS=true
|
||||||
|
ENABLE_OUTPUT_GUARDRAILS=true
|
||||||
|
ENABLE_JUDGES=true
|
||||||
|
ENABLE_SUPERVISOR=true
|
||||||
|
ENABLE_OUTPUT_SUPERVISOR=true
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||||
|
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||||
|
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||||
|
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Gateway de canais
|
||||||
|
###############################################################################
|
||||||
|
DEFAULT_CHANNEL=web
|
||||||
|
# embedded = backend may parse simple/native channel payloads.
|
||||||
|
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
ENABLE_VOICE_ADAPTER=true
|
||||||
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
|
ENABLE_TEXT_ADAPTER=true
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# ENTERPRISE ROUTING
|
||||||
|
#################################################
|
||||||
|
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||||
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
# Semantic route stickiness (optional).
|
||||||
|
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||||
|
# There are no regexes or deterministic language rules.
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP / Tools
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
MCP_SERVERS_CONFIG_PATH=./config/mcp_servers.yaml
|
||||||
|
TOOLS_CONFIG_PATH=./config/tools.yaml
|
||||||
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
|
ROUTING_MODE=router
|
||||||
|
|
||||||
|
# Usage/cost accounting
|
||||||
|
USAGE_REPOSITORY_PROVIDER=autonomous
|
||||||
|
IDENTITY_CONFIG_PATH=./config/identity.yaml
|
||||||
|
MCP_PARAMETER_MAPPING_PATH=./config/mcp_parameter_mapping.yaml
|
||||||
|
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# ConversationSummaryMemory / compressão de contexto conversacional
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
ENABLE_CONVERSATION_SUMMARY_MEMORY=true
|
||||||
|
MEMORY_CONTEXT_STRATEGY=summary
|
||||||
|
MEMORY_HISTORY_LIMIT=80
|
||||||
|
MEMORY_RECENT_MESSAGES_LIMIT=8
|
||||||
|
MEMORY_SUMMARY_TRIGGER_MESSAGES=20
|
||||||
|
MEMORY_MAX_SUMMARY_CHARS=6000
|
||||||
|
MEMORY_SUMMARY_USE_LLM=true
|
||||||
|
MEMORY_INJECT_RECENT_MESSAGES=true
|
||||||
|
MEMORY_INJECT_SUMMARY=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LONG-TERM MEMORY
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||||
|
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
@@ -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,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
# Compatibilidade local do template/backend.
|
||||||
|
# A implementação oficial agora fica no framework para evitar duplicação entre agentes.
|
||||||
|
from agent_framework.runtime import AgentRuntimeMixin, MessageBuilder, RuntimeContext
|
||||||
|
from app.presentation import register_tool_renderers
|
||||||
|
|
||||||
|
register_tool_renderers()
|
||||||
|
|
||||||
|
__all__ = ["AgentRuntimeMixin", "MessageBuilder", "RuntimeContext"]
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
from app.agents.prompting import apply_agent_profile_prompt
|
||||||
|
from app.agents.runtime import AgentRuntimeMixin
|
||||||
|
|
||||||
|
|
||||||
|
class SupportAgent(AgentRuntimeMixin):
|
||||||
|
name = "support_agent"
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
llm,
|
||||||
|
telemetry=None,
|
||||||
|
tool_router=None,
|
||||||
|
rag_service=None,
|
||||||
|
cache=None,
|
||||||
|
settings=None,
|
||||||
|
observer=None,
|
||||||
|
memory=None,
|
||||||
|
summary_memory=None,
|
||||||
|
):
|
||||||
|
self.llm = llm
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.tool_router = tool_router
|
||||||
|
self.rag_service = rag_service
|
||||||
|
self.cache = cache
|
||||||
|
self.settings = settings
|
||||||
|
self.observer = observer
|
||||||
|
self.memory = memory
|
||||||
|
self.summary_memory = summary_memory
|
||||||
|
|
||||||
|
async def run(self, state):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_AGENT_STARTED",
|
||||||
|
state,
|
||||||
|
{"business_component": "suporte"},
|
||||||
|
component="agent.support.start",
|
||||||
|
)
|
||||||
|
|
||||||
|
tool_context = await self._collect_tool_context(state)
|
||||||
|
if tool_context:
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_MCP_CONTEXT_COLLECTED",
|
||||||
|
state,
|
||||||
|
{"tool_result_count": len(tool_context)},
|
||||||
|
component="agent.support.mcp",
|
||||||
|
)
|
||||||
|
|
||||||
|
state["mcp_results"] = tool_context
|
||||||
|
clarification_message = self.transaction_clarification_message(state)
|
||||||
|
if clarification_message:
|
||||||
|
return {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {clarification_message}",
|
||||||
|
"next_state": state.get("next_state") or "COLLECTING_PARAMETERS",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
confirmation_message = self.transaction_confirmation_message(state)
|
||||||
|
if confirmation_message:
|
||||||
|
result = {
|
||||||
|
"answer": f"[{self.__class__.__name__}] {confirmation_message}",
|
||||||
|
"next_state": state.get("next_state"),
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
direct_answer = self.build_direct_mcp_answer(state, tool_context, agent_label="SupportAgent")
|
||||||
|
if direct_answer:
|
||||||
|
return {
|
||||||
|
"answer": direct_answer,
|
||||||
|
"next_state": state.get("next_state") or "ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": {"enabled": False, "skipped": True, "reason": "direct_mcp_answer"},
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
rag_context, rag_metadata = await self._retrieve_rag_context(state)
|
||||||
|
if rag_metadata.get("enabled"):
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_RAG_CONTEXT_RETRIEVED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"document_count": rag_metadata.get("document_count"),
|
||||||
|
"graph_neighbors": rag_metadata.get("graph_neighbors"),
|
||||||
|
"latency_ms": rag_metadata.get("latency_ms"),
|
||||||
|
},
|
||||||
|
component="agent.support.rag",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepara ConversationSummaryMemory antes de montar o prompt.
|
||||||
|
# O build_messages() do framework injeta resumo + últimas mensagens quando habilitado.
|
||||||
|
await self.prepare_memory_context(state)
|
||||||
|
|
||||||
|
messages = self.build_messages(
|
||||||
|
state,
|
||||||
|
system_prompt=apply_agent_profile_prompt(
|
||||||
|
state,
|
||||||
|
"Você é um agente de suporte de varejo para troca, devolução e garantia.",
|
||||||
|
),
|
||||||
|
mcp_results=tool_context,
|
||||||
|
rag_context=rag_context,
|
||||||
|
rag_metadata=rag_metadata,
|
||||||
|
)
|
||||||
|
|
||||||
|
answer = await self._invoke_llm_cached(state, "SupportAgent", messages)
|
||||||
|
result = {
|
||||||
|
"answer": f"[SupportAgent] {answer}",
|
||||||
|
"next_state": "SUPPORT_ACTIVE",
|
||||||
|
"mcp_results": tool_context,
|
||||||
|
"rag": rag_metadata,
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.support.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Exemplos de uso do template backend enterprise."""
|
||||||
@@ -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,647 @@
|
|||||||
|
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.channels.interruption import classify_processing_interruption, evaluate_interruption
|
||||||
|
from agent_framework.channels.transcription import fix_whole_utterance_transcription
|
||||||
|
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 sessions.upsert(session)
|
||||||
|
|
||||||
|
fixed_message_text = fix_whole_utterance_transcription(msg.text)
|
||||||
|
if fixed_message_text != msg.text:
|
||||||
|
await telemetry.event(
|
||||||
|
"channel.transcription.fixed",
|
||||||
|
{
|
||||||
|
"session_id": agent_session_id,
|
||||||
|
"original_text": msg.text,
|
||||||
|
"fixed_text": fixed_message_text,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
interruption = evaluate_interruption(
|
||||||
|
payload=payload,
|
||||||
|
message_text=fixed_message_text,
|
||||||
|
session_metadata=session.metadata,
|
||||||
|
terminal_fallback_text=getattr(settings, "POST_FINALIZE_REPLAY_MESSAGE", ""),
|
||||||
|
)
|
||||||
|
if interruption.action == "classify":
|
||||||
|
prior_history = await memory.list(agent_session_id)
|
||||||
|
prior_user_text = ""
|
||||||
|
for prior in reversed(prior_history):
|
||||||
|
role = getattr(prior, "role", None)
|
||||||
|
content = getattr(prior, "content", "")
|
||||||
|
if str(role or "") == "user" and str(content or "").strip():
|
||||||
|
prior_user_text = str(content).strip()
|
||||||
|
break
|
||||||
|
regenerate = await classify_processing_interruption(
|
||||||
|
llm,
|
||||||
|
original_agent=interruption.replay_text,
|
||||||
|
original_client=prior_user_text,
|
||||||
|
supplement_client=interruption.text,
|
||||||
|
)
|
||||||
|
await telemetry.event(
|
||||||
|
"channel.processing_interruption.classified",
|
||||||
|
{
|
||||||
|
"session_id": agent_session_id,
|
||||||
|
"regenerate": regenerate,
|
||||||
|
"profile_name": "processing_interruption_classifier",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if regenerate:
|
||||||
|
interruption.action = "process"
|
||||||
|
interruption.reason = "classifier_result_1"
|
||||||
|
else:
|
||||||
|
interruption.action = "replay"
|
||||||
|
interruption.reason = "classifier_result_0"
|
||||||
|
|
||||||
|
if interruption.action == "replay":
|
||||||
|
response = ChannelResponse(
|
||||||
|
channel=msg.channel,
|
||||||
|
session_id=agent_session_id,
|
||||||
|
text=interruption.replay_text,
|
||||||
|
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,
|
||||||
|
"replay": True,
|
||||||
|
"replay_reason": interruption.reason,
|
||||||
|
"is_interruptible": interruption.is_interruptible,
|
||||||
|
"framework_short_circuit": True,
|
||||||
|
"terminal_status": interruption.terminal_status,
|
||||||
|
"llm_called": False,
|
||||||
|
"tools_called": False,
|
||||||
|
"guardrails_called": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
rendered = await gateway.render(response)
|
||||||
|
await telemetry.event("gateway.message.replayed", {"session_id": agent_session_id, "reason": interruption.reason})
|
||||||
|
await sse_hub.emit(agent_session_id, "message.responded", rendered) if emit_sse else None
|
||||||
|
return rendered
|
||||||
|
|
||||||
|
effective_text = interruption.text
|
||||||
|
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=effective_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": effective_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"],
|
||||||
|
# Chave estável de LTM. Nunca use session_id como identidade de longo prazo.
|
||||||
|
"long_term_memory_subject_key": business_context.customer_key or session.user_id,
|
||||||
|
"customer_key": business_context.customer_key,
|
||||||
|
"user_id": session.user_id,
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"user_text": effective_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"),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal_status = str(result.get("terminal_status") or "").strip()
|
||||||
|
session_ended = bool(result.get("session_ended")) or bool(terminal_status)
|
||||||
|
session.metadata = {
|
||||||
|
**(session.metadata or {}),
|
||||||
|
"last_assistant_text": answer,
|
||||||
|
"last_assistant_is_interruptible": bool(result.get("is_interruptible", True)),
|
||||||
|
"last_route": result.get("route"),
|
||||||
|
"last_intent": result.get("intent"),
|
||||||
|
"conversation_closed": session_ended,
|
||||||
|
"terminal_status": terminal_status or ("resolvido" if session_ended else ""),
|
||||||
|
"terminal_replay_text": answer if session_ended else "",
|
||||||
|
}
|
||||||
|
await sessions.upsert(session)
|
||||||
|
|
||||||
|
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"),
|
||||||
|
"transaction_evidence": result.get("relevant_transaction_evidence", []),
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"judges": result.get("judge_results"),
|
||||||
|
"guardrails": result.get("guardrail_decisions"),
|
||||||
|
"long_term_memory": {
|
||||||
|
"subject_key": business_context.customer_key or session.user_id,
|
||||||
|
"loaded": result.get("long_term_memories", []),
|
||||||
|
"context": result.get("long_term_memory_context", ""),
|
||||||
|
"load_error": result.get("long_term_memory_load_error"),
|
||||||
|
"write_result": result.get("long_term_memory_write_result", {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
"long_term_memory": {
|
||||||
|
"enabled": getattr(settings, "ENABLE_LONG_TERM_MEMORY", False),
|
||||||
|
"provider": getattr(settings, "LONG_TERM_MEMORY_PROVIDER", None),
|
||||||
|
"sqlite_path": getattr(settings, "LONG_TERM_MEMORY_SQLITE_PATH", None),
|
||||||
|
"table": getattr(settings, "LONG_TERM_MEMORY_TABLE", None),
|
||||||
|
"auto_extract": getattr(settings, "LONG_TERM_MEMORY_AUTO_EXTRACT", None),
|
||||||
|
"inject_context": getattr(settings, "LONG_TERM_MEMORY_INJECT_CONTEXT", None),
|
||||||
|
},
|
||||||
|
"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": effective_text,
|
||||||
|
"sanitized_input": msg.text,
|
||||||
|
"history": [],
|
||||||
|
"context": {**context, "session": context.get("session", {}), "channel": msg.channel, "business_context": business_context.model_dump()},
|
||||||
|
}
|
||||||
|
if settings.ROUTING_MODE == "supervisor":
|
||||||
|
plan = await workflow.supervisor.route_plan(state)
|
||||||
|
return {"mode": "supervisor", "route": "supervisor_agent", "agents": plan.agents, "intent": plan.intent, "confidence": plan.confidence, "reason": plan.reason, "metadata": plan.metadata}
|
||||||
|
decision = await workflow.router.route(state)
|
||||||
|
data = decision.model_dump(mode="json")
|
||||||
|
data["mode"] = "router"
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/debug/identity")
|
||||||
|
async def debug_identity(req: GatewayRequest):
|
||||||
|
msg = await gateway.normalize(req.channel, req.payload)
|
||||||
|
identity, context, business_context, missing_identity_keys = _resolve_identity(req, msg)
|
||||||
|
return {
|
||||||
|
"technical_identity": {
|
||||||
|
"tenant_id": identity.tenant_id,
|
||||||
|
"agent_id": identity.agent_id,
|
||||||
|
"conversation_key": identity.conversation_key(),
|
||||||
|
"original_session_id": msg.session_id,
|
||||||
|
},
|
||||||
|
"business_context": business_context.model_dump(),
|
||||||
|
"identity_missing": missing_identity_keys,
|
||||||
|
"context_keys": sorted(context.keys()),
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.get("/debug/usage")
|
||||||
|
async def debug_usage(tenant_id: str | None = None, session_id: str | None = None):
|
||||||
|
return await usage_repository.summarize(tenant_id=tenant_id, session_id=session_id)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/debug/mcp/tools")
|
||||||
|
async def debug_mcp_tools():
|
||||||
|
return {"enabled": tool_router.enabled, "tools": tool_router.describe_tools()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/debug/mcp/call/{tool_name}")
|
||||||
|
async def debug_mcp_call(tool_name: str, arguments: dict | None = None):
|
||||||
|
arguments = arguments or {}
|
||||||
|
ctx = arguments.get("business_context") or arguments.get("identity") or {}
|
||||||
|
result = await tool_router.call(
|
||||||
|
tool_name,
|
||||||
|
arguments,
|
||||||
|
business_context=ctx,
|
||||||
|
original_context=arguments,
|
||||||
|
)
|
||||||
|
return result.model_dump(mode="json")
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/gateway/message")
|
||||||
|
async def gateway_message(req: GatewayRequest):
|
||||||
|
return await _process_gateway_message(req, emit_sse=False)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/gateway/message/sse")
|
||||||
|
async def gateway_message_sse(req: GatewayRequest):
|
||||||
|
return await _process_gateway_message(req, emit_sse=True)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/gateway/events/{session_id}")
|
||||||
|
async def gateway_events(session_id: str, request: Request):
|
||||||
|
last = request.headers.get("last-event-id") or request.query_params.get("last_event_id") or "0"
|
||||||
|
return StreamingResponse(
|
||||||
|
sse_hub.subscribe(session_id, int(last)),
|
||||||
|
media_type="text/event-stream",
|
||||||
|
headers={"Cache-Control": "no-cache", "Connection": "keep-alive", "X-Accel-Buffering": "no"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sessions/{session_id}/messages")
|
||||||
|
async def get_session_messages(session_id: str, limit: int = 50):
|
||||||
|
return {"session_id": session_id, "messages": [m.model_dump(mode="json") for m in await memory.list(session_id, limit)]}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/sessions/{session_id}/checkpoint")
|
||||||
|
async def get_session_checkpoint(session_id: str):
|
||||||
|
return {"session_id": session_id, "checkpoint": await checkpoints.get_latest(session_id)}
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_event("shutdown")
|
||||||
|
async def shutdown():
|
||||||
|
telemetry.shutdown()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from agent_framework.gateways import MCPGatewayClient
|
||||||
|
|
||||||
|
|
||||||
|
def build_mcp_gateway_client() -> MCPGatewayClient | None:
|
||||||
|
if os.getenv("MCP_GATEWAY_ENABLED", "true").lower() != "true":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return MCPGatewayClient(
|
||||||
|
base_url=os.getenv("MCP_GATEWAY_URL", "http://localhost:8300"),
|
||||||
|
token=os.getenv("MCP_GATEWAY_TOKEN") or None,
|
||||||
|
timeout_seconds=int(os.getenv("MCP_GATEWAY_TIMEOUT_SECONDS", "60")),
|
||||||
|
)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
"""Observer adapter that emits IC/NOC/GRL through framework Telemetry only.
|
||||||
|
|
||||||
|
This avoids a second Langfuse root trace created by AgentObserver ->
|
||||||
|
AnalyticsPublisher while preserving the events inside the active request span.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_ic_code(code: str) -> str:
|
||||||
|
code = str(code or "UNKNOWN").strip()
|
||||||
|
return code if code.startswith(("IC.", "AGA.", "NOC.", "GRL.")) else f"IC.{code}"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_noc_code(code: str) -> str:
|
||||||
|
code = str(code or "UNKNOWN").strip()
|
||||||
|
return code if code.startswith("NOC.") else f"NOC.{code}"
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_grl_code(code: str) -> str:
|
||||||
|
code = str(code or "UNKNOWN").strip()
|
||||||
|
return code if code.startswith("GRL.") else f"GRL.{code}"
|
||||||
|
|
||||||
|
|
||||||
|
def _kind_for(event_type: str) -> str:
|
||||||
|
if event_type.startswith(("IC.", "AGA.")):
|
||||||
|
return "ic"
|
||||||
|
if event_type.startswith("NOC."):
|
||||||
|
return "noc"
|
||||||
|
if event_type.startswith("GRL."):
|
||||||
|
return "grl"
|
||||||
|
return "event"
|
||||||
|
|
||||||
|
|
||||||
|
class TelemetryBackedAgentObserver:
|
||||||
|
"""Drop-in subset of AgentObserver backed by Telemetry.event.
|
||||||
|
|
||||||
|
Do not publish through AnalyticsPublisher here. Analytics publishing may be
|
||||||
|
configured with a Langfuse provider, and that path creates an extra root
|
||||||
|
trace for business events such as IC.AGENT_COMPLETED/NOC.006. Telemetry.event
|
||||||
|
uses the active span/trace context, so these events appear inside the single
|
||||||
|
request trace.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, telemetry: Any, *, source: str = "agent_framework") -> None:
|
||||||
|
self.telemetry = telemetry
|
||||||
|
self.source = source
|
||||||
|
|
||||||
|
async def emit(
|
||||||
|
self,
|
||||||
|
event_type: str,
|
||||||
|
payload: dict[str, Any] | None = None,
|
||||||
|
*,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
source: str | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
body = dict(payload or {})
|
||||||
|
meta = dict(metadata or {})
|
||||||
|
body.setdefault("tag", event_type)
|
||||||
|
event = {
|
||||||
|
"eventType": event_type,
|
||||||
|
"source": source or self.source,
|
||||||
|
"eventDate": datetime.now(timezone.utc).isoformat(),
|
||||||
|
"body": body,
|
||||||
|
"metadata": meta,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
await self.telemetry.event(event_type, event, kind=_kind_for(event_type))
|
||||||
|
except TypeError:
|
||||||
|
# Compatibility with older Telemetry.event signatures.
|
||||||
|
await self.telemetry.event(event_type, event)
|
||||||
|
return event
|
||||||
|
|
||||||
|
async def emit_ic(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||||
|
return await self.emit(_normalize_ic_code(code), payload, metadata={**metadata, "ic": True})
|
||||||
|
|
||||||
|
async def emit_noc(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||||
|
return await self.emit(_normalize_noc_code(code), payload, metadata={**metadata, "noc": True})
|
||||||
|
|
||||||
|
async def emit_grl(self, code: str, payload: dict[str, Any] | None = None, **metadata: Any) -> dict[str, Any]:
|
||||||
|
return await self.emit(_normalize_grl_code(code), payload, metadata={**metadata, "grl": True})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
from .tool_renderers import register_tool_renderers
|
||||||
|
|
||||||
|
__all__ = ["register_tool_renderers"]
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from agent_framework.presentation import register_tool_response_renderer
|
||||||
|
|
||||||
|
|
||||||
|
def _money_brl(value: Any) -> str:
|
||||||
|
try:
|
||||||
|
return f"{float(value):.2f}".replace(".", ",")
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_invoice(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
return f"[{agent_label}] Fatura consultada: {result}."
|
||||||
|
|
||||||
|
|
||||||
|
def render_telecom_plan(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
plano = result.get("plano")
|
||||||
|
if plano is None:
|
||||||
|
return None
|
||||||
|
parts = [f"[{agent_label}] Seu plano é {plano}"]
|
||||||
|
internet_gb = result.get("internet_gb")
|
||||||
|
status = result.get("status")
|
||||||
|
if internet_gb is not None:
|
||||||
|
parts.append(f"com {internet_gb} GB")
|
||||||
|
if status is not None:
|
||||||
|
parts.append(f"status {status}")
|
||||||
|
return ", ".join(parts) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_order(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
status = result.get("status")
|
||||||
|
if order_id is None or status is None:
|
||||||
|
return None
|
||||||
|
lines = [f"[{agent_label}] Pedido {order_id}: status {status}."]
|
||||||
|
total = result.get("valor_total")
|
||||||
|
if total is not None:
|
||||||
|
lines.append(f"Valor total: R$ {_money_brl(total)}.")
|
||||||
|
items = result.get("itens") or []
|
||||||
|
rendered_items: list[str] = []
|
||||||
|
if isinstance(items, list):
|
||||||
|
for item in items:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
value = item.get("descricao") or item.get("nome") or item.get("sku")
|
||||||
|
else:
|
||||||
|
value = item
|
||||||
|
if value not in (None, ""):
|
||||||
|
rendered_items.append(str(value))
|
||||||
|
if rendered_items:
|
||||||
|
lines.append("Itens: " + "; ".join(rendered_items) + ".")
|
||||||
|
return " ".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def render_retail_delivery(*, tool_name: str, result: dict[str, Any], state: dict[str, Any], agent_label: str) -> str | None:
|
||||||
|
order_id = result.get("order_id")
|
||||||
|
transportadora = result.get("transportadora")
|
||||||
|
codigo = result.get("codigo_rastreio")
|
||||||
|
previsao = result.get("previsao_entrega")
|
||||||
|
if any(v is None for v in (order_id, transportadora, codigo, previsao)):
|
||||||
|
return None
|
||||||
|
return (
|
||||||
|
f"[{agent_label}] Entrega do pedido {order_id}: transportadora {transportadora}, "
|
||||||
|
f"rastreio {codigo}, previsão {previsao}."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool_renderers() -> None:
|
||||||
|
register_tool_response_renderer("telecom.invoice", render_telecom_invoice)
|
||||||
|
register_tool_response_renderer("telecom.plan", render_telecom_plan)
|
||||||
|
register_tool_response_renderer("retail.order", render_retail_order)
|
||||||
|
register_tool_response_renderer("retail.delivery", render_retail_delivery)
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
transaction_evidence: list[dict[str, Any]]
|
||||||
|
last_transaction_evidence: dict[str, Any]
|
||||||
|
relevant_transaction_evidence: list[dict[str, Any]]
|
||||||
|
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]
|
||||||
|
long_term_memory_subject_key: str
|
||||||
|
long_term_memory_load_error: str
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
from . import devolucao # noqa: F401
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"""Actions de domínio permanecem no agente; o runtime está no framework."""
|
||||||
|
from agent_framework.workflows import workflow_action
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_action("validar_pedido")
|
||||||
|
async def validar_pedido(params: dict, state: dict) -> dict:
|
||||||
|
return {"valid": bool(params.get("order_id"))}
|
||||||
|
|
||||||
|
|
||||||
|
@workflow_action("registrar_devolucao")
|
||||||
|
async def registrar_devolucao(params: dict, state: dict) -> dict:
|
||||||
|
# Substitua pela chamada real ao serviço/MCP e use chave idempotente.
|
||||||
|
return {"protocol": f"DEV-{params['order_id']}", "status": "REQUESTED"}
|
||||||
@@ -0,0 +1,892 @@
|
|||||||
|
from agent_framework.checkpoints.langgraph_saver import create_langgraph_checkpointer
|
||||||
|
from agent_framework.workflows import END, START, FrameworkStateGraph
|
||||||
|
|
||||||
|
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 = FrameworkStateGraph(AgentState)
|
||||||
|
builder.add_node("input_guardrails", self._node("input_guardrails", self.input_guardrails))
|
||||||
|
builder.add_node("load_long_term_memory", self._node("load_long_term_memory", self.load_long_term_memory))
|
||||||
|
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": "load_long_term_memory"},
|
||||||
|
)
|
||||||
|
builder.add_edge("load_long_term_memory", "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", [])
|
||||||
|
relevant_transaction_evidence = list(state.get("relevant_transaction_evidence") or [])
|
||||||
|
judge_context["transaction_evidence"] = relevant_transaction_evidence
|
||||||
|
current_evidence = list(state.get("mcp_results", []) or [])
|
||||||
|
current_evidence.extend(relevant_transaction_evidence)
|
||||||
|
judge_context["evidence"] = current_evidence 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 load_long_term_memory(self, state):
|
||||||
|
"""Carrega LTM antes do roteamento e mantém o resultado no estado.
|
||||||
|
|
||||||
|
A carga explícita evita depender apenas do agente selecionado para realizar
|
||||||
|
a recuperação e facilita o diagnóstico de identidade/namespace.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
memories = await self.long_term_memory_manager.load(state)
|
||||||
|
serialized = []
|
||||||
|
context_lines = []
|
||||||
|
for item in memories or []:
|
||||||
|
if hasattr(item, "model_dump"):
|
||||||
|
data = item.model_dump(mode="json")
|
||||||
|
elif hasattr(item, "__dict__"):
|
||||||
|
data = dict(item.__dict__)
|
||||||
|
elif isinstance(item, dict):
|
||||||
|
data = dict(item)
|
||||||
|
else:
|
||||||
|
data = {"value": str(item)}
|
||||||
|
serialized.append(data)
|
||||||
|
key = data.get("key") or data.get("memory_key") or data.get("category") or "memory"
|
||||||
|
value = data.get("value") or data.get("memory_value")
|
||||||
|
if value not in (None, ""):
|
||||||
|
context_lines.append(f"- {key}: {value}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memories": serialized,
|
||||||
|
"long_term_memory_context": "\n".join(context_lines),
|
||||||
|
}
|
||||||
|
except Exception as exc:
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.load.failed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"long_term_memories": [],
|
||||||
|
"long_term_memory_context": "",
|
||||||
|
"long_term_memory_load_error": str(exc),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def persist_long_term_memory(self, state):
|
||||||
|
try:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.persist.completed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"result": result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"long_term_memory_write_result": result}
|
||||||
|
except Exception as exc:
|
||||||
|
await self.telemetry.event(
|
||||||
|
"long_term_memory.persist.failed",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"subject_key": state.get("long_term_memory_subject_key"),
|
||||||
|
"error": str(exc),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {"long_term_memory_write_result": {"saved": 0, "error": str(exc)}}
|
||||||
|
|
||||||
|
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", []),
|
||||||
|
"transaction_evidence": state.get("relevant_transaction_evidence", []),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
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,104 @@
|
|||||||
|
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
|
||||||
|
cancelar_pedido:
|
||||||
|
map:
|
||||||
|
session_key: session_id
|
||||||
|
extract:
|
||||||
|
order_id:
|
||||||
|
from: message
|
||||||
|
type: string
|
||||||
|
strategy: hybrid
|
||||||
|
description: Extraia somente o identificador do pedido informado explicitamente pelo usuário. Retorne null quando não houver identificador de pedido na mensagem.
|
||||||
|
pattern: (?i)\\b(?:pedido|order)\\s*[:#-]?\\s*([A-Z0-9-]+)\\b
|
||||||
|
group: 1
|
||||||
|
|
||||||
|
solicitar_troca:
|
||||||
|
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,147 @@
|
|||||||
|
# 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_cancel
|
||||||
|
domain: retail
|
||||||
|
agent: orders_agent
|
||||||
|
description: Cancelamento explícito de pedido ou compra.
|
||||||
|
priority: 20
|
||||||
|
mcp_tools:
|
||||||
|
- consultar_pedido
|
||||||
|
- cancelar_pedido
|
||||||
|
keywords:
|
||||||
|
- cancelar pedido
|
||||||
|
- cancelamento do pedido
|
||||||
|
- cancelar a compra
|
||||||
|
- cancelar compra
|
||||||
|
examples:
|
||||||
|
- Quero cancelar meu pedido.
|
||||||
|
- Cancele o pedido.
|
||||||
|
- Quero cancelar a compra.
|
||||||
|
|
||||||
|
- name: retail_order_tracking
|
||||||
|
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,32 @@
|
|||||||
|
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:
|
||||||
|
cancelar_pedido:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
requires: [order_id]
|
||||||
|
|
||||||
|
|
||||||
|
solicitar_troca:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
|
||||||
|
solicitar_devolucao:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
requires: [order_id, reason]
|
||||||
|
execution:
|
||||||
|
mode: workflow
|
||||||
|
workflow: devolucao_pedido
|
||||||
|
version: active
|
||||||
|
|
||||||
|
# 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,130 @@
|
|||||||
|
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
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.invoice
|
||||||
|
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
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: telecom.plan
|
||||||
|
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
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.order
|
||||||
|
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
|
||||||
|
response:
|
||||||
|
mode: renderer
|
||||||
|
renderer: retail.delivery
|
||||||
|
cancelar_pedido:
|
||||||
|
description: Simula o cancelamento de um pedido de varejo.
|
||||||
|
mcp_server: retail
|
||||||
|
enabled: true
|
||||||
|
tool_type: action
|
||||||
|
requires:
|
||||||
|
- order_id
|
||||||
|
confirmation_required: true
|
||||||
|
args_schema:
|
||||||
|
order_id: string
|
||||||
|
selection_keywords:
|
||||||
|
- cancelar pedido
|
||||||
|
- cancelamento do pedido
|
||||||
|
- cancelar compra
|
||||||
|
- cancelar a compra
|
||||||
|
|
||||||
|
|
||||||
|
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,82 @@
|
|||||||
|
# Teste e diagnóstico de Long-Term Memory
|
||||||
|
|
||||||
|
## O que foi corrigido
|
||||||
|
|
||||||
|
1. A LTM agora é carregada explicitamente antes do roteamento.
|
||||||
|
2. O estado recebe uma chave estável em `long_term_memory_subject_key`, baseada em `business_context.customer_key` e, como fallback, `user_id`.
|
||||||
|
3. O resultado de carga e persistência aparece em `metadata.long_term_memory` da resposta.
|
||||||
|
4. `/health` informa a configuração efetiva de LTM carregada pelo processo.
|
||||||
|
5. Falhas de leitura e gravação geram eventos `long_term_memory.load.failed` e `long_term_memory.persist.failed`.
|
||||||
|
|
||||||
|
## Teste
|
||||||
|
|
||||||
|
Primeira sessão:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel":"web",
|
||||||
|
"payload":{
|
||||||
|
"text":"Meu nome preferido é Cris e minha linguagem preferida é Python.",
|
||||||
|
"session_id":"ltm-session-001",
|
||||||
|
"user_id":"ltm-user-001",
|
||||||
|
"customer_id":"ltm-customer-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifique na resposta:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"long_term_memory": {
|
||||||
|
"subject_key": "ltm-customer-001",
|
||||||
|
"write_result": {
|
||||||
|
"saved": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Nova sessão, mesma identidade:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel":"web",
|
||||||
|
"payload":{
|
||||||
|
"text":"Qual é meu nome preferido e qual linguagem eu prefiro?",
|
||||||
|
"session_id":"ltm-session-002",
|
||||||
|
"user_id":"ltm-user-001",
|
||||||
|
"customer_id":"ltm-customer-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Na segunda resposta, confira:
|
||||||
|
|
||||||
|
- `metadata.long_term_memory.subject_key` igual à primeira chamada;
|
||||||
|
- `metadata.long_term_memory.loaded` com registros;
|
||||||
|
- `metadata.long_term_memory.context` preenchido;
|
||||||
|
- ausência de `load_error`.
|
||||||
|
|
||||||
|
## Diagnóstico rápido
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
A seção `long_term_memory` deve mostrar:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "sqlite",
|
||||||
|
"sqlite_path": "./data/agent_framework.db",
|
||||||
|
"table": "agentfw_long_term_memory",
|
||||||
|
"auto_extract": true,
|
||||||
|
"inject_context": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute o backend com o diretório do projeto como diretório de trabalho. Como o caminho SQLite é relativo, iniciar a aplicação em outro diretório pode criar ou consultar outro arquivo `./data/agent_framework.db`.
|
||||||
@@ -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 @@
|
|||||||
|
version: 1
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: devolucao_pedido
|
||||||
|
version: 1
|
||||||
|
start: validar_pedido
|
||||||
|
nodes:
|
||||||
|
- id: validar_pedido
|
||||||
|
action: validar_pedido
|
||||||
|
input:
|
||||||
|
order_id: $.input.order_id
|
||||||
|
- id: registrar_devolucao
|
||||||
|
action: registrar_devolucao
|
||||||
|
retry: 1
|
||||||
|
input:
|
||||||
|
order_id: $.input.order_id
|
||||||
|
reason: $.input.reason
|
||||||
|
edges:
|
||||||
|
- from: validar_pedido
|
||||||
|
to: registrar_devolucao
|
||||||
|
when:
|
||||||
|
path: $.nodes.validar_pedido.valid
|
||||||
|
equals: true
|
||||||
|
- from: validar_pedido
|
||||||
|
to: END
|
||||||
|
when:
|
||||||
|
path: $.nodes.validar_pedido.valid
|
||||||
|
equals: false
|
||||||
|
- from: registrar_devolucao
|
||||||
|
to: END
|
||||||
28
docs/TRANSACTION_OPERATIONAL_EVIDENCE_FIX.md
Normal file
28
docs/TRANSACTION_OPERATIONAL_EVIDENCE_FIX.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Transaction Operational Evidence
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
A confirmed transactional tool result was available only in the execution turn. On a later read-only turn, conversational memory could still mention the prior transaction (for example, a cancellation protocol), while the groundedness judge received only the current MCP results. This could classify a factually correct follow-up as unsupported.
|
||||||
|
|
||||||
|
## Fix
|
||||||
|
|
||||||
|
The framework now records completed/failed transactional tool outcomes as bounded operational evidence in LangGraph state/checkpoint (`transaction_evidence`). This is operational state, not Long Term Memory.
|
||||||
|
|
||||||
|
For each new turn, the runtime correlates previous transaction evidence with the current resource using generic identifiers (`*_id`, `order_id`, `invoice_id`, `asset_id`, `resource_key`, etc.). Only relevant evidence is materialized as `relevant_transaction_evidence`.
|
||||||
|
|
||||||
|
The same relevant evidence is:
|
||||||
|
|
||||||
|
- injected into the answering LLM prompt;
|
||||||
|
- merged with current MCP results for groundedness judges;
|
||||||
|
- exposed in response metadata as `transaction_evidence` for diagnostics;
|
||||||
|
- emitted with the completion telemetry event.
|
||||||
|
|
||||||
|
The history is bounded to the 10 most recent transaction outcomes, and at most 5 correlated entries are injected for a turn.
|
||||||
|
|
||||||
|
## Expected retail example
|
||||||
|
|
||||||
|
1. `cancelar_pedido(PED-1001)` returns protocol `CANCEL-2026-001`.
|
||||||
|
2. The result is persisted as transaction evidence.
|
||||||
|
3. The next `consultar_pedido(PED-1001)` returns `EM_TRANSPORTE`.
|
||||||
|
4. The answering agent and groundedness judge receive both the current order result and the prior cancellation evidence.
|
||||||
|
5. A response that mentions `CANCEL-2026-001` is grounded rather than treated as an unsupported claim.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1448,14 +1448,122 @@ class AgentRuntimeMixin:
|
|||||||
state["active_transaction"] = tx
|
state["active_transaction"] = tx
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
def _finish_active_transaction(self, state: dict[str, Any], status: str) -> None:
|
@staticmethod
|
||||||
"""Close the active transaction without leaving operational state behind."""
|
def _collect_resource_identifiers(value: Any) -> set[tuple[str, str]]:
|
||||||
|
"""Collect stable business/resource identifiers from nested evidence.
|
||||||
|
|
||||||
|
Identifier names are deliberately generic (``*_id`` plus common business
|
||||||
|
keys) so the framework can correlate transaction evidence across domains
|
||||||
|
without embedding telecom/retail-specific behavior.
|
||||||
|
"""
|
||||||
|
identifiers: set[tuple[str, str]] = set()
|
||||||
|
common = {
|
||||||
|
"resource_key", "customer_key", "contract_key", "account_key",
|
||||||
|
"session_key", "subject", "msisdn", "order_id", "invoice_id",
|
||||||
|
"asset_id", "product_id", "service_id", "protocol", "protocolo",
|
||||||
|
}
|
||||||
|
|
||||||
|
def walk(item: Any) -> None:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
for key, raw in item.items():
|
||||||
|
key_s = str(key).strip().lower()
|
||||||
|
if raw not in (None, "", [], {}) and (key_s.endswith("_id") or key_s in common):
|
||||||
|
if isinstance(raw, (str, int, float, bool)):
|
||||||
|
identifiers.add((key_s, str(raw).strip().lower()))
|
||||||
|
walk(raw)
|
||||||
|
elif isinstance(item, (list, tuple, set)):
|
||||||
|
for child in item:
|
||||||
|
walk(child)
|
||||||
|
|
||||||
|
walk(value)
|
||||||
|
return identifiers
|
||||||
|
|
||||||
|
def _record_transaction_evidence(
|
||||||
|
self,
|
||||||
|
state: dict[str, Any],
|
||||||
|
*,
|
||||||
|
transaction: dict[str, Any] | None,
|
||||||
|
status: str,
|
||||||
|
result: dict[str, Any] | None,
|
||||||
|
) -> None:
|
||||||
|
"""Persist compact structured evidence from an executed transaction.
|
||||||
|
|
||||||
|
This is operational evidence, not semantic/LTM memory. It survives later
|
||||||
|
turns through the LangGraph state/checkpoint and can be used both by the
|
||||||
|
answering LLM and groundedness judges.
|
||||||
|
"""
|
||||||
|
if not isinstance(transaction, dict) or not transaction.get("tool_name"):
|
||||||
|
return
|
||||||
|
# Only execution outcomes are evidence. A rejected/not-yet-executed action
|
||||||
|
# must not become a factual claim about the external system.
|
||||||
|
if status not in {"COMPLETED", "FAILED"} or not isinstance(result, dict):
|
||||||
|
return
|
||||||
|
|
||||||
|
evidence = {
|
||||||
|
"transaction_id": transaction.get("transaction_id"),
|
||||||
|
"tool_name": transaction.get("tool_name"),
|
||||||
|
"arguments": dict(transaction.get("arguments") or {}),
|
||||||
|
"status": status,
|
||||||
|
"started_from_intent": transaction.get("started_from_intent"),
|
||||||
|
"result": result,
|
||||||
|
}
|
||||||
|
history = [x for x in (state.get("transaction_evidence") or []) if isinstance(x, dict)]
|
||||||
|
txid = evidence.get("transaction_id")
|
||||||
|
if txid:
|
||||||
|
history = [x for x in history if x.get("transaction_id") != txid]
|
||||||
|
history.append(evidence)
|
||||||
|
# Bound checkpoint growth while retaining enough recent operational history.
|
||||||
|
state["transaction_evidence"] = history[-10:]
|
||||||
|
state["last_transaction_evidence"] = evidence
|
||||||
|
|
||||||
|
def transaction_evidence_for_turn(
|
||||||
|
self,
|
||||||
|
state: dict[str, Any],
|
||||||
|
mcp_results: list[dict[str, Any]] | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Return transaction evidence relevant to the current resource/turn."""
|
||||||
|
history = [x for x in (state.get("transaction_evidence") or []) if isinstance(x, dict)]
|
||||||
|
if not history:
|
||||||
|
return []
|
||||||
|
|
||||||
|
current_identifiers = self._collect_resource_identifiers(mcp_results or [])
|
||||||
|
if not current_identifiers:
|
||||||
|
current_identifiers |= self._collect_resource_identifiers(state.get("business_context") or {})
|
||||||
|
|
||||||
|
if current_identifiers:
|
||||||
|
relevant = []
|
||||||
|
for evidence in history:
|
||||||
|
evidence_ids = self._collect_resource_identifiers(evidence)
|
||||||
|
# Match by value as well as key: integrations sometimes rename
|
||||||
|
# resource identifiers between transaction/read models.
|
||||||
|
current_values = {value for _, value in current_identifiers}
|
||||||
|
evidence_values = {value for _, value in evidence_ids}
|
||||||
|
if current_identifiers & evidence_ids or current_values & evidence_values:
|
||||||
|
relevant.append(evidence)
|
||||||
|
return relevant[-5:]
|
||||||
|
|
||||||
|
# With no resource identifier, expose only the latest evidence to avoid
|
||||||
|
# leaking unrelated historical operations into a new topic.
|
||||||
|
return history[-1:]
|
||||||
|
|
||||||
|
def _finish_active_transaction(
|
||||||
|
self,
|
||||||
|
state: dict[str, Any],
|
||||||
|
status: str,
|
||||||
|
*,
|
||||||
|
result: dict[str, Any] | None = None,
|
||||||
|
) -> None:
|
||||||
|
"""Close the active transaction and retain its result as operational evidence."""
|
||||||
active = self._active_transaction(state)
|
active = self._active_transaction(state)
|
||||||
if isinstance(active, dict):
|
if isinstance(active, dict):
|
||||||
state["last_transaction"] = {
|
state["last_transaction"] = {
|
||||||
**active,
|
**active,
|
||||||
"status": status,
|
"status": status,
|
||||||
|
**({"result": result} if isinstance(result, dict) else {}),
|
||||||
}
|
}
|
||||||
|
self._record_transaction_evidence(
|
||||||
|
state, transaction=active, status=status, result=result
|
||||||
|
)
|
||||||
state["active_transaction"] = None
|
state["active_transaction"] = None
|
||||||
state["selected_tool_call"] = {}
|
state["selected_tool_call"] = {}
|
||||||
state["pending_tool_call"] = {}
|
state["pending_tool_call"] = {}
|
||||||
@@ -1499,6 +1607,7 @@ class AgentRuntimeMixin:
|
|||||||
"transaction_status", "confirmation_required", "confirmation_received",
|
"transaction_status", "confirmation_required", "confirmation_received",
|
||||||
"tool_policy_result", "missing_parameters", "next_state", "pending_domain_workflow", "pending_tool_clarification",
|
"tool_policy_result", "missing_parameters", "next_state", "pending_domain_workflow", "pending_tool_clarification",
|
||||||
"business_workflows_executed", "active_transaction", "last_transaction",
|
"business_workflows_executed", "active_transaction", "last_transaction",
|
||||||
|
"transaction_evidence", "last_transaction_evidence", "relevant_transaction_evidence",
|
||||||
)
|
)
|
||||||
return {key: state.get(key) for key in keys if key in state}
|
return {key: state.get(key) for key in keys if key in state}
|
||||||
|
|
||||||
@@ -1936,7 +2045,7 @@ class AgentRuntimeMixin:
|
|||||||
self._capture_pending_tool_clarification(state, result, tool_name=tool_name, arguments=arguments)
|
self._capture_pending_tool_clarification(state, result, tool_name=tool_name, arguments=arguments)
|
||||||
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
||||||
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
||||||
self._finish_active_transaction(state, final_status)
|
self._finish_active_transaction(state, final_status, result=result)
|
||||||
else:
|
else:
|
||||||
state.update({
|
state.update({
|
||||||
"transaction_status": final_status,
|
"transaction_status": final_status,
|
||||||
@@ -1969,7 +2078,7 @@ class AgentRuntimeMixin:
|
|||||||
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
||||||
state["tool_policy_result"] = {"action": "executed_after_confirmation", "tool_name": tool_name}
|
state["tool_policy_result"] = {"action": "executed_after_confirmation", "tool_name": tool_name}
|
||||||
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
||||||
self._finish_active_transaction(state, final_status)
|
self._finish_active_transaction(state, final_status, result=result)
|
||||||
else:
|
else:
|
||||||
state.update({
|
state.update({
|
||||||
"transaction_status": final_status,
|
"transaction_status": final_status,
|
||||||
@@ -2102,7 +2211,7 @@ class AgentRuntimeMixin:
|
|||||||
self._capture_pending_domain_workflow(state, result)
|
self._capture_pending_domain_workflow(state, result)
|
||||||
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
final_status = ("WORKFLOW_PAUSED" if state.get("pending_domain_workflow") else ("TOOL_RESULT_CLARIFICATION" if state.get("pending_tool_clarification") else ("COMPLETED" if result.get("ok") else "FAILED")))
|
||||||
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
if final_status in _TERMINAL_TRANSACTION_STATUSES:
|
||||||
self._finish_active_transaction(state, final_status)
|
self._finish_active_transaction(state, final_status, result=result)
|
||||||
else:
|
else:
|
||||||
state.update({
|
state.update({
|
||||||
"transaction_status": final_status,
|
"transaction_status": final_status,
|
||||||
@@ -2117,7 +2226,12 @@ class AgentRuntimeMixin:
|
|||||||
return results
|
return results
|
||||||
|
|
||||||
async def _collect_mcp_context(self, state: dict[str, Any]) -> list[dict[str, Any]]:
|
async def _collect_mcp_context(self, state: dict[str, Any]) -> list[dict[str, Any]]:
|
||||||
return await self.execute_tools_for_intent(state)
|
results = await self.execute_tools_for_intent(state)
|
||||||
|
# Materialize the relevant prior operational evidence in graph state so
|
||||||
|
# downstream nodes (output supervision/judges/telemetry) consume the same
|
||||||
|
# evidence set used by the answering agent.
|
||||||
|
state["relevant_transaction_evidence"] = self.transaction_evidence_for_turn(state, results)
|
||||||
|
return results
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Conversation memory / context compression
|
# Conversation memory / context compression
|
||||||
@@ -2271,6 +2385,12 @@ class AgentRuntimeMixin:
|
|||||||
sections.append(f"BusinessContext canônico:\n{runtime.business_context or '[sem business_context]'}")
|
sections.append(f"BusinessContext canônico:\n{runtime.business_context or '[sem business_context]'}")
|
||||||
if mcp_results is not None:
|
if mcp_results is not None:
|
||||||
sections.append(f"Resultados MCP normalizados pelo framework:\n{mcp_results}")
|
sections.append(f"Resultados MCP normalizados pelo framework:\n{mcp_results}")
|
||||||
|
transaction_evidence = self.transaction_evidence_for_turn(state, mcp_results)
|
||||||
|
if transaction_evidence:
|
||||||
|
sections.append(
|
||||||
|
"Evidências operacionais de transações anteriores relevantes ao recurso atual "
|
||||||
|
f"(persistidas pelo framework, não inferidas pela memória conversacional):\n{transaction_evidence}"
|
||||||
|
)
|
||||||
if rag_context is not None:
|
if rag_context is not None:
|
||||||
sections.append(f"Contexto RAG nativo do framework:\n{rag_context or '[sem contexto RAG]'}")
|
sections.append(f"Contexto RAG nativo do framework:\n{rag_context or '[sem contexto RAG]'}")
|
||||||
if rag_metadata is not None:
|
if rag_metadata is not None:
|
||||||
|
|||||||
@@ -460,6 +460,7 @@ async def _process_gateway_message(req: GatewayRequest, emit_sse: bool = False)
|
|||||||
"domain": result.get("domain"),
|
"domain": result.get("domain"),
|
||||||
"mcp_tools": result.get("mcp_tools"),
|
"mcp_tools": result.get("mcp_tools"),
|
||||||
"mcp_results": result.get("mcp_results"),
|
"mcp_results": result.get("mcp_results"),
|
||||||
|
"transaction_evidence": result.get("relevant_transaction_evidence", []),
|
||||||
"business_context": business_context.model_dump(),
|
"business_context": business_context.model_dump(),
|
||||||
"identity_missing": missing_identity_keys,
|
"identity_missing": missing_identity_keys,
|
||||||
"judges": result.get("judge_results"),
|
"judges": result.get("judge_results"),
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ class AgentState(TypedDict, total=False):
|
|||||||
selected_tool_call: dict[str, Any]
|
selected_tool_call: dict[str, Any]
|
||||||
pending_tool_call: dict[str, Any]
|
pending_tool_call: dict[str, Any]
|
||||||
transaction_status: str
|
transaction_status: str
|
||||||
|
transaction_evidence: list[dict[str, Any]]
|
||||||
|
last_transaction_evidence: dict[str, Any]
|
||||||
|
relevant_transaction_evidence: list[dict[str, Any]]
|
||||||
confirmation_required: bool
|
confirmation_required: bool
|
||||||
confirmation_received: bool
|
confirmation_received: bool
|
||||||
tool_policy_result: dict[str, Any]
|
tool_policy_result: dict[str, Any]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user