UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

485 lines (484 loc) 17.2 kB
/** * LongTermMemory implementation * Optimized for persistent storage with semantic search capabilities */ import { v4 as uuidv4 } from 'uuid'; /** * File-based storage adapter implementation * In a real implementation, this would use proper file I/O with atomic writes */ class InMemoryStorageAdapter { storage = new Map(); async save(key, value) { this.storage.set(key, value); } async load(key) { return this.storage.get(key) || null; } async delete(key) { return this.storage.delete(key); } async clear() { this.storage.clear(); } async keys() { return Array.from(this.storage.keys()); } } /** * LongTermMemory class for persistent storage of memories * Optimized for: * - Persistent storage with indexing * - Efficient retrieval with caching * - Semantic search when available */ export class LongTermMemory { storage; namespace; useCache; cacheSize; vectorDbPath; archiveAgeMs; // In-memory cache for faster access cache = new Map(); cacheOrder = []; // Index for faster searching contentIndex = new Map(); metadataIndex = new Map(); constructor(options = {}) { this.storage = options.storageAdapter || new InMemoryStorageAdapter(); this.namespace = options.namespace || 'crewai'; this.useCache = options.useCache ?? true; this.cacheSize = options.cacheSize || 1000; this.vectorDbPath = options.vectorDbPath; this.archiveAgeMs = options.archiveAgeMs || 30 * 24 * 60 * 60 * 1000; // 30 days default // Initialize the memory store this.init(); } /** * Initialize the memory store */ async init() { // Load the index if it exists await this.loadIndex(); } /** * Add an item to long-term memory with persistence */ async add(content, metadata) { // Create new memory item const item = { id: uuidv4(), content, metadata, createdAt: Date.now(), lastAccessedAt: Date.now() }; // Save to storage await this.storage.save(this.getItemKey(item.id), item); // Add to cache if enabled if (this.useCache) { this.addToCache(item); } // Update indexes this.indexItem(item); return item; } /** * Search for items in long-term memory with optimized indexing */ async search(params) { const { query, metadata, limit = 10, offset = 0, minRelevance = 0 } = params; // If we have a vector database and a query, use semantic search if (this.vectorDbPath && query && query.length > 0) { return this.semanticSearch(query, metadata, limit, offset, minRelevance); } // Otherwise, use index-based search return this.indexSearch(query, metadata, limit, offset, minRelevance); } /** * Get an item by its ID with caching */ async get(id) { // Check cache first if enabled if (this.useCache) { const cachedItem = this.cache.get(id); if (cachedItem) { // Update last accessed time cachedItem.lastAccessedAt = Date.now(); // Update cache order this.updateCacheOrder(id); return cachedItem; } } // If not in cache, load from storage const item = await this.storage.load(this.getItemKey(id)); if (!item) { return null; } // Update last accessed time item.lastAccessedAt = Date.now(); // Save the updated item await this.storage.save(this.getItemKey(id), item); // Add to cache if enabled if (this.useCache) { this.addToCache(item); } return item; } /** * Update an existing memory item with delta updates */ async update(id, updates) { // Get the existing item const existingItem = await this.get(id); if (!existingItem) { return null; } // Update the item const updatedItem = { ...existingItem, ...updates, lastAccessedAt: Date.now() }; // Remove from index if content or metadata is changing if (updates.content || updates.metadata) { this.removeFromIndex(existingItem); } // Save the updated item await this.storage.save(this.getItemKey(id), updatedItem); // Update cache if enabled if (this.useCache) { this.cache.set(id, updatedItem); this.updateCacheOrder(id); } // Update index if content or metadata changed if (updates.content || updates.metadata) { this.indexItem(updatedItem); } return updatedItem; } /** * Remove an item from long-term memory */ async remove(id) { // Get the item first to remove from index const item = await this.get(id); if (!item) { return false; } // Remove from index this.removeFromIndex(item); // Remove from storage const removed = await this.storage.delete(this.getItemKey(id)); // Remove from cache if enabled if (this.useCache) { this.cache.delete(id); this.cacheOrder = this.cacheOrder.filter(itemId => itemId !== id); } return removed; } /** * Clear all items from long-term memory */ async clear() { // Clear storage await this.storage.clear(); // Clear cache this.cache.clear(); this.cacheOrder = []; // Clear indexes this.contentIndex.clear(); this.metadataIndex.clear(); } /** * Reset the memory (clear and initialize) */ async reset() { await this.clear(); await this.init(); } /** * Archive old memories to reduce storage size * In a real implementation, this would move old items to cold storage */ async archiveOldMemories() { const keys = await this.storage.keys(); const itemKeys = keys.filter(key => key.startsWith(`${this.namespace}:item:`)); let archivedCount = 0; const now = Date.now(); const cutoffTime = now - this.archiveAgeMs; for (const key of itemKeys) { const item = await this.storage.load(key); if (item && item.createdAt < cutoffTime) { // Archive the item (in a real implementation, this would move it to archive storage) await this.remove(item.id); archivedCount++; } } return archivedCount; } /** * Get the storage key for an item */ getItemKey(id) { return `${this.namespace}:item:${id}`; } /** * Add an item to the in-memory cache */ addToCache(item) { // If cache is at capacity, remove the least recently used item if (this.cache.size >= this.cacheSize) { const oldestId = this.cacheOrder.shift(); if (oldestId) { this.cache.delete(oldestId); } } // Add to cache this.cache.set(item.id, item); this.cacheOrder.push(item.id); } /** * Update the order of items in cache (LRU tracking) */ updateCacheOrder(id) { // Remove from current position this.cacheOrder = this.cacheOrder.filter(itemId => itemId !== id); // Add to end (most recently used) this.cacheOrder.push(id); } /** * Index an item for faster searching */ indexItem(item) { // Index content (simple word-based indexing) const words = item.content.toLowerCase().split(/\s+/); for (const word of words) { if (word.length > 2) { // Only index words longer than 2 characters const wordSet = this.contentIndex.get(word) || new Set(); wordSet.add(item.id); this.contentIndex.set(word, wordSet); } } // Index metadata if (item.metadata) { for (const [key, value] of Object.entries(item.metadata)) { // Skip null or undefined values if (value === null || value === undefined) { continue; } const keyMap = this.metadataIndex.get(key) || new Map(); const valueSet = keyMap.get(value) || new Set(); valueSet.add(item.id); keyMap.set(value, valueSet); this.metadataIndex.set(key, keyMap); } } } /** * Remove an item from the index */ removeFromIndex(item) { // Remove from content index const words = item.content.toLowerCase().split(/\s+/); for (const word of words) { if (word.length > 2) { const wordSet = this.contentIndex.get(word); if (wordSet) { wordSet.delete(item.id); if (wordSet.size === 0) { this.contentIndex.delete(word); } } } } // Remove from metadata index if (item.metadata) { for (const [key, value] of Object.entries(item.metadata)) { if (value === null || value === undefined) { continue; } const keyMap = this.metadataIndex.get(key); if (keyMap) { const valueSet = keyMap.get(value); if (valueSet) { valueSet.delete(item.id); if (valueSet.size === 0) { keyMap.delete(value); if (keyMap.size === 0) { this.metadataIndex.delete(key); } } } } } } } /** * Search using the index */ async indexSearch(query, metadata, limit = 10, offset = 0, minRelevance = 0) { // Find matching item IDs based on query and metadata const matchingIds = new Set(); // If we have metadata criteria, find matching items if (metadata) { let isFirstMetadata = true; for (const [key, value] of Object.entries(metadata)) { const keyMap = this.metadataIndex.get(key); if (!keyMap) { return { items: [], total: 0 }; // No matches for this key } const valueSet = keyMap.get(value); if (!valueSet) { return { items: [], total: 0 }; // No matches for this value } if (isFirstMetadata) { // For the first metadata criterion, add all matching IDs for (const id of valueSet) { matchingIds.add(id); } isFirstMetadata = false; } else { // For subsequent criteria, keep only IDs that match all criteria for (const id of matchingIds) { if (!valueSet.has(id)) { matchingIds.delete(id); } } } // Short-circuit if we've eliminated all items if (matchingIds.size === 0) { return { items: [], total: 0 }; } } } // If we have a query, find matching items by content if (query && query.length > 0) { const queryWords = query.toLowerCase().split(/\s+/) .filter(word => word.length > 2); // Only search for words longer than 2 characters if (queryWords.length > 0) { // Find IDs matching any query word const queryMatchingIds = new Set(); const wordMatches = new Map(); // Track how many words match each ID for (const word of queryWords) { const wordSet = this.contentIndex.get(word); if (wordSet) { for (const id of wordSet) { queryMatchingIds.add(id); wordMatches.set(id, (wordMatches.get(id) || 0) + 1); } } } if (metadata) { // If we also have metadata criteria, find the intersection for (const id of matchingIds) { if (!queryMatchingIds.has(id)) { matchingIds.delete(id); } } } else { // If we only have a query, use the query matching IDs for (const id of queryMatchingIds) { matchingIds.add(id); } } // If we have no matches, return empty result if (matchingIds.size === 0) { return { items: [], total: 0 }; } } } // If we have no query or metadata, match all items if (!query && !metadata) { const keys = await this.storage.keys(); for (const key of keys) { if (key.startsWith(`${this.namespace}:item:`)) { const id = key.substring(`${this.namespace}:item:`.length); matchingIds.add(id); } } } // Load the matching items const matchedItems = []; for (const id of matchingIds) { const item = await this.get(id); if (item) { matchedItems.push(item); } } // Calculate relevance scores and filter by minimum relevance const scoredItems = matchedItems.map(item => { // Calculate a simple relevance score based on recency const ageMs = Date.now() - item.createdAt; const recencyScore = Math.max(0, 1 - (ageMs / this.archiveAgeMs)); // If we have a query, also consider word match count let queryScore = 0; if (query) { const queryWords = query.toLowerCase().split(/\s+/) .filter(word => word.length > 2); // Count how many query words appear in the content let matchCount = 0; for (const word of queryWords) { if (item.content.toLowerCase().includes(word)) { matchCount++; } } // Calculate query score based on proportion of words matched queryScore = queryWords.length > 0 ? matchCount / queryWords.length : 0; } // Combine scores - we weight query score higher than recency const relevanceScore = query ? (0.7 * queryScore + 0.3 * recencyScore) : recencyScore; return { ...item, relevanceScore }; }) .filter(item => item.relevanceScore >= minRelevance) .sort((a, b) => b.relevanceScore - a.relevanceScore); // Apply pagination const paginatedItems = scoredItems.slice(offset, offset + limit); return { items: paginatedItems, total: scoredItems.length }; } /** * Search using semantic vectors * This is a placeholder implementation - in a real implementation, this would * use a vector database for semantic search */ async semanticSearch(query, metadata, limit = 10, offset = 0, minRelevance = 0) { // This would use the vector database to perform a semantic search // For now, fall back to index search return this.indexSearch(query, metadata, limit, offset, minRelevance); } /** * Load the index from storage */ async loadIndex() { // In a real implementation, this would load a persisted index // For now, rebuild the index from all items const keys = await this.storage.keys(); const itemKeys = keys.filter(key => key.startsWith(`${this.namespace}:item:`)); for (const key of itemKeys) { const item = await this.storage.load(key); if (item) { this.indexItem(item); // Add to cache if enabled (up to cache size) if (this.useCache && this.cache.size < this.cacheSize) { this.addToCache(item); } } } } /** * Save the index to storage */ async saveIndex() { // In a real implementation, this would persist the index // For now, do nothing as we rebuild the index on load } }