dgx-spark-playbooks/nvidia/txt2kg/assets/frontend/lib/query-logger.ts
2025-10-06 17:05:41 +00:00

232 lines
6.9 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import { promises as fsPromises } from 'fs';
export interface QueryLogEntry {
query: string;
queryMode: 'traditional' | 'vector-search' | 'pure-rag';
timestamp: string;
metrics: {
executionTimeMs: number;
relevanceScore?: number;
precision?: number;
recall?: number;
resultCount: number;
};
}
export interface QueryLogSummary {
query: string;
count: number;
firstQueried: string;
lastQueried: string;
metrics: {
avgExecutionTimeMs: number;
avgRelevanceScore: number;
avgPrecision: number;
avgRecall: number;
avgResultCount: number;
};
executionCount: number;
}
/**
* Service for logging queries to a file
*/
export class QueryLoggerService {
private static instance: QueryLoggerService;
private logFilePath: string;
private initialized: boolean = false;
private constructor() {
// Default path is in the data directory of the project
this.logFilePath = path.join(process.cwd(), 'data', 'query-logs.json');
}
/**
* Get the singleton instance of the QueryLoggerService
*/
public static getInstance(): QueryLoggerService {
if (!QueryLoggerService.instance) {
QueryLoggerService.instance = new QueryLoggerService();
}
return QueryLoggerService.instance;
}
/**
* Initialize the logger
* @param customPath Optional custom path for log file
*/
public async initialize(customPath?: string): Promise<void> {
try {
if (customPath) {
this.logFilePath = customPath;
}
// Ensure the directory exists
const dir = path.dirname(this.logFilePath);
await fsPromises.mkdir(dir, { recursive: true });
// Check if file exists, create it if it doesn't
if (!fs.existsSync(this.logFilePath)) {
await fsPromises.writeFile(this.logFilePath, JSON.stringify([]));
console.log(`[QueryLogger] Initialized empty log file at ${this.logFilePath}`);
} else {
console.log(`[QueryLogger] Using existing log file at ${this.logFilePath}`);
}
this.initialized = true;
} catch (error) {
console.error('[QueryLogger] Error initializing logger:', error);
throw error;
}
}
/**
* Log a RAG query with its performance metrics
*/
public async logQuery(
query: string,
queryMode: 'traditional' | 'vector-search' | 'pure-rag',
metrics: {
executionTimeMs: number;
relevanceScore?: number;
precision?: number;
recall?: number;
resultCount: number;
}
): Promise<void> {
if (!this.initialized) {
await this.initialize();
}
console.log(`[QueryLogger] Logging query: "${query}" (${queryMode})`);
try {
// Read existing logs
const existingLogsRaw = await fsPromises.readFile(this.logFilePath, 'utf-8');
const existingLogs: QueryLogEntry[] = JSON.parse(existingLogsRaw || '[]');
// Add new log entry
const newEntry: QueryLogEntry = {
query,
queryMode,
timestamp: new Date().toISOString(),
metrics
};
existingLogs.push(newEntry);
// Write updated logs back to file
await fsPromises.writeFile(this.logFilePath, JSON.stringify(existingLogs, null, 2));
console.log(`[QueryLogger] Query logged successfully to file`);
} catch (error) {
console.error('[QueryLogger] Error logging query to file:', error);
// Non-critical error, so just log it but don't throw
}
}
/**
* Get query logs with performance metrics
* @param limit Maximum number of query logs to return
* @returns Promise resolving to an array of query logs
*/
public async getQueryLogs(limit: number = 100): Promise<QueryLogSummary[]> {
if (!this.initialized) {
await this.initialize();
}
console.log(`[QueryLogger] Getting query logs with limit: ${limit}`);
try {
// Read logs from file
const logsRaw = await fsPromises.readFile(this.logFilePath, 'utf-8');
const logs: QueryLogEntry[] = JSON.parse(logsRaw || '[]');
if (logs.length === 0) {
console.log('[QueryLogger] No query logs found');
return [];
}
// Group logs by query
const querySummaries = new Map<string, {
query: string;
count: number;
timestamps: string[];
executionTimes: number[];
relevanceScores: number[];
precisions: number[];
recalls: number[];
resultCounts: number[];
}>();
logs.forEach(entry => {
const existing = querySummaries.get(entry.query) || {
query: entry.query,
count: 0,
timestamps: [],
executionTimes: [],
relevanceScores: [],
precisions: [],
recalls: [],
resultCounts: []
};
existing.count++;
existing.timestamps.push(entry.timestamp);
existing.executionTimes.push(entry.metrics.executionTimeMs);
if (entry.metrics.relevanceScore !== undefined) existing.relevanceScores.push(entry.metrics.relevanceScore);
if (entry.metrics.precision !== undefined) existing.precisions.push(entry.metrics.precision);
if (entry.metrics.recall !== undefined) existing.recalls.push(entry.metrics.recall);
existing.resultCounts.push(entry.metrics.resultCount);
querySummaries.set(entry.query, existing);
});
// Convert to array and format
const result: QueryLogSummary[] = Array.from(querySummaries.values()).map(summary => ({
query: summary.query,
count: summary.count,
firstQueried: summary.timestamps[0],
lastQueried: summary.timestamps[summary.timestamps.length - 1],
metrics: {
avgExecutionTimeMs: this.calculateAverage(summary.executionTimes),
avgRelevanceScore: this.calculateAverage(summary.relevanceScores),
avgPrecision: this.calculateAverage(summary.precisions),
avgRecall: this.calculateAverage(summary.recalls),
avgResultCount: this.calculateAverage(summary.resultCounts)
},
executionCount: summary.count
}));
// Sort by count (descending) and limit
return result
.sort((a, b) => b.count - a.count)
.slice(0, limit);
} catch (error) {
console.error('[QueryLogger] Error getting query logs:', error);
throw error;
}
}
/**
* Calculate average of an array of numbers
* @param values Array of numbers
* @returns Average value or 0 if array is empty
*/
private calculateAverage(values: number[]): number {
if (values.length === 0) return 0;
return values.reduce((sum, val) => sum + val, 0) / values.length;
}
/**
* Check if the logger is initialized
*/
public isInitialized(): boolean {
return this.initialized;
}
}
// Create and export singleton instance
const queryLoggerService = QueryLoggerService.getInstance();
export default queryLoggerService;