/** * 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; estimatedLatency: number; costPerQuery: number; query?: (operation: string, params: any) => Promise; // 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 = 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 { // 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 { // 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 { // 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 { 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 { 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 { // 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 { // 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 = { '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 { // 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'; } }