UNPKG

claude-flow

Version:

Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration

586 lines (484 loc) 16.9 kB
/** * Semantic Router - Neural BMSSP-powered intelligent routing * * Uses WebAssembly-accelerated neural pathfinding with embeddings * to match tasks to the best-suited teammates based on semantic similarity. * * @module @claude-flow/teammate-plugin/semantic * @version 1.0.0-alpha.1 */ import type { TeammateInfo, TeamState } from './types.js'; // Dynamic import for Neural BMSSP let WasmNeuralBMSSP: any = null; async function loadNeuralBMSSP(): Promise<void> { if (WasmNeuralBMSSP) return; try { const bmssp = await import('@ruvnet/bmssp'); await bmssp.default(); // Initialize WASM WasmNeuralBMSSP = bmssp.WasmNeuralBMSSP; } catch (error) { console.warn('[SemanticRouter] Neural BMSSP not available, using fallback'); } } // ============================================================================ // Types // ============================================================================ export interface TeammateProfile { id: string; role: string; skills: string[]; embedding?: Float64Array; performance: { tasksCompleted: number; successRate: number; averageLatencyMs: number; }; } export interface TaskProfile { id: string; description: string; requiredSkills: string[]; embedding?: Float64Array; priority: 'urgent' | 'high' | 'normal' | 'low'; estimatedDuration?: number; } export interface MatchResult { teammateId: string; score: number; semanticDistance: number; skillMatch: number; loadFactor: number; confidence: number; } export interface RoutingDecision { task: TaskProfile; matches: MatchResult[]; selectedTeammate: string | null; reasoning: string; alternates: string[]; } export interface SemanticRouterConfig { embeddingDim: number; skillWeight: number; semanticWeight: number; loadWeight: number; performanceWeight: number; minConfidence: number; } // ============================================================================ // Default Configuration // ============================================================================ export const DEFAULT_SEMANTIC_CONFIG: SemanticRouterConfig = { embeddingDim: 64, skillWeight: 0.3, semanticWeight: 0.4, loadWeight: 0.15, performanceWeight: 0.15, minConfidence: 0.5, }; // ============================================================================ // Skill Embeddings (Pre-computed for common roles) // ============================================================================ const SKILL_EMBEDDINGS: Record<string, number[]> = { // Development skills 'typescript': [0.9, 0.8, 0.7, 0.1, 0.0, 0.0, 0.2, 0.3], 'javascript': [0.85, 0.9, 0.65, 0.1, 0.0, 0.0, 0.2, 0.3], 'python': [0.7, 0.6, 0.8, 0.3, 0.5, 0.0, 0.1, 0.2], 'rust': [0.8, 0.5, 0.9, 0.0, 0.0, 0.0, 0.4, 0.5], // Testing skills 'testing': [0.3, 0.4, 0.5, 0.9, 0.2, 0.0, 0.3, 0.2], 'tdd': [0.35, 0.45, 0.55, 0.95, 0.1, 0.0, 0.35, 0.25], 'e2e': [0.25, 0.35, 0.45, 0.85, 0.15, 0.0, 0.25, 0.15], // Architecture skills 'architecture': [0.4, 0.3, 0.5, 0.2, 0.1, 0.9, 0.6, 0.7], 'design': [0.45, 0.35, 0.55, 0.15, 0.05, 0.85, 0.65, 0.75], 'api': [0.6, 0.5, 0.65, 0.3, 0.1, 0.7, 0.5, 0.4], // Security skills 'security': [0.3, 0.2, 0.4, 0.4, 0.0, 0.6, 0.9, 0.8], 'audit': [0.25, 0.15, 0.35, 0.5, 0.0, 0.55, 0.85, 0.9], // Research skills 'research': [0.2, 0.1, 0.3, 0.1, 0.9, 0.4, 0.2, 0.3], 'analysis': [0.25, 0.15, 0.35, 0.2, 0.85, 0.45, 0.25, 0.35], // Role-based embeddings 'coder': [0.9, 0.85, 0.8, 0.3, 0.1, 0.4, 0.2, 0.3], 'tester': [0.4, 0.5, 0.45, 0.95, 0.2, 0.3, 0.3, 0.2], 'reviewer': [0.6, 0.55, 0.65, 0.7, 0.3, 0.6, 0.5, 0.4], 'architect': [0.5, 0.4, 0.6, 0.3, 0.2, 0.95, 0.7, 0.8], 'researcher': [0.3, 0.2, 0.4, 0.2, 0.95, 0.5, 0.3, 0.4], 'coordinator': [0.4, 0.35, 0.45, 0.4, 0.4, 0.7, 0.5, 0.6], 'security-specialist': [0.35, 0.25, 0.45, 0.45, 0.1, 0.6, 0.95, 0.85], }; // ============================================================================ // Semantic Router Class // ============================================================================ export class SemanticRouter { private neuralGraph: any = null; private profiles: Map<string, TeammateProfile> = new Map(); private nodeMap: Map<string, number> = new Map(); private nodeCount: number = 0; private initialized: boolean = false; private useFallback: boolean = false; private config: SemanticRouterConfig; constructor(config: Partial<SemanticRouterConfig> = {}) { this.config = { ...DEFAULT_SEMANTIC_CONFIG, ...config }; } /** * Initialize the router with WASM support */ async initialize(): Promise<boolean> { try { await loadNeuralBMSSP(); this.initialized = true; this.useFallback = !WasmNeuralBMSSP; if (!this.useFallback) { this.neuralGraph = new WasmNeuralBMSSP(100, this.config.embeddingDim); } return !this.useFallback; } catch { this.useFallback = true; this.initialized = true; return false; } } /** * Register a teammate with their profile */ registerTeammate(teammate: TeammateInfo, skills: string[] = []): TeammateProfile { const profile: TeammateProfile = { id: teammate.id, role: teammate.role, skills: skills.length > 0 ? skills : this.inferSkills(teammate.role), embedding: this.computeEmbedding(skills.length > 0 ? skills : [teammate.role]), performance: { tasksCompleted: 0, successRate: 1.0, averageLatencyMs: 0, }, }; this.profiles.set(teammate.id, profile); // Add to neural graph if (!this.nodeMap.has(teammate.id)) { const index = this.nodeCount++; this.nodeMap.set(teammate.id, index); if (!this.useFallback && this.neuralGraph && profile.embedding) { this.neuralGraph.set_embedding(index, profile.embedding); } } return profile; } /** * Build profiles from team state */ async buildFromTeam(team: TeamState): Promise<void> { if (!this.initialized) { await this.initialize(); } for (const teammate of team.teammates) { this.registerTeammate(teammate); } // Build semantic edges between similar teammates this.buildSemanticEdges(); } /** * Find best teammate match for a task */ findBestMatch(task: TaskProfile): RoutingDecision { const taskEmbedding = task.embedding || this.computeEmbedding(task.requiredSkills); const matches: MatchResult[] = []; for (const [id, profile] of this.profiles) { const match = this.computeMatchScore(task, taskEmbedding, profile); matches.push(match); } // Sort by score descending matches.sort((a, b) => b.score - a.score); const selectedTeammate = matches.length > 0 && matches[0].confidence >= this.config.minConfidence ? matches[0].teammateId : null; const alternates = matches .slice(1, 4) .filter(m => m.confidence >= this.config.minConfidence * 0.8) .map(m => m.teammateId); return { task, matches, selectedTeammate, reasoning: this.generateReasoning(task, matches[0], selectedTeammate), alternates, }; } /** * Batch match multiple tasks to teammates */ batchMatch(tasks: TaskProfile[]): Map<string, RoutingDecision> { const results = new Map<string, RoutingDecision>(); const assignedTeammates = new Set<string>(); // Sort tasks by priority const sortedTasks = [...tasks].sort((a, b) => { const priorityOrder = { urgent: 0, high: 1, normal: 2, low: 3 }; return priorityOrder[a.priority] - priorityOrder[b.priority]; }); for (const task of sortedTasks) { const decision = this.findBestMatch(task); // If selected teammate is already assigned, try alternates if (decision.selectedTeammate && assignedTeammates.has(decision.selectedTeammate)) { for (const alternate of decision.alternates) { if (!assignedTeammates.has(alternate)) { decision.selectedTeammate = alternate; decision.reasoning = `${decision.reasoning} (reassigned due to load)`; break; } } } if (decision.selectedTeammate) { assignedTeammates.add(decision.selectedTeammate); } results.set(task.id, decision); } return results; } /** * Get semantic distance between two teammates */ getSemanticDistance(id1: string, id2: string): number { const index1 = this.nodeMap.get(id1); const index2 = this.nodeMap.get(id2); if (index1 === undefined || index2 === undefined) { return Infinity; } if (!this.useFallback && this.neuralGraph) { return this.neuralGraph.semantic_distance(index1, index2); } else { // Fallback: cosine distance on embeddings const profile1 = this.profiles.get(id1); const profile2 = this.profiles.get(id2); if (!profile1?.embedding || !profile2?.embedding) { return Infinity; } return this.cosineDistance(profile1.embedding, profile2.embedding); } } /** * Update teammate performance metrics */ updatePerformance( teammateId: string, taskSuccess: boolean, latencyMs: number ): void { const profile = this.profiles.get(teammateId); if (!profile) return; const prev = profile.performance; const total = prev.tasksCompleted + 1; profile.performance = { tasksCompleted: total, successRate: (prev.successRate * prev.tasksCompleted + (taskSuccess ? 1 : 0)) / total, averageLatencyMs: (prev.averageLatencyMs * prev.tasksCompleted + latencyMs) / total, }; // Update embedding with performance gradient (if WASM available) if (!this.useFallback && this.neuralGraph && profile.embedding) { const gradient = new Float64Array(this.config.embeddingDim); const factor = taskSuccess ? 0.01 : -0.01; for (let i = 0; i < gradient.length; i++) { gradient[i] = profile.embedding[i] * factor; } const index = this.nodeMap.get(teammateId); if (index !== undefined) { this.neuralGraph.update_embeddings(gradient, 0.001, this.config.embeddingDim); } } } /** * Get teammate profile */ getProfile(teammateId: string): TeammateProfile | undefined { return this.profiles.get(teammateId); } /** * Get all profiles */ getAllProfiles(): TeammateProfile[] { return Array.from(this.profiles.values()); } /** * Clear all data */ clear(): void { if (this.neuralGraph) { try { this.neuralGraph.free(); } catch { // Ignore cleanup errors } this.neuralGraph = null; } this.profiles.clear(); this.nodeMap.clear(); this.nodeCount = 0; } /** * Free resources */ dispose(): void { this.clear(); this.initialized = false; } // ============================================================================ // Private Methods // ============================================================================ private inferSkills(role: string): string[] { const roleSkills: Record<string, string[]> = { coder: ['typescript', 'javascript', 'python', 'api'], tester: ['testing', 'tdd', 'e2e'], reviewer: ['analysis', 'security', 'architecture'], architect: ['architecture', 'design', 'api'], researcher: ['research', 'analysis'], coordinator: ['architecture', 'design'], 'security-architect': ['security', 'audit', 'architecture'], }; const normalizedRole = role.toLowerCase().replace(/[-_]/g, ''); for (const [key, skills] of Object.entries(roleSkills)) { if (normalizedRole.includes(key.replace(/[-_]/g, ''))) { return skills; } } return [role]; // Use role as skill if no match } private computeEmbedding(skills: string[]): Float64Array { const embedding = new Float64Array(this.config.embeddingDim); // Average skill embeddings let count = 0; for (const skill of skills) { const skillEmbed = SKILL_EMBEDDINGS[skill.toLowerCase()]; if (skillEmbed) { for (let i = 0; i < Math.min(skillEmbed.length, this.config.embeddingDim); i++) { embedding[i] += skillEmbed[i]; } count++; } } // Normalize if (count > 0) { for (let i = 0; i < embedding.length; i++) { embedding[i] /= count; } } // Pad with random noise for diversity for (let i = 8; i < this.config.embeddingDim; i++) { embedding[i] = Math.random() * 0.1; } return embedding; } private buildSemanticEdges(): void { if (this.useFallback || !this.neuralGraph) return; const ids = Array.from(this.nodeMap.keys()); for (let i = 0; i < ids.length; i++) { for (let j = i + 1; j < ids.length; j++) { const idx1 = this.nodeMap.get(ids[i])!; const idx2 = this.nodeMap.get(ids[j])!; // Alpha based on skill overlap const profile1 = this.profiles.get(ids[i]); const profile2 = this.profiles.get(ids[j]); if (profile1 && profile2) { const overlap = this.skillOverlap(profile1.skills, profile2.skills); const alpha = overlap * 0.5 + 0.5; // [0.5, 1.0] this.neuralGraph.add_semantic_edge(idx1, idx2, alpha); this.neuralGraph.add_semantic_edge(idx2, idx1, alpha); } } } } private computeMatchScore( task: TaskProfile, taskEmbedding: Float64Array, profile: TeammateProfile ): MatchResult { // Skill match score const skillMatch = this.skillOverlap(task.requiredSkills, profile.skills); // Semantic distance (lower is better) let semanticDistance = 1.0; if (profile.embedding) { semanticDistance = this.cosineDistance(taskEmbedding, profile.embedding); } const semanticScore = 1 - Math.min(1, semanticDistance); // Load factor (1.0 = idle, 0.0 = busy) const loadFactor = 1.0; // Would be computed from actual teammate status // Performance score const performanceScore = profile.performance.successRate; // Weighted combination const score = this.config.skillWeight * skillMatch + this.config.semanticWeight * semanticScore + this.config.loadWeight * loadFactor + this.config.performanceWeight * performanceScore; // Confidence based on data quality const confidence = Math.min(1, (skillMatch + semanticScore) / 2 + 0.3); return { teammateId: profile.id, score, semanticDistance, skillMatch, loadFactor, confidence, }; } private skillOverlap(skills1: string[], skills2: string[]): number { const set1 = new Set(skills1.map(s => s.toLowerCase())); const set2 = new Set(skills2.map(s => s.toLowerCase())); let intersection = 0; for (const skill of set1) { if (set2.has(skill)) intersection++; } const union = set1.size + set2.size - intersection; return union > 0 ? intersection / union : 0; } private cosineDistance(a: Float64Array, b: Float64Array): number { let dotProduct = 0; let normA = 0; let normB = 0; const len = Math.min(a.length, b.length); for (let i = 0; i < len; i++) { dotProduct += a[i] * b[i]; normA += a[i] * a[i]; normB += b[i] * b[i]; } const magnitude = Math.sqrt(normA) * Math.sqrt(normB); if (magnitude === 0) return 1.0; const similarity = dotProduct / magnitude; return 1 - similarity; // Convert to distance } private generateReasoning( task: TaskProfile, bestMatch: MatchResult | undefined, selected: string | null ): string { if (!bestMatch || !selected) { return 'No suitable teammate found with sufficient confidence.'; } const profile = this.profiles.get(selected); if (!profile) { return 'Selected teammate profile not found.'; } const reasons: string[] = []; if (bestMatch.skillMatch > 0.5) { reasons.push(`skill match: ${(bestMatch.skillMatch * 100).toFixed(0)}%`); } if (bestMatch.semanticDistance < 0.5) { reasons.push(`semantic fit: ${((1 - bestMatch.semanticDistance) * 100).toFixed(0)}%`); } if (profile.performance.successRate > 0.9) { reasons.push(`success rate: ${(profile.performance.successRate * 100).toFixed(0)}%`); } return `Selected ${profile.role} "${selected}" (${reasons.join(', ')})`; } } // ============================================================================ // Factory Function // ============================================================================ export async function createSemanticRouter( config?: Partial<SemanticRouterConfig> ): Promise<SemanticRouter> { const router = new SemanticRouter(config); await router.initialize(); return router; } export default SemanticRouter;