UNPKG

bc-code-intelligence-mcp

Version:

BC Code Intelligence MCP Server - Complete Specialist Bundle with AI-driven expert consultation, seamless handoffs, and context-preserving workflows

387 lines 15.8 kB
/** * Specialist Discovery Service * * Analyzes user queries and suggests relevant specialists based on keywords, * expertise domains, and conversation context. */ export class SpecialistDiscoveryService { layerService; specialists = []; keywordMappings = new Map(); initialized = false; constructor(layerService) { this.layerService = layerService; } /** * Initialize the discovery service with specialist data */ async initialize() { this.specialists = await this.layerService.getAllSpecialists(); this.buildKeywordMappings(); this.initialized = true; } /** * Suggest specialists for a given query and context */ async suggestSpecialists(context, maxSuggestions = 3) { await this.ensureInitialized(); if (!context.query) { return this.getDefaultSpecialists(); } // First, try name-based matching const nameMatch = await this.findSpecialistByName(context.query); if (nameMatch) { return [{ specialist: nameMatch, confidence: 0.95, reasons: ['Direct name match'], keywords_matched: [this.extractNameFromQuery(context.query)], match_type: 'name_match' }]; } // Fall back to content-based matching const suggestions = []; for (const specialist of this.specialists) { const suggestion = this.analyzeSpecialistMatch(specialist, context); if (suggestion.confidence > 0.1) { // Minimum confidence threshold suggestions.push(suggestion); } } // Sort by confidence and return top suggestions return suggestions .sort((a, b) => b.confidence - a.confidence) .slice(0, maxSuggestions); } /** * Get the best single specialist suggestion */ async getBestSpecialist(context) { const suggestions = await this.suggestSpecialists(context, 1); return suggestions.length > 0 ? suggestions[0] : null; } /** * Get specialists by domain */ async getSpecialistsByDomain(domain) { await this.ensureInitialized(); return this.specialists.filter(specialist => (specialist.expertise?.primary && specialist.expertise.primary.includes(domain)) || (specialist.expertise?.secondary && specialist.expertise.secondary.includes(domain)) || (specialist.domains && specialist.domains.includes(domain))); } /** * Get all available specialists grouped by their primary domains */ async getSpecialistsByCategory() { await this.ensureInitialized(); const categories = {}; for (const specialist of this.specialists) { const primaryDomain = specialist.domains?.[0] || 'general'; if (!categories[primaryDomain]) { categories[primaryDomain] = []; } categories[primaryDomain].push(specialist); } return categories; } /** * Get a specific specialist by ID */ async getSpecialistById(specialistId) { await this.ensureInitialized(); return this.specialists.find(specialist => specialist.specialist_id === specialistId) || null; } /** * Find specialist by partial/fuzzy name matching * Handles cases like "Sam" -> "sam-coder", "Dean" -> "dean-debug", etc. */ async findSpecialistByName(partialName) { await this.ensureInitialized(); const searchTerm = partialName.toLowerCase().trim(); // First try exact specialist_id match (case insensitive) let match = this.specialists.find(specialist => specialist.specialist_id.toLowerCase() === searchTerm); if (match) return match; // Try partial match in specialist_id match = this.specialists.find(specialist => specialist.specialist_id.toLowerCase().includes(searchTerm)); if (match) return match; // Try matching first part of specialist_id (before the dash) match = this.specialists.find(specialist => { const firstName = specialist.specialist_id.split('-')[0].toLowerCase(); return firstName === searchTerm; }); if (match) return match; // Try matching in title match = this.specialists.find(specialist => specialist.title?.toLowerCase().includes(searchTerm)); return match || null; } /** * Analyze how well a specialist matches the given context */ analyzeSpecialistMatch(specialist, context) { let confidence = 0; const reasons = []; const keywords_matched = []; let domain_match; if (!context.query) { return { specialist, confidence: 0, reasons: [], keywords_matched: [] }; } const queryLower = context.query.toLowerCase(); // Tokenize query for better matching (Issue #17 fix) const queryTokens = queryLower .split(/[\s,]+/) .filter(token => token.length > 3) .map(token => token.replace(/[^a-z0-9]/g, '')); // Check keyword matches const specialistKeywords = this.keywordMappings.get(specialist.specialist_id) || new Set(); for (const token of queryTokens) { if (specialistKeywords.has(token)) { keywords_matched.push(token); confidence += 0.15; } } // Check domain expertise matches with token-based matching if (specialist.expertise?.primary) { for (const expertise of specialist.expertise.primary) { const expertiseTokens = expertise .toLowerCase() .replace(/[-_]/g, ' ') .split(/\s+/) .filter(t => t.length > 3); // Check if any query token matches any expertise token (bidirectional) for (const queryToken of queryTokens) { if (expertiseTokens.some(et => et.includes(queryToken) || queryToken.includes(et))) { confidence += 0.15; reasons.push(`Primary expertise in ${expertise}`); domain_match = expertise; break; // Only count each expertise once } } } } if (specialist.expertise?.secondary) { for (const expertise of specialist.expertise.secondary) { const expertiseTokens = expertise .toLowerCase() .replace(/[-_]/g, ' ') .split(/\s+/) .filter(t => t.length > 3); for (const queryToken of queryTokens) { if (expertiseTokens.some(et => et.includes(queryToken) || queryToken.includes(et))) { confidence += 0.1; reasons.push(`Secondary expertise in ${expertise}`); domain_match = domain_match || expertise; break; } } } } // Check domain matches with token-based matching if (specialist.domains) { for (const domain of specialist.domains) { const domainTokens = domain .toLowerCase() .replace(/[-_]/g, ' ') .split(/\s+/) .filter(t => t.length > 3); for (const queryToken of queryTokens) { if (domainTokens.some(dt => dt.includes(queryToken) || queryToken.includes(dt))) { confidence += 0.1; reasons.push(`Domain specialist for ${domain}`); domain_match = domain_match || domain; break; } } } } // Check "when to use" scenarios with token-based matching if (specialist.when_to_use) { for (const scenario of specialist.when_to_use) { const scenarioTokens = scenario .toLowerCase() .replace(/[-_]/g, ' ') .split(/\s+/) .filter(t => t.length > 3); for (const queryToken of queryTokens) { if (scenarioTokens.some(st => st.includes(queryToken) || queryToken.includes(st))) { confidence += 0.15; reasons.push(`Ideal for ${scenario}`); break; } } } } // Boost confidence for exact role matches with token-based matching if (specialist.role) { const roleTokens = specialist.role .toLowerCase() .replace(/[-_]/g, ' ') .split(/\s+/) .filter(t => t.length > 3); for (const queryToken of queryTokens) { if (roleTokens.some(rt => rt.includes(queryToken) || queryToken.includes(rt))) { confidence += 0.2; reasons.push(`Role matches: ${specialist.role}`); break; } } } // Context domain bonus if (context.current_domain && specialist.domains?.includes(context.current_domain)) { confidence += 0.15; reasons.push(`Active in current domain: ${context.current_domain}`); } // Cap confidence at 1.0 confidence = Math.min(confidence, 1.0); // Add generic reasons if confidence is reasonable but no specific reasons if (confidence > 0.3 && reasons.length === 0) { reasons.push('Good keyword and expertise match'); } return { specialist, confidence, reasons, keywords_matched, domain_match, match_type: 'content_match' }; } /** * Build keyword mappings for efficient matching */ buildKeywordMappings() { for (const specialist of this.specialists) { const keywords = new Set(); // Add specialist ID keywords keywords.add(specialist.specialist_id); keywords.add(specialist.specialist_id.replace('-', ' ')); // Add title keywords (this is the specialist's display name) if (specialist.title) { specialist.title.toLowerCase().split(/\s+/).forEach(word => keywords.add(word)); } // Add role keywords if (specialist.role) { specialist.role.toLowerCase().split(/\s+/).forEach(word => keywords.add(word)); } // Add expertise keywords if (specialist.expertise?.primary) { specialist.expertise.primary.forEach(exp => { exp.toLowerCase().split(/[-\s]+/).forEach(word => keywords.add(word)); }); } if (specialist.expertise?.secondary) { specialist.expertise.secondary.forEach(exp => { exp.toLowerCase().split(/[-\s]+/).forEach(word => keywords.add(word)); }); } // Add domain keywords if (specialist.domains) { specialist.domains.forEach(domain => { domain.toLowerCase().split(/[-\s]+/).forEach(word => keywords.add(word)); }); } // Add "when to use" keywords if (specialist.when_to_use) { specialist.when_to_use.forEach(scenario => { scenario.toLowerCase().split(/\s+/).forEach(word => keywords.add(word)); }); } // Add personality keywords if available if (specialist.persona?.personality) { specialist.persona.personality.forEach(trait => { trait.toLowerCase().split(/[-\s]+/).forEach(word => keywords.add(word)); }); } this.keywordMappings.set(specialist.specialist_id, keywords); } } /** * Get default specialists for when no query is provided */ getDefaultSpecialists() { // Return most versatile specialists as defaults const defaultIds = ['casey-copilot', 'sam-coder', 'alex-architect']; return this.specialists .filter(s => defaultIds.includes(s.specialist_id)) .map(specialist => ({ specialist, confidence: 0.5, reasons: ['Popular general-purpose specialist'], keywords_matched: [] })); } /** * Ensure the service is initialized */ async ensureInitialized() { if (!this.initialized) { await this.initialize(); } } /** * Format specialist suggestions for display */ formatSuggestions(suggestions) { if (suggestions.length === 0) { return "No specific specialist recommendations. Try asking Casey Copilot for general guidance!"; } let result = "🎯 **Specialist Recommendations:**\n\n"; for (const suggestion of suggestions) { const emoji = suggestion.specialist.emoji || '👤'; const name = suggestion.specialist.title || suggestion.specialist.specialist_id; const confidence = Math.round(suggestion.confidence * 100); result += `${emoji} **${name}** (${confidence}% match)\n`; if (suggestion.reasons.length > 0) { result += ` • ${suggestion.reasons.join('\n • ')}\n`; } if (suggestion.keywords_matched.length > 0) { result += ` • Keywords: ${suggestion.keywords_matched.join(', ')}\n`; } result += ` • Try: "${this.generateExampleQuery(suggestion.specialist)}"\n\n`; } return result; } /** * Generate an example query for a specialist */ generateExampleQuery(specialist) { if (specialist.when_to_use && specialist.when_to_use.length > 0) { return `Help me with ${specialist.when_to_use[0].toLowerCase()}`; } if (specialist.expertise?.primary && specialist.expertise.primary.length > 0) { return `I need help with ${specialist.expertise.primary[0].toLowerCase()}`; } return `I need help with ${specialist.role?.toLowerCase() || 'development'}`; } /** * Extract the specialist name from a query string */ extractNameFromQuery(query) { const words = query.toLowerCase().split(/\s+/); const commonNames = ['sam', 'alex', 'dean', 'eva', 'jordan', 'logan', 'maya', 'morgan', 'quinn', 'roger', 'seth', 'taylor', 'uma', 'casey', 'chris']; for (const word of words) { if (commonNames.includes(word)) { return word; } } return query.split(/\s+/)[0]; // Return first word as fallback } /** * Get all available specialists with basic info */ async getAllSpecialistsInfo() { await this.ensureInitialized(); return this.specialists.map(specialist => ({ id: specialist.specialist_id, title: specialist.title || specialist.specialist_id, role: specialist.role || 'Specialist' })); } } //# sourceMappingURL=specialist-discovery.js.map