First commit
This commit is contained in:
241
.env.example
Normal file
241
.env.example
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
###############################################################################
|
||||||
|
# AI AGENT PLATFORM - CONFIGURAÇÃO ÚNICA
|
||||||
|
# Este arquivo é lido por Pydantic Settings no framework e no backend template.
|
||||||
|
###############################################################################
|
||||||
|
|
||||||
|
APP_NAME=ai-agent-template
|
||||||
|
APP_ENV=local
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
API_HOST=0.0.0.0
|
||||||
|
API_PORT=8000
|
||||||
|
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LLM - OCI Generative AI como provider principal
|
||||||
|
###############################################################################
|
||||||
|
# Opções: mock, oci_openai, oci_sdk, openai_compatible
|
||||||
|
LLM_PROVIDER=oci_openai
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2048
|
||||||
|
LLM_TIMEOUT_SECONDS=120
|
||||||
|
|
||||||
|
# OCI OpenAI-compatible endpoint
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.us-chicago-1.oci.oraclecloud.com/openai/v1
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_API_KEY=sk-ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6ph3FgX6
|
||||||
|
OCI_GENAI_PROJECT_OCID=
|
||||||
|
|
||||||
|
# OCI_AUTH_MODE=config_file|instance_principal|resource_principal
|
||||||
|
OCI_AUTH_MODE=config_file
|
||||||
|
# OCI SDK / signer / profiles
|
||||||
|
OCI_CONFIG_FILE=~/.oci/config
|
||||||
|
OCI_PROFILE=DEFAULT
|
||||||
|
OCI_COMPARTMENT_ID=ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
||||||
|
OCI_REGION=us-chicago-1
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Persistência
|
||||||
|
###############################################################################
|
||||||
|
# Opções: memory, autonomous, mongodb
|
||||||
|
SESSION_REPOSITORY_PROVIDER=autonomous
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=autonomous
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
|
||||||
|
# Autonomous Database
|
||||||
|
ADB_USER=admin
|
||||||
|
ADB_PASSWORD=fjhsdf04954hf
|
||||||
|
ADB_DSN=oradb23aidev_high
|
||||||
|
ADB_WALLET_LOCATION=/ORACLE/DEFAULT/Wallet_ORADB23aiDev
|
||||||
|
ADB_WALLET_PASSWORD=fjhsdf04954hf
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
|
||||||
|
# MongoDB - também pode representar Autonomous usando API compatível com Mongo, se habilitada no ambiente
|
||||||
|
MONGODB_URI=mongodb://mongo:mongopassword@localhost:27017
|
||||||
|
MONGODB_DATABASE=agent_platform
|
||||||
|
|
||||||
|
# Redis
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
ENABLE_REDIS_CACHE=false
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RAG / Vector / Graph
|
||||||
|
###############################################################################
|
||||||
|
VECTOR_STORE_PROVIDER=memory
|
||||||
|
GRAPH_STORE_PROVIDER=memory
|
||||||
|
RAG_TOP_K=5
|
||||||
|
EMBEDDING_PROVIDER=mock
|
||||||
|
OCI_EMBEDDING_MODEL=cohere.embed-multilingual-v3.0
|
||||||
|
RAG_FILE_GLOBS=*.md,*.txt,*.yaml,*.yml,*.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Observabilidade
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_TRACE_MODE=verbose # Opcional: verbose, compact
|
||||||
|
LANGFUSE_ROOT_SPAN_NAME=agent.gateway_message
|
||||||
|
LANGFUSE_LEGACY_IO_FALLBACK=true
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-bd9b0c7e-2b8b-4e5b-a382-284a9b4413b3
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-5f5cc18d-0bb5-424e-b5d0-cb3664d58c20
|
||||||
|
LANGFUSE_HOST=http://localhost:3005
|
||||||
|
ENABLE_OTEL=false
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=
|
||||||
|
OTEL_SERVICE_NAME=ai-agent-template
|
||||||
|
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Analytics / Observer corporativo
|
||||||
|
###############################################################################
|
||||||
|
# Quando true, AgentObserver publica eventos IC.*, NOC.* e GRL.* nos providers abaixo.
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
# Providers aceitos: oci_streaming,pubsub,noop
|
||||||
|
ANALYTICS_PROVIDERS=pubsub
|
||||||
|
# Compatibilidade FIRST/TIM: pode informar AGENT_PUBSUB_TOPIC diretamente.
|
||||||
|
AGENT_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TOPIC_PATH=
|
||||||
|
GCP_PROJECT_ID=
|
||||||
|
GCP_PUBSUB_TOPIC=
|
||||||
|
GCP_PUBSUB_TIMEOUT_SECONDS=30
|
||||||
|
# Credencial GCP segue padrão Google:
|
||||||
|
# GOOGLE_APPLICATION_CREDENTIALS=/secrets/gcp-service-account.json
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# OCI Streaming
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_OCI_STREAMING=false
|
||||||
|
OCI_STREAM_ENDPOINT=
|
||||||
|
OCI_STREAM_OCID=
|
||||||
|
OCI_STREAM_PARTITION_KEY=agent-events
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Guardrails, Judges, Supervisor
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_INPUT_GUARDRAILS=true
|
||||||
|
ENABLE_OUTPUT_GUARDRAILS=true
|
||||||
|
ENABLE_JUDGES=true
|
||||||
|
ENABLE_SUPERVISOR=true
|
||||||
|
ENABLE_OUTPUT_SUPERVISOR=true
|
||||||
|
ENABLE_PARALLEL_GUARDRAILS=true
|
||||||
|
GUARDRAILS_FAIL_FAST=true
|
||||||
|
OUTPUT_SUPERVISOR_MAX_RETRIES=3
|
||||||
|
GUARDRAILS_CONFIG_PATH=./config/guardrails.yaml
|
||||||
|
JUDGES_CONFIG_PATH=./config/judges.yaml
|
||||||
|
PROMPT_POLICY_PATH=./config/prompt_policy.yaml
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# Gateway de canais
|
||||||
|
###############################################################################
|
||||||
|
DEFAULT_CHANNEL=web
|
||||||
|
# embedded = backend may parse simple/native channel payloads.
|
||||||
|
# external = backend only accepts GatewayRequest normalized by an external Channel Gateway.
|
||||||
|
FRAMEWORK_CHANNEL_INPUT_MODE=embedded
|
||||||
|
ENABLE_VOICE_ADAPTER=true
|
||||||
|
ENABLE_WHATSAPP_ADAPTER=true
|
||||||
|
ENABLE_TEXT_ADAPTER=true
|
||||||
|
|
||||||
|
#################################################
|
||||||
|
# ENTERPRISE ROUTING
|
||||||
|
#################################################
|
||||||
|
# Arquivo YAML com intents, keywords, políticas de estado e fallback.
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
# true = usa LLM para classificar quando keywords/estado não resolverem.
|
||||||
|
# Em produção, costuma ser útil; em desenvolvimento, false evita custo e latência.
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
|
||||||
|
# Semantic route stickiness (optional).
|
||||||
|
# Uses a lightweight LLM profile to decide only CONTINUE vs ROUTE.
|
||||||
|
# There are no regexes or deterministic language rules.
|
||||||
|
ENABLE_ROUTE_STICKINESS=false
|
||||||
|
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
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
|
MCP_TOOL_TIMEOUT_SECONDS=30
|
||||||
|
|
||||||
|
# router = EnterpriseRouter seleciona um agente; supervisor = pode acionar múltiplos agentes
|
||||||
|
ROUTING_MODE=router
|
||||||
|
|
||||||
|
# Usage/cost accounting
|
||||||
|
USAGE_REPOSITORY_PROVIDER=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
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# MCP Gateway
|
||||||
|
###############################################################################
|
||||||
|
# true = framework routes tool calls to the dedicated MCP Gateway.
|
||||||
|
# false = framework calls MCP servers directly from mcp_servers.yaml.
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
# MCP_GATEWAY_TOKEN=
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# LONG-TERM MEMORY
|
||||||
|
###############################################################################
|
||||||
|
ENABLE_LONG_TERM_MEMORY=true
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
|
||||||
|
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
|
||||||
|
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
|
||||||
|
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT=true
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT=true
|
||||||
|
|
||||||
|
# Optional agent/deployment observability contract mapping.
|
||||||
|
# Keep disabled in the generic framework; agents may enable their own YAML mapping.
|
||||||
|
OBSERVABILITY_CODE_MAPPING_ENABLED=false
|
||||||
|
OBSERVABILITY_CODE_MAPPING_PATH=
|
||||||
|
|
||||||
|
###############################################################################
|
||||||
|
# RAG provider selection (mutually exclusive at runtime)
|
||||||
|
###############################################################################
|
||||||
|
# standard = RAG original do agent_framework_oci (default, backward compatible)
|
||||||
|
# kbdb = KBDB enterprise via PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE
|
||||||
|
RAG_PROVIDER=standard
|
||||||
|
|
||||||
|
# Somente usados quando RAG_PROVIDER=kbdb. Se vazios, credenciais caem para ADB_*.
|
||||||
|
KBDB_DB_USER=
|
||||||
|
KBDB_DB_PASSWORD=
|
||||||
|
KBDB_DB_DSN=
|
||||||
|
KBDB_DB_WALLET_LOCATION=
|
||||||
|
KBDB_DB_WALLET_PASSWORD=
|
||||||
|
KBDB_SEARCH_TYPE=hybrid
|
||||||
|
KBDB_NODE_EXPANSION=true
|
||||||
|
KBDB_NODE_MAX_RELATED=8
|
||||||
|
KBDB_GRAPH_CROSS_REF=false
|
||||||
|
KBDB_MAX_CROSS_REF_HOPS=1
|
||||||
|
KBDB_DOCUMENT_TYPE=customer_safe
|
||||||
|
KBDB_METADATA_JSON=
|
||||||
|
KBDB_MIN_SCORE=
|
||||||
|
|
||||||
|
# Framework vocabulary and standard responses: pt-BR | en
|
||||||
|
AGENT_LOCALE=pt-BR
|
||||||
|
# Optional directory containing complete pt-BR.json and en.json catalogs.
|
||||||
|
# AGENT_LOCALES_DIR=./config/locales
|
||||||
57
ATUALIZACAO_TESTES.md
Normal file
57
ATUALIZACAO_TESTES.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# Atualização dos testes após mudanças de contrato
|
||||||
|
|
||||||
|
Esta revisão mantém os quatro nomes de arquivo e a quantidade de testes. Oito
|
||||||
|
testes foram atualizados, sem `skip`, `xfail` ou alterações no runtime para
|
||||||
|
acomodar as expectativas antigas. Os nomes de sete funções foram ajustados para
|
||||||
|
descrever o comportamento atual. Os comentários nos testes registram o histórico.
|
||||||
|
|
||||||
|
## Histórico verificado no Git do pacote original
|
||||||
|
|
||||||
|
- `1711575`, 2026-09-07, `bugfix reconciliation extractor`: removeu
|
||||||
|
`field_evidence_views`, a construção de candidatos estruturais e partes da
|
||||||
|
reconciliação anterior.
|
||||||
|
- `82636ec`, 2026-09-08, `bugfix: extractor cases 8 and 16`: consolidou a coleta
|
||||||
|
de parâmetros no `single_extractor`, com contexto e pré-validação autoritativa.
|
||||||
|
- `36f9781`, 2026-09-18, `new feature: transaction interruption for same intent replacement`:
|
||||||
|
também adicionou sanitização e reavaliação ao DLEX_OUT e passou a usar
|
||||||
|
`sanitized_text` somente quando há um texto substituto.
|
||||||
|
|
||||||
|
## O que os testes passam a verificar
|
||||||
|
|
||||||
|
| Arquivo | Contrato coberto |
|
||||||
|
|---|---|
|
||||||
|
| `tests/test_transactional_tool_flow.py` | Após `NEEDS_PARAMETER`, o fluxo aguarda nova informação; a nova validação ocorre no turno seguinte e a operação não executa antes da confirmação. |
|
||||||
|
| `tests/test_transaction_parameter_llm_precedence.py` | O reconciliador recebe schema e contexto, aceita o campo resolvido e respeita `preserve` sem sobrescrever o valor conhecido. |
|
||||||
|
| `tests/test_transaction_explicit_abandonment_and_structural_candidate.py` | Campo indefinido continua pendente; extração contextual bem-sucedida passa pela validação antes da confirmação; `clear` não inventa um valor nem apaga os demais campos. |
|
||||||
|
| `tests/test_dlex_out_expected_protocol_authorization.py` | Protocolo autorizado não exige substituição do texto; credencial é removida e a resposta é reavaliada; outra violação mantém o bloqueio. |
|
||||||
|
|
||||||
|
Os simuladores deixaram de depender de frases específicas do prompt, como
|
||||||
|
“FOCO PRINCIPAL DA RECONCILIAÇÃO”. Eles devolvem saídas compatíveis com o contrato
|
||||||
|
da chamada. A identificação semântica por um modelo real não é comprovada por
|
||||||
|
esses testes unitários: os testes verificam como o framework utiliza suas saídas.
|
||||||
|
|
||||||
|
## Verificações realizadas
|
||||||
|
|
||||||
|
- 160 testes selecionados aprovados no pacote atualizado, incluindo os 41 testes
|
||||||
|
de localização e os quatro arquivos desta revisão.
|
||||||
|
- Os 66 testes dos quatro arquivos também foram executados usando a biblioteca
|
||||||
|
original do ZIP: todos aprovados. Assim, a atualização dos testes não depende
|
||||||
|
da alteração de idiomas para passar.
|
||||||
|
- Contagem mantida por arquivo: 31, 13, 17 e 5 testes, respectivamente.
|
||||||
|
- Nenhuma chamada a OCI, modelos remotos ou ferramentas reais de negócio.
|
||||||
|
- Esta revisão não afirma que a suíte completa do repositório foi executada.
|
||||||
|
|
||||||
|
## Aplicar ao repositório
|
||||||
|
|
||||||
|
O ZIP completo inclui a localização em `pt-BR` e `en` entregue anteriormente e
|
||||||
|
esta revisão dos testes. Copie o conteúdo da pasta `agent_platform_oci` para a
|
||||||
|
raiz do repositório, preservando a pasta `.git` que já existe no seu checkout.
|
||||||
|
|
||||||
|
Se a localização já estiver aplicada, o ZIP complementar de testes contém apenas
|
||||||
|
os quatro arquivos atualizados e esta documentação. Não é preciso usar ambos.
|
||||||
|
|
||||||
|
Sugestão de mensagem de commit para a revisão dos testes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
test: align transaction and DLEX_OUT tests with current contracts
|
||||||
|
```
|
||||||
BIN
Documentacao/.DS_Store
vendored
Normal file
BIN
Documentacao/.DS_Store
vendored
Normal file
Binary file not shown.
24
Documentacao/.oca/custom_code_review_guidelines.txt
Normal file
24
Documentacao/.oca/custom_code_review_guidelines.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Sample guideline, please follow similar structure for guideline with code samples
|
||||||
|
# 1. Suggest using streams instead of simple loops for better readability.
|
||||||
|
# <example>
|
||||||
|
# *Comment:
|
||||||
|
# Category: Minor
|
||||||
|
# Issue: Use streams instead of a loop for better readability.
|
||||||
|
# Code Block:
|
||||||
|
#
|
||||||
|
# ```java
|
||||||
|
# // Calculate squares of numbers
|
||||||
|
# List<Integer> squares = new ArrayList<>();
|
||||||
|
# for (int number : numbers) {
|
||||||
|
# squares.add(number * number);
|
||||||
|
# }
|
||||||
|
# ```
|
||||||
|
# Recommendation:
|
||||||
|
#
|
||||||
|
# ```java
|
||||||
|
# // Calculate squares of numbers
|
||||||
|
# List<Integer> squares = Arrays.stream(numbers)
|
||||||
|
# .map(n -> n * n) // Map each number to its square
|
||||||
|
# .toList();
|
||||||
|
# ```
|
||||||
|
# </example>
|
||||||
BIN
Documentacao/Arquitetura_Geral_Agent_Framework_OCI.docx
Normal file
BIN
Documentacao/Arquitetura_Geral_Agent_Framework_OCI.docx
Normal file
Binary file not shown.
37
Documentacao/FIX_DEADLOCK_SEQUENCE_CROSS_LOOP.md
Normal file
37
Documentacao/FIX_DEADLOCK_SEQUENCE_CROSS_LOOP.md
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# Correção: deadlock/espera cross-loop na geração de sequence
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
A API síncrona `agent_framework.observer.event()` podia ser chamada em uma worker thread sem event loop ativo. Nesse caso, a implementação anterior executava `asyncio.run(aevent(...))`, criando um novo event loop temporário. Ao mesmo tempo, `analytics/tim_sequence.py` compartilhava instâncias globais de `asyncio.Lock` (`_mongo_index_lock` e `_memory_lock`) entre chamadas que podiam vir de event loops diferentes.
|
||||||
|
|
||||||
|
Na primeira operação Mongo, `_ensure_mongo_ttl_index_once()` mantinha `_mongo_index_lock` durante a criação do índice TTL. A contenção por outro loop podia deixar a segunda chamada aguardando indefinidamente.
|
||||||
|
|
||||||
|
## Alterações aplicadas
|
||||||
|
|
||||||
|
1. `observer.py`
|
||||||
|
- removido `asyncio.run()` do caminho síncrono de `event()`;
|
||||||
|
- adicionado um event loop dedicado e reutilizável para chamadas síncronas;
|
||||||
|
- submissão cross-thread feita com `asyncio.run_coroutine_threadsafe()`;
|
||||||
|
- encerramento best-effort do loop no shutdown do processo.
|
||||||
|
|
||||||
|
2. `analytics/tim_sequence.py`
|
||||||
|
- `_mongo_index_lock`: `asyncio.Lock` -> `threading.Lock`;
|
||||||
|
- `_memory_lock`: `asyncio.Lock` -> `threading.Lock`;
|
||||||
|
- inicialização do índice TTL movida para uma função síncrona protegida por lock de thread e chamada via `asyncio.to_thread()`;
|
||||||
|
- o contador de fallback em memória usa uma seção crítica curta e thread-safe.
|
||||||
|
|
||||||
|
3. Testes
|
||||||
|
- `tests/test_observer_cross_loop_deadlock_fix.py` valida:
|
||||||
|
- múltiplas worker threads usando `event()` compartilham o mesmo loop síncrono do observer;
|
||||||
|
- sequence em memória permanece monotônica entre event loops independentes;
|
||||||
|
- criação do índice TTL ocorre apenas uma vez sob contenção cross-loop.
|
||||||
|
|
||||||
|
## Validação executada
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/test_observer_cross_loop_deadlock_fix.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado: `3 passed`.
|
||||||
|
|
||||||
|
A suíte completa do repositório possui falhas preexistentes/independentes desta alteração, incluindo conflitos de coleta de arquivos `test_long_term_memory.py`, caminhos estáticos de template e testes de checkpoint/workflow. Esses itens não foram alterados por esta correção.
|
||||||
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
64
Documentacao/IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Implementação — workflows transacionais determinísticos
|
||||||
|
|
||||||
|
## Entrega
|
||||||
|
|
||||||
|
Foi adicionada ao `agent_framework_oci` uma capacidade opcional para executar transações multi-etapas como workflows determinísticos compilados em LangGraph.
|
||||||
|
|
||||||
|
### Módulo novo
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/workflows/`
|
||||||
|
|
||||||
|
- `models.py`: contratos Pydantic e validação estrutural;
|
||||||
|
- `repository.py`: resolução de versão ativa e leitura de YAML imutável;
|
||||||
|
- `registry.py`: registro desacoplado de actions sync/async;
|
||||||
|
- `runtime.py`: compilação, cache e execução do StateGraph;
|
||||||
|
- `tool_executor.py`: integração com a política da tool;
|
||||||
|
- `__init__.py`: API pública.
|
||||||
|
|
||||||
|
### Política expandida
|
||||||
|
|
||||||
|
`ToolPolicy` agora aceita:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
execution:
|
||||||
|
mode: direct_tool | workflow | agent
|
||||||
|
workflow: nome_do_workflow
|
||||||
|
version: active | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
O default permanece `direct_tool`, preservando compatibilidade.
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Foram adicionados:
|
||||||
|
|
||||||
|
- `ENABLE_TRANSACTIONAL_WORKFLOWS=false`;
|
||||||
|
- `WORKFLOWS_PATH=./workflows`.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
Inclui um exemplo completo de devolução de pedido com:
|
||||||
|
|
||||||
|
- confirmação e campos obrigatórios pela política;
|
||||||
|
- workflow YAML versionado;
|
||||||
|
- actions de domínio no backend;
|
||||||
|
- bifurcação determinística baseada no resultado da validação.
|
||||||
|
|
||||||
|
## Validação realizada
|
||||||
|
|
||||||
|
- `tests/unit/test_tool_policies.py`: 4 testes aprovados;
|
||||||
|
- compilação Python de framework, template e novos testes: aprovada;
|
||||||
|
- o teste funcional novo do LangGraph foi criado, mas não pôde ser executado neste container porque `langgraph` não está instalado no ambiente. A dependência já está declarada no `pyproject.toml` do framework.
|
||||||
|
|
||||||
|
## Escopo e segurança
|
||||||
|
|
||||||
|
Esta entrega cria o motor e a integração de política. Para operações críticas em produção ainda é necessário conectar:
|
||||||
|
|
||||||
|
- execution store persistente;
|
||||||
|
- idempotência de negócio nas actions/APIs;
|
||||||
|
- autorização por escopo;
|
||||||
|
- telemetria IC/NOC específica de workflow;
|
||||||
|
- compensação/Saga quando aplicável;
|
||||||
|
- estratégia corporativa de timeout e retry.
|
||||||
|
|
||||||
|
Esses itens foram explicitamente documentados para evitar a falsa impressão de que retry por si só garante segurança transacional.
|
||||||
133
Documentacao/INVENTARIO_AGENT_GATEWAY_MCP_GATEWAY.md
Normal file
133
Documentacao/INVENTARIO_AGENT_GATEWAY_MCP_GATEWAY.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Inventário — Agent Gateway + MCP Gateway Overlay
|
||||||
|
|
||||||
|
Este inventário lista os arquivos incluídos no overlay `agent_platform_agent_gateway_mcp_gateway_overlay.zip`, indicando a área, o tipo de alteração e a finalidade de cada arquivo.
|
||||||
|
|
||||||
|
## Resumo
|
||||||
|
|
||||||
|
| Área | Quantidade |
|
||||||
|
|---|---:|
|
||||||
|
| Documentação | 1 |
|
||||||
|
| Agent Gateway | 10 |
|
||||||
|
| MCP Gateway | 5 |
|
||||||
|
| Agent Framework | 4 |
|
||||||
|
| Template Backend | 2 |
|
||||||
|
| MCP Server Mock | 2 |
|
||||||
|
| Deploy | 2 |
|
||||||
|
|
||||||
|
## Arquivos por área
|
||||||
|
|
||||||
|
### Documentação
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md` | Novo / overlay | Documento principal do overlay. Explica a nova arquitetura sem AI Gateway separado, com Agent Gateway governando políticas/modelos e MCP Gateway separado para tools. |
|
||||||
|
|
||||||
|
### Agent Gateway
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps/agent_gateway/app/config/governance_loader.py` | Novo / overlay | Carrega o arquivo YAML de governança do Agent Gateway a partir de AGENT_GATEWAY_GOVERNANCE_CONFIG. |
|
||||||
|
| `apps/agent_gateway/app/governance/__init__.py` | Novo / overlay | Inicializa o pacote Python de governança do Agent Gateway. |
|
||||||
|
| `apps/agent_gateway/app/governance/audit.py` | Novo / overlay | Centraliza logging/auditoria das decisões de governança do Agent Gateway, com proteção simples para não logar mensagem completa. |
|
||||||
|
| `apps/agent_gateway/app/governance/evaluation_hooks.py` | Novo / overlay | Hooks antes e depois da chamada ao backend/runtime. Serve para amostragem, evaluator, scoring ou integração futura com Langfuse. |
|
||||||
|
| `apps/agent_gateway/app/governance/model_policies.py` | Novo / overlay | Resolve políticas de modelo/profile no Agent Gateway. Define qual provider/model/profile deve ser usado por operação, tenant e agente. |
|
||||||
|
| `apps/agent_gateway/app/governance/rate_limit.py` | Novo / overlay | Implementa rate limit em memória por tenant, agente e canal antes de encaminhar a requisição ao backend/runtime. |
|
||||||
|
| `apps/agent_gateway/app/governance/usage.py` | Novo / overlay | Hook para registrar uso de gateway, políticas aplicadas e respostas do backend. Pronto para plugar métricas, banco, Langfuse ou OTEL. |
|
||||||
|
| `apps/agent_gateway/app/governance_middleware.py` | Novo / overlay | Componente principal de governança do Agent Gateway. Aplica rate limit, resolve model_policy, gera headers/metadados e executa hooks antes/depois do backend. |
|
||||||
|
| `apps/agent_gateway/app/routes/governed_proxy_example.py` | Novo / overlay | Exemplo de rota governada para demonstrar como aplicar governança antes de encaminhar para o Agent Backend/Runtime. |
|
||||||
|
| `apps/agent_gateway/config/gateway_governance.yaml` | Novo / overlay | Configuração de governança do Agent Gateway: profiles, operation_profiles, providers permitidos, rate limits, headers propagados e evaluation hooks. |
|
||||||
|
|
||||||
|
### MCP Gateway
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `apps/mcp_gateway/Dockerfile` | Novo / overlay | Imagem Docker do MCP Gateway. |
|
||||||
|
| `apps/mcp_gateway/app/__init__.py` | Novo / overlay | Inicializa o pacote Python da aplicação MCP Gateway. |
|
||||||
|
| `apps/mcp_gateway/app/main.py` | Novo / overlay | Aplicação FastAPI do MCP Gateway. Expõe health, ready, catálogo de tools e endpoint de invoke com auth, autorização, mapping, cache, timeout e retry. |
|
||||||
|
| `apps/mcp_gateway/config/mcp_gateway.yaml` | Novo / overlay | Configuração central do MCP Gateway: MCP servers, tools, versões, cache, timeout, retry, autorização por agente/canal e mapping BusinessContext → parâmetros. |
|
||||||
|
| `apps/mcp_gateway/requirements.txt` | Novo / overlay | Dependências Python do MCP Gateway. |
|
||||||
|
|
||||||
|
### Agent Framework
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateway_policy_context.py` | Novo / overlay | Helper no framework para o Runtime ler a política de modelo enviada pelo Agent Gateway em state['metadata']['model_policy']. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateways/__init__.py` | Novo / overlay | Inicializa o pacote de clients de gateways no framework, exportando MCPGatewayClient. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py` | Novo / overlay | Client assíncrono do framework para chamar o MCP Gateway: listar tools e executar tools. |
|
||||||
|
| `libs/agent_framework/src/agent_framework/runtime_mcp_gateway_adapter.py` | Novo / overlay | Mixin opcional para agentes/runtime chamarem tools via MCP Gateway e anexarem resultados em state['mcp_results']. |
|
||||||
|
|
||||||
|
### Template Backend
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `templates/agent_template_backend/app/mcp_gateway_client_factory.py` | Novo / overlay | Factory no template backend para construir MCPGatewayClient a partir de variáveis de ambiente. |
|
||||||
|
|
||||||
|
### MCP Server Mock
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `mcp/servers/mock_telecom_mcp/app.py` | Novo / overlay | Mock MCP Server com tools consultar_fatura e consultar_pagamentos para validar o MCP Gateway localmente. |
|
||||||
|
| `mcp/servers/mock_telecom_mcp/requirements.txt` | Novo / overlay | Dependências do mock MCP Server de telecom usado para testes locais. |
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
| Arquivo | Tipo | Finalidade |
|
||||||
|
|---|---|---|
|
||||||
|
| `deploy/docker/docker-compose.mcp-gateway.yml` | Novo / overlay | Docker Compose para subir MCP Gateway e mock_telecom_mcp localmente. |
|
||||||
|
| `deploy/k8s/mcp-gateway.yaml` | Novo / overlay | Manifest Kubernetes de Deployment e Service do MCP Gateway. |
|
||||||
|
|
||||||
|
## Observações de integração
|
||||||
|
|
||||||
|
### Agent Gateway
|
||||||
|
|
||||||
|
Os arquivos em `apps/agent_gateway` não criam um novo serviço. Eles evoluem o Agent Gateway existente para atuar como gateway dedicado da plataforma, centralizando:
|
||||||
|
|
||||||
|
- políticas de modelo/profile;
|
||||||
|
- rate limit;
|
||||||
|
- auditoria;
|
||||||
|
- hooks de avaliação;
|
||||||
|
- propagação de metadados de governança para o Runtime.
|
||||||
|
|
||||||
|
A rota `governed_proxy_example.py` é um exemplo de integração. O handler real do `POST /gateway/message` deve aplicar:
|
||||||
|
|
||||||
|
```python
|
||||||
|
governed_body, headers = governance.prepare_backend_request(body)
|
||||||
|
```
|
||||||
|
|
||||||
|
antes de chamar o backend/runtime, e:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return governance.process_backend_response(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
após receber a resposta.
|
||||||
|
|
||||||
|
### MCP Gateway
|
||||||
|
|
||||||
|
O MCP Gateway é um serviço separado. Ele centraliza:
|
||||||
|
|
||||||
|
- catálogo de tools;
|
||||||
|
- autorização por agente/canal;
|
||||||
|
- versionamento de tools;
|
||||||
|
- mapping de BusinessContext para parâmetros;
|
||||||
|
- cache;
|
||||||
|
- timeout;
|
||||||
|
- retry;
|
||||||
|
- auditoria simples.
|
||||||
|
|
||||||
|
### Runtime / Backend
|
||||||
|
|
||||||
|
O Runtime continua responsável por:
|
||||||
|
|
||||||
|
- LangGraph;
|
||||||
|
- estado;
|
||||||
|
- memória;
|
||||||
|
- checkpoints;
|
||||||
|
- fluxo;
|
||||||
|
- providers LLM existentes.
|
||||||
|
|
||||||
|
O Runtime passa a chamar tools via MCP Gateway usando `MCPGatewayClient` e/ou `MCPGatewayRuntimeMixin`.
|
||||||
|
|
||||||
|
### AI Gateway
|
||||||
|
|
||||||
|
Este overlay não cria `apps/ai_gateway`. A governança de modelo fica no Agent Gateway, e a execução LLM continua no Runtime/backend usando os providers já existentes.
|
||||||
987
Documentacao/Implementando_Basic_Auth.md
Normal file
987
Documentacao/Implementando_Basic_Auth.md
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
# Implementando Basic Auth
|
||||||
|
|
||||||
|
Para validar **todo o circuito com Basic Auth**, você precisa configurar três relações distintas:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cliente de teste
|
||||||
|
└─ Basic Auth A ─► Agent Gateway :8010
|
||||||
|
└─ Basic Auth B ─► Agent Backend :8000
|
||||||
|
└─ Basic Auth C ─► MCP Gateway :8300
|
||||||
|
```
|
||||||
|
|
||||||
|
Há um detalhe importante: no pacote atual, a autenticação Basic já funciona para chamadas **de entrada**, mas os clientes internos ainda não enviam Basic Auth:
|
||||||
|
|
||||||
|
* `Agent Gateway → Agent Backend` não envia credencial;
|
||||||
|
* `Agent Backend → MCP Gateway` envia apenas Bearer Token.
|
||||||
|
|
||||||
|
Portanto, para testar o circuito inteiro com Basic Auth, faça os dois pequenos ajustes de código descritos abaixo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Preparar o ambiente
|
||||||
|
|
||||||
|
Considere que o ZIP foi extraído em:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci_authentication_v2_1
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie um único ambiente virtual para facilitar o teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Instale o framework e as dependências dos três componentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -U pip
|
||||||
|
|
||||||
|
pip install -e ./libs/agent_framework
|
||||||
|
|
||||||
|
pip install \
|
||||||
|
-r ./Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt \
|
||||||
|
-r ./apps/agent_gateway/requirements.txt \
|
||||||
|
-r ./apps/mcp_gateway/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirme a importação:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "from agent_framework.security import install_authentication; print('framework ok')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Criar três pares de Client ID e Secret
|
||||||
|
|
||||||
|
Use credenciais diferentes para cada trecho. Para teste local:
|
||||||
|
|
||||||
|
| Fluxo | Client ID | Secret de teste |
|
||||||
|
| ----------------------- | -------------------- | --------------------------- |
|
||||||
|
| Cliente → Agent Gateway | `tia-test` | `TiaGateway-Test-2026!` |
|
||||||
|
| Agent Gateway → Backend | `agent-gateway-test` | `GatewayBackend-Test-2026!` |
|
||||||
|
| Backend → MCP Gateway | `agent-backend-test` | `BackendMcp-Test-2026!` |
|
||||||
|
|
||||||
|
Esses valores são apenas para ambiente local. Não os reutilize em produção.
|
||||||
|
|
||||||
|
## Gerar os hashes
|
||||||
|
|
||||||
|
O script está em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
scripts/generate_secret_hash.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'TiaGateway-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'GatewayBackend-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
E:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'BackendMcp-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Você receberá três valores semelhantes a:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pbkdf2_sha256:310000:<salt>:<digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
Guarde-os temporariamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH_CLIENT_GATEWAY='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_GATEWAY_BACKEND='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_BACKEND_MCP='pbkdf2_sha256:310000:...'
|
||||||
|
```
|
||||||
|
|
||||||
|
O hash muda a cada execução porque o salt é aleatório. Isso é esperado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Configurar o Agent Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione ao final do `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: cliente/TIA -> Agent Gateway
|
||||||
|
AGENT_GATEWAY_AUTH_ENABLED=true
|
||||||
|
AGENT_GATEWAY_AUTH_MODE=basic
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_CLIENT_ID=tia-test
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_CLIENT_GATEWAY
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_REALM=agent-gateway
|
||||||
|
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
# Saída: Agent Gateway -> Agent Backend
|
||||||
|
BACKEND_AUTH_MODE=basic
|
||||||
|
BACKEND_AUTH_CLIENT_ID=agent-gateway-test
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Não coloque aspas no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo de backends já aponta o backend Contas para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
contas:
|
||||||
|
url: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/config/backends.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para este teste, mantenha apenas o backend `contas` ou force o backend no payload. Caso contrário, pedidos sobre ofertas e suporte podem ser roteados para portas em que nenhum backend está rodando.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Fazer o Agent Gateway enviar Basic Auth ao backend
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/global_supervisor/client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a classe `BackendClient` por uma versão que aceite autenticação Basic.
|
||||||
|
|
||||||
|
No início do arquivo, adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o construtor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BackendClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout_seconds: float = 120.0,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _auth(self) -> httpx.BasicAuth | None:
|
||||||
|
if self.basic_client_id and self.basic_secret:
|
||||||
|
return httpx.BasicAuth(
|
||||||
|
username=self.basic_client_id,
|
||||||
|
password=self.basic_secret,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `call_message`, troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(url, json=payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
auth=self._auth(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `health`, você pode manter `/health` público. Caso queira enviar autenticação também, use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.get(url, auth=self._auth())
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS,
|
||||||
|
basic_client_id=os.getenv("BACKEND_AUTH_CLIENT_ID"),
|
||||||
|
basic_secret=os.getenv("BACKEND_AUTH_SECRET"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso implementa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway → Agent Backend
|
||||||
|
Authorization: Basic base64(agent-gateway-test:GatewayBackend-Test-2026!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Configurar o Agent Backend autenticado
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste a seção de autenticação:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Gateway -> Agent Backend
|
||||||
|
AGENT_AUTH_ENABLED=true
|
||||||
|
AGENT_AUTH_MODE=basic
|
||||||
|
AGENT_AUTH_BASIC_CLIENT_ID=agent-gateway-test
|
||||||
|
AGENT_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_GATEWAY_BACKEND
|
||||||
|
AGENT_AUTH_BASIC_REALM=agent-contas
|
||||||
|
|
||||||
|
AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_AUTH_PUBLIC_PREFIXES=
|
||||||
|
```
|
||||||
|
|
||||||
|
Para usar o MCP Gateway:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
|
||||||
|
# Saída: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Para evitar dependências externas durante o primeiro teste, configure também:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
ENABLE_LANGFUSE=false
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
|
||||||
|
SESSION_REPOSITORY_PROVIDER=memory
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=memory
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=memory
|
||||||
|
CACHE_PROVIDER=memory
|
||||||
|
USAGE_REPOSITORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
Os nomes exatos de alguns providers podem depender do arquivo de configuração atual do framework. Caso o `.env.example` já contenha valores locais ou mock, preserve-os.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Fazer o Backend enviar Basic Auth ao MCP Gateway
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a implementação por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MCPGatewayClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
token: str | None = None,
|
||||||
|
timeout_seconds: int = 60,
|
||||||
|
auth_mode: str | None = None,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.auth_mode = (auth_mode or "").strip().lower()
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
if (
|
||||||
|
self.auth_mode == "basic"
|
||||||
|
and self.basic_client_id
|
||||||
|
and self.basic_secret
|
||||||
|
):
|
||||||
|
raw = f"{self.basic_client_id}:{self.basic_secret}".encode("utf-8")
|
||||||
|
encoded = base64.b64encode(raw).decode("ascii")
|
||||||
|
return {"Authorization": f"Basic {encoded}"}
|
||||||
|
|
||||||
|
if self.token:
|
||||||
|
return {"Authorization": f"Bearer {self.token}"}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def list_tools(self) -> dict[str, Any]:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/v1/tools",
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def invoke_tool(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
channel: str | None,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
business_context: dict[str, Any] | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": arguments or {},
|
||||||
|
"business_context": business_context or {},
|
||||||
|
"metadata": metadata or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/v1/tools/{tool_name}/invoke",
|
||||||
|
json=payload,
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/mcp/tool_router.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
auth_mode=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_AUTH_MODE",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_client_id=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_secret=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione estes campos em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/config/settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Próximo das configurações existentes de MCP Gateway:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCP_GATEWAY_AUTH_MODE: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_SECRET: str | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Há também uma factory local em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
app/mcp_gateway_client_factory.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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")
|
||||||
|
),
|
||||||
|
auth_mode=os.getenv("MCP_GATEWAY_AUTH_MODE"),
|
||||||
|
basic_client_id=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID"
|
||||||
|
),
|
||||||
|
basic_secret=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Configurar o MCP Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_ENABLED=true
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_BACKEND_MCP
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_REALM=mcp-gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PATHS=/health,/ready,/docs,/openapi.json,/redoc
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desabilitar o mecanismo Bearer legado
|
||||||
|
|
||||||
|
O MCP Gateway ainda possui um segundo mecanismo antigo, configurado dentro de:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize a seção:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso é necessário porque o novo middleware já faz a autenticação Basic. Caso o `auth_check()` legado continue habilitado, a requisição passará pelo Basic e depois será rejeitada por não possuir Bearer Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Subir os componentes
|
||||||
|
|
||||||
|
Use quatro terminais.
|
||||||
|
|
||||||
|
## Terminal 1 — MCP Servers
|
||||||
|
|
||||||
|
O MCP Gateway precisa ter pelo menos um servidor MCP disponível para demonstrar uma chamada real.
|
||||||
|
|
||||||
|
Na raiz do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba o servidor telecom:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.telecom_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8100 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Em outro terminal, caso queira também o retail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.retail_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8200 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Confira as URLs configuradas em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para execução local, devem apontar para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8100
|
||||||
|
```
|
||||||
|
|
||||||
|
e:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 2 — MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba usando `--env-file`. Isso é importante porque o middleware lê variáveis com `os.getenv()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8300 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste a saúde pública:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste um endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-backend-test:BackendMcp-Test-2026!' \
|
||||||
|
http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 3 — Agent Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
source ../../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8000 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial usada pelo Agent Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste mensagem diretamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "teste-backend-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 4 — Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8010 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8010/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial externa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Validar o circuito completo
|
||||||
|
|
||||||
|
Force o backend `contas` para evitar que o roteador selecione um backend não iniciado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8010/gateway/message \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"backend_id": "contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-circuito-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
O circuito esperado é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
curl
|
||||||
|
│ Basic tia-test
|
||||||
|
▼
|
||||||
|
Agent Gateway :8010
|
||||||
|
│ Basic agent-gateway-test
|
||||||
|
▼
|
||||||
|
Agent Backend :8000
|
||||||
|
│ Basic agent-backend-test
|
||||||
|
▼
|
||||||
|
MCP Gateway :8300
|
||||||
|
▼
|
||||||
|
MCP Server :8100 ou :8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Como comprovar cada autenticação
|
||||||
|
|
||||||
|
Faça testes negativos em cada trecho.
|
||||||
|
|
||||||
|
## Secret externo incorreto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:senha-errada' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secret do gateway para backend incorreto
|
||||||
|
|
||||||
|
Altere temporariamente no `apps/agent_gateway/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o Agent Gateway e envie uma mensagem.
|
||||||
|
|
||||||
|
O gateway deverá retornar erro de backend, normalmente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
502 Bad Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
O erro interno será originado por um:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
do Agent Backend.
|
||||||
|
|
||||||
|
## Secret do backend para MCP incorreto
|
||||||
|
|
||||||
|
Altere temporariamente:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o backend e execute uma frase que acione uma ferramenta MCP.
|
||||||
|
|
||||||
|
O backend deverá registrar falha na chamada ao MCP Gateway com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Verificação rápida de portas
|
||||||
|
|
||||||
|
No Linux ou WSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -lntp | grep -E ':8000|:8010|:8100|:8200|:8300'
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-NetTCPConnection -State Listen |
|
||||||
|
Where-Object LocalPort -in 8000,8010,8100,8200,8300 |
|
||||||
|
Sort-Object LocalPort
|
||||||
|
```
|
||||||
|
|
||||||
|
Você deverá ver:
|
||||||
|
|
||||||
|
```text
|
||||||
|
8000 Agent Backend
|
||||||
|
8010 Agent Gateway
|
||||||
|
8100 Telecom MCP Server
|
||||||
|
8200 Retail MCP Server
|
||||||
|
8300 MCP Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
O segredo original precisa existir no componente cliente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TIA ou curl:
|
||||||
|
TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Gateway:
|
||||||
|
GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Os componentes servidores armazenam apenas os hashes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway:
|
||||||
|
hash de TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
hash de GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
MCP Gateway:
|
||||||
|
hash de BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Em produção, os segredos originais e hashes devem vir de Vault ou Kubernetes Secret, não de arquivos `.env`.
|
||||||
520
Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
Normal file
520
Documentacao/Long_Term_Memory_Implementation_Guide_EN.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Long-Term Memory Implementation Guide
|
||||||
|
|
||||||
|
### Concept
|
||||||
|
|
||||||
|
Long-Term Memory (LTM) is the `agent_framework` capability that stores and retrieves durable facts beyond the lifetime of a conversation session.
|
||||||
|
|
||||||
|
Unlike message history, which is normally associated with a `session_id`, Long-Term Memory is associated with the business identity of the user or customer. In the current implementation, this identity consists of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows an agent to retrieve preferences, identity information, projects and constraints even when a new session is created.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Long-Term Memory is used to:
|
||||||
|
|
||||||
|
- maintain continuity across sessions;
|
||||||
|
- personalize responses;
|
||||||
|
- prevent users from repeating previously supplied information;
|
||||||
|
- reduce the need to send the full conversation history to the model;
|
||||||
|
- store preferences, current projects, preferred names and constraints;
|
||||||
|
- isolate memory across tenants, agents and customers.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Session A:
|
||||||
|
"Call me Cris. My preferred language is Python."
|
||||||
|
|
||||||
|
Session B, with another session_id and the same customer_key:
|
||||||
|
"What do you remember about me?"
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
"Your preferred name is Cris and your preferred language is Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory type differences
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Stores messages from the current conversation and is normally associated with the `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Stores a summary of the conversation to reduce the context size sent to the model.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Stores durable facts across sessions and is associated with the business identity, primarily the `customer_key`.
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Coordinates:
|
||||||
|
|
||||||
|
- memory loading;
|
||||||
|
- identity-based retrieval;
|
||||||
|
- context rendering;
|
||||||
|
- durable fact extraction;
|
||||||
|
- fact persistence;
|
||||||
|
- deduplication and updates.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Persistence interface used by the manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Reference implementation based on SQLite.
|
||||||
|
|
||||||
|
It is suitable for:
|
||||||
|
|
||||||
|
- local development;
|
||||||
|
- testing;
|
||||||
|
- demonstrations;
|
||||||
|
- low-scale environments.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
In-memory implementation used for quick tests.
|
||||||
|
|
||||||
|
Its content is lost when the backend process stops.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Identifies durable facts in messages.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Data model representing a persisted item, including identity, key, value, category, confidence and metadata.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Loads memory before agent execution and injects the rendered context into the prompt.
|
||||||
|
|
||||||
|
#### persist_long_term_memory node
|
||||||
|
|
||||||
|
LangGraph node responsible for persisting facts after the final response has been generated and validated.
|
||||||
|
|
||||||
|
### File structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
User message
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent prompt
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Framework configuration
|
||||||
|
|
||||||
|
### New modules
|
||||||
|
|
||||||
|
Copy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update memory/__init__.py
|
||||||
|
|
||||||
|
Export the Long-Term Memory components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update settings.py
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentRuntime integration
|
||||||
|
|
||||||
|
The runtime must:
|
||||||
|
|
||||||
|
1. verify that the feature is enabled;
|
||||||
|
2. create the manager when needed;
|
||||||
|
3. retrieve facts using the identity;
|
||||||
|
4. populate the workflow state;
|
||||||
|
5. inject the rendered context into the prompt.
|
||||||
|
|
||||||
|
State fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentWorkflow initialization
|
||||||
|
|
||||||
|
Create the manager in `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correct agent initialization
|
||||||
|
|
||||||
|
Do not pass `long_term_memory_manager` through `agent_kwargs` when the constructors of `BillingAgent`, `ProductAgent`, `OrdersAgent` and `SupportAgent` do not declare that parameter.
|
||||||
|
|
||||||
|
This initialization causes an error:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
The recommended approach is to create agents using their existing signatures and inject the manager as an attribute after initialization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach avoids changing every agent constructor and keeps the feature encapsulated in the framework.
|
||||||
|
|
||||||
|
### LangGraph configuration
|
||||||
|
|
||||||
|
Register the node:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the edges:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Final flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite database path
|
||||||
|
|
||||||
|
A relative path is resolved from the directory in which the backend is started.
|
||||||
|
|
||||||
|
To prevent different databases from being created accidentally, prefer an absolute path in development environments:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the directory before starting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
### Test 1 — Persistence
|
||||||
|
|
||||||
|
Send:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Call me Cris. My preferred language is Python and my current project is Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2 — Retrieval in another session
|
||||||
|
|
||||||
|
Use another `session_id` while keeping the same `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "What do you remember about me, my preferences and my project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Your preferred name is Cris.
|
||||||
|
Your preferred language is Python.
|
||||||
|
Your current project is Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3 — Isolation
|
||||||
|
|
||||||
|
Use another customer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "another-customer",
|
||||||
|
"message": "What is my preferred name and current project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The data associated with `11999999999` must not be returned.
|
||||||
|
|
||||||
|
### Test 4 — Frontend reset
|
||||||
|
|
||||||
|
Restart or reset the frontend and verify that it still sends the same `customer_key`.
|
||||||
|
|
||||||
|
Memory must survive a `session_id` change. Resetting the frontend does not delete the SQLite database.
|
||||||
|
|
||||||
|
### Test 5 — Backend restart
|
||||||
|
|
||||||
|
Restart Uvicorn and repeat the query.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
memory must remain available.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
memory is lost when the process stops.
|
||||||
|
|
||||||
|
### Direct SQLite verification
|
||||||
|
|
||||||
|
Find the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Open it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Query:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success criteria
|
||||||
|
|
||||||
|
The implementation is working when:
|
||||||
|
|
||||||
|
- memory is retrieved with another `session_id`;
|
||||||
|
- the same `customer_key` retrieves previous facts;
|
||||||
|
- another `customer_key` cannot access those facts;
|
||||||
|
- restarting the frontend does not erase memory;
|
||||||
|
- restarting the backend does not erase memory when using SQLite;
|
||||||
|
- the `persist_long_term_memory` node runs;
|
||||||
|
- the prompt receives `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Best practices
|
||||||
|
|
||||||
|
- Persist only durable facts.
|
||||||
|
- Do not store the complete conversation as Long-Term Memory.
|
||||||
|
- Isolate data by `tenant_id`, `agent_id` and `customer_key`.
|
||||||
|
- Do not use `session_id` as the permanent user identity.
|
||||||
|
- Persist only after final validations.
|
||||||
|
- Avoid persisting temporary tool results.
|
||||||
|
- Record telemetry for reads, writes, updates and failures.
|
||||||
|
- Define retention and deletion policies.
|
||||||
|
- Use an absolute SQLite path in environments with multiple working directories.
|
||||||
|
- Move to an enterprise database for production and high-availability environments.
|
||||||
|
|
||||||
|
### Reference implementation limitations
|
||||||
|
|
||||||
|
The current implementation uses rule-based extraction and SQLite as the reference provider.
|
||||||
|
|
||||||
|
Recommended future enhancements:
|
||||||
|
|
||||||
|
- LLM-based fact extraction;
|
||||||
|
- vector-based semantic memory;
|
||||||
|
- episodic memory;
|
||||||
|
- expiration and versioning;
|
||||||
|
- semantic deduplication;
|
||||||
|
- consent policies;
|
||||||
|
- query and deletion APIs;
|
||||||
|
- Oracle Autonomous Database provider;
|
||||||
|
- encryption and sensitive-data classification.
|
||||||
272
Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
272
Documentacao/MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Agent Platform OCI — Manual Oficial de Agent Gateway e MCP Gateway
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Este documento consolida:
|
||||||
|
- Arquitetura oficial
|
||||||
|
- Inventário dos componentes
|
||||||
|
- Procedimento completo de execução local
|
||||||
|
- MCP Gateway
|
||||||
|
- Agent Gateway
|
||||||
|
- Backend Runtime
|
||||||
|
- Frontend
|
||||||
|
- Testes E2E
|
||||||
|
- Troubleshooting
|
||||||
|
- Decisões arquiteturais
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arquitetura Oficial
|
||||||
|
|
||||||
|
Frontend (5173)
|
||||||
|
↓
|
||||||
|
Agent Gateway (9000)
|
||||||
|
↓
|
||||||
|
Agent Template Backend / Runtime (8000)
|
||||||
|
↓
|
||||||
|
MCP Gateway (8300)
|
||||||
|
↓
|
||||||
|
Telecom MCP Server (8100)
|
||||||
|
Retail MCP Server (8200)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portas Oficiais
|
||||||
|
|
||||||
|
| Componente | Porta |
|
||||||
|
|------------|--------|
|
||||||
|
| Frontend | 5173 |
|
||||||
|
| Agent Gateway | 9000 |
|
||||||
|
| Backend Runtime | 8000 |
|
||||||
|
| MCP Gateway | 8300 |
|
||||||
|
| Telecom MCP Server | 8100 |
|
||||||
|
| Retail MCP Server | 8200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Variáveis Oficiais
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ordem de Inicialização
|
||||||
|
|
||||||
|
1. Telecom MCP Server
|
||||||
|
2. Retail MCP Server
|
||||||
|
3. MCP Gateway
|
||||||
|
4. Agent Template Backend
|
||||||
|
5. Agent Gateway
|
||||||
|
6. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 1 — Telecom MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 2 — Retail MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 3 — MCP Gateway
|
||||||
|
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 4 — Agent Template Backend
|
||||||
|
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 5 — Agent Gateway
|
||||||
|
|
||||||
|
cd apps/agent_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:9000/gateway/message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 6 — Frontend
|
||||||
|
|
||||||
|
cd agent_frontend
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
http://localhost:5173
|
||||||
|
|
||||||
|
Backend URL:
|
||||||
|
|
||||||
|
http://localhost:9000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fluxo de Tools
|
||||||
|
|
||||||
|
Agent
|
||||||
|
↓
|
||||||
|
MCPToolRouter
|
||||||
|
↓
|
||||||
|
MCPGatewayClient
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Teste Integrado E2E
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
↓
|
||||||
|
Backend Runtime
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
Telecom MCP Server
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
- Agent Gateway recebe requisição
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- MCP Gateway resolve tool
|
||||||
|
- MCP Server responde
|
||||||
|
- Usuário recebe resposta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Backend chamando MCP Server direto
|
||||||
|
|
||||||
|
Confirmar:
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
|
||||||
|
## Porta incorreta
|
||||||
|
|
||||||
|
A porta oficial do MCP Gateway é:
|
||||||
|
|
||||||
|
8300
|
||||||
|
|
||||||
|
## Agent Gateway não encontra Backend
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
## MCP Gateway não encontra MCP Server
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decisões Arquiteturais Oficiais
|
||||||
|
|
||||||
|
- Agent Gateway centraliza governança
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- Runtime executa LLM
|
||||||
|
- MCP Gateway centraliza tools
|
||||||
|
- MCP Servers executam tools
|
||||||
|
- Backend usa MCP Gateway
|
||||||
|
- gateway_runtime.env.example foi removido
|
||||||
|
- MCP_GATEWAY_* fica no .env do backend
|
||||||
|
- Porta oficial MCP Gateway = 8300
|
||||||
@@ -0,0 +1,627 @@
|
|||||||
|
# Manual de Execução Local
|
||||||
|
## Agent Gateway + MCP Gateway + Agent Template Backend + Frontend
|
||||||
|
|
||||||
|
## 1. Arquitetura de execução
|
||||||
|
|
||||||
|
A arquitetura local fica assim:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend
|
||||||
|
porta 5173
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent Gateway
|
||||||
|
porta 9000
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent Template Backend / Agent Runtime
|
||||||
|
porta 8000
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MCP Gateway
|
||||||
|
porta 8300
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MCP Server / Mock Telecom MCP
|
||||||
|
porta 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
A governança de modelo, rate limit, auditoria e políticas ficam no **Agent Gateway**.
|
||||||
|
|
||||||
|
O **Agent Runtime / Agent Template Backend** continua responsável por:
|
||||||
|
|
||||||
|
- LangGraph;
|
||||||
|
- estado;
|
||||||
|
- memória;
|
||||||
|
- checkpoints;
|
||||||
|
- supervisor/router;
|
||||||
|
- guardrails;
|
||||||
|
- judges;
|
||||||
|
- chamada LLM via providers existentes;
|
||||||
|
- chamada de tools via MCP Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Portas
|
||||||
|
|
||||||
|
| Componente | Porta | URL |
|
||||||
|
|---|---:|---|
|
||||||
|
| Frontend | 5173 | `http://localhost:5173` |
|
||||||
|
| Agent Gateway | 9000 | `http://localhost:9000` |
|
||||||
|
| Agent Template Backend | 8000 | `http://localhost:8000` |
|
||||||
|
| MCP Gateway | 8300 | `http://localhost:8300` |
|
||||||
|
| MCP Server / Mock Telecom MCP | 8001 | `http://localhost:8001` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Ordem recomendada para subir
|
||||||
|
|
||||||
|
Subir nesta ordem:
|
||||||
|
|
||||||
|
1. MCP Server / Mock Telecom MCP
|
||||||
|
2. MCP Gateway
|
||||||
|
3. Agent Template Backend
|
||||||
|
4. Agent Gateway
|
||||||
|
5. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Terminal 1 — MCP Server / Mock Telecom MCP
|
||||||
|
|
||||||
|
Se estiver usando o mock incluído no overlay:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/mcp/servers/mock_telecom_mcp
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "mock_telecom_mcp"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Terminal 2 — MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar health:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar readiness:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
```
|
||||||
|
|
||||||
|
Listar tools:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8300/v1/tools | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Executar tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"ok": true,
|
||||||
|
"data": {
|
||||||
|
"invoice_id": "INV-001",
|
||||||
|
"msisdn": "11999999999",
|
||||||
|
"valor_total": 249.9,
|
||||||
|
"vencimento": "2026-06-10",
|
||||||
|
"status": "ABERTA"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Terminal 3 — Agent Template Backend / Agent Runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/templates/agent_template_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
ou, se o seu backend estiver em outra pasta:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/templates/agent_template_backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Ativar ambiente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Se ainda não existir `.venv`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar variáveis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export MCP_GATEWAY_ENABLED=true
|
||||||
|
export MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
export MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Se estiver usando OCI/OpenAI-compatible, manter também as variáveis já existentes do backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_PROVIDER=oci_openai
|
||||||
|
export OCI_GENAI_API_KEY=<sua-chave>
|
||||||
|
```
|
||||||
|
|
||||||
|
ou, para mock:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export LLM_PROVIDER=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar agentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/agents | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Testar backend direto:
|
||||||
|
|
||||||
|
```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": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Terminal 4 — Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/apps/agent_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Ativar ambiente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar variáveis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir Agent Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a rota governada de exemplo estiver registrada no `app.main`, testar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:9000/gateway/message/governed \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"payload": {
|
||||||
|
"message": "Quero consultar minha fatura",
|
||||||
|
"session_id": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"metadata": {
|
||||||
|
"operation": "agent.final_answer"
|
||||||
|
},
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
Se a rota real for `/gateway/message`, testar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:9000/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": "session-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"message_id": "msg-001",
|
||||||
|
"metadata": {
|
||||||
|
"operation": "agent.final_answer"
|
||||||
|
},
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Terminal 5 — Frontend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci/agent_frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
ou a pasta onde estiver o frontend.
|
||||||
|
|
||||||
|
Instalar dependências:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Subir:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Configurar no frontend:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Backend URL: http://localhost:9000
|
||||||
|
Agent: telecom_contas
|
||||||
|
Session ID: session-001
|
||||||
|
Customer Key: 11999999999
|
||||||
|
Contract Key: INV-001
|
||||||
|
```
|
||||||
|
|
||||||
|
O frontend deve chamar o **Agent Gateway** na porta 9000, não o MCP Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Fluxo final esperado
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend 5173
|
||||||
|
↓
|
||||||
|
Agent Gateway 9000
|
||||||
|
↓
|
||||||
|
Agent Template Backend 8000
|
||||||
|
↓
|
||||||
|
MCP Gateway 8300
|
||||||
|
↓
|
||||||
|
Mock Telecom MCP 8001
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Docker Compose para MCP Gateway + Mock MCP
|
||||||
|
|
||||||
|
Também é possível subir MCP Gateway + Mock MCP com Docker Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_platform_oci
|
||||||
|
|
||||||
|
docker compose -f deploy/docker/docker-compose.mcp-gateway.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso sobe:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP Gateway http://localhost:8300
|
||||||
|
Mock Telecom MCP http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois subir manualmente:
|
||||||
|
|
||||||
|
- Agent Template Backend na porta 8000;
|
||||||
|
- Agent Gateway na porta 9000;
|
||||||
|
- Frontend na porta 5173.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Checklist de validação
|
||||||
|
|
||||||
|
## MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Runtime
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 12. Erros comuns
|
||||||
|
|
||||||
|
## 12.1. Frontend chamando porta errada
|
||||||
|
|
||||||
|
Errado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend → http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Correto:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend → http://localhost:9000
|
||||||
|
```
|
||||||
|
|
||||||
|
Se você quiser testar sem Agent Gateway, pode apontar temporariamente para 8000. Mas no modelo final, o frontend deve usar o Agent Gateway.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.2. MCP Gateway sem MCP Server
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP server unavailable
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8001/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Se falhar, subir o mock MCP server.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.3. Tool sem BusinessContext
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"missing_business_keys": ["customer_key", "contract_key"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
enviar:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.4. Agent Gateway não encontra backend
|
||||||
|
|
||||||
|
Sintoma:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Connection refused http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Correção:
|
||||||
|
|
||||||
|
validar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
e configurar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12.5. Rota governada não registrada
|
||||||
|
|
||||||
|
Se `/gateway/message/governed` retornar 404, significa que o arquivo de exemplo ainda não foi incluído no `app.main`.
|
||||||
|
|
||||||
|
Nesse caso, use a rota real `/gateway/message` ou registre no `main.py`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.routes.governed_proxy_example import router as governed_router
|
||||||
|
|
||||||
|
app.include_router(governed_router)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 13. Variáveis consolidadas
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
```env
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 14. Resumo rápido
|
||||||
|
|
||||||
|
Em cinco terminais:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1
|
||||||
|
cd mcp/servers/mock_telecom_mcp
|
||||||
|
source .venv/bin/activate
|
||||||
|
uvicorn app:app --host 0.0.0.0 --port 8001 --reload
|
||||||
|
|
||||||
|
# Terminal 2
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
source .venv/bin/activate
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
# Terminal 3
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
source .venv/bin/activate
|
||||||
|
export MCP_GATEWAY_ENABLED=true
|
||||||
|
export MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
# Terminal 4
|
||||||
|
cd apps/agent_gateway
|
||||||
|
source .venv/bin/activate
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
# Terminal 5
|
||||||
|
cd agent_frontend
|
||||||
|
npm install
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
```
|
||||||
93
Documentacao/MCP_GATEWAY_RUNBOOK.md
Normal file
93
Documentacao/MCP_GATEWAY_RUNBOOK.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# MCP Gateway Runbook
|
||||||
|
|
||||||
|
## Arquitetura corrigida
|
||||||
|
|
||||||
|
O backend/agente não deve chamar diretamente os MCP servers finais. O fluxo correto é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend / agent_framework
|
||||||
|
-> MCP Gateway Client
|
||||||
|
-> apps/mcp_gateway
|
||||||
|
-> mcp/servers/telecom_mcp_server ou mcp/servers/retail_mcp_server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subir localmente
|
||||||
|
|
||||||
|
A partir da raiz do projeto:
|
||||||
|
|
||||||
|
### Terminal 1 - Telecom MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 2 - Retail MCP Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 3 - MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
### Terminal 4 - Backend/agente
|
||||||
|
|
||||||
|
No `.env` do backend/agente ou do runtime que usa o `agent_framework`, habilite:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testes rápidos
|
||||||
|
|
||||||
|
### Health do gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lista de tools expostas pelo gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chamada de tool via gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"arguments": {
|
||||||
|
"msisdn": "11999999999",
|
||||||
|
"invoice_id": "INV-123"
|
||||||
|
},
|
||||||
|
"business_context": {},
|
||||||
|
"metadata": {"session_id": "local-test"}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Resposta esperada: `ok: true`, `data.invoice_id`, `data.msisdn`, `metadata.server: telecom`.
|
||||||
|
|
||||||
|
## O que foi corrigido
|
||||||
|
|
||||||
|
- `apps/mcp_gateway/config/mcp_gateway.yaml` agora aponta para os MCP servers reais nas portas `8100` e `8200`.
|
||||||
|
- O MCP Gateway agora suporta o contrato legado dos MCP servers: `POST /mcp/tools/call` com `{tool_name, arguments}`.
|
||||||
|
- O `agent_framework` ganhou flags `MCP_GATEWAY_ENABLED`, `MCP_GATEWAY_URL`, `MCP_GATEWAY_TOKEN`, `MCP_GATEWAY_AGENT_ID` e `MCP_GATEWAY_TENANT_ID`.
|
||||||
|
- O `MCPToolRouter` passa a chamar o MCP Gateway quando `MCP_GATEWAY_ENABLED=true`.
|
||||||
|
- `libs/agent_framework/config/mcp_servers.yaml` foi mantido como registry lógico/fallback, não como caminho principal quando o gateway está ativo.
|
||||||
BIN
Documentacao/Manual de Roteamento Multi-Agent.docx
Normal file
BIN
Documentacao/Manual de Roteamento Multi-Agent.docx
Normal file
Binary file not shown.
BIN
Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx
Normal file
BIN
Documentacao/Manual_Desenvolvedor_AI_Agent_Framework_OCI.docx
Normal file
Binary file not shown.
BIN
Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx
Normal file
BIN
Documentacao/Manual_Integracao_MCP_Servers_Agent_Framework.docx
Normal file
Binary file not shown.
520
Documentacao/Manual_Long_Term_Memory_PT.md
Normal file
520
Documentacao/Manual_Long_Term_Memory_PT.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Manual de Implementação — Long-Term Memory
|
||||||
|
|
||||||
|
### Conceito
|
||||||
|
|
||||||
|
A Long-Term Memory (LTM) é a capacidade do `agent_framework` de armazenar e recuperar fatos duradouros além da duração de uma sessão de conversa.
|
||||||
|
|
||||||
|
Diferentemente do histórico de mensagens, que normalmente está associado a um `session_id`, a memória de longo prazo é associada à identidade de negócio do usuário ou cliente. Na implementação atual, essa identidade é composta por:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso permite que um agente recupere preferências, informações de identidade, projetos e restrições mesmo quando uma nova sessão é criada.
|
||||||
|
|
||||||
|
### Para que serve
|
||||||
|
|
||||||
|
A Long-Term Memory serve para:
|
||||||
|
|
||||||
|
- manter continuidade entre sessões;
|
||||||
|
- personalizar respostas;
|
||||||
|
- evitar que o usuário repita informações já fornecidas;
|
||||||
|
- reduzir a necessidade de enviar todo o histórico ao modelo;
|
||||||
|
- armazenar preferências, projetos atuais, nomes preferidos e restrições;
|
||||||
|
- isolar a memória entre tenants, agentes e clientes.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Sessão A:
|
||||||
|
"Me chame de Cris. Minha linguagem preferida é Python."
|
||||||
|
|
||||||
|
Sessão B, com outro session_id e o mesmo customer_key:
|
||||||
|
"O que você lembra sobre mim?"
|
||||||
|
|
||||||
|
Resposta esperada:
|
||||||
|
"Seu nome preferido é Cris e sua linguagem preferida é Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diferença entre os tipos de memória
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Mantém as mensagens da conversa atual e normalmente está associada ao `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Mantém um resumo da conversa para reduzir o tamanho do contexto enviado ao modelo.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Mantém fatos duradouros entre sessões e é associada à identidade de negócio, principalmente ao `customer_key`.
|
||||||
|
|
||||||
|
### Componentes da funcionalidade
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Responsável por coordenar:
|
||||||
|
|
||||||
|
- carregamento das memórias;
|
||||||
|
- recuperação por identidade;
|
||||||
|
- renderização do contexto;
|
||||||
|
- extração de novos fatos;
|
||||||
|
- persistência dos fatos;
|
||||||
|
- deduplicação e atualização.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Interface de persistência utilizada pelo manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação de referência baseada em SQLite.
|
||||||
|
|
||||||
|
É apropriada para:
|
||||||
|
|
||||||
|
- desenvolvimento local;
|
||||||
|
- testes;
|
||||||
|
- demonstrações;
|
||||||
|
- ambientes de baixa escala.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação em memória utilizada para testes rápidos.
|
||||||
|
|
||||||
|
O conteúdo é perdido quando o processo do backend é encerrado.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Responsável por identificar fatos duradouros nas mensagens.
|
||||||
|
|
||||||
|
Exemplos de fatos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Modelo que representa um item persistido, incluindo identidade, chave, valor, categoria, confiança e metadados.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Carrega a memória antes da execução do agente e injeta o contexto no prompt.
|
||||||
|
|
||||||
|
#### Nó persist_long_term_memory
|
||||||
|
|
||||||
|
Nó do LangGraph responsável por persistir os fatos após a geração e validação da resposta final.
|
||||||
|
|
||||||
|
### Estrutura dos arquivos
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fluxo de execução
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mensagem do usuário
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Prompt do agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuração do framework
|
||||||
|
|
||||||
|
### Novos módulos
|
||||||
|
|
||||||
|
Copie os arquivos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de memory/__init__.py
|
||||||
|
|
||||||
|
Exporte os componentes da Long-Term Memory:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de settings.py
|
||||||
|
|
||||||
|
Adicione as configurações:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integração com AgentRuntime
|
||||||
|
|
||||||
|
O runtime deve:
|
||||||
|
|
||||||
|
1. verificar se a funcionalidade está habilitada;
|
||||||
|
2. criar o manager quando necessário;
|
||||||
|
3. recuperar os fatos pela identidade;
|
||||||
|
4. preencher o estado;
|
||||||
|
5. injetar o contexto no prompt.
|
||||||
|
|
||||||
|
Campos adicionados ao estado:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização no AgentWorkflow
|
||||||
|
|
||||||
|
O manager deve ser criado no `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização correta dos agentes
|
||||||
|
|
||||||
|
O `long_term_memory_manager` não deve ser passado pelo `agent_kwargs` caso os construtores de `BillingAgent`, `ProductAgent`, `OrdersAgent` e `SupportAgent` não declarem esse parâmetro.
|
||||||
|
|
||||||
|
Esta inicialização causa erro:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Erro resultante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
A forma recomendada é criar os agentes com a assinatura já existente e injetar o manager como atributo após a inicialização:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa abordagem evita alterar os construtores de todos os agentes e mantém a funcionalidade encapsulada no framework.
|
||||||
|
|
||||||
|
### Configuração do LangGraph
|
||||||
|
|
||||||
|
Registre o nó:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o fluxo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemente o método:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fluxo final:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variáveis de ambiente
|
||||||
|
|
||||||
|
```env
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caminho do banco SQLite
|
||||||
|
|
||||||
|
O caminho relativo é resolvido a partir do diretório em que o backend é iniciado.
|
||||||
|
|
||||||
|
Para evitar que bancos diferentes sejam criados acidentalmente, prefira um caminho absoluto em ambientes de desenvolvimento:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie a pasta antes de iniciar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Como testar
|
||||||
|
|
||||||
|
### Teste 1 — Gravação
|
||||||
|
|
||||||
|
Envie:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Me chame de Cris. Minha linguagem preferida é Python e meu projeto atual se chama Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 2 — Recuperação em outra sessão
|
||||||
|
|
||||||
|
Utilize outro `session_id`, mantendo o mesmo `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "O que você lembra sobre mim, minhas preferências e meu projeto?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Seu nome preferido é Cris.
|
||||||
|
Sua linguagem preferida é Python.
|
||||||
|
Seu projeto atual se chama Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 3 — Isolamento
|
||||||
|
|
||||||
|
Utilize outro cliente:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "outro-cliente",
|
||||||
|
"message": "Qual é meu nome preferido e qual é meu projeto atual?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Os dados de `11999999999` não devem aparecer.
|
||||||
|
|
||||||
|
### Teste 4 — Reinicialização do frontend
|
||||||
|
|
||||||
|
Reinicie ou resete o frontend e confirme que ele continua enviando o mesmo `customer_key`.
|
||||||
|
|
||||||
|
A memória deve sobreviver à troca do `session_id`. O reset do frontend não apaga o SQLite.
|
||||||
|
|
||||||
|
### Teste 5 — Reinicialização do backend
|
||||||
|
|
||||||
|
Reinicie o Uvicorn e repita a consulta.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória deve continuar disponível.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória será perdida quando o processo for encerrado.
|
||||||
|
|
||||||
|
### Verificação direta no SQLite
|
||||||
|
|
||||||
|
Localize o banco:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Consulte:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critérios de sucesso
|
||||||
|
|
||||||
|
A implementação está funcionando quando:
|
||||||
|
|
||||||
|
- a memória é recuperada com outro `session_id`;
|
||||||
|
- o mesmo `customer_key` recupera os fatos anteriores;
|
||||||
|
- outro `customer_key` não acessa esses fatos;
|
||||||
|
- reiniciar o frontend não apaga a memória;
|
||||||
|
- reiniciar o backend não apaga a memória quando o provider é SQLite;
|
||||||
|
- o nó `persist_long_term_memory` é executado;
|
||||||
|
- o prompt recebe `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Boas práticas
|
||||||
|
|
||||||
|
- Persistir somente fatos duradouros.
|
||||||
|
- Não armazenar a conversa completa como Long-Term Memory.
|
||||||
|
- Isolar dados por `tenant_id`, `agent_id` e `customer_key`.
|
||||||
|
- Não utilizar `session_id` como identidade permanente do usuário.
|
||||||
|
- Persistir somente depois das validações finais.
|
||||||
|
- Evitar armazenar resultados temporários de ferramentas.
|
||||||
|
- Registrar telemetria de leitura, escrita, atualização e falha.
|
||||||
|
- Definir políticas de retenção e exclusão.
|
||||||
|
- Usar caminho absoluto para SQLite em ambientes com múltiplos diretórios de execução.
|
||||||
|
- Migrar para um banco corporativo em ambientes de produção e alta disponibilidade.
|
||||||
|
|
||||||
|
### Limitações da implementação de referência
|
||||||
|
|
||||||
|
A implementação atual utiliza extração baseada em regras e SQLite como provider de referência.
|
||||||
|
|
||||||
|
Evoluções recomendadas:
|
||||||
|
|
||||||
|
- extração de fatos com LLM;
|
||||||
|
- memória semântica com vetores;
|
||||||
|
- memória episódica;
|
||||||
|
- expiração e versionamento;
|
||||||
|
- deduplicação semântica;
|
||||||
|
- política de consentimento;
|
||||||
|
- API de consulta e exclusão;
|
||||||
|
- provider Oracle Autonomous Database;
|
||||||
|
- criptografia e classificação de dados sensíveis.
|
||||||
129
Documentacao/README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md
Normal file
129
Documentacao/README_AGENT_GATEWAY_AND_MCP_GATEWAY_EVOLUTION.md
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Agent Platform OCI — Agent Gateway + MCP Gateway Evolution
|
||||||
|
|
||||||
|
Este overlay remove o conceito de `AI Gateway` separado.
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
├── governance
|
||||||
|
├── model policies
|
||||||
|
├── rate limit
|
||||||
|
├── audit
|
||||||
|
└── evaluation hooks
|
||||||
|
↓
|
||||||
|
Agent Backend / Runtime
|
||||||
|
├── LangGraph
|
||||||
|
├── state
|
||||||
|
├── memory
|
||||||
|
├── checkpoints
|
||||||
|
└── LLM providers via profiles existentes
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Servers
|
||||||
|
```
|
||||||
|
|
||||||
|
## O que entra no Agent Gateway
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/governance/
|
||||||
|
apps/agent_gateway/app/governance_middleware.py
|
||||||
|
apps/agent_gateway/app/routes/governed_proxy_example.py
|
||||||
|
apps/agent_gateway/config/gateway_governance.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## O que entra no MCP Gateway
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/
|
||||||
|
libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py
|
||||||
|
libs/agent_framework/src/agent_framework/runtime_mcp_gateway_adapter.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Aplicar overlay
|
||||||
|
|
||||||
|
```bash
|
||||||
|
unzip agent_platform_agent_gateway_mcp_gateway_overlay.zip -d /tmp/overlay
|
||||||
|
rsync -av /tmp/overlay/ ./
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subir MCP Gateway local
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f deploy/docker/docker-compose.mcp-gateway.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Serviços:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MCP Gateway http://localhost:8300
|
||||||
|
Mock Telecom MCP http://localhost:8001
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testar MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Executar tool:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"channel": "web",
|
||||||
|
"tool_name": "consultar_fatura",
|
||||||
|
"business_context": {
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"contract_key": "INV-001",
|
||||||
|
"session_key": "session-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como plugar no Agent Gateway
|
||||||
|
|
||||||
|
No handler real do `POST /gateway/message`, antes de encaminhar ao backend/runtime:
|
||||||
|
|
||||||
|
```python
|
||||||
|
governed_body, headers = governance.prepare_backend_request(body)
|
||||||
|
```
|
||||||
|
|
||||||
|
Ao receber resposta do backend:
|
||||||
|
|
||||||
|
```python
|
||||||
|
return governance.process_backend_response(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo abaixo mostra um exemplo completo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/routes/governed_proxy_example.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variáveis do Runtime
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
```
|
||||||
|
|
||||||
|
## Importante
|
||||||
|
|
||||||
|
Não existe `apps/ai_gateway`.
|
||||||
|
|
||||||
|
A governança de modelo fica no Agent Gateway como policy/metadados.
|
||||||
|
|
||||||
|
O Runtime continua usando os LLM providers existentes, podendo ler a política enviada pelo Gateway em:
|
||||||
|
|
||||||
|
```python
|
||||||
|
state["metadata"]["model_policy"]
|
||||||
|
```
|
||||||
68
Documentacao/README_CHECKPOINT_ENTERPRISE.md
Normal file
68
Documentacao/README_CHECKPOINT_ENTERPRISE.md
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# Checkpoint Enterprise no Agent Framework OCI
|
||||||
|
|
||||||
|
Esta versão adiciona quatro capacidades ao checkpointer do LangGraph usado pelo framework:
|
||||||
|
|
||||||
|
1. **Checkpoint Integrity**: cada checkpoint é salvo dentro de um envelope com `schema_version`, `checkpoint_id`, `payload_hash` SHA-256 e `created_at`. Na leitura, o hash é recalculado. Se o payload foi truncado, alterado ou corrompido, o checkpoint é ignorado no recovery.
|
||||||
|
2. **Checkpoint Compaction**: checkpoints antigos são removidos automaticamente conforme a configuração `CHECKPOINT_COMPACT_EVERY` e `CHECKPOINT_KEEP_LAST`. Isso evita crescimento infinito da tabela `workflow_checkpoints`.
|
||||||
|
3. **Resilient Checkpointer**: gravações e leituras usam retry com backoff e jitter. A camada resiliente funciona sobre memory, SQLite e Oracle/Autonomous Database.
|
||||||
|
4. **Checkpoint Recovery**: ao recuperar o estado, o framework varre os últimos checkpoints e retorna o mais recente válido, pulando checkpoints corrompidos.
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
No `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||||
|
ENABLE_RESILIENT_CHECKPOINTER=true
|
||||||
|
ENABLE_CHECKPOINT_INTEGRITY=true
|
||||||
|
ENABLE_CHECKPOINT_COMPACTION=true
|
||||||
|
CHECKPOINT_COMPACT_EVERY=50
|
||||||
|
CHECKPOINT_KEEP_LAST=20
|
||||||
|
CHECKPOINT_RECOVERY_SCAN_LIMIT=25
|
||||||
|
CHECKPOINT_RETRY_MAX_ATTEMPTS=3
|
||||||
|
CHECKPOINT_RETRY_BASE_DELAY_SECONDS=0.05
|
||||||
|
CHECKPOINT_RETRY_MAX_DELAY_SECONDS=1.0
|
||||||
|
CHECKPOINT_RETRY_JITTER_SECONDS=0.05
|
||||||
|
```
|
||||||
|
|
||||||
|
Para produção com múltiplos pods, prefira:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=autonomous
|
||||||
|
ADB_USER=...
|
||||||
|
ADB_PASSWORD=...
|
||||||
|
ADB_DSN=...
|
||||||
|
ADB_WALLET_LOCATION=...
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso no LangGraph
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.checkpoints import create_langgraph_checkpointer
|
||||||
|
|
||||||
|
checkpointer = create_langgraph_checkpointer(settings)
|
||||||
|
graph = builder.compile(checkpointer=checkpointer)
|
||||||
|
|
||||||
|
config = {"configurable": {"thread_id": session_id}}
|
||||||
|
result = graph.invoke(input_state, config=config)
|
||||||
|
```
|
||||||
|
|
||||||
|
O `thread_id` continua sendo a chave de recuperação da conversa. Em ambiente com Load Balancer, qualquer pod consegue retomar a execução se usar o mesmo repositório persistente.
|
||||||
|
|
||||||
|
## Arquivos alterados
|
||||||
|
|
||||||
|
- `agent_framework/src/agent_framework/checkpoints/checkpoint_repository.py`
|
||||||
|
- `agent_framework/src/agent_framework/checkpoints/langgraph_saver.py`
|
||||||
|
- `agent_framework/src/agent_framework/checkpoints/__init__.py`
|
||||||
|
- `agent_framework/src/agent_framework/config/settings.py`
|
||||||
|
- `tests/unit/test_resilient_checkpointer.py`
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
O provider `memory` agora também usa o `RepositoryCheckpointSaver` quando `ENABLE_RESILIENT_CHECKPOINTER=true`. Para voltar ao `MemorySaver` puro do LangGraph em testes locais, configure:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_RESILIENT_CHECKPOINTER=false
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=memory
|
||||||
|
```
|
||||||
114
Documentacao/README_ENTERPRISE_ROUTING.md
Normal file
114
Documentacao/README_ENTERPRISE_ROUTING.md
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# AI Agent Platform — Enterprise Routing Edition
|
||||||
|
|
||||||
|
Esta versão inclui o projeto completo com:
|
||||||
|
|
||||||
|
- `agent_framework`: framework reutilizável.
|
||||||
|
- `agent_template_backend`: backend FastAPI com LangGraph, OCI Generative AI, Langfuse, guardrails, judges, supervisor e roteamento enterprise.
|
||||||
|
- `agent_frontend`: frontend web independente.
|
||||||
|
- `templates/template_telecom_billing_product`: template de exemplo para telecom com agentes de Fatura e Produto.
|
||||||
|
- `templates/template_retail_orders_support`: template de exemplo para e-commerce com agentes de Pedido e Suporte.
|
||||||
|
|
||||||
|
## Roteamento enterprise
|
||||||
|
|
||||||
|
O roteamento fica em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/routing/
|
||||||
|
```
|
||||||
|
|
||||||
|
Componentes principais:
|
||||||
|
|
||||||
|
- `models.py`: modelos `IntentDefinition`, `RouterStatePolicy`, `RouteDecision`.
|
||||||
|
- `config_loader.py`: carrega o YAML de intents e políticas.
|
||||||
|
- `enterprise_router.py`: decide o agente de destino por estado, keyword, LLM ou fallback.
|
||||||
|
|
||||||
|
O template usa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend/config/routing.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ordem de decisão
|
||||||
|
|
||||||
|
1. Estado conversacional (`state_policies`).
|
||||||
|
2. Keywords/intents configuráveis.
|
||||||
|
3. LLM Router opcional (`ENABLE_LLM_ROUTER=true`).
|
||||||
|
4. Fallback (`router.fallback_agent`).
|
||||||
|
|
||||||
|
## Como testar roteamento sem chamar o agente final
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/debug/route \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"payload": {
|
||||||
|
"text": "Minha fatura veio alta",
|
||||||
|
"user_id": "u1",
|
||||||
|
"channel_id": "browser-1",
|
||||||
|
"context": {"msisdn": "5511999999999"}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Resposta esperada:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "billing_agent",
|
||||||
|
"agent": "billing_agent",
|
||||||
|
"intent": "billing_invoice_explanation",
|
||||||
|
"method": "keyword"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como habilitar roteamento por LLM
|
||||||
|
|
||||||
|
No `.env` do backend:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=oci_openai
|
||||||
|
OCI_GENAI_API_KEY=...
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com/openai/v1
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
ENABLE_LLM_ROUTER=true
|
||||||
|
ROUTING_CONFIG_PATH=./config/routing.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como adicionar novo agente
|
||||||
|
|
||||||
|
1. Criar classe do agente em `agent_template_backend/app/agents/`.
|
||||||
|
2. Instanciar o agente em `AgentWorkflow.__init__`.
|
||||||
|
3. Adicionar node no LangGraph.
|
||||||
|
4. Adicionar a rota no `add_conditional_edges`.
|
||||||
|
5. Criar intent no `config/routing.yaml` apontando `agent: nome_do_agente`.
|
||||||
|
|
||||||
|
## Templates incluídos
|
||||||
|
|
||||||
|
### Template 1 — Telecom
|
||||||
|
|
||||||
|
Diretório:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/template_telecom_billing_product
|
||||||
|
```
|
||||||
|
|
||||||
|
Agentes:
|
||||||
|
|
||||||
|
- BillingAgent
|
||||||
|
- ProductAgent
|
||||||
|
|
||||||
|
### Template 2 — Retail/E-commerce
|
||||||
|
|
||||||
|
Diretório:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/template_retail_orders_support
|
||||||
|
```
|
||||||
|
|
||||||
|
Agentes:
|
||||||
|
|
||||||
|
- OrdersAgent
|
||||||
|
- SupportAgent
|
||||||
|
|
||||||
|
Este segundo template mostra como reutilizar a mesma arquitetura para outro domínio de negócio.
|
||||||
23
Documentacao/README_FIRST_ENTERPRISE_DELTA.md
Normal file
23
Documentacao/README_FIRST_ENTERPRISE_DELTA.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Delta Implementado para Padrão FIRST
|
||||||
|
|
||||||
|
Esta versão corrige as prioridades levantadas na comparação com o FIRST:
|
||||||
|
|
||||||
|
1. Oracle Session Repository real
|
||||||
|
2. Oracle Message History real
|
||||||
|
3. Oracle LangGraph Checkpoint Repository real
|
||||||
|
4. LangGraph Deep Telemetry
|
||||||
|
5. Token Accounting
|
||||||
|
6. Cost Accounting
|
||||||
|
7. Session Lock SSE
|
||||||
|
8. Replay Buffer SSE
|
||||||
|
9. KeepAlive SSE
|
||||||
|
10. Recovery por Last-Event-ID
|
||||||
|
11. Redis Provider e Distributed Cache
|
||||||
|
12. Oracle Vector Provider
|
||||||
|
13. Oracle Graph Provider
|
||||||
|
14. RAG Telemetry
|
||||||
|
15. Langfuse Generation Tracking
|
||||||
|
16. OpenTelemetry/Event Bus compatível
|
||||||
|
17. OCI Streaming Exporter preservado
|
||||||
|
|
||||||
|
A lógica de domínio continua genérica; o framework não copia regras específicas de cobrança do FIRST.
|
||||||
45
Documentacao/README_FIRST_ENTERPRISE_PLUS.md
Normal file
45
Documentacao/README_FIRST_ENTERPRISE_PLUS.md
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Agent Framework FIRST Enterprise Plus
|
||||||
|
|
||||||
|
Esta versão evolui o framework nos quatro blocos solicitados:
|
||||||
|
|
||||||
|
1. **Langfuse Enterprise completo**
|
||||||
|
- `Telemetry.span()` com trace/session/user/metadata/tags.
|
||||||
|
- `Telemetry.generation()` com `usage`, token/cost metadata e compatibilidade Langfuse v2/v3.
|
||||||
|
- `Telemetry.score()` para judges/avaliações.
|
||||||
|
- Eventos arbitrários são registrados como spans seguros para evitar `Unknown observation type` no Langfuse.
|
||||||
|
|
||||||
|
2. **Token/Cost Accounting completo**
|
||||||
|
- `TokenUsageCollector` suporta `prompt_tokens`, `completion_tokens`, `cached_tokens`, `reasoning_tokens` e `total_tokens`.
|
||||||
|
- Tabela de preços por modelo via `MODEL_PRICES_JSON`.
|
||||||
|
- Conversão USD→BRL via `USD_BRL_RATE`.
|
||||||
|
- Persistência em `UsageRepository` e endpoint `/debug/usage`.
|
||||||
|
|
||||||
|
3. **Redis distribuído**
|
||||||
|
- `DistributedCache`: L1 memória + L2 Redis/SQLite/Oracle.
|
||||||
|
- `RedisCache` com `redis.asyncio` quando disponível e fallback sync.
|
||||||
|
- Namespace por `CACHE_KEY_PREFIX`.
|
||||||
|
- Telemetria de cache hit/miss/set/delete.
|
||||||
|
|
||||||
|
4. **Oracle Vector + PGQL reais**
|
||||||
|
- `OracleVectorStore` usa `VECTOR_DISTANCE(..., COSINE)` e `TO_VECTOR()` no Oracle 23ai.
|
||||||
|
- Tentativa automática de criar vector index quando suportado.
|
||||||
|
- `OracleGraphStore` usa tabelas `GRAPH_NODE` e `GRAPH_EDGE`.
|
||||||
|
- Suporte a criação de Property Graph e consulta por `GRAPH_TABLE`/PGQL, com fallback SQL.
|
||||||
|
|
||||||
|
Também foi corrigido o problema de duplicação SSE por replay + fila live usando controle de `max_replayed_id` no `SSEHub.subscribe()`.
|
||||||
|
|
||||||
|
## Testes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=agent_framework/src pytest -q tests/unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado validado nesta geração:
|
||||||
|
|
||||||
|
```text
|
||||||
|
17 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Segurança
|
||||||
|
|
||||||
|
Os arquivos `.env` foram higienizados para não conter chaves reais. Configure suas credenciais localmente antes de usar OCI/Langfuse.
|
||||||
105
Documentacao/README_FIRST_MAX_OPERATIONAL_FIXES.md
Normal file
105
Documentacao/README_FIRST_MAX_OPERATIONAL_FIXES.md
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
# Ajustes operacionais finais — padrão FIRST
|
||||||
|
|
||||||
|
Esta versão corrige os gaps identificados na comparação contra o FIRST.
|
||||||
|
|
||||||
|
## Correções aplicadas
|
||||||
|
|
||||||
|
### 1. Checkpoint LangGraph operacional
|
||||||
|
|
||||||
|
O workflow não compila mais com `MemorySaver()` diretamente. Foi criado o adaptador:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/checkpoints/langgraph_saver.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ele conecta o LangGraph ao repository configurado do framework:
|
||||||
|
|
||||||
|
- `memory`
|
||||||
|
- `sqlite`
|
||||||
|
- `oracle` / `autonomous`
|
||||||
|
|
||||||
|
No workflow:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Telemetria LangGraph envolvendo a execução real
|
||||||
|
|
||||||
|
Foi adicionado wrapper de nó no workflow:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self._node("billing_agent", self.billing_agent)
|
||||||
|
```
|
||||||
|
|
||||||
|
Assim o span/evento `langgraph.node.*` envolve a execução real do nó, não apenas um bloco vazio.
|
||||||
|
|
||||||
|
Eventos emitidos:
|
||||||
|
|
||||||
|
- `langgraph.node.started`
|
||||||
|
- `langgraph.node.completed`
|
||||||
|
- `langgraph.node.failed`
|
||||||
|
- `langgraph.edge.selected`
|
||||||
|
|
||||||
|
### 3. RAG integrado aos agentes
|
||||||
|
|
||||||
|
Os agentes agora recebem `RagService` e usam o contexto recuperado no prompt:
|
||||||
|
|
||||||
|
- BillingAgent
|
||||||
|
- ProductAgent
|
||||||
|
- OrdersAgent
|
||||||
|
- SupportAgent
|
||||||
|
|
||||||
|
O RAG usa:
|
||||||
|
|
||||||
|
- `VECTOR_STORE_PROVIDER=memory|sqlite|oracle|autonomous`
|
||||||
|
- `GRAPH_STORE_PROVIDER=memory|oracle|autonomous`
|
||||||
|
- `RAG_TOP_K`
|
||||||
|
|
||||||
|
### 4. Cache integrado ao runtime dos agentes
|
||||||
|
|
||||||
|
Criado mixin:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend/app/agents/runtime.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ele adiciona:
|
||||||
|
|
||||||
|
- busca RAG padronizada;
|
||||||
|
- chave de cache para chamada LLM;
|
||||||
|
- hit/miss com telemetria;
|
||||||
|
- cache distribuído via `create_cache(settings)`.
|
||||||
|
|
||||||
|
### 5. Testes unitários
|
||||||
|
|
||||||
|
Criada pasta:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit
|
||||||
|
```
|
||||||
|
|
||||||
|
Cobertura inicial:
|
||||||
|
|
||||||
|
- cache;
|
||||||
|
- SSE;
|
||||||
|
- RAG;
|
||||||
|
- checkpoint saver;
|
||||||
|
- telemetria LangGraph;
|
||||||
|
- runtime dos agentes;
|
||||||
|
- verificação estática do workflow;
|
||||||
|
- imports principais.
|
||||||
|
|
||||||
|
Validação local executada:
|
||||||
|
|
||||||
|
```text
|
||||||
|
12 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como testar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd projeto_agent_framework_first_ready
|
||||||
|
pip install -r agent_template_backend/requirements.txt
|
||||||
|
pytest -q tests/unit
|
||||||
|
```
|
||||||
379
Documentacao/README_FIRST_READY.md
Normal file
379
Documentacao/README_FIRST_READY.md
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
# Projeto Agent Framework FIRST-ready
|
||||||
|
|
||||||
|
Esta versão mantém a arquitetura do `meu_projeto_agent_framework` e adiciona os padrões operacionais encontrados no projeto FIRST.
|
||||||
|
|
||||||
|
## Recursos adicionados
|
||||||
|
|
||||||
|
1. **SSE no padrão FIRST**
|
||||||
|
- `GET /gateway/events/{session_id}` para stream `text/event-stream`.
|
||||||
|
- `POST /gateway/message/sse` para processar mensagem emitindo eventos SSE.
|
||||||
|
- Eventos: `connected`, `flow.start`, `session.upserted`, `message.received`, `workflow.started`, `workflow.completed`, `message.responded`, `flow.end`.
|
||||||
|
- Keepalive configurável por `SSE_KEEPALIVE_SECONDS`.
|
||||||
|
- Lock por sessão para evitar concorrência dentro da mesma conversa.
|
||||||
|
- Replay de eventos via `Last-Event-ID` ou query param `last_event_id`.
|
||||||
|
|
||||||
|
2. **Persistência de sessão e mensagens**
|
||||||
|
- Implementado provider `sqlite`, executável localmente.
|
||||||
|
- `SESSION_REPOSITORY_PROVIDER=sqlite`.
|
||||||
|
- `MEMORY_REPOSITORY_PROVIDER=sqlite`.
|
||||||
|
- Tabelas locais: `agent_sessions`, `agent_messages`.
|
||||||
|
- Idempotência por `message_id`.
|
||||||
|
|
||||||
|
3. **Checkpoint persistente**
|
||||||
|
- Implementado provider `sqlite` para checkpoint final do workflow.
|
||||||
|
- `CHECKPOINT_REPOSITORY_PROVIDER=sqlite`.
|
||||||
|
- Endpoint de leitura: `GET /sessions/{session_id}/checkpoint`.
|
||||||
|
|
||||||
|
4. **Histórico de mensagens**
|
||||||
|
- Endpoint: `GET /sessions/{session_id}/messages`.
|
||||||
|
- Histórico usado como memória conversacional antes de chamar o LangGraph.
|
||||||
|
|
||||||
|
5. **Cache**
|
||||||
|
- Novo módulo `agent_framework.cache.cache`.
|
||||||
|
- Suporta cache local em memória e Redis se `ENABLE_REDIS_CACHE=true`.
|
||||||
|
|
||||||
|
6. **RAG / Vector Store**
|
||||||
|
- `agent_framework.rag.vector_store` agora possui `InMemoryVectorStore`, `SQLiteVectorStore` e contrato `AutonomousVectorStore`.
|
||||||
|
- A versão SQLite usa busca lexical local para desenvolvimento.
|
||||||
|
- O contrato permite trocar por Oracle Vector Search sem alterar a camada de aplicação.
|
||||||
|
|
||||||
|
7. **Observabilidade**
|
||||||
|
- Mantém Langfuse existente.
|
||||||
|
- Acrescenta eventos de gateway/SSE/workflow com `session_id`, `agent_id`, `tenant_id`, `message_id`, rota e intenção.
|
||||||
|
|
||||||
|
## Arquitetura resultante
|
||||||
|
|
||||||
|
```text
|
||||||
|
Browser
|
||||||
|
|-- POST /gateway/message/sse
|
||||||
|
|-- GET /gateway/events/{session_id}
|
||||||
|
|
|
||||||
|
FastAPI Template Backend
|
||||||
|
|
|
||||||
|
ChannelGateway
|
||||||
|
|
|
||||||
|
SessionRepository + MessageHistory + CheckpointRepository
|
||||||
|
|
|
||||||
|
LangGraph AgentWorkflow
|
||||||
|
|
|
||||||
|
Guardrails -> Router/Supervisor -> Agent -> Output Guardrails -> Judges
|
||||||
|
|
|
||||||
|
Telemetry / Langfuse / OCI Streaming
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como rodar localmente
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_template_backend
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
pip install -e ../agent_framework
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Frontend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_frontend
|
||||||
|
python -m http.server 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Variáveis principais
|
||||||
|
|
||||||
|
```env
|
||||||
|
SESSION_REPOSITORY_PROVIDER=sqlite
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=sqlite
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||||
|
VECTOR_STORE_PROVIDER=sqlite
|
||||||
|
SQLITE_DB_PATH=./data/agent_framework.db
|
||||||
|
ENABLE_SSE=true
|
||||||
|
SSE_KEEPALIVE_SECONDS=15
|
||||||
|
ENABLE_MESSAGE_IDEMPOTENCY=true
|
||||||
|
```
|
||||||
|
|
||||||
|
## Teste via curl
|
||||||
|
|
||||||
|
Mensagem normal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"text":"teste","message":"teste","session_id":"s1","user_id":"u1","message_id":"m1"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Mensagem com SSE:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -N http://localhost:8000/gateway/events/s1
|
||||||
|
```
|
||||||
|
|
||||||
|
Em outro terminal:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message/sse \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"text":"teste","message":"teste","session_id":"s1","user_id":"u1","message_id":"m2"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Histórico:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/sessions/s1/messages
|
||||||
|
```
|
||||||
|
|
||||||
|
Checkpoint:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/sessions/s1/checkpoint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
A versão adicionada é executável localmente com SQLite. As classes `AutonomousSessionRepository`, `DatabaseMessageHistory`, `AutonomousCheckpointRepository` e `AutonomousVectorStore` mantêm o contrato para Oracle Autonomous Database, mas nesta entrega usam SQLite como backend local para permitir rodar e testar sem infraestrutura Oracle.
|
||||||
|
|
||||||
|
## Evolução de Observabilidade no padrão FIRST
|
||||||
|
|
||||||
|
Esta versão adiciona uma camada corporativa de observabilidade ao framework, mantendo os componentes reutilizáveis dentro de `agent_framework`.
|
||||||
|
|
||||||
|
### Componentes adicionados
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/observability/
|
||||||
|
├── context.py # ContextVar: request_id, session_id, user_id, tenant_id, agent_id, channel, ura_call_id, workflow_id, message_id
|
||||||
|
├── telemetry.py # Facade central: span, event, generation, rag_event, cache_event, checkpoint_event
|
||||||
|
├── event_bus.py # Event bus interno para plugar logs, SSE, OCI Streaming, Elastic, Phoenix etc.
|
||||||
|
├── otel.py # OpenTelemetry opcional via OTLP
|
||||||
|
├── workflow_events.py # workflow.started, node.started, node.completed, edge.selected, workflow.failed
|
||||||
|
├── guardrail_events.py # guardrail.<CODE>.evaluated e guardrail.<CODE>.blocked
|
||||||
|
├── judge_events.py # judge.<NAME>.evaluated
|
||||||
|
├── streaming_events.py # sse.connected, sse.keepalive, sse.event.emitted
|
||||||
|
└── decorators.py # decorator @traced para classes do framework
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correlação ponta-a-ponta
|
||||||
|
|
||||||
|
Cada chamada HTTP cria ou propaga `x-request-id` e o fluxo de mensagem vincula:
|
||||||
|
|
||||||
|
```text
|
||||||
|
request_id → tenant_id → agent_id → session_id → user_id → channel → message_id → workflow_id
|
||||||
|
```
|
||||||
|
|
||||||
|
O contexto usa `ContextVar`, portanto funciona em chamadas assíncronas, FastAPI, LangGraph e providers LLM.
|
||||||
|
|
||||||
|
### Langfuse
|
||||||
|
|
||||||
|
Ative no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_PUBLIC_KEY=pk-lf-...
|
||||||
|
LANGFUSE_SECRET_KEY=sk-lf-...
|
||||||
|
LANGFUSE_HOST=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
O framework registra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Trace de conversa
|
||||||
|
├── http.request
|
||||||
|
├── agent.gateway_message
|
||||||
|
├── workflow.langgraph.ainvoke
|
||||||
|
├── workflow.input_guardrails
|
||||||
|
│ └── guardrail.<CODE>.evaluated / blocked
|
||||||
|
├── workflow.routing_decision
|
||||||
|
├── workflow.agent.<agent>
|
||||||
|
│ └── generation.<model>
|
||||||
|
├── workflow.output_guardrails
|
||||||
|
├── workflow.judge
|
||||||
|
│ └── judge.<NAME>.evaluated
|
||||||
|
├── workflow.supervisor_review
|
||||||
|
├── workflow.persist
|
||||||
|
└── sse.event.emitted / sse.keepalive
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenTelemetry
|
||||||
|
|
||||||
|
Ative no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_OTEL=true
|
||||||
|
OTEL_SERVICE_NAME=agent-framework-template
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318/v1/traces
|
||||||
|
```
|
||||||
|
|
||||||
|
Com isso, os mesmos spans são exportados via OTLP para Elastic, Grafana Tempo, Jaeger, Collector ou outro backend compatível.
|
||||||
|
|
||||||
|
### SSE observável
|
||||||
|
|
||||||
|
O `SSEHub` agora registra eventos de:
|
||||||
|
|
||||||
|
- conexão aberta;
|
||||||
|
- replay de eventos;
|
||||||
|
- evento emitido;
|
||||||
|
- keepalive;
|
||||||
|
- lock por sessão no processamento de mensagem.
|
||||||
|
|
||||||
|
### Guardrails e Judges
|
||||||
|
|
||||||
|
Além dos eventos agregados (`guardrails.input.completed`, `judges.completed`), cada decisão individual gera telemetria própria:
|
||||||
|
|
||||||
|
```text
|
||||||
|
guardrail.MSK.evaluated
|
||||||
|
guardrail.OOS.blocked
|
||||||
|
judge.response_quality.evaluated
|
||||||
|
judge.groundedness.evaluated
|
||||||
|
```
|
||||||
|
|
||||||
|
### Extensão para outros backends
|
||||||
|
|
||||||
|
A classe `Telemetry.event_bus` permite plugar novos handlers sem alterar o workflow. Exemplo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def enviar_para_elastic(event):
|
||||||
|
...
|
||||||
|
|
||||||
|
telemetry.event_bus.subscribe(enviar_para_elastic)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Evolução FIRST Enterprise Completa
|
||||||
|
|
||||||
|
Esta versão recebeu os componentes que faltavam para aproximar o framework do padrão operacional do projeto FIRST:
|
||||||
|
|
||||||
|
### Persistência Oracle Autonomous Database
|
||||||
|
|
||||||
|
Foram adicionados providers reais Oracle:
|
||||||
|
|
||||||
|
- `OracleSessionRepository`
|
||||||
|
- `OracleMessageHistory`
|
||||||
|
- `OracleCheckpointRepository`
|
||||||
|
- `OracleCache`
|
||||||
|
- `OracleVectorStore`
|
||||||
|
- `OracleGraphStore`
|
||||||
|
- `OracleStore`
|
||||||
|
|
||||||
|
Tabelas criadas automaticamente com prefixo configurável `ADB_TABLE_PREFIX`:
|
||||||
|
|
||||||
|
- `<PREFIX>_AGENT_SESSION`
|
||||||
|
- `<PREFIX>_AGENT_MESSAGE`
|
||||||
|
- `<PREFIX>_WORKFLOW_CHECKPOINT`
|
||||||
|
- `<PREFIX>_WORKFLOW_CHECKPOINT_WRITE`
|
||||||
|
- `<PREFIX>_WORKFLOW_CHECKPOINT_BLOB`
|
||||||
|
- `<PREFIX>_SSE_EVENT`
|
||||||
|
- `<PREFIX>_CACHE_ENTRY`
|
||||||
|
- `<PREFIX>_RAG_DOCUMENT`
|
||||||
|
- `<PREFIX>_GRAPH_EDGE`
|
||||||
|
|
||||||
|
### Configuração Oracle
|
||||||
|
|
||||||
|
```env
|
||||||
|
SESSION_REPOSITORY_PROVIDER=oracle
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=oracle
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=oracle
|
||||||
|
CACHE_BACKEND_PROVIDER=oracle
|
||||||
|
VECTOR_STORE_PROVIDER=oracle
|
||||||
|
GRAPH_STORE_PROVIDER=oracle
|
||||||
|
SSE_STORE_PROVIDER=oracle
|
||||||
|
|
||||||
|
ADB_USER=ADMIN
|
||||||
|
ADB_PASSWORD=***
|
||||||
|
ADB_DSN=meu_adb_high
|
||||||
|
ADB_WALLET_LOCATION=/path/wallet
|
||||||
|
ADB_WALLET_PASSWORD=***
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
```
|
||||||
|
|
||||||
|
### SSE Enterprise
|
||||||
|
|
||||||
|
O SSE agora possui:
|
||||||
|
|
||||||
|
- lock por sessão (`SessionLockManager`)
|
||||||
|
- keepalive configurável
|
||||||
|
- replay por `Last-Event-ID`
|
||||||
|
- persistência de eventos em SQLite ou Oracle
|
||||||
|
- telemetria de conexão, replay, keepalive e desconexão
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /gateway/events/{session_id}?last_event_id=123
|
||||||
|
```
|
||||||
|
|
||||||
|
### LangGraph Deep Telemetry
|
||||||
|
|
||||||
|
Foi adicionado `LangGraphDeepTelemetry` com eventos:
|
||||||
|
|
||||||
|
- `langgraph.node.started`
|
||||||
|
- `langgraph.node.completed`
|
||||||
|
- `langgraph.node.failed`
|
||||||
|
- `langgraph.edge.selected`
|
||||||
|
|
||||||
|
Esses eventos são enviados para o Event Bus, Langfuse e OpenTelemetry quando habilitados.
|
||||||
|
|
||||||
|
### Token e Cost Accounting
|
||||||
|
|
||||||
|
Foi adicionado:
|
||||||
|
|
||||||
|
- `TokenUsageCollector`
|
||||||
|
- `CostTracker`
|
||||||
|
- cálculo de `prompt_tokens`, `completion_tokens`, `cached_tokens`, `total_tokens`
|
||||||
|
- cálculo de `cost_usd` e `cost_brl`
|
||||||
|
|
||||||
|
Configuração opcional:
|
||||||
|
|
||||||
|
```env
|
||||||
|
USD_BRL_RATE=5.0
|
||||||
|
MODEL_PRICES_JSON={"openai.gpt-4.1":{"input_per_1m":"2.00","output_per_1m":"8.00"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cache Enterprise
|
||||||
|
|
||||||
|
O cache agora é em cascata:
|
||||||
|
|
||||||
|
```text
|
||||||
|
L1: InMemory
|
||||||
|
L2: Redis, SQLite ou Oracle
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuração:
|
||||||
|
|
||||||
|
```env
|
||||||
|
ENABLE_REDIS_CACHE=true
|
||||||
|
REDIS_URL=redis://localhost:6379/0
|
||||||
|
```
|
||||||
|
|
||||||
|
ou:
|
||||||
|
|
||||||
|
```env
|
||||||
|
CACHE_BACKEND_PROVIDER=oracle
|
||||||
|
```
|
||||||
|
|
||||||
|
### RAG Oracle 23ai
|
||||||
|
|
||||||
|
Foi adicionado `OracleVectorStore`, com suporte a coluna `VECTOR` e `VECTOR_DISTANCE()` quando um embedding provider for conectado.
|
||||||
|
Sem embedding provider, mantém fallback lexical para desenvolvimento local.
|
||||||
|
|
||||||
|
Também foi adicionado `OracleGraphStore` com tabela de arestas, pronto para evoluir para PGQL/Property Graph.
|
||||||
|
|
||||||
|
### Langfuse
|
||||||
|
|
||||||
|
Cada chamada LLM agora gera `generation` com:
|
||||||
|
|
||||||
|
- input
|
||||||
|
- output
|
||||||
|
- model
|
||||||
|
- provider
|
||||||
|
- token usage
|
||||||
|
- cost metadata
|
||||||
|
|
||||||
|
Além disso, spans de workflow, guardrails, judges, RAG, cache, checkpoint, SSE e LangGraph são publicados pelo mesmo Event Bus.
|
||||||
|
|
||||||
62
Documentacao/README_GUARDRAILS_IMPLEMENTADOS.md
Normal file
62
Documentacao/README_GUARDRAILS_IMPLEMENTADOS.md
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
# Guardrails implementados no framework
|
||||||
|
|
||||||
|
Esta versão adiciona uma camada pragmática de guardrails ao `agent_framework`, inspirada na separação de rails por estágio: input, output, retrieval e execução/tool.
|
||||||
|
|
||||||
|
## Rails de input
|
||||||
|
|
||||||
|
- `MSIZE` — bloqueia mensagens excessivamente grandes.
|
||||||
|
- `MSK` — mascara CPF, CNPJ, telefone, e-mail, cartão, CEP, RG, tokens e chaves.
|
||||||
|
- `TOX` — detecta toxicidade e registra severidade sem bloquear por padrão.
|
||||||
|
- `PINJ` — detecta prompt injection e registra score.
|
||||||
|
- `JBRK` — detecta jailbreak/roleplay de burla e registra score.
|
||||||
|
- `VLOOP` — bloqueia loop conversacional repetitivo.
|
||||||
|
|
||||||
|
## Rails de output
|
||||||
|
|
||||||
|
- `PII_OUT` — mascara PII na resposta do agente.
|
||||||
|
- `CMP` — suaviza promessas absolutas e linguagem de garantia excessiva.
|
||||||
|
- `REVPREC` — bloqueia verbalização de ação operacional sem confirmação de tool.
|
||||||
|
- `GND` — sinaliza groundedness/risco quando há resposta específica sem evidência.
|
||||||
|
- `ALUC_RISK` — marca risco de alucinação para telemetria e judges.
|
||||||
|
|
||||||
|
## Rails opcionais
|
||||||
|
|
||||||
|
- `RET_REL` — valida relevância de chunks de retrieval por score mínimo.
|
||||||
|
- `TOOL_VAL` — valida ferramenta MCP/tool, argumentos obrigatórios, valores negativos e allowlist.
|
||||||
|
|
||||||
|
## Arquivos alterados
|
||||||
|
|
||||||
|
- `agent_framework/src/agent_framework/guardrails/rails.py`
|
||||||
|
- `agent_framework/src/agent_framework/guardrails/pipeline.py`
|
||||||
|
- `agent_framework/src/agent_framework/guardrails/__init__.py`
|
||||||
|
|
||||||
|
## Uso rápido
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.guardrails.pipeline import GuardrailPipeline
|
||||||
|
|
||||||
|
pipeline = GuardrailPipeline()
|
||||||
|
|
||||||
|
sanitized_input, input_decisions = await pipeline.run_input(
|
||||||
|
user_text,
|
||||||
|
{"history_texts": history_texts},
|
||||||
|
)
|
||||||
|
|
||||||
|
final_answer, output_decisions = await pipeline.run_output(
|
||||||
|
answer,
|
||||||
|
context,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Para tools/MCP:
|
||||||
|
|
||||||
|
```python
|
||||||
|
_, decisions = await pipeline.run_tool(
|
||||||
|
"cancelar_produto",
|
||||||
|
{"produto": "VAS", "valor": 0},
|
||||||
|
{
|
||||||
|
"required_args": ["produto"],
|
||||||
|
"allowed_tools": ["cancelar_produto", "consultar_fatura"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
```
|
||||||
139
Documentacao/README_MAX_OPERACIONAL.md
Normal file
139
Documentacao/README_MAX_OPERACIONAL.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Projeto Agent Framework — FIRST Operational Max
|
||||||
|
|
||||||
|
Esta versão adiciona os ajustes operacionais que faltavam para aproximar o framework do padrão FIRST em produção.
|
||||||
|
|
||||||
|
## Ajustes incluídos nesta versão
|
||||||
|
|
||||||
|
### 1. Langfuse Enterprise Adapter
|
||||||
|
Novo módulo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/observability/langfuse_enterprise.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Inclui adaptador compatível com SDKs Langfuse v2/v3 para:
|
||||||
|
|
||||||
|
- atualização de trace;
|
||||||
|
- score/avaliação de trace;
|
||||||
|
- prompt registry quando suportado pelo SDK;
|
||||||
|
- isolamento das diferenças de API do Langfuse.
|
||||||
|
|
||||||
|
### 2. Token e Cost Accounting persistente
|
||||||
|
Novo pacote:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/billing/
|
||||||
|
```
|
||||||
|
|
||||||
|
Inclui:
|
||||||
|
|
||||||
|
- `UsageRecord`
|
||||||
|
- `SQLiteUsageRepository`
|
||||||
|
- `OracleUsageRepository`
|
||||||
|
- `create_usage_repository(settings)`
|
||||||
|
|
||||||
|
O provider LLM agora registra automaticamente:
|
||||||
|
|
||||||
|
- `prompt_tokens`
|
||||||
|
- `completion_tokens`
|
||||||
|
- `cached_tokens`
|
||||||
|
- `total_tokens`
|
||||||
|
- `cost_usd`
|
||||||
|
- `cost_brl`
|
||||||
|
- `tenant_id`
|
||||||
|
- `agent_id`
|
||||||
|
- `session_id`
|
||||||
|
- `message_id`
|
||||||
|
|
||||||
|
Novo endpoint:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /debug/usage
|
||||||
|
GET /debug/usage?tenant_id=default
|
||||||
|
GET /debug/usage?session_id=<id>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. RAG Service operacional
|
||||||
|
Novo módulo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/rag/rag_service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Inclui:
|
||||||
|
|
||||||
|
- `RagService.add_documents()`
|
||||||
|
- `RagService.retrieve()`
|
||||||
|
- `RagResult.as_prompt_context()`
|
||||||
|
- telemetria de latência, quantidade de documentos, top scores e grafo.
|
||||||
|
|
||||||
|
### 4. Configuração nova
|
||||||
|
Variável adicionada:
|
||||||
|
|
||||||
|
```env
|
||||||
|
USAGE_REPOSITORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
Valores:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sqlite
|
||||||
|
oracle
|
||||||
|
autonomous
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Compatibilidade operacional local
|
||||||
|
Por padrão, a contabilização de uso usa SQLite mesmo que o restante esteja em memória. Assim é possível testar localmente sem Oracle.
|
||||||
|
|
||||||
|
## Teste rápido
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_template_backend
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste uma mensagem:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"text":"teste","user_id":"u1","session_id":"s1"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Verifique uso/custo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/debug/usage
|
||||||
|
```
|
||||||
|
|
||||||
|
## Para rodar com padrão mais próximo de produção
|
||||||
|
|
||||||
|
```env
|
||||||
|
SESSION_REPOSITORY_PROVIDER=sqlite
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=sqlite
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=sqlite
|
||||||
|
USAGE_REPOSITORY_PROVIDER=sqlite
|
||||||
|
CACHE_BACKEND_PROVIDER=sqlite
|
||||||
|
VECTOR_STORE_PROVIDER=sqlite
|
||||||
|
ENABLE_LANGFUSE=true
|
||||||
|
LANGFUSE_HOST=http://localhost:3000
|
||||||
|
LANGFUSE_PUBLIC_KEY=...
|
||||||
|
LANGFUSE_SECRET_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Para Autonomous Database:
|
||||||
|
|
||||||
|
```env
|
||||||
|
SESSION_REPOSITORY_PROVIDER=oracle
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=oracle
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=oracle
|
||||||
|
USAGE_REPOSITORY_PROVIDER=oracle
|
||||||
|
CACHE_BACKEND_PROVIDER=oracle
|
||||||
|
VECTOR_STORE_PROVIDER=oracle
|
||||||
|
GRAPH_STORE_PROVIDER=oracle
|
||||||
|
ADB_USER=...
|
||||||
|
ADB_PASSWORD=...
|
||||||
|
ADB_DSN=...
|
||||||
|
ADB_WALLET_LOCATION=...
|
||||||
|
ADB_TABLE_PREFIX=AGENTFW
|
||||||
|
```
|
||||||
88
Documentacao/README_MCP.md
Normal file
88
Documentacao/README_MCP.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# AI Agent Platform com MCP Tools
|
||||||
|
|
||||||
|
Esta versão adiciona uma camada MCP ao framework:
|
||||||
|
|
||||||
|
- `agent_framework.mcp.MCPToolRouter`
|
||||||
|
- `agent_template_backend/config/mcp_servers.yaml`
|
||||||
|
- `agent_template_backend/config/tools.yaml`
|
||||||
|
- `mcp_servers/telecom_mcp_server`
|
||||||
|
- `mcp_servers/retail_mcp_server`
|
||||||
|
|
||||||
|
## Subir localmente
|
||||||
|
|
||||||
|
Terminal 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash ./scripts/run_mcp_servers.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminal 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_template_backend
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -e ../agent_framework
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload --reload-dir app --reload-dir config --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminal 3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_frontend
|
||||||
|
python -m http.server 5173
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testes rápidos
|
||||||
|
|
||||||
|
Listar tools MCP carregadas pelo backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/debug/mcp/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Chamar tool diretamente via backend:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/debug/mcp/call/consultar_fatura \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"msisdn":"11999999999","invoice_id":"INV-001"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Roteamento Telecom + MCP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"session_id":"sess-tel-1","message":"Minha fatura veio alta","context":{"msisdn":"11999999999","invoice_id":"INV-001"}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Roteamento Retail + MCP:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"session_id":"sess-ret-1","message":"Meu pedido não chegou","context":{"order_id":"PED-1001","customer_id":"C-001"}}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
No compose, o backend usa `config/mcp_servers.docker.yaml` para apontar para `telecom-mcp` e `retail-mcp`.
|
||||||
|
|
||||||
|
## Operações read-only e transacionais
|
||||||
|
|
||||||
|
Use `config/tool_policies.yaml` no backend para classificar somente as operações que precisam de tratamento adicional. A validação é aplicada no roteador central antes do MCP Gateway/Server. O arquivo é opcional e templates antigos continuam usando as políticas já presentes em `tools.yaml`. A configuração completa e o roteiro de migração estão em [README_TOOL_POLICIES.md](README_TOOL_POLICIES.md).
|
||||||
|
|
||||||
|
|
||||||
|
## Chamadas MCP multi-item
|
||||||
|
|
||||||
|
Para operações com listas de produtos, pedidos, ativos ou outros recursos, **não implemente loops de chamadas MCP dentro do agente**. O framework possui contrato genérico para parâmetros `array/list`, pre-validation, confirmação e resultado por item (`SUCCESS`, `PARTIAL_SUCCESS`, `FAILED`).
|
||||||
|
|
||||||
|
Guia completo: [MCP Multi-item — Guia do Desenvolvedor](../docs/MCP_MULTI_ITEM_DEVELOPER_GUIDE.md).
|
||||||
|
|
||||||
|
A tool primária deve retornar `results[]` com `success` ou `ok` booleano por item. Em workflows, use preferencialmente `output[tool_name].results`.
|
||||||
108
Documentacao/README_MULTI_AGENT_ISOLATION.md
Normal file
108
Documentacao/README_MULTI_AGENT_ISOLATION.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# Multi-agent isolation
|
||||||
|
|
||||||
|
Esta versão permite subir mais de um `agent_template` no mesmo backend e chavear por `agent_id` sem misturar estado.
|
||||||
|
|
||||||
|
## O que ficou isolado
|
||||||
|
|
||||||
|
A chave lógica usada pelo backend é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id:agent_id:session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
Com isso ficam isolados:
|
||||||
|
|
||||||
|
- memória conversacional;
|
||||||
|
- checkpoints do LangGraph (`thread_id`);
|
||||||
|
- telemetria/tags;
|
||||||
|
- prompts por perfil de agente;
|
||||||
|
- configuração de guardrails por agente;
|
||||||
|
- configuração de judges por agente;
|
||||||
|
- metadados de sessão.
|
||||||
|
|
||||||
|
## Arquivo principal
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend/config/agents.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
default_agent_id: telecom_contas
|
||||||
|
agents:
|
||||||
|
- agent_id: telecom_contas
|
||||||
|
prompt_policy_path: ./config/agents/telecom_contas/prompt_policy.yaml
|
||||||
|
guardrails_config_path: ./config/agents/telecom_contas/guardrails.yaml
|
||||||
|
judges_config_path: ./config/agents/telecom_contas/judges.yaml
|
||||||
|
|
||||||
|
- agent_id: retail_orders
|
||||||
|
prompt_policy_path: ./config/agents/retail_orders/prompt_policy.yaml
|
||||||
|
guardrails_config_path: ./config/agents/retail_orders/guardrails.yaml
|
||||||
|
judges_config_path: ./config/agents/retail_orders/judges.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como escolher o agente na chamada
|
||||||
|
|
||||||
|
### Telecom
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"tenant_id": "tim",
|
||||||
|
"payload": {
|
||||||
|
"session_id": "sessao-123",
|
||||||
|
"user_id": "cliente-1",
|
||||||
|
"message": "Quero entender minha fatura",
|
||||||
|
"context": {"invoice_id": "FAT-001"}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retail
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"agent_id": "retail_orders",
|
||||||
|
"tenant_id": "loja",
|
||||||
|
"payload": {
|
||||||
|
"session_id": "sessao-123",
|
||||||
|
"user_id": "cliente-1",
|
||||||
|
"message": "Onde está meu pedido?",
|
||||||
|
"context": {"order_id": "PED-001"}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Mesmo usando o mesmo `session_id`, as conversas ficam separadas porque as chaves finais serão:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tim:telecom_contas:sessao-123
|
||||||
|
loja:retail_orders:sessao-123
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints úteis
|
||||||
|
|
||||||
|
```text
|
||||||
|
GET /agents
|
||||||
|
GET /health
|
||||||
|
POST /debug/route
|
||||||
|
POST /gateway/message
|
||||||
|
```
|
||||||
|
|
||||||
|
## Como adicionar um novo agent_template
|
||||||
|
|
||||||
|
1. Crie uma pasta em `agent_template_backend/config/agents/<novo_agent_id>/`.
|
||||||
|
2. Adicione `prompt_policy.yaml`, `guardrails.yaml` e `judges.yaml`.
|
||||||
|
3. Registre o agente em `config/agents.yaml`.
|
||||||
|
4. Chame `/gateway/message` usando `agent_id=<novo_agent_id>`.
|
||||||
|
|
||||||
|
## Observação arquitetural
|
||||||
|
|
||||||
|
O backend continua usando um único processo FastAPI e um único framework instalado, mas o estado persistido não usa mais `session_id` sozinho. Isso evita que dois agentes compartilhem memória, checkpoints ou decisões de governança acidentalmente.
|
||||||
244
Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
Normal file
244
Documentacao/README_ROUTE_STICKINESS_SEMANTICA.md
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
# Route Stickiness Semântica e Controle Global de Sessão no Agent Framework OCI
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
A route stickiness semântica evita executar novamente o Enterprise Router quando uma nova mensagem continua claramente sob responsabilidade do agente ativo. A implementação usa um perfil LLM leve e não contém regexes, listas de frases, palavras específicas de idioma ou regras conversacionais por domínio.
|
||||||
|
|
||||||
|
A funcionalidade é opcional e preserva integralmente o comportamento anterior quando desabilitada, quando não existe agente ativo, quando a confiança é baixa ou quando ocorre erro na inferência.
|
||||||
|
|
||||||
|
## Decisão arquitetural
|
||||||
|
|
||||||
|
O classificador possui uma responsabilidade transversal e restrita:
|
||||||
|
|
||||||
|
- `CONTINUE`: a mensagem continua com o agente ativo;
|
||||||
|
- `ROUTE`: a mensagem deve seguir para o Enterprise Router normal;
|
||||||
|
- `HUMAN_HANDOFF`: o usuário solicitou atendimento humano;
|
||||||
|
- `END_SESSION`: o usuário solicitou ou confirmou o encerramento do atendimento.
|
||||||
|
|
||||||
|
Ele não responde ao usuário, não escolhe outro agente, não executa ferramentas e não interpreta regras de negócio. As duas ações globais são encaminhadas para nós próprios do grafo, evitando que cada agente implemente prompts ou regras de sessão.
|
||||||
|
|
||||||
|
Fluxo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Todos os turnos com a funcionalidade habilitada
|
||||||
|
-> classificador semântico leve
|
||||||
|
CONTINUE + agente ativo -> agente ativo
|
||||||
|
ROUTE/baixa confiança/erro -> Enterprise Router
|
||||||
|
HUMAN_HANDOFF -> nó global human_handoff
|
||||||
|
END_SESSION -> nó global end_session
|
||||||
|
|
||||||
|
No primeiro turno, CONTINUE é normalizado para ROUTE porque ainda não existe agente ativo. Handoff e encerramento podem ser reconhecidos mesmo no primeiro turno.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Por que não há regras determinísticas
|
||||||
|
|
||||||
|
A interpretação de linguagem natural por regex exige manutenção contínua para novas construções, idiomas e domínios. Além disso, transfere aos times dos agentes a responsabilidade de manter flags e padrões de continuidade.
|
||||||
|
|
||||||
|
Esta implementação mantém no código apenas decisões técnicas inevitáveis:
|
||||||
|
|
||||||
|
- funcionalidade habilitada ou desabilitada;
|
||||||
|
- validação de que `CONTINUE` exige agente ativo;
|
||||||
|
- threshold de confiança;
|
||||||
|
- fallback em timeout, erro ou JSON inválido.
|
||||||
|
|
||||||
|
Não existem `DEFAULT_FOLLOWUP_PATTERNS`, regras de repetição, listas de pronomes ou keywords de continuidade.
|
||||||
|
|
||||||
|
## Configuração
|
||||||
|
|
||||||
|
### `.env`
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=Vou encaminhar seu atendimento para uma pessoa.
|
||||||
|
END_SESSION_MESSAGE=Atendimento encerrado. Obrigado pelo contato.
|
||||||
|
```
|
||||||
|
|
||||||
|
- `ENABLE_ROUTE_STICKINESS`: ativa a capacidade.
|
||||||
|
- `ROUTE_STICKINESS_LLM_PROFILE`: perfil existente em `llm_profiles.yaml`.
|
||||||
|
- `ROUTE_STICKINESS_CONFIDENCE_THRESHOLD`: confiança mínima para bypass.
|
||||||
|
- `ROUTE_STICKINESS_HISTORY_TURNS`: quantidade de turnos recentes enviados ao classificador.
|
||||||
|
- `ROUTE_STICKINESS_MAX_TOKENS`: limite de saída do classificador.
|
||||||
|
- `HUMAN_HANDOFF_MESSAGE`: mensagem devolvida pelo nó global de transferência humana.
|
||||||
|
- `END_SESSION_MESSAGE`: mensagem devolvida pelo nó global de encerramento.
|
||||||
|
|
||||||
|
### Perfil leve
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
route_continuity:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
O modelo acima é apenas um exemplo. Deve ser substituído pelo menor modelo aprovado e disponível no ambiente OCI. O framework reutiliza o mecanismo já existente de `LLM_PROFILES_PATH`; não há uma segunda configuração de provider/model específica para a funcionalidade.
|
||||||
|
|
||||||
|
## Contexto enviado ao modelo
|
||||||
|
|
||||||
|
O classificador recebe somente:
|
||||||
|
|
||||||
|
- agente ativo;
|
||||||
|
- descrições das capacidades dos agentes derivadas das intents já existentes;
|
||||||
|
- intent e domínio anteriores;
|
||||||
|
- histórico recente limitado;
|
||||||
|
- mensagem atual.
|
||||||
|
|
||||||
|
Não são enviados RAG completo, resultados MCP integrais, prompt do agente ou regras de negócio.
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
### Continuidade
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário: Qual é o meu plano?
|
||||||
|
Agente: Seu plano é Controle 50GB.
|
||||||
|
Usuário: O que está incluso?
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"method": "continuity",
|
||||||
|
"route": "product_agent",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mudança de domínio
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário: Qual é o meu plano?
|
||||||
|
Agente: Seu plano é Controle 50GB.
|
||||||
|
Usuário: Agora quero contestar uma cobrança.
|
||||||
|
```
|
||||||
|
|
||||||
|
O classificador retorna `ROUTE` e o Enterprise Router seleciona o agente apropriado.
|
||||||
|
|
||||||
|
### Baixa confiança ou falha
|
||||||
|
|
||||||
|
Qualquer resultado abaixo do threshold, timeout ou JSON inválido executa o Enterprise Router. A funcionalidade é fail-safe e nunca força continuidade em caso de dúvida.
|
||||||
|
|
||||||
|
## Telemetria
|
||||||
|
|
||||||
|
Evento `router.continuity`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"decision": "CONTINUE",
|
||||||
|
"confidence": 0.97,
|
||||||
|
"active_agent": "product_agent",
|
||||||
|
"route_bypassed": true,
|
||||||
|
"profile_name": "route_continuity"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Quando ocorre bypass, `route_decision.method` é `continuity` e o estado final contém:
|
||||||
|
|
||||||
|
- `active_agent`;
|
||||||
|
- `route_bypassed`;
|
||||||
|
- `continuity_signal`.
|
||||||
|
|
||||||
|
## Testes
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Os testes validam:
|
||||||
|
|
||||||
|
- continuidade com bypass;
|
||||||
|
- mudança de assunto com fallback para o router;
|
||||||
|
- baixa confiança;
|
||||||
|
- saída inválida;
|
||||||
|
- primeiro turno sem chamada ao classificador.
|
||||||
|
|
||||||
|
## Benchmark recomendado
|
||||||
|
|
||||||
|
Executar a mesma conversação com a funcionalidade desabilitada e habilitada, registrando por turno:
|
||||||
|
|
||||||
|
- `route_bypassed`;
|
||||||
|
- `route_decision.method`;
|
||||||
|
- latência do `llm.route_continuity`;
|
||||||
|
- chamadas ao `llm.router`;
|
||||||
|
- tokens por perfil;
|
||||||
|
- latência total p50, p95 e p99.
|
||||||
|
|
||||||
|
A redução de tempo total somente deve ser atribuída à stickiness quando houver `route_bypassed=true` e ausência da geração `llm.router` no mesmo turno.
|
||||||
|
|
||||||
|
|
||||||
|
## Contratos globais
|
||||||
|
|
||||||
|
### Human handoff
|
||||||
|
|
||||||
|
Quando a decisão for `HUMAN_HANDOFF`, o router retorna:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "human_handoff",
|
||||||
|
"intent": "human_handoff",
|
||||||
|
"method": "continuity",
|
||||||
|
"handoff": true,
|
||||||
|
"metadata": {
|
||||||
|
"session_control": "HUMAN_HANDOFF",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O nó `human_handoff` produz os campos:
|
||||||
|
|
||||||
|
- `session_control=HUMAN_HANDOFF`;
|
||||||
|
- `human_handoff_requested=true`;
|
||||||
|
- `session_ended=false`;
|
||||||
|
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||||
|
|
||||||
|
O evento `session.human_handoff.requested` é emitido para que o Channel Gateway ou a integração do cliente encaminhe a conversa à plataforma humana. O framework não presume uma fila, fornecedor ou protocolo específico.
|
||||||
|
|
||||||
|
### Encerramento
|
||||||
|
|
||||||
|
Quando a decisão for `END_SESSION`, o router retorna:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"route": "end_session",
|
||||||
|
"intent": "end_session",
|
||||||
|
"method": "continuity",
|
||||||
|
"metadata": {
|
||||||
|
"session_control": "END_SESSION",
|
||||||
|
"route_bypassed": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
O nó `end_session` produz:
|
||||||
|
|
||||||
|
- `session_control=END_SESSION`;
|
||||||
|
- `session_ended=true`;
|
||||||
|
- `human_handoff_requested=false`;
|
||||||
|
- `next_state=SESSION_ENDED`.
|
||||||
|
|
||||||
|
O evento `session.end.requested` é emitido antes da persistência. O backend continua responsável por aplicar a política concreta de expiração, fechamento ou limpeza da sessão em cada canal.
|
||||||
|
|
||||||
|
## Exemplos
|
||||||
|
|
||||||
|
| Mensagem | Contexto | Decisão esperada | Destino |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `o que está incluso?` | `product_agent` ativo | `CONTINUE` | `product_agent` |
|
||||||
|
| `agora quero contestar uma cobrança` | `product_agent` ativo | `ROUTE` | Enterprise Router |
|
||||||
|
| `quero falar com uma pessoa` | com ou sem agente ativo | `HUMAN_HANDOFF` | nó `human_handoff` |
|
||||||
|
| `obrigado, pode encerrar` | com ou sem agente ativo | `END_SESSION` | nó `end_session` |
|
||||||
|
|
||||||
|
## Segurança e fallback
|
||||||
|
|
||||||
|
- Somente decisões acima do threshold são aceitas.
|
||||||
|
- `CONTINUE` sem agente ativo vira `ROUTE`.
|
||||||
|
- JSON inválido, timeout ou erro usa o Enterprise Router.
|
||||||
|
- Handoff e encerramento não executam agentes de domínio nem ferramentas MCP.
|
||||||
|
- O classificador não encerra fisicamente conexões nem seleciona filas humanas; ele emite um contrato global para integração.
|
||||||
281
Documentacao/README_ROUTING_MODES.md
Normal file
281
Documentacao/README_ROUTING_MODES.md
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
# Modos de roteamento multi-agent: Enterprise Router e Supervisor
|
||||||
|
|
||||||
|
Este projeto suporta dois desenhos arquiteturais para roteamento entre agentes, sem precisar criar dois frameworks diferentes.
|
||||||
|
|
||||||
|
## Modos disponíveis
|
||||||
|
|
||||||
|
Configure por variável de ambiente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROUTING_MODE=router
|
||||||
|
```
|
||||||
|
|
||||||
|
ou:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROUTING_MODE=supervisor
|
||||||
|
```
|
||||||
|
|
||||||
|
Também existe a chave documental em `agent_template_backend/config/routing.yaml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
router:
|
||||||
|
mode: router
|
||||||
|
```
|
||||||
|
|
||||||
|
A variável de ambiente `ROUTING_MODE` é a forma recomendada para ativar um modo em runtime, especialmente em Docker, Kubernetes ou OCI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opção 1: Enterprise Router
|
||||||
|
|
||||||
|
Fluxo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário
|
||||||
|
-> Input Guardrails
|
||||||
|
-> EnterpriseRouter
|
||||||
|
-> AgentRegistry
|
||||||
|
-> 1 agente especialista
|
||||||
|
-> Output Guardrails
|
||||||
|
-> Judges
|
||||||
|
-> Supervisor Review
|
||||||
|
-> Persistência/eventos
|
||||||
|
```
|
||||||
|
|
||||||
|
Uso recomendado quando cada mensagem deve ser atendida por um único agente especialista.
|
||||||
|
|
||||||
|
Exemplos:
|
||||||
|
|
||||||
|
- `Minha fatura veio alta` -> `billing_agent`
|
||||||
|
- `Onde está meu pedido?` -> `orders_agent`
|
||||||
|
- `Quero trocar um produto com defeito` -> `support_agent`
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- Menor latência.
|
||||||
|
- Menor custo de tokens.
|
||||||
|
- Debug mais simples.
|
||||||
|
- Mais fácil de operar em produção.
|
||||||
|
|
||||||
|
Limitação:
|
||||||
|
|
||||||
|
- Uma mensagem com múltiplos assuntos precisa ser roteada para um agente principal ou tratada por handoff.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Opção 2: Supervisor
|
||||||
|
|
||||||
|
Fluxo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usuário
|
||||||
|
-> Input Guardrails
|
||||||
|
-> Supervisor.route_plan
|
||||||
|
-> supervisor_agent
|
||||||
|
-> billing_agent opcional
|
||||||
|
-> orders_agent opcional
|
||||||
|
-> product_agent opcional
|
||||||
|
-> support_agent opcional
|
||||||
|
-> Consolidação
|
||||||
|
-> Output Guardrails
|
||||||
|
-> Judges
|
||||||
|
-> Supervisor Review
|
||||||
|
-> Persistência/eventos
|
||||||
|
```
|
||||||
|
|
||||||
|
Uso recomendado quando uma única mensagem pode envolver vários agentes.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Meu pedido não chegou e também fui cobrado duas vezes.
|
||||||
|
```
|
||||||
|
|
||||||
|
Neste caso, o supervisor pode acionar:
|
||||||
|
|
||||||
|
- `orders_agent`
|
||||||
|
- `billing_agent`
|
||||||
|
|
||||||
|
Vantagens:
|
||||||
|
|
||||||
|
- Suporta múltiplas intenções na mesma mensagem.
|
||||||
|
- Permite consolidação de respostas.
|
||||||
|
- Facilita cenários enterprise com vários domínios.
|
||||||
|
|
||||||
|
Custos:
|
||||||
|
|
||||||
|
- Maior latência.
|
||||||
|
- Maior consumo de tokens.
|
||||||
|
- Mais complexidade operacional.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## O que foi alterado no código
|
||||||
|
|
||||||
|
### 1. Configuração
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/config/settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Foi adicionada a configuração:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ROUTING_MODE: Literal['router','supervisor'] = 'router'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Workflow LangGraph
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_template_backend/app/workflows/agent_graph.py
|
||||||
|
```
|
||||||
|
|
||||||
|
O nó `enterprise_route` foi substituído por um nó genérico:
|
||||||
|
|
||||||
|
```text
|
||||||
|
routing_decision
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse nó decide o caminho com base em `ROUTING_MODE`:
|
||||||
|
|
||||||
|
- `router` usa `EnterpriseRouter`.
|
||||||
|
- `supervisor` usa `Supervisor.route_plan`.
|
||||||
|
|
||||||
|
Também foi adicionado o nó:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_agent
|
||||||
|
```
|
||||||
|
|
||||||
|
Ele executa um ou mais agentes e consolida o resultado.
|
||||||
|
|
||||||
|
### 3. Supervisor
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
agent_framework/src/agent_framework/supervisor/supervisor.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Foi adicionada a estrutura:
|
||||||
|
|
||||||
|
```python
|
||||||
|
SupervisorPlan
|
||||||
|
```
|
||||||
|
|
||||||
|
E o método:
|
||||||
|
|
||||||
|
```python
|
||||||
|
route_plan(state)
|
||||||
|
```
|
||||||
|
|
||||||
|
Esse método retorna uma lista de agentes a executar.
|
||||||
|
|
||||||
|
### 4. Debug
|
||||||
|
|
||||||
|
Endpoint:
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /debug/route
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora respeita `ROUTING_MODE` e permite verificar rapidamente como uma mensagem será roteada.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Como testar localmente
|
||||||
|
|
||||||
|
### Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_template_backend
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -U pip setuptools wheel
|
||||||
|
pip install -e ../agent_framework
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modo Router
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ROUTING_MODE=router
|
||||||
|
uvicorn app.main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/debug/route \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"text":"Onde está meu pedido?","session_id":"s1"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "router",
|
||||||
|
"route": "orders_agent"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modo Supervisor
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ROUTING_MODE=supervisor
|
||||||
|
uvicorn app.main:app --reload --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/debug/route \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{"channel":"web","payload":{"text":"Meu pedido atrasou e minha fatura veio duplicada","session_id":"s2"}}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mode": "supervisor",
|
||||||
|
"route": "supervisor_agent",
|
||||||
|
"agents": ["billing_agent", "orders_agent"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Isolamento
|
||||||
|
|
||||||
|
A chave lógica de isolamento permanece:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id:agent_id:session_id
|
||||||
|
```
|
||||||
|
|
||||||
|
Use essa chave para memória, sessão, checkpoint e telemetria. Em produção, recomenda-se padronizar `agent_id` por agente especialista ou por template, dependendo do nível de isolamento desejado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recomendação
|
||||||
|
|
||||||
|
Comece em produção com:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROUTING_MODE=router
|
||||||
|
```
|
||||||
|
|
||||||
|
Ative:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ROUTING_MODE=supervisor
|
||||||
|
```
|
||||||
|
|
||||||
|
quando houver necessidade real de múltiplos agentes na mesma mensagem.
|
||||||
86
Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
Normal file
86
Documentacao/README_SEMANTIC_ROUTE_STICKINESS.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Semantic Route Stickiness and Global Session Control in Agent Framework OCI
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This optional capability uses a lightweight LLM profile and no regex, phrase lists, or domain-specific language rules. It classifies each turn as:
|
||||||
|
|
||||||
|
- `CONTINUE`: keep the active agent;
|
||||||
|
- `ROUTE`: run the regular Enterprise Router;
|
||||||
|
- `HUMAN_HANDOFF`: request human assistance;
|
||||||
|
- `END_SESSION`: finish the automated session.
|
||||||
|
|
||||||
|
The classifier does not answer the user, execute tools, or implement domain rules. Human handoff and session ending are handled by global graph nodes.
|
||||||
|
|
||||||
|
## Flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
Incoming turn
|
||||||
|
-> lightweight semantic classifier
|
||||||
|
CONTINUE + active agent -> active agent
|
||||||
|
ROUTE / low confidence / error -> Enterprise Router
|
||||||
|
HUMAN_HANDOFF -> human_handoff node
|
||||||
|
END_SESSION -> end_session node
|
||||||
|
```
|
||||||
|
|
||||||
|
`CONTINUE` is converted to `ROUTE` when there is no active agent. Global session actions can be detected on the first turn.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
ENABLE_ROUTE_STICKINESS=true
|
||||||
|
ROUTE_STICKINESS_LLM_PROFILE=route_continuity
|
||||||
|
ROUTE_STICKINESS_CONFIDENCE_THRESHOLD=0.90
|
||||||
|
ROUTE_STICKINESS_HISTORY_TURNS=2
|
||||||
|
ROUTE_STICKINESS_MAX_TOKENS=80
|
||||||
|
HUMAN_HANDOFF_MESSAGE=I will transfer your request to a person.
|
||||||
|
END_SESSION_MESSAGE=The session has ended. Thank you for contacting us.
|
||||||
|
```
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
profiles:
|
||||||
|
route_continuity:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 80
|
||||||
|
timeout_seconds: 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Use the smallest approved model available in the target OCI environment.
|
||||||
|
|
||||||
|
## Human handoff contract
|
||||||
|
|
||||||
|
The router returns route `human_handoff`, intent `human_handoff`, `handoff=true`, and metadata `session_control=HUMAN_HANDOFF`. The graph node sets:
|
||||||
|
|
||||||
|
- `human_handoff_requested=true`;
|
||||||
|
- `session_ended=false`;
|
||||||
|
- `next_state=HUMAN_HANDOFF_REQUESTED`.
|
||||||
|
|
||||||
|
It emits `session.human_handoff.requested`. The customer integration remains responsible for choosing the human queue and protocol.
|
||||||
|
|
||||||
|
## End-session contract
|
||||||
|
|
||||||
|
The router returns route `end_session`, intent `end_session`, and metadata `session_control=END_SESSION`. The graph node sets:
|
||||||
|
|
||||||
|
- `session_ended=true`;
|
||||||
|
- `human_handoff_requested=false`;
|
||||||
|
- `next_state=SESSION_ENDED`.
|
||||||
|
|
||||||
|
It emits `session.end.requested`. Channel-specific session expiration or connection closing remains an integration responsibility.
|
||||||
|
|
||||||
|
## Safety behavior
|
||||||
|
|
||||||
|
- Only decisions above the configured confidence threshold are accepted.
|
||||||
|
- Invalid JSON, timeout, low confidence, or errors fall back to the Enterprise Router.
|
||||||
|
- Human handoff and session ending do not execute domain agents or MCP tools.
|
||||||
|
- The classifier never selects a human queue and never physically closes a channel connection.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The suite covers CONTINUE, ROUTE, low confidence, invalid output, HUMAN_HANDOFF, END_SESSION, first-turn global actions, and CONTINUE without an active agent.
|
||||||
86
Documentacao/README_TEMPLATE_BUSINESS_CONTEXT_V2.md
Normal file
86
Documentacao/README_TEMPLATE_BUSINESS_CONTEXT_V2.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# Template Backend/Frontend alinhado ao BusinessContext v2
|
||||||
|
|
||||||
|
Este pacote atualiza o `agent_template_backend` e o `agent_frontend` para refletir o framework novo, onde as chaves vindas do canal/front-end são resolvidas uma vez como chaves canônicas e propagadas pelas camadas até o MCP Server.
|
||||||
|
|
||||||
|
## Fluxo implementado
|
||||||
|
|
||||||
|
1. O front-end envia `tenant_id`, `agent_id`, `session_id` e `business_context`.
|
||||||
|
2. O backend normaliza a mensagem via `ChannelGateway` preservando todo o payload no `context`.
|
||||||
|
3. O backend usa `IdentityResolver` com `config/identity.yaml` para gerar `BusinessContext`:
|
||||||
|
- `customer_key`
|
||||||
|
- `contract_key`
|
||||||
|
- `interaction_key`
|
||||||
|
- `account_key`
|
||||||
|
- `resource_key`
|
||||||
|
- `session_key`
|
||||||
|
4. O workflow recebe `context.business_context`.
|
||||||
|
5. Os agentes de exemplo não montam mais argumentos específicos como `msisdn`, `invoice_id` ou `order_id` diretamente.
|
||||||
|
6. O `MCPToolRouter` usa `config/mcp_parameter_mapping.yaml` para converter chaves canônicas em parâmetros reais de cada tool MCP.
|
||||||
|
|
||||||
|
## Arquivos principais ajustados
|
||||||
|
|
||||||
|
- `agent_template_backend/app/main.py`
|
||||||
|
- carrega `IdentityResolver`;
|
||||||
|
- resolve `BusinessContext` por mensagem;
|
||||||
|
- persiste as chaves na sessão/memória/metadata/SSE;
|
||||||
|
- adiciona `/debug/identity`.
|
||||||
|
|
||||||
|
- `agent_template_backend/app/agents/runtime.py`
|
||||||
|
- adiciona `_collect_mcp_context()` centralizado;
|
||||||
|
- repassa `business_context` e `original_context` para o MCP Router.
|
||||||
|
|
||||||
|
- `agent_template_backend/app/agents/*_agent.py`
|
||||||
|
- agentes passam a usar `_collect_mcp_context()` em vez de montar argumentos específicos.
|
||||||
|
|
||||||
|
- `agent_template_backend/config/identity.yaml`
|
||||||
|
- define como campos do canal/front-end alimentam as chaves canônicas.
|
||||||
|
|
||||||
|
- `agent_template_backend/config/mcp_parameter_mapping.yaml`
|
||||||
|
- define como chaves canônicas viram parâmetros reais por tool MCP.
|
||||||
|
|
||||||
|
- `agent_frontend/index.html` e `agent_frontend/app.js`
|
||||||
|
- adicionam campos de `tenant`, `agent` e chaves canônicas;
|
||||||
|
- enviam `business_context` no payload;
|
||||||
|
- mantêm aliases de domínio para compatibilidade (`msisdn`, `invoice_id`, `order_id`, etc.).
|
||||||
|
|
||||||
|
## Teste rápido
|
||||||
|
|
||||||
|
Suba backend, frontend e MCP servers. Depois teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s http://localhost:8000/health | jq
|
||||||
|
|
||||||
|
curl -s -X POST http://localhost:8000/debug/identity \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel":"web",
|
||||||
|
"tenant_id":"default",
|
||||||
|
"agent_id":"telecom_contas",
|
||||||
|
"payload":{
|
||||||
|
"message":"Minha fatura veio alta",
|
||||||
|
"session_id":"teste-001",
|
||||||
|
"msisdn":"11999999999",
|
||||||
|
"invoice_id":"3000131180",
|
||||||
|
"ura_call_id":"URA-123",
|
||||||
|
"business_context":{
|
||||||
|
"customer_key":"11999999999",
|
||||||
|
"contract_key":"3000131180",
|
||||||
|
"interaction_key":"URA-123",
|
||||||
|
"session_key":"teste-001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
|
||||||
|
curl -s -X POST http://localhost:8000/debug/mcp/call/consultar_fatura \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"business_context": {
|
||||||
|
"customer_key":"11999999999",
|
||||||
|
"contract_key":"3000131180",
|
||||||
|
"interaction_key":"URA-123",
|
||||||
|
"session_key":"teste-001"
|
||||||
|
}
|
||||||
|
}' | jq
|
||||||
|
```
|
||||||
|
|
||||||
|
No log do backend, procure por `mcp.tool.mapped`. Ele deve indicar as chaves mapeadas e `has_msisdn=true`, `has_invoice_id=true` para o domínio telecom.
|
||||||
28
Documentacao/README_TESTES_UNITARIOS.md
Normal file
28
Documentacao/README_TESTES_UNITARIOS.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Testes unitários do framework
|
||||||
|
|
||||||
|
Esta versão inclui uma pasta `tests/unit` cobrindo os componentes principais:
|
||||||
|
|
||||||
|
- cache local e distribuído;
|
||||||
|
- SSE com encode, persistência e replay;
|
||||||
|
- RAG com busca vetorial em memória;
|
||||||
|
- checkpoint saver compatível com LangGraph;
|
||||||
|
- telemetria profunda de LangGraph;
|
||||||
|
- runtime dos agentes com cache/RAG;
|
||||||
|
- verificação estática do workflow para garantir que não usa mais `MemorySaver()` diretamente.
|
||||||
|
|
||||||
|
## Como executar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd projeto_agent_framework_first_ready
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r agent_template_backend/requirements.txt
|
||||||
|
pip install pytest pytest-asyncio
|
||||||
|
pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Para rodar apenas os testes unitários:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest -q tests/unit
|
||||||
|
```
|
||||||
90
Documentacao/README_TOOL_POLICIES.md
Normal file
90
Documentacao/README_TOOL_POLICIES.md
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
# Políticas mínimas para tools MCP read-only e transacionais
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
O framework diferencia operações de consulta (`read_only`) e operações que alteram estado (`transactional`) imediatamente antes da chamada MCP. Essa classificação não substitui autorização, idempotência ou regras de negócio do servidor MCP; ela acrescenta somente a proteção conversacional mínima, especialmente confirmação explícita.
|
||||||
|
|
||||||
|
## Onde configurar
|
||||||
|
|
||||||
|
A parametrização pertence ao backend da aplicação:
|
||||||
|
|
||||||
|
```text
|
||||||
|
templates/agent_template_backend/config/tool_policies.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
A biblioteca compartilhada contém apenas o loader e a validação. O caminho é opcional:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
TOOL_POLICIES_PATH=./config/tool_policies.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exemplo
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
operation_type: read_only
|
||||||
|
require_confirmation: false
|
||||||
|
|
||||||
|
tool_policies:
|
||||||
|
consultar_plano:
|
||||||
|
operation_type: read_only
|
||||||
|
|
||||||
|
alterar_plano:
|
||||||
|
operation_type: transactional
|
||||||
|
require_confirmation: true
|
||||||
|
requires: [new_plan_id]
|
||||||
|
```
|
||||||
|
|
||||||
|
Para executar `alterar_plano`, os argumentos precisam conter `new_plan_id` e um booleano literal de confirmação:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"new_plan_id": "CONTROLE_100", "confirmed": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Também é aceito `"confirmation": true`. Strings como `"true"` não são aceitas como confirmação.
|
||||||
|
|
||||||
|
## Compatibilidade
|
||||||
|
|
||||||
|
- Se `tool_policies.yaml` não existir, o framework continua usando `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
|
||||||
|
- Tools antigas sem política continuam executando como antes.
|
||||||
|
- Uma política explícita no arquivo novo prevalece para `operation_type` e confirmação daquela tool.
|
||||||
|
- O catálogo `tools.yaml` continua sendo a fonte de endpoint, schema, habilitação e cache.
|
||||||
|
- O novo arquivo não deve ser colocado em `libs/agent_framework`, pois as decisões variam por aplicação e domínio.
|
||||||
|
|
||||||
|
## Fluxo de execução
|
||||||
|
|
||||||
|
```text
|
||||||
|
agente -> MCPToolRouter -> validação da política -> mapeamento de parâmetros -> MCP Gateway/Server
|
||||||
|
```
|
||||||
|
|
||||||
|
Uma chamada bloqueada retorna `ok=false`, `metadata.blocked_by_policy=true`, o tipo da operação e a origem da política. O servidor MCP permanece a autoridade final para autenticação, autorização, validação, idempotência e transação de negócio.
|
||||||
|
|
||||||
|
## Migração recomendada
|
||||||
|
|
||||||
|
1. Atualize a biblioteca sem criar o arquivo: o comportamento permanece legado.
|
||||||
|
2. Crie `config/tool_policies.yaml` no backend.
|
||||||
|
3. Cadastre primeiro apenas operações transacionais que exigem confirmação.
|
||||||
|
4. Teste chamadas sem confirmação, com confirmação booleana e com campos obrigatórios ausentes.
|
||||||
|
5. Remova gradualmente duplicações de confirmação de `tools.yaml` quando todos os templates consumidores já usarem a nova configuração.
|
||||||
|
|
||||||
|
|
||||||
|
## Runtime transacional mínimo (correção de amarração)
|
||||||
|
|
||||||
|
A lista `mcp_tools` do roteamento é uma **allowlist**, não uma ordem para executar todas as ferramentas. O runtime agora:
|
||||||
|
|
||||||
|
1. executa automaticamente somente ferramentas `read_only`;
|
||||||
|
2. seleciona no máximo uma ação transacional compatível com o pedido do usuário;
|
||||||
|
3. quando `require_confirmation: true`, persiste `pending_tool_call` e `transaction_status: AWAITING_CONFIRMATION`;
|
||||||
|
4. no turno de confirmação, reutiliza a mesma chamada e executa com `confirmed: true`;
|
||||||
|
5. publica no estado `available_mcp_tools`, `selected_tool_call`, `tool_policy_result`, `confirmation_required` e `confirmation_received`.
|
||||||
|
|
||||||
|
Para o cenário de exemplo, o pedido `123` (ou `PED-ENTREGUE`) retorna `ENTREGUE` no MCP Retail. Use:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Quero devolver o pedido 123 porque me arrependi da compra.
|
||||||
|
Sim, confirmo a devolução.
|
||||||
|
```
|
||||||
|
|
||||||
|
O contrato MCP foi padronizado para usar `reason` tanto no catálogo quanto no servidor FastMCP. `tool_policies.yaml` prevalece sobre os campos legados de `tools.yaml`; estes permanecem alinhados nos templates para compatibilidade.
|
||||||
113
Documentacao/README_old.md
Normal file
113
Documentacao/README_old.md
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
# AI Agent Platform — LangGraph + OCI
|
||||||
|
|
||||||
|
Monorepo com três projetos independentes:
|
||||||
|
|
||||||
|
- `agent_framework/`: biblioteca reutilizável para agentes escaláveis.
|
||||||
|
- `agent_template_backend/`: backend FastAPI usando o framework, com dois agentes, roteador, máquina de estados, sessão persistente e gateway de canais.
|
||||||
|
- `agent_frontend/`: frontend web simples e independente para conversar com o backend via gateway HTTP.
|
||||||
|
|
||||||
|
## Visão de arquitetura
|
||||||
|
|
||||||
|
```text
|
||||||
|
Frontend Web / WhatsApp / Voz / Texto
|
||||||
|
↓
|
||||||
|
Channel Gateway + Adapters
|
||||||
|
↓
|
||||||
|
SessionRepository persistente
|
||||||
|
↓
|
||||||
|
Supervisor / Router Agent
|
||||||
|
↓
|
||||||
|
LangGraph StateGraph
|
||||||
|
↓
|
||||||
|
Agent A Agent B
|
||||||
|
↓ ↓
|
||||||
|
Guardrails → LLM OCI Generative AI → Output Guardrails → Judges
|
||||||
|
↓
|
||||||
|
Memory / RAG / Vector / Graph / Telemetry / Streaming
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Quickstart local
|
||||||
|
|
||||||
|
Suba a estrutura de Langfuse, MongoDB, REDIS para seu ambiente de desenvolvimento:
|
||||||
|
|
||||||
|
Vá até o folder ./agent_framework/Infrastructure_Langfuse/, onde existe o docker-compose.yml e execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up
|
||||||
|
```
|
||||||
|
O langfuse estará em:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
http://localhost:3005
|
||||||
|
```
|
||||||
|
Crie sua Organização e seu projeto
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Será criado também um MongoDB e um REDIS, logo seu .env terá a configuração para apontar para estes recursos conteinerizados.
|
||||||
|
Você pode também apontar para um banco de dados Autonomous Oracle, basta configurar no arquivo .env.
|
||||||
|
|
||||||
|
Depois compile do Agent Framework dentro do agent_template_backend (agente template que se utiliza do Framework):
|
||||||
|
|
||||||
|
Obs: configure o arquivo .env.
|
||||||
|
|
||||||
|
Terminal 1:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
cd agent_template_backend
|
||||||
|
pip install -e ../agent_framework
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload --reload-dir app --reload-dir config --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminal 2:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci
|
||||||
|
bash ./scripts/run_mcp_servers.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Terminal 3:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci
|
||||||
|
cd agent_frontend
|
||||||
|
python -m http.server 5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra `http://localhost:5173`.
|
||||||
|
|
||||||
|
## OCI LLM
|
||||||
|
|
||||||
|
Configure no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=oci_openai
|
||||||
|
OCI_GENAI_BASE_URL=https://inference.generativeai.sa-saopaulo-1.oci.oraclecloud.com/openai/v1
|
||||||
|
OCI_GENAI_MODEL=openai.gpt-4.1
|
||||||
|
OCI_GENAI_API_KEY=...
|
||||||
|
```
|
||||||
|
|
||||||
|
Para rodar sem credenciais, use:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
```
|
||||||
|
|
||||||
|
## Estado do projeto
|
||||||
|
|
||||||
|
Este é um template de referência funcional/simulável. Conectores reais de Autonomous Database, MongoDB, Redis, Langfuse, OCI Streaming e OCI GenAI estão isolados por interfaces/adapters para facilitar evolução e deploy.
|
||||||
|
|
||||||
|
## Enterprise Routing Edition
|
||||||
|
|
||||||
|
Esta versão também possui `README_ENTERPRISE_ROUTING.md`, com detalhes sobre roteamento por estado, intents configuráveis, LLM Router opcional e dois templates de exemplo.
|
||||||
|
|
||||||
|
## Multi-agent isolation
|
||||||
|
|
||||||
|
Esta distribuição inclui suporte para múltiplos `agent_template` no mesmo backend.
|
||||||
|
Consulte `README_MULTI_AGENT_ISOLATION.md`.
|
||||||
1545
Documentacao/README_old2.md
Normal file
1545
Documentacao/README_old2.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
|||||||
|
# Release Notes — Generic Deterministic Intent Shift v15
|
||||||
|
|
||||||
|
## Problema corrigido
|
||||||
|
|
||||||
|
A Route Stickiness podia preservar a intent anterior quando a nova mensagem correspondia a uma intent configurada no `routing.yaml`, mas a frase do usuário omitia conectores curtos presentes na keyword configurada.
|
||||||
|
|
||||||
|
Exemplo real de configuração:
|
||||||
|
|
||||||
|
- keyword: `qual é o meu plano`
|
||||||
|
- mensagem: `qual o meu plano`
|
||||||
|
|
||||||
|
A classificação determinística não reconhecia a nova intent e a continuity acabava mantendo a intent anterior.
|
||||||
|
|
||||||
|
## Correção
|
||||||
|
|
||||||
|
O `EnterpriseRouter` continua usando, nesta ordem:
|
||||||
|
|
||||||
|
1. match exato;
|
||||||
|
2. sequência completa de tokens com palavras inseridas (`ordered_tokens`);
|
||||||
|
3. sequência de tokens informativos tolerando a omissão de conectores curtos presentes na keyword (`ordered_content_tokens`).
|
||||||
|
|
||||||
|
A terceira estratégia ignora, somente no lado da keyword, tokens de até dois caracteres e exige pelo menos dois tokens informativos. Não há nomes de intents, agentes, domínios ou verbos de negócio hardcoded.
|
||||||
|
|
||||||
|
Assim, a solução é dirigida integralmente pelas intents carregadas do `routing.yaml` da aplicação.
|
||||||
|
|
||||||
|
## Precedência sobre Route Stickiness
|
||||||
|
|
||||||
|
Quando o candidato determinístico encontrado é diferente da intent ativa, ele preempta a stickiness e retorna:
|
||||||
|
|
||||||
|
- `route_stickiness_preempted: true`
|
||||||
|
- `previous_agent`
|
||||||
|
- `previous_intent`
|
||||||
|
- `keyword_match_strategy`
|
||||||
|
|
||||||
|
A continuity LLM não é chamada nesse caminho.
|
||||||
|
|
||||||
|
## Casos cobertos
|
||||||
|
|
||||||
|
### Mesmo agente, nova intent
|
||||||
|
|
||||||
|
`retail_order_tracking` -> `quero cancelar meu pedido` -> `retail_order_cancel`
|
||||||
|
|
||||||
|
### Mesmo agente, tools diferentes
|
||||||
|
|
||||||
|
`contas_invoice_query` -> `qual o meu plano` -> `contas_plan_information`
|
||||||
|
|
||||||
|
Mesmo que ambas as intents usem `faturas_agent`, as tools mudam de `consultar_faturas` para `consultar_plano`.
|
||||||
25
Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
Normal file
25
Documentacao/RELEASE_NOTES_MCP_PARAMETER_EXTRACTION_FIX.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Correção da extração de parâmetros MCP
|
||||||
|
|
||||||
|
## Problema corrigido
|
||||||
|
|
||||||
|
O bloco `extract` do `mcp_parameter_mapping.yaml` existia na configuração e na
|
||||||
|
documentação, mas não era executado pelo runtime. Além disso, valores do
|
||||||
|
Business Context podiam sobrescrever argumentos explícitos, fazendo
|
||||||
|
`contract_key` substituir o `order_id` informado pelo usuário.
|
||||||
|
|
||||||
|
## Correções
|
||||||
|
|
||||||
|
- implementação da extração genérica `strategy: llm` após a escolha da tool;
|
||||||
|
- suporte preservado para `strategy: month_name_pt`;
|
||||||
|
- profile dedicado `mcp_parameter_extraction`;
|
||||||
|
- telemetria `llm.mcp_parameter_extraction`;
|
||||||
|
- `extract` deixou de ser interpretado como mapeamento simples;
|
||||||
|
- argumentos explícitos/extraídos têm precedência sobre Business Context;
|
||||||
|
- remoção de `contract_key: order_id` dos templates;
|
||||||
|
- `order_id` configurado como `string`;
|
||||||
|
- atualização das variantes em `Tuning-Performance`.
|
||||||
|
|
||||||
|
## Resultado esperado
|
||||||
|
|
||||||
|
Para a mensagem `consultar pedido 123`, a chamada MCP deve receber
|
||||||
|
`order_id=123`, mesmo quando o Business Context contém outro `contract_key`.
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# Route Stickiness — deterministic intent shift (v14)
|
||||||
|
|
||||||
|
## Problema corrigido
|
||||||
|
|
||||||
|
Uma keyword multi-token como `cancelar pedido` não era reconhecida em frases como `quero cancelar meu pedido`. O match legado usava substring literal; assim, a keyword genérica `pedido` podia manter `retail_order_tracking` e a continuidade reutilizava a intent anterior.
|
||||||
|
|
||||||
|
## Correção
|
||||||
|
|
||||||
|
O `EnterpriseRouter` agora possui um segundo estágio determinístico para keywords multi-token: ordered-token matching com até três tokens intermediários. Não há chamada adicional de LLM.
|
||||||
|
|
||||||
|
Exemplos reconhecidos pela keyword configurada `cancelar pedido`:
|
||||||
|
|
||||||
|
- `quero cancelar meu pedido`
|
||||||
|
- `quero cancelar o meu pedido`
|
||||||
|
- `pode cancelar esse pedido`
|
||||||
|
- `gostaria de cancelar meu pedido`
|
||||||
|
|
||||||
|
Quando esse match identifica uma intent diferente da ativa, ele preempta a route stickiness antes do LLM de continuidade.
|
||||||
|
|
||||||
|
Metadados de auditoria esperados:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"method": "keyword",
|
||||||
|
"intent": "retail_order_cancel",
|
||||||
|
"metadata": {
|
||||||
|
"matched_keyword": "cancelar pedido",
|
||||||
|
"keyword_match_strategy": "ordered_tokens",
|
||||||
|
"route_stickiness_preempted": true,
|
||||||
|
"previous_intent": "retail_order_tracking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custo de LLM
|
||||||
|
|
||||||
|
Para mudança explícita reconhecida deterministicamente, o classificador LLM de continuity não é chamado. Para mensagens sem sinal explícito, a Route Stickiness continua com o comportamento configurado.
|
||||||
|
|
||||||
|
## Regressão
|
||||||
|
|
||||||
|
Testes cobrem mudança `retail_order_tracking -> retail_order_cancel` no mesmo `orders_agent`, inclusive com palavras intermediárias. A suíte relacionada passou com 18 testes.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# Correção — mudança de consulta para ação transacional
|
||||||
|
|
||||||
|
## Problema
|
||||||
|
|
||||||
|
Após `consultar pedido 123`, a mensagem `Quero devolver o pedido 123` podia permanecer no `orders_agent` por route stickiness. Como a intent anterior só expunha tools de consulta, o runtime executava novamente `consultar_pedido` e a resposta direta repetia o status do pedido.
|
||||||
|
|
||||||
|
## Correções
|
||||||
|
|
||||||
|
- Keywords explícitas configuradas no `routing.yaml` podem preemptar a route stickiness quando apontam para outra intent/agente.
|
||||||
|
- `retail_support_exchange_return` passa a ter prioridade maior que `retail_order_tracking` para mensagens de troca/devolução.
|
||||||
|
- Tools transacionais declaram `selection_keywords` no `tools.yaml`.
|
||||||
|
- A resposta direta read-only é bloqueada quando a mensagem contém uma ação transacional registrada, mesmo que a intent anterior ainda esteja ativa.
|
||||||
|
- A seleção da action tool usa configuração, não aliases de domínio fixos no runtime.
|
||||||
|
|
||||||
|
## Fluxo esperado
|
||||||
|
|
||||||
|
1. `consultar pedido 123` → `orders_agent` → `consultar_pedido` → resposta direta.
|
||||||
|
2. `Quero devolver o pedido 123` → preempção da stickiness → `support_agent` / `retail_support_exchange_return`.
|
||||||
|
3. `consultar_pedido` valida o pedido.
|
||||||
|
4. `solicitar_devolucao` é selecionada e, com confirmação obrigatória, gera `AWAITING_CONFIRMATION`.
|
||||||
|
5. `Sim, confirmo` executa a action tool uma única vez.
|
||||||
36
Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
Normal file
36
Documentacao/RELEASE_NOTES_TOOL_POLICIES.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Release notes - políticas read-only/transacionais
|
||||||
|
|
||||||
|
## Alterações
|
||||||
|
|
||||||
|
- Novo `ToolPolicyRegistry` opcional na biblioteca compartilhada.
|
||||||
|
- Validação central no `MCPToolRouter`, inclusive para chamadas diretas.
|
||||||
|
- Tipos mínimos `read_only` e `transactional`.
|
||||||
|
- Confirmação estrita por `confirmed: true` ou `confirmation: true`.
|
||||||
|
- Suporte opcional a campos obrigatórios por política.
|
||||||
|
- Fallback automático para `tool_type`, `requires`, `confirmation_required` e `execution_policy` de `tools.yaml`.
|
||||||
|
- `config/tool_policies.yaml` e variável `TOOL_POLICIES_PATH` nos templates principais, Day Zero e variantes de `Tuning-Performance/Normal` e `Tuning-Performance/Route_Stickness`.
|
||||||
|
- Testes unitários de política e compatibilidade adicionados em `tests/unit/test_tool_policies.py`.
|
||||||
|
|
||||||
|
## Verificações executadas
|
||||||
|
|
||||||
|
- Compilação de `libs`, `templates`, `Tuning-Performance` e `tests`: aprovada.
|
||||||
|
- Validação estrutural dos seis arquivos YAML: aprovada.
|
||||||
|
- Casos isolados do loader (política transacional, confirmação, ausência de arquivo e ausência de cadastro): aprovados.
|
||||||
|
- Renderização dos dois manuais Word atualizados: aprovada, sem cortes ou sobreposição nas páginas adicionadas.
|
||||||
|
|
||||||
|
## Limitação do ambiente de validação
|
||||||
|
|
||||||
|
A suíte `pytest` foi preparada, mas não pôde ser executada integralmente neste ambiente porque `pytest` e as dependências de runtime do projeto não estavam instalados e o acesso ao índice de pacotes expirou. Para reproduzir em um ambiente do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src:templates/agent_template_backend python -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Correção de integração backend/MCP
|
||||||
|
- `mcp_tools` passou a ser tratado como allowlist.
|
||||||
|
- Ações não são mais executadas automaticamente junto com consultas.
|
||||||
|
- Confirmação transacional é persistida e retomada no turno seguinte.
|
||||||
|
- Corrigida incompatibilidade `reason`/`motivo` no MCP Retail.
|
||||||
|
- Adicionado pedido entregue determinístico para testes (`123`).
|
||||||
|
- Removida keyword genérica `produto` da intenção Telecom para evitar colisão com devoluções Retail.
|
||||||
|
- Templates Normal e Route_Stickness em `Tuning-Performance` foram sincronizados.
|
||||||
Binary file not shown.
BIN
Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx
Normal file
BIN
Documentacao/Route_Stickiness_Semantica_Agent_Framework_OCI.docx
Normal file
Binary file not shown.
35
Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
Normal file
35
Documentacao/TEST_RESULTS_ROUTE_STICKINESS.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Test Results - Semantic Route Stickiness and Global Session Control
|
||||||
|
|
||||||
|
Date: 2026-07-31
|
||||||
|
|
||||||
|
## Command
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/unit/test_semantic_route_stickiness.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## Result
|
||||||
|
|
||||||
|
```text
|
||||||
|
9 passed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Covered scenarios
|
||||||
|
|
||||||
|
1. `CONTINUE` bypasses the Enterprise Router.
|
||||||
|
2. `ROUTE` falls back to the Enterprise Router.
|
||||||
|
3. Low-confidence `CONTINUE` falls back safely.
|
||||||
|
4. Invalid model output falls back safely.
|
||||||
|
5. With no active agent, the lightweight classifier can still detect global session actions.
|
||||||
|
6. `HUMAN_HANDOFF` returns the global `human_handoff` route and session-control metadata.
|
||||||
|
7. `END_SESSION` returns the global `end_session` route and session-control metadata.
|
||||||
|
8. Global actions work on the first turn.
|
||||||
|
9. `CONTINUE` without an active agent is normalized to `ROUTE`.
|
||||||
|
|
||||||
|
## Additional validation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m compileall -q libs/agent_framework/src templates/agent_template_backend/app
|
||||||
|
```
|
||||||
|
|
||||||
|
Compilation completed successfully.
|
||||||
27
Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
Normal file
27
Documentacao/VALIDACAO_TRANSACIONAL_BACKEND_MCP.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Validação — integração transacional Agent Template Backend / MCP
|
||||||
|
|
||||||
|
## Correções implementadas
|
||||||
|
|
||||||
|
- `mcp_tools` é tratado como allowlist, não como lista de execução automática.
|
||||||
|
- Tools `read_only` continuam disponíveis para enriquecimento de contexto.
|
||||||
|
- Somente uma tool transacional compatível com a solicitação é selecionada.
|
||||||
|
- `require_confirmation: true` cria `pending_tool_call` e `AWAITING_CONFIRMATION`.
|
||||||
|
- O turno de confirmação executa a chamada pendente com `confirmed: true`.
|
||||||
|
- O estado expõe `selected_tool_call`, `tool_policy_result`, `confirmation_required`, `confirmation_received` e `transaction_status`.
|
||||||
|
- `reason` foi padronizado entre catálogo, mapping e FastMCP Retail.
|
||||||
|
- Pedido `123` e `PED-ENTREGUE` retornam status `ENTREGUE` para testes positivos.
|
||||||
|
- A keyword genérica `produto` foi removida da intenção Telecom para não capturar devoluções Retail.
|
||||||
|
- Templates `Normal` e `Route_Stickness` em `Tuning-Performance` foram atualizados.
|
||||||
|
|
||||||
|
## Teste recomendado
|
||||||
|
|
||||||
|
1. `Quero devolver o pedido 123 porque me arrependi da compra.`
|
||||||
|
2. Esperado: `transaction_status=AWAITING_CONFIRMATION`, sem execução de `solicitar_devolucao`.
|
||||||
|
3. `Sim, confirmo a devolução.`
|
||||||
|
4. Esperado: `transaction_status=COMPLETED` e execução única de `solicitar_devolucao`.
|
||||||
|
|
||||||
|
## Resultado automatizado
|
||||||
|
|
||||||
|
```text
|
||||||
|
7 passed
|
||||||
|
```
|
||||||
38
Documentacao/docs_GLOBAL_SUPERVISOR_VALIDATION.txt
Normal file
38
Documentacao/docs_GLOBAL_SUPERVISOR_VALIDATION.txt
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
VALIDAÇÃO - GLOBAL SUPERVISOR
|
||||||
|
|
||||||
|
Alterações implementadas:
|
||||||
|
|
||||||
|
1. Framework
|
||||||
|
- agent_framework.global_supervisor.models
|
||||||
|
- agent_framework.global_supervisor.config
|
||||||
|
- agent_framework.global_supervisor.session_store
|
||||||
|
- agent_framework.global_supervisor.router
|
||||||
|
- agent_framework.global_supervisor.client
|
||||||
|
|
||||||
|
2. Novo serviço
|
||||||
|
- agent_gateway/app/main.py
|
||||||
|
- agent_gateway/app/settings.py
|
||||||
|
- agent_gateway/config/backends.yaml
|
||||||
|
- agent_gateway/README.md
|
||||||
|
- agent_gateway/Dockerfile
|
||||||
|
- agent_gateway/docs/ARQUITETURA_GLOBAL_SUPERVISOR.md
|
||||||
|
|
||||||
|
3. Docker Compose
|
||||||
|
- serviço agent-gateway adicionado na porta 8010.
|
||||||
|
|
||||||
|
Validações executadas:
|
||||||
|
|
||||||
|
- python3 -m compileall -q agent_framework/src/agent_framework/global_supervisor agent_gateway/app
|
||||||
|
Resultado: OK
|
||||||
|
|
||||||
|
- Smoke test do roteamento híbrido:
|
||||||
|
Entrada 1: "Minha fatura veio alta" -> contas
|
||||||
|
Entrada 2: "e esse valor?" na mesma session_id -> contas por active_backend
|
||||||
|
Resultado: OK
|
||||||
|
|
||||||
|
- Smoke test de import do app FastAPI:
|
||||||
|
from app.main import app, registry, router
|
||||||
|
Resultado: OK
|
||||||
|
|
||||||
|
Observação:
|
||||||
|
- O proxy SSE do gateway foi deixado como etapa futura. O endpoint /gateway/message/sse já roteia e encaminha como mensagem normal; para SSE fim-a-fim, pode-se implementar proxy de /gateway/events/{session_id} para o backend ativo.
|
||||||
5
Documentacao/docs_VALIDATION_GUARDRAILS_IC.txt
Normal file
5
Documentacao/docs_VALIDATION_GUARDRAILS_IC.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
VALIDATION REPORT - guardrails parallel fail-fast + observer IC
|
||||||
|
Date: 2026-06-03
|
||||||
|
|
||||||
|
compileall: OK
|
||||||
|
smoke-tests: OK
|
||||||
BIN
Documentacao/img.png
Normal file
BIN
Documentacao/img.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 186 KiB |
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal file
30
FIX_TRANSACTION_INTENT_LOOP.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# Correção: escape de estado transacional / mudança de intenção
|
||||||
|
|
||||||
|
Correção aplicada em 2026-08-20 para impedir que uma sessão fique presa em `COLLECTING_PARAMETERS` ou `AWAITING_CONFIRMATION` quando o usuário muda explicitamente de assunto.
|
||||||
|
|
||||||
|
## Comportamento corrigido
|
||||||
|
|
||||||
|
Antes:
|
||||||
|
|
||||||
|
1. uma transação entrava em `COLLECTING_PARAMETERS`;
|
||||||
|
2. `next_state` forçava o mesmo agente via `state_policies`;
|
||||||
|
3. toda mensagem seguinte era tratada como tentativa de preencher o parâmetro faltante;
|
||||||
|
4. uma nova intenção como `quais sao meus servicos` permanecia presa no fluxo anterior.
|
||||||
|
|
||||||
|
Agora:
|
||||||
|
|
||||||
|
- o `EnterpriseRouter` verifica mudança explícita de intenção antes de aplicar o lock de estado;
|
||||||
|
- keyword explícita tem prioridade;
|
||||||
|
- quando necessário, o LLM router pode detectar mudança com confiança >= `router.confidence_threshold`;
|
||||||
|
- a decisão recebe `metadata.transaction_interruption=intent_shift`;
|
||||||
|
- o runtime encerra a transação pendente como `CANCELLED`, limpa `next_state`, parâmetros e latches, e prossegue com a nova intent;
|
||||||
|
- cancelamentos explícitos como `cancele essa operação anterior` funcionam também durante `COLLECTING_PARAMETERS`.
|
||||||
|
|
||||||
|
## Testes adicionados
|
||||||
|
|
||||||
|
- mudança de intent durante `COLLECTING_PARAMETERS`;
|
||||||
|
- resposta curta/baixa confiança permanece na transação;
|
||||||
|
- cancelamento explícito durante coleta de parâmetros;
|
||||||
|
- limpeza do estado transacional antes de executar a nova intent.
|
||||||
|
|
||||||
|
Testes focados: 19 passed.
|
||||||
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal file
102
FIX_TRANSACTION_PARAMETER_PRECEDENCE.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Precedência transacional + extração LLM de parâmetros
|
||||||
|
|
||||||
|
Esta correção remove a extração textual hardcoded de parâmetros transacionais e faz a coleta de `policy.requires` por um extrator LLM genérico.
|
||||||
|
|
||||||
|
## Regra de precedência
|
||||||
|
|
||||||
|
Enquanto existir uma transação ativa, o framework trata o turno nesta ordem:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ACTIVE_TRANSACTION
|
||||||
|
|
|
||||||
|
+-- COLLECTING_PARAMETERS
|
||||||
|
| |
|
||||||
|
| +-- LLM tenta extrair SOMENTE os parâmetros ainda pendentes
|
||||||
|
| |
|
||||||
|
| +-- extraiu >= 1 ?
|
||||||
|
| |
|
||||||
|
| +-- SIM -> continua a transação; NÃO avalia intent_shift
|
||||||
|
| |
|
||||||
|
| +-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||||
|
|
|
||||||
|
+-- AWAITING_CONFIRMATION
|
||||||
|
|
|
||||||
|
+-- reconhece confirmação/rejeição explícita
|
||||||
|
|
|
||||||
|
+-- reconheceu ?
|
||||||
|
|
|
||||||
|
+-- SIM -> continua/cancela a transação; NÃO avalia intent_shift
|
||||||
|
|
|
||||||
|
+-- NÃO -> libera EnterpriseRouter para avaliar intent_shift
|
||||||
|
```
|
||||||
|
|
||||||
|
## TransactionParameterExtractor
|
||||||
|
|
||||||
|
Novo componente:
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py`
|
||||||
|
|
||||||
|
A extração textual dos parâmetros de negócio é feita exclusivamente por LLM. O componente recebe:
|
||||||
|
|
||||||
|
- nome da tool/transação ativa;
|
||||||
|
- parâmetros atualmente pendentes;
|
||||||
|
- argumentos já conhecidos;
|
||||||
|
- schema/tipos declarados em `tools.yaml` quando disponíveis;
|
||||||
|
- descrição da tool;
|
||||||
|
- mensagem atual do usuário.
|
||||||
|
|
||||||
|
Ele não conhece nomes de domínio como `order_id`, `reason`, `subject`, `valor`, TIM ou retail. Não há regex de entidades de negócio.
|
||||||
|
|
||||||
|
A LLM pode interpretar, por exemplo:
|
||||||
|
|
||||||
|
- `PED-1001` quando só há um parâmetro compatível pendente;
|
||||||
|
- `o pedido é PED-1001`;
|
||||||
|
- `PED-1001, desisti da compra` preenchendo dois parâmetros no mesmo turno;
|
||||||
|
- respostas com o nome do parâmetro seguido do valor;
|
||||||
|
- respostas apenas com o valor, quando semanticamente inequívocas.
|
||||||
|
|
||||||
|
Em caso de dúvida, o prompt manda retornar `null`. Uma nova solicitação não deve ser transformada em valor de parâmetro.
|
||||||
|
|
||||||
|
## Separação de responsabilidades
|
||||||
|
|
||||||
|
`tool_policies.yaml` continua sendo a fonte de verdade para `requires`.
|
||||||
|
|
||||||
|
`tools.yaml` pode fornecer tipos via `args_schema` e descrição da tool para melhorar a interpretação sem introduzir código específico de domínio.
|
||||||
|
|
||||||
|
`mcp_parameter_mapping.yaml` continua responsável pelos parâmetros auxiliares/contrato MCP. As strategies do mapper são explicitamente excluídas dos campos presentes em `policy.requires`, para não misturar extração MCP com coleta transacional.
|
||||||
|
|
||||||
|
O `EnterpriseRouter` usa o mesmo extrator LLM apenas como *probe* de precedência. Se pelo menos um parâmetro pendente for encontrado, o turno permanece no estado transacional. Os valores extraídos são colocados no metadata da decisão e reutilizados pelo runtime, evitando uma segunda chamada LLM no mesmo turno.
|
||||||
|
|
||||||
|
## Profile LLM
|
||||||
|
|
||||||
|
Foi adicionado aos templates:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
transaction_parameter_extraction:
|
||||||
|
provider: oci_openai
|
||||||
|
model: openai.gpt-4.1-mini
|
||||||
|
temperature: 0
|
||||||
|
max_tokens: 500
|
||||||
|
timeout_seconds: 8
|
||||||
|
```
|
||||||
|
|
||||||
|
Generation/component:
|
||||||
|
|
||||||
|
- `llm.transaction_parameter_extraction`
|
||||||
|
- `transaction_parameter_extraction`
|
||||||
|
|
||||||
|
## Limpeza de estado
|
||||||
|
|
||||||
|
Em `intent_shift`, `transaction_pre_validation` da transação abandonada é removido para não contaminar a nova transação. O resultado de pre-validation continua preservado enquanto pertence à própria transação para auditoria.
|
||||||
|
|
||||||
|
## Testes adicionados
|
||||||
|
|
||||||
|
`tests/test_transaction_parameter_llm_precedence.py`
|
||||||
|
|
||||||
|
Cobertura:
|
||||||
|
|
||||||
|
1. dois parâmetros extraídos no mesmo turno;
|
||||||
|
2. um parâmetro preenchido ganha precedência sobre keyword que indicaria outra intent;
|
||||||
|
3. nenhum parâmetro encontrado libera `intent_shift`;
|
||||||
|
4. ausência do antigo `_extract_action_arguments()` hardcoded;
|
||||||
|
5. confirmação `sim` ganha precedência sobre intent shift.
|
||||||
64
IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
64
IMPLEMENTACAO_WORKFLOWS_TRANSACIONAIS.md
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Implementação — workflows transacionais determinísticos
|
||||||
|
|
||||||
|
## Entrega
|
||||||
|
|
||||||
|
Foi adicionada ao `agent_framework_oci` uma capacidade opcional para executar transações multi-etapas como workflows determinísticos compilados em LangGraph.
|
||||||
|
|
||||||
|
### Módulo novo
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/workflows/`
|
||||||
|
|
||||||
|
- `models.py`: contratos Pydantic e validação estrutural;
|
||||||
|
- `repository.py`: resolução de versão ativa e leitura de YAML imutável;
|
||||||
|
- `registry.py`: registro desacoplado de actions sync/async;
|
||||||
|
- `runtime.py`: compilação, cache e execução do StateGraph;
|
||||||
|
- `tool_executor.py`: integração com a política da tool;
|
||||||
|
- `__init__.py`: API pública.
|
||||||
|
|
||||||
|
### Política expandida
|
||||||
|
|
||||||
|
`ToolPolicy` agora aceita:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
execution:
|
||||||
|
mode: direct_tool | workflow | agent
|
||||||
|
workflow: nome_do_workflow
|
||||||
|
version: active | 1
|
||||||
|
```
|
||||||
|
|
||||||
|
O default permanece `direct_tool`, preservando compatibilidade.
|
||||||
|
|
||||||
|
### Configuração
|
||||||
|
|
||||||
|
Foram adicionados:
|
||||||
|
|
||||||
|
- `ENABLE_TRANSACTIONAL_WORKFLOWS=false`;
|
||||||
|
- `WORKFLOWS_PATH=./workflows`.
|
||||||
|
|
||||||
|
### Template
|
||||||
|
|
||||||
|
Inclui um exemplo completo de devolução de pedido com:
|
||||||
|
|
||||||
|
- confirmação e campos obrigatórios pela política;
|
||||||
|
- workflow YAML versionado;
|
||||||
|
- actions de domínio no backend;
|
||||||
|
- bifurcação determinística baseada no resultado da validação.
|
||||||
|
|
||||||
|
## Validação realizada
|
||||||
|
|
||||||
|
- `tests/unit/test_tool_policies.py`: 4 testes aprovados;
|
||||||
|
- compilação Python de framework, template e novos testes: aprovada;
|
||||||
|
- o teste funcional novo do LangGraph foi criado, mas não pôde ser executado neste container porque `langgraph` não está instalado no ambiente. A dependência já está declarada no `pyproject.toml` do framework.
|
||||||
|
|
||||||
|
## Escopo e segurança
|
||||||
|
|
||||||
|
Esta entrega cria o motor e a integração de política. Para operações críticas em produção ainda é necessário conectar:
|
||||||
|
|
||||||
|
- execution store persistente;
|
||||||
|
- idempotência de negócio nas actions/APIs;
|
||||||
|
- autorização por escopo;
|
||||||
|
- telemetria IC/NOC específica de workflow;
|
||||||
|
- compensação/Saga quando aplicável;
|
||||||
|
- estratégia corporativa de timeout e retry.
|
||||||
|
|
||||||
|
Esses itens foram explicitamente documentados para evitar a falsa impressão de que retry por si só garante segurança transacional.
|
||||||
987
Implementando_Basic_Auth.md
Normal file
987
Implementando_Basic_Auth.md
Normal file
@@ -0,0 +1,987 @@
|
|||||||
|
# Implementando Basic Auth
|
||||||
|
|
||||||
|
Para validar **todo o circuito com Basic Auth**, você precisa configurar três relações distintas:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Cliente de teste
|
||||||
|
└─ Basic Auth A ─► Agent Gateway :8010
|
||||||
|
└─ Basic Auth B ─► Agent Backend :8000
|
||||||
|
└─ Basic Auth C ─► MCP Gateway :8300
|
||||||
|
```
|
||||||
|
|
||||||
|
Há um detalhe importante: no pacote atual, a autenticação Basic já funciona para chamadas **de entrada**, mas os clientes internos ainda não enviam Basic Auth:
|
||||||
|
|
||||||
|
* `Agent Gateway → Agent Backend` não envia credencial;
|
||||||
|
* `Agent Backend → MCP Gateway` envia apenas Bearer Token.
|
||||||
|
|
||||||
|
Portanto, para testar o circuito inteiro com Basic Auth, faça os dois pequenos ajustes de código descritos abaixo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 1. Preparar o ambiente
|
||||||
|
|
||||||
|
Considere que o ZIP foi extraído em:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd agent_framework_oci_authentication_v2_1
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie um único ambiente virtual para facilitar o teste:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.\.venv\Scripts\Activate.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
Instale o framework e as dependências dos três componentes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -U pip
|
||||||
|
|
||||||
|
pip install -e ./libs/agent_framework
|
||||||
|
|
||||||
|
pip install \
|
||||||
|
-r ./Tuning-Performance/Authentication/agent_template_backend_authentication/requirements.txt \
|
||||||
|
-r ./apps/agent_gateway/requirements.txt \
|
||||||
|
-r ./apps/mcp_gateway/requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirme a importação:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -c "from agent_framework.security import install_authentication; print('framework ok')"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 2. Criar três pares de Client ID e Secret
|
||||||
|
|
||||||
|
Use credenciais diferentes para cada trecho. Para teste local:
|
||||||
|
|
||||||
|
| Fluxo | Client ID | Secret de teste |
|
||||||
|
| ----------------------- | -------------------- | --------------------------- |
|
||||||
|
| Cliente → Agent Gateway | `tia-test` | `TiaGateway-Test-2026!` |
|
||||||
|
| Agent Gateway → Backend | `agent-gateway-test` | `GatewayBackend-Test-2026!` |
|
||||||
|
| Backend → MCP Gateway | `agent-backend-test` | `BackendMcp-Test-2026!` |
|
||||||
|
|
||||||
|
Esses valores são apenas para ambiente local. Não os reutilize em produção.
|
||||||
|
|
||||||
|
## Gerar os hashes
|
||||||
|
|
||||||
|
O script está em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
scripts/generate_secret_hash.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Execute:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'TiaGateway-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Depois:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'GatewayBackend-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
E:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python Tuning-Performance/Authentication/agent_template_backend_authentication/scripts/generate_secret_hash.py \
|
||||||
|
--secret 'BackendMcp-Test-2026!'
|
||||||
|
```
|
||||||
|
|
||||||
|
Você receberá três valores semelhantes a:
|
||||||
|
|
||||||
|
```text
|
||||||
|
pbkdf2_sha256:310000:<salt>:<digest>
|
||||||
|
```
|
||||||
|
|
||||||
|
Guarde-os temporariamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
HASH_CLIENT_GATEWAY='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_GATEWAY_BACKEND='pbkdf2_sha256:310000:...'
|
||||||
|
HASH_BACKEND_MCP='pbkdf2_sha256:310000:...'
|
||||||
|
```
|
||||||
|
|
||||||
|
O hash muda a cada execução porque o salt é aleatório. Isso é esperado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 3. Configurar o Agent Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione ao final do `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: cliente/TIA -> Agent Gateway
|
||||||
|
AGENT_GATEWAY_AUTH_ENABLED=true
|
||||||
|
AGENT_GATEWAY_AUTH_MODE=basic
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_CLIENT_ID=tia-test
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_CLIENT_GATEWAY
|
||||||
|
AGENT_GATEWAY_AUTH_BASIC_REALM=agent-gateway
|
||||||
|
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
# Saída: Agent Gateway -> Agent Backend
|
||||||
|
BACKEND_AUTH_MODE=basic
|
||||||
|
BACKEND_AUTH_CLIENT_ID=agent-gateway-test
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Não coloque aspas no `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=GatewayBackend-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
O arquivo de backends já aponta o backend Contas para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
contas:
|
||||||
|
url: http://localhost:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
Arquivo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/config/backends.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para este teste, mantenha apenas o backend `contas` ou force o backend no payload. Caso contrário, pedidos sobre ofertas e suporte podem ser roteados para portas em que nenhum backend está rodando.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 4. Fazer o Agent Gateway enviar Basic Auth ao backend
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/global_supervisor/client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a classe `BackendClient` por uma versão que aceite autenticação Basic.
|
||||||
|
|
||||||
|
No início do arquivo, adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o construtor:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class BackendClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
timeout_seconds: float = 120.0,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _auth(self) -> httpx.BasicAuth | None:
|
||||||
|
if self.basic_client_id and self.basic_secret:
|
||||||
|
return httpx.BasicAuth(
|
||||||
|
username=self.basic_client_id,
|
||||||
|
password=self.basic_secret,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `call_message`, troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(url, json=payload)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.post(
|
||||||
|
url,
|
||||||
|
json=payload,
|
||||||
|
auth=self._auth(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
No método `health`, você pode manter `/health` público. Caso queira enviar autenticação também, use:
|
||||||
|
|
||||||
|
```python
|
||||||
|
resp = await client.get(url, auth=self._auth())
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/agent_gateway/app/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import os
|
||||||
|
```
|
||||||
|
|
||||||
|
Troque:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
backend_client = BackendClient(
|
||||||
|
timeout_seconds=settings.BACKEND_TIMEOUT_SECONDS,
|
||||||
|
basic_client_id=os.getenv("BACKEND_AUTH_CLIENT_ID"),
|
||||||
|
basic_secret=os.getenv("BACKEND_AUTH_SECRET"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso implementa:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway → Agent Backend
|
||||||
|
Authorization: Basic base64(agent-gateway-test:GatewayBackend-Test-2026!)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 5. Configurar o Agent Backend autenticado
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
```
|
||||||
|
|
||||||
|
Copie o exemplo:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste a seção de autenticação:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Gateway -> Agent Backend
|
||||||
|
AGENT_AUTH_ENABLED=true
|
||||||
|
AGENT_AUTH_MODE=basic
|
||||||
|
AGENT_AUTH_BASIC_CLIENT_ID=agent-gateway-test
|
||||||
|
AGENT_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_GATEWAY_BACKEND
|
||||||
|
AGENT_AUTH_BASIC_REALM=agent-contas
|
||||||
|
|
||||||
|
AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_AUTH_PUBLIC_PREFIXES=
|
||||||
|
```
|
||||||
|
|
||||||
|
Para usar o MCP Gateway:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
|
||||||
|
# Saída: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Para evitar dependências externas durante o primeiro teste, configure também:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=mock
|
||||||
|
ENABLE_LANGFUSE=false
|
||||||
|
ENABLE_ANALYTICS=false
|
||||||
|
|
||||||
|
SESSION_REPOSITORY_PROVIDER=memory
|
||||||
|
MEMORY_REPOSITORY_PROVIDER=memory
|
||||||
|
CHECKPOINT_REPOSITORY_PROVIDER=memory
|
||||||
|
CACHE_PROVIDER=memory
|
||||||
|
USAGE_REPOSITORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
Os nomes exatos de alguns providers podem depender do arquivo de configuração atual do framework. Caso o `.env.example` já contenha valores locais ou mock, preserve-os.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 6. Fazer o Backend enviar Basic Auth ao MCP Gateway
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/gateways/mcp_gateway_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Substitua a implementação por:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class MCPGatewayClient:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
token: str | None = None,
|
||||||
|
timeout_seconds: int = 60,
|
||||||
|
auth_mode: str | None = None,
|
||||||
|
basic_client_id: str | None = None,
|
||||||
|
basic_secret: str | None = None,
|
||||||
|
):
|
||||||
|
self.base_url = base_url.rstrip("/")
|
||||||
|
self.token = token
|
||||||
|
self.timeout_seconds = timeout_seconds
|
||||||
|
self.auth_mode = (auth_mode or "").strip().lower()
|
||||||
|
self.basic_client_id = basic_client_id
|
||||||
|
self.basic_secret = basic_secret
|
||||||
|
|
||||||
|
def _headers(self) -> dict[str, str]:
|
||||||
|
if (
|
||||||
|
self.auth_mode == "basic"
|
||||||
|
and self.basic_client_id
|
||||||
|
and self.basic_secret
|
||||||
|
):
|
||||||
|
raw = f"{self.basic_client_id}:{self.basic_secret}".encode("utf-8")
|
||||||
|
encoded = base64.b64encode(raw).decode("ascii")
|
||||||
|
return {"Authorization": f"Basic {encoded}"}
|
||||||
|
|
||||||
|
if self.token:
|
||||||
|
return {"Authorization": f"Bearer {self.token}"}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
async def list_tools(self) -> dict[str, Any]:
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.get(
|
||||||
|
f"{self.base_url}/v1/tools",
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
async def invoke_tool(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
tenant_id: str,
|
||||||
|
agent_id: str,
|
||||||
|
channel: str | None,
|
||||||
|
tool_name: str,
|
||||||
|
arguments: dict[str, Any] | None = None,
|
||||||
|
business_context: dict[str, Any] | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
payload = {
|
||||||
|
"tenant_id": tenant_id,
|
||||||
|
"agent_id": agent_id,
|
||||||
|
"channel": channel,
|
||||||
|
"tool_name": tool_name,
|
||||||
|
"arguments": arguments or {},
|
||||||
|
"business_context": business_context or {},
|
||||||
|
"metadata": metadata or {},
|
||||||
|
}
|
||||||
|
|
||||||
|
async with httpx.AsyncClient(
|
||||||
|
timeout=self.timeout_seconds
|
||||||
|
) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{self.base_url}/v1/tools/{tool_name}/invoke",
|
||||||
|
json=payload,
|
||||||
|
headers=self._headers(),
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
```
|
||||||
|
|
||||||
|
Agora abra:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/mcp/tool_router.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCPGatewayClient(
|
||||||
|
base_url=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_URL",
|
||||||
|
"http://localhost:8300",
|
||||||
|
),
|
||||||
|
token=getattr(settings, "MCP_GATEWAY_TOKEN", None),
|
||||||
|
timeout_seconds=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_TIMEOUT_SECONDS",
|
||||||
|
settings.MCP_TOOL_TIMEOUT_SECONDS,
|
||||||
|
),
|
||||||
|
auth_mode=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_AUTH_MODE",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_client_id=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
basic_secret=getattr(
|
||||||
|
settings,
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET",
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione estes campos em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/config/settings.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Próximo das configurações existentes de MCP Gateway:
|
||||||
|
|
||||||
|
```python
|
||||||
|
MCP_GATEWAY_AUTH_MODE: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_CLIENT_ID: str | None = None
|
||||||
|
MCP_GATEWAY_BASIC_SECRET: str | None = None
|
||||||
|
```
|
||||||
|
|
||||||
|
Há também uma factory local em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Tuning-Performance/Authentication/
|
||||||
|
agent_template_backend_authentication/
|
||||||
|
app/mcp_gateway_client_factory.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Ajuste para:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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")
|
||||||
|
),
|
||||||
|
auth_mode=os.getenv("MCP_GATEWAY_AUTH_MODE"),
|
||||||
|
basic_client_id=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_CLIENT_ID"
|
||||||
|
),
|
||||||
|
basic_secret=os.getenv(
|
||||||
|
"MCP_GATEWAY_BASIC_SECRET"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 7. Configurar o MCP Gateway
|
||||||
|
|
||||||
|
Entre no diretório:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie `.env`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Adicione:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# Entrada: Agent Backend -> MCP Gateway
|
||||||
|
MCP_GATEWAY_AUTH_ENABLED=true
|
||||||
|
MCP_GATEWAY_AUTH_MODE=basic
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_CLIENT_ID=agent-backend-test
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_SECRET_HASH=COLE_AQUI_HASH_BACKEND_MCP
|
||||||
|
MCP_GATEWAY_AUTH_BASIC_REALM=mcp-gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PATHS=/health,/ready,/docs,/openapi.json,/redoc
|
||||||
|
MCP_GATEWAY_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Desabilitar o mecanismo Bearer legado
|
||||||
|
|
||||||
|
O MCP Gateway ainda possui um segundo mecanismo antigo, configurado dentro de:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Localize a seção:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: true
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
enabled: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso é necessário porque o novo middleware já faz a autenticação Basic. Caso o `auth_check()` legado continue habilitado, a requisição passará pelo Basic e depois será rejeitada por não possuir Bearer Token.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 8. Subir os componentes
|
||||||
|
|
||||||
|
Use quatro terminais.
|
||||||
|
|
||||||
|
## Terminal 1 — MCP Servers
|
||||||
|
|
||||||
|
O MCP Gateway precisa ter pelo menos um servidor MCP disponível para demonstrar uma chamada real.
|
||||||
|
|
||||||
|
Na raiz do projeto:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba o servidor telecom:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.telecom_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8100 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Em outro terminal, caso queira também o retail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn mcp.servers.retail_mcp_server.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8200 \
|
||||||
|
--reload
|
||||||
|
```
|
||||||
|
|
||||||
|
Confira as URLs configuradas em:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/mcp_gateway/config/mcp_gateway.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
Para execução local, devem apontar para:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8100
|
||||||
|
```
|
||||||
|
|
||||||
|
e:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
url: http://localhost:8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 2 — MCP Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba usando `--env-file`. Isso é importante porque o middleware lê variáveis com `os.getenv()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8300 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste a saúde pública:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste um endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-backend-test:BackendMcp-Test-2026!' \
|
||||||
|
http://localhost:8300/v1/tools
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 3 — Agent Backend
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd Tuning-Performance/Authentication/agent_template_backend_authentication
|
||||||
|
source ../../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8000 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial usada pelo Agent Gateway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
http://localhost:8000/agents
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste mensagem diretamente:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8000/gateway/message \
|
||||||
|
-u 'agent-gateway-test:GatewayBackend-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "teste-backend-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Terminal 4 — Agent Gateway
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/agent_gateway
|
||||||
|
source ../../.venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Suba:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app \
|
||||||
|
--host 0.0.0.0 \
|
||||||
|
--port 8010 \
|
||||||
|
--reload \
|
||||||
|
--env-file .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste saúde:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:8010/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste endpoint protegido sem credencial:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
Teste com a credencial externa:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 9. Validar o circuito completo
|
||||||
|
|
||||||
|
Force o backend `contas` para evitar que o roteador selecione um backend não iniciado:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:8010/gateway/message \
|
||||||
|
-u 'tia-test:TiaGateway-Test-2026!' \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-d '{
|
||||||
|
"channel": "web",
|
||||||
|
"backend_id": "contas",
|
||||||
|
"tenant_id": "default",
|
||||||
|
"agent_id": "telecom_contas",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"payload": {
|
||||||
|
"text": "Quero consultar minha fatura",
|
||||||
|
"session_id": "circuito-basic-001",
|
||||||
|
"user_id": "user-001",
|
||||||
|
"customer_id": "12345",
|
||||||
|
"message_id": "msg-circuito-001"
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
O circuito esperado é:
|
||||||
|
|
||||||
|
```text
|
||||||
|
curl
|
||||||
|
│ Basic tia-test
|
||||||
|
▼
|
||||||
|
Agent Gateway :8010
|
||||||
|
│ Basic agent-gateway-test
|
||||||
|
▼
|
||||||
|
Agent Backend :8000
|
||||||
|
│ Basic agent-backend-test
|
||||||
|
▼
|
||||||
|
MCP Gateway :8300
|
||||||
|
▼
|
||||||
|
MCP Server :8100 ou :8200
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 10. Como comprovar cada autenticação
|
||||||
|
|
||||||
|
Faça testes negativos em cada trecho.
|
||||||
|
|
||||||
|
## Secret externo incorreto
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -i \
|
||||||
|
-u 'tia-test:senha-errada' \
|
||||||
|
http://localhost:8010/backends
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
## Secret do gateway para backend incorreto
|
||||||
|
|
||||||
|
Altere temporariamente no `apps/agent_gateway/.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BACKEND_AUTH_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o Agent Gateway e envie uma mensagem.
|
||||||
|
|
||||||
|
O gateway deverá retornar erro de backend, normalmente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
502 Bad Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
O erro interno será originado por um:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
do Agent Backend.
|
||||||
|
|
||||||
|
## Secret do backend para MCP incorreto
|
||||||
|
|
||||||
|
Altere temporariamente:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MCP_GATEWAY_BASIC_SECRET=senha-errada
|
||||||
|
```
|
||||||
|
|
||||||
|
Reinicie o backend e execute uma frase que acione uma ferramenta MCP.
|
||||||
|
|
||||||
|
O backend deverá registrar falha na chamada ao MCP Gateway com:
|
||||||
|
|
||||||
|
```text
|
||||||
|
401 Unauthorized
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 11. Verificação rápida de portas
|
||||||
|
|
||||||
|
No Linux ou WSL:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ss -lntp | grep -E ':8000|:8010|:8100|:8200|:8300'
|
||||||
|
```
|
||||||
|
|
||||||
|
No Windows PowerShell:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-NetTCPConnection -State Listen |
|
||||||
|
Where-Object LocalPort -in 8000,8010,8100,8200,8300 |
|
||||||
|
Sort-Object LocalPort
|
||||||
|
```
|
||||||
|
|
||||||
|
Você deverá ver:
|
||||||
|
|
||||||
|
```text
|
||||||
|
8000 Agent Backend
|
||||||
|
8010 Agent Gateway
|
||||||
|
8100 Telecom MCP Server
|
||||||
|
8200 Retail MCP Server
|
||||||
|
8300 MCP Gateway
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observação importante
|
||||||
|
|
||||||
|
O segredo original precisa existir no componente cliente:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TIA ou curl:
|
||||||
|
TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Gateway:
|
||||||
|
GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Os componentes servidores armazenam apenas os hashes:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Agent Gateway:
|
||||||
|
hash de TiaGateway-Test-2026!
|
||||||
|
|
||||||
|
Agent Backend:
|
||||||
|
hash de GatewayBackend-Test-2026!
|
||||||
|
|
||||||
|
MCP Gateway:
|
||||||
|
hash de BackendMcp-Test-2026!
|
||||||
|
```
|
||||||
|
|
||||||
|
Em produção, os segredos originais e hashes devem vir de Vault ou Kubernetes Secret, não de arquivos `.env`.
|
||||||
264
LICENSE.txt
Normal file
264
LICENSE.txt
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
The Universal Permissive License (UPL), Version 1.0
|
||||||
|
This page provides a copy of and also attempts to answer some frequently asked questions about the Universal Permissive License version 1.0. The UPL was first drafted by Oracle in the Spring of 2014, was revised several times after extensive consultation with the Java Community Process Executive Committee and through the Open Source Initiative review process, and was finally approved by the Open Source Initiative in February 2015.
|
||||||
|
|
||||||
|
To begin with, this is the final approved text of the Universal Permissive License v 1.0:
|
||||||
|
|
||||||
|
Copyright (c) [year] [copyright holders]
|
||||||
|
|
||||||
|
The Universal Permissive License (UPL), Version 1.0
|
||||||
|
|
||||||
|
Subject to the condition set forth below, permission is hereby granted to any
|
||||||
|
person obtaining a copy of this software, associated documentation and/or data
|
||||||
|
(collectively the "Software"), free of charge and under any and all copyright
|
||||||
|
rights in the Software, and any and all patent rights owned or freely
|
||||||
|
licensable by each licensor hereunder covering either (i) the unmodified
|
||||||
|
Software as contributed to or provided by such licensor, or (ii) the Larger
|
||||||
|
Works (as defined below), to deal in both
|
||||||
|
|
||||||
|
(a) the Software, and
|
||||||
|
(b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
|
||||||
|
one is included with the Software (each a "Larger Work" to which the Software
|
||||||
|
is contributed by such licensors),
|
||||||
|
|
||||||
|
without restriction, including without limitation the rights to copy, create
|
||||||
|
derivative works of, display, perform, and distribute the Software and make,
|
||||||
|
use, sell, offer for sale, import, export, have made, and have sold the
|
||||||
|
Software and the Larger Work(s), and to sublicense the foregoing rights on
|
||||||
|
either these or other terms.
|
||||||
|
|
||||||
|
This license is subject to the following condition:
|
||||||
|
The above copyright notice and either this complete permission notice or at
|
||||||
|
a minimum a reference to the UPL must be included in all copies or
|
||||||
|
substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
When referring to the license, you may link to it at https://oss.oracle.com/licenses/upl or you may also find a copy on the Open Source Initiative web site at https://opensource.org/licenses/UPL.
|
||||||
|
|
||||||
|
Frequently Asked Questions
|
||||||
|
What is the UPL and why did you create it?
|
||||||
|
What are the most important features of the UPL?
|
||||||
|
Can you point to some concrete problems the UPL solves that you could not have addressed with the MIT, BSD or Apache licenses?
|
||||||
|
So why is the absence of an express patent license in the MIT or BSD licenses a problem - isn't the implied license enough?
|
||||||
|
Why didn't you include a defensive patent termination provision?
|
||||||
|
I heard this licenses our entire patent portfolio to Oracle, is that right?
|
||||||
|
How should I apply the UPL to my code?
|
||||||
|
What goes in the Larger Works file and what should it look like?
|
||||||
|
Can someone else change my Larger Works file and license my patents for other things?
|
||||||
|
What is the UPL and why did you create it?
|
||||||
|
The UPL is a highly permissive license, including both copyright and patent licenses, which permits use and relicensing under both copyleft and commercial terms, and also facilitates use as a contributor license agreement.
|
||||||
|
|
||||||
|
It was originally borne out of a discussion in the Java Community Process around what it would take to permit collaboration on JSR reference implementations in open source forges without the necessity of collaborators signing the JSPA or an Oracle Contributor Agreement. In order to accommodate the broadest possible use case, the license needed to include an express patent license and to be broadly agreed to be GPLv2, GPLv2+Classpath Exception, CDDL, and commercial license compatible, including the ability to cut and paste code between modules, and while it is almost universally agreed among those knowledgeable in FOSS licensing that license proliferation is something to be avoided without good cause, since a license meeting these relatively simple and obvious criteria did not exist, it was decided to draft one.
|
||||||
|
|
||||||
|
Drafting started with the MIT license, and after a lot of discussion with other open source lawyers, developers, members of the Java Community Process Executive Committee, and reviewers and board members of the Open Source Initiative, and several rounds of revision, finally arrived at the vetted license text you see here. The license was approved by the OSI as conforming with the Open Source Definition and non-duplicative of existing permissive licenses in February 2015.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
What are the most important features of the UPL?
|
||||||
|
The most important features and things to understand about the UPL are the following:
|
||||||
|
|
||||||
|
Clear patent protection. The UPL is a broad permissive license including both a copyright license and an express patent license, covering at least a version licensed by someone under the license (for example a distributor) and/or a version someone contributed to even if they never distribute the whole. (The reason the latter is needed is discussed below.) By virtue of the unambiguous patent license, the UPL is materially clearer with respect to the rights licensed and likely broader than either the MIT or BSD licenses.
|
||||||
|
Clear & simplified relicensing. The UPL expressly permits sublicensing under either the UPL or under other terms, which clearly allows someone to relicense code received under the UPL either on copyleft terms, on proprietary terms, or otherwise, thus permitting maximum flexibility in reuse.
|
||||||
|
Reduced overhead in source files. The UPL expressly permits use of the license without including a full copy of the text, which is useful for JavaScript or other cases where minimizing space consumed by licenses is desired - see below for our recommendation of how to apply the license or include proper attribution without the full text.
|
||||||
|
It can be used as a contributor agreement. Finally, the UPL may be used as a contributor license agreement licensing both the software itself and also contributor patents for use in one or more "Larger Works." The Larger Works licensed in this fashion are designated by the use of a separate file accompanying the license, akin to the NOTICE file that accompanies the Apache License, Version 2.0. The Larger Works file can be used to control for both contributions to other works (for example, we could specify MySQL in a Larger Works file for a work, which would then license contributor patents for MySQL as well as the contribution), to set patent license scope for specific versions (for example, we could specify the approved reference implementation of JSR-xxx including Maintenance Releases to ensure that all contributors to an RI are licensing both the final version of the RI and qualified updates under the JCP program), or both.
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
Can you point to some concrete problems the UPL solves that you could not have addressed with the MIT, BSD or Apache licenses?
|
||||||
|
The most important problem the UPL solves is the inclusion of an express patent license in a license that is both broadly agreed to be uniformly copyleft compatible and also permits the software to be freely used in commercially licensed software (i.e., without concerns about reciprocal license obligations).
|
||||||
|
|
||||||
|
The MIT and BSD licenses do not include express patent licenses, and while many argue that there is an implied patent license, there is nontrivial debate about the existence & scope of any such implied license. This is addressed in some more detail below. The Apache license, on the other hand, is argued by some not to be GPLv2 compatible, which makes it tricky to use in connection with GPLv2 licensed code - the GPLv2 is still by far the most popular open source license, and unambiguous, undisputed GPL compatibility was a crucial qualifier for any license considered for use in the JCP. (Even the BSD license has been argued not to be GPL compatible - while this need not be addressed here, and it is not the conclusion of the FSF or most others, by virtue of expressly permitting relicensing on other terms, the UPL is definitively and fully GPLv2 compatible.)
|
||||||
|
|
||||||
|
Another problem solved with the UPL, which seems like a small thing but adds compliance and clarity, is that licensors and distributors are expressly permitted to reference the UPL rather than including a complete copy of the text in each item. What is the license solving for here? The MIT and BSD licenses expressly require that you include a complete copy of the license in each piece of source code. This means that if you have 50 different JavaScript files being downloaded from a page, each one should be giving you a complete copy of the license - you may be using as much bandwidth for licenses as you do for the actual code! And if you do not include a complete copy of the license, as some folks do not, merely referring to the license by URL or name, then you are not really applying the license in the intended manner and are in fact suggesting that downstream recipients not strictly follow those terms either.
|
||||||
|
|
||||||
|
Finally, the inclusion of the Larger Works concept facilitates use as a contributor license agreement (CLA), with contributors granting a patent license to the works to which they're contributing, and therefore creating a common & safe platform for collaboration where no one is going to assert infringement by the same project in which they're an active participant.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
So why is the absence of an express patent license in the MIT or BSD licenses a problem - isn't the implied license enough?
|
||||||
|
Some authorities think it is enough, but not everyone agrees on this point. To begin with, it has been suggested that in some jurisdictions there may be no implied license at all, or only a license to people to whom a licensor directly distributed the software.
|
||||||
|
|
||||||
|
But even if one were to take the position that a patent license covering any contribution is implied, and further that it covers the entirety of a work you distribute, in our world of cloud offerings, many contributors may never distribute the entirety of the work at all. This may reduce the likelihood that a court would find them to have granted an implied license for the whole of the work.
|
||||||
|
|
||||||
|
The UPL attempts to solve these problems by having each party that either contributes to the project or distributes it under the UPL expressly license all patents that they own or can freely license that cover the work - thus, e.g., licensing the whole even from contributors who never distribute the whole.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
Why isn't there a defensive patent termination provision?
|
||||||
|
A defensive patent termination provision may sound like a great idea at first, especially for a pure open source project, but does not really work for contributions to software that is also licensed on commercial terms.
|
||||||
|
|
||||||
|
Why? A company that takes on commercial obligations with respect to a piece of code needs to retain the ability to keep supporting and shipping that code. A defensive patent termination provision allows someone who makes a contribution and then breaches the project license to the project to then potentially effectively block infringement suits against them, even when they have willfully and flagrantly disregarded community norms and IP rights in the project. Such a provision is therefore in reality as much of a sword as it is a shield.
|
||||||
|
|
||||||
|
Say, for example, that a fictional company WidgetCo were to make a contribution to fictional company GadgetCo's product GadgetX under a license with a patent termination provision. Now, WidgetCo decides to violate GadgetCo's terms and distribute GadgetX without a license. GadgetCo's options for stopping the violation would ordinarily include both copyright and also patent infringement claims for any patents covering GadgetX. If, on the other hand, WidgetCo has made patent covered contributions under an agreement with a so-called defensive termination clause, GadgetCo's ability to stop WidgetCo from violating the license may be significantly compromised because it can no longer bring patent claims without risking losing its own license to the patents on the contributions, which therefore might require it to stop shipping GadgetX immediately and stop supporting versions that customers are relying on.
|
||||||
|
|
||||||
|
As a commercial distributor of software, GadgetCo's customers rely on it to have the rights it needs to be able to continue to deliver and support the software, and defensive patent termination rights in contributions are inconsistent with those obligations. Defensive patent termination only helps someone who may be subject to infringement litigation by someone who is using the code - it won't stop patent trolls, who aren't normally using the code to begin with. So who does it stop, in reality? Someone who is using the code but has IP rights that someone else using the code is infringing. Now, it may be reasonable to tell someone who has no relationship to a community that they cannot simultaneously expect to be able to use the code and also sue for its use - thus making such provisions at least not entirely crazy for a pure FOSS project that no one is signing up to develop or support on a commercial basis, and which is rarely if ever going to need to enforce its rights anyway. But for anyone who both actually expects to need to enforce its license and also wants to take contributions, this makes no sense - you need to be able to enforce your rights in the project without needing to stop shipping or using it, and accepting contributions under terms with a patent termination provision does not satisfy this basic need.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
I heard this licenses our entire patent portfolio to Oracle, is that right?
|
||||||
|
No, it is not, this is simply untrue except in odd and contrived circumstances that would apply to other FOSS licenses as well. If your entire patent portfolio happens to read on a UPL licensed project that you have distributed on those terms or contributed to, or its designated Larger Works, then yes, it would all be licensed for use in that Software and those specified Larger Works, not just to Oracle but to all licensees under the UPL, just as it would all be licensed for a project under the Apache Software License or Eclipse Public License if your entire portfolio read on a project under the ASL or EPL. Patents that do not read on the project as you contributed to or distributed it or the specified Larger Works are not licensed, there is no magic here.
|
||||||
|
|
||||||
|
The patent scope is not really all that complicated. It covers your contributions, the project you contributed to in the state it existed when you contributed, and anything specified in the Larger Works file (which may be another application, it may be other or later versions, it may be a piece of hardware, or any combination of those things - or, just as importantly, it may be none of those things, since many projects may have no Larger Works file at all, in which case the patent license is little different than the express patent license in the Apache license, only without the so-called defensive termination provision).
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
How should I apply the UPL to my code?
|
||||||
|
It is best to insert a copyright notice and a copy of the license at the top of each source file, and if there is a package of numerous files, to also insert a LICENSE file including a copy of the UPL in the root of the distribution. Here is an example:
|
||||||
|
|
||||||
|
/*
|
||||||
|
** MyWidget version 1.0.
|
||||||
|
**
|
||||||
|
** Copyright (c) 2015 WidgetCo, Inc.
|
||||||
|
**
|
||||||
|
** The Universal Permissive License (UPL), Version 1.0
|
||||||
|
**
|
||||||
|
** Subject to the condition set forth below, permission is hereby granted to any
|
||||||
|
** person obtaining a copy of this software, associated documentation and/or data
|
||||||
|
** (collectively the "Software"), free of charge and under any and all copyright
|
||||||
|
** rights in the Software, and any and all patent rights owned or freely
|
||||||
|
** licensable by each licensor hereunder covering either (i) the unmodified
|
||||||
|
** Software as contributed to or provided by such licensor, or (ii) the Larger
|
||||||
|
** Works (as defined below), to deal in both
|
||||||
|
**
|
||||||
|
** (a) the Software, and
|
||||||
|
** (b) any piece of software and/or hardware listed in the lrgrwrks.txt file if
|
||||||
|
** one is included with the Software (each a "Larger Work" to which the Software
|
||||||
|
** is contributed by such licensors),
|
||||||
|
**
|
||||||
|
** without restriction, including without limitation the rights to copy, create
|
||||||
|
** derivative works of, display, perform, and distribute the Software and make,
|
||||||
|
** use, sell, offer for sale, import, export, have made, and have sold the
|
||||||
|
** Software and the Larger Work(s), and to sublicense the foregoing rights on
|
||||||
|
** either these or other terms.
|
||||||
|
**
|
||||||
|
** This license is subject to the following condition:
|
||||||
|
** The above copyright notice and either this complete permission notice or at
|
||||||
|
** a minimum a reference to the UPL must be included in all copies or
|
||||||
|
** substantial portions of the Software.
|
||||||
|
**
|
||||||
|
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
** FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
** AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
** LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
** OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
** SOFTWARE.
|
||||||
|
*/
|
||||||
|
If using the UPL for JavaScript or other circumstances in which space is at a premium, you may use a reference to the license rather than a full copy (though a full copy is always preferred if there's room and you won't be transmitting a bunch of copies to the same user). If you want to use just a short reference, you should still insert a copyright notice, and along with it, a statement that the file is licensed under the UPL, and may link to it at https://oss.oracle.com/licenses/upl or on the Open Source Initiative web site at https://opensource.org/licenses/UPL.
|
||||||
|
|
||||||
|
Here is an example header with a short reference for use in source code:
|
||||||
|
|
||||||
|
/*
|
||||||
|
** MyWidget version 1.0.
|
||||||
|
**
|
||||||
|
** Copyright (c) 2015 WidgetCo, Inc.
|
||||||
|
** Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/
|
||||||
|
*/
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
What goes in the Larger Works file and what should it look like?
|
||||||
|
To begin with, we should note that the Larger Works concept will be irrelevant to many people and to be clear, there is no need to license any Larger Works at all in order to use the UPL on your code - this is still advantageous versus other permissive licenses because licensees can count on a patent license of unambiguous scope from each contributor, can freely relicense the code as necessary for their needs, and can abbreviate the license notice if appropriate.
|
||||||
|
|
||||||
|
The purpose of the Larger Works addition to the license is to allow you to use the UPL as a contributor license agreement, commonly referred to as a CLA, licensing patents for a specified larger work or set of works with which contributions may be used. If you desire to use the UPL as a CLA, in addition to including the UPL and/or references to it in your source, you should insert a lrgrwrks.txt file in the root of the distribution specifying the licensed Larger Works. You will name the licensed project/product there, and if appropriate, specify what versions are covered. In theory you could use a single word naming a product, which would be effective, but some slightly prettier examples are as follows.
|
||||||
|
|
||||||
|
Here is what we are hoping to use in connection with JCP Reference Implementations:
|
||||||
|
|
||||||
|
/* Larger Works File for JSR-xxx
|
||||||
|
**
|
||||||
|
** Licensed Larger Works: The Reference Implementation for JSR-xxx Including Maintenance Releases
|
||||||
|
** as defined in the JCP Procedures documentation.
|
||||||
|
*/
|
||||||
|
So what is the intent here? We are not expanding the scope of the licensed project beyond the RI, however, we want to make sure that the RI is fully covered such that an early contributor who stops adding new code still licenses the RI as a whole, including Maintenance Releases down the line.
|
||||||
|
|
||||||
|
A somewhat broader example is as follows:
|
||||||
|
|
||||||
|
/* Larger Works File for MyWidget
|
||||||
|
**
|
||||||
|
** Larger Works: MyApp
|
||||||
|
*/
|
||||||
|
Here, someone has set up the Larger Works file to act as a CLA for the application MyApp. The licensors of MyWidget are also licensing their patents for use in MyApp, and because there is no version restriction, this is presumably true for future versions of MyApp as well as the current version. If they wanted to license only the current version, they could have said:
|
||||||
|
|
||||||
|
/* Larger Works File for MyWidget
|
||||||
|
**
|
||||||
|
** Larger Works: MyApp 1.0
|
||||||
|
*/
|
||||||
|
Or if Company A wanted to use the UPL with a Larger Works file as a CLA for all of their products, they could use one of the following:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Company A Projects
|
||||||
|
**
|
||||||
|
** Larger Works: All Software Authored or Distributed by Company A
|
||||||
|
*/
|
||||||
|
or
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Company A Projects
|
||||||
|
**
|
||||||
|
** Larger Works: All Company A Products and Projects
|
||||||
|
*/
|
||||||
|
If the Apache Software Foundation wanted to use the UPL as a CLA, they could use the following:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to ASF Projects
|
||||||
|
**
|
||||||
|
** Larger Works: All Apache Software Foundation Projects
|
||||||
|
*/
|
||||||
|
or even more broadly, to include dependencies that are not ASF projects:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to ASF projects
|
||||||
|
**
|
||||||
|
** Larger Works: All Apache Software Foundation projects and their dependencies
|
||||||
|
*/
|
||||||
|
or they could make it project specific, for single or multiple Projects:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Apache Ant & Maven
|
||||||
|
**
|
||||||
|
** Larger Works: Apache Ant and Apache Maven and their subprojects
|
||||||
|
*/
|
||||||
|
or project and version specific:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Cassandra
|
||||||
|
**
|
||||||
|
** Larger Works: Apache Cassandra 2.0.15 and updates other than major version updates
|
||||||
|
*/
|
||||||
|
or subject area specific:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Build Tools
|
||||||
|
**
|
||||||
|
** Larger Works: All Apache Software Foundation projects pertaining to software builds and related processes, including without
|
||||||
|
** limitation Ant, Ant Libraries, Ivy, IvyDE, EasyAnt, Maven and other software for controlling build processes tools
|
||||||
|
*/
|
||||||
|
As you can see, the intent of the Larger Works file is to permit nearly infinite flexibility in what is being licensed beyond the Software itself, as every company and FOSS project has different needs. Note that while a comment format has been used for these examples, since this is not executable code, this isn't strictly necessary, it was done as a matter of habit and in case someone imports one of the files into some executable code.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
Can someone else change my Larger Works file and license my patents for other things?
|
||||||
|
To begin with, a party who alters the Larger Works file cannot alter the scope of the license someone else granted, any more than they could alter the license text of the UPL, the GPL, or any other license and assert that the original licensors are bound by their changes. So no one can offer a broader license to your patents than the one you declared. The only power they have is to sublicense downstream recipients on different terms under whatever IP rights they have.
|
||||||
|
|
||||||
|
And on the flip side, what happens if someone simply removes the Larger Works file from a code base and then redistributes without it? Then they are sublicensing the code to a downstream recipient on other terms, without any Larger Works being licensed under their own patents, though the license from the original licensors would still survive on those terms as to people who receive the Software from other sources.
|
||||||
|
|
||||||
|
Similarly, if someone alters the Larger Works file and then distributes the code to new recipients themselves under the UPL, then that new distributor is granting a license of a different scope than the one granted by the original authors, which they are permitted to do, just as they would be permitted to grant their own license to the code under the GPL or a commercial license. However, the polite thing to do in this case would be to address the altered scope of their license in the Larger Works file while leaving intact the Larger Works designation from prior licensors. Let's say, for example, we start off with a Larger Works file that looks like this, covering all of Java:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Project X
|
||||||
|
**
|
||||||
|
** Larger Works: All Oracle Java implementations including without limitation Oracle Java EE, Java SE, Java ME, and OpenJDK
|
||||||
|
*/
|
||||||
|
This would grant a license to all of Java from each contributor on those terms. Now let's say Company A only wants to license a particular JSR, not all of Java. They would ideally add separate license and Larger Works files that travel with their contributions calling out only that JSR, most effectively by placing them in a separate directly clearly demarcating what is under that license. (Separating these things out is always a trick when you try to commingle source in the same file, but if you keep files separate, this is not generally difficult.)
|
||||||
|
|
||||||
|
In the alternative, if the contributions were not easily separable from the larger project in another directory, and for some reason they could not add a separate directory just for the license either, and therefore absolutely needed a way to combine their own, distinct Larger Works designation with that from others, they could in theory also add their own declaration to the existing Larger Works file without purporting to change the license from others as follows:
|
||||||
|
|
||||||
|
/* Larger Works File for Contributions to Project X
|
||||||
|
**
|
||||||
|
** Larger Works: All Oracle Java implementations including without limitation Oracle Java EE, Java SE, Java ME, and OpenJDK
|
||||||
|
**
|
||||||
|
** Exception: Company A licenses only the following Larger Works: The Reference Implementation for JSR-xxx including
|
||||||
|
** Maintenance Releases as defined in the JCP Procedures documentation
|
||||||
|
*/
|
||||||
|
This would therefore clearly inform a recipient that only the RI was licensed under Company A's patents, but leave intact the reference to the broader license from other contributors.
|
||||||
|
|
||||||
|
There are certainly other possibilities for accommodating multiple Larger Works scopes as well, about which your counsel can advise you.
|
||||||
|
|
||||||
|
Return to FAQ.
|
||||||
|
|
||||||
|
Return to top.
|
||||||
133
LOCALIZACAO.md
Normal file
133
LOCALIZACAO.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Termos e frases configuráveis — pt-BR e en
|
||||||
|
|
||||||
|
Esta distribuição inclui dois catálogos completos, em JSON UTF-8:
|
||||||
|
|
||||||
|
- `libs/agent_framework/src/agent_framework/locales/pt-BR.json`
|
||||||
|
- `libs/agent_framework/src/agent_framework/locales/en.json`
|
||||||
|
|
||||||
|
Cada arquivo reúne termos reconhecidos, mensagens padronizadas, instruções de
|
||||||
|
recuperação de guardrails e padrões de extração. JSON permite editar o catálogo
|
||||||
|
sem uma dependência adicional no carregador. Os dois arquivos usam as mesmas chaves.
|
||||||
|
|
||||||
|
## Selecionar o idioma
|
||||||
|
|
||||||
|
No `.env` do backend:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
AGENT_LOCALE=en
|
||||||
|
```
|
||||||
|
|
||||||
|
Para português, use `AGENT_LOCALE=pt-BR` ou omita a variável. Também são aceitos
|
||||||
|
`pt`, `pt_BR`, `en-US` e `en-GB`. Uma opção inexistente gera erro; não há troca
|
||||||
|
silenciosa para português quando o idioma solicitado não está disponível.
|
||||||
|
|
||||||
|
O idioma é o padrão do backend. Os prompts e as configurações de negócio dos
|
||||||
|
agentes continuam sendo definidos pelo projeto; use prompts e descrições no
|
||||||
|
idioma desejado. Esta alteração não traduz automaticamente conteúdos de RAG,
|
||||||
|
respostas dos servidores MCP, mensagens declaradas por um workflow, arquivos de
|
||||||
|
exemplo, regras de negócio ou textos personalizados do aplicativo/frontend.
|
||||||
|
Prompts internos de classificação podem continuar em português: as decisões são
|
||||||
|
estruturadas. As instruções de recuperação que impunham português foram separadas
|
||||||
|
nos catálogos, incluindo sua variante em inglês.
|
||||||
|
|
||||||
|
## Personalizar sem alterar a biblioteca
|
||||||
|
|
||||||
|
Copie os catálogos para um diretório da aplicação e configure:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
AGENT_LOCALE=en
|
||||||
|
AGENT_LOCALES_DIR=./config/locales
|
||||||
|
```
|
||||||
|
|
||||||
|
Nesse exemplo, o arquivo usado será `./config/locales/en.json`. O catálogo é
|
||||||
|
completo, e não um arquivo de alterações parciais. Preserve as chaves e os campos
|
||||||
|
como `{action}`, `{target}`, `{options}` e `{subject}` ao traduzir as frases.
|
||||||
|
Não é necessário manter a mesma quantidade de termos nas listas.
|
||||||
|
|
||||||
|
Reinicie o backend após editar os arquivos: o carregador mantém os catálogos em
|
||||||
|
cache. Na inicialização, ele verifica a estrutura, os campos das mensagens, a
|
||||||
|
sintaxe dos padrões e conflitos entre confirmação e recusa.
|
||||||
|
|
||||||
|
As principais seções são:
|
||||||
|
|
||||||
|
| Seção | Conteúdo |
|
||||||
|
|---|---|
|
||||||
|
| `terms.confirmation` | Confirmações e recusas explícitas de transações |
|
||||||
|
| `terms.workflow` | Respostas de compatibilidade para checkpoints antigos |
|
||||||
|
| `terms.multi_intent` | Conjunções, verbos e prefixos de confirmação |
|
||||||
|
| `terms.global_router` | Termos de continuidade e encerramento |
|
||||||
|
| `terms.groundedness` | Indícios de especificidade e incerteza |
|
||||||
|
| `terms.mock` | Termos dos classificadores locais usados em testes/desenvolvimento |
|
||||||
|
| `messages` | Frases de confirmação, nova tentativa, esclarecimento e fallback |
|
||||||
|
| `prompts.fallback` | Instruções de reescrita e regeneração por guardrail |
|
||||||
|
| `prompts.judge_rewrite` | Instruções de recuperação dos judges |
|
||||||
|
| `memory` | Padrões para extração de memória de longo prazo |
|
||||||
|
| `protocol` | Dígitos, letras e nomes usados na vocalização de protocolos |
|
||||||
|
| `safety` | Padrões determinísticos de segurança e escopo |
|
||||||
|
| `legacy_extraction` | Padrões do extrator legado de pedido/motivo |
|
||||||
|
|
||||||
|
## Decisões internas e contratos de workflow
|
||||||
|
|
||||||
|
Os códigos internos permanecem estáveis: `confirm`, `reject`, `SIM`, `NAO`,
|
||||||
|
`OUTRO` e nomes de estados não devem ser traduzidos em arquivos existentes.
|
||||||
|
O catálogo trata as palavras reconhecidas e os textos apresentados.
|
||||||
|
|
||||||
|
Um workflow que declara `expected_input`, `reprompt` ou mensagens próprias
|
||||||
|
continua tendo precedência. Para aceitar `YES` e `NO` em um contrato enumerado,
|
||||||
|
configure essas opções no workflow e ajuste suas transições, ou mantenha os
|
||||||
|
códigos atuais e use seu classificador semântico para mapear as respostas.
|
||||||
|
O dicionário global não substitui silenciosamente o contrato de um fluxo.
|
||||||
|
|
||||||
|
As listas de confirmação transacional usam correspondência da frase completa,
|
||||||
|
ignorando maiúsculas, espaços repetidos e pontuação final simples. Não inclua
|
||||||
|
frases ambíguas como “entendi”/“understood” nessas listas: compreender uma
|
||||||
|
explicação não equivale a autorizar uma operação. O fallback semântico existente
|
||||||
|
continua disponível para respostas que não constam do catálogo.
|
||||||
|
|
||||||
|
Os padrões de segurança mantêm cobertura em português e inglês nos dois arquivos.
|
||||||
|
Nomes de concorrentes presentes em `safety` preservam a configuração original de
|
||||||
|
telecom; adapte essas regras quando mudar o domínio da aplicação.
|
||||||
|
|
||||||
|
## Vários idiomas no mesmo processo
|
||||||
|
|
||||||
|
Para aplicações que selecionam o idioma por agente ou atendimento, envolva a
|
||||||
|
execução completa (roteamento, runtime e guardrails) no contexto correspondente:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.localization import use_locale
|
||||||
|
|
||||||
|
with use_locale("en"):
|
||||||
|
# Execute aqui o roteamento, o agente e seus guardrails.
|
||||||
|
result = await handle_turn(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
`handle_turn` representa o ponto de entrada da sua aplicação; não é uma nova API
|
||||||
|
do framework. O contexto usa `ContextVar`, é isolado entre tarefas assíncronas e
|
||||||
|
restaura o idioma anterior ao sair. Não altere `os.environ` a cada atendimento.
|
||||||
|
O idioma da conversa não é detectado automaticamente.
|
||||||
|
|
||||||
|
## Validação desta entrega
|
||||||
|
|
||||||
|
- 41 testes novos de localização aprovados.
|
||||||
|
- 160 testes selecionados aprovados após a atualização de oito testes antigos.
|
||||||
|
- As oito expectativas antigas estavam desalinhadas com mudanças de contrato
|
||||||
|
anteriores à localização. Histórico, escopo e validação estão em
|
||||||
|
`ATUALIZACAO_TESTES.md`. Os 66 testes dos quatro arquivos atualizados também
|
||||||
|
passaram com a biblioteca original do ZIP.
|
||||||
|
- As verificações incluem execução/cancelamento real do runtime com ferramentas
|
||||||
|
simuladas, confirmação determinística no roteador sem LLM, múltiplas intenções,
|
||||||
|
retomada de workflows, memória, protocolos, segurança, concorrência e catálogo
|
||||||
|
personalizado.
|
||||||
|
- Não foram feitas chamadas a modelos remotos, OCI ou ferramentas reais de negócio.
|
||||||
|
|
||||||
|
Os testes novos estão em `tests/test_locale_catalogs.py`. Para executá-los em um
|
||||||
|
ambiente com as dependências do projeto e pytest/pytest-asyncio:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=libs/agent_framework/src pytest -q tests/test_locale_catalogs.py
|
||||||
|
```
|
||||||
|
|
||||||
|
O pacote de entrega conserva os arquivos do projeto e remove metadados do macOS,
|
||||||
|
cache de bytecode, builds antigos e diretórios de Git/IDE. As fontes corrigidas
|
||||||
|
ficam em `libs/agent_framework/src`. As variantes de demonstração em
|
||||||
|
`Tuning-Performance` mantêm suas configurações de domínio originais.
|
||||||
520
Long_Term_Memory_Implementation_Guide_EN.md
Normal file
520
Long_Term_Memory_Implementation_Guide_EN.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Long-Term Memory Implementation Guide
|
||||||
|
|
||||||
|
### Concept
|
||||||
|
|
||||||
|
Long-Term Memory (LTM) is the `agent_framework` capability that stores and retrieves durable facts beyond the lifetime of a conversation session.
|
||||||
|
|
||||||
|
Unlike message history, which is normally associated with a `session_id`, Long-Term Memory is associated with the business identity of the user or customer. In the current implementation, this identity consists of:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows an agent to retrieve preferences, identity information, projects and constraints even when a new session is created.
|
||||||
|
|
||||||
|
### Purpose
|
||||||
|
|
||||||
|
Long-Term Memory is used to:
|
||||||
|
|
||||||
|
- maintain continuity across sessions;
|
||||||
|
- personalize responses;
|
||||||
|
- prevent users from repeating previously supplied information;
|
||||||
|
- reduce the need to send the full conversation history to the model;
|
||||||
|
- store preferences, current projects, preferred names and constraints;
|
||||||
|
- isolate memory across tenants, agents and customers.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Session A:
|
||||||
|
"Call me Cris. My preferred language is Python."
|
||||||
|
|
||||||
|
Session B, with another session_id and the same customer_key:
|
||||||
|
"What do you remember about me?"
|
||||||
|
|
||||||
|
Expected response:
|
||||||
|
"Your preferred name is Cris and your preferred language is Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory type differences
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Stores messages from the current conversation and is normally associated with the `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Stores a summary of the conversation to reduce the context size sent to the model.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Stores durable facts across sessions and is associated with the business identity, primarily the `customer_key`.
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Coordinates:
|
||||||
|
|
||||||
|
- memory loading;
|
||||||
|
- identity-based retrieval;
|
||||||
|
- context rendering;
|
||||||
|
- durable fact extraction;
|
||||||
|
- fact persistence;
|
||||||
|
- deduplication and updates.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Persistence interface used by the manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Reference implementation based on SQLite.
|
||||||
|
|
||||||
|
It is suitable for:
|
||||||
|
|
||||||
|
- local development;
|
||||||
|
- testing;
|
||||||
|
- demonstrations;
|
||||||
|
- low-scale environments.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
In-memory implementation used for quick tests.
|
||||||
|
|
||||||
|
Its content is lost when the backend process stops.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Identifies durable facts in messages.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Data model representing a persisted item, including identity, key, value, category, confidence and metadata.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Loads memory before agent execution and injects the rendered context into the prompt.
|
||||||
|
|
||||||
|
#### persist_long_term_memory node
|
||||||
|
|
||||||
|
LangGraph node responsible for persisting facts after the final response has been generated and validated.
|
||||||
|
|
||||||
|
### File structure
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Execution flow
|
||||||
|
|
||||||
|
```text
|
||||||
|
User message
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent prompt
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agent
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Framework configuration
|
||||||
|
|
||||||
|
### New modules
|
||||||
|
|
||||||
|
Copy:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update memory/__init__.py
|
||||||
|
|
||||||
|
Export the Long-Term Memory components:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update settings.py
|
||||||
|
|
||||||
|
Add:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentRuntime integration
|
||||||
|
|
||||||
|
The runtime must:
|
||||||
|
|
||||||
|
1. verify that the feature is enabled;
|
||||||
|
2. create the manager when needed;
|
||||||
|
3. retrieve facts using the identity;
|
||||||
|
4. populate the workflow state;
|
||||||
|
5. inject the rendered context into the prompt.
|
||||||
|
|
||||||
|
State fields:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### AgentWorkflow initialization
|
||||||
|
|
||||||
|
Create the manager in `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Correct agent initialization
|
||||||
|
|
||||||
|
Do not pass `long_term_memory_manager` through `agent_kwargs` when the constructors of `BillingAgent`, `ProductAgent`, `OrdersAgent` and `SupportAgent` do not declare that parameter.
|
||||||
|
|
||||||
|
This initialization causes an error:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting error:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
The recommended approach is to create agents using their existing signatures and inject the manager as an attribute after initialization:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
This approach avoids changing every agent constructor and keeps the feature encapsulated in the framework.
|
||||||
|
|
||||||
|
### LangGraph configuration
|
||||||
|
|
||||||
|
Register the node:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Update the edges:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implement:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Final flow:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment variables
|
||||||
|
|
||||||
|
```env
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### SQLite database path
|
||||||
|
|
||||||
|
A relative path is resolved from the directory in which the backend is started.
|
||||||
|
|
||||||
|
To prevent different databases from being created accidentally, prefer an absolute path in development environments:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the directory before starting:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
### Test 1 — Persistence
|
||||||
|
|
||||||
|
Send:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Call me Cris. My preferred language is Python and my current project is Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 2 — Retrieval in another session
|
||||||
|
|
||||||
|
Use another `session_id` while keeping the same `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "What do you remember about me, my preferences and my project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected result:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Your preferred name is Cris.
|
||||||
|
Your preferred language is Python.
|
||||||
|
Your current project is Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test 3 — Isolation
|
||||||
|
|
||||||
|
Use another customer:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "another-customer",
|
||||||
|
"message": "What is my preferred name and current project?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The data associated with `11999999999` must not be returned.
|
||||||
|
|
||||||
|
### Test 4 — Frontend reset
|
||||||
|
|
||||||
|
Restart or reset the frontend and verify that it still sends the same `customer_key`.
|
||||||
|
|
||||||
|
Memory must survive a `session_id` change. Resetting the frontend does not delete the SQLite database.
|
||||||
|
|
||||||
|
### Test 5 — Backend restart
|
||||||
|
|
||||||
|
Restart Uvicorn and repeat the query.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
memory must remain available.
|
||||||
|
|
||||||
|
With:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
memory is lost when the process stops.
|
||||||
|
|
||||||
|
### Direct SQLite verification
|
||||||
|
|
||||||
|
Find the database:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Open it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Query:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Success criteria
|
||||||
|
|
||||||
|
The implementation is working when:
|
||||||
|
|
||||||
|
- memory is retrieved with another `session_id`;
|
||||||
|
- the same `customer_key` retrieves previous facts;
|
||||||
|
- another `customer_key` cannot access those facts;
|
||||||
|
- restarting the frontend does not erase memory;
|
||||||
|
- restarting the backend does not erase memory when using SQLite;
|
||||||
|
- the `persist_long_term_memory` node runs;
|
||||||
|
- the prompt receives `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Best practices
|
||||||
|
|
||||||
|
- Persist only durable facts.
|
||||||
|
- Do not store the complete conversation as Long-Term Memory.
|
||||||
|
- Isolate data by `tenant_id`, `agent_id` and `customer_key`.
|
||||||
|
- Do not use `session_id` as the permanent user identity.
|
||||||
|
- Persist only after final validations.
|
||||||
|
- Avoid persisting temporary tool results.
|
||||||
|
- Record telemetry for reads, writes, updates and failures.
|
||||||
|
- Define retention and deletion policies.
|
||||||
|
- Use an absolute SQLite path in environments with multiple working directories.
|
||||||
|
- Move to an enterprise database for production and high-availability environments.
|
||||||
|
|
||||||
|
### Reference implementation limitations
|
||||||
|
|
||||||
|
The current implementation uses rule-based extraction and SQLite as the reference provider.
|
||||||
|
|
||||||
|
Recommended future enhancements:
|
||||||
|
|
||||||
|
- LLM-based fact extraction;
|
||||||
|
- vector-based semantic memory;
|
||||||
|
- episodic memory;
|
||||||
|
- expiration and versioning;
|
||||||
|
- semantic deduplication;
|
||||||
|
- consent policies;
|
||||||
|
- query and deletion APIs;
|
||||||
|
- Oracle Autonomous Database provider;
|
||||||
|
- encryption and sensitive-data classification.
|
||||||
272
MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
272
MANUAL_AGENT_PLATFORM_GATEWAYS.md
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
# Agent Platform OCI — Manual Oficial de Agent Gateway e MCP Gateway
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Este documento consolida:
|
||||||
|
- Arquitetura oficial
|
||||||
|
- Inventário dos componentes
|
||||||
|
- Procedimento completo de execução local
|
||||||
|
- MCP Gateway
|
||||||
|
- Agent Gateway
|
||||||
|
- Backend Runtime
|
||||||
|
- Frontend
|
||||||
|
- Testes E2E
|
||||||
|
- Troubleshooting
|
||||||
|
- Decisões arquiteturais
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Arquitetura Oficial
|
||||||
|
|
||||||
|
Frontend (5173)
|
||||||
|
↓
|
||||||
|
Agent Gateway (9000)
|
||||||
|
↓
|
||||||
|
Agent Template Backend / Runtime (8000)
|
||||||
|
↓
|
||||||
|
MCP Gateway (8300)
|
||||||
|
↓
|
||||||
|
Telecom MCP Server (8100)
|
||||||
|
Retail MCP Server (8200)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Portas Oficiais
|
||||||
|
|
||||||
|
| Componente | Porta |
|
||||||
|
|------------|--------|
|
||||||
|
| Frontend | 5173 |
|
||||||
|
| Agent Gateway | 9000 |
|
||||||
|
| Backend Runtime | 8000 |
|
||||||
|
| MCP Gateway | 8300 |
|
||||||
|
| Telecom MCP Server | 8100 |
|
||||||
|
| Retail MCP Server | 8200 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Variáveis Oficiais
|
||||||
|
|
||||||
|
## Agent Template Backend
|
||||||
|
|
||||||
|
ENABLE_MCP_TOOLS=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
MCP_GATEWAY_TIMEOUT_SECONDS=60
|
||||||
|
MCP_GATEWAY_AGENT_ID=telecom_contas
|
||||||
|
MCP_GATEWAY_TENANT_ID=default
|
||||||
|
|
||||||
|
## Agent Gateway
|
||||||
|
|
||||||
|
DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
## MCP Gateway
|
||||||
|
|
||||||
|
MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ordem de Inicialização
|
||||||
|
|
||||||
|
1. Telecom MCP Server
|
||||||
|
2. Retail MCP Server
|
||||||
|
3. MCP Gateway
|
||||||
|
4. Agent Template Backend
|
||||||
|
5. Agent Gateway
|
||||||
|
6. Frontend
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 1 — Telecom MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/telecom_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8100 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 2 — Retail MCP Server
|
||||||
|
|
||||||
|
cd mcp/servers/retail_mcp_server
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn main:app --host 0.0.0.0 --port 8200 --reload
|
||||||
|
|
||||||
|
Validação:
|
||||||
|
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 3 — MCP Gateway
|
||||||
|
|
||||||
|
cd apps/mcp_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export MCP_GATEWAY_CONFIG_PATH=config/mcp_gateway.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8300 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8300/health
|
||||||
|
curl http://localhost:8300/ready
|
||||||
|
curl http://localhost:8300/v1/tools
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:8300/v1/tools/consultar_fatura/invoke
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 4 — Agent Template Backend
|
||||||
|
|
||||||
|
cd templates/agent_template_backend
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
curl http://localhost:8000/agents
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 5 — Agent Gateway
|
||||||
|
|
||||||
|
cd apps/agent_gateway
|
||||||
|
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
export DEFAULT_AGENT_BACKEND_URL=http://localhost:8000
|
||||||
|
export AGENT_GATEWAY_GOVERNANCE_CONFIG=config/gateway_governance.yaml
|
||||||
|
|
||||||
|
python -m uvicorn app.main:app --host 0.0.0.0 --port 9000 --reload
|
||||||
|
|
||||||
|
Validações:
|
||||||
|
|
||||||
|
curl http://localhost:9000/health
|
||||||
|
|
||||||
|
Teste:
|
||||||
|
|
||||||
|
curl -X POST http://localhost:9000/gateway/message
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Terminal 6 — Frontend
|
||||||
|
|
||||||
|
cd agent_frontend
|
||||||
|
|
||||||
|
npm install
|
||||||
|
|
||||||
|
npm run dev -- --host 0.0.0.0 --port 5173
|
||||||
|
|
||||||
|
Abrir:
|
||||||
|
|
||||||
|
http://localhost:5173
|
||||||
|
|
||||||
|
Backend URL:
|
||||||
|
|
||||||
|
http://localhost:9000
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fluxo de Tools
|
||||||
|
|
||||||
|
Agent
|
||||||
|
↓
|
||||||
|
MCPToolRouter
|
||||||
|
↓
|
||||||
|
MCPGatewayClient
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
MCP Server
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Teste Integrado E2E
|
||||||
|
|
||||||
|
Frontend
|
||||||
|
↓
|
||||||
|
Agent Gateway
|
||||||
|
↓
|
||||||
|
Backend Runtime
|
||||||
|
↓
|
||||||
|
MCP Gateway
|
||||||
|
↓
|
||||||
|
Telecom MCP Server
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
- Agent Gateway recebe requisição
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- MCP Gateway resolve tool
|
||||||
|
- MCP Server responde
|
||||||
|
- Usuário recebe resposta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Backend chamando MCP Server direto
|
||||||
|
|
||||||
|
Confirmar:
|
||||||
|
|
||||||
|
MCP_GATEWAY_ENABLED=true
|
||||||
|
|
||||||
|
MCP_GATEWAY_URL=http://localhost:8300
|
||||||
|
|
||||||
|
## Porta incorreta
|
||||||
|
|
||||||
|
A porta oficial do MCP Gateway é:
|
||||||
|
|
||||||
|
8300
|
||||||
|
|
||||||
|
## Agent Gateway não encontra Backend
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8000/health
|
||||||
|
|
||||||
|
## MCP Gateway não encontra MCP Server
|
||||||
|
|
||||||
|
Validar:
|
||||||
|
|
||||||
|
curl http://localhost:8100/health
|
||||||
|
curl http://localhost:8200/health
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Decisões Arquiteturais Oficiais
|
||||||
|
|
||||||
|
- Agent Gateway centraliza governança
|
||||||
|
- Runtime executa LangGraph
|
||||||
|
- Runtime executa LLM
|
||||||
|
- MCP Gateway centraliza tools
|
||||||
|
- MCP Servers executam tools
|
||||||
|
- Backend usa MCP Gateway
|
||||||
|
- gateway_runtime.env.example foi removido
|
||||||
|
- MCP_GATEWAY_* fica no .env do backend
|
||||||
|
- Porta oficial MCP Gateway = 8300
|
||||||
1920
MANUAL_REGRAS_AGENT_TEMPLATE_BACKEND.md
Normal file
1920
MANUAL_REGRAS_AGENT_TEMPLATE_BACKEND.md
Normal file
File diff suppressed because it is too large
Load Diff
1917
MANUAL_RULES_AGENT_TEMPLATE_BACKEND_EN.md
Normal file
1917
MANUAL_RULES_AGENT_TEMPLATE_BACKEND_EN.md
Normal file
File diff suppressed because it is too large
Load Diff
29
MULTI_ITEM_FRAMEWORK_FIX_20260918.md
Normal file
29
MULTI_ITEM_FRAMEWORK_FIX_20260918.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Multi-item result preservation fix
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
Preserve the terminal outcome of each item executed by the primary transactional tool. A failure in a later auxiliary/composite step must not erase successful item outcomes or turn an already-completed multi-item operation into a total failure.
|
||||||
|
|
||||||
|
## Framework-only change
|
||||||
|
No agent/template business logic is required for this fix. The runtime now recognizes an explicit structural contract on the requested tool: a `results` array with two or more entries carrying boolean `success` or `ok` fields.
|
||||||
|
|
||||||
|
For workflow-backed tools the authoritative source is preferentially:
|
||||||
|
|
||||||
|
`workflow.output[tool_name].results`
|
||||||
|
|
||||||
|
The framework derives one of:
|
||||||
|
- `SUCCESS`
|
||||||
|
- `PARTIAL_SUCCESS`
|
||||||
|
- `FAILED`
|
||||||
|
|
||||||
|
and publishes a generic `metadata.multi_item` summary with item counts.
|
||||||
|
|
||||||
|
When one or more primary items completed successfully but the outer wrapper is `ok=false` because a later auxiliary/post-processing step failed, the normalized result preserves the original failure as `original_ok=false` / `secondary_error` while exposing the requested operation as operationally successful (`ok=true`).
|
||||||
|
|
||||||
|
The LLM context also receives an explicit generic instruction that successful per-item terminal evidence cannot be overwritten by a secondary error and that mixed outcomes must be reported individually.
|
||||||
|
|
||||||
|
## Regression tests
|
||||||
|
Added `tests/test_multi_item_result_normalization.py` covering:
|
||||||
|
- 2 success + 1 failure -> `PARTIAL_SUCCESS`;
|
||||||
|
- all primary items successful + later secondary failure -> primary `SUCCESS` preserved;
|
||||||
|
- single-item behavior unchanged;
|
||||||
|
- unrelated nested tool results cannot override failure of the requested primary tool.
|
||||||
520
Manual_Long_Term_Memory_PT.md
Normal file
520
Manual_Long_Term_Memory_PT.md
Normal file
@@ -0,0 +1,520 @@
|
|||||||
|
### Manual de Implementação — Long-Term Memory
|
||||||
|
|
||||||
|
### Conceito
|
||||||
|
|
||||||
|
A Long-Term Memory (LTM) é a capacidade do `agent_framework` de armazenar e recuperar fatos duradouros além da duração de uma sessão de conversa.
|
||||||
|
|
||||||
|
Diferentemente do histórico de mensagens, que normalmente está associado a um `session_id`, a memória de longo prazo é associada à identidade de negócio do usuário ou cliente. Na implementação atual, essa identidade é composta por:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tenant_id
|
||||||
|
agent_id
|
||||||
|
customer_key
|
||||||
|
```
|
||||||
|
|
||||||
|
Isso permite que um agente recupere preferências, informações de identidade, projetos e restrições mesmo quando uma nova sessão é criada.
|
||||||
|
|
||||||
|
### Para que serve
|
||||||
|
|
||||||
|
A Long-Term Memory serve para:
|
||||||
|
|
||||||
|
- manter continuidade entre sessões;
|
||||||
|
- personalizar respostas;
|
||||||
|
- evitar que o usuário repita informações já fornecidas;
|
||||||
|
- reduzir a necessidade de enviar todo o histórico ao modelo;
|
||||||
|
- armazenar preferências, projetos atuais, nomes preferidos e restrições;
|
||||||
|
- isolar a memória entre tenants, agentes e clientes.
|
||||||
|
|
||||||
|
Exemplo:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Sessão A:
|
||||||
|
"Me chame de Cris. Minha linguagem preferida é Python."
|
||||||
|
|
||||||
|
Sessão B, com outro session_id e o mesmo customer_key:
|
||||||
|
"O que você lembra sobre mim?"
|
||||||
|
|
||||||
|
Resposta esperada:
|
||||||
|
"Seu nome preferido é Cris e sua linguagem preferida é Python."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Diferença entre os tipos de memória
|
||||||
|
|
||||||
|
#### Conversation Memory
|
||||||
|
|
||||||
|
Mantém as mensagens da conversa atual e normalmente está associada ao `session_id`.
|
||||||
|
|
||||||
|
#### Summary Memory
|
||||||
|
|
||||||
|
Mantém um resumo da conversa para reduzir o tamanho do contexto enviado ao modelo.
|
||||||
|
|
||||||
|
#### Long-Term Memory
|
||||||
|
|
||||||
|
Mantém fatos duradouros entre sessões e é associada à identidade de negócio, principalmente ao `customer_key`.
|
||||||
|
|
||||||
|
### Componentes da funcionalidade
|
||||||
|
|
||||||
|
#### LongTermMemoryManager
|
||||||
|
|
||||||
|
Responsável por coordenar:
|
||||||
|
|
||||||
|
- carregamento das memórias;
|
||||||
|
- recuperação por identidade;
|
||||||
|
- renderização do contexto;
|
||||||
|
- extração de novos fatos;
|
||||||
|
- persistência dos fatos;
|
||||||
|
- deduplicação e atualização.
|
||||||
|
|
||||||
|
#### LongTermMemoryStore
|
||||||
|
|
||||||
|
Interface de persistência utilizada pelo manager.
|
||||||
|
|
||||||
|
#### SQLiteLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação de referência baseada em SQLite.
|
||||||
|
|
||||||
|
É apropriada para:
|
||||||
|
|
||||||
|
- desenvolvimento local;
|
||||||
|
- testes;
|
||||||
|
- demonstrações;
|
||||||
|
- ambientes de baixa escala.
|
||||||
|
|
||||||
|
#### InMemoryLongTermMemoryStore
|
||||||
|
|
||||||
|
Implementação em memória utilizada para testes rápidos.
|
||||||
|
|
||||||
|
O conteúdo é perdido quando o processo do backend é encerrado.
|
||||||
|
|
||||||
|
#### LongTermMemoryExtractor
|
||||||
|
|
||||||
|
Responsável por identificar fatos duradouros nas mensagens.
|
||||||
|
|
||||||
|
Exemplos de fatos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
preferred_name = Cris
|
||||||
|
preferred_language = Python
|
||||||
|
current_project = Atlas
|
||||||
|
```
|
||||||
|
|
||||||
|
#### LongTermMemoryItem
|
||||||
|
|
||||||
|
Modelo que representa um item persistido, incluindo identidade, chave, valor, categoria, confiança e metadados.
|
||||||
|
|
||||||
|
#### AgentRuntime
|
||||||
|
|
||||||
|
Carrega a memória antes da execução do agente e injeta o contexto no prompt.
|
||||||
|
|
||||||
|
#### Nó persist_long_term_memory
|
||||||
|
|
||||||
|
Nó do LangGraph responsável por persistir os fatos após a geração e validação da resposta final.
|
||||||
|
|
||||||
|
### Estrutura dos arquivos
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/
|
||||||
|
└── agent_framework/
|
||||||
|
└── src/
|
||||||
|
└── agent_framework/
|
||||||
|
└── memory/
|
||||||
|
├── __init__.py
|
||||||
|
├── long_term_extractor.py
|
||||||
|
├── long_term_memory.py
|
||||||
|
├── long_term_models.py
|
||||||
|
└── long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fluxo de execução
|
||||||
|
|
||||||
|
```text
|
||||||
|
Mensagem do usuário
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
AgentRuntime.prepare_memory_context()
|
||||||
|
│
|
||||||
|
├── Conversation Memory
|
||||||
|
├── Summary Memory
|
||||||
|
└── Long-Term Memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
long_term_memory_context
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Prompt do agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Agente
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Guardrails / Judges / Supervisor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryExtractor
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
LongTermMemoryStore
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuração do framework
|
||||||
|
|
||||||
|
### Novos módulos
|
||||||
|
|
||||||
|
Copie os arquivos:
|
||||||
|
|
||||||
|
```text
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_extractor.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_memory.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_models.py
|
||||||
|
libs/agent_framework/src/agent_framework/memory/long_term_store.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de memory/__init__.py
|
||||||
|
|
||||||
|
Exporte os componentes da Long-Term Memory:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from agent_framework.memory.long_term_memory import (
|
||||||
|
LongTermMemoryManager,
|
||||||
|
create_long_term_memory_manager,
|
||||||
|
)
|
||||||
|
from agent_framework.memory.long_term_models import LongTermMemoryItem
|
||||||
|
from agent_framework.memory.long_term_store import (
|
||||||
|
InMemoryLongTermMemoryStore,
|
||||||
|
LongTermMemoryStore,
|
||||||
|
SQLiteLongTermMemoryStore,
|
||||||
|
create_long_term_memory_store,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Atualização de settings.py
|
||||||
|
|
||||||
|
Adicione as configurações:
|
||||||
|
|
||||||
|
```python
|
||||||
|
ENABLE_LONG_TERM_MEMORY: bool = False
|
||||||
|
LONG_TERM_MEMORY_PROVIDER: str = "sqlite"
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH: str = "./data/agent_framework.db"
|
||||||
|
LONG_TERM_MEMORY_TABLE: str = "agentfw_long_term_memory"
|
||||||
|
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS: int = 20
|
||||||
|
LONG_TERM_MEMORY_MIN_CONFIDENCE: float = 0.70
|
||||||
|
LONG_TERM_MEMORY_AUTO_EXTRACT: bool = True
|
||||||
|
LONG_TERM_MEMORY_INJECT_CONTEXT: bool = True
|
||||||
|
```
|
||||||
|
|
||||||
|
### Integração com AgentRuntime
|
||||||
|
|
||||||
|
O runtime deve:
|
||||||
|
|
||||||
|
1. verificar se a funcionalidade está habilitada;
|
||||||
|
2. criar o manager quando necessário;
|
||||||
|
3. recuperar os fatos pela identidade;
|
||||||
|
4. preencher o estado;
|
||||||
|
5. injetar o contexto no prompt.
|
||||||
|
|
||||||
|
Campos adicionados ao estado:
|
||||||
|
|
||||||
|
```python
|
||||||
|
long_term_memories: list[dict]
|
||||||
|
long_term_memory_context: str
|
||||||
|
long_term_memory_write_result: dict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização no AgentWorkflow
|
||||||
|
|
||||||
|
O manager deve ser criado no `AgentWorkflow`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
self.long_term_memory_manager = create_long_term_memory_manager(
|
||||||
|
settings,
|
||||||
|
telemetry=telemetry,
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inicialização correta dos agentes
|
||||||
|
|
||||||
|
O `long_term_memory_manager` não deve ser passado pelo `agent_kwargs` caso os construtores de `BillingAgent`, `ProductAgent`, `OrdersAgent` e `SupportAgent` não declarem esse parâmetro.
|
||||||
|
|
||||||
|
Esta inicialização causa erro:
|
||||||
|
|
||||||
|
```python
|
||||||
|
agent_kwargs = {
|
||||||
|
"telemetry": telemetry,
|
||||||
|
"settings": settings,
|
||||||
|
"memory": memory,
|
||||||
|
"summary_memory": summary_memory,
|
||||||
|
"long_term_memory_manager": self.long_term_memory_manager,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.billing = BillingAgent(llm, **agent_kwargs)
|
||||||
|
```
|
||||||
|
|
||||||
|
Erro resultante:
|
||||||
|
|
||||||
|
```text
|
||||||
|
TypeError: BillingAgent.__init__() got an unexpected keyword argument
|
||||||
|
'long_term_memory_manager'
|
||||||
|
```
|
||||||
|
|
||||||
|
A forma recomendada é criar os agentes com a assinatura já existente e injetar o manager como atributo após a inicialização:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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)
|
||||||
|
|
||||||
|
for agent in (
|
||||||
|
self.billing,
|
||||||
|
self.product,
|
||||||
|
self.orders,
|
||||||
|
self.support,
|
||||||
|
):
|
||||||
|
agent.long_term_memory_manager = self.long_term_memory_manager
|
||||||
|
```
|
||||||
|
|
||||||
|
Essa abordagem evita alterar os construtores de todos os agentes e mantém a funcionalidade encapsulada no framework.
|
||||||
|
|
||||||
|
### Configuração do LangGraph
|
||||||
|
|
||||||
|
Registre o nó:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self._node(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
self.persist_long_term_memory,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Altere o fluxo:
|
||||||
|
|
||||||
|
```python
|
||||||
|
builder.add_edge(
|
||||||
|
"supervisor_review",
|
||||||
|
"persist_long_term_memory",
|
||||||
|
)
|
||||||
|
builder.add_edge(
|
||||||
|
"persist_long_term_memory",
|
||||||
|
"persist",
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemente o método:
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def persist_long_term_memory(
|
||||||
|
self,
|
||||||
|
state: AgentState,
|
||||||
|
) -> dict[str, object]:
|
||||||
|
result = await self.long_term_memory_manager.persist_turn(state)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"long_term_memory_write_result": result,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Fluxo final:
|
||||||
|
|
||||||
|
```text
|
||||||
|
supervisor_review
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist_long_term_memory
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
persist
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variáveis de ambiente
|
||||||
|
|
||||||
|
```env
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
### Caminho do banco SQLite
|
||||||
|
|
||||||
|
O caminho relativo é resolvido a partir do diretório em que o backend é iniciado.
|
||||||
|
|
||||||
|
Para evitar que bancos diferentes sejam criados acidentalmente, prefira um caminho absoluto em ambientes de desenvolvimento:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_SQLITE_PATH=/mnt/c/Asus_Projects/agent_platform_oci_long_term_memory/data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Crie a pasta antes de iniciar:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Como testar
|
||||||
|
|
||||||
|
### Teste 1 — Gravação
|
||||||
|
|
||||||
|
Envie:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-a",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "Me chame de Cris. Minha linguagem preferida é Python e meu projeto atual se chama Atlas."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 2 — Recuperação em outra sessão
|
||||||
|
|
||||||
|
Utilize outro `session_id`, mantendo o mesmo `customer_key`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-b",
|
||||||
|
"customer_key": "11999999999",
|
||||||
|
"message": "O que você lembra sobre mim, minhas preferências e meu projeto?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Seu nome preferido é Cris.
|
||||||
|
Sua linguagem preferida é Python.
|
||||||
|
Seu projeto atual se chama Atlas.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste 3 — Isolamento
|
||||||
|
|
||||||
|
Utilize outro cliente:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "default:telecom_contas:memory-session-c",
|
||||||
|
"customer_key": "outro-cliente",
|
||||||
|
"message": "Qual é meu nome preferido e qual é meu projeto atual?"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Os dados de `11999999999` não devem aparecer.
|
||||||
|
|
||||||
|
### Teste 4 — Reinicialização do frontend
|
||||||
|
|
||||||
|
Reinicie ou resete o frontend e confirme que ele continua enviando o mesmo `customer_key`.
|
||||||
|
|
||||||
|
A memória deve sobreviver à troca do `session_id`. O reset do frontend não apaga o SQLite.
|
||||||
|
|
||||||
|
### Teste 5 — Reinicialização do backend
|
||||||
|
|
||||||
|
Reinicie o Uvicorn e repita a consulta.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=sqlite
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória deve continuar disponível.
|
||||||
|
|
||||||
|
Com:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LONG_TERM_MEMORY_PROVIDER=memory
|
||||||
|
```
|
||||||
|
|
||||||
|
a memória será perdida quando o processo for encerrado.
|
||||||
|
|
||||||
|
### Verificação direta no SQLite
|
||||||
|
|
||||||
|
Localize o banco:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
find . -name "agent_framework.db" -type f
|
||||||
|
```
|
||||||
|
|
||||||
|
Abra:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 ./data/agent_framework.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Consulte:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
SELECT
|
||||||
|
tenant_id,
|
||||||
|
agent_id,
|
||||||
|
customer_key,
|
||||||
|
memory_type,
|
||||||
|
memory_key,
|
||||||
|
memory_value,
|
||||||
|
confidence,
|
||||||
|
created_at,
|
||||||
|
updated_at
|
||||||
|
FROM agentfw_long_term_memory
|
||||||
|
ORDER BY updated_at DESC;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Critérios de sucesso
|
||||||
|
|
||||||
|
A implementação está funcionando quando:
|
||||||
|
|
||||||
|
- a memória é recuperada com outro `session_id`;
|
||||||
|
- o mesmo `customer_key` recupera os fatos anteriores;
|
||||||
|
- outro `customer_key` não acessa esses fatos;
|
||||||
|
- reiniciar o frontend não apaga a memória;
|
||||||
|
- reiniciar o backend não apaga a memória quando o provider é SQLite;
|
||||||
|
- o nó `persist_long_term_memory` é executado;
|
||||||
|
- o prompt recebe `long_term_memory_context`.
|
||||||
|
|
||||||
|
### Boas práticas
|
||||||
|
|
||||||
|
- Persistir somente fatos duradouros.
|
||||||
|
- Não armazenar a conversa completa como Long-Term Memory.
|
||||||
|
- Isolar dados por `tenant_id`, `agent_id` e `customer_key`.
|
||||||
|
- Não utilizar `session_id` como identidade permanente do usuário.
|
||||||
|
- Persistir somente depois das validações finais.
|
||||||
|
- Evitar armazenar resultados temporários de ferramentas.
|
||||||
|
- Registrar telemetria de leitura, escrita, atualização e falha.
|
||||||
|
- Definir políticas de retenção e exclusão.
|
||||||
|
- Usar caminho absoluto para SQLite em ambientes com múltiplos diretórios de execução.
|
||||||
|
- Migrar para um banco corporativo em ambientes de produção e alta disponibilidade.
|
||||||
|
|
||||||
|
### Limitações da implementação de referência
|
||||||
|
|
||||||
|
A implementação atual utiliza extração baseada em regras e SQLite como provider de referência.
|
||||||
|
|
||||||
|
Evoluções recomendadas:
|
||||||
|
|
||||||
|
- extração de fatos com LLM;
|
||||||
|
- memória semântica com vetores;
|
||||||
|
- memória episódica;
|
||||||
|
- expiração e versionamento;
|
||||||
|
- deduplicação semântica;
|
||||||
|
- política de consentimento;
|
||||||
|
- API de consulta e exclusão;
|
||||||
|
- provider Oracle Autonomous Database;
|
||||||
|
- criptografia e classificação de dados sensíveis.
|
||||||
17
OBSERVABILITY_FIX_NOTES.md
Normal file
17
OBSERVABILITY_FIX_NOTES.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Observability severity patch
|
||||||
|
|
||||||
|
- Central `severity_policy.py` classifies explicit MCP failures, guardrail blocks, judge rejections, RAG failures and memory/cache errors.
|
||||||
|
- `Telemetry.event()` accepts `level` and `status_message` and preserves WARNING/ERROR events as individual observations in compact mode.
|
||||||
|
- `Telemetry.span()` handle supports `set_status(level, message)`; failed MCP results mark their span ERROR without raising exceptions.
|
||||||
|
- Long-term-memory read/write failures emit telemetry while preserving existing fallback behavior.
|
||||||
|
- Guardrail blocked and negative judge evaluation emit WARNING when these helper methods are invoked.
|
||||||
|
- Normal cache misses and empty RAG results are not automatically failures.
|
||||||
|
|
||||||
|
## Scope / limitations
|
||||||
|
- Existing call sites must invoke `GuardrailTelemetry.blocked` for guardrail blocking events; this patch does not rewrite every pipeline call site.
|
||||||
|
- Redis cache currently has no injected Telemetry instance, so its existing logger errors are not yet Langfuse observations. A separate dependency-injection change is required to cover that path safely.
|
||||||
|
- RAG database connection errors that are caught by external KBDB integrations without emitting a telemetry event cannot be classified automatically.
|
||||||
|
- No live Langfuse ingestion test was possible in this environment. Verify both compact and verbose modes against your server.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
`PYTHONPATH=libs/agent_framework/src python -m pytest -q tests/test_observability_severity_policy.py`
|
||||||
11900
README_pt.md
Normal file
11900
README_pt.md
Normal file
File diff suppressed because it is too large
Load Diff
38
SECURITY.md
Normal file
38
SECURITY.md
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Reporting security vulnerabilities
|
||||||
|
|
||||||
|
Oracle values the independent security research community and believes that
|
||||||
|
responsible disclosure of security vulnerabilities helps us ensure the security
|
||||||
|
and privacy of all our users.
|
||||||
|
|
||||||
|
Please do NOT raise a GitHub Issue to report a security vulnerability. If you
|
||||||
|
believe you have found a security vulnerability, please submit a report to
|
||||||
|
[secalert_us@oracle.com][1] preferably with a proof of concept. Please review
|
||||||
|
some additional information on [how to report security vulnerabilities to Oracle][2].
|
||||||
|
We encourage people who contact Oracle Security to use email encryption using
|
||||||
|
[our encryption key][3].
|
||||||
|
|
||||||
|
We ask that you do not use other channels or contact the project maintainers
|
||||||
|
directly.
|
||||||
|
|
||||||
|
Non-vulnerability related security issues including ideas for new or improved
|
||||||
|
security features are welcome on GitHub Issues.
|
||||||
|
|
||||||
|
## Security updates, alerts and bulletins
|
||||||
|
|
||||||
|
Security updates will be released on a regular cadence. Many of our projects
|
||||||
|
will typically release security fixes in conjunction with the
|
||||||
|
Oracle Critical Patch Update program. Additional
|
||||||
|
information, including past advisories, is available on our [security alerts][4]
|
||||||
|
page.
|
||||||
|
|
||||||
|
## Security-related information
|
||||||
|
|
||||||
|
We will provide security related information such as a threat model, considerations
|
||||||
|
for secure use, or any known security issues in our documentation. Please note
|
||||||
|
that labs and sample code are intended to demonstrate a concept and may not be
|
||||||
|
sufficiently hardened for production use.
|
||||||
|
|
||||||
|
[1]: mailto:secalert_us@oracle.com
|
||||||
|
[2]: https://www.oracle.com/corporate/security-practices/assurance/vulnerability/reporting.html
|
||||||
|
[3]: https://www.oracle.com/security-alerts/encryptionkey.html
|
||||||
|
[4]: https://www.oracle.com/security-alerts/
|
||||||
BIN
Tuning-Performance/.DS_Store
vendored
Normal file
BIN
Tuning-Performance/.DS_Store
vendored
Normal file
Binary file not shown.
24
Tuning-Performance/Authentication/DISCLAIMER.md
Normal file
24
Tuning-Performance/Authentication/DISCLAIMER.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Disclaimer — template de autenticação
|
||||||
|
|
||||||
|
Este conteúdo é um **template de referência técnica** criado para demonstrar como integrar mecanismos genéricos de autenticação ao Agent Framework OCI, ao Agent Gateway, ao MCP Gateway e a aplicações FastAPI independentes.
|
||||||
|
|
||||||
|
O código, os arquivos YAML, as variáveis de ambiente, os providers, as políticas por rota e os exemplos de deployment **não constituem uma implementação final ou automaticamente adequada para produção**. Cada projeto deve lapidar e adaptar a solução conforme sua arquitetura, seus fluxos de confiança e suas exigências de segurança.
|
||||||
|
|
||||||
|
Antes de usar em homologação ou produção, é responsabilidade da equipe do projeto avaliar e implementar, conforme aplicável:
|
||||||
|
|
||||||
|
- integração com o provedor corporativo de identidade;
|
||||||
|
- definição de autenticação e autorização por sistema, rota, método, tenant, role e scope;
|
||||||
|
- armazenamento, distribuição e rotação de credenciais e chaves;
|
||||||
|
- TLS ou mTLS e proteção das comunicações internas e externas;
|
||||||
|
- bloqueio de acessos que contornem gateways ou proxies de confiança;
|
||||||
|
- validação de issuer, audience, algoritmo, expiração e revogação de tokens;
|
||||||
|
- proteção contra replay, brute force, credential stuffing e abuso de endpoints;
|
||||||
|
- rate limiting, timeout, circuit breaker e controles de disponibilidade;
|
||||||
|
- mascaramento de dados sensíveis em logs, traces e mensagens de erro;
|
||||||
|
- auditoria, observabilidade, alertas e resposta a incidentes;
|
||||||
|
- requisitos legais, regulatórios e políticas corporativas;
|
||||||
|
- threat modeling, security review, testes de integração, testes de carga e testes de segurança.
|
||||||
|
|
||||||
|
Os exemplos de Basic Authentication, API Key, Bearer estático, JWT, OAuth2 Introspection e Trusted Proxy devem ser entendidos como pontos de extensão. A seleção e a configuração finais dependem do cliente, da infraestrutura e do modelo de risco.
|
||||||
|
|
||||||
|
A promoção para produção deve ocorrer somente após aprovação formal das equipes responsáveis por arquitetura, segurança, infraestrutura e operação.
|
||||||
29
Tuning-Performance/Authentication/IMPLEMENTACAO.md
Normal file
29
Tuning-Performance/Authentication/IMPLEMENTACAO.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Implementação técnica
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Template de referência — requer adequação antes do uso produtivo.**
|
||||||
|
> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção.
|
||||||
|
## Biblioteca
|
||||||
|
|
||||||
|
`libs/agent_framework/src/agent_framework/security` contém:
|
||||||
|
|
||||||
|
- contratos e resultados de autenticação;
|
||||||
|
- Basic, API Key, Bearer estático, JWT, OAuth2 Introspection e Trusted Proxy;
|
||||||
|
- provider `none` para rotas públicas;
|
||||||
|
- provider `deny` para default seguro;
|
||||||
|
- middleware de provider único;
|
||||||
|
- middleware de políticas por rota;
|
||||||
|
- factory por ambiente ou mapping;
|
||||||
|
- instalador reutilizável para qualquer app FastAPI.
|
||||||
|
|
||||||
|
## Integrações
|
||||||
|
|
||||||
|
- `apps/agent_gateway/app/main.py`: `AGENT_GATEWAY_AUTH_*`
|
||||||
|
- `apps/mcp_gateway/app/main.py`: `MCP_GATEWAY_AUTH_*`
|
||||||
|
- `Tuning-Performance/Authentication/agent_template_backend_authentication/app/main.py`: `AGENT_AUTH_*`
|
||||||
|
|
||||||
|
Nenhuma integração é obrigatória. A instalação ocorre somente quando `*_AUTH_ENABLED=true`, quando um modo diferente de `none` é configurado ou quando existe `*_AUTH_POLICIES_FILE`.
|
||||||
|
|
||||||
|
## Compatibilidade
|
||||||
|
|
||||||
|
`AuthenticationMiddleware` e `create_authentication_provider()` foram mantidos para compatibilidade. O caminho recomendado para novos projetos é `install_authentication()`.
|
||||||
19
Tuning-Performance/Authentication/README.md
Normal file
19
Tuning-Performance/Authentication/README.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# Authentication
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> **Template de referência — requer adequação antes do uso produtivo.**
|
||||||
|
> Esta implementação demonstra pontos de extensão, providers, middleware e exemplos de configuração para autenticação. Ela não deve ser considerada uma solução pronta para produção nem substitui o desenho de segurança do projeto. Antes da implantação, a equipe responsável deve revisar, testar e adaptar o código às políticas corporativas, ao modelo de identidade, à topologia de rede, à gestão e rotação de segredos, aos requisitos regulatórios, à observabilidade, à alta disponibilidade e ao processo de resposta a incidentes do ambiente do cliente. Recomenda-se executar security review, threat modeling, testes de integração e testes de segurança antes da homologação e da produção.
|
||||||
|
Implementação de referência para autenticação transversal no Agent Framework OCI.
|
||||||
|
|
||||||
|
Inclui:
|
||||||
|
|
||||||
|
- providers genéricos em `libs/agent_framework/security`;
|
||||||
|
- instalação opcional por `install_authentication()`;
|
||||||
|
- políticas por rota, método, roles e scopes;
|
||||||
|
- integração opcional em `apps/agent_gateway`;
|
||||||
|
- integração opcional em `apps/mcp_gateway`;
|
||||||
|
- backend independente autenticado em `agent_template_backend_authentication`;
|
||||||
|
- exemplos YAML sem secrets embutidos;
|
||||||
|
- manual completo no diretório `docs` do agente.
|
||||||
|
|
||||||
|
A implementação não pressupõe o uso de gateways. Cada fronteira HTTP pode ativar autenticação com um prefixo de ambiente isolado.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# Select: none | basic | api_key | bearer_static | jwt | oauth2_introspection | trusted_proxy
|
||||||
|
AGENT_AUTH_MODE=basic
|
||||||
|
|
||||||
|
# Public endpoints. Avoid exposing /debug in production.
|
||||||
|
AGENT_AUTH_PUBLIC_PATHS=/health,/docs,/openapi.json,/redoc
|
||||||
|
AGENT_AUTH_PUBLIC_PREFIXES=
|
||||||
|
|
||||||
|
# HTTP Basic - TIA -> Agent example
|
||||||
|
AGENT_AUTH_BASIC_CLIENT_ID=tia-contas
|
||||||
|
# Supported formats: plain:value | sha256:hex | pbkdf2_sha256:iterations:salt:digest
|
||||||
|
AGENT_AUTH_BASIC_SECRET_HASH=pbkdf2_sha256:310000:replace-salt:replace-digest
|
||||||
|
AGENT_AUTH_BASIC_REALM=agent-contas
|
||||||
|
|
||||||
|
# API Key
|
||||||
|
# AGENT_AUTH_API_KEY_HEADER=x-api-key
|
||||||
|
# AGENT_AUTH_API_KEY_HASH=sha256:replace-hex
|
||||||
|
# AGENT_AUTH_API_KEY_PRINCIPAL=tia
|
||||||
|
|
||||||
|
# Static Bearer token
|
||||||
|
# AGENT_AUTH_BEARER_TOKEN_HASH=sha256:replace-hex
|
||||||
|
# AGENT_AUTH_BEARER_PRINCIPAL=tia
|
||||||
|
|
||||||
|
# JWT / OIDC access token validation. For production, prefer asymmetric algorithms.
|
||||||
|
# AGENT_AUTH_JWT_KEY=-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----
|
||||||
|
# AGENT_AUTH_JWT_ALGORITHMS=RS256
|
||||||
|
# AGENT_AUTH_JWT_AUDIENCE=agent-contas
|
||||||
|
# AGENT_AUTH_JWT_ISSUER=https://identity.example.com/
|
||||||
|
|
||||||
|
# OAuth2 opaque-token introspection
|
||||||
|
# AGENT_AUTH_OAUTH2_INTROSPECTION_URL=https://identity.example.com/oauth2/introspect
|
||||||
|
# AGENT_AUTH_OAUTH2_CLIENT_ID=agent-contas
|
||||||
|
# AGENT_AUTH_OAUTH2_CLIENT_SECRET=replace-from-vault
|
||||||
|
# AGENT_AUTH_OAUTH2_TIMEOUT_SECONDS=5
|
||||||
|
|
||||||
|
# Authentication delegated to API Gateway / service mesh.
|
||||||
|
# Only trust these headers when direct access to the pod is blocked.
|
||||||
|
# AGENT_AUTH_PROXY_SUBJECT_HEADER=x-authenticated-subject
|
||||||
|
# AGENT_AUTH_PROXY_SHARED_SECRET_HEADER=x-internal-auth
|
||||||
|
# AGENT_AUTH_PROXY_SHARED_SECRET_HASH=sha256:replace-hex
|
||||||
|
|
||||||
|
|
||||||
|
# RAG / KBDB
|
||||||
|
# RAG provider: standard (local) ou kbdb (PKG_KB_SERVING.SEARCH_KNOWLEDGE_BASE)
|
||||||
|
RAG_PROVIDER=standard
|
||||||
|
# KBDB usa credenciais próprias; não há fallback para ADB_*.
|
||||||
|
KBDB_DB_USER=
|
||||||
|
KBDB_DB_PASSWORD=
|
||||||
|
KBDB_DB_DSN=
|
||||||
|
KBDB_DB_WALLET_LOCATION=
|
||||||
|
KBDB_DB_WALLET_PASSWORD=
|
||||||
|
KBDB_SEARCH_TYPE=hybrid
|
||||||
|
KBDB_NODE_EXPANSION=true
|
||||||
|
KBDB_NODE_MAX_RELATED=8
|
||||||
|
KBDB_GRAPH_CROSS_REF=false
|
||||||
|
KBDB_MAX_CROSS_REF_HOPS=1
|
||||||
|
KBDB_DOCUMENT_TYPE=customer_safe
|
||||||
|
KBDB_METADATA_JSON=
|
||||||
|
KBDB_MIN_SCORE=
|
||||||
|
KBDB_IDENTIFY_DOCUMENT=true
|
||||||
|
KBDB_STORE_QUERY=true
|
||||||
|
KBDB_IDENTIFY_TOP_N=3
|
||||||
|
RAG_GROUNDED_ONLY=false
|
||||||
|
KBDB_GROUNDED_ONLY=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.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,15 @@
|
|||||||
|
# Agentes do Template Backend Enterprise
|
||||||
|
|
||||||
|
Os arquivos desta pasta preservam a estrutura real esperada pelo workflow, mas
|
||||||
|
não executam lógica de negócio pronta.
|
||||||
|
|
||||||
|
Cada agente mostra:
|
||||||
|
|
||||||
|
- como emitir IC;
|
||||||
|
- como emitir NOC;
|
||||||
|
- como emitir GRL;
|
||||||
|
- como coletar MCP via `_collect_tool_context()`;
|
||||||
|
- como recuperar RAG via `_retrieve_rag_context()`;
|
||||||
|
- onde chamar LLM/cache.
|
||||||
|
|
||||||
|
A implementação original do exemplo está comentada no fim de cada arquivo.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,137 @@
|
|||||||
|
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,
|
||||||
|
"rag_context": rag_context,
|
||||||
|
"rag_results": [{
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"agent": self.name,
|
||||||
|
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||||
|
"metadata": rag_metadata,
|
||||||
|
**({"context": rag_context} if rag_context else {}),
|
||||||
|
}],
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**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,137 @@
|
|||||||
|
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,
|
||||||
|
"rag_context": rag_context,
|
||||||
|
"rag_results": [{
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"agent": self.name,
|
||||||
|
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||||
|
"metadata": rag_metadata,
|
||||||
|
**({"context": rag_context} if rag_context else {}),
|
||||||
|
}],
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**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,137 @@
|
|||||||
|
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,
|
||||||
|
"rag_context": rag_context,
|
||||||
|
"rag_results": [{
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"agent": self.name,
|
||||||
|
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||||
|
"metadata": rag_metadata,
|
||||||
|
**({"context": rag_context} if rag_context else {}),
|
||||||
|
}],
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**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,137 @@
|
|||||||
|
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,
|
||||||
|
"rag_context": rag_context,
|
||||||
|
"rag_results": [{
|
||||||
|
"intent": state.get("intent"),
|
||||||
|
"agent": self.name,
|
||||||
|
"source_text": str(state.get("sanitized_input") or state.get("user_text") or ""),
|
||||||
|
"metadata": rag_metadata,
|
||||||
|
**({"context": rag_context} if rag_context else {}),
|
||||||
|
}],
|
||||||
|
"memory_context_metadata": state.get("memory_context_metadata"),
|
||||||
|
**self.transaction_state_patch(state),
|
||||||
|
}
|
||||||
|
|
||||||
|
await self._emit_ic(
|
||||||
|
"IC.SUPPORT_AGENT_COMPLETED",
|
||||||
|
state,
|
||||||
|
{
|
||||||
|
"answer_chars": len(result.get("answer") or ""),
|
||||||
|
"has_mcp_results": bool(tool_context),
|
||||||
|
"rag_enabled": bool(rag_metadata.get("enabled")),
|
||||||
|
"memory_context": state.get("memory_context_metadata"),
|
||||||
|
},
|
||||||
|
component="agent.support.completed",
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def _collect_tool_context(self, state):
|
||||||
|
return await self._collect_mcp_context(state)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Exemplos de uso do template backend enterprise."""
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,37 @@
|
|||||||
|
"""Exemplos de GRL.
|
||||||
|
|
||||||
|
GRL representa eventos de guardrails. Em regra, GRL.001..GRL.009 são emitidos
|
||||||
|
pelo pipeline de guardrails e pelo OutputSupervisor do framework. Use emissão
|
||||||
|
manual apenas para validações customizadas do agente.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
async def exemplo_guardrail_observado(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||||
|
await observer.emit_grl(
|
||||||
|
"OBSERVE",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"rail_code": rail_code,
|
||||||
|
"reason": reason,
|
||||||
|
},
|
||||||
|
component="examples.grl",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def exemplo_guardrail_block(observer: Any, state: dict[str, Any], rail_code: str, reason: str) -> None:
|
||||||
|
await observer.emit_grl(
|
||||||
|
"004",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"rail_code": rail_code,
|
||||||
|
"reason": reason,
|
||||||
|
"action": "block",
|
||||||
|
},
|
||||||
|
component="examples.grl",
|
||||||
|
)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""Exemplos de IC - Item de Controle.
|
||||||
|
|
||||||
|
ICs representam eventos de negócio. Eles alimentam Informacional, Curadoria,
|
||||||
|
analytics, BigQuery ou qualquer publisher configurado no framework.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
async def exemplo_fatura_consultada(observer: Any, state: dict[str, Any], invoice_id: str) -> None:
|
||||||
|
await observer.emit_ic(
|
||||||
|
"IC.FATURA_CONSULTADA",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"invoice_id": invoice_id,
|
||||||
|
},
|
||||||
|
component="examples.ic",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def exemplo_acao_concluida(observer: Any, state: dict[str, Any], action_name: str, ok: bool) -> None:
|
||||||
|
await observer.emit_ic(
|
||||||
|
"IC.ACAO_CONCLUIDA",
|
||||||
|
{
|
||||||
|
"session_id": state.get("conversation_key") or state.get("session_id"),
|
||||||
|
"tenant_id": state.get("tenant_id"),
|
||||||
|
"agent_id": state.get("agent_id"),
|
||||||
|
"action_name": action_name,
|
||||||
|
"ok": ok,
|
||||||
|
},
|
||||||
|
component="examples.ic",
|
||||||
|
)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user