@langgraph-js/memory
Version:
A memory management system based on PostgreSQL + pgvector for LangGraph workflows
520 lines (409 loc) • 14.6 kB
Markdown
# LangGraph-Memory
A memory management system based on PostgreSQL + pgvector, designed for memory storage and retrieval in LangGraph
workflows.
## Features
- **Vector Similarity Search**: Support for HNSW and IVFFlat indexes for efficient vector similarity search
- **LLM-Powered Memory Management**: Intelligent memory merging and deduplication using large language models
- **Multi-dimensional Filtering**: Advanced querying capabilities with multiple filter options
- **Complete CRUD Operations**: Full create, read, update, and delete operations for memory items
- **LangChain Integration**: Seamless integration with LangChain's embedding and LLM capabilities, including
`LangChainEmbedder` wrapper
- **Extensible Embedder Interface**: Support for custom embedding providers beyond LangChain
- **Memory Expiration**: Support for time-based memory expiration
- **Immutable Memories**: Option to mark memories as read-only
- **Metadata Support**: Flexible metadata storage for enhanced memory organization
- **Multi-tenant Support**: Organization-based isolation for enterprise use cases
## Prerequisites
- Node.js 18+
- PostgreSQL 15+ with pgvector extension
- OpenAI API Key (required for testing and examples)
## Installation and Setup
### 1. Install Dependencies
```bash
pnpm install
```
### 2. Start PostgreSQL with pgvector
```bash
docker-compose up -d
```
### 3. Create Test Database
```bash
createdb langgraph_memory_test
```
### 4. Set Environment Variables
```bash
export OPENAI_API_KEY="your-openai-api-key-here"
```
## Testing
Run the complete test suite (requires internet connection and OpenAI API):
```bash
pnpm test
```
Run a specific test file:
```bash
pnpm test memory-database.test.ts
```
## Usage Examples
### Basic Setup
```typescript
import { MemoryDataBase } from './src/MemoryDatabase';
import { PostgresVectorStore } from './src/vector-store/pg';
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { Pool } from 'pg';
// Initialize database connection
const pool = new Pool({
host: 'localhost',
port: 5432,
user: 'postgres',
password: 'postgres',
database: 'langgraph_memory',
});
// Initialize vector store
const vectorStore = new PostgresVectorStore({
pool,
tableName: 'memory_vectors',
dimension: 1536, // text-embedding-3-small dimension
});
// Create embedder (direct implementation)
const embedder = {
embed: async (text: string) => {
const openaiEmbedder = new OpenAIEmbeddings({
modelName: 'text-embedding-3-small',
});
return await openaiEmbedder.embedQuery(text);
},
embedBatch: async (texts: string[]) => {
const openaiEmbedder = new OpenAIEmbeddings({
modelName: 'text-embedding-3-small',
});
const embeddings = await openaiEmbedder.embedDocuments(texts);
return embeddings.map((embedding, index) => ({
embedding,
original: texts[index],
}));
},
};
// Initialize memory database
const memoryDB = new MemoryDataBase('your-org-id', new ChatOpenAI({ modelName: 'gpt-4o-mini' }), embedder, vectorStore);
// Setup database schema
await vectorStore.initialize();
```
### Adding Memories
```typescript
import { HumanMessage, AIMessage } from '@langchain/core/messages';
// Add conversation memories
// Note: At least one of userId, agentId, or runId is required
const messages = [
new HumanMessage('What is TypeScript?'),
new AIMessage('TypeScript is a programming language developed by Microsoft...'),
];
const result = await memoryDB.add(messages, {
userId: 'user123',
agentId: 'agent456',
metadata: {
topic: 'programming',
language: 'typescript',
},
});
console.log('Added memories:', result.results.length);
```
### Searching Memories
```typescript
// Search with text query
// Note: At least one of userId, agentId, or runId is required
const searchResult = await memoryDB.search('programming languages', {
userId: 'user123',
limit: 5,
filters: {
categories: 'technical', // Filter by category
createdAtAfter: '2024-01-01T00:00:00Z', // Filter by creation time
},
});
console.log('Search results:', searchResult.results);
```
### Advanced Filtering
The memory system supports comprehensive filtering capabilities:
```typescript
// Get memories with multiple filters
const filteredMemories = await memoryDB.getAll({
userId: 'user123',
categories: ['hobby', 'work'], // Must contain both categories (AND operation)
createdAtBefore: new Date().toISOString(),
createdAtAfter: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // Last 7 days
limit: 20,
});
// Search with category filter
const hobbyMemories = await memoryDB.search('interests', {
userId: 'user123',
filters: {
categories: 'hobby', // Single category filter
},
limit: 10,
});
// Filter by time ranges
const recentMemories = await memoryDB.getAll({
userId: 'user123',
updatedAtBefore: new Date().toISOString(),
updatedAtAfter: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // Last 24 hours
});
// Filter by expiration date
const expiringMemories = await memoryDB.getAll({
userId: 'user123',
expirationDateBefore: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(), // Expires within 7 days
});
```
### Memory Management
```typescript
// Get a specific memory
const memory = await memoryDB.get('memory-id-123');
// Update memory content
await memoryDB.update('memory-id-123', 'Updated memory content');
// Delete a specific memory
await memoryDB.delete('memory-id-123');
// Delete all memories for a user
await memoryDB.deleteAll({
userId: 'user123',
});
// Delete memories by category
await memoryDB.deleteAll({
userId: 'user123',
categories: 'temporary',
});
// Delete memories by time range
await memoryDB.deleteAll({
userId: 'user123',
createdAtBefore: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(), // Older than 30 days
});
// Delete expired memories
await memoryDB.deleteAll({
expirationDateBefore: new Date().toISOString(),
});
// Get all memories with pagination
const allMemories = await memoryDB.getAll({
userId: 'user123',
limit: 10,
});
```
## API Reference
### PostgresVectorStore Class
```typescript
constructor(config: {
pool: Pool;
tableName?: string;
dimension?: number;
})
```
**Parameters:**
- `pool`: PostgreSQL connection pool
- `tableName`: Table name for vector storage (default: 'memories')
- `dimension`: Vector dimension (default: 1536)
#### Methods
- `initialize(): Promise<void>` - Create tables and indexes
- `insert(id, orgId, memory, embedding, metadata?): Promise<void>` - Insert vector data
- `search(queryEmbedding, config): Promise<VectorSearchResult[]>` - Search similar vectors
- `delete(id): Promise<void>` - Delete vector by ID
- `reset(): Promise<void>` - Clear all data
- `close(): Promise<void>` - Close connections
### Embedder Interface
```typescript
interface Embedder {
embed(text: string): Promise<number[]>;
embedBatch(texts: string[]): Promise<
{
embedding: number[];
original: string;
}[]
>;
}
```
The embedder interface provides text-to-vector conversion methods. You can implement this interface with any embedding
provider (OpenAI, HuggingFace, etc.).
#### Direct Implementation
```typescript
const embedder: Embedder = {
embed: async (text: string) => {
const openaiEmbedder = new OpenAIEmbeddings({
modelName: 'text-embedding-3-small',
});
return await openaiEmbedder.embedQuery(text);
},
embedBatch: async (texts: string[]) => {
const openaiEmbedder = new OpenAIEmbeddings({
modelName: 'text-embedding-3-small',
});
const embeddings = await openaiEmbedder.embedDocuments(texts);
return embeddings.map((embedding, index) => ({
embedding,
original: texts[index],
}));
},
};
```
#### Custom Embedder Implementation
If you need to use a different embedding provider, you can implement the `Embedder` interface directly:
```typescript
class CustomEmbedder implements Embedder {
async embed(text: string): Promise<number[]> {
// Your custom embedding logic
return [
/* embedding vector */
];
}
async embedBatch(texts: string[]): Promise<{ embedding: number[]; original: string }[]> {
// Your custom batch embedding logic
return texts.map((text) => ({
embedding: [
/* embedding vector */
],
original: text,
}));
}
}
const embedder = new CustomEmbedder();
```
### MemoryDataBase Class
#### Constructor
```typescript
constructor(
org_id: string,
llm: BaseChatModel,
embedder: Embedder,
vectorStore: PostgresVectorStore,
customPrompt?: string
)
```
**Parameters:**
- `org_id`: Organization identifier for multi-tenant support
- `llm`: Language model for memory processing and deduplication
- `embedder`: Embedder implementation with `embed` and `embedBatch` methods
- `vectorStore`: PostgreSQL vector store instance
- `customPrompt`: Optional custom prompt for memory extraction
#### Methods
- `setup(): Promise<void>` - Initialize database schema and indexes
- `add(messages, config): Promise<SearchResult>` - Add new memories from conversation messages
- `get(memoryId: string): Promise<MemoryItem | null>` - Retrieve a specific memory
- `search(query: string, config): Promise<SearchResult>` - Search memories by text similarity
- `update(memoryId: string, data: string): Promise<{ message: string }>` - Update memory content
- `delete(memoryId: string): Promise<{ message: string }>` - Delete a specific memory
- `deleteAll(config: DeleteAllMemoryOptions): Promise<{ message: string }>` - Delete all memories matching filters
- `reset(): Promise<void>` - Reset the entire memory database
- `getAll(config: GetAllMemoryOptions): Promise<SearchResult>` - Get all memories with optional filtering
### MemoryItem Interface
```typescript
interface MemoryItem {
id: string;
org_id: string;
agent_id?: string;
user_id?: string;
app_id?: string;
run_id?: string;
immutable?: boolean;
memory: string;
categories?: string[];
metadata?: Record<string, any>;
score?: number;
updated_at: string;
created_at: string;
expiration_date?: string;
}
```
### Configuration Options
#### MemoryFilters Interface
```typescript
interface MemoryFilters extends IdSet {
categories?: string[] | string; // Single category or array of categories
createdAtBefore?: string; // ISO date string
createdAtAfter?: string; // ISO date string
updatedAtBefore?: string; // ISO date string
updatedAtAfter?: string; // ISO date string
expirationDateBefore?: string; // ISO date string
expirationDateAfter?: string; // ISO date string
[key: string]: any; // Additional custom filters
}
```
#### Add Configuration
```typescript
interface AddConfig extends IdSet {
metadata?: Record<string, any>; // Additional metadata to store
filters?: MemoryFilters; // Filters for the operation
infer?: boolean; // Whether to infer categories (default: true)
}
```
#### Search Configuration
```typescript
interface SearchConfig extends IdSet {
limit?: number; // Maximum number of results (default: 100)
filters?: MemoryFilters; // Additional filters to apply
}
```
#### GetAll Configuration
```typescript
interface GetAllMemoryOptions extends MemoryFilters {
limit?: number; // Maximum number of results (default: 100)
}
```
#### DeleteAll Configuration
```typescript
interface DeleteAllMemoryOptions extends MemoryFilters {
// Same as MemoryFilters - at least one filter is required
}
```
## Advanced Features
### Multi-tenant Organization Isolation
The memory system provides complete organization-based isolation, ensuring that different organizations cannot access
each other's memories:
```typescript
// Create separate memory databases for different organizations
const orgAMemoryDB = new MemoryDataBase('org-a', llm, embedder, vectorStore);
const orgBMemoryDB = new MemoryDataBase('org-b', llm, embedder, vectorStore);
// Add memories for different organizations
await orgAMemoryDB.add([new HumanMessage('Organization A data')], { userId: 'user1' });
await orgBMemoryDB.add([new HumanMessage('Organization B data')], { userId: 'user1' });
// Each organization can only access their own data
const orgAData = await orgAMemoryDB.getAll({ userId: 'user1' }); // Only sees org-a data
const orgBData = await orgBMemoryDB.getAll({ userId: 'user1' }); // Only sees org-b data
// Reset only affects the current organization
await orgAMemoryDB.reset(); // Only clears org-a data
```
### Memory Merging and Deduplication
The system uses LLM to intelligently merge and deduplicate memories when adding new content. This prevents duplicate
information while preserving important context.
### Custom Prompts
You can customize the behavior of memory extraction and merging by providing custom prompts:
```typescript
const memoryDB = new MemoryDataBase(
'your-org-id',
llm,
embedder,
vectorStore,
'Your custom prompt for memory processing...',
);
```
### Vector Store Configuration
The vector store supports various configuration options for performance optimization:
```typescript
const vectorStore = new PostgresVectorStore(pool, {
tableName: 'memory_vectors',
dimension: 1536,
indexType: 'hnsw', // 'hnsw' or 'ivfflat'
hnswM: 16, // HNSW parameter
hnswEfConstruction: 64, // HNSW parameter
ivfflatLists: 100, // IVFFlat parameter
});
```
## Performance Considerations
- **Indexing**: Choose appropriate vector indexes based on your data size and query patterns
- **Batch Operations**: Use batch embedding for better performance when adding multiple memories
- **Connection Pooling**: Configure PostgreSQL connection pooling for production use
- **Memory Expiration**: Regularly clean up expired memories to maintain optimal performance
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests for new functionality
5. Ensure all tests pass
6. Submit a pull request
## Acknowledgments
This project draws inspiration from the [mem0](https://github.com/mem0ai/mem0) project's source code.
## License
Apache License 2.0