mirror of
https://github.com/hoshikawa2/agent_platform_oci.git
synced 2026-09-07 10:13:46 +00:00
Disclaimer best practicies Oracle for Security
This commit is contained in:
14
.env
14
.env
@@ -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
|
||||
|
||||
16
.env.example
16
.env.example
@@ -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
|
||||
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.
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
11
libs/agent_framework/docs/LONG_TERM_MEMORY.md
Normal file
11
libs/agent_framework/docs/LONG_TERM_MEMORY.md
Normal 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
|
||||
```
|
||||
63
libs/agent_framework/docs/LONG_TERM_MEMORY_AUTONOMOUS_EN.md
Normal file
63
libs/agent_framework/docs/LONG_TERM_MEMORY_AUTONOMOUS_EN.md
Normal 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.
|
||||
63
libs/agent_framework/docs/LONG_TERM_MEMORY_AUTONOMOUS_PT.md
Normal file
63
libs/agent_framework/docs/LONG_TERM_MEMORY_AUTONOMOUS_PT.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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"),
|
||||
)
|
||||
@@ -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')}",
|
||||
|
||||
40
specs/Disclaimer Auth and Security_PT.md
Normal file
40
specs/Disclaimer Auth and Security_PT.md
Normal 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.
|
||||
40
specs/Disclaimer_Auth_and_Security_EN.md
Normal file
40
specs/Disclaimer_Auth_and_Security_EN.md
Normal 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.
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
|
||||
Binary file not shown.
@@ -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",
|
||||
|
||||
Binary file not shown.
@@ -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())
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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]
|
||||
|
||||
Binary file not shown.
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
44
tests/unit/test_long_term_memory_autonomous.py
Normal file
44
tests/unit/test_long_term_memory_autonomous.py
Normal 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,
|
||||
)
|
||||
Reference in New Issue
Block a user