UNPKG

@langgraph-js/memory

Version:

A memory management system based on PostgreSQL + pgvector for LangGraph workflows

520 lines (409 loc) 14.6 kB
# 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