Disclaimer best practicies Oracle for Security

This commit is contained in:
2026-07-30 12:03:41 -03:00
parent 26d33892f3
commit e684b0ecc3
59 changed files with 2138 additions and 4 deletions

14
.env
View File

@@ -162,3 +162,17 @@ MEMORY_MAX_SUMMARY_CHARS=6000
MEMORY_SUMMARY_USE_LLM=true
MEMORY_INJECT_RECENT_MESSAGES=true
MEMORY_INJECT_SUMMARY=true
###############################################################################
# LONG-TERM MEMORY
###############################################################################
ENABLE_LONG_TERM_MEMORY=true
LONG_TERM_MEMORY_PROVIDER=sqlite
LONG_TERM_MEMORY_SQLITE_PATH=./data/agent_framework.db
LONG_TERM_MEMORY_TABLE=agentfw_long_term_memory
# For Autonomous/Oracle, defaults to ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY
# LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70
LONG_TERM_MEMORY_AUTO_EXTRACT=true
LONG_TERM_MEMORY_INJECT_CONTEXT=true

View File

@@ -180,4 +180,18 @@ 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
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

View 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.

View 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.

View File

@@ -105,8 +105,9 @@ Detalha o framework de avaliação e certificação, incluindo arquitetura de av
Define o modelo de readiness operacional e SRE da plataforma, incluindo componentes operados, health checks, readiness, SLOs, métricas, dashboards, alertas, runbooks, gestão de incidentes, capacidade e checklist de produção.
### [IMPORTANTE: Disclaimer Auth and Security](specs/Disclaimer%20Auth%20and%20Security_PT.md)
Recomendações de melhores práticas de segurança da Oracle Cloud Infrastructure.
---
## 1. Visão geral da arquitetura

View File

@@ -104,6 +104,9 @@ Details the evaluation and certification framework, including evaluation archite
Defines the platform operational readiness and SRE model, including managed components, health checks, readiness, SLOs, metrics, dashboards, alerts, runbooks, incident management, capacity planning, and production checklists.
### [IMPORTANT: Disclaimer Auth and Security](specs/Disclaimer%20Auth%20and%20Security_EN.md)
Oracle Cloud Infrastructure Security Best Practice Recommendations.
---

View File

@@ -0,0 +1,11 @@
# Long-Term Memory
Capacidade nativa do `agent_framework`, isolada por `tenant_id + agent_id + customer_key`.
O runtime carrega e injeta as memórias automaticamente. Os dois templates persistem os fatos após `supervisor_review`. Os agentes individuais não precisam de alteração.
## Teste
```bash
PYTHONPATH=libs/agent_framework/src python templates/agent_template_backend/scripts/test_long_term_memory.py
```

View File

@@ -0,0 +1,63 @@
### Long-Term Memory on Oracle Autonomous Database
### Activation
```env
ENABLE_LONG_TERM_MEMORY=true
LONG_TERM_MEMORY_PROVIDER=autonomous
ADB_USER=ADMIN
ADB_PASSWORD=<password>
ADB_DSN=<autonomous_service_name>
ADB_WALLET_LOCATION=/path/to/wallet
ADB_WALLET_PASSWORD=<wallet_password_if_applicable>
ADB_TABLE_PREFIX=AGENTFW
# Optional. Default: ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY.
LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
```
`LONG_TERM_MEMORY_PROVIDER=oracle` is also accepted.
### Dependency
```bash
pip install oracledb
```
### Schema initialization
On the first operation, the provider automatically creates the table and index. The user configured in `ADB_USER` needs permission to create tables and indexes. If a DBA provisions the schema beforehand, initialization accepts the existing objects.
### Identity and isolation
The logical key consists of:
```text
tenant_id + agent_id + subject_key + category + memory_key
```
In the current integration, `subject_key` is derived from `customer_key`.
### Test
1. Start the backend with the `autonomous` provider.
2. Store facts in session A.
3. Open session B with the same `customer_key`.
4. Verify retrieval.
5. Restart the backend and repeat the query.
6. Query `AGENTFW_LONG_TERM_MEMORY` in Autonomous Database.
```sql
SELECT TENANT_ID, AGENT_ID, SUBJECT_KEY, CATEGORY, MEMORY_KEY,
MEMORY_VALUE, CONFIDENCE, UPDATED_AT
FROM AGENTFW_LONG_TERM_MEMORY
ORDER BY UPDATED_AT DESC;
```
### Notes
- The provider uses `python-oracledb` in thin mode.
- Synchronous operations run through `asyncio.to_thread`.
- A wallet is optional when walletless TLS is configured.
- SQLite and InMemory remain available for development and testing.

View File

@@ -0,0 +1,63 @@
### Long-Term Memory no Oracle Autonomous Database
### Ativação
```env
ENABLE_LONG_TERM_MEMORY=true
LONG_TERM_MEMORY_PROVIDER=autonomous
ADB_USER=ADMIN
ADB_PASSWORD=<senha>
ADB_DSN=<service_name_do_autonomous>
ADB_WALLET_LOCATION=/caminho/para/wallet
ADB_WALLET_PASSWORD=<senha_wallet_se_aplicavel>
ADB_TABLE_PREFIX=AGENTFW
# Opcional. O padrão é ${ADB_TABLE_PREFIX}_LONG_TERM_MEMORY.
LONG_TERM_MEMORY_ORACLE_TABLE=AGENTFW_LONG_TERM_MEMORY
```
Também é aceito `LONG_TERM_MEMORY_PROVIDER=oracle`.
### Dependência
```bash
pip install oracledb
```
### Inicialização do schema
Na primeira operação, o provider cria automaticamente a tabela e o índice. O usuário configurado em `ADB_USER` precisa de permissão para criar tabela e índice. Se o schema for provisionado previamente por DBA, a inicialização reconhece os objetos existentes.
### Identidade e isolamento
A chave lógica é composta por:
```text
tenant_id + agent_id + subject_key + category + memory_key
```
Na integração atual, `subject_key` é derivado do `customer_key`.
### Teste
1. Inicie o backend com provider `autonomous`.
2. Grave fatos na sessão A.
3. Abra a sessão B com o mesmo `customer_key`.
4. Confirme a recuperação.
5. Reinicie o backend e repita a consulta.
6. Consulte a tabela `AGENTFW_LONG_TERM_MEMORY` no Autonomous Database.
```sql
SELECT TENANT_ID, AGENT_ID, SUBJECT_KEY, CATEGORY, MEMORY_KEY,
MEMORY_VALUE, CONFIDENCE, UPDATED_AT
FROM AGENTFW_LONG_TERM_MEMORY
ORDER BY UPDATED_AT DESC;
```
### Observações
- O provider usa `python-oracledb` em thin mode.
- As operações síncronas são executadas com `asyncio.to_thread`.
- Wallet é opcional quando a conexão TLS sem wallet estiver configurada.
- SQLite e InMemory continuam disponíveis para desenvolvimento e testes.

