widgetdc-cortex / apps /backend /src /mcp /SmartToolRouter.ts
Kraft102's picture
Initial deployment - WidgeTDC Cortex Backend v2.1.0
529090e
/**
* ╔═══════════════════════════════════════════════════════════════════════════╗
* β•‘ 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;
}