From 2e9a5e9e7f70d1422c7d64769eee915099cc6650 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sat, 4 Apr 2026 16:33:33 +0200 Subject: [PATCH] cli --- .gitignore | 3 +- package.json | 1 + searchCLI.js | 80 +++++++++++++++++++++++++++++++ src/config/env.js | 1 + src/maintenanceApp.js | 55 +++++++++++++++++++++ src/server.js | 55 +++++++++++++-------- src/services/openRouterLogger.js | 58 +++------------------- src/services/openRouterService.js | 36 +++++++++++--- src/services/searchService.js | 11 ++++- 9 files changed, 218 insertions(+), 82 deletions(-) create mode 100644 searchCLI.js create mode 100644 src/maintenanceApp.js diff --git a/.gitignore b/.gitignore index 1dcef2d..e14d388 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules -.env \ No newline at end of file +.env +logs \ No newline at end of file diff --git a/package.json b/package.json index 46749c4..6f25a6d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "serve": "node restSearch.js", + "search": "node searchCLI.js", "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", diff --git a/searchCLI.js b/searchCLI.js new file mode 100644 index 0000000..979d19d --- /dev/null +++ b/searchCLI.js @@ -0,0 +1,80 @@ +import dotenv from 'dotenv'; +import { createClients } from './src/clients.js'; +import { getConfig, validateConfig } from './src/config/env.js'; +import { createSearchService } from './src/services/searchService.js'; + +// Load environment variables from .env file +dotenv.config(); + +function printUsage() { + console.log('Usage: node searchCLI.js '); + console.log(''); + console.log('Example:'); + console.log(' node searchCLI.js "What are the latest developments in AI?"'); +} + +function createSimpleBroadcast() { + return (message, type = 'info', data = null) => { + const timestamp = new Date().toLocaleTimeString(); + const prefix = type === 'error' ? '❌' : type === 'warning' ? '⚠️' : type === 'success' ? '✅' : 'ℹ️'; + console.log(`[${timestamp}] ${prefix} ${message}`); + if (data) { + console.log(` Data: ${JSON.stringify(data, null, 2)}`); + } + }; +} + +async function runCLI() { + const args = process.argv.slice(2); + + if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + printUsage(); + process.exit(args.includes('--help') || args.includes('-h') ? 0 : 1); + } + + const question = args.join(' '); + + try { + const config = getConfig(); + validateConfig(config); + + const broadcast = createSimpleBroadcast(); + const clients = createClients(config); + const searchService = createSearchService({ + ...clients, + broadcast, + }); + + broadcast(`Starting search for: "${question}"`, 'info'); + + const result = await searchService.search(question); + + + console.log(result); + console.log(''); + console.log('═══════════════════════════════════════════════════════════'); + console.log('FINAL ANSWER:'); + console.log('═══════════════════════════════════════════════════════════'); + console.log(result.fullAnswerHTMLSnippet || result.answer || 'No answer generated'); + console.log(''); + + if (result.mostRelevantSources && result.mostRelevantSources.length > 0) { + console.log('SOURCES:'); + result.mostRelevantSources.forEach((source, index) => { + console.log(` ${index + 1}. ${source}`); + }); + } + console.log('═══════════════════════════════════════════════════════════'); + + process.exit(0); + } catch (error) { + console.error(''); + console.error('❌ Error:', error.message); + if (error.details) { + console.error(' Details:', error.details); + } + process.exit(1); + } +} + +runCLI(); diff --git a/src/config/env.js b/src/config/env.js index 80a2b87..700359e 100644 --- a/src/config/env.js +++ b/src/config/env.js @@ -10,6 +10,7 @@ export function getConfig() { host: process.env.HOST || '0.0.0.0', exaApiKey: process.env.EXA_API_KEY, openRouterApiKey: process.env.OPENROUTER_API_KEY, + maintenanceMode: process.env.MAINTENANCE_MODE === 'true', }; } diff --git a/src/maintenanceApp.js b/src/maintenanceApp.js new file mode 100644 index 0000000..20790ab --- /dev/null +++ b/src/maintenanceApp.js @@ -0,0 +1,55 @@ +import express from 'express'; + +export function createMaintenanceApp() { + const app = express(); + + app.get('/{*path}', (req, res) => { + res.status(503).send(` + + + + + + Under Construction + + + +
+
🚧
+

