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

348 lines 16.2 kB
/** * Enhanced conversation continuity manager for the hive-tools consensus tool * * This provides a sophisticated system for maintaining conversation continuity * with timestamp-based ordering and prioritization of recent conversations. * It works in conjunction with the SQLite-based knowledge retrieval system. * * Features: * - Timestamp-based ordering of conversations and messages * - Importance scoring for messages based on content and role * - Recency prioritization with decay function * - Thematic topic tracking for better context retrieval * - Integration with vector-based semantic search */ import { v4 as uuidv4 } from 'uuid'; import { createChatCompletion } from './provider-client.js'; import * as database from '../../storage/database.js'; // In-memory store of conversations const conversations = []; // Track the latest conversation ID for quick access let latestConversationId = null; /** * Get an appropriate model for conversation analysis from user's configured profile * Uses the validator model since it's typically configured for fast, precise analysis */ async function getConversationAnalysisModel() { try { // Get user's configured default profile from SQLite const { securePipelineConfig } = await import('../../security/secure-pipeline-config.js'); const profiles = await securePipelineConfig.getProfiles(); const defaultProfile = profiles.find(p => p.is_default); if (defaultProfile?.validator?.model) { // Use the validator model - typically fast and precise console.log(`Using user's configured validator model: ${defaultProfile.validator.model}`); return defaultProfile.validator.model; } if (defaultProfile?.generator?.model) { // Fallback to generator model if validator not available console.log(`Using user's configured generator model: ${defaultProfile.generator.model}`); return defaultProfile.generator.model; } // If no profile configured, get any available OpenRouter model from bulletproof database try { const { getDatabase } = await import('../../storage/unified-database.js'); const database = await getDatabase(); const anyModel = await database.get(` SELECT openrouter_id FROM openrouter_models WHERE is_active = 1 LIMIT 1 `); if (anyModel) { console.log(`Using first available OpenRouter model: ${anyModel.openrouter_id}`); return anyModel.openrouter_id; } } catch (error) { console.error('Error loading models from database:', error); } throw new Error('No models available. Please run: hive-ai models update && hive-ai setup'); } catch (error) { console.warn('Could not load user profile for conversation analysis:', error.message); throw new Error('No model configured. Please run: hive-ai setup'); } } /** * Update recency scores for all conversations * This applies a time-decay function to prioritize recent conversations */ function updateRecencyScores() { const now = Date.now(); // Update recency scores for all conversations for (const conversation of conversations) { const lastUpdated = new Date(conversation.lastUpdated).getTime(); const hoursSinceUpdate = (now - lastUpdated) / (60 * 60 * 1000); // Exponential decay function: score = 10 * e^(-hoursSinceUpdate/24) // This gives a score of 10 for just updated conversations, // ~7.5 after 6 hours, ~5 after 12 hours, ~3.7 after 24 hours, etc. conversation.recencyScore = 10 * Math.exp(-hoursSinceUpdate / 24); } } /** * Create a new conversation with enhanced metadata */ export function createConversation() { const id = uuidv4(); const now = new Date().toISOString(); const conversation = { id, messages: [], lastUpdated: now, createdAt: now, topics: [], isActive: true, // Mark as active by default recencyScore: 10 // Maximum recency score for new conversations }; // Add a system message to track creation time conversation.messages.push({ role: 'system', content: `Conversation started at ${now}`, timestamp: now, importance: 0 // Low importance for system messages }); conversations.push(conversation); latestConversationId = id; // Update recency scores for all conversations updateRecencyScores(); console.log(`[${new Date().toLocaleTimeString()}] Created new in-memory conversation: ${id}`); return id; } /** * Get a conversation by ID */ export function getConversation(id) { return conversations.find(c => c.id === id); } /** * Add a message to a conversation with enhanced metadata */ export function addMessage(conversationId, role, content, importance) { const conversation = getConversation(conversationId); if (!conversation) { console.error(`Conversation not found: ${conversationId}`); return; } const now = new Date(); const timestamp = now.toISOString(); // Calculate default importance based on role, content length, and presence of questions // This helps prioritize more significant messages in the conversation let calculatedImportance = importance; if (calculatedImportance === undefined) { // User messages are generally more important as they contain the queries const roleImportance = role === 'user' ? 2 : role === 'assistant' ? 1 : 0; // Longer messages might contain more context const lengthImportance = Math.min(2, Math.floor(content.length / 500)); // Questions are typically more important for context const hasQuestion = content.includes('?'); const questionImportance = hasQuestion ? 1 : 0; calculatedImportance = roleImportance + lengthImportance + questionImportance; } // Add the message with timestamp and importance const newMessage = { role, content, timestamp, importance: calculatedImportance }; conversation.messages.push(newMessage); // Update conversation metadata conversation.lastUpdated = timestamp; conversation.isActive = true; // Mark this conversation as active // Track the last user query for better context retrieval if (role === 'user') { conversation.lastUserQuery = content; conversation.lastUserQueryTime = timestamp; } // Set as latest conversation latestConversationId = conversationId; // Update recency scores for all conversations updateRecencyScores(); // Log with timestamp for debugging console.log(`[${now.toLocaleTimeString()}] Added ${role} message to conversation ${conversationId.substring(0, 8)} (importance: ${calculatedImportance})`); } /** * Enhanced algorithm to find the most semantically similar conversation * This is a fallback for the SQLite-based system and works in coordination with it */ export async function findRelevantConversation(query) { if (conversations.length === 0) { return null; } try { // Hierarchical approach to conversation retrieval: // 1. First check for very recent conversations (within the last hour) // 2. Then check for thematic matches using topic detection // 3. Finally fall back to semantic similarity // If we have a conversation that was updated very recently, use that const FIFTEEN_MINUTES = 15 * 60 * 1000; const ONE_HOUR = 60 * 60 * 1000; const now = Date.now(); const veryRecentTime = new Date(now - FIFTEEN_MINUTES).toISOString(); // Find conversations updated within the last 15 minutes - these are very recent and likely to be continuations const veryRecentConversations = conversations.filter(c => c.lastUpdated >= veryRecentTime); if (veryRecentConversations.length > 0) { console.log(`Found ${veryRecentConversations.length} conversations updated within the last 15 minutes`); // If there's only one very recent conversation, use that for continuity if (veryRecentConversations.length === 1) { console.log(`Using the only very recent conversation: ${veryRecentConversations[0].id}`); return veryRecentConversations[0].id; } // If multiple very recent conversations, find the most relevant one const veryRecentMatch = await findMostRelevantByContent(query, veryRecentConversations); if (veryRecentMatch) { return veryRecentMatch; } } // Find conversations updated within the last hour const recentTime = new Date(now - ONE_HOUR).toISOString(); const recentConversations = conversations.filter(c => c.lastUpdated >= recentTime); if (recentConversations.length > 0) { console.log(`Found ${recentConversations.length} conversations updated within the last hour`); // If there's only one recent conversation, use that for continuity if (recentConversations.length === 1) { console.log(`Using the only recent conversation: ${recentConversations[0].id}`); return recentConversations[0].id; } // If there are multiple recent conversations, find the most relevant one const recentMatch = await findMostRelevantByContent(query, recentConversations); if (recentMatch) { return recentMatch; } } // Check for thematic matches using topic detection via the database try { // Use the database.getConversationsByTopic function instead of findConversationByTopic // This function returns conversations that match the given topic const thematicConversations = await database.getConversationsByTopic(query); if (thematicConversations && thematicConversations.length > 0) { const thematicMatch = thematicConversations[0].id; console.log(`Found thematic match in database: ${thematicMatch}`); return thematicMatch; } } catch (error) { console.error('Error finding conversation by topic:', error); } // If no recent or thematic matches, try to find a relevant one from all conversations console.log('No recent or thematic matches found, checking all conversations for semantic relevance'); return findMostRelevantByContent(query, conversations.sort((a, b) => { // Sort by recency score (descending) return (b.recencyScore ?? 0) - (a.recencyScore ?? 0); })); } catch (error) { console.error('Error finding relevant conversation:', error); return null; } } /** * Helper function to find the most relevant conversation based on content similarity */ async function findMostRelevantByContent(query, candidateConversations) { // If no conversations to check, return null if (candidateConversations.length === 0) { return null; } try { let bestMatchId = null; let bestMatchScore = 0; // Check each conversation for relevance for (const conversation of candidateConversations) { // Get the most recent messages (up to 5) to check for continuity const recentMessages = conversation.messages .sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()) .slice(0, 5) .map(m => `${m.role}: ${m.content}`) .join('\n'); // Check semantic similarity using the user's configured model const analysisModel = await getConversationAnalysisModel(); const similarityResult = await createChatCompletion({ model: analysisModel, // Use model from user's configured profile messages: [ { role: "system", content: "You are analyzing conversation continuity. Your task is to determine if a new message is a follow-up or related to previous conversation topics. Rate the relevance on a scale of 0-10, where 0 is completely unrelated and 10 is directly related. Respond with ONLY the number." }, { role: "user", content: `Previous conversation:\n${recentMessages}\n\nPossible follow-up message: ${query}\n\nOn a scale of 0-10, how relevant is this new message to the previous conversation? Respond with ONLY a number.` } ], temperature: 0.1, // Lower temperature for more consistent responses max_tokens: 10 }); // Extract the relevance score (0-10) const relevanceText = similarityResult.content?.trim() || '0'; const relevanceScore = parseInt(relevanceText, 10) || 0; console.log(`Conversation ${conversation.id.substring(0, 8)} relevance score: ${relevanceScore}/10`); // Calculate recency bonus (0-2 points) const lastUpdated = new Date(conversation.lastUpdated).getTime(); const now = Date.now(); const hoursSinceUpdate = (now - lastUpdated) / (60 * 60 * 1000); const recencyBonus = Math.max(0, 2 - (hoursSinceUpdate / 12)); // 2 points if very recent, decreasing over 24 hours // Calculate final score with recency bonus const finalScore = relevanceScore + recencyBonus; console.log(`Conversation ${conversation.id.substring(0, 8)} final score: ${finalScore.toFixed(1)} (relevance: ${relevanceScore}, recency bonus: ${recencyBonus.toFixed(1)})`); // Update best match if this is better if (finalScore > bestMatchScore) { bestMatchScore = finalScore; bestMatchId = conversation.id; } } // Use the best match if it's reasonably relevant (score > 5) if (bestMatchScore > 5) { console.log(`Using most relevant conversation: ${bestMatchId} with score ${bestMatchScore.toFixed(1)}`); return bestMatchId; } // If no good match found but we have a latest conversation, use that if (latestConversationId) { console.log(`No strong match found, using latest conversation: ${latestConversationId}`); return latestConversationId; } // No relevant conversation found return null; } catch (error) { console.error('Error finding most relevant conversation by content:', error); return latestConversationId; // Fall back to latest conversation } } /** * Get the most recent conversation */ export function getLatestConversation() { if (!latestConversationId) { return undefined; } return getConversation(latestConversationId); } /** * Get the conversations sorted by recency score */ export function getConversationsByRecency(limit = 10) { return [...conversations] .sort((a, b) => (b.recencyScore || 0) - (a.recencyScore || 0)) .slice(0, limit); } /** * Get or create a conversation */ export function getOrCreateConversation(id) { if (id) { const conversation = getConversation(id); if (conversation) { // Update the conversation's activity status conversation.isActive = true; const now = new Date(); conversation.lastUpdated = now.toISOString(); // Update recency score directly const lastUpdated = new Date(conversation.lastUpdated).getTime(); const hoursSinceUpdate = (now.getTime() - lastUpdated) / (60 * 60 * 1000); conversation.recencyScore = 10 * Math.exp(-hoursSinceUpdate / 24); return id; } } return createConversation(); } //# sourceMappingURL=conversation-memory.js.map