View File

@@ -61,6 +61,16 @@ class Settings(BaseSettings):
MEMORY_INJECT_RECENT_MESSAGES: bool = True
MEMORY_INJECT_SUMMARY: bool = True
ENABLE_LONG_TERM_MEMORY: bool = False
LONG_TERM_MEMORY_PROVIDER: Literal['memory','sqlite','autonomous','oracle'] = 'sqlite'
LONG_TERM_MEMORY_SQLITE_PATH: str | None = None
LONG_TERM_MEMORY_TABLE: str = 'agentfw_long_term_memory'
LONG_TERM_MEMORY_ORACLE_TABLE: str | None = None
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
# LangGraph enterprise checkpointing
ENABLE_RESILIENT_CHECKPOINTER: bool = True
ENABLE_CHECKPOINT_INTEGRITY: bool = True

View File

@@ -0,0 +1,25 @@
from __future__ import annotations
import re
from typing import Any
_PATTERNS = [
('identity', 'preferred_name', re.compile(r'\b(?:me chame de|pode me chamar de|meu nome preferido é)\s+([A-Za-zÀ-ÿ][A-Za-zÀ-ÿ0-9 _-]{1,40})', re.I)),
('preference', 'preferred_language', re.compile(r'\b(?:minha linguagem preferida é|prefiro programar em)\s+(Python|Java|JavaScript|TypeScript|Go|Rust|C#|C\+\+)\b', re.I)),
('project', 'current_project', re.compile(r'\b(?:meu projeto atual se chama|estou trabalhando no projeto|o projeto se chama)\s+([A-Za-zÀ-ÿ0-9._ -]{2,60})', re.I)),
('constraint', 'meeting_restriction', re.compile(r'\b(não (?:marque|agende) reuniões?[^.!?\n]{3,120})', re.I)),
('preference', 'communication_style', re.compile(r'\b(?:prefiro respostas|responda de forma)\s+(curtas?|detalhadas?|objetivas?|técnicas?|didáticas?)', re.I)),
]
def extract_long_term_memory(text: str, min_confidence: float = 0.70) -> list[dict[str, Any]]:
normalized = ' '.join((text or '').split())
output: list[dict[str, Any]] = []
seen: set[tuple[str, str]] = set()
for category, key, pattern in _PATTERNS:
match = pattern.search(normalized)
if not match or (category, key) in seen:
continue
seen.add((category, key))
confidence = 0.98
if confidence >= min_confidence:
output.append({'category': category, 'key': key, 'value': match.group(1).strip(' .,;:'), 'confidence': confidence, 'metadata': {'extractor': 'regex-v1'}})
return output

View File

@@ -0,0 +1,64 @@
from __future__ import annotations
import logging
from .long_term_extractor import extract_long_term_memory
from .long_term_store import create_long_term_memory_store
logger = logging.getLogger('agent_framework.memory.long_term')
class LongTermMemoryManager:
def __init__(self, settings, store=None, telemetry=None):
self.settings = settings
self.store = store or create_long_term_memory_store(settings)
self.telemetry = telemetry
@property
def enabled(self):
return bool(getattr(self.settings, 'ENABLE_LONG_TERM_MEMORY', False))
def identity(self, state):
context = state.get('context') or {}
session = context.get('session') or {}
business = context.get('business_context') or state.get('business_context') or {}
metadata = session.get('metadata') or {}
tenant = str(state.get('tenant_id') or session.get('tenant_id') or 'default')
agent = str(state.get('agent_id') or state.get('route') or session.get('active_agent') or 'default')
subject = business.get('customer_key') or state.get('customer_key') or context.get('user_id') or session.get('user_id') or metadata.get('customer_key')
return tenant, agent, str(subject) if subject else None
async def load(self, state):
if not self.enabled:
return []
tenant, agent, subject = self.identity(state)
if not subject:
return []
try:
return await self.store.search(tenant_id=tenant, agent_id=agent, subject_key=subject, limit=int(getattr(self.settings, 'LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS', 20)))
except Exception:
logger.exception('Falha não crítica ao carregar LTM')
return []
async def persist_turn(self, state):
if not self.enabled or not bool(getattr(self.settings, 'LONG_TERM_MEMORY_AUTO_EXTRACT', True)):
return {'saved': 0, 'enabled': self.enabled}
tenant, agent, subject = self.identity(state)
if not subject:
return {'saved': 0, 'warning': 'customer_key ausente'}
text = str(state.get('sanitized_input') or state.get('user_text') or '')
candidates = extract_long_term_memory(text, float(getattr(self.settings, 'LONG_TERM_MEMORY_MIN_CONFIDENCE', 0.70)))
try:
saved = await self.store.upsert_many(tenant_id=tenant, agent_id=agent, subject_key=subject, items=candidates, source_session_id=str(state.get('conversation_key') or state.get('session_id') or ''), source_message_id=str((state.get('context') or {}).get('message_id') or ''))
return {'saved': len(saved), 'items': [item.to_dict() for item in saved]}
except Exception as exc:
logger.exception('Falha não crítica ao persistir LTM')
return {'saved': 0, 'error': str(exc)}
def render(self, items):
if not items:
return ''
lines = ['Memórias duráveis relevantes do usuário atual:']
lines.extend(f'- {item.key}: {item.value}' for item in items)
lines.extend(['Use somente estas memórias; não invente lembranças.', 'A mensagem atual prevalece se houver conflito.'])
return '\n'.join(lines)
def create_long_term_memory_manager(settings, telemetry=None):
return LongTermMemoryManager(settings, telemetry=telemetry)