Under Construction

+

Please return later.

+
+ + + `); + }); + + return app; +} diff --git a/src/server.js b/src/server.js index 681d014..4f72227 100644 --- a/src/server.js +++ b/src/server.js @@ -1,37 +1,50 @@ import { createApp } from './app.js'; +import { createMaintenanceApp } from './maintenanceApp.js'; import { createClients } from './clients.js'; import { getConfig, validateConfig } from './config/env.js'; import { createSseHub } from './lib/sseHub.js'; import { createSearchService } from './services/searchService.js'; -function logStartup(host, port) { - console.log(`REST Search Service running at http://${host}:${port}`); - console.log(`Web UI: http://localhost:${port}`); - console.log('API Documentation:'); - console.log(' POST /search - Search for a query and return summarized results'); - console.log(' GET /health - Health check endpoint'); - console.log(' GET /stream - SSE endpoint for streaming log messages'); - console.log('\nExample:'); - console.log(` curl -X POST http://${host}:${port}/search \\`); - console.log(' -H "Content-Type: application/json" \\'); - console.log(' -d \'{"question": "What are the latest developments in AI?"}\''); - console.log('\nSSE Log Stream:'); - console.log(` Open http://localhost:${port}/stream in a browser or use EventSource in JavaScript`); +function logStartup(host, port, isMaintenance) { + if (isMaintenance) { + console.log(`🚧 Maintenance Mode running at http://${host}:${port}`); + console.log('All requests will receive "Under Construction" page'); + } else { + console.log(`REST Search Service running at http://${host}:${port}`); + console.log(`Web UI: http://localhost:${port}`); + console.log('API Documentation:'); + console.log(' POST /search - Search for a query and return summarized results'); + console.log(' GET /health - Health check endpoint'); + console.log(' GET /stream - SSE endpoint for streaming log messages'); + console.log('\nExample:'); + console.log(` curl -X POST http://${host}:${port}/search \\`); + console.log(' -H "Content-Type: application/json" \\'); + console.log(' -d \'{"question": "What are the latest developments in AI?"}\''); + console.log('\nSSE Log Stream:'); + console.log(` Open http://localhost:${port}/stream in a browser or use EventSource in JavaScript`); + } } export function startServer() { const config = getConfig(); validateConfig(config); - const clients = createClients(config); - const sseHub = createSseHub(); - const searchService = createSearchService({ - ...clients, - broadcast: sseHub.broadcast, - }); - const app = createApp({ searchService, sseHub }); + const isMaintenance = config.maintenanceMode; + + let app; + if (isMaintenance) { + app = createMaintenanceApp(); + } else { + const clients = createClients(config); + const sseHub = createSseHub(); + const searchService = createSearchService({ + ...clients, + broadcast: sseHub.broadcast, + }); + app = createApp({ searchService, sseHub }); + } app.listen(config.port, config.host, () => { - logStartup(config.host, config.port); + logStartup(config.host, config.port, isMaintenance); }); } diff --git a/src/services/openRouterLogger.js b/src/services/openRouterLogger.js index 3503235..a31621c 100644 --- a/src/services/openRouterLogger.js +++ b/src/services/openRouterLogger.js @@ -6,7 +6,6 @@ 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 @@ -27,14 +26,6 @@ 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') @@ -47,47 +38,12 @@ export async function logOpenRouterCall(operation, params, response, error = nul 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, - }; + const logFileName = `openrouter-${timestamp.replace(/[:.]/g, '-')}.log`; + const logFilePath = path.join(LOG_DIR, logFileName); - // 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 logContent = `${timestamp} | ${operation}\n\nParams:\n${JSON.stringify(params, null, 2)}\n\nResponse:\n${JSON.stringify(response, null, 2)}${error ? `\n\nError: ${error.message}` : ''}\n`; - 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'); + await fs.writeFile(logFilePath, logContent, 'utf8'); } catch (logError) { console.error('Failed to log OpenRouter call:', logError); // Don't throw - logging should not break the main functionality @@ -95,8 +51,8 @@ export async function logOpenRouterCall(operation, params, response, error = nul } /** - * Gets the path to the OpenRouter log file + * Gets the path to the OpenRouter log directory */ -export function getOpenRouterLogFilePath() { - return LOG_FILE; +export function getOpenRouterLogDirPath() { + return LOG_DIR; } \ No newline at end of file diff --git a/src/services/openRouterService.js b/src/services/openRouterService.js index bbbf8ec..193bcee 100644 --- a/src/services/openRouterService.js +++ b/src/services/openRouterService.js @@ -1,7 +1,7 @@ import { logOpenRouterCall } from './openRouterLogger.js'; function parseResponse(response) { - return JSON.parse(response.choices[0].message.content); + return { cost: response.usage.cost, data: JSON.parse(response.choices[0].message.content) }; } export async function summarizeSources({ openrouter, text, question }) { @@ -18,9 +18,9 @@ export async function summarizeSources({ openrouter, text, question }) { { role: 'user', content: text }, ], reasoning: { effort: 'low' }, - responseFormat: { + response_format: { type: 'json_schema', - jsonSchema: { + json_schema: { name: 'search_summaries', strict: true, schema: { @@ -47,7 +47,17 @@ export async function summarizeSources({ openrouter, text, question }) { stream: false, }; - const response = await openrouter.chat.send({ chatRequest: params }); + // Using direct fetch API instead of OpenRouter SDK + const apiKey = process.env.OPENROUTER_API_KEY; + const fetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const response = await fetchResponse.json(); await logOpenRouterCall('summarizeSources', params, response); return parseResponse(response); } @@ -67,9 +77,9 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) { { role: 'user', content: text }, ], reasoning: { effort: 'none' }, - responseFormat: { + response_format: { type: 'json_schema', - jsonSchema: { + json_schema: { name: 'response', strict: true, schema: { @@ -88,7 +98,19 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) { stream: false, }; - const response = await openrouter.chat.send({ chatRequest: params }); + // Using direct fetch API instead of OpenRouter SDK + const apiKey = process.env.OPENROUTER_API_KEY; + const fetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const response = await fetchResponse.json(); + await logOpenRouterCall('summarizeFinalAnswer', params, response); + return parseResponse(response); } diff --git a/src/services/searchService.js b/src/services/searchService.js index 05b1c29..cd38d83 100644 --- a/src/services/searchService.js +++ b/src/services/searchService.js @@ -82,6 +82,7 @@ export function createSearchService({ exa, openrouter, broadcast }) { broadcast(`✅ Found ${result.results.length} results`, 'success', { count: result.results.length, }); + let cost = result.costDollars.total; const extractedContent = extractContent(result); @@ -95,12 +96,15 @@ export function createSearchService({ exa, openrouter, broadcast }) { broadcast('📝 Generating summary with OpenRouter...', 'info'); let summary; + try { - summary = await summarizeSources({ + const summaryResult = await summarizeSources({ openrouter, text: extractedContent, question, }); + summary = summaryResult.data; + cost += summaryResult.cost; } catch (error) { throw new SearchServiceError('Failed to generate summary', 500, error); } @@ -128,11 +132,13 @@ export function createSearchService({ exa, openrouter, broadcast }) { let finalSummary; try { - finalSummary = await summarizeFinalAnswer({ + const finalSummaryResult = await summarizeFinalAnswer({ openrouter, text: formatSummarySources(summary.sources), question, }); + finalSummary = finalSummaryResult.data; + cost += finalSummaryResult.cost; } catch (error) { throw new SearchServiceError('Failed to generate final summary', 500, error); } @@ -145,6 +151,7 @@ export function createSearchService({ exa, openrouter, broadcast }) { answerLength: finalSummary.fullAnswerHTMLSnippet?.length || 0, sourcesCount: finalSummary.mostRelevantSources?.length || 0, }); + console.log(`Total cost of API calls: $${cost.toFixed(6)}`); return finalSummary; },