Spaces:
Paused
Paused
| /** | |
| * βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| * β 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, unknown> | string; | |
| } | |
| // Lightweight domain synonym map to widen recall without heavy models | |
| const SYNONYMS: Record<string, string[]> = { | |
| 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, unknown> | string | null; | |
| } | |
| class SmartToolRouter { | |
| private toolDefs: ToolDefinition[] = TOOL_DEFINITIONS; | |
| private feedback: Map<string, { success: number; failure: number }> = 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, unknown> | 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; | |
| } | |