@quantumai/quantum-cli-core
Version:
Quantum CLI Core - Multi-LLM Collaboration System
426 lines • 15.8 kB
JavaScript
/**
* @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