UNPKG

@quantumai/quantum-cli-core

Version:

Quantum CLI Core - Multi-LLM Collaboration System

426 lines 15.8 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { QueryType } from '../types.js'; /** * Default configuration for query history */ export const DEFAULT_HISTORY_CONFIG = { maxQueriesPerUser: 1000, retentionDays: 365, enableSimilarityDetection: true, similarityThreshold: 0.8, enablePatternLearning: true, patternUpdateFrequency: 10, }; /** * Query history manager for user behavior tracking */ export class QueryHistoryManager { config; history = new Map(); // userId -> records patterns = new Map(); // userId -> patterns stats = new Map(); // userId -> stats constructor(config = {}) { this.config = { ...DEFAULT_HISTORY_CONFIG, ...config }; } /** * Records a query in user's history */ recordQuery(record) { const queryRecord = { ...record, id: this.generateId(), timestamp: new Date(), queryHash: this.calculateQueryHash(record.query, record.analysis), }; // Add to history const userHistory = this.history.get(record.userId) || []; userHistory.unshift(queryRecord); // Add to beginning for recency // Enforce retention limits this.enforceRetention(userHistory); this.history.set(record.userId, userHistory); // Update statistics this.updateUserStats(record.userId, queryRecord); // Update patterns if enabled if (this.config.enablePatternLearning && userHistory.length % this.config.patternUpdateFrequency === 0) { this.updatePatterns(record.userId); } return queryRecord.id; } /** * Updates user feedback for a query */ updateFeedback(userId, queryId, rating, feedback, satisfied) { const userHistory = this.history.get(userId); if (!userHistory) return false; const record = userHistory.find(r => r.id === queryId); if (!record) return false; record.userRating = Math.max(1, Math.min(5, rating)); record.userFeedback = feedback; if (satisfied !== undefined) { record.satisfied = satisfied; } // Update statistics this.updateUserStats(userId, record); return true; } /** * Finds similar queries in user's history */ findSimilarQueries(userId, query, analysis, limit = 5) { if (!this.config.enableSimilarityDetection) { return []; } const userHistory = this.history.get(userId); if (!userHistory) return []; const queryHash = this.calculateQueryHash(query, analysis); const similarities = []; for (const record of userHistory) { if (record.queryHash === queryHash) continue; // Skip identical queries const similarity = this.calculateSimilarity(query, analysis, record.query, record.analysis); if (similarity >= this.config.similarityThreshold) { similarities.push({ record, similarity, reasons: this.getSimilarityReasons(analysis, record.analysis), }); } } // Sort by similarity and limit results return similarities .sort((a, b) => b.similarity - a.similarity) .slice(0, limit); } /** * Gets user's query patterns */ getUserPatterns(userId) { return this.patterns.get(userId) || []; } /** * Gets user's query statistics */ getUserStats(userId) { return this.stats.get(userId); } /** * Gets query history for a user */ getHistory(userId, limit) { const history = this.history.get(userId) || []; return limit ? history.slice(0, limit) : history; } /** * Gets queries by type for a user */ getQueriesByType(userId, type, limit) { const history = this.history.get(userId) || []; const filtered = history.filter(r => r.analysis.type === type); return limit ? filtered.slice(0, limit) : filtered; } /** * Gets successful queries with high ratings */ getSuccessfulQueries(userId, minRating = 4) { const history = this.history.get(userId) || []; return history.filter(r => r.userRating && r.userRating >= minRating && r.satisfied); } /** * Cleans up old data based on retention policy */ cleanup() { const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays); for (const [userId, history] of this.history.entries()) { const filtered = history.filter(r => r.timestamp > cutoffDate); if (filtered.length !== history.length) { this.history.set(userId, filtered); // Recalculate stats and patterns after cleanup this.recalculateUserStats(userId, filtered); this.updatePatterns(userId); } } } /** * Exports user data for analysis */ exportUserData(userId) { return { history: this.getHistory(userId), patterns: this.getUserPatterns(userId), stats: this.getUserStats(userId), }; } /** * Calculates query hash for similarity detection */ calculateQueryHash(query, analysis) { const content = [ query.toLowerCase().trim(), analysis.type, analysis.domain || '', analysis.language || '', analysis.frameworks.sort().join(','), Math.round(analysis.complexity * 10).toString(), ].join('|'); return this.simpleHash(content); } /** * Calculates similarity between two queries */ calculateSimilarity(query1, analysis1, query2, analysis2) { let similarity = 0; let totalWeight = 0; // Type similarity (high weight) const typeWeight = 0.3; if (analysis1.type === analysis2.type) { similarity += typeWeight; } totalWeight += typeWeight; // Domain similarity const domainWeight = 0.2; if (analysis1.domain && analysis2.domain && analysis1.domain === analysis2.domain) { similarity += domainWeight; } totalWeight += domainWeight; // Language similarity const languageWeight = 0.15; if (analysis1.language && analysis2.language && analysis1.language === analysis2.language) { similarity += languageWeight; } totalWeight += languageWeight; // Framework similarity const frameworkWeight = 0.15; const commonFrameworks = analysis1.frameworks.filter(f => analysis2.frameworks.includes(f)); if (commonFrameworks.length > 0) { const frameworkSim = commonFrameworks.length / Math.max(analysis1.frameworks.length, analysis2.frameworks.length); similarity += frameworkWeight * frameworkSim; } totalWeight += frameworkWeight; // Keyword similarity const keywordWeight = 0.1; const commonKeywords = analysis1.keywords.filter(k => analysis2.keywords.includes(k)); if (commonKeywords.length > 0) { const keywordSim = commonKeywords.length / Math.max(analysis1.keywords.length, analysis2.keywords.length); similarity += keywordWeight * keywordSim; } totalWeight += keywordWeight; // Complexity similarity const complexityWeight = 0.1; const complexityDiff = Math.abs(analysis1.complexity - analysis2.complexity); const complexitySim = Math.max(0, 1 - complexityDiff); similarity += complexityWeight * complexitySim; totalWeight += complexityWeight; return totalWeight > 0 ? similarity / totalWeight : 0; } /** * Gets reasons for similarity match */ getSimilarityReasons(analysis1, analysis2) { const reasons = []; if (analysis1.type === analysis2.type) { reasons.push(`Same query type: ${analysis1.type}`); } if (analysis1.domain === analysis2.domain && analysis1.domain) { reasons.push(`Same domain: ${analysis1.domain}`); } if (analysis1.language === analysis2.language && analysis1.language) { reasons.push(`Same language: ${analysis1.language}`); } const commonFrameworks = analysis1.frameworks.filter(f => analysis2.frameworks.includes(f)); if (commonFrameworks.length > 0) { reasons.push(`Common frameworks: ${commonFrameworks.join(', ')}`); } const commonKeywords = analysis1.keywords.filter(k => analysis2.keywords.includes(k)); if (commonKeywords.length > 0) { reasons.push(`Common keywords: ${commonKeywords.slice(0, 3).join(', ')}`); } return reasons; } /** * Updates user statistics */ updateUserStats(userId, record) { let stats = this.stats.get(userId); if (!stats) { stats = { userId, totalQueries: 0, averageRating: 0, mostUsedType: QueryType.GENERAL, preferredProviders: {}, averageCost: 0, totalCost: 0, queryTypeCounts: {}, domainCounts: {}, timeOfDayDistribution: {}, dayOfWeekDistribution: {}, lastActivity: record.timestamp, createdAt: record.timestamp, }; } // Update basic stats stats.totalQueries++; stats.totalCost += record.cost; stats.averageCost = stats.totalCost / stats.totalQueries; stats.lastActivity = record.timestamp; // Update type counts stats.queryTypeCounts[record.analysis.type] = (stats.queryTypeCounts[record.analysis.type] || 0) + 1; // Find most used type let maxCount = 0; for (const [type, count] of Object.entries(stats.queryTypeCounts)) { if (count > maxCount) { maxCount = count; stats.mostUsedType = type; } } // Update domain counts if (record.analysis.domain) { stats.domainCounts[record.analysis.domain] = (stats.domainCounts[record.analysis.domain] || 0) + 1; } // Update provider preferences stats.preferredProviders[record.selectedProvider] = (stats.preferredProviders[record.selectedProvider] || 0) + 1; // Update time distributions const hour = record.timestamp.getHours(); const dayOfWeek = record.timestamp.getDay(); stats.timeOfDayDistribution[hour] = (stats.timeOfDayDistribution[hour] || 0) + 1; stats.dayOfWeekDistribution[dayOfWeek] = (stats.dayOfWeekDistribution[dayOfWeek] || 0) + 1; // Update average rating if (record.userRating) { const userHistory = this.history.get(userId) || []; const ratedQueries = userHistory.filter(r => r.userRating); const totalRating = ratedQueries.reduce((sum, r) => sum + (r.userRating || 0), 0); stats.averageRating = totalRating / ratedQueries.length; } this.stats.set(userId, stats); } /** * Recalculates user statistics from scratch */ recalculateUserStats(userId, history) { this.stats.delete(userId); for (const record of history.reverse()) { // Process in chronological order this.updateUserStats(userId, record); } } /** * Updates query patterns for a user */ updatePatterns(userId) { const history = this.history.get(userId) || []; if (history.length < 5) return; // Need minimum queries for pattern detection const patterns = []; const patternMap = new Map(); // Group queries by similar characteristics for (const record of history) { const key = this.getPatternKey(record.analysis); const group = patternMap.get(key) || []; group.push(record); patternMap.set(key, group); } // Create patterns from groups for (const [key, group] of patternMap.entries()) { if (group.length < 2) continue; // Need at least 2 queries for a pattern const analysis = group[0].analysis; const ratings = group.filter(r => r.userRating).map(r => r.userRating); const providers = group.map(r => r.selectedProvider); const providerCounts = new Map(); for (const provider of providers) { providerCounts.set(provider, (providerCounts.get(provider) || 0) + 1); } const preferredProviders = Array.from(providerCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([provider]) => provider); patterns.push({ id: this.simpleHash(key), type: analysis.type, domain: analysis.domain, complexity: analysis.complexity, keywords: analysis.keywords, frameworks: analysis.frameworks, language: analysis.language, queryCount: group.length, averageRating: ratings.length > 0 ? ratings.reduce((a, b) => a + b, 0) / ratings.length : 0, preferredProviders, lastUsed: group[0].timestamp, // Most recent }); } // Sort patterns by usage and recency patterns.sort((a, b) => { const scoreA = a.queryCount * 0.7 + (a.averageRating / 5) * 0.3; const scoreB = b.queryCount * 0.7 + (b.averageRating / 5) * 0.3; return scoreB - scoreA; }); this.patterns.set(userId, patterns.slice(0, 20)); // Keep top 20 patterns } /** * Gets pattern key for grouping */ getPatternKey(analysis) { return [ analysis.type, analysis.domain || 'none', analysis.language || 'none', analysis.frameworks.sort().join(','), Math.round(analysis.complexity * 2) / 2, // Round to nearest 0.5 ].join('|'); } /** * Enforces retention limits */ enforceRetention(history) { // Remove excess queries if (history.length > this.config.maxQueriesPerUser) { history.splice(this.config.maxQueriesPerUser); } // Remove old queries const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - this.config.retentionDays); for (let i = history.length - 1; i >= 0; i--) { if (history[i].timestamp < cutoffDate) { history.splice(i, 1); } } } /** * Generates unique ID */ generateId() { return Date.now().toString(36) + Math.random().toString(36).substr(2); } /** * Simple hash function */ simpleHash(str) { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(36); } /** * Updates configuration */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Gets current configuration */ getConfig() { return { ...this.config }; } } //# sourceMappingURL=query-history.js.map