/** * ╔═══════════════════════════════════════════════════════════════════════════╗ * ║ SMART TOOL ROUTER - INTENT-BASED SELECTION ║ * ║═══════════════════════════════════════════════════════════════════════════║ * ║ Automatically selects the best MCP tool based on user intent using ║ * ║ semantic matching and context analysis. ║ * ║ ║ * ║ Reduces cognitive load for AI agents by inferring the right tool ║ * ║ from natural language queries instead of requiring explicit tool names. ║ * ╚═══════════════════════════════════════════════════════════════════════════╝ */ import { hyperLog } from '../services/HyperLog.js'; // ═══════════════════════════════════════════════════════════════════════════ // TOOL DEFINITIONS WITH SEMANTIC KEYWORDS // ═══════════════════════════════════════════════════════════════════════════ interface ToolDefinition { name: string; description: string; keywords: string[]; intentPatterns: string[]; category: 'query' | 'mutation' | 'system' | 'analysis' | 'communication'; priority: number; // Higher = preferred when multiple match payloadTemplate?: Record | string; } // Lightweight domain synonym map to widen recall without heavy models const SYNONYMS: Record = { health: ['status', 'alive', 'uptime', 'heartbeat'], graph: ['neo4j', 'relationships', 'nodes', 'edges'], ingest: ['import', 'harvest', 'index', 'scan'], latency: ['delay', 'performance', 'response'], prototype: ['wireframe', 'mockup', 'design'], chat: ['message', 'notify', 'communicate'], archive: ['vidensarkiv', 'library', 'repository'], }; const TOOL_DEFINITIONS: ToolDefinition[] = [ // System & Health { name: 'get_system_health', description: 'Get WidgeTDC system health status', keywords: ['health', 'status', 'alive', 'running', 'up', 'down', 'working'], intentPatterns: ['is the system working', 'check health', 'system status', 'is it up'], category: 'system', priority: 10, payloadTemplate: {}, }, // Knowledge Graph Queries { name: 'query_knowledge_graph', description: 'Query the Neo4j knowledge graph', keywords: [ 'find', 'search', 'query', 'graph', 'neo4j', 'nodes', 'relationships', 'knowledge', 'entities', ], intentPatterns: [ 'find in graph', 'search knowledge', 'query entities', 'what do we know about', 'related to', ], category: 'query', priority: 8, payloadTemplate: { cypher: 'MATCH (n) RETURN n LIMIT 10', params: {} }, }, { name: 'get_graph_stats', description: 'Get graph statistics', keywords: ['stats', 'statistics', 'count', 'how many', 'size', 'graph size'], intentPatterns: ['how many nodes', 'graph statistics', 'database size', 'count entities'], category: 'query', priority: 6, payloadTemplate: {}, }, // Graph Mutations { name: 'graph_mutation', description: 'Create or modify graph nodes and relationships', keywords: ['create', 'add', 'insert', 'new', 'node', 'relationship', 'connect', 'link'], intentPatterns: ['create a node', 'add relationship', 'connect entities', 'insert into graph'], category: 'mutation', priority: 7, payloadTemplate: { cypher: 'CREATE (n:Entity {id: $id, name: $name})', params: { id: '...', name: '...' }, }, }, // File Access { name: 'dropzone_files', description: 'Access files in DropZone', keywords: ['file', 'files', 'dropzone', 'read', 'list', 'folder', 'document'], intentPatterns: ['read file', 'list files', 'whats in dropzone', 'check files'], category: 'query', priority: 5, payloadTemplate: { path: '/', action: 'list' }, }, { name: 'vidensarkiv_files', description: 'Access knowledge archive files', keywords: ['vidensarkiv', 'archive', 'knowledge', 'documents', 'library'], intentPatterns: ['check archive', 'knowledge library', 'archived documents'], category: 'query', priority: 5, payloadTemplate: { path: '/', action: 'list' }, }, // Ingestion { name: 'ingest_knowledge_graph', description: 'Ingest repository into knowledge graph', keywords: ['ingest', 'import', 'harvest', 'scan', 'index', 'repository', 'codebase'], intentPatterns: ['ingest repo', 'scan codebase', 'import to graph', 'harvest knowledge'], category: 'mutation', priority: 6, payloadTemplate: { repoUrl: 'https://github.com/org/repo', branch: 'main' }, }, // Communication { name: 'neural_chat', description: 'Agent-to-agent communication', keywords: ['chat', 'message', 'send', 'communicate', 'talk', 'notify', 'channel'], intentPatterns: ['send message', 'chat with', 'notify agent', 'communicate'], category: 'communication', priority: 7, payloadTemplate: { to: 'agent-name', message: '...' }, }, { name: 'agent_messages', description: 'Read or send agent messages', keywords: ['inbox', 'outbox', 'messages', 'mail', 'notifications'], intentPatterns: ['check messages', 'read inbox', 'send mail'], category: 'communication', priority: 6, payloadTemplate: { action: 'list', limit: 10 }, }, // Task Delegation { name: 'capability_broker', description: 'Delegate tasks to appropriate agents', keywords: ['delegate', 'task', 'capability', 'route', 'assign', 'who can'], intentPatterns: ['delegate task', 'who can handle', 'route request', 'find agent for'], category: 'system', priority: 8, payloadTemplate: { task: 'describe your task here', priority: 'medium' }, }, // Analysis { name: 'activate_associative_memory', description: 'Cognitive pattern matching across memories', keywords: ['remember', 'recall', 'memory', 'associative', 'pattern', 'cognitive'], intentPatterns: ['what do we remember', 'recall patterns', 'associative search'], category: 'analysis', priority: 7, payloadTemplate: { query: 'pattern to recall' }, }, { name: 'emit_sonar_pulse', description: 'Check service latencies and health', keywords: ['latency', 'ping', 'sonar', 'response time', 'performance'], intentPatterns: ['check latency', 'ping services', 'performance test'], category: 'system', priority: 5, payloadTemplate: { targets: ['neo4j', 'postgres', 'redis'] }, }, // Prototypes { name: 'prototype_manager', description: 'Generate or manage PRD prototypes', keywords: ['prototype', 'prd', 'generate', 'design', 'mockup', 'wireframe'], intentPatterns: ['generate prototype', 'create design', 'build from prd'], category: 'mutation', priority: 6, payloadTemplate: { prd: 'Paste PRD text here', output: 'wireframe' }, }, ]; // ═══════════════════════════════════════════════════════════════════════════ // SMART TOOL ROUTER CLASS // ═══════════════════════════════════════════════════════════════════════════ export interface ToolMatch { tool: string; confidence: number; reason: string; category: string; } export interface RouterResult { bestMatch: ToolMatch | null; alternatives: ToolMatch[]; query: string; processingTimeMs: number; suggestions?: ToolMatch[]; clarifyQuestion?: string; payloadTemplate?: Record | string | null; } class SmartToolRouter { private toolDefs: ToolDefinition[] = TOOL_DEFINITIONS; private feedback: Map = new Map(); /** * Route a natural language query to the best matching tool */ route(query: string): RouterResult { const startTime = Date.now(); const normalizedQuery = query.toLowerCase().trim(); // Expand query with synonyms to improve recall without an embedding model const baseTokens = normalizedQuery.split(/\s+/).filter(Boolean); const expandedTokens: string[] = []; for (const token of baseTokens) { const syns = SYNONYMS[token]; if (syns) expandedTokens.push(...syns.map(s => s.toLowerCase())); } const augmentedQuery = [normalizedQuery, ...expandedTokens].join(' '); if (!normalizedQuery) { const fallback = this.getFallbackMatch('Tom forespørgsel - bruger capability_broker'); return { bestMatch: fallback, alternatives: [], query, processingTimeMs: Date.now() - startTime, }; } const queryWords = new Set([...baseTokens, ...expandedTokens]); const matches: ToolMatch[] = []; for (const tool of this.toolDefs) { let score = 0; const reasons: string[] = []; // 1. Keyword matching (40% weight) const keywordMatches = tool.keywords.filter(kw => augmentedQuery.includes(kw.toLowerCase())); if (keywordMatches.length > 0) { score += (keywordMatches.length / tool.keywords.length) * 40; reasons.push(`Keywords: ${keywordMatches.join(', ')}`); } // 2. Intent pattern matching (40% weight) const patternMatches = tool.intentPatterns.filter(pattern => this.fuzzyMatch(augmentedQuery, pattern.toLowerCase()) ); if (patternMatches.length > 0) { score += (patternMatches.length / tool.intentPatterns.length) * 40; reasons.push(`Intent: ${patternMatches[0]}`); } // 3. Word overlap (15% weight) const toolWords = new Set( [...tool.keywords, ...tool.name.split('_')].map(w => w.toLowerCase()) ); const overlap = [...queryWords].filter(w => toolWords.has(w)).length; if (overlap > 0) { score += Math.min(overlap * 5, 15); } // 4. Priority boost (5% weight) score += (tool.priority / 10) * 5; // 5. Feedback boost (up to ~5 points) score += this.getFeedbackBoost(tool.name); if (score > 15) { // Minimum threshold matches.push({ tool: tool.name, confidence: Math.min(score / 100, 0.99), reason: reasons.join('; ') || 'Priority match', category: tool.category, }); } } // Sort by confidence matches.sort((a, b) => b.confidence - a.confidence); // Lightweight re-rank to boost semantic coverage and prefer richer reasons const reranked = this.rerankCandidates(augmentedQuery, matches); const bestCandidate = reranked.length > 0 ? reranked[0] : this.getFallbackMatch('Ingen match - fallback med forslag'); const alternatives = reranked.length > 1 ? reranked.slice(1, 4) : this.getSuggestionAlternatives(bestCandidate); const payloadTemplate = bestCandidate ? this.getPayloadTemplate(bestCandidate.tool) : null; const clarifyQuestion = bestCandidate && bestCandidate.confidence < 0.35 ? 'Uddyb hvad du vil gøre, fx data, system eller filnavn?' : undefined; const result: RouterResult = { bestMatch: bestCandidate, alternatives, query, processingTimeMs: Date.now() - startTime, suggestions: alternatives, payloadTemplate, clarifyQuestion, }; // Log routing decision if (result.bestMatch) { hyperLog.logEvent('TOOL_ROUTED', { query: query.substring(0, 100), tool: result.bestMatch.tool, confidence: result.bestMatch.confidence, alternatives: result.alternatives.length, }); } else { hyperLog.logEvent('TOOL_ROUTE_FAILED', { query: query.substring(0, 100), }); } return result; } /** * Fuzzy string matching for intent patterns */ private fuzzyMatch(query: string, pattern: string): boolean { const patternWords = pattern.split(/\s+/); const queryLower = query.toLowerCase(); // Check if all pattern words appear in query (in any order) let matchCount = 0; for (const word of patternWords) { if (queryLower.includes(word)) { matchCount++; } } // Consider match if 60%+ of pattern words found return matchCount / patternWords.length >= 0.6; } /** * Get tool suggestions for a category */ getToolsByCategory(category: ToolDefinition['category']): string[] { return this.toolDefs .filter(t => t.category === category) .sort((a, b) => b.priority - a.priority) .map(t => t.name); } /** * Auto-complete tool name from partial input */ autocomplete(partial: string): string[] { const normalizedPartial = partial.toLowerCase(); return this.toolDefs .filter( t => t.name.toLowerCase().includes(normalizedPartial) || t.keywords.some(k => k.toLowerCase().includes(normalizedPartial)) ) .map(t => t.name); } /** * Add custom tool definition (for dynamic tools) */ registerTool(tool: ToolDefinition): void { this.toolDefs.push(tool); hyperLog.logEvent('TOOL_REGISTERED', { name: tool.name, category: tool.category }); } /** * Allow external feedback to adjust routing preferences over time */ registerFeedback(toolName: string, outcome: 'success' | 'failure' | 'timeout'): void { const stats = this.feedback.get(toolName) || { success: 0, failure: 0 }; if (outcome === 'success') stats.success += 1; else stats.failure += 1; this.feedback.set(toolName, stats); } /** * Lightweight reranker to reward better coverage and reasons */ private rerankCandidates(query: string, matches: ToolMatch[]): ToolMatch[] { if (!matches.length) return matches; const queryTokens = new Set(query.split(/\s+/).filter(Boolean)); return [...matches] .map(m => { const coverage = [...queryTokens].filter(t => m.reason.toLowerCase().includes(t)).length; const reasonBonus = Math.min(coverage * 0.01, 0.05); const boosted = Math.min(m.confidence + reasonBonus, 0.99); return { ...m, confidence: boosted }; }) .sort((a, b) => b.confidence - a.confidence); } private getPayloadTemplate(toolName: string): Record | string | null { const def = this.toolDefs.find(t => t.name === toolName); return def?.payloadTemplate ?? null; } private getFeedbackBoost(toolName: string): number { const stats = this.feedback.get(toolName); if (!stats) return 0; const total = stats.success + stats.failure; if (total === 0) return 0; const ratio = stats.success / total; return Math.min(ratio * 5, 5); // up to 5 bonus points } /** * Provide suggestion alternatives when fallback is used */ private getSuggestionAlternatives(best: ToolMatch | null): ToolMatch[] { if (best) return []; const topByPriority = [...this.toolDefs].sort((a, b) => b.priority - a.priority).slice(0, 3); return topByPriority.map(t => ({ tool: t.name, confidence: (t.priority / 10) * 0.2, reason: 'Suggestion based on priority', category: t.category, })); } /** * Fallback tool match when no candidate passes threshold */ private getFallbackMatch(reason?: string): ToolMatch | null { const fallback = this.toolDefs.find(t => t.name === 'capability_broker'); if (!fallback) return null; return { tool: fallback.name, confidence: 0.35, reason: reason || 'Fallback: capability_broker', category: fallback.category, }; } } // ═══════════════════════════════════════════════════════════════════════════ // SINGLETON EXPORT // ═══════════════════════════════════════════════════════════════════════════ export const smartToolRouter = new SmartToolRouter(); /** * Convenience function for quick routing */ export function routeToTool(query: string): ToolMatch | null { return smartToolRouter.route(query).bestMatch; }