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

1,038 lines (988 loc) 76.5 kB
/** * Consensus Engine - SOURCE_OF_TRUTH Implementation * * Core 4-stage consensus pipeline per exact design specifications. * Each complete cycle (4 stages) counts as one conversation against user's daily limit. */ import { v4 as uuidv4 } from 'uuid'; import { callOpenRouter } from './openrouter-client.js'; import { initializeUnifiedDatabase, addMessage, createConversation, getDefaultPipelineProfile, getPipelineProfile, getCurrentModelId, getOpenRouterApiKey, validateOpenRouterApiKey, getCurrentUserId, addToPendingSync, markSyncComplete, recordSyncAttempt } from '../storage/unified-database.js'; import { conversationGateway, ConversationGatewayError, UsageLimitExceededError } from '../auth/conversation-gateway.js'; import { globalErrorHandler } from './error-handling.js'; import { globalHealthMonitor } from './health-monitor.js'; import { structuredLogger } from './structured-logger.js'; /** * Run a single consensus stage per SOURCE_OF_TRUTH design * Function: runConsensusStage(stageName, question, previousAnswer, model, conversationId) */ export async function runConsensusStage(stageName, question, previousAnswer, model, conversationId) { // Initialize database await initializeUnifiedDatabase(); // Get OpenRouter API key const apiKey = await getOpenRouterApiKey(); if (!apiKey) { throw new Error('No OpenRouter API key configured. Please run: hive-ai configure'); } if (!validateOpenRouterApiKey(apiKey)) { throw new Error('Invalid OpenRouter API key format. Please reconfigure.'); } // Build messages based on stage and context const messages = await buildStageMessages(stageName, question, previousAnswer); try { // Call OpenRouter API with bulletproof error handling const [provider, modelName] = model.includes('/') ? model.split('/') : ['openrouter', model]; const requestId = `${conversationId}-${stageName}-${Date.now()}`; structuredLogger.stageStart(stageName, { provider, model: modelName, conversationId, requestId }); const startTime = Date.now(); const { result: response } = await globalErrorHandler.executeWithFallback(async (fallbackProvider, fallbackModel) => { const fullModel = fallbackProvider === 'openrouter' ? fallbackModel : `${fallbackProvider}/${fallbackModel}`; structuredLogger.apiRequestStart(fallbackProvider, fallbackModel, { requestId }); const apiStartTime = Date.now(); try { const result = await callOpenRouter(fullModel, messages, apiKey); structuredLogger.apiRequestComplete(fallbackProvider, fallbackModel, Date.now() - apiStartTime, { requestId }); return result; } catch (error) { structuredLogger.apiRequestError(fallbackProvider, fallbackModel, error, { requestId }); throw error; } }, provider, modelName, stageName, requestId); // Generate stage ID and prepare result const stageId = uuidv4(); const result = { stageId, stageName, question, answer: response.content, model: response.model || model, conversationId, timestamp: new Date().toISOString() }; // Save stage result to SQLite await saveStageResult(result); const duration = Date.now() - startTime; structuredLogger.stageComplete(stageName, duration, { provider, model: modelName, conversationId, requestId }); return result; } catch (error) { const [provider, modelName] = model.includes('/') ? model.split('/') : ['openrouter', model]; const requestId = `${conversationId}-${stageName}-${Date.now()}`; structuredLogger.stageError(stageName, error, { provider, model: modelName, conversationId, requestId }); console.error(`❌ ${stageName} stage failed after all retries and fallbacks:`, error.message); throw new Error(`${stageName} stage failed: ${error.message}`); } } /** * Build stage-specific messages per SOURCE_OF_TRUTH design * CRITICAL: "Original user question + Stage X answer (as supplemental knowledge)" */ export async function buildStageMessages(stageName, question, previousAnswer) { const messages = []; // Get curator knowledge as learning material for all stages const curatorKnowledge = await getCuratorKnowledgeBase(question); // ALL stages get the same question + curator knowledge as context if (curatorKnowledge) { messages.push({ role: 'system', content: `IMPORTANT: The following are AUTHORITATIVE ARTICLES from previous curator-verified answers. These represent established knowledge and should be treated as factual sources, NOT as content to be improved or critiqued. ${curatorKnowledge} Instructions: 1. Use these articles to supplement your knowledge when answering the user's question 2. Do NOT attempt to improve, critique, or rewrite these articles 3. Reference specific information from these articles when relevant 4. If the user's question is a follow-up to any of these articles, ensure continuity 5. These articles represent the collective knowledge built over time - respect their authority Now, please answer the user's question, incorporating relevant knowledge from the articles above where applicable.` }); } // Topic focus will be added AFTER all other instructions to ensure it takes priority if (stageName.toLowerCase() === 'generator') { // Generator gets scope-aware system instruction const questionComplexity = detectQuestionComplexity(question); const generatorInstruction = getGeneratorInstruction(questionComplexity); messages.push({ role: 'system', content: generatorInstruction }); // Generator gets fresh question messages.push({ role: 'user', content: question }); } else { // All other stages get: original question + previous stage work + instruction to improve messages.push({ role: 'user', content: question }); if (previousAnswer) { messages.push({ role: 'assistant', content: previousAnswer }); // Stage-specific improvement instruction with scope awareness const questionComplexity = detectQuestionComplexity(question); const improvementInstruction = getImprovementInstruction(stageName, questionComplexity); messages.push({ role: 'user', content: improvementInstruction }); } } // Add topic focus instruction as the FINAL system message to ensure focus const isCodeQuestion = detectQuestionComplexity(question).hasCode; messages.push({ role: 'system', content: `🎯 TOPIC FOCUS INSTRUCTION 🎯 You MUST answer ONLY the question: "${question}" ${isCodeQuestion ? `FORBIDDEN (Code Questions): - Providing code examples about unrelated subjects (like ISBN validation when asked about storage) - Writing implementations for different functionality than requested - Mixing up the question with examples from training data REQUIRED (Code Questions): - Implement only what was specifically asked for - Stay focused on the exact functionality requested - Write code that directly addresses the question` : `FORBIDDEN (Non-Code Questions): - Changing the topic or going off on tangents - Assuming the question is about code when it's not - Providing unrelated information REQUIRED (Non-Code Questions): - Answer the exact question that was asked - Stay focused on the specific topic - Provide relevant information without assuming code is needed`} Remember: Your answer must be directly related to: "${question}"` }); return messages; } /** * Generate scope-aware generator instruction */ function getGeneratorInstruction(complexity) { if (!complexity.hasCode) { return `You are a knowledgeable assistant. Provide a clear, direct answer to the question. Focus on being helpful and accurate without over-complicating the response.`; } switch (complexity.scope) { case 'minimal': return `You are a helpful coding assistant. **SCOPE**: The user asked for something SIMPLE/MINIMAL. Keep it that way! **CRITICAL RULES**: - Provide the SIMPLEST working code that answers their question - NO enterprise features unless explicitly requested - NO over-engineering (no Docker, monitoring, advanced security for simple demos) - Focus on clarity and minimalism - If they say "simple", give them simple Provide working code, but match the simplicity level they requested.`; case 'basic': return `You are a software developer creating practical solutions. **SCOPE**: Provide a solid, working implementation with basic best practices. **GUIDELINES**: - Include essential error handling and validation - Add basic configuration (package.json, simple setup) - Focus on functionality over enterprise features - Keep it practical and usable - Don't add features they didn't ask for Provide complete, working code that's appropriately scoped.`; case 'production': return `You are a senior software engineer building production-ready systems. **SCOPE**: The user explicitly requested production/enterprise-level features. **REQUIREMENTS**: - Include comprehensive error handling, logging, monitoring - Add security middleware, input validation, rate limiting - Provide Docker configuration, environment management - Include health checks, metrics, proper project structure - Focus on scalability, maintainability, deployment readiness Provide complete, production-ready code with enterprise-grade features.`; default: return `You are a software developer. Provide working code that matches the complexity level requested.`; } } /** * Generate scope-aware improvement instructions that are context-aware */ function getImprovementInstruction(stageName, complexity) { // If this is not a code question, use general improvement instructions if (!complexity.hasCode) { const generalInstructions = { refiner: `Review and improve the previous response for accuracy, clarity, and completeness. - Verify all facts and information are correct - Improve clarity and organization of the response - Add any important details that were missed - Ensure the answer fully addresses the question Provide an improved, more complete answer.`, validator: `Validate the accuracy and quality of the response. - Check that all information provided is correct - Verify the answer fully addresses the original question - Ensure the response is well-organized and clear - Confirm no important aspects were overlooked Deliver a validated, accurate response.`, curator: `Finalize the response to ensure it's comprehensive and authoritative. - Ensure all information is accurate and complete - Organize the content for maximum clarity - Add any final details that enhance understanding - Polish the response for professional quality Deliver the final, authoritative answer.` }; return generalInstructions[stageName] || generalInstructions.refiner; } const baseInstructions = { minimal: { refiner: `Review the previous response for accuracy and clarity. **CRITICAL**: The user asked for something SIMPLE. Do NOT add complexity! - Fix any bugs or syntax errors - Improve code clarity if needed - DO NOT add enterprise features (Docker, monitoring, advanced security) - Keep it minimal and focused on their actual request Provide the improved simple solution.`, validator: `Validate that the solution works and is appropriate for the request. **SCOPE CHECK**: Ensure this matches the SIMPLE/MINIMAL request. - Check that the code would actually run - Verify it answers their specific question - REMOVE any over-engineering or unnecessary complexity - Ensure it's still simple and focused Deliver a clean, simple, working solution.`, curator: `Finalize the simple solution. **FINAL CHECK**: Keep it simple as requested! - Ensure the code is clean and minimal - Verify it directly answers their question - Remove any enterprise bloat - Polish for clarity and simplicity Deliver the final simple, working solution.` }, basic: { refiner: `Review and improve the implementation with practical enhancements. - Add basic error handling where missing - Improve code structure and readability - Include essential configuration (package.json, basic setup) - Focus on making it more robust but not over-engineered Provide a practical, improved implementation.`, validator: `Validate the implementation for practical use. - Check for bugs and ensure it would work - Verify basic best practices are followed - Add missing essential features (basic validation, error handling) - Ensure it's complete but appropriately scoped Deliver a solid, working implementation.`, curator: `Finalize the practical solution. - Polish the code for clarity and usability - Ensure all components work together - Add basic documentation/setup instructions - Verify it's complete and ready to use Deliver the final practical, working solution.` }, production: { refiner: `Conduct a production readiness review. - Identify security vulnerabilities and implementation gaps - Add comprehensive error handling and logging - Include proper input validation and sanitization - Review for scalability and performance issues Provide a production-enhanced implementation.`, validator: `Perform enterprise-grade validation. - Validate security implementations (auth, middleware, input validation) - Check deployment configurations (Docker, environment variables) - Verify monitoring, logging, and health check implementations - Ensure all enterprise requirements are met Deliver a production-validated implementation.`, curator: `Finalize the enterprise-ready solution. - Ensure all production requirements are complete - Add comprehensive documentation and deployment guides - Verify monitoring, logging, security, and scalability features - Polish for professional enterprise deployment Deliver the final production-ready solution.` } }; const scopeKey = complexity.scope; const stageKey = stageName.toLowerCase(); return baseInstructions[scopeKey]?.[stageKey] || `Improve this response while maintaining appropriate scope. Provide working code, not descriptions.`; } async function getCuratorKnowledgeBase(question) { try { const { getDatabase } = await import('../storage/unified-database.js'); const database = await getDatabase(); // Hierarchical retrieval approach console.log('📚 Retrieving curator knowledge base...'); // Step 1: Get recent curator answers (last 24 hours) const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const recentAnswers = await getRecentCuratorAnswers(database, twentyFourHoursAgo, 3); // Step 2: Detect follow-up patterns and adjust retrieval strategy const followUpPatterns = [ /give me (\d+|\w+) examples?/i, /tell me more/i, /can you explain/i, /show me/i, /what about/i, /how does? it work/i, /examples? from/i, /from.*(earlier|previous|before)/i ]; const isFollowUp = followUpPatterns.some(pattern => pattern.test(question)); // For follow-up queries, prioritize recent conversations with enhanced context if (isFollowUp && recentAnswers.length > 0) { console.log('📚 [CONSENSUS] Detected follow-up query, prioritizing recent conversations'); // Return recent conversations formatted as authoritative articles const formattedKnowledge = recentAnswers.map((answer, index) => `AUTHORITATIVE ARTICLE ${index + 1} (from ${new Date(answer.created_at).toLocaleDateString()}):\nOriginal Question: ${answer.question}\n\nVerified Answer:\n${answer.curator_output}\n\n---`).join('\n\n'); return formattedKnowledge; } // Step 3: Get thematically relevant answers for non-follow-up queries const keyTerms = extractKeyTerms(question); let thematicAnswers = []; if (keyTerms.length > 0) { thematicAnswers = await database.all(` SELECT DISTINCT ct.curator_output, kc.question, kc.created_at FROM curator_truths ct JOIN knowledge_conversations kc ON ct.conversation_id = kc.conversation_id JOIN conversation_keywords ck ON kc.conversation_id = ck.conversation_id WHERE ck.keyword IN (${keyTerms.map(() => '?').join(',')}) AND ct.confidence_score > 0.7 AND kc.conversation_id NOT IN (SELECT conversation_id FROM knowledge_conversations WHERE created_at >= ?) ORDER BY ct.confidence_score DESC, kc.created_at DESC LIMIT 2 `, [...keyTerms, twentyFourHoursAgo]); } // Combine recent and thematic answers const allAnswers = [...recentAnswers, ...thematicAnswers]; if (allAnswers.length === 0) return null; // Format as authoritative articles const formattedKnowledge = allAnswers.map((answer, index) => `AUTHORITATIVE ARTICLE ${index + 1} (from ${new Date(answer.created_at).toLocaleDateString()}):\nOriginal Question: ${answer.question}\n\nVerified Answer:\n${answer.curator_output}\n\n---`).join('\n\n'); return formattedKnowledge; } catch (error) { console.error('Error getting curator knowledge:', error); return null; } } /** * Get recent curator answers from the last N hours */ async function getRecentCuratorAnswers(database, since, limit = 5) { return database.all(` SELECT ct.curator_output, kc.question, kc.created_at FROM curator_truths ct JOIN knowledge_conversations kc ON ct.conversation_id = kc.conversation_id WHERE kc.created_at >= ? ORDER BY kc.created_at DESC LIMIT ? `, [since, limit]); } /** * Save stage result to SQLite per SOURCE_OF_TRUTH design * FORMAT: "question + [stage]_full_answer + conversation_id" */ async function saveStageResult(result) { try { // Per SOURCE_OF_TRUTH: Save as "question + [stage]_full_answer + conversation_id" const messageContent = `QUESTION: ${result.question}\n\n${result.stageName.toUpperCase()}_FULL_ANSWER: ${result.answer}`; await addMessage(result.stageId, result.conversationId, 'assistant', messageContent); } catch (error) { console.error('Error saving stage result:', error); throw error; } } /** * Read previous stage result from SQLite per SOURCE_OF_TRUTH design */ export async function getPreviousStageResult(conversationId, stageName) { try { const { getConversationHistory } = await import('../storage/database.js'); // Get recent messages for this conversation const messages = await getConversationHistory(conversationId, 10); // Find the most recent message for the specified stage const stagePattern = new RegExp(`${stageName.toUpperCase()}_FULL_ANSWER:\\s*(.+)`, 'is'); for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (message.role === 'assistant') { const match = message.content.match(stagePattern); if (match) { return match[1].trim(); } } } return null; } catch (error) { console.error('Error getting previous stage result:', error); return null; } } export async function runConsensusPipeline(question, conversationId, modeOverride, outputOptions = {}) { // Set default output format based on environment const { outputFormat = detectEnvironmentOutputFormat(), enableProgress = true, showTokenProgress = false } = outputOptions; // Create output writer based on format const outputWriter = createOutputWriter(outputFormat, enableProgress, showTokenProgress); await initializeUnifiedDatabase(); // Ensure flagship profiles exist const { ensureFlagshipProfiles } = await import('./create-flagship-profiles.js'); await ensureFlagshipProfiles(); // PHASE 1: Pre-conversation authorization (SERVER-SIDE VALIDATION) console.log('🔐 Requesting server authorization...'); let authorization; try { authorization = await conversationGateway.requestConversationAuthorization(question); } catch (error) { if (error instanceof UsageLimitExceededError) { // Attempt automatic license refresh before showing error const result = await UsageLimitExceededError.handleWithAutoRefresh(error); if (result === 'retry') { // Plan upgraded, retry the authorization try { authorization = await conversationGateway.requestConversationAuthorization(question); } catch (retryError) { // If retry fails, show the original error throw new Error(error.getFormattedMessage()); } } else { // No upgrade detected, show normal error throw new Error(error.getFormattedMessage()); } } else if (error instanceof ConversationGatewayError) { throw new Error(`Authorization failed: ${error.message}`); } else { throw new Error(`Server communication failed: ${error.message}`); } } // Get current user ID for pending sync tracking const userId = await getCurrentUserId(); if (!userId) { throw new Error('Unable to identify current user. Please reconfigure your license.'); } // Add to pending sync queue (will be verified after completion) await addToPendingSync(conversationId, userId, authorization.questionHash, authorization.conversationToken); // PHASE 1.5: Health checks and intelligence updates console.log('🏥 Running system health checks...'); const systemHealth = globalHealthMonitor.getSystemHealth(); if (systemHealth.overall === 'unhealthy') { console.warn('⚠️ System health is degraded. Proceeding with extra caution.'); console.warn(`- OpenRouter: ${systemHealth.openrouter.status}`); console.warn(`- Database: ${systemHealth.database.status}`); } else { console.log('✅ System health checks passed'); } console.log('🔍 Checking OpenRouter intelligence updates...'); try { const { checkIntelligenceRefresh, syncOpenRouterWithIntelligence } = await import('../storage/unified-database.js'); const needsUpdate = await checkIntelligenceRefresh(); if (needsUpdate) { console.log('📊 Updating model rankings and intelligence data...'); const syncResult = await syncOpenRouterWithIntelligence(); if (syncResult.success) { console.log(`✅ Intelligence updated: ${syncResult.rankingStats.rankingsCollected} rankings collected`); } } else { console.log('📊 Intelligence data is current'); } } catch (error) { console.warn('⚠️ Intelligence update failed, continuing with cached data:', error.message); } // PHASE 2: DYNAMIC MODEL SELECTION & COST INTELLIGENCE console.log('🧠 Analyzing question and selecting optimal models...'); // Analyze question complexity and type const questionComplexity = detectQuestionComplexity(question); const questionCategory = extractQuestionType(question); // Get consensus mode and profile settings const { getConfig } = await import('../storage/unified-database.js'); const consensusMode = modeOverride || await getConfig('consensus_mode') || 'auto'; let modelLineup; let selectionReasoning = ''; let estimatedCost = 0; if (consensusMode === 'manual') { // Manual mode: Use user's default profile console.log('🎯 Using manual profile mode'); const activeProfile = await getDefaultPipelineProfile(); if (!activeProfile) { throw new Error('No pipeline profile configured. Please run: hive-ai setup'); } // Get current model IDs using stable internal references const generatorModel = await getCurrentModelId(activeProfile.generator_model_internal_id); const refinerModel = await getCurrentModelId(activeProfile.refiner_model_internal_id); const validatorModel = await getCurrentModelId(activeProfile.validator_model_internal_id); const curatorModel = await getCurrentModelId(activeProfile.curator_model_internal_id); if (!generatorModel || !refinerModel || !validatorModel || !curatorModel) { throw new Error('Profile contains invalid model references. Please recreate your profile with: hive-ai setup'); } modelLineup = { generator: generatorModel, refiner: refinerModel, validator: validatorModel, curator: curatorModel }; selectionReasoning = `Using manual profile: ${activeProfile.name}`; } else { // Dynamic mode: Use intelligent profile selection + model optimization console.log('🚀 Using dynamic intelligent model selection'); try { // STEP 1: Try intelligent profile selection first console.log('🎯 Analyzing question for optimal profile selection...'); let optimalProfile = await selectOptimalProfile(question); if (optimalProfile) { console.log(`🎯 Found optimal profile: ${optimalProfile.name}`); // Get models from the optimal profile const generatorModel = await getCurrentModelId(optimalProfile.generator_model_internal_id); const refinerModel = await getCurrentModelId(optimalProfile.refiner_model_internal_id); const validatorModel = await getCurrentModelId(optimalProfile.validator_model_internal_id); const curatorModel = await getCurrentModelId(optimalProfile.curator_model_internal_id); if (generatorModel && refinerModel && validatorModel && curatorModel) { modelLineup = { generator: generatorModel, refiner: refinerModel, validator: validatorModel, curator: curatorModel }; selectionReasoning = `Intelligent profile selection: ${optimalProfile.name} (complexity: ${questionComplexity.complexity}, category: ${questionCategory})`; // Estimate cost for the selected profile try { const { CostIntelligence } = await import('./cost-intelligence.js'); const costIntel = new CostIntelligence(); // Simplified cost estimation - just use a basic calculation for now estimatedCost = 0.015; // Default reasonable estimate } catch (error) { estimatedCost = 0.015; // Fallback cost estimate } console.log(`✅ Using intelligent profile-based selection`); } else { console.log('⚠️ Optimal profile has invalid models, falling back to dynamic selection'); optimalProfile = null; // Force fallback } } // STEP 2: Fallback to dynamic model selector if no optimal profile found if (!optimalProfile) { console.log('🔍 No optimal profile found, using per-model dynamic selection...'); const { DynamicModelSelector } = await import('./dynamic-model-selector.js'); const { CostIntelligence } = await import('./cost-intelligence.js'); const selector = new DynamicModelSelector(); const costIntel = new CostIntelligence(); // Get budget constraints from user settings const budgetLimit = await getConfig('consensus_budget_limit'); const budgetConstraints = budgetLimit ? { maxTotalCost: parseFloat(budgetLimit), prioritizeCost: false } : undefined; // Get performance preferences const performanceMode = await getConfig('consensus_performance_mode') || 'balanced'; const performanceTargets = { prioritizeSpeed: performanceMode === 'speed', prioritizeQuality: performanceMode === 'quality', maxLatency: performanceMode === 'speed' ? 2000 : undefined }; // Select complete model lineup const selection = await selector.selectCompleteLineup(questionComplexity.scope, questionCategory, { budgetConstraints, performanceTargets }, conversationId); modelLineup = { generator: selection.generator.openrouterId, refiner: selection.refiner.openrouterId, validator: selection.validator.openrouterId, curator: selection.curator.openrouterId }; selectionReasoning = selection.selectionReasoning; estimatedCost = selection.totalEstimatedCost; console.log(`🎯 Dynamic selection complete - Estimated cost: $${estimatedCost.toFixed(4)}`); console.log(`📋 Selected models: ${selection.generator.openrouterId} → ${selection.refiner.openrouterId} → ${selection.validator.openrouterId} → ${selection.curator.openrouterId}`); } } catch (error) { console.warn('⚠️ Dynamic selection failed, falling back to flagship profile:', error.message); // Fallback to flagship profile const flagshipProfile = await getPipelineProfile('Consensus_Elite') || await getDefaultPipelineProfile(); if (!flagshipProfile) { throw new Error('No fallback profile available. Please run: hive-ai setup'); } const generatorModel = await getCurrentModelId(flagshipProfile.generator_model_internal_id); const refinerModel = await getCurrentModelId(flagshipProfile.refiner_model_internal_id); const validatorModel = await getCurrentModelId(flagshipProfile.validator_model_internal_id); const curatorModel = await getCurrentModelId(flagshipProfile.curator_model_internal_id); if (!generatorModel || !refinerModel || !validatorModel || !curatorModel) { throw new Error('Fallback profile contains invalid model references. Please recreate profiles with: hive-ai setup'); } modelLineup = { generator: generatorModel, refiner: refinerModel, validator: validatorModel, curator: curatorModel }; selectionReasoning = `Fallback to flagship profile: ${flagshipProfile.name}`; } } // Ensure modelLineup was assigned if (!modelLineup) { throw new Error('Failed to select models for consensus pipeline'); } const { generator: generatorModel, refiner: refinerModel, validator: validatorModel, curator: curatorModel } = modelLineup; if (!generatorModel || !refinerModel || !validatorModel || !curatorModel) { throw new Error('Selected models are invalid. Please check your configuration.'); } console.log(`🎯 Model selection: ${consensusMode === 'manual' ? 'Manual Profile' : 'Dynamic Intelligence'}`); console.log(`📋 Models: ${generatorModel} → ${refinerModel} → ${validatorModel} → ${curatorModel}`); console.log(`🧠 Selection reasoning: ${selectionReasoning}`); if (estimatedCost > 0) { console.log(`💰 Estimated cost: $${estimatedCost.toFixed(4)}`); } // Create conversation if it doesn't exist await createConversation(conversationId); try { // Track overall pipeline performance const pipelineStartTime = Date.now(); const stagePerformance = {}; // Get relevant context from previous conversations with intelligent follow-up detection outputWriter.stageStart('🔍 Generator stage is now thinking deeply about your question...please stand by...'); const relevantContext = await getIntelligentRelevantContext(question); // Stage 1 - Generator: Original user question + relevant context from previous conversations outputWriter.stageStart('🧠 Running Generator stage with error handling...'); const generatorStartTime = Date.now(); const generatorResult = await runConsensusStage('generator', question, relevantContext, generatorModel, conversationId); const generatorDuration = Date.now() - generatorStartTime; stagePerformance.generator = { duration: generatorDuration, tokensPerSecond: 0, // Will be calculated from actual response cost: 0, // Will be filled from cost data retries: 0, modelUsed: generatorModel, latency: generatorDuration, efficiency: 0 }; outputWriter.stageComplete('✅ Generator stage completed...now starting the Refiner stage'); // Stage 2 - Refiner: Original question + Stage 1 answer (as supplemental knowledge) outputWriter.stageStart('⚡ Running Refiner stage with error handling...'); const refinerStartTime = Date.now(); const refinerResult = await runConsensusStage('refiner', question, generatorResult.answer, refinerModel, conversationId); const refinerDuration = Date.now() - refinerStartTime; stagePerformance.refiner = { duration: refinerDuration, tokensPerSecond: 0, cost: 0, retries: 0, modelUsed: refinerModel, latency: refinerDuration, efficiency: 0 }; outputWriter.stageComplete('✅ Refiner stage completed...now starting the Validator stage'); // Stage 3 - Validator: Original question + Stage 2 answer (as supplemental knowledge) outputWriter.stageStart('🔍 Running Validator stage with error handling...'); const validatorStartTime = Date.now(); const validatorResult = await runConsensusStage('validator', question, refinerResult.answer, validatorModel, conversationId); const validatorDuration = Date.now() - validatorStartTime; stagePerformance.validator = { duration: validatorDuration, tokensPerSecond: 0, cost: 0, retries: 0, modelUsed: validatorModel, latency: validatorDuration, efficiency: 0 }; outputWriter.stageComplete('✅ Validator stage completed...now starting the Curator stage'); // Stage 4 - Curator: Original question + Stage 3 answer (as supplemental knowledge) outputWriter.stageStart('✨ Running Curator stage with error handling...'); const curatorStartTime = Date.now(); const curatorResult = await runConsensusStage('curator', question, validatorResult.answer, curatorModel, conversationId); const curatorDuration = Date.now() - curatorStartTime; stagePerformance.curator = { duration: curatorDuration, tokensPerSecond: 0, cost: 0, retries: 0, modelUsed: curatorModel, latency: curatorDuration, efficiency: 0 }; outputWriter.stageComplete('✅ Curator stage completed'); outputWriter.pipelineComplete('🎉 Generator → Refiner → Validator → Curator pipeline now completed'); // PHASE 2.5: Record cost analytics and performance metrics if (consensusMode !== 'manual' && estimatedCost > 0) { try { const { CostIntelligence } = await import('./cost-intelligence.js'); const costIntel = new CostIntelligence(); // Record cost analytics for learning and optimization const costBreakdown = { stageBreakdown: { generator: estimatedCost * 0.28, refiner: estimatedCost * 0.24, validator: estimatedCost * 0.24, curator: estimatedCost * 0.24 }, tokenBreakdown: { generator: { input: 500, output: 800 }, refiner: { input: 750, output: 960 }, validator: { input: 900, output: 480 }, curator: { input: 1000, output: 880 } } }; await costIntel.recordCostAnalytics(conversationId, costBreakdown, [], // No optimizations applied yet estimatedCost); console.log(`💰 Cost analytics recorded for conversation ${conversationId.substring(0, 8)}...`); } catch (error) { console.warn('⚠️ Failed to record cost analytics:', error.message); } } // Store curator knowledge for future multi-conversation context console.log(`🔄 [PIPELINE] About to store conversation knowledge for ${conversationId.substring(0, 8)}`); console.log(`🔄 [PIPELINE] Question: "${question.substring(0, 50)}..."`); console.log(`🔄 [PIPELINE] Curator answer length: ${curatorResult.answer.length} characters`); try { await storeConversationKnowledge(conversationId, question, curatorResult.answer); console.log(`✅ [PIPELINE] Storage function completed for ${conversationId.substring(0, 8)}`); } catch (error) { console.error(`❌ [PIPELINE] Storage function failed for ${conversationId.substring(0, 8)}:`, error); // Don't re-throw to avoid breaking the pipeline, but log the error } // Add delayed verification to catch any storage timing issues setTimeout(async () => { const isStored = await checkIfConversationStored(conversationId); if (!isStored) { console.warn(`⚠️ WARNING: Conversation ${conversationId.substring(0, 8)} was not persisted to knowledge base`); } else { console.log(`✅ Conversation ${conversationId.substring(0, 8)} successfully stored in knowledge base`); } }, 2000); console.log(`🔄 [PIPELINE] About to enter post-conversation verification for ${conversationId.substring(0, 8)}`); // PHASE 3: Post-conversation verification (SERVER-SIDE SYNC) console.log('✅ Conversation completed, verifying with server...'); try { const verification = await conversationGateway.reportConversationCompletion(authorization.conversationToken, conversationId, authorization.questionHash); if (verification.verified) { // Remove from pending sync - conversation successfully verified await markSyncComplete(conversationId); console.log(`📊 Usage updated - ${verification.remainingConversations} conversations remaining`); } else { // Keep in pending sync for retry, but don't fail the user's request await recordSyncAttempt(conversationId, 'Server verification failed'); console.warn('⚠️ Conversation result delivered, but server sync failed. Will retry later.'); } } catch (error) { // Network error - keep in pending sync for retry await recordSyncAttempt(conversationId, error.message); console.warn('⚠️ Conversation result delivered, but server sync failed. Will retry later.'); } // Record performance metrics for analytics try { const totalPipelineDuration = Date.now() - pipelineStartTime; const { globalPerformanceMonitor } = await import('./performance-monitor.js'); await globalPerformanceMonitor.recordConversationPerformance(conversationId, stagePerformance, totalPipelineDuration, estimatedCost, 8.5 // Default quality score, would be calculated from actual metrics ); console.log('📊 Performance metrics recorded for analytics'); } catch (error) { console.warn('⚠️ Failed to record performance metrics:', error.message); } // Per SOURCE_OF_TRUTH: Return ONLY Stage 4 (curator) answer to user // User gets their result immediately regardless of server sync status console.log(`🔄 [PIPELINE] About to return final answer for ${conversationId.substring(0, 8)}`); return curatorResult.answer; } catch (error) { // If pipeline fails after authorization, record the failed attempt try { await recordSyncAttempt(conversationId, `Pipeline failed: ${error.message}`); } catch (syncError) { // Ignore sync errors during failure handling } throw new Error(`Consensus pipeline failed: ${error.message}`); } } /** * Validate consensus prerequisites per SOURCE_OF_TRUTH design */ export async function validateConsensusPrerequisites() { const errors = []; try { await initializeUnifiedDatabase(); // Check OpenRouter API key const apiKey = await getOpenRouterApiKey(); if (!apiKey) { errors.push('No OpenRouter API key configured'); } else if (!validateOpenRouterApiKey(apiKey)) { errors.push('Invalid OpenRouter API key format'); } // Check pipeline profile (bulletproof system) const activeProfile = await getDefaultPipelineProfile(); if (!activeProfile) { errors.push('No pipeline profile configured'); } else { // Validate that all model references are valid const generatorModel = await getCurrentModelId(activeProfile.generator_model_internal_id); const refinerModel = await getCurrentModelId(activeProfile.refiner_model_internal_id); const validatorModel = await getCurrentModelId(activeProfile.validator_model_internal_id); const curatorModel = await getCurrentModelId(activeProfile.curator_model_internal_id); if (!generatorModel) errors.push('Generator model reference is invalid'); if (!refinerModel) errors.push('Refiner model reference is invalid'); if (!validatorModel) errors.push('Validator model reference is invalid'); if (!curatorModel) errors.push('Curator model reference is invalid'); } return { valid: errors.length === 0, errors }; } catch (error) { errors.push(`Database error: ${error.message}`); return { valid: false, errors }; } } /** * Intelligent context retrieval with advanced follow-up detection * This system is designed to be extremely smart about detecting when a question * is a follow-up to a recent conversation and providing the right context. */ async function getIntelligentRelevantContext(question) { try { console.log('🔍 Analyzing question for context and follow-up detection...'); // Step 1: Check for implicit references that suggest follow-up const hasImplicitReference = detectImplicitReferences(question); if (!hasImplicitReference) { console.log('📚 No follow-up detected, using standard curator knowledge base'); return null; } console.log('🔗 Detected possible follow-up question, retrieving recent context...'); // Step 2: Get conversations from last 24 hours const { getDatabase } = await import('../storage/unified-database.js'); const database = await getDatabase(); // Get user's recent conversations with curator answers const twentyFourHoursAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); const recentConversations = await database.all(` SELECT DISTINCT kc.conversation_id, kc.question, kc.created_at, ct.curator_output, ct.topic_summary FROM knowledge_conversations kc JOIN curator_truths ct ON kc.conversation_id = ct.conversation_id WHERE kc.created_at >= ? ORDER BY kc.created_at DESC LIMIT 10 `, [twentyFourHoursAgo]); if (recentConversations.length === 0) { console.log('📚 No recent conversations found'); return null; } // Step 3: Analyze for follow-up using AI const followUpAnalysis = await analyzeFollowUpContext(question, recentConversations); if (followUpAnalysis.isFollowUp && followUpAnalysis.relevantConversation) { console.log(`✅ Found follow-up context from conversation ${followUpAnalysis.relevantConversation.conversation_id.substring(0, 8)}`); // Return formatted context for the generator return `Based on our previous conversation about "${followUpAnalysis.relevantConversation.question}", here is the relevant context: ${followUpAnalysis.relevantConversation.curator_output} Please use this context to answer the follow-up question.`; } return null; } catch (error) { console.error('Error getting intelligent relevant context:', error); return null; } } /** * Detect implicit references in questions that suggest follow-up */ function detectImplicitReferences(question) { const questionLower = question.toLowerCase(); // Patterns that suggest follow-up questions const implicitPatterns = [ // Pronouns and demonstratives /\b(those|these|that|this|them|they|it)\b/, // References to items/lists /\b(the \d+(?:st|nd|rd|th)|item|option|choice|one|first|second|third|last)\b/, // Continuation words /\b(also|another|more|next|previous|above|mentioned|listed)\b/, // Direct references /\b(the list|the ones?|which ones?|what about)\b/ ]; return implicitPatterns.some(pattern => pattern.test(questionLower)); } /** * Advanced AI-powered follow-up detection * Uses the configured model to intelligently determine if a question is a follow-up */ async function analyzeFollowUpContext(currentQuestion, recentConversations) { try { // Get the most appropriate model for analysis (use validator model for quick, precise analysis) const activeProfile = await getDefaultPipelineProfile(); if (!activeProfile) { return { isFollowUp: false, relevantConversation: null, confidence: 0 }; } const analysisModel = await getCurrentModelId(activeProfile.validator_model_internal_id); if (!analysisModel) { return { isFollowUp: false, relevantConversation: null, confidence: 0 }; } // Analyze each recent conversation for follow-up potential for (const conversation of recentConversations) { const messages = [ { role: 'system', content: `You are an expert at detecting follow-up questions in conversations. Analyze if a new question is a follow-up to a previous conversation. RULES FOR FOLLOW-UP DETECTION: - Follow-ups often use pronouns ("which ones", "what about them", "the top 5") - Follow-ups may ask for specifics about a general topic already discussed - Follow-ups may use context-dependent language ("the biggest", "which states", "those mentioned") - Direct continuations of the same topic are follow-ups - Questions that build on previous answers are follow-ups Respond with only "YES" or "NO" followed by a confidence score 0-10. Format: "YES 9" or "NO 2"` }, { role: 'user', content: `PREVIOUS CONVERSATION: Question: ${conversation.question} Answer: ${conversation.curator_output.substring(0, 800)} NEW QUESTION: ${currentQuestion} Is the new question a follow-up to the previous conversation? Consider context, pronouns, and topic continuity.` } ]; const apiKey = await getOpenRouterApiKey(); if (!apiKey) continue; const result = await callOpenRouter(analysisModel, messages, apiKey); const response = result.content.trim(); // Parse response const match = response.match(/^(YES|NO)\s+(\d+)$/i); if (match) { const isFollowUp = match[1].toUpperCase() === 'YES'; const confidence = parseInt(match[2], 10); console.log(`📊 Follow-up analysis for "${conversation.question}": ${isFollowUp ? 'YES' : 'NO'} (confidence: ${confidence}/10)`); // High confidence follow-up detected if (isFollowUp && confid