crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
517 lines (516 loc) • 17.7 kB
JavaScript
/**
* Memory types for the Flow system - Performance optimized and compatible with Python implementation
*/
/**
* ContextualMemory class for optimized memory operations
* This implementation includes performance enhancements:
* - Multi-level caching strategy
* - Efficient metadata indexing
* - Memory-optimized data structures
*/
export class ContextualMemory {
options;
// Storage for memory items with optimized lookup
items = new Map();
// Metadata index for fast searching
metadataIndex = new Map();
// Query cache for optimized repeat access patterns
queryCache = new Map();
// Performance metrics tracking
metrics = {
cacheHits: 0,
cacheMisses: 0,
averageQueryTimeMs: 0,
totalQueries: 0
};
// Cache TTL in milliseconds
cacheTTL = 5 * 60 * 1000; // 5 minutes default
/**
* Creates a new ContextualMemory instance with the specified options
* @param options Configuration options for memory management
*/
constructor(options = {}) {
this.options = options;
// Apply defaults and overrides
this.cacheTTL = options.cacheTTL || this.cacheTTL;
// Set up automatic cache cleanup
if (options.autoCacheCleanup !== false) {
// Schedule cleanup every minute to prevent memory leaks
setInterval(() => this.cleanCache(), 60 * 1000);
}
}
/**
* Add a memory item to the store with efficient indexing
* @param id Unique identifier for the memory item
* @param content The content of the memory item
* @param metadata Optional metadata for improved retrievability
* @returns The ID of the added memory item
*/
async add(id, content, metadata = {}) {
// Create the memory item with timestamp
const item = {
id,
content,
metadata,
timestamp: Date.now()
};
// Store the item in main storage
this.items.set(id, item);
// Index metadata for fast retrieval
this.indexMetadata(id, metadata);
// Clear query cache to ensure fresh results
this.queryCache.clear();
return id;
}
/**
* Retrieve a memory item by ID with cache optimization
* @param id The ID of the memory item to retrieve
* @returns The memory item or null if not found
*/
async get(id) {
// Start performance tracking
const startTime = Date.now();
// Check cache first for optimization
const cacheKey = `get:${id}`;
const cached = this.getCachedResult(cacheKey);
if (cached !== null) {
// Track cache hit
this.metrics.cacheHits++;
return cached;
}
// Cache miss - retrieve from storage
this.metrics.cacheMisses++;
const item = this.items.get(id) || null;
// Update cache with result
this.cacheResult(cacheKey, item);
// Update performance metrics
this.updateQueryMetrics(startTime);
return item;
}
/**
* Delete a memory item with efficient cleanup
* @param id The ID of the memory item to delete
* @returns True if the item was deleted, false otherwise
*/
async delete(id) {
// Check if item exists
const item = this.items.get(id);
if (!item) {
return false;
}
// Remove from main storage
this.items.delete(id);
// Remove from metadata index
this.removeFromMetadataIndex(id, item.metadata);
// Clear related cache entries
this.queryCache.clear();
return true;
}
/**
* Search for memory items by metadata with performance optimizations
* @param metadata The metadata to search for
* @param limit Maximum number of results to return
* @returns Array of matching memory items
*/
async searchByMetadata(metadata, limit = 10) {
// Start performance tracking
const startTime = Date.now();
// Create cache key from metadata query
const cacheKey = `metadata:${JSON.stringify(metadata)}:${limit}`;
const cached = this.getCachedResult(cacheKey);
if (cached !== null) {
// Track cache hit
this.metrics.cacheHits++;
return cached;
}
// Cache miss - perform search
this.metrics.cacheMisses++;
// Find matching IDs from metadata index
const matchingIds = this.findItemsMatchingMetadata(metadata);
// Retrieve actual items and limit results
const results = Array.from(matchingIds)
.map(id => this.items.get(id))
.filter(item => item !== undefined);
const limitedResults = results.slice(0, limit);
// Cache results for future queries
this.cacheResult(cacheKey, limitedResults);
// Update performance metrics
this.updateQueryMetrics(startTime);
return limitedResults;
}
/**
* Get performance metrics for monitoring
* @returns Current memory performance metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Index metadata for efficient searching
* @param id Item ID to index
* @param metadata Metadata to index
*/
indexMetadata(id, metadata) {
// Process each metadata key-value pair
for (const [key, value] of Object.entries(metadata)) {
// Convert value to string for indexing
const valueStr = String(value);
// Create index key
const indexKey = `${key}:${valueStr}`;
// Get or create the set of IDs for this metadata value
if (!this.metadataIndex.has(indexKey)) {
this.metadataIndex.set(indexKey, new Set());
}
// Add ID to the index
this.metadataIndex.get(indexKey)?.add(id);
}
}
/**
* Remove item from metadata index
* @param id Item ID to remove
* @param metadata Metadata to de-index
*/
removeFromMetadataIndex(id, metadata) {
// Process each metadata key-value pair
for (const [key, value] of Object.entries(metadata)) {
// Convert value to string for indexing
const valueStr = String(value);
// Create index key
const indexKey = `${key}:${valueStr}`;
// Remove ID from the index
const ids = this.metadataIndex.get(indexKey);
if (ids) {
ids.delete(id);
// Clean up empty sets
if (ids.size === 0) {
this.metadataIndex.delete(indexKey);
}
}
}
}
/**
* Find items matching the provided metadata
* @param metadata Metadata to match
* @returns Set of matching item IDs
*/
findItemsMatchingMetadata(metadata) {
// Track potential matches by ID
const matchCounts = new Map();
const metadataEntries = Object.entries(metadata);
// Early return for empty metadata
if (metadataEntries.length === 0) {
return new Set(Array.from(this.items.keys()));
}
// Count matches for each metadata key-value pair
for (const [key, value] of metadataEntries) {
const valueStr = String(value);
const indexKey = `${key}:${valueStr}`;
const matchingIds = this.metadataIndex.get(indexKey);
if (matchingIds) {
for (const id of matchingIds) {
matchCounts.set(id, (matchCounts.get(id) || 0) + 1);
}
}
}
// Only return items that match all metadata criteria
const result = new Set();
for (const [id, count] of matchCounts.entries()) {
if (count === metadataEntries.length) {
result.add(id);
}
}
return result;
}
/**
* Get a cached result with TTL enforcement
* @param key Cache key
* @returns Cached result or null if not found
*/
getCachedResult(key) {
const cached = this.queryCache.get(key);
if (!cached)
return null;
// Check if cache entry has expired
const now = Date.now();
if (now - cached.timestamp > this.cacheTTL) {
this.queryCache.delete(key);
return null;
}
return cached.result;
}
/**
* Cache a query result
* @param key Cache key
* @param result Result to cache
*/
cacheResult(key, result) {
this.queryCache.set(key, {
result,
timestamp: Date.now()
});
}
/**
* Update query performance metrics
* @param startTime Query start timestamp
*/
updateQueryMetrics(startTime) {
const queryTime = Date.now() - startTime;
// Update average query time using weighted average
this.metrics.totalQueries++;
const totalQueries = this.metrics.totalQueries;
if (totalQueries === 1) {
// First query sets the initial average
this.metrics.averageQueryTimeMs = queryTime;
}
else {
// Weighted average formula
this.metrics.averageQueryTimeMs = (this.metrics.averageQueryTimeMs * (totalQueries - 1) + queryTime) / totalQueries;
}
}
/**
* Clean expired cache entries to prevent memory leaks
*/
cleanCache() {
const now = Date.now();
// Find and remove expired entries
for (const [key, entry] of this.queryCache.entries()) {
if (now - entry.timestamp > this.cacheTTL) {
this.queryCache.delete(key);
}
}
}
}
/**
* VectorStoreMemory class for optimized vector-based memory operations
* Efficiently manages embeddings with similarity search capabilities
*/
export class VectorStoreMemory {
options;
// Storage for vector items with optimized lookup
items = new Map();
// Cache for expensive operations
queryCache = new Map();
// Performance metrics tracking
metrics = {
cacheHits: 0,
cacheMisses: 0,
averageQueryTimeMs: 0,
totalQueries: 0
};
// Cache TTL in milliseconds
cacheTTL = 5 * 60 * 1000; // 5 minutes default
/**
* Creates a new VectorStoreMemory instance
* @param options Configuration options
*/
constructor(options = {}) {
this.options = options;
// Apply defaults and overrides
this.cacheTTL = options.cacheTTL || this.cacheTTL;
// Set up automatic cache cleanup
if (options.autoCacheCleanup !== false) {
// Schedule cleanup every minute
setInterval(() => this.cleanCache(), 60 * 1000);
}
}
/**
* Add a vector item to the store
* @param id Unique identifier
* @param vector The vector embedding
* @param content The associated content
* @param metadata Optional metadata
* @returns The ID of the added vector item
*/
async add(id, vector, content, metadata = {}) {
// Validate vector input
if (!Array.isArray(vector) || vector.length === 0) {
throw new Error('Vector must be a non-empty array of numbers');
}
// Create the vector item
const item = {
id,
vector,
content,
metadata,
timestamp: Date.now()
};
// Store the item
this.items.set(id, item);
// Clear query cache to ensure fresh results
this.queryCache.clear();
return id;
}
/**
* Retrieve a vector item by ID
* @param id The ID of the vector item to retrieve
* @returns The vector item or null if not found
*/
async get(id) {
// Start performance tracking
const startTime = Date.now();
// Check cache first
const cacheKey = `get:${id}`;
const cached = this.getCachedResult(cacheKey);
if (cached !== null) {
// Track cache hit
this.metrics.cacheHits++;
return cached;
}
// Cache miss - retrieve from storage
this.metrics.cacheMisses++;
const item = this.items.get(id) || null;
// Update cache with result
this.cacheResult(cacheKey, item);
// Update performance metrics
this.updateQueryMetrics(startTime);
return item;
}
/**
* Delete a vector item
* @param id The ID of the vector item to delete
* @returns True if the item was deleted, false otherwise
*/
async delete(id) {
// Check if item exists
if (!this.items.has(id)) {
return false;
}
// Remove from storage
this.items.delete(id);
// Clear related cache entries
this.queryCache.clear();
return true;
}
/**
* Perform optimized similarity search
* @param queryVector The query vector for similarity comparison
* @param limit Maximum number of results
* @param scoreThreshold Minimum similarity score threshold
* @returns Array of matching items with similarity scores
*/
async similaritySearch(queryVector, limit = 5, scoreThreshold = 0.7) {
// Start performance tracking
const startTime = Date.now();
// Validate query vector
if (!Array.isArray(queryVector) || queryVector.length === 0) {
throw new Error('Query vector must be a non-empty array of numbers');
}
// Create cache key from query parameters
const cacheKey = `similarity:${queryVector.toString()}:${limit}:${scoreThreshold}`;
const cached = this.getCachedResult(cacheKey);
if (cached !== null) {
// Track cache hit
this.metrics.cacheHits++;
return cached;
}
// Cache miss - perform search
this.metrics.cacheMisses++;
// Calculate similarities for all items
const results = Array.from(this.items.values())
.map(item => ({
item,
score: this.cosineSimilarity(queryVector, item.vector)
}))
.filter(result => result.score >= scoreThreshold)
.sort((a, b) => b.score - a.score)
.slice(0, limit);
// Cache results for future queries
this.cacheResult(cacheKey, results);
// Update performance metrics
this.updateQueryMetrics(startTime);
return results;
}
/**
* Get performance metrics
* @returns Current vector memory performance metrics
*/
getMetrics() {
return { ...this.metrics };
}
/**
* Calculate cosine similarity between two vectors
* Optimized implementation with early returns and minimal operations
* @param vectorA First vector
* @param vectorB Second vector
* @returns Cosine similarity score between 0 and 1
*/
cosineSimilarity(vectorA, vectorB) {
// Ensure both vectors are defined and have proper length
if (!vectorA || !vectorB || vectorA.length !== vectorB.length || vectorA.length === 0) {
return 0;
}
// Single-pass calculation for dot product and magnitudes
let dotProduct = 0;
let magnitudeA = 0;
let magnitudeB = 0;
for (let i = 0; i < vectorA.length; i++) {
// Null safety checks with defaults to 0 for undefined values
const valA = vectorA[i] ?? 0;
const valB = vectorB[i] ?? 0;
dotProduct += valA * valB;
magnitudeA += valA * valA;
magnitudeB += valB * valB;
}
// Avoid division by zero
if (magnitudeA === 0 || magnitudeB === 0) {
return 0;
}
// Calculate final similarity
return dotProduct / (Math.sqrt(magnitudeA) * Math.sqrt(magnitudeB));
}
/**
* Get a cached result with TTL enforcement
* @param key Cache key
* @returns Cached result or null if not found
*/
getCachedResult(key) {
const cached = this.queryCache.get(key);
if (!cached)
return null;
// Check if cache entry has expired
const now = Date.now();
if (now - cached.timestamp > this.cacheTTL) {
this.queryCache.delete(key);
return null;
}
return cached.result;
}
/**
* Cache a query result
* @param key Cache key
* @param result Result to cache
*/
cacheResult(key, result) {
this.queryCache.set(key, {
result,
timestamp: Date.now()
});
}
/**
* Update query performance metrics
* @param startTime Query start timestamp
*/
updateQueryMetrics(startTime) {
const queryTime = Date.now() - startTime;
// Update average query time
this.metrics.totalQueries++;
const totalQueries = this.metrics.totalQueries;
if (totalQueries === 1) {
this.metrics.averageQueryTimeMs = queryTime;
}
else {
this.metrics.averageQueryTimeMs = (this.metrics.averageQueryTimeMs * (totalQueries - 1) + queryTime) / totalQueries;
}
}
/**
* Clean expired cache entries
*/
cleanCache() {
const now = Date.now();
for (const [key, entry] of this.queryCache.entries()) {
if (now - entry.timestamp > this.cacheTTL) {
this.queryCache.delete(key);
}
}
}
}