diff --git a/src/services/extractionLogger.js b/src/services/extractionLogger.js new file mode 100644 index 0000000..daa9114 --- /dev/null +++ b/src/services/extractionLogger.js @@ -0,0 +1,74 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const LOG_DIR = path.join(__dirname, '../../logs'); +const LOG_FILE = path.join(LOG_DIR, 'extraction.log'); + +/** + * Ensures the log directory exists + */ +async function ensureLogDirectory() { + try { + await fs.mkdir(LOG_DIR, { recursive: true }); + } catch (error) { + console.error('Failed to create log directory:', error); + throw error; + } +} + +/** + * Formats a timestamp for logging + */ +function formatTimestamp() { + return new Date().toISOString(); +} + +/** + * Logs an extraction event with input and output + * @param {Object} input - The input data (search result from Exa) + * @param {string} output - The extracted text content + */ +export async function logExtraction(input, output) { + try { + await ensureLogDirectory(); + + const timestamp = formatTimestamp(); + const logEntry = { + timestamp, + input: { + resultCount: input?.results?.length || 0, + results: input?.results?.map((r) => ({ + title: r.title, + url: r.url, + textPreview: r.text?.substring(0, 200) + (r.text?.length > 200 ? '...' : ''), + summaryPreview: r.summary?.substring(0, 200) + (r.summary?.length > 200 ? '...' : ''), + highlightsCount: r.highlights?.length || 0, + publishedDate: r.publishedDate, + author: r.author, + })), + }, + output: { + length: output.length, + preview: output.substring(0, 500) + (output.length > 500 ? '...' : ''), + }, + }; + + const logLine = `${timestamp} | Input: ${JSON.stringify(logEntry.input)} | Output: [${logEntry.output.length} chars] ${logEntry.output.preview}\n`; + + await fs.appendFile(LOG_FILE, logLine, 'utf8'); + } catch (error) { + console.error('Failed to log extraction:', error); + // Don't throw - logging should not break the main functionality + } +} + +/** + * Gets the path to the extraction log file + */ +export function getLogFilePath() { + return LOG_FILE; +} \ No newline at end of file diff --git a/src/services/openRouterLogger.js b/src/services/openRouterLogger.js new file mode 100644 index 0000000..3503235 --- /dev/null +++ b/src/services/openRouterLogger.js @@ -0,0 +1,102 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const LOG_DIR = path.join(__dirname, '../../logs'); +const LOG_FILE = path.join(LOG_DIR, 'openrouter.log'); + +/** + * Ensures the log directory exists + */ +async function ensureLogDirectory() { + try { + await fs.mkdir(LOG_DIR, { recursive: true }); + } catch (error) { + console.error('Failed to create log directory:', error); + throw error; + } +} + +/** + * Formats a timestamp for logging + */ +function formatTimestamp() { + return new Date().toISOString(); +} + +/** + * Safely truncates a string + */ +function truncate(str, maxLength = 1000) { + if (!str) return ''; + return str.length > maxLength ? str.substring(0, maxLength) + '...' : str; +} + +/** + * Logs an OpenRouter API call with params and response + * @param {string} operation - The operation name (e.g., 'summarizeSources', 'summarizeFinalAnswer') + * @param {Object} params - The request parameters sent to OpenRouter + * @param {Object} response - The response from OpenRouter + * @param {Error} [error] - Optional error if the call failed + */ +export async function logOpenRouterCall(operation, params, response, error = null) { + try { + await ensureLogDirectory(); + + const timestamp = formatTimestamp(); + + // Sanitize and truncate params for logging + const sanitizedParams = { + model: params.model, + messages: params.messages?.map((msg) => ({ + role: msg.role, + contentPreview: truncate(msg.content, 500), + })), + reasoning: params.reasoning, + responseFormat: params.responseFormat?.jsonSchema?.name, + stream: params.stream, + }; + + // Sanitize response + const sanitizedResponse = response + ? { + choices: response.choices?.map((choice) => ({ + messagePreview: truncate(choice.message?.content, 500), + finishReason: choice.finish_reason, + })), + model: response.model, + usage: response.usage, + } + : null; + + const logEntry = { + timestamp, + operation, + params: sanitizedParams, + response: sanitizedResponse, + error: error + ? { + name: error.name, + message: error.message, + } + : null, + }; + + const logLine = `${timestamp} | ${operation} | Params: ${JSON.stringify(logEntry.params)} | Response: ${JSON.stringify(logEntry.response)}${error ? ` | Error: ${error.message}` : ''}\n`; + + await fs.appendFile(LOG_FILE, logLine, 'utf8'); + } catch (logError) { + console.error('Failed to log OpenRouter call:', logError); + // Don't throw - logging should not break the main functionality + } +} + +/** + * Gets the path to the OpenRouter log file + */ +export function getOpenRouterLogFilePath() { + return LOG_FILE; +} \ No newline at end of file diff --git a/src/services/openRouterService.js b/src/services/openRouterService.js index be2f8cb..bbbf8ec 100644 --- a/src/services/openRouterService.js +++ b/src/services/openRouterService.js @@ -1,3 +1,5 @@ +import { logOpenRouterCall } from './openRouterLogger.js'; + function parseResponse(response) { return JSON.parse(response.choices[0].message.content); } @@ -46,6 +48,7 @@ export async function summarizeSources({ openrouter, text, question }) { }; const response = await openrouter.chat.send({ chatRequest: params }); + await logOpenRouterCall('summarizeSources', params, response); return parseResponse(response); } @@ -86,5 +89,6 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) { }; const response = await openrouter.chat.send({ chatRequest: params }); + await logOpenRouterCall('summarizeFinalAnswer', params, response); return parseResponse(response); } diff --git a/src/services/searchService.js b/src/services/searchService.js index e2692cb..05b1c29 100644 --- a/src/services/searchService.js +++ b/src/services/searchService.js @@ -1,4 +1,5 @@ import { extractContent } from './extractContent.js'; +import { logExtraction } from './extractionLogger.js'; import { summarizeFinalAnswer, summarizeSources } from './openRouterService.js'; import { formatSummarySources } from './searchFormatter.js'; @@ -84,6 +85,9 @@ export function createSearchService({ exa, openrouter, broadcast }) { const extractedContent = extractContent(result); + // Log the extraction + await logExtraction(result, extractedContent); + if (!extractedContent.trim()) { throw new SearchServiceError('No content extracted from results'); }