crewai-ts
Version:
TypeScript port of crewAI for agent-based workflows
244 lines (243 loc) • 8.06 kB
JavaScript
/**
* 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;
}
}
});
}