Spaces:
Paused
Paused
| /** | |
| * Decision Engine - AI-Powered Source Selection | |
| * | |
| * Makes intelligent decisions about which data source to use | |
| * based on learned patterns, current health, context, AND semantic relevance | |
| */ | |
| import { CognitiveMemory } from '../memory/CognitiveMemory.js'; | |
| import { getEmbeddingService } from '../../services/embeddings/EmbeddingService.js'; | |
| import { logger } from '../../utils/logger.js'; | |
| export interface QueryIntent { | |
| type: string; | |
| domain: string; | |
| operation: string; | |
| params: any; | |
| priority?: 'low' | 'normal' | 'high'; | |
| freshness?: 'stale' | 'normal' | 'realtime'; | |
| } | |
| export interface DataSource { | |
| name: string; | |
| type: string; | |
| capabilities: string[]; | |
| description?: string; // Added description for semantic matching | |
| isHealthy: () => Promise<boolean>; | |
| estimatedLatency: number; | |
| costPerQuery: number; | |
| query?: (operation: string, params: any) => Promise<any>; // Optional query method | |
| } | |
| export interface SourceScore { | |
| source: DataSource; | |
| score: number; | |
| breakdown: { | |
| performance: number; | |
| reliability: number; | |
| cost: number; | |
| freshness: number; | |
| history: number; | |
| semantic: number; | |
| }; | |
| reasoning: string; | |
| } | |
| export interface DecisionResult { | |
| selectedSource: DataSource; | |
| score: number; | |
| confidence: number; | |
| reasoning: string; | |
| alternatives: SourceScore[]; | |
| } | |
| export class DecisionEngine { | |
| private memory: CognitiveMemory; | |
| private embeddings = getEmbeddingService(); | |
| private sourceEmbeddings: Map<string, number[]> = new Map(); | |
| // Scoring weights (can be tuned based on priority) | |
| private weights = { | |
| performance: 0.20, | |
| reliability: 0.20, | |
| cost: 0.15, | |
| freshness: 0.05, | |
| history: 0.10, | |
| semantic: 0.30 // High weight for semantic relevance | |
| }; | |
| constructor(memory: CognitiveMemory) { | |
| this.memory = memory; | |
| this.initializeEmbeddings().catch(err => { | |
| logger.warn('Failed to initialize source embeddings:', err); | |
| }); | |
| } | |
| private async initializeEmbeddings() { | |
| // Initialize embedding service (and GPU bridge if applicable) | |
| await this.embeddings.initialize(); | |
| } | |
| /** | |
| * Analyze query intent to understand requirements | |
| */ | |
| async analyzeIntent(query: any): Promise<QueryIntent> { | |
| // Extract intent from query structure | |
| const intent: QueryIntent = { | |
| type: query.type || 'unknown', | |
| domain: query.domain || this.inferDomain(query), | |
| operation: query.operation || this.inferOperation(query), | |
| params: query.params || {}, | |
| priority: query.priority || 'normal', | |
| freshness: query.freshness || 'normal' | |
| }; | |
| return intent; | |
| } | |
| private inferOperation(query: any): string { | |
| if (query?.type && typeof query.type === 'string' && query.type.includes('.')) { | |
| const parts = query.type.split('.'); | |
| if (parts.length > 1) return parts[1]; | |
| } | |
| return 'read'; | |
| } | |
| /** | |
| * Score all available sources for a query | |
| */ | |
| async scoreAllSources( | |
| sources: DataSource[], | |
| intent: QueryIntent | |
| ): Promise<SourceScore[]> { | |
| // Ensure embeddings service is ready | |
| await this.embeddings.initialize(); | |
| const scores = await Promise.all( | |
| sources.map(source => this.scoreSource(source, intent)) | |
| ); | |
| // Sort by score descending | |
| return scores.sort((a, b) => b.score - a.score); | |
| } | |
| /** | |
| * Score a single source for this query | |
| */ | |
| async scoreSource( | |
| source: DataSource, | |
| intent: QueryIntent | |
| ): Promise<SourceScore> { | |
| // Adjust weights based on priority | |
| const weights = this.getWeights(intent); | |
| // Calculate individual scores | |
| const performance = await this.scorePerformance(source, intent); | |
| const reliability = await this.scoreReliability(source, intent); | |
| const cost = this.scoreCost(source, intent); | |
| const freshness = this.scoreFreshness(source, intent); | |
| const history = await this.scoreHistory(source, intent); | |
| const semantic = await this.scoreSemanticRelevance(source, intent); | |
| // Weighted total | |
| const totalScore = | |
| performance * weights.performance + | |
| reliability * weights.reliability + | |
| cost * weights.cost + | |
| freshness * weights.freshness + | |
| history * weights.history + | |
| semantic * weights.semantic; | |
| // Generate reasoning | |
| const reasoning = this.generateReasoning({ | |
| performance, | |
| reliability, | |
| cost, | |
| freshness, | |
| history, | |
| semantic | |
| }, weights); | |
| return { | |
| source, | |
| score: totalScore, | |
| breakdown: { | |
| performance, | |
| reliability, | |
| cost, | |
| freshness, | |
| history, | |
| semantic | |
| }, | |
| reasoning | |
| }; | |
| } | |
| /** | |
| * Make final decision from scored sources | |
| */ | |
| async decide( | |
| sources: DataSource[], | |
| intent: QueryIntent | |
| ): Promise<DecisionResult> { | |
| const scored = await this.scoreAllSources(sources, intent); | |
| if (scored.length === 0) { | |
| throw new Error('No available sources for this query'); | |
| } | |
| const best = scored[0]; | |
| // Confidence is based on score gap between #1 and #2 | |
| const confidence = scored.length > 1 | |
| ? Math.min(1.0, (best.score - scored[1].score) / 0.3 + 0.5) | |
| : 1.0; | |
| return { | |
| selectedSource: best.source, | |
| score: best.score, | |
| confidence, | |
| reasoning: best.reasoning, | |
| alternatives: scored.slice(1, 4) // Top 3 alternatives | |
| }; | |
| } | |
| /** | |
| * Score semantic relevance using embeddings | |
| */ | |
| private async scoreSemanticRelevance( | |
| source: DataSource, | |
| intent: QueryIntent | |
| ): Promise<number> { | |
| try { | |
| // 1. Get or generate embedding for source description/capabilities | |
| let sourceVector = this.sourceEmbeddings.get(source.name); | |
| if (!sourceVector) { | |
| const description = `${source.name} ${source.type} ${source.capabilities.join(' ')} ${source.description || ''}`; | |
| sourceVector = await this.embeddings.generateEmbedding(description); | |
| this.sourceEmbeddings.set(source.name, sourceVector); | |
| } | |
| // 2. Generate embedding for query intent | |
| const queryText = `${intent.type} ${intent.domain} ${intent.operation} ${JSON.stringify(intent.params)}`; | |
| const queryVector = await this.embeddings.generateEmbedding(queryText); | |
| // 3. Calculate cosine similarity | |
| return this.cosineSimilarity(sourceVector, queryVector); | |
| } catch (error) { | |
| // Fallback to keyword matching if embedding fails | |
| logger.warn(`Semantic scoring failed for ${source.name}:`, error); | |
| // Simple keyword overlap fallback | |
| const queryStr = JSON.stringify(intent).toLowerCase(); | |
| const capsStr = source.capabilities.join(' ').toLowerCase(); | |
| if (capsStr.includes(intent.type.toLowerCase())) return 0.8; | |
| if (queryStr.includes(source.name.toLowerCase())) return 0.6; | |
| return 0.3; // Default low relevance | |
| } | |
| } | |
| private cosineSimilarity(vecA: number[], vecB: number[]): number { | |
| if (vecA.length !== vecB.length) return 0; | |
| let dot = 0; | |
| let magA = 0; | |
| let magB = 0; | |
| for (let i = 0; i < vecA.length; i++) { | |
| dot += vecA[i] * vecB[i]; | |
| magA += vecA[i] * vecA[i]; | |
| magB += vecB[i] * vecB[i]; | |
| } | |
| return magA === 0 || magB === 0 ? 0 : dot / (Math.sqrt(magA) * Math.sqrt(magB)); | |
| } | |
| /** | |
| * Score performance (latency, throughput) | |
| */ | |
| private async scorePerformance( | |
| source: DataSource, | |
| intent: QueryIntent | |
| ): Promise<number> { | |
| // Get average latency from memory | |
| const avgLatency = await this.memory.getAverageLatency(source.name); | |
| // Normalize: 0-50ms = 1.0, 500ms+ = 0.0 | |
| const latencyScore = Math.max(0, Math.min(1, 1 - (avgLatency / 500))); | |
| // For high priority queries, penalize slow sources more | |
| if (intent.priority === 'high' && avgLatency > 200) { | |
| return latencyScore * 0.5; | |
| } | |
| return latencyScore; | |
| } | |
| /** | |
| * Score reliability (uptime, success rate) | |
| */ | |
| private async scoreReliability( | |
| source: DataSource, | |
| intent: QueryIntent | |
| ): Promise<number> { | |
| // Current health check | |
| const isHealthy = await source.isHealthy(); | |
| if (!isHealthy) { | |
| return 0.0; // Unhealthy source gets zero score | |
| } | |
| // Historical success rate | |
| const successRate = await this.memory.getSuccessRate( | |
| source.name, | |
| intent.type | |
| ); | |
| // Get failure intelligence | |
| const intelligence = await this.memory.getSourceIntelligence(source.name); | |
| // Penalize if there were recent failures | |
| const recentFailurePenalty = Math.min(0.3, intelligence.recentFailures * 0.05); | |
| return Math.max(0, successRate - recentFailurePenalty); | |
| } | |
| /** | |
| * Score cost (API costs, compute) | |
| */ | |
| private scoreCost(source: DataSource, intent: QueryIntent): number { | |
| const cost = source.costPerQuery || 0; | |
| // Normalize: $0 = 1.0, $0.10+ = 0.0 | |
| const costScore = Math.max(0, Math.min(1, 1 - (cost / 0.1))); | |
| // For low priority queries, strongly prefer free sources | |
| if (intent.priority === 'low' && cost > 0) { | |
| return costScore * 0.5; | |
| } | |
| return costScore; | |
| } | |
| /** | |
| * Score data freshness | |
| */ | |
| private scoreFreshness(source: DataSource, intent: QueryIntent): number { | |
| // Database sources are typically fresher than cached/file sources | |
| const freshnessMap: Record<string, number> = { | |
| 'database': 1.0, | |
| 'api': 0.9, | |
| 'cache': 0.5, | |
| 'file': 0.3 | |
| }; | |
| const baseScore = freshnessMap[source.type] || 0.5; | |
| // Adjust based on required freshness | |
| if (intent.freshness === 'realtime') { | |
| return source.type === 'database' || source.type === 'api' ? 1.0 : 0.2; | |
| } else if (intent.freshness === 'stale') { | |
| return 1.0; // Don't care about freshness | |
| } | |
| return baseScore; | |
| } | |
| /** | |
| * Score based on historical patterns | |
| */ | |
| private async scoreHistory( | |
| source: DataSource, | |
| intent: QueryIntent | |
| ): Promise<number> { | |
| // Check if this source has been successful for similar queries | |
| const historyScore = await this.memory.getSimilarQuerySuccess( | |
| intent.type, | |
| intent.params | |
| ); | |
| return historyScore; | |
| } | |
| /** | |
| * Adjust weights based on query intent | |
| */ | |
| private getWeights(intent: QueryIntent) { | |
| const weights = { ...this.weights }; | |
| // High priority: favor performance and reliability | |
| if (intent.priority === 'high') { | |
| weights.performance = 0.30; | |
| weights.reliability = 0.30; | |
| weights.semantic = 0.20; // Reduce semantic weight slightly | |
| weights.cost = 0.10; | |
| weights.freshness = 0.05; | |
| weights.history = 0.05; | |
| } | |
| // Low priority: favor cost | |
| else if (intent.priority === 'low') { | |
| weights.performance = 0.10; | |
| weights.reliability = 0.20; | |
| weights.semantic = 0.20; | |
| weights.cost = 0.40; | |
| weights.freshness = 0.05; | |
| weights.history = 0.05; | |
| } | |
| // Realtime freshness: favor databases/APIs | |
| if (intent.freshness === 'realtime') { | |
| weights.freshness = 0.30; | |
| weights.performance = 0.20; | |
| weights.semantic = 0.20; | |
| weights.reliability = 0.20; | |
| weights.cost = 0.10; | |
| } | |
| return weights; | |
| } | |
| /** | |
| * Generate human-readable reasoning | |
| */ | |
| private generateReasoning( | |
| breakdown: SourceScore['breakdown'], | |
| weights: typeof this.weights | |
| ): string { | |
| const reasons: string[] = []; | |
| // Find strongest factor | |
| const factors = Object.entries(breakdown).sort((a, b) => b[1] - a[1]); | |
| const [topFactor, topScore] = factors[0]; | |
| if (topScore > 0.8) { | |
| reasons.push(`Excellent ${topFactor} (${(topScore * 100).toFixed(0)}%)`); | |
| } else if (topScore > 0.6) { | |
| reasons.push(`Good ${topFactor} (${(topScore * 100).toFixed(0)}%)`); | |
| } | |
| // Explicitly mention semantic match if high | |
| if (breakdown.semantic > 0.8) { | |
| reasons.push(`Strong conceptual match`); | |
| } | |
| // Note any weak factors | |
| for (const [factor, score] of factors) { | |
| if (score < 0.3 && weights[factor as keyof typeof weights] > 0.15) { | |
| reasons.push(`Low ${factor} (${(score * 100).toFixed(0)}%)`); | |
| } | |
| } | |
| return reasons.join(', ') || 'Balanced scores across all factors'; | |
| } | |
| /** | |
| * Infer domain from query structure | |
| */ | |
| private inferDomain(query: any): string { | |
| // Simple heuristics | |
| if (query?.type && typeof query.type === 'string' && query.type.includes('.')) { | |
| return query.type.split('.')[0]; | |
| } | |
| if (query.uri?.startsWith('agents://')) return 'agents'; | |
| if (query.uri?.startsWith('security://')) return 'security'; | |
| if (query.tool?.includes('search')) return 'search'; | |
| if (query.tool?.includes('agent')) return 'agents'; | |
| return 'general'; | |
| } | |
| } | |