@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
JavaScript
/**
* 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