Skip to main content

Memory Systems

Memory Systems provide persistent storage for conversations and knowledge, enabling AI to remember past interactions and retrieve relevant context. Clear AI v2 uses a dual-memory architecture: Neo4j for episodic memory (conversation flow) and Pinecone for semantic memory (searchable knowledge).

What Problem Does This Solve?

The Problem: Without memory, AI:

  • Forgets previous conversations immediately
  • Can't learn from past interactions
  • Repeats mistakes
  • Has no context beyond current conversation
  • Can't build relationships with users

The Solution: Dual memory system:

  • Episodic Memory (Neo4j): Remembers conversation flow, relationships, temporal order
  • Semantic Memory (Pinecone): Stores searchable knowledge, facts, and learned information

Architecture

┌────────────────────────────────┐
│ Conversation Context │
└───────────┬────────────────────┘

┌───────┴────────┐
│ │
┌───▼────────┐ ┌───▼─────────┐
│ Episodic │ │ Semantic │
│ Memory │ │ Memory │
│ (Neo4j) │ │ (Pinecone) │
└────────────┘ └─────────────┘
│ │
│ "When did │ "What does
│ user ask │ user prefer
│ about X?" │ for Y?"
│ │
└────────┬───────┘

┌──────▼───────┐
│ Retrieval │
└──────────────┘

Episodic Memory (Neo4j)

Stores conversation flow as a graph:

  • Who said what, when
  • How topics relate
  • Conversation structure
  • Temporal relationships

Basic Usage

import { Neo4jMemory } from 'clear-ai-v2/shared';

const neo4j = new Neo4jMemory({
uri: process.env.NEO4J_URI,
user: process.env.NEO4J_USER,
password: process.env.NEO4J_PASSWORD
});

await neo4j.connect();

// Store conversation
await neo4j.storeMemory({
sessionId: 'user_123_session_1',
userId: 'user_123',
content: 'Show me shipments from last week',
type: 'query',
timestamp: new Date(),
metadata: {
intent: 'data_request',
entities: { timeframe: 'last week' }
}
});

// Retrieve conversation history
const history = await neo4j.getConversationHistory(
'user_123',
{ limit: 10 }
);

// Find related conversations
const related = await neo4j.findRelated('user_123', 'shipments');

Graph Structure

(User) -[:HAD_SESSION]-> (Session)
(Session) -[:CONTAINS]-> (Message)
(Message) -[:FOLLOWED_BY]-> (Message)
(Message) -[:MENTIONS]-> (Entity)
(Message) -[:RELATES_TO]-> (Topic)

Semantic Memory (Pinecone)

Stores searchable knowledge as vectors:

  • Facts and information
  • User preferences
  • Learned patterns
  • Contextual knowledge

Basic Usage

import { PineconeMemory } from 'clear-ai-v2/shared';

const pinecone = new PineconeMemory({
apiKey: process.env.PINECONE_API_KEY,
index: process.env.PINECONE_INDEX,
embeddingProvider: 'ollama' // or 'openai'
});

await pinecone.connect();

// Store knowledge
await pinecone.storeMemory({
id: 'fact_001',
text: 'User prefers weekly reports on Mondays',
metadata: {
userId: 'user_123',
type: 'preference',
category: 'reporting'
}
});

// Semantic search
const results = await pinecone.search(
'When does user want reports?',
{ limit: 5, filter: { userId: 'user_123' } }
);

// Results ranked by semantic similarity
console.log(results[0].text);
// "User prefers weekly reports on Mondays"
console.log(results[0].score); // 0.92

Memory Manager

Orchestrates both memory systems:

import { MemoryManager } from 'clear-ai-v2/shared';

const memory = new MemoryManager({
neo4j: {
uri: process.env.NEO4J_URI,
user: process.env.NEO4J_USER,
password: process.env.NEO4J_PASSWORD
},
pinecone: {
apiKey: process.env.PINECONE_API_KEY,
index: process.env.PINECONE_INDEX
},
embedding: {
provider: 'ollama',
model: 'nomic-embed-text'
}
});

await memory.connect();

// Store in both systems
await memory.storeConversation({
sessionId: 'session_1',
userId: 'user_123',
messages: conversationMessages
});

// Retrieve relevant context
const context = await memory.retrieveContext({
userId: 'user_123',
query: 'contaminated shipments',
limit: 5
});

Real-World Example

Complete agent with memory:

import {
MemoryManager,
ContextManager,
LLMProvider,
ResponseBuilder
} from 'clear-ai-v2/shared';

class MemoryEnhancedAgent {
private memory: MemoryManager;
private context: ContextManager;
private llm: LLMProvider;

constructor() {
this.memory = new MemoryManager({
/* config */
});
this.context = new ContextManager({ maxTokens: 4000 });
this.llm = new LLMProvider();
}

async chat(userId: string, message: string) {
// 1. Retrieve relevant memories
const memories = await this.memory.retrieveContext({
userId,
query: message,
limit: 3
});

// 2. Add memories to context
if (memories.length > 0) {
await this.context.addMessage({
role: 'system',
content: `Relevant context:\n${memories.map(m => m.text).join('\n')}`
});
}

// 3. Add user message
await this.context.addMessage({
role: 'user',
content: message
});

// 4. Get LLM response
const formatted = this.context.getFormattedMessages();
const response = await this.llm.chat(formatted);

// 5. Store conversation in memory
await this.memory.storeConversation({
sessionId: `session_${Date.now()}`,
userId,
messages: [
{ role: 'user', content: message },
{ role: 'assistant', content: response }
]
});

// 6. Extract and store new knowledge
const knowledge = await this.extractKnowledge(message, response);
if (knowledge) {
await this.memory.storeKnowledge({
userId,
text: knowledge,
metadata: { extractedAt: new Date() }
});
}

return ResponseBuilder.answer(response);
}

async extractKnowledge(userMsg: string, agentMsg: string): Promise<string | null> {
// Extract facts, preferences, or learned information
if (userMsg.includes('I prefer') || userMsg.includes('I like')) {
return userMsg; // Store user preference
}
return null;
}
}

// Usage
const agent = new MemoryEnhancedAgent();

// First conversation
await agent.chat('user_123', 'I prefer weekly reports on Mondays');
// Stored in memory

// Later conversation (different session)
await agent.chat('user_123', 'When should I send reports?');
// Agent recalls: "Based on your preference, send reports on Mondays"

Configuration

Neo4j Setup

# .env
NEO4J_URI=bolt://localhost:7687
NEO4J_USER=neo4j
NEO4J_PASSWORD=your-password
ENABLE_NEO4J=true

Pinecone Setup

# .env
PINECONE_API_KEY=your-api-key
PINECONE_INDEX=clear-ai-memories
ENABLE_PINECONE=true

# Embedding configuration
MEMORY_EMBEDDING_PROVIDER=ollama # or openai
MEMORY_EMBEDDING_MODEL=nomic-embed-text
MEMORY_EMBEDDING_DIMENSIONS=768

Optional Configuration

const memory = new MemoryManager({
// Neo4j config
neo4j: {
uri: '...',
user: '...',
password: '...',
database: 'neo4j' // Optional
},

// Pinecone config
pinecone: {
apiKey: '...',
index: '...',
namespace: 'production' // Optional
},

// Embedding config
embedding: {
provider: 'ollama',
model: 'nomic-embed-text',
apiKey: '...', // For OpenAI
dimensions: 768
},

// Retrieval config
retrieval: {
defaultLimit: 5,
minScore: 0.7, // Minimum similarity
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days
}
});

Query Patterns

Find Recent Conversations

const recent = await neo4j.getConversationHistory(userId, {
limit: 10,
since: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // Last 7 days
});

Search by Topic

const about = await pinecone.search('contamination issues', {
filter: { userId, type: 'issue' },
limit: 5
});
const similar = await neo4j.findSimilarUsers(userId, {
basedOn: 'queries',
limit: 5
});

Temporal Queries

const timeline = await neo4j.getTimeline(userId, {
from: startDate,
to: endDate,
topics: ['shipments', 'facilities']
});

Testing

Memory systems have comprehensive tests (15 integration tests):

# Unit tests (with mocks)
yarn test neo4j.test.ts
yarn test pinecone.test.ts
yarn test manager.test.ts

# Integration tests (real services)
yarn test:integration memory

Best Practices

1. Enable Memory Selectively

// ❌ Don't enable for simple queries
if (simpleCalculation) {
// No need for memory
}

// ✅ Enable for conversational use cases
if (multiTurnConversation) {
await memory.storeConversation(...)
}

2. Clean Old Memories

// Set up periodic cleanup
setInterval(async () => {
await memory.cleanup({
olderThan: 90 * 24 * 60 * 60 * 1000, // 90 days
keepImportant: true
});
}, 24 * 60 * 60 * 1000); // Daily

3. Use Appropriate Filters

// ❌ Don't search everything
const results = await pinecone.search(query);

// ✅ Filter by user/context
const results = await pinecone.search(query, {
filter: {
userId: currentUser,
type: 'preference'
}
});

4. Handle Connection Failures

try {
await memory.connect();
} catch (error) {
console.error('Memory unavailable, continuing without');
// Agent still works, just without memory
}

Performance

Neo4j:

  • Store: ~10-50ms
  • Retrieve: ~20-100ms
  • Complex queries: ~100-500ms

Pinecone:

  • Store: ~50-200ms (includes embedding)
  • Search: ~100-300ms
  • Batch operations: More efficient

Embeddings:

  • Ollama (local): ~100ms, free
  • OpenAI: ~200ms, $0.0001 per 1K tokens

Privacy & Security

Data Isolation

// Always filter by userId
filter: { userId: currentUser }

// Never mix user data
// ❌ BAD
await pinecone.search(query); // Searches all users

// ✅ GOOD
await pinecone.search(query, {
filter: { userId }
});

Sensitive Data

// Don't store sensitive information
// ❌ BAD
await memory.store({
text: `User password is ${password}`
});

// ✅ GOOD
await memory.store({
text: 'User updated their password',
metadata: { action: 'security_update' }
});

Next: Embeddings - Vector generation for semantic memory