View File

@@ -0,0 +1,26 @@
from __future__ import annotations
from dataclasses import asdict, dataclass, field
from datetime import datetime, timezone
from typing import Any
def utc_now() -> str:
return datetime.now(timezone.utc).isoformat()
@dataclass(slots=True)
class LongTermMemoryItem:
memory_id: str
tenant_id: str
agent_id: str
subject_key: str
category: str
key: str
value: str
confidence: float = 1.0
source_session_id: str | None = None
source_message_id: str | None = None
created_at: str = field(default_factory=utc_now)
updated_at: str = field(default_factory=utc_now)
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return asdict(self)

View File

@@ -0,0 +1,546 @@
from __future__ import annotations
import asyncio
import json
import re
import sqlite3
import uuid
from contextlib import contextmanager
from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Protocol, Sequence
from .long_term_models import LongTermMemoryItem, utc_now
class LongTermMemoryStore(Protocol):
async def upsert_many(
self,
*,
tenant_id: str,
agent_id: str,
subject_key: str,
items: Sequence[dict[str, Any]],
source_session_id: str | None = None,
source_message_id: str | None = None,
) -> list[LongTermMemoryItem]: ...
async def search(
self,
*,
tenant_id: str,
agent_id: str,
subject_key: str,
limit: int = 20,
) -> list[LongTermMemoryItem]: ...
class InMemoryLongTermMemoryStore:
def __init__(self):
self._items: dict[tuple[str, str, str, str, str], LongTermMemoryItem] = {}
async def upsert_many(self, **kwargs):
saved = []
now = utc_now()
for raw in kwargs["items"]:
key = (
kwargs["tenant_id"],
kwargs["agent_id"],
kwargs["subject_key"],
str(raw.get("category") or "fact"),
str(raw.get("key") or ""),
)
if not key[-1] or not raw.get("value"):
continue
old = self._items.get(key)
item = LongTermMemoryItem(
old.memory_id if old else str(uuid.uuid4()),
key[0], key[1], key[2], key[3], key[4],
str(raw["value"]),
float(raw.get("confidence", 1.0)),
kwargs.get("source_session_id"),
kwargs.get("source_message_id"),
old.created_at if old else now,
now,
dict(raw.get("metadata") or {}),
)
self._items[key] = item
saved.append(item)
return saved
async def search(self, *, tenant_id, agent_id, subject_key, limit=20):
values = [
value for key, value in self._items.items()
if key[:3] == (tenant_id, agent_id, subject_key)
]
return sorted(
values,
key=lambda item: (item.confidence, item.updated_at),
reverse=True,
)[:limit]
class SQLiteLongTermMemoryStore:
def __init__(
self,
path: str = "./data/agent_framework.db",
table: str = "agentfw_long_term_memory",
):
self.path = str(path)
self.table = _validate_identifier(table, upper=False)
Path(self.path).parent.mkdir(parents=True, exist_ok=True)
self._ready = False
self._lock = asyncio.Lock()
def _connect(self):
return sqlite3.connect(self.path)
def _init_sync(self):
sql = f"""CREATE TABLE IF NOT EXISTS {self.table} (
memory_id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL, agent_id TEXT NOT NULL,
subject_key TEXT NOT NULL, category TEXT NOT NULL, memory_key TEXT NOT NULL,
memory_value TEXT NOT NULL, confidence REAL NOT NULL, source_session_id TEXT,
source_message_id TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
metadata_json TEXT, UNIQUE(tenant_id,agent_id,subject_key,category,memory_key))"""
with self._connect() as db:
db.execute(sql)
db.execute(
f"CREATE INDEX IF NOT EXISTS idx_{self.table}_subject "
f"ON {self.table}(tenant_id,agent_id,subject_key,updated_at)"
)
async def _ensure(self):
if self._ready:
return
async with self._lock:
if not self._ready:
await asyncio.to_thread(self._init_sync)
self._ready = True
def _upsert_sync(
self, tenant_id, agent_id, subject_key, items,
source_session_id, source_message_id,
):
now = utc_now()
saved = []
with self._connect() as db:
for raw in items:
category = str(raw.get("category") or "fact").lower()
key = str(raw.get("key") or "").lower()
value = str(raw.get("value") or "").strip()
if not key or not value:
continue
row = db.execute(
f"SELECT memory_id,created_at FROM {self.table} "
"WHERE tenant_id=? AND agent_id=? AND subject_key=? "
"AND category=? AND memory_key=?",
(tenant_id, agent_id, subject_key, category, key),
).fetchone()
memory_id = row[0] if row else str(uuid.uuid4())
created_at = row[1] if row else now
confidence = float(raw.get("confidence", 1.0))
metadata = dict(raw.get("metadata") or {})
db.execute(
f"""INSERT INTO {self.table}(
memory_id,tenant_id,agent_id,subject_key,category,memory_key,
memory_value,confidence,source_session_id,source_message_id,
created_at,updated_at,metadata_json)
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)
ON CONFLICT(tenant_id,agent_id,subject_key,category,memory_key)
DO UPDATE SET memory_value=excluded.memory_value,
confidence=excluded.confidence,
source_session_id=excluded.source_session_id,
source_message_id=excluded.source_message_id,
updated_at=excluded.updated_at,
metadata_json=excluded.metadata_json""",
(
memory_id, tenant_id, agent_id, subject_key, category, key,
value, confidence, source_session_id, source_message_id,
created_at, now, json.dumps(metadata, ensure_ascii=False),
),
)
saved.append(LongTermMemoryItem(
memory_id, tenant_id, agent_id, subject_key, category, key,
value, confidence, source_session_id, source_message_id,
created_at, now, metadata,
))
return saved
async def upsert_many(self, **kwargs):
await self._ensure()
return await asyncio.to_thread(
self._upsert_sync,
kwargs["tenant_id"], kwargs["agent_id"], kwargs["subject_key"],
list(kwargs["items"]), kwargs.get("source_session_id"),
kwargs.get("source_message_id"),
)
def _search_sync(self, tenant_id, agent_id, subject_key, limit):
with self._connect() as db:
rows = db.execute(
f"SELECT memory_id,tenant_id,agent_id,subject_key,category,memory_key,"
f"memory_value,confidence,source_session_id,source_message_id,"
f"created_at,updated_at,metadata_json FROM {self.table} "
"WHERE tenant_id=? AND agent_id=? AND subject_key=? "
"ORDER BY confidence DESC,updated_at DESC LIMIT ?",
(tenant_id, agent_id, subject_key, int(limit)),
).fetchall()
return [
LongTermMemoryItem(*row[:12], metadata=json.loads(row[12] or "{}"))
for row in rows
]
async def search(self, **kwargs):
await self._ensure()
return await asyncio.to_thread(
self._search_sync,
kwargs["tenant_id"], kwargs["agent_id"], kwargs["subject_key"],
kwargs.get("limit", 20),
)
def _validate_identifier(value: str, *, upper: bool = True) -> str:
identifier = str(value or "").strip()
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_$#]{0,127}", identifier):
raise ValueError(f"Invalid SQL identifier: {value!r}")
return identifier.upper() if upper else identifier
def _as_iso(value: Any) -> str:
if isinstance(value, datetime):
if value.tzinfo is None:
value = value.replace(tzinfo=timezone.utc)
return value.isoformat()
return str(value)
def _load_json(value: Any) -> dict[str, Any]:
if value is None:
return {}
if hasattr(value, "read"):
value = value.read()
if isinstance(value, bytes):
value = value.decode("utf-8")
try:
loaded = json.loads(value)
return loaded if isinstance(loaded, dict) else {}
except (TypeError, ValueError, json.JSONDecodeError):
return {}
class OracleAutonomousLongTermMemoryStore:
"""Long-Term Memory provider for Oracle Autonomous Database.
The implementation uses python-oracledb in thin mode and reuses the
framework's ADB_* settings. Synchronous database operations run in worker
threads so FastAPI/LangGraph's event loop is not blocked.
"""
def __init__(self, settings):
self.user = str(getattr(settings, "ADB_USER", "") or "")
self.password = str(getattr(settings, "ADB_PASSWORD", "") or "")
self.dsn = str(getattr(settings, "ADB_DSN", "") or "")
self.wallet_location = getattr(settings, "ADB_WALLET_LOCATION", None)
self.wallet_password = getattr(settings, "ADB_WALLET_PASSWORD", None)
default_table = (
f"{getattr(settings, 'ADB_TABLE_PREFIX', 'AGENTFW')}_LONG_TERM_MEMORY"
)
configured_table = (
getattr(settings, "LONG_TERM_MEMORY_ORACLE_TABLE", None)
or default_table
)
self.table = _validate_identifier(configured_table)
self.index_name = _validate_identifier(f"IX_{self.table}_SUBJECT")
self.constraint_name = _validate_identifier(f"UQ_{self.table}_FACT")
self._ready = False
self._lock = asyncio.Lock()
if not self.user or not self.password or not self.dsn:
raise RuntimeError(
"ADB_USER, ADB_PASSWORD and ADB_DSN are required when "
"LONG_TERM_MEMORY_PROVIDER is autonomous/oracle"
)
@contextmanager
def _connect(self):
try:
import oracledb
except ImportError as exc:
raise RuntimeError(
"python-oracledb is required for the Autonomous Long-Term "
"Memory provider. Install it with: pip install oracledb"
) from exc
oracledb.defaults.fetch_lobs = False
kwargs: dict[str, Any] = {}
if self.wallet_location:
kwargs["config_dir"] = self.wallet_location
kwargs["wallet_location"] = self.wallet_location
if self.wallet_password:
kwargs["wallet_password"] = self.wallet_password
connection = oracledb.connect(
user=self.user,
password=self.password,
dsn=self.dsn,
**kwargs,
)
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
finally:
connection.close()
@staticmethod
def _ignore_already_exists(cursor, ddl: str) -> None:
try:
cursor.execute(ddl)
except Exception as exc:
message = str(exc)
if "ORA-00955" in message or "ORA-01408" in message:
return
raise
def _init_sync(self) -> None:
with self._connect() as connection:
cursor = connection.cursor()
self._ignore_already_exists(cursor, f"""
CREATE TABLE {self.table} (
MEMORY_ID VARCHAR2(36) PRIMARY KEY,
TENANT_ID VARCHAR2(128) NOT NULL,
AGENT_ID VARCHAR2(128) NOT NULL,
SUBJECT_KEY VARCHAR2(512) NOT NULL,
CATEGORY VARCHAR2(128) NOT NULL,
MEMORY_KEY VARCHAR2(256) NOT NULL,
MEMORY_VALUE CLOB NOT NULL,
CONFIDENCE NUMBER(5,4) DEFAULT 1 NOT NULL,
SOURCE_SESSION_ID VARCHAR2(512),
SOURCE_MESSAGE_ID VARCHAR2(256),
CREATED_AT TIMESTAMP WITH TIME ZONE NOT NULL,
UPDATED_AT TIMESTAMP WITH TIME ZONE NOT NULL,
METADATA_JSON CLOB CHECK (METADATA_JSON IS JSON),
CONSTRAINT {self.constraint_name} UNIQUE (
TENANT_ID, AGENT_ID, SUBJECT_KEY, CATEGORY, MEMORY_KEY
)
)
""")
self._ignore_already_exists(cursor, f"""
CREATE INDEX {self.index_name}
ON {self.table} (
TENANT_ID, AGENT_ID, SUBJECT_KEY, UPDATED_AT DESC
)
""")
async def _ensure(self) -> None:
if self._ready:
return
async with self._lock:
if not self._ready:
await asyncio.to_thread(self._init_sync)
self._ready = True
def _find_existing(
self, cursor, tenant_id: str, agent_id: str, subject_key: str,
category: str, memory_key: str,
) -> tuple[str, Any] | None:
cursor.execute(
f"""SELECT MEMORY_ID, CREATED_AT FROM {self.table}
WHERE TENANT_ID = :tenant_id
AND AGENT_ID = :agent_id
AND SUBJECT_KEY = :subject_key
AND CATEGORY = :category
AND MEMORY_KEY = :memory_key""",
tenant_id=tenant_id,
agent_id=agent_id,
subject_key=subject_key,
category=category,
memory_key=memory_key,
)
return cursor.fetchone()
def _upsert_sync(
self, tenant_id: str, agent_id: str, subject_key: str,
items: Sequence[dict[str, Any]], source_session_id: str | None,
source_message_id: str | None,
) -> list[LongTermMemoryItem]:
now = datetime.now(timezone.utc)
saved: list[LongTermMemoryItem] = []
with self._connect() as connection:
cursor = connection.cursor()
for raw in items:
category = str(raw.get("category") or "fact").strip().lower()
memory_key = str(raw.get("key") or "").strip().lower()
value = str(raw.get("value") or "").strip()
if not memory_key or not value:
continue
existing = self._find_existing(
cursor, tenant_id, agent_id, subject_key,
category, memory_key,
)
memory_id = str(existing[0]) if existing else str(uuid.uuid4())
created_at = existing[1] if existing else now
confidence = float(raw.get("confidence", 1.0))
metadata = dict(raw.get("metadata") or {})
metadata_json = json.dumps(metadata, ensure_ascii=False, default=str)
cursor.execute(f"""
MERGE INTO {self.table} target
USING (
SELECT
:tenant_id AS TENANT_ID,
:agent_id AS AGENT_ID,
:subject_key AS SUBJECT_KEY,
:category AS CATEGORY,
:memory_key AS MEMORY_KEY
FROM dual
) source
ON (
target.TENANT_ID = source.TENANT_ID
AND target.AGENT_ID = source.AGENT_ID
AND target.SUBJECT_KEY = source.SUBJECT_KEY
AND target.CATEGORY = source.CATEGORY
AND target.MEMORY_KEY = source.MEMORY_KEY
)
WHEN MATCHED THEN UPDATE SET
target.MEMORY_VALUE = :memory_value,
target.CONFIDENCE = :confidence,
target.SOURCE_SESSION_ID = :source_session_id,
target.SOURCE_MESSAGE_ID = :source_message_id,
target.UPDATED_AT = :updated_at,
target.METADATA_JSON = :metadata_json
WHEN NOT MATCHED THEN INSERT (
MEMORY_ID, TENANT_ID, AGENT_ID, SUBJECT_KEY,
CATEGORY, MEMORY_KEY, MEMORY_VALUE, CONFIDENCE,
SOURCE_SESSION_ID, SOURCE_MESSAGE_ID,
CREATED_AT, UPDATED_AT, METADATA_JSON
) VALUES (
:memory_id, :tenant_id, :agent_id, :subject_key,
:category, :memory_key, :memory_value, :confidence,
:source_session_id, :source_message_id,
:created_at, :updated_at, :metadata_json
)
""", {
"memory_id": memory_id,
"tenant_id": tenant_id,
"agent_id": agent_id,
"subject_key": subject_key,
"category": category,
"memory_key": memory_key,
"memory_value": value,
"confidence": confidence,
"source_session_id": source_session_id,
"source_message_id": source_message_id,
"created_at": created_at,
"updated_at": now,
"metadata_json": metadata_json,
})
saved.append(LongTermMemoryItem(
memory_id=memory_id,
tenant_id=tenant_id,
agent_id=agent_id,
subject_key=subject_key,
category=category,
key=memory_key,
value=value,
confidence=confidence,
source_session_id=source_session_id,
source_message_id=source_message_id,
created_at=_as_iso(created_at),
updated_at=_as_iso(now),
metadata=metadata,
))
return saved
async def upsert_many(self, **kwargs):
await self._ensure()
return await asyncio.to_thread(
self._upsert_sync,
kwargs["tenant_id"],
kwargs["agent_id"],
kwargs["subject_key"],
list(kwargs["items"]),
kwargs.get("source_session_id"),
kwargs.get("source_message_id"),
)
def _search_sync(
self, tenant_id: str, agent_id: str, subject_key: str, limit: int,
) -> list[LongTermMemoryItem]:
safe_limit = max(1, min(int(limit), 500))
with self._connect() as connection:
cursor = connection.cursor()
cursor.execute(f"""
SELECT
MEMORY_ID, TENANT_ID, AGENT_ID, SUBJECT_KEY,
CATEGORY, MEMORY_KEY, MEMORY_VALUE, CONFIDENCE,
SOURCE_SESSION_ID, SOURCE_MESSAGE_ID,
CREATED_AT, UPDATED_AT, METADATA_JSON
FROM {self.table}
WHERE TENANT_ID = :tenant_id
AND AGENT_ID = :agent_id
AND SUBJECT_KEY = :subject_key
ORDER BY CONFIDENCE DESC, UPDATED_AT DESC
FETCH FIRST {safe_limit} ROWS ONLY
""", {
"tenant_id": tenant_id,
"agent_id": agent_id,
"subject_key": subject_key,
})
rows = cursor.fetchall()
result: list[LongTermMemoryItem] = []
for row in rows:
result.append(LongTermMemoryItem(
memory_id=str(row[0]),
tenant_id=str(row[1]),
agent_id=str(row[2]),
subject_key=str(row[3]),
category=str(row[4]),
key=str(row[5]),
value=str(row[6]),
confidence=float(row[7]),
source_session_id=str(row[8]) if row[8] is not None else None,
source_message_id=str(row[9]) if row[9] is not None else None,
created_at=_as_iso(row[10]),
updated_at=_as_iso(row[11]),
metadata=_load_json(row[12]),
))
return result
async def search(self, **kwargs):
await self._ensure()
return await asyncio.to_thread(
self._search_sync,
kwargs["tenant_id"],
kwargs["agent_id"],
kwargs["subject_key"],
kwargs.get("limit", 20),
)
AutonomousLongTermMemoryStore = OracleAutonomousLongTermMemoryStore
def create_long_term_memory_store(settings):
provider = str(
getattr(settings, "LONG_TERM_MEMORY_PROVIDER", "sqlite")
).strip().lower()
if provider == "memory":
return InMemoryLongTermMemoryStore()
if provider in {"autonomous", "oracle"}:
return OracleAutonomousLongTermMemoryStore(settings)
if provider != "sqlite":
raise ValueError(
"Unsupported LONG_TERM_MEMORY_PROVIDER: "
f"{provider!r}. Expected memory, sqlite, autonomous or oracle."
)
path = (
getattr(settings, "LONG_TERM_MEMORY_SQLITE_PATH", None)
or getattr(settings, "SQLITE_DB_PATH", "./data/agent_framework.db")
)
return SQLiteLongTermMemoryStore(
path,
getattr(settings, "LONG_TERM_MEMORY_TABLE", "agentfw_long_term_memory"),
)

