UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

244 lines (243 loc) 8.06 kB
/** * Custom Tool Factory * Provides a flexible framework for creating and extending tools * with proper typing, validation, and performance optimizations */ import { createStructuredTool } from '../StructuredTool.js'; /** * Create a LRU cache implementation */ class LRUCache { cache = new Map(); maxSize; ttl; constructor(maxSize = 100, ttlMs = 3600000) { this.maxSize = maxSize; this.ttl = ttlMs; } get(key) { const entry = this.cache.get(key); if (!entry) return null; // Check for expiration if (Date.now() - entry.timestamp > this.ttl) { this.cache.delete(key); return null; } // Move to the front of the LRU (by deleting and reinserting) this.cache.delete(key); this.cache.set(key, { value: entry.value, timestamp: Date.now() }); return entry.value; } set(key, value) { // Implement LRU eviction if cache is full if (this.cache.size >= this.maxSize) { // Find oldest entry let oldestKey = null; let oldestTime = Date.now(); for (const [k, v] of this.cache.entries()) { if (v.timestamp < oldestTime) { oldestTime = v.timestamp; oldestKey = k; } } // Delete oldest entry if (oldestKey) { this.cache.delete(oldestKey); } } this.cache.set(key, { value, timestamp: Date.now(), }); } clear() { this.cache.clear(); } clearExpired() { const now = Date.now(); for (const [key, value] of this.cache.entries()) { if (now - value.timestamp > this.ttl) { this.cache.delete(key); } } } } /** * Performance metrics tracker */ class PerformanceTracker { metrics = {}; startTime = 0; startMemory = 0; options; constructor(options) { this.options = options; } start() { if (!this.options?.enabled) return; this.startTime = performance.now(); // Track memory if available and requested if (this.options.metrics?.includes('memory') && global.process?.memoryUsage) { this.startMemory = global.process.memoryUsage().heapUsed; } } end(success) { if (!this.options?.enabled) return {}; // Track latency if (this.options.metrics?.includes('latency')) { this.metrics.latencyMs = performance.now() - this.startTime; } // Track memory if available and requested if (this.options.metrics?.includes('memory') && global.process?.memoryUsage) { const endMemory = global.process.memoryUsage().heapUsed; this.metrics.memoryUsageBytes = endMemory - this.startMemory; } // Track success rate if (this.options.metrics?.includes('success_rate')) { this.metrics.success = success; } // Process metrics if callback provided if (this.options.processMetrics) { this.options.processMetrics(this.metrics); } return this.metrics; } getMetrics() { return this.metrics; } } /** * Default cache key generator */ function defaultKeyGenerator(input) { try { return JSON.stringify(input); } catch (error) { // For non-serializable inputs, use a stable hash const inputStr = String(input); let hash = 0; for (let i = 0; i < inputStr.length; i++) { const char = inputStr.charCodeAt(i); hash = (hash << 5) - hash + char; hash |= 0; // Convert to 32-bit integer } return String(hash); } } /** * Implementation of a retry mechanism with exponential backoff */ async function withRetry(fn, options) { const maxRetries = options.maxRetries ?? 3; const initialDelay = options.initialDelayMs ?? 1000; const maxDelay = options.maxDelayMs ?? 30000; const isRetryable = options.isRetryable ?? (() => true); let attempt = 0; let lastError; while (attempt <= maxRetries) { try { return await fn(); } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if we should retry if (!isRetryable(lastError) || attempt >= maxRetries) { throw lastError; } // Calculate delay with exponential backoff and jitter const delay = Math.min(maxDelay, initialDelay * Math.pow(2, attempt) * (0.5 + Math.random() * 0.5)); await new Promise(resolve => setTimeout(resolve, delay)); attempt++; } } throw lastError; } /** * Creates a custom tool with the provided options */ export function createCustomTool(options) { // Optional cache setup let cache = null; let keyGenerator = defaultKeyGenerator; if (options.cache?.enabled) { cache = new LRUCache(options.cache.maxSize, options.cache.ttlMs); if (options.cache.keyGenerator) { // Use type assertion to ensure type compatibility keyGenerator = options.cache.keyGenerator; } } // Create the tool with the structured tool factory return createStructuredTool({ name: options.name, description: options.description, inputSchema: options.inputSchema, func: async (input) => { // Create performance tracker if needed const tracker = options.tracking?.enabled ? new PerformanceTracker(options.tracking) : null; // Start tracking if enabled tracker?.start(); try { // Check cache first if enabled if (cache) { const cacheKey = keyGenerator(input); const cachedResult = cache.get(cacheKey); if (cachedResult) { // Add metrics for cached results if tracking enabled if (tracker) { tracker.end(true); return { ...cachedResult, _metrics: { ...tracker.getMetrics(), fromCache: true } }; } return cachedResult; } } // Execute with retry if configured let result; if (options.retry) { result = await withRetry(() => options.execute(input), options.retry); } else { result = await options.execute(input); } // Store in cache if enabled if (cache) { const cacheKey = keyGenerator(input); cache.set(cacheKey, result); } // End tracking if enabled with success if (tracker) { const metrics = tracker.end(true); return { ...result, _metrics: metrics }; } return result; } catch (error) { // End tracking with failure tracker?.end(false); // Use custom error handler if provided if (options.handleError) { return options.handleError(error instanceof Error ? error : new Error(String(error)), input); } // Re-throw the error if no handler throw error; } } }); }