UNPKG

@hivetechs/hive-ai

Version:

Real-time streaming AI consensus platform with HTTP+SSE MCP integration for Claude Code, VS Code, Cursor, and Windsurf - powered by OpenRouter's unified API

619 lines (614 loc) 27.8 kB
/** * Consensus Effectiveness Analyzer * * Professional-grade system for measuring the effectiveness of 4-stage consensus * vs single model performance. Provides comprehensive A/B testing, quality metrics, * and cost-effectiveness analysis for optimization recommendations. */ import { v4 as uuidv4 } from 'uuid'; import { structuredLogger } from './structured-logger.js'; export class ConsensusEffectivenessAnalyzer { /** * Run A/B comparison: single model vs 4-stage consensus */ async runABComparison(question, baselineModel, options = {}) { const comparisonId = uuidv4(); const startTime = Date.now(); structuredLogger.info('Starting A/B consensus comparison', { comparisonId, question: question.substring(0, 100), baselineModel, options }); const questionCategory = this.categorizeQuestion(question); const questionComplexity = this.assessComplexity(question); try { // Run single model if not skipped let singleModelResult = null; if (!options.skipSingleModel) { structuredLogger.info('Running single model baseline', { comparisonId, baselineModel }); singleModelResult = await this.runSingleModelBaseline(question, baselineModel); } // Run consensus if not skipped let consensusResult = null; if (!options.skipConsensus) { structuredLogger.info('Running 4-stage consensus', { comparisonId }); consensusResult = await this.runConsensusComparison(question); } // Create comparison result const comparison = { id: comparisonId, question, questionCategory, questionComplexity, timestamp: new Date().toISOString(), singleModel: singleModelResult, consensus: consensusResult, analysis: { improvementScore: 0, costEffectiveness: 0, timeEfficiency: 0, qualityDelta: { completeness: 0, accuracy: 0, clarity: 0, usefulness: 0, overallImprovement: 0 }, recommendation: 'depends', reasoning: 'Analysis pending' } }; // Analyze quality if both results available if (singleModelResult && consensusResult && options.autoAnalyze) { await this.analyzeQualityDifference(comparison); } // Save comparison to database await this.saveComparison(comparison); const duration = Date.now() - startTime; structuredLogger.info('A/B comparison completed', { comparisonId, duration, hasAnalysis: !!comparison.analysis.improvementScore }); return comparison; } catch (error) { structuredLogger.error('A/B comparison failed', { comparisonId }, error); throw error; } } /** * Run single model baseline for comparison */ async runSingleModelBaseline(question, model) { const startTime = Date.now(); try { // Import OpenRouter client for single call const { callOpenRouter } = await import('./openrouter-client.js'); const { getOpenRouterApiKey } = await import('../storage/unified-database.js'); const apiKey = await getOpenRouterApiKey(); if (!apiKey) { throw new Error('OpenRouter API key not configured'); } const messages = [ { role: 'user', content: question } ]; const result = await callOpenRouter(model, messages, apiKey); const duration = Date.now() - startTime; // Estimate cost for single model const estimatedCost = await this.estimateSingleModelCost(model, result.content.length); return { model, answer: result.content, duration, tokenCount: this.estimateTokenCount(result.content), cost: estimatedCost }; } catch (error) { structuredLogger.error('Single model baseline failed', { model }, error); throw error; } } /** * Run consensus for comparison */ async runConsensusComparison(question) { try { // Import consensus engine const { runConsensusPipeline } = await import('./enhanced-consensus-engine.js'); const conversationId = uuidv4(); const startTime = Date.now(); const result = await runConsensusPipeline(question, conversationId, null); const duration = Date.now() - startTime; // Get stage breakdown from conversation history const stageBreakdown = await this.extractStageBreakdown(conversationId); // Get model lineup const models = await this.getConsensusModels(); // Calculate total metrics const totalTokens = Object.values(stageBreakdown).reduce((sum, stage) => sum + stage.tokenCount, 0); const totalCost = Object.values(stageBreakdown).reduce((sum, stage) => sum + stage.cost, 0); return { models, answer: result, duration, tokenCount: totalTokens, cost: totalCost, stageBreakdown: stageBreakdown }; } catch (error) { structuredLogger.error('Consensus comparison failed', {}, error); throw error; } } /** * Analyze quality difference between single model and consensus */ async analyzeQualityDifference(comparison) { try { structuredLogger.info('Analyzing quality difference', { comparisonId: comparison.id }); // Use AI to assess quality of both answers const singleQuality = await this.assessAnswerQuality(comparison.question, comparison.singleModel.answer, comparison.questionCategory); const consensusQuality = await this.assessAnswerQuality(comparison.question, comparison.consensus.answer, comparison.questionCategory); // Update comparison with quality metrics comparison.singleModel.quality = singleQuality; comparison.consensus.quality = consensusQuality; // Calculate quality delta const qualityDelta = { completeness: consensusQuality.completeness - singleQuality.completeness, accuracy: consensusQuality.accuracy - singleQuality.accuracy, clarity: consensusQuality.clarity - singleQuality.clarity, usefulness: consensusQuality.usefulness - singleQuality.usefulness, overallImprovement: consensusQuality.overallScore - singleQuality.overallScore }; if (singleQuality.codeQuality && consensusQuality.codeQuality) { qualityDelta.codeQuality = consensusQuality.codeQuality - singleQuality.codeQuality; } // Calculate improvement score (0-1) const improvementScore = Math.max(0, Math.min(1, (qualityDelta.overallImprovement + 10) / 20)); // Calculate cost effectiveness const costRatio = comparison.consensus.cost / comparison.singleModel.cost; const costEffectiveness = qualityDelta.overallImprovement / costRatio; // Calculate time efficiency const timeRatio = comparison.consensus.duration / comparison.singleModel.duration; const timeEfficiency = qualityDelta.overallImprovement / timeRatio; // Generate recommendation const { recommendation, reasoning } = this.generateRecommendation(qualityDelta.overallImprovement, costRatio, timeRatio, comparison.questionComplexity); // Update analysis comparison.analysis = { improvementScore, costEffectiveness, timeEfficiency, qualityDelta, recommendation, reasoning }; structuredLogger.info('Quality analysis completed', { comparisonId: comparison.id, improvementScore, costEffectiveness, recommendation }); } catch (error) { structuredLogger.error('Quality analysis failed', { comparisonId: comparison.id }, error); // Set default analysis on failure comparison.analysis = { improvementScore: 0.5, costEffectiveness: 0, timeEfficiency: 0, qualityDelta: { completeness: 0, accuracy: 0, clarity: 0, usefulness: 0, overallImprovement: 0 }, recommendation: 'depends', reasoning: 'Analysis failed - manual review recommended' }; } } /** * Assess answer quality using AI */ async assessAnswerQuality(question, answer, category) { try { // Use a validator model for quality assessment const { callOpenRouter } = await import('./openrouter-client.js'); const { getOpenRouterApiKey } = await import('../storage/unified-database.js'); const apiKey = await getOpenRouterApiKey(); if (!apiKey) { throw new Error('OpenRouter API key not configured'); } const assessmentPrompt = this.buildQualityAssessmentPrompt(question, answer, category); const messages = [ { role: 'user', content: assessmentPrompt } ]; const result = await callOpenRouter('openai/gpt-4o', messages, apiKey); const scores = this.parseQualityScores(result.content); return scores; } catch (error) { structuredLogger.warn('Quality assessment failed, using defaults', { error: error.message }); // Return default scores on failure return { completeness: 5, accuracy: 5, clarity: 5, usefulness: 5, codeQuality: category.includes('code') ? 5 : undefined, overallScore: 5 }; } } /** * Build quality assessment prompt */ buildQualityAssessmentPrompt(question, answer, category) { const codeAssessment = category.includes('code') ? ` - Code Quality (0-10): How well-written, maintainable, and following best practices is the code?` : ''; return `Assess the quality of this answer on a scale of 0-10 for each metric: QUESTION: ${question} ANSWER: ${answer} Please evaluate: - Completeness (0-10): How complete and thorough is the answer? - Accuracy (0-10): How accurate and correct is the information? - Clarity (0-10): How clear, well-structured, and easy to understand is the answer? - Usefulness (0-10): How practically useful is this answer to the user?${codeAssessment} Respond in this exact format: Completeness: X Accuracy: X Clarity: X Usefulness: X${category.includes('code') ? '\nCode Quality: X' : ''} Brief reasoning: [1-2 sentences explaining the scores]`; } /** * Parse quality scores from AI response */ parseQualityScores(response) { const scores = {}; // Extract scores using regex const completenessMatch = response.match(/Completeness:\s*(\d+)/i); const accuracyMatch = response.match(/Accuracy:\s*(\d+)/i); const clarityMatch = response.match(/Clarity:\s*(\d+)/i); const usefulnessMatch = response.match(/Usefulness:\s*(\d+)/i); const codeQualityMatch = response.match(/Code Quality:\s*(\d+)/i); scores.completeness = completenessMatch ? parseInt(completenessMatch[1], 10) : 5; scores.accuracy = accuracyMatch ? parseInt(accuracyMatch[1], 10) : 5; scores.clarity = clarityMatch ? parseInt(clarityMatch[1], 10) : 5; scores.usefulness = usefulnessMatch ? parseInt(usefulnessMatch[1], 10) : 5; if (codeQualityMatch) { scores.codeQuality = parseInt(codeQualityMatch[1], 10); } // Calculate overall score const validScores = [scores.completeness, scores.accuracy, scores.clarity, scores.usefulness]; if (scores.codeQuality !== undefined) { validScores.push(scores.codeQuality); } scores.overallScore = validScores.reduce((sum, score) => sum + score, 0) / validScores.length; return scores; } /** * Generate recommendation based on metrics */ generateRecommendation(qualityImprovement, costRatio, timeRatio, complexity) { // Strong consensus recommendation if (qualityImprovement >= 2 && costRatio <= 3) { return { recommendation: 'consensus', reasoning: `Significant quality improvement (+${qualityImprovement.toFixed(1)}) with reasonable cost increase (${costRatio.toFixed(1)}x)` }; } // Strong single model recommendation if (qualityImprovement <= 0.5 && costRatio >= 5) { return { recommendation: 'single_model', reasoning: `Minimal quality improvement (+${qualityImprovement.toFixed(1)}) but high cost increase (${costRatio.toFixed(1)}x)` }; } // Complexity-based recommendation if (complexity === 'complex' && qualityImprovement >= 1) { return { recommendation: 'consensus', reasoning: `Complex question benefits from consensus (+${qualityImprovement.toFixed(1)} quality improvement)` }; } if (complexity === 'simple' && costRatio >= 3) { return { recommendation: 'single_model', reasoning: `Simple question doesn't justify consensus cost (${costRatio.toFixed(1)}x increase)` }; } // Default depends return { recommendation: 'depends', reasoning: `Moderate improvement (+${qualityImprovement.toFixed(1)}) with ${costRatio.toFixed(1)}x cost - depends on budget and quality requirements` }; } /** * Generate effectiveness report from stored comparisons */ async generateEffectivenessReport(options = {}) { try { const { getDatabase } = await import('../storage/unified-database.js'); const db = await getDatabase(); // Build query with filters let whereClause = '1=1'; const params = []; if (options.dateFrom) { whereClause += ' AND timestamp >= ?'; params.push(options.dateFrom); } if (options.dateTo) { whereClause += ' AND timestamp <= ?'; params.push(options.dateTo); } if (options.category) { whereClause += ' AND question_category = ?'; params.push(options.category); } if (options.complexity) { whereClause += ' AND question_complexity = ?'; params.push(options.complexity); } // Get all comparisons const comparisons = await db.all(` SELECT * FROM consensus_metrics WHERE ${whereClause} ORDER BY created_at DESC `, params); if (comparisons.length === 0) { return { totalComparisons: 0, consensusWins: 0, singleModelWins: 0, averageImprovementScore: 0, averageCostEffectiveness: 0, categoryBreakdown: {}, complexityBreakdown: {}, recommendations: ['No data available for analysis'] }; } // Calculate overall stats const consensusWins = comparisons.filter(c => c.improvement_score > 0.6).length; const singleModelWins = comparisons.filter(c => c.improvement_score < 0.4).length; const averageImprovementScore = comparisons.reduce((sum, c) => sum + c.improvement_score, 0) / comparisons.length; // Calculate cost effectiveness const costMetrics = JSON.parse(comparisons[0]?.cost_comparison || '{}'); const averageCostEffectiveness = Object.values(costMetrics).length > 0 ? Object.values(costMetrics).reduce((sum, val) => sum + (val.effectiveness || 0), 0) / Object.values(costMetrics).length : 0; // Generate category breakdown const categoryBreakdown = this.generateCategoryBreakdown(comparisons, 'question_category'); const complexityBreakdown = this.generateCategoryBreakdown(comparisons, 'question_complexity'); // Generate recommendations const recommendations = this.generateRecommendations(comparisons, { consensusWinRate: consensusWins / comparisons.length, averageImprovementScore, averageCostEffectiveness }); return { totalComparisons: comparisons.length, consensusWins, singleModelWins, averageImprovementScore, averageCostEffectiveness, categoryBreakdown, complexityBreakdown, recommendations }; } catch (error) { structuredLogger.error('Failed to generate effectiveness report', {}, error); throw error; } } /** * Utility functions */ categorizeQuestion(question) { const questionLower = question.toLowerCase(); if (questionLower.includes('react') || questionLower.includes('vue') || questionLower.includes('angular')) { return 'frontend_development'; } if (questionLower.includes('api') || questionLower.includes('backend') || questionLower.includes('server')) { return 'backend_development'; } if (questionLower.includes('database') || questionLower.includes('sql')) { return 'database'; } if (questionLower.includes('python') || questionLower.includes('javascript') || questionLower.includes('code')) { return 'programming'; } if (questionLower.includes('deploy') || questionLower.includes('docker') || questionLower.includes('aws')) { return 'devops'; } return 'general'; } assessComplexity(question) { const length = question.length; const words = question.split(' ').length; const questionLower = question.toLowerCase(); // Simple indicators if (length < 100 || words < 15 || questionLower.includes('simple') || questionLower.includes('basic') || questionLower.includes('quick')) { return 'simple'; } // Complex indicators if (length > 300 || words > 50 || questionLower.includes('complex') || questionLower.includes('enterprise') || questionLower.includes('production') || questionLower.includes('scalable')) { return 'complex'; } return 'moderate'; } estimateTokenCount(text) { // Rough estimate: ~4 characters per token return Math.round(text.length / 4); } async estimateSingleModelCost(model, contentLength) { try { const { getDatabase } = await import('../storage/unified-database.js'); const db = await getDatabase(); const modelData = await db.get(` SELECT pricing_input, pricing_output FROM openrouter_models WHERE openrouter_id = ? `, [model]); if (modelData?.pricing_input > 0) { const estimatedInputTokens = 100; // Rough estimate for question const estimatedOutputTokens = Math.round(contentLength / 4); return (estimatedInputTokens * modelData.pricing_input) + (estimatedOutputTokens * modelData.pricing_output); } // Fallback pricing const tokenCount = Math.round(contentLength / 4); return tokenCount * 0.000002; // $2 per 1M tokens fallback } catch (error) { return 0.001; // Fallback cost estimate } } async extractStageBreakdown(conversationId) { // This would extract actual stage metrics from the conversation // For now, return estimated breakdown return { generator: { duration: 3000, tokenCount: 400, cost: 0.002, tokensPerSecond: 133 }, refiner: { duration: 2500, tokenCount: 450, cost: 0.0018, tokensPerSecond: 180 }, validator: { duration: 2000, tokenCount: 350, cost: 0.0012, tokensPerSecond: 175 }, curator: { duration: 2800, tokenCount: 420, cost: 0.0019, tokensPerSecond: 150 } }; } async getConsensusModels() { try { const { getDefaultPipelineProfile, getCurrentModelId } = await import('../storage/unified-database.js'); const profile = await getDefaultPipelineProfile(); if (!profile) { throw new Error('No pipeline profile found'); } return { generator: await getCurrentModelId(profile.generator_model_internal_id) || 'unknown', refiner: await getCurrentModelId(profile.refiner_model_internal_id) || 'unknown', validator: await getCurrentModelId(profile.validator_model_internal_id) || 'unknown', curator: await getCurrentModelId(profile.curator_model_internal_id) || 'unknown' }; } catch (error) { return { generator: 'anthropic/claude-3.5-sonnet', refiner: 'openai/gpt-4o', validator: 'google/gemini-pro-1.5', curator: 'anthropic/claude-3.5-sonnet' }; } } async saveComparison(comparison) { try { const { getDatabase, createConversation } = await import('../storage/unified-database.js'); const db = await getDatabase(); // Create a conversation record for the A/B test to satisfy foreign key constraint await createConversation(comparison.id, undefined, undefined); // Remove id from INSERT since it's AUTOINCREMENT await db.run(` INSERT INTO consensus_metrics ( conversation_id, baseline_model, baseline_result, consensus_result, improvement_score, quality_metrics, cost_comparison, time_comparison, user_rating, question_complexity, question_category, effectiveness_notes, created_at ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `, [ comparison.id, // Use comparison ID as conversation ID comparison.singleModel.model, comparison.singleModel.answer, comparison.consensus.answer, comparison.analysis.improvementScore, JSON.stringify({ singleModel: comparison.singleModel.quality, consensus: comparison.consensus.quality, delta: comparison.analysis.qualityDelta }), JSON.stringify({ singleModelCost: comparison.singleModel.cost, consensusCost: comparison.consensus.cost, ratio: comparison.consensus.cost / comparison.singleModel.cost, effectiveness: comparison.analysis.costEffectiveness }), JSON.stringify({ singleModelTime: comparison.singleModel.duration, consensusTime: comparison.consensus.duration, ratio: comparison.consensus.duration / comparison.singleModel.duration, efficiency: comparison.analysis.timeEfficiency }), null, // user_rating - to be filled later comparison.questionComplexity, comparison.questionCategory, comparison.analysis.reasoning, comparison.timestamp ]); structuredLogger.info('Comparison saved to database', { comparisonId: comparison.id }); } catch (error) { structuredLogger.error('Failed to save comparison', { comparisonId: comparison.id }, error); // Don't throw - comparison can still be used even if not saved } } generateCategoryBreakdown(comparisons, field) { const breakdown = {}; // Group by field const groups = comparisons.reduce((acc, comp) => { const key = comp[field] || 'unknown'; if (!acc[key]) acc[key] = []; acc[key].push(comp); return acc; }, {}); // Calculate stats for each group Object.entries(groups).forEach(([key, comps]) => { const compArray = comps; const consensusWins = compArray.filter(c => c.improvement_score > 0.6).length; const avgImprovement = compArray.reduce((sum, c) => sum + c.improvement_score, 0) / compArray.length; breakdown[key] = { comparisons: compArray.length, consensusWinRate: consensusWins / compArray.length, averageImprovement: avgImprovement, averageCostRatio: 3.2, // Estimated from cost comparison JSON timeRatio: 2.8 // Estimated from time comparison JSON }; }); return breakdown; } generateRecommendations(comparisons, stats) { const recommendations = []; if (stats.consensusWinRate > 0.7) { recommendations.push('Consensus shows strong advantages - recommend using consensus for most questions'); } else if (stats.consensusWinRate < 0.3) { recommendations.push('Single models perform well - consider using consensus only for complex questions'); } else { recommendations.push('Mixed results - recommend consensus for complex questions, single models for simple ones'); } if (stats.averageCostEffectiveness > 1) { recommendations.push('Consensus provides good cost-effectiveness - quality improvements justify the cost'); } else { recommendations.push('Monitor cost-effectiveness - consider budget vs quality requirements'); } if (stats.averageImprovementScore < 0.3) { recommendations.push('Consider optimizing consensus pipeline - current setup may not be adding sufficient value'); } return recommendations; } } /** * Factory function to create consensus effectiveness analyzer */ export function createConsensusAnalyzer() { return new ConsensusEffectivenessAnalyzer(); } //# sourceMappingURL=consensus-effectiveness-analyzer.js.map