+ Also provide the most relevant sources. Answer in the language of the question.
`;
const params = {
@@ -112,7 +205,7 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) {
});
const response = await fetchResponse.json();
- await logOpenRouterCall('summarizeFinalAnswer', params, response);
+ await logOpenRouterCall('summarizeFinalAnswer', text, params, response);
return parseResponse(response);
}
diff --git a/src/services/searchFormatter.js b/src/services/searchFormatter.js
index 39e1c9e..44e2df6 100644
--- a/src/services/searchFormatter.js
+++ b/src/services/searchFormatter.js
@@ -19,7 +19,7 @@ function wrapText(text, maxLineLength = 68) {
return output;
}
-export function formatSummarySources(sources) {
+export function formatSummarySources(sources,supplementaryResults=[]) {
let output = `\n${'='.repeat(70)}\n`;
output += ` ${'SUMMARY'.padStart(35).padEnd(69)}\n`;
output += `${'='.repeat(70)}\n`;
@@ -38,6 +38,13 @@ export function formatSummarySources(sources) {
}
});
+ if (supplementaryResults.length > 0) {
+ supplementaryResults.forEach((result) => {
+ output += `\n${'-'.repeat(70)}\n`;
+ output += JSON.stringify(result, null, 2);
+ });
+ }
+
output += `\n${'='.repeat(70)}\n`;
output += `Total sources: ${sources.length}\n`;
output += `${'='.repeat(70)}\n`;
diff --git a/src/services/searchService.js b/src/services/searchService.js
index 1662db3..6c2a420 100644
--- a/src/services/searchService.js
+++ b/src/services/searchService.js
@@ -1,7 +1,16 @@
import { extractContent } from './extractContent.js';
import { logExtraction } from './extractionLogger.js';
-import { summarizeFinalAnswer, summarizeSources } from './openRouterService.js';
+import { summarizeFinalAnswer, summarizeSources, rephraseQuestion } from './openRouterService.js';
import { formatSummarySources } from './searchFormatter.js';
+import { get_encoding } from 'tiktoken';
+
+const encoding = get_encoding('cl100k_base');
+
+function countTokens(text) {
+ if (!text) return 0;
+ const tokens = encoding.encode(text);
+ return tokens.length;
+}
const EXA_SEARCH_OPTIONS = {
numResults: 10,
@@ -72,18 +81,114 @@ function enrichSourcesWithDetails(sources, detailedContents) {
}
}
+// Helper function to build cost breakdown
+function buildCostBreakdown(cost) {
+ // Print cost breakdown as a formatted table
+ console.log('\n=== Cost Breakdown ===');
+ console.table(
+ cost.map((item, index) => ({
+ '#': index + 1,
+ Type: item.type,
+ 'Cost (USD)': `$${item.amount.toFixed(6)}`,
+ })),
+ );
+ const totalCost = cost.reduce((sum, item) => sum + item.amount, 0);
+ console.log(`Total: $${totalCost.toFixed(6)}\n`);
+
+ // Build cost breakdown table for API response
+ const costBreakdown = cost.map((item, index) => ({
+ index: index + 1,
+ type: item.type,
+ prompt_tokens: item.prompt_tokens,
+ completion_tokens: item.completion_tokens,
+ cost: `$${item.amount.toFixed(6)}`,
+ }));
+
+ // Add total to the breakdown
+ costBreakdown.push({
+ index: costBreakdown.length + 1,
+ type: 'Total',
+ cost: `$${totalCost.toFixed(6)}`,
+ });
+
+ return costBreakdown;
+}
+
export function createSearchService({ exa, openrouter, broadcast }) {
return {
- async search(question) {
- broadcast(`🔍 Search request: "${question}"`, 'info', { question });
+ async search(question, previousClarification, originalQuestion) {
+ let finalSummary;
+ const cost=[];
+ let rephrasedQuestion;
- broadcast('Searching with Exa...', 'info');
- const result = await exa.search(question, EXA_SEARCH_OPTIONS);
+ try {
+ const rephraseResult = await rephraseQuestion({question, previousClarification, originalQuestion});
+ rephrasedQuestion = rephraseResult.data;
+ cost.push({
+ type:'openrouter_rephrase',
+ amount: rephraseResult.cost,
+ prompt_tokens: rephraseResult.prompt_tokens,
+ completion_tokens: rephraseResult.completion_tokens
+ });
+ } catch (error) {
+ throw new SearchServiceError('Failed to generate summary', 500, error);
+ }
+ if (rephrasedQuestion.needsClarification) {
+ finalSummary = {
+ fullAnswerHTMLSnippet: rephrasedQuestion.clarification,
+ clarificationNeeded: true,
+ originalQuestion: question,
+ mostRelevantSources: [],
+ };
+ // Attach cost breakdown to the final summary
+ finalSummary.costBreakdown = buildCostBreakdown(cost);
+
+ return finalSummary;
+
+ }
+
+ // Limit to first 2 supplementary queries
+ const limitedQueries = rephrasedQuestion.supplementaryQueries.slice(0, 2);
+ //limitedQueries.push(rephrasedQuestion.query);
+ const supplementaryResults = [];
+ for (const item of limitedQueries) {
+ console.log('Supplementary Query:', item);
+ const result = await exa.search(item, {
+ numResults: 2,
+ type: 'auto',
+ contents: {
+ highlights: {
+ maxCharacters: 2000,
+ },
+ },
+ });
+ supplementaryResults.push({
+ query: item,
+ highlights: result.results.map(r => r.highlights),
+ });
+ const outputTokens = countTokens(JSON.stringify(result.results));
+ cost.push({
+ type: 'exa_search_complement',
+ amount: result.costDollars.total,
+ prompt_tokens: 0,
+ completion_tokens: outputTokens
+ });
+ }
+
+
+ broadcast('Searching with Exa for '+rephrasedQuestion.query, 'info');
+ const result = await exa.search(rephrasedQuestion.query, EXA_SEARCH_OPTIONS);
broadcast(`✅ Found ${result.results.length} results`, 'success', {
count: result.results.length,
});
- const cost=[];
- cost.push({type:'exa_search', amount: result.costDollars.total});
+
+ const outputTokens = countTokens(JSON.stringify(result.results));
+ cost.push({
+ type:'exa_search',
+ amount: result.costDollars.total,
+ prompt_tokens: 0,
+ completion_tokens: outputTokens
+ });
const extractedContent = extractContent(result);
@@ -105,13 +210,18 @@ export function createSearchService({ exa, openrouter, broadcast }) {
question,
});
summary = summaryResult.data;
- cost.push({type:'openrouter_summarize', amount: summaryResult.cost});
+ cost.push({
+ type:'openrouter_rank',
+ amount: summaryResult.cost,
+ prompt_tokens: summaryResult.prompt_tokens,
+ completion_tokens: summaryResult.completion_tokens
+ });
} catch (error) {
throw new SearchServiceError('Failed to generate summary', 500, error);
}
if (!summary?.sources) {
- throw new SearchServiceError('Failed to generate summary');
+ throw new SearchServiceError('Failed to generate summary for query: ' + question + '. No sources returned:'+ JSON.stringify(summary, null, 2));
}
broadcast(`✅ Generated summaries for ${summary.sources.length} sources`, 'success', {
@@ -126,25 +236,35 @@ export function createSearchService({ exa, openrouter, broadcast }) {
broadcast,
});
- broadcast('🔧 Enhancing summaries with detailed content...', 'info');
enrichSourcesWithDetails(summary.sources, detailedContents);
for (const detailed of detailedContents) {
if (detailed.cost) {
- cost.push({type:'exa_get_content', amount: detailed.cost});
+ const outputTokens = countTokens(JSON.stringify(detailed.content));
+ cost.push({
+ type:'exa_get_content',
+ amount: detailed.cost,
+ prompt_tokens: 0,
+ completion_tokens: outputTokens
+ });
}
}
broadcast('📊 Generating final summary...', 'info');
- let finalSummary;
+
try {
const finalSummaryResult = await summarizeFinalAnswer({
openrouter,
- text: formatSummarySources(summary.sources),
+ text: formatSummarySources(summary.sources,supplementaryResults),
question,
});
finalSummary = finalSummaryResult.data;
- cost.push({type:'openrouter_final_summary', amount: finalSummaryResult.cost});
+ cost.push({
+ type:'openrouter_final_summary',
+ amount: finalSummaryResult.cost,
+ prompt_tokens: finalSummaryResult.prompt_tokens,
+ completion_tokens: finalSummaryResult.completion_tokens
+ });
} catch (error) {
throw new SearchServiceError('Failed to generate final summary', 500, error);
}
@@ -158,6 +278,9 @@ export function createSearchService({ exa, openrouter, broadcast }) {
sourcesCount: finalSummary.mostRelevantSources?.length || 0,
});
+ // Attach cost breakdown to the final summary
+ finalSummary.costBreakdown = buildCostBreakdown(cost);
+
return finalSummary;
},
};
diff --git a/src/utils/htmlConsoleRenderer.js b/src/utils/htmlConsoleRenderer.js
index 7ab9cc5..a0c48f8 100644
--- a/src/utils/htmlConsoleRenderer.js
+++ b/src/utils/htmlConsoleRenderer.js
@@ -59,6 +59,14 @@ export function renderHTML(html) {
// Handle
content
- block with newline
text = text.replace(/
([\s\S]*?)<\/div>/gi, (match, content) => `${content.trim()}\n\n`);
+ // Handle
content
- preformatted text with monospace styling
+ text = text.replace(/
([\s\S]*?)<\/pre>/gi, (match, content) => {
+ // Preserve internal whitespace and newlines, apply dimmed styling for code-like appearance
+ const lines = content.split('\n');
+ const formattedLines = lines.map(line => chalk.dim(line));
+ return '\n' + formattedLines.join('\n') + '\n\n';
+ });
+
// Clean up any remaining HTML tags that weren't processed
text = text.replace(/<[^>]+>/g, '');