quantum-cli-core
Version:
Quantum CLI Core - Multi-LLM Collaboration System
249 lines • 8.58 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { promises as fs } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
export class FeedbackService {
feedbackDir;
feedbackFile;
statsFile;
usageFile;
constructor() {
this.feedbackDir = join(homedir(), '.quantum');
this.feedbackFile = join(this.feedbackDir, 'feedback.json');
this.statsFile = join(this.feedbackDir, 'feedback-stats.json');
this.usageFile = join(this.feedbackDir, 'usage-stats.json');
}
async initialize() {
try {
await fs.mkdir(this.feedbackDir, { recursive: true });
// Initialize files if they don't exist
await this.ensureFileExists(this.feedbackFile, '[]');
await this.ensureFileExists(this.statsFile, JSON.stringify(this.getDefaultStats()));
await this.ensureFileExists(this.usageFile, JSON.stringify(this.getDefaultUsageStats()));
}
catch (error) {
console.warn('Failed to initialize feedback service:', error);
}
}
async ensureFileExists(filePath, defaultContent) {
try {
await fs.access(filePath);
}
catch {
await fs.writeFile(filePath, defaultContent, 'utf8');
}
}
async saveFeedback(feedback) {
try {
const existingFeedback = await this.loadFeedback();
existingFeedback.push(feedback);
// Keep only last 1000 feedback entries to prevent unbounded growth
const trimmedFeedback = existingFeedback.slice(-1000);
await fs.writeFile(this.feedbackFile, JSON.stringify(trimmedFeedback, null, 2), 'utf8');
await this.updateStats();
}
catch (error) {
console.warn('Failed to save feedback:', error);
}
}
async loadFeedback() {
try {
const content = await fs.readFile(this.feedbackFile, 'utf8');
const feedback = JSON.parse(content);
// Convert timestamp strings back to Date objects
return feedback.map((entry) => ({
...entry,
timestamp: new Date(entry.timestamp),
}));
}
catch (error) {
console.warn('Failed to load feedback:', error);
return [];
}
}
async getFeedbackStats() {
try {
const content = await fs.readFile(this.statsFile, 'utf8');
const stats = JSON.parse(content);
return {
...stats,
lastUpdated: new Date(stats.lastUpdated),
};
}
catch (error) {
console.warn('Failed to load feedback stats:', error);
return this.getDefaultStats();
}
}
async recordUsage(action, metadata) {
try {
const stats = await this.getUsageStats();
// Update counters based on action
switch (action) {
case 'session_start':
stats.totalSessions++;
break;
case 'query_sent':
stats.totalQueries++;
break;
case 'collaboration_enabled':
stats.collaborationEnabled++;
break;
case 'verify_used':
stats.verifyUsage++;
break;
case 'compare_used':
stats.compareUsage++;
break;
case 'confidence_used':
stats.confidenceUsage++;
break;
}
stats.lastActive = new Date();
await fs.writeFile(this.usageFile, JSON.stringify(stats, null, 2), 'utf8');
}
catch (error) {
console.warn('Failed to record usage:', error);
}
}
async getUsageStats() {
try {
const content = await fs.readFile(this.usageFile, 'utf8');
const stats = JSON.parse(content);
return {
...stats,
lastActive: new Date(stats.lastActive),
};
}
catch (error) {
console.warn('Failed to load usage stats:', error);
return this.getDefaultUsageStats();
}
}
async exportFeedback(outputPath) {
try {
const feedback = await this.loadFeedback();
const stats = await this.getFeedbackStats();
const usage = await this.getUsageStats();
const exportData = {
feedback,
stats,
usage,
exportedAt: new Date(),
version: '1.0',
};
const exportPath = outputPath ||
join(this.feedbackDir, `feedback-export-${Date.now()}.json`);
await fs.writeFile(exportPath, JSON.stringify(exportData, null, 2), 'utf8');
return exportPath;
}
catch (error) {
throw new Error(`Failed to export feedback: ${error}`);
}
}
async clearOldFeedback(daysToKeep = 30) {
try {
const feedback = await this.loadFeedback();
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
const recentFeedback = feedback.filter((entry) => entry.timestamp > cutoffDate);
const removedCount = feedback.length - recentFeedback.length;
if (removedCount > 0) {
await fs.writeFile(this.feedbackFile, JSON.stringify(recentFeedback, null, 2), 'utf8');
await this.updateStats();
}
return removedCount;
}
catch (error) {
console.warn('Failed to clear old feedback:', error);
return 0;
}
}
async updateStats() {
try {
const feedback = await this.loadFeedback();
const stats = this.calculateStats(feedback);
await fs.writeFile(this.statsFile, JSON.stringify(stats, null, 2), 'utf8');
}
catch (error) {
console.warn('Failed to update stats:', error);
}
}
calculateStats(feedback) {
const stats = {
totalFeedback: feedback.length,
qualityRatings: {
thumbs_up: 0,
thumbs_down: 0,
neutral: 0,
},
verificationUseful: 0,
verificationNotUseful: 0,
averageRating: 0,
lastUpdated: new Date(),
};
let totalRatingScore = 0;
let ratingCount = 0;
for (const entry of feedback) {
if (entry.type === 'quality') {
stats.qualityRatings[entry.rating]++;
// Convert rating to numeric score for average calculation
let score = 0;
switch (entry.rating) {
case 'thumbs_up':
score = 1;
break;
case 'neutral':
score = 0.5;
break;
case 'thumbs_down':
score = 0;
break;
}
totalRatingScore += score;
ratingCount++;
}
else if (entry.type === 'verification') {
if (entry.rating === 'thumbs_up') {
stats.verificationUseful++;
}
else if (entry.rating === 'thumbs_down') {
stats.verificationNotUseful++;
}
}
}
stats.averageRating = ratingCount > 0 ? totalRatingScore / ratingCount : 0;
return stats;
}
getDefaultStats() {
return {
totalFeedback: 0,
qualityRatings: {
thumbs_up: 0,
thumbs_down: 0,
neutral: 0,
},
verificationUseful: 0,
verificationNotUseful: 0,
averageRating: 0,
lastUpdated: new Date(),
};
}
getDefaultUsageStats() {
return {
totalSessions: 0,
totalQueries: 0,
collaborationEnabled: 0,
verifyUsage: 0,
compareUsage: 0,
confidenceUsage: 0,
averageSessionLength: 0,
lastActive: new Date(),
};
}
}
//# sourceMappingURL=feedbackService.js.map