View File

@@ -817,6 +817,16 @@ class AgentRuntimeMixin:
state["memory_context"] = memory_context
state["memory_context_metadata"] = memory_context.metadata
if bool(getattr(settings, "ENABLE_LONG_TERM_MEMORY", False)):
manager = getattr(self, "long_term_memory_manager", None)
if manager is None:
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
manager = create_long_term_memory_manager(settings, telemetry=getattr(self, "telemetry", None))
self.long_term_memory_manager = manager
items = await manager.load(state)
state["long_term_memories"] = [item.to_dict() for item in items]
state["long_term_memory_context"] = manager.render(items)
if memory_context.compressed:
await self._emit_ic(
"IC.MEMORY_COMPRESSION_TRIGGERED",
@@ -891,6 +901,8 @@ class AgentRuntimeMixin:
runtime = self.get_runtime_context(state)
sections = []
sections.extend(self._render_memory_sections(state))
if bool(getattr(getattr(self, "settings", None), "LONG_TERM_MEMORY_INJECT_CONTEXT", True)) and state.get("long_term_memory_context"):
sections.append(str(state["long_term_memory_context"]))
sections.extend([
f"Mensagem do usuário:\n{user_text if user_text is not None else runtime.sanitized_input}",
f"Intent/rota escolhidos pelo framework:\nintent={state.get('intent')} route={state.get('route')}",

View File

@@ -0,0 +1,40 @@
### Recomendações de segurança, autenticação e autorização
Os componentes e templates deste framework podem ser adaptados a diferentes arquiteturas e requisitos de segurança. Para ambientes produtivos, recomenda-se que a solução seja avaliada de acordo com as políticas corporativas, os requisitos regulatórios aplicáveis e as melhores práticas de segurança da Oracle Cloud Infrastructure.
Como orientação geral, recomenda-se considerar autenticação e autorização em todas as interfaces acessíveis por usuários, canais, sistemas externos ou outros serviços. Na OCI, uma opção é utilizar o OCI API Gateway, ou uma camada equivalente, em conjunto com OAuth 2.0/OpenID Connect, validação de tokens e políticas de autorização por rota, scope, papel e tenant.
Métodos como HTTP Basic e API keys podem ser adequados para determinados cenários de integração, especialmente ambientes controlados ou sistemas legados. Nesses casos, recomenda-se utilizá-los sobre TLS, manter as credenciais em um serviço seguro de gerenciamento de segredos e adotar mecanismos de expiração e rotação.
A avaliação de segurança deve considerar, conforme os componentes utilizados pela solução:
- Agent Gateway, Channel Gateway e MCP Gateway;
- backends de agentes e comunicação entre gateways e backends;
- aplicações frontend e APIs consumidas pelo navegador;
- callbacks e webhooks provenientes de canais externos;
- conexões SSE, WebSocket ou outros mecanismos de streaming;
- histórico, memória, checkpoints e dados de sessão;
- endpoints administrativos, de debug, documentação, health e métricas;
- integrações com LLMs, bancos de dados, caches, mensageria e plataformas de observabilidade.
Recomenda-se tratar identificadores recebidos em payloads ou headers — como `tenant_id`, `agent_id`, `user_id`, `customer_id` e `session_id` — como informações de contexto, e não como evidência suficiente da identidade do solicitante. Quando aplicável, esses identificadores podem ser derivados de claims validadas ou relacionados à identidade autenticada antes da execução da operação.
Além da autenticação, recomenda-se avaliar a autorização sobre cada recurso acessado. Essa verificação pode considerar se o usuário ou serviço autenticado possui permissão para acessar o tenant, agente, sessão, histórico, checkpoint, backend ou ferramenta MCP solicitado.
Para comunicação entre serviços, podem ser consideradas identidades específicas por workload e mecanismos como OAuth 2.0 client credentials, OCI IAM, OKE Workload Identity, Instance Principals, Resource Principals ou mTLS. A escolha deve considerar a plataforma de execução e o modelo de confiança definido para a solução.
Para callbacks e webhooks, recomenda-se avaliar os mecanismos disponibilizados pelo provedor do canal, como assinatura digital ou HMAC, JWT, timestamp, identificador de mensagem, proteção contra replay e idempotência.
Em relação aos endpoints operacionais, é recomendável avaliar separadamente:
- endpoints de liveness, com resposta mínima sobre o estado do processo;
- endpoints de readiness, preferencialmente acessíveis apenas pela infraestrutura;
- endpoints de métricas, destinados aos coletores autorizados;
- endpoints de debug e teste, normalmente restritos a ambientes não produtivos;
- documentação OpenAPI, que pode ser desabilitada ou protegida em produção.
Também é recomendável utilizar TLS nas comunicações, restringir a exposição de serviços por meio de redes privadas, sub-redes, NSGs e allowlists, e considerar rate limiting, auditoria, rastreabilidade e monitoramento de acessos negados.
Segredos, tokens, senhas, certificados e chaves podem ser mantidos no OCI Secret Management ou em solução corporativa equivalente, evitando seu armazenamento em código-fonte ou arquivos de configuração versionados. Recomenda-se estabelecer políticas de acesso de menor privilégio, expiração e rotação compatíveis com a criticidade de cada credencial.
Estas recomendações representam uma referência inicial de melhores práticas. A definição final dos mecanismos de autenticação, autorização, proteção de rede e gestão de segredos permanece sob responsabilidade da equipe responsável pela arquitetura e pelo deployment, considerando o contexto, os riscos e os requisitos específicos de cada implementação.

View File

@@ -0,0 +1,40 @@
### Security, Authentication, and Authorization Recommendations
The components and templates in this framework can be adapted to different architectures and security requirements. For production environments, it is recommended that the solution be assessed in accordance with corporate policies, applicable regulatory requirements, and Oracle Cloud Infrastructure security best practices.
As general guidance, authentication and authorization should be considered for all interfaces accessible by users, channels, external systems, or other services. In OCI, one option is to use OCI API Gateway, or an equivalent layer, together with OAuth 2.0/OpenID Connect, token validation, and authorization policies based on route, scope, role, and tenant.
Methods such as HTTP Basic authentication and API keys may be suitable for certain integration scenarios, particularly in controlled environments or with legacy systems. In such cases, it is recommended that they be used over TLS, that credentials be stored in a secure secrets management service, and that expiration and rotation mechanisms be adopted.
The security assessment should consider, according to the components used by the solution:
- Agent Gateway, Channel Gateway, and MCP Gateway;
- agent backends and communication between gateways and backends;
- frontend applications and APIs consumed by the browser;
- callbacks and webhooks originating from external channels;
- SSE, WebSocket, or other streaming connections;
- history, memory, checkpoints, and session data;
- administrative, debug, documentation, health, and metrics endpoints;
- integrations with LLMs, databases, caches, messaging systems, and observability platforms.
Identifiers received in payloads or headers—such as `tenant_id`, `agent_id`, `user_id`, `customer_id`, and `session_id`—should be treated as contextual information rather than sufficient proof of the requester's identity. When applicable, these identifiers may be derived from validated claims or associated with the authenticated identity before the operation is executed.
In addition to authentication, authorization should be evaluated for each accessed resource. This verification may consider whether the authenticated user or service has permission to access the requested tenant, agent, session, history, checkpoint, backend, or MCP tool.
For service-to-service communication, dedicated workload identities and mechanisms such as OAuth 2.0 client credentials, OCI IAM, OKE Workload Identity, Instance Principals, Resource Principals, or mTLS may be considered. The choice should take into account the execution platform and the trust model defined for the solution.
For callbacks and webhooks, the mechanisms provided by the channel provider should be evaluated, such as digital signatures or HMAC, JWT, timestamps, message identifiers, replay protection, and idempotency.
Operational endpoints should also be evaluated separately:
- liveness endpoints, with a minimal response regarding the process status;
- readiness endpoints, preferably accessible only by the infrastructure;
- metrics endpoints, intended for authorized collectors;
- debug and test endpoints, typically restricted to non-production environments;
- OpenAPI documentation, which may be disabled or protected in production.
It is also recommended to use TLS for communications, restrict service exposure through private networks, subnets, NSGs, and allowlists, and consider rate limiting, auditing, traceability, and monitoring of denied access attempts.
Secrets, tokens, passwords, certificates, and keys may be stored in OCI Secret Management or an equivalent corporate solution, avoiding storage in source code or version-controlled configuration files. Least-privilege access policies, expiration, and rotation practices appropriate to the criticality of each credential should be established.
These recommendations provide an initial reference for best practices. The final definition of authentication, authorization, network protection, and secrets management mechanisms remains the responsibility of the team accountable for the architecture and deployment, taking into consideration the context, risks, and specific requirements of each implementation.

View File

@@ -178,3 +178,17 @@ 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

View File

@@ -33,3 +33,6 @@ class AgentState(TypedDict, total=False):
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool
long_term_memories: list[dict[str, Any]]
long_term_memory_context: str
long_term_memory_write_result: dict[str, Any]

View File

@@ -21,6 +21,7 @@ from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.rag.embedding_provider import create_embedding_provider
from agent_framework.cache.cache import create_cache
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
class LegacyOutputGuardrailRail:
@@ -94,6 +95,7 @@ class AgentWorkflow:
self.settings = settings
self.tool_router = tool_router
self.summary_memory = summary_memory
self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry)
self.guardrails = GuardrailPipeline(
observer=self.observer,
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
@@ -121,6 +123,11 @@ class AgentWorkflow:
self.product = ProductAgent(llm, **agent_kwargs)
self.orders = OrdersAgent(llm, **agent_kwargs)
self.support = SupportAgent(llm, **agent_kwargs)
# The existing agent constructors intentionally keep their stable API.
# Long-term memory is injected as a runtime capability after creation.
for agent in (self.billing, self.product, self.orders, self.support):
agent.long_term_memory_manager = self.long_term_memory_manager
self.graph = self._build_graph()
def _node(self, name, fn):
@@ -143,6 +150,7 @@ class AgentWorkflow:
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
builder.add_node("judge", self._node("judge", self.judge))
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory))
builder.add_node("persist", self._node("persist", self.persist))
builder.add_edge(START, "input_guardrails")
@@ -172,7 +180,8 @@ class AgentWorkflow:
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist")
builder.add_edge("supervisor_review", "persist_long_term_memory")
builder.add_edge("persist_long_term_memory", "persist")
builder.add_edge("persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
@@ -598,6 +607,10 @@ class AgentWorkflow:
)
return {"final_answer": answer if ok else answer}
async def persist_long_term_memory(self, state):
result = await self.long_term_memory_manager.persist_turn(state)
return {"long_term_memory_write_result": result}
async def persist(self, state):
async with self.telemetry.span(
"workflow.persist",

View File

@@ -0,0 +1,29 @@
import asyncio
import tempfile
from types import SimpleNamespace
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
async def main():
with tempfile.TemporaryDirectory() as d:
settings = SimpleNamespace(
ENABLE_LONG_TERM_MEMORY=True,
LONG_TERM_MEMORY_PROVIDER='sqlite',
LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db',
LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory',
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20,
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70,
LONG_TERM_MEMORY_AUTO_EXTRACT=True,
)
manager = create_long_term_memory_manager(settings)
first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}}
assert (await manager.persist_turn(first))['saved'] >= 3
second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}}
values = {item.key:item.value for item in await manager.load(second)}
assert values['preferred_name'].lower() == 'cris'
assert values['preferred_language'].lower() == 'python'
assert values['current_project'].lower() == 'atlas'
isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}}
assert await manager.load(isolated) == []
print('OK: persistência, recuperação entre sessões e isolamento validados')
asyncio.run(main())

View File

@@ -175,3 +175,17 @@ 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

View File

@@ -33,3 +33,6 @@ class AgentState(TypedDict, total=False):
supervisor_handover_reason: str
output_supervisor_results: list[dict[str, Any]]
output_guardrails_already_applied: bool
long_term_memories: list[dict[str, Any]]
long_term_memory_context: str
long_term_memory_write_result: dict[str, Any]

View File

@@ -21,6 +21,7 @@ from app.state import AgentState
from agent_framework.rag.rag_service import RagService
from agent_framework.rag.embedding_provider import create_embedding_provider
from agent_framework.cache.cache import create_cache
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
class LegacyOutputGuardrailRail:
@@ -94,6 +95,7 @@ class AgentWorkflow:
self.settings = settings
self.tool_router = tool_router
self.summary_memory = summary_memory
self.long_term_memory_manager = create_long_term_memory_manager(settings, telemetry=telemetry)
self.guardrails = GuardrailPipeline(
observer=self.observer,
enable_parallel=bool(getattr(settings, "ENABLE_PARALLEL_GUARDRAILS", True)),
@@ -121,6 +123,11 @@ class AgentWorkflow:
self.product = ProductAgent(llm, **agent_kwargs)
self.orders = OrdersAgent(llm, **agent_kwargs)
self.support = SupportAgent(llm, **agent_kwargs)
# The existing agent constructors intentionally keep their stable API.
# Long-term memory is injected as a runtime capability after creation.
for agent in (self.billing, self.product, self.orders, self.support):
agent.long_term_memory_manager = self.long_term_memory_manager
self.graph = self._build_graph()
def _node(self, name, fn):
@@ -143,6 +150,7 @@ class AgentWorkflow:
builder.add_node("output_guardrails", self._node("output_guardrails", self.output_guardrails))
builder.add_node("judge", self._node("judge", self.judge))
builder.add_node("supervisor_review", self._node("supervisor_review", self.supervisor_review))
builder.add_node("persist_long_term_memory", self._node("persist_long_term_memory", self.persist_long_term_memory))
builder.add_node("persist", self._node("persist", self.persist))
builder.add_edge(START, "input_guardrails")
@@ -172,7 +180,8 @@ class AgentWorkflow:
builder.add_edge("output_supervisor", "output_guardrails")
builder.add_edge("output_guardrails", "judge")
builder.add_edge("judge", "supervisor_review")
builder.add_edge("supervisor_review", "persist")
builder.add_edge("supervisor_review", "persist_long_term_memory")
builder.add_edge("persist_long_term_memory", "persist")
builder.add_edge("persist", END)
return builder.compile(checkpointer=create_langgraph_checkpointer(self.settings))
@@ -598,6 +607,10 @@ class AgentWorkflow:
)
return {"final_answer": answer if ok else answer}
async def persist_long_term_memory(self, state):
result = await self.long_term_memory_manager.persist_turn(state)
return {"long_term_memory_write_result": result}
async def persist(self, state):
async with self.telemetry.span(
"workflow.persist",

View File

@@ -0,0 +1,29 @@
import asyncio
import tempfile
from types import SimpleNamespace
from agent_framework.memory.long_term_memory import create_long_term_memory_manager
async def main():
with tempfile.TemporaryDirectory() as d:
settings = SimpleNamespace(
ENABLE_LONG_TERM_MEMORY=True,
LONG_TERM_MEMORY_PROVIDER='sqlite',
LONG_TERM_MEMORY_SQLITE_PATH=f'{d}/memory.db',
LONG_TERM_MEMORY_TABLE='agentfw_long_term_memory',
LONG_TERM_MEMORY_MAX_CONTEXT_ITEMS=20,
LONG_TERM_MEMORY_MIN_CONFIDENCE=0.70,
LONG_TERM_MEMORY_AUTO_EXTRACT=True,
)
manager = create_long_term_memory_manager(settings)
first = {'tenant_id':'default','agent_id':'memory_test','session_id':'a','user_text':'Me chame de Cris. Minha linguagem preferida é Python. Meu projeto atual se chama Atlas.','context':{'business_context':{'customer_key':'MEM-001'}}}
assert (await manager.persist_turn(first))['saved'] >= 3
second = {'tenant_id':'default','agent_id':'memory_test','session_id':'b','context':{'business_context':{'customer_key':'MEM-001'}}}
values = {item.key:item.value for item in await manager.load(second)}
assert values['preferred_name'].lower() == 'cris'
assert values['preferred_language'].lower() == 'python'
assert values['current_project'].lower() == 'atlas'
isolated = {'tenant_id':'default','agent_id':'memory_test','session_id':'c','context':{'business_context':{'customer_key':'MEM-002'}}}
assert await manager.load(isolated) == []
print('OK: persistência, recuperação entre sessões e isolamento validados')
asyncio.run(main())

View File

@@ -0,0 +1,44 @@
from types import SimpleNamespace
from agent_framework.memory.long_term_store import (
InMemoryLongTermMemoryStore,
OracleAutonomousLongTermMemoryStore,
SQLiteLongTermMemoryStore,
create_long_term_memory_store,
)
def settings(provider: str):
return SimpleNamespace(
LONG_TERM_MEMORY_PROVIDER=provider,
LONG_TERM_MEMORY_SQLITE_PATH=":memory:",
LONG_TERM_MEMORY_TABLE="agentfw_long_term_memory",
LONG_TERM_MEMORY_ORACLE_TABLE=None,
ADB_USER="user",
ADB_PASSWORD="password",
ADB_DSN="service_high",
ADB_WALLET_LOCATION=None,
ADB_WALLET_PASSWORD=None,
ADB_TABLE_PREFIX="AGENTFW",
)
def test_factory_memory():
assert isinstance(create_long_term_memory_store(settings("memory")), InMemoryLongTermMemoryStore)
def test_factory_sqlite():
assert isinstance(create_long_term_memory_store(settings("sqlite")), SQLiteLongTermMemoryStore)
def test_factory_autonomous():
store = create_long_term_memory_store(settings("autonomous"))
assert isinstance(store, OracleAutonomousLongTermMemoryStore)
assert store.table == "AGENTFW_LONG_TERM_MEMORY"
def test_factory_oracle_alias():
assert isinstance(
create_long_term_memory_store(settings("oracle")),
OracleAutonomousLongTermMemoryStore,
)