A memory management system based on PostgreSQL + pgvector, designed for memory storage and retrieval in LangGraph workflows.
- 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
LangChainEmbedderwrapper - 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
- Node.js 18+
- PostgreSQL 15+ with pgvector extension
- OpenAI API Key (required for testing and examples)
pnpm installdocker-compose up -dcreatedb langgraph_memory_testexport OPENAI_API_KEY="your-openai-api-key-here"Run the complete test suite (requires internet connection and OpenAI API):
pnpm testRun a specific test file:
pnpm test memory-database.test.tsimport { 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();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);// 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);The memory system supports comprehensive filtering capabilities:
// 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
});// 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,
});constructor(config: {
pool: Pool;
tableName?: string;
dimension?: number;
})Parameters:
pool: PostgreSQL connection pooltableName: Table name for vector storage (default: 'memories')dimension: Vector dimension (default: 1536)
initialize(): Promise<void>- Create tables and indexesinsert(id, orgId, memory, embedding, metadata?): Promise<void>- Insert vector datasearch(queryEmbedding, config): Promise<VectorSearchResult[]>- Search similar vectorsdelete(id): Promise<void>- Delete vector by IDreset(): Promise<void>- Clear all dataclose(): Promise<void>- Close connections
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.).
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],
}));
},
};If you need to use a different embedding provider, you can implement the Embedder interface directly:
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();constructor(
org_id: string,
llm: BaseChatModel,
embedder: Embedder,
vectorStore: PostgresVectorStore,
customPrompt?: string
)Parameters:
org_id: Organization identifier for multi-tenant supportllm: Language model for memory processing and deduplicationembedder: Embedder implementation withembedandembedBatchmethodsvectorStore: PostgreSQL vector store instancecustomPrompt: Optional custom prompt for memory extraction
setup(): Promise<void>- Initialize database schema and indexesadd(messages, config): Promise<SearchResult>- Add new memories from conversation messagesget(memoryId: string): Promise<MemoryItem | null>- Retrieve a specific memorysearch(query: string, config): Promise<SearchResult>- Search memories by text similarityupdate(memoryId: string, data: string): Promise<{ message: string }>- Update memory contentdelete(memoryId: string): Promise<{ message: string }>- Delete a specific memorydeleteAll(config: DeleteAllMemoryOptions): Promise<{ message: string }>- Delete all memories matching filtersreset(): Promise<void>- Reset the entire memory databasegetAll(config: GetAllMemoryOptions): Promise<SearchResult>- Get all memories with optional filtering
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;
}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
}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)
}interface SearchConfig extends IdSet {
limit?: number; // Maximum number of results (default: 100)
filters?: MemoryFilters; // Additional filters to apply
}interface GetAllMemoryOptions extends MemoryFilters {
limit?: number; // Maximum number of results (default: 100)
}interface DeleteAllMemoryOptions extends MemoryFilters {
// Same as MemoryFilters - at least one filter is required
}The memory system provides complete organization-based isolation, ensuring that different organizations cannot access each other's memories:
// 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 dataThe system uses LLM to intelligently merge and deduplicate memories when adding new content. This prevents duplicate information while preserving important context.
You can customize the behavior of memory extraction and merging by providing custom prompts:
const memoryDB = new MemoryDataBase(
'your-org-id',
llm,
embedder,
vectorStore,
'Your custom prompt for memory processing...',
);The vector store supports various configuration options for performance optimization:
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
});- 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
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
This project draws inspiration from the mem0 project's source code.
Apache License 2.0