UNPKG

crewai-ts

Version:

TypeScript port of crewAI for agent-based workflows

435 lines (434 loc) 16.1 kB
/** * Types of routing conditions that can be evaluated */ export var ConditionType; (function (ConditionType) { ConditionType["EQUALS"] = "equals"; ConditionType["NOT_EQUALS"] = "not_equals"; ConditionType["GREATER_THAN"] = "greater_than"; ConditionType["LESS_THAN"] = "less_than"; ConditionType["CONTAINS"] = "contains"; ConditionType["NOT_CONTAINS"] = "not_contains"; ConditionType["REGEX"] = "regex"; ConditionType["FUNCTION"] = "function"; })(ConditionType || (ConditionType = {})); /** * FlowRouter provides efficient conditional routing for flows */ export class FlowRouter { defaultOptions; // Routes storage with fast lookup routes = new Map(); routePriorities = new Map(); // Optimization features conditionCache = new Map(); compiledFunctions = new Map(); fastPathLookup = new Map(); // Route evaluation counters evaluationCount = 0; cacheHitCount = 0; /** * Create a new FlowRouter with optimized routing capabilities * @param options Default options for route evaluation */ constructor(defaultOptions = {}) { this.defaultOptions = defaultOptions; // Set default options this.defaultOptions = { enableConditionCache: true, cacheTTLMs: 10000, // 10 seconds default traceEvaluation: false, shortCircuitEvaluation: true, maxTraceResults: 100, ...defaultOptions }; } /** * Register a new route * @param route Route definition to register */ registerRoute(route) { if (this.routes.has(route.id)) { throw new Error(`Route with id ${route.id} already exists`); } // Store route this.routes.set(route.id, route); // Set priority (default to 0 if not specified) this.routePriorities.set(route.id, route.priority ?? 0); // Build fast path lookup for direct property access this.buildFastPathLookup(route.id, route.condition); // Pre-compile function conditions if possible if (this.isFunctionCondition(route.condition)) { this.compiledFunctions.set(route.id, route.condition.predicate); } } /** * Update an existing route * @param routeId ID of the route to update * @param updates Updates to apply to the route */ updateRoute(routeId, updates) { const existingRoute = this.routes.get(routeId); if (!existingRoute) { throw new Error(`Route with id ${routeId} does not exist`); } // Apply updates const updatedRoute = { ...existingRoute, ...updates }; // Update route this.routes.set(routeId, updatedRoute); // Update priority if changed if (updates.priority !== undefined) { this.routePriorities.set(routeId, updates.priority); } // Rebuild fast path lookup if condition changed if (updates.condition) { this.buildFastPathLookup(routeId, updatedRoute.condition); // Update compiled function if (this.isFunctionCondition(updatedRoute.condition)) { this.compiledFunctions.set(routeId, updatedRoute.condition.predicate); } else { // Remove compiled function if condition is no longer a function this.compiledFunctions.delete(routeId); } } // Clear condition cache for this route this.clearConditionCache(); } /** * Remove a route * @param routeId ID of the route to remove */ removeRoute(routeId) { if (!this.routes.has(routeId)) { return false; } // Remove route this.routes.delete(routeId); this.routePriorities.delete(routeId); // Clean up lookup structures this.compiledFunctions.delete(routeId); // Clear fast path lookup for (const [path, routeIds] of this.fastPathLookup.entries()) { routeIds.delete(routeId); if (routeIds.size === 0) { this.fastPathLookup.delete(path); } } return true; } /** * Find matching routes for a given state * @param state State to evaluate routes against * @param options Route evaluation options */ findMatchingRoutes(state, options = {}) { // Merge with default options const evalOptions = { ...this.defaultOptions, ...options }; // Initialize result const result = { matched: [], matchedIds: [], trace: evalOptions.traceEvaluation ? [] : undefined }; // Get optimized route evaluation order const routesToEvaluate = this.getOptimizedRouteEvaluationOrder(state); // Evaluate each route for (const routeId of routesToEvaluate) { const route = this.routes.get(routeId); if (!route) continue; const startTime = evalOptions.traceEvaluation ? performance.now() : 0; // Evaluate route condition const matches = this.evaluateCondition(route.condition, state, routeId, evalOptions); // Add to trace if enabled if (evalOptions.traceEvaluation && result.trace) { const endTime = performance.now(); result.trace.push({ route: route.id, condition: route.condition, result: matches, timestamp: Date.now(), evaluationTimeMs: endTime - startTime }); // Limit trace size if (result.trace.length > evalOptions.maxTraceResults) { result.trace = result.trace.slice(-evalOptions.maxTraceResults); } } // Add to matches if condition is true if (matches) { result.matched.push(route); result.matchedIds.push(route.id); // Short-circuit if only interested in first match if (evalOptions.shortCircuitEvaluation) { break; } } } return result; } /** * Get a specific route by ID * @param routeId ID of the route to get */ getRoute(routeId) { return this.routes.get(routeId); } /** * Get all registered routes */ getAllRoutes() { return Array.from(this.routes.values()); } /** * Clear the condition evaluation cache */ clearConditionCache() { this.conditionCache.clear(); } /** * Get evaluation performance metrics */ getPerformanceMetrics() { const cacheHitRate = this.evaluationCount > 0 ? this.cacheHitCount / this.evaluationCount : 0; return { evaluations: this.evaluationCount, cacheHitRate }; } /** * Get an optimized evaluation order for routes * This prioritizes routes based on: * 1. Explicit priority (higher first) * 2. Matching properties in state (routes that check properties present in state) * 3. Complexity (simpler conditions evaluated first) */ getOptimizedRouteEvaluationOrder(state) { const routeIds = Array.from(this.routes.keys()); // Find properties that exist in the state const stateProps = new Set(); this.extractPropertyPaths(state, '', stateProps); return routeIds.sort((a, b) => { // 1. Sort by priority (higher first) const priorityA = this.routePriorities.get(a) ?? 0; const priorityB = this.routePriorities.get(b) ?? 0; if (priorityA !== priorityB) { return priorityB - priorityA; } // 2. Sort by property match (routes that check properties in state first) const routeA = this.routes.get(a); const routeB = this.routes.get(b); const aRelevance = this.calculateStateRelevance(routeA.condition, stateProps); const bRelevance = this.calculateStateRelevance(routeB.condition, stateProps); if (aRelevance !== bRelevance) { return bRelevance - aRelevance; } // 3. Sort by condition complexity (simpler first) return this.getConditionComplexity(routeA.condition) - this.getConditionComplexity(routeB.condition); }); } /** * Evaluate a route condition against a state */ evaluateCondition(condition, state, routeId, options) { this.evaluationCount++; // Check cache if enabled if (options.enableConditionCache) { const cacheKey = `${routeId}:${JSON.stringify(state)}`; const cached = this.conditionCache.get(cacheKey); if (cached) { // Check if cache entry is still valid const now = Date.now(); if (now - cached.timestamp < options.cacheTTLMs) { this.cacheHitCount++; return cached.result; } // Remove expired cache entry this.conditionCache.delete(cacheKey); } // Evaluate and cache result const result = this.doEvaluateCondition(condition, state); this.conditionCache.set(cacheKey, { result, timestamp: Date.now() }); return result; } // Evaluate without caching return this.doEvaluateCondition(condition, state); } /** * Actual condition evaluation logic */ doEvaluateCondition(condition, state) { // Handle logical groups if ('operator' in condition) { if (condition.operator === 'AND') { // Short-circuit AND (all must be true) for (const subCondition of condition.conditions) { if (!this.doEvaluateCondition(subCondition, state)) { return false; } } return true; } else { // OR // Short-circuit OR (any can be true) for (const subCondition of condition.conditions) { if (this.doEvaluateCondition(subCondition, state)) { return true; } } return false; } } // Handle basic conditions const value = this.getValueFromPath(state, condition.path); switch (condition.type) { case ConditionType.EQUALS: return value === condition.value; case ConditionType.NOT_EQUALS: return value !== condition.value; case ConditionType.GREATER_THAN: return value > condition.value; case ConditionType.LESS_THAN: return value < condition.value; case ConditionType.CONTAINS: if (Array.isArray(value)) { return value.includes(condition.value); } else if (typeof value === 'string') { return value.includes(String(condition.value)); } return false; case ConditionType.NOT_CONTAINS: if (Array.isArray(value)) { return !value.includes(condition.value); } else if (typeof value === 'string') { return !value.includes(String(condition.value)); } return true; case ConditionType.REGEX: if (typeof value === 'string') { const regex = new RegExp(condition.pattern, condition.flags); return regex.test(value); } return false; case ConditionType.FUNCTION: return condition.predicate(value, state); default: return false; } } /** * Get a value from a dotted path in an object */ getValueFromPath(obj, path) { if (!path) return obj; const parts = path.split('.'); let current = obj; for (const part of parts) { if (current === null || current === undefined) { return undefined; } current = current[part]; } return current; } /** * Calculate how relevant a condition is to the current state * Returns a number representing relevance (higher is more relevant) */ calculateStateRelevance(condition, stateProps) { // For logical groups, sum relevance of children if ('operator' in condition) { return condition.conditions.reduce((sum, subCondition) => sum + this.calculateStateRelevance(subCondition, stateProps), 0); } // Basic condition - check if path exists in state return stateProps.has(condition.path) ? 1 : 0; } /** * Extract all property paths from an object * Used to find which properties are available in the state */ extractPropertyPaths(obj, prefix, result) { if (!obj || typeof obj !== 'object') return; for (const key of Object.keys(obj)) { const path = prefix ? `${prefix}.${key}` : key; result.add(path); // Recursively extract nested properties (up to a reasonable depth) if (obj[key] && typeof obj[key] === 'object' && prefix.split('.').length < 3) { this.extractPropertyPaths(obj[key], path, result); } } } /** * Calculate the complexity of a condition * Used for sorting conditions by complexity (simpler first) */ getConditionComplexity(condition) { // Logical groups are more complex if ('operator' in condition) { return 10 + condition.conditions.reduce((sum, subCondition) => sum + this.getConditionComplexity(subCondition), 0); } // Function conditions are most complex if (condition.type === ConditionType.FUNCTION) { return 5; } // Regex conditions are more complex than basic conditions if (condition.type === ConditionType.REGEX) { return 3; } // Basic conditions are simplest return 1; } /** * Build fast path lookup for direct property access * This optimizes route finding by indexing routes by the properties they check */ buildFastPathLookup(routeId, condition) { const paths = new Set(); this.extractConditionPaths(condition, paths); // Remove this route from existing path lookups for (const [path, routeIds] of this.fastPathLookup.entries()) { routeIds.delete(routeId); if (routeIds.size === 0) { this.fastPathLookup.delete(path); } } // Add to new path lookups for (const path of paths) { if (!this.fastPathLookup.has(path)) { this.fastPathLookup.set(path, new Set()); } this.fastPathLookup.get(path).add(routeId); } } /** * Extract all condition paths from a condition */ extractConditionPaths(condition, result) { if ('operator' in condition) { // Logical group - extract from all subconditions for (const subCondition of condition.conditions) { this.extractConditionPaths(subCondition, result); } } else { // Basic condition - add path result.add(condition.path); } } /** * Type guard for function conditions */ isFunctionCondition(condition) { return !('operator' in condition) && condition.type === ConditionType.FUNCTION; } }