feat: Zusammenfassung der endgültigen Antwort in zwei Schritten umgestaltet

- Zerlegung von `summarizeFinalAnswer` in zwei separate LLM-Aufrufe:
  1. Zusammenfassung, Quellen und vorgeschlagene Suchen
  2. Anreicherung mit Emojis und HTML-Tags
- Verbesserte Prompt-Formulierung für detailliertere Zusammenfassungen
- Verwendung eines günstigeren Modells für den Formatierungsschritt
- Hinzufügen separater Kostenprotokollierung pro Schritt
- Aktualisierung der JSON-Schema-Antwortstruktur
This commit is contained in:
sebseb7
2026-04-05 02:04:35 +02:00
parent 92845a5a4c
commit 38e702ec2f
2 changed files with 127 additions and 26 deletions

View File

@@ -206,30 +206,33 @@ export async function rephraseQuestion({ question, previousClarification, origin
}
export async function summarizeFinalAnswer({ openrouter, text, question }) {
const prompt = `
const apiKey = process.env.OPENROUTER_API_KEY;
// Step 1: Get summary, sources, and suggested searches
const summaryPrompt = `
You are a search result analyst. Today is the date of ${new Date().toLocaleDateString()}.
Based on the following search results for the query "${question}",
Summarize the search results to answer the original query. Use Emoji and HTML. Tags allowed: <b>, <i>, <u>, <pre>, <ul>, <li>, <span style="color:...">, <p> <div> <hr/>
Also provide the most relevant sources. Answer in the language of the question. You may suggest followup searched to the user.
Summarize the search results to answer the original query in a detailed manner.
Also provide the most relevant sources. Answer in the language of the question. You may suggest 2 followup searches to the user.
`;
const params = {
const summaryParams = {
model: 'openai/gpt-oss-120b:nitro',
messages: [
{ role: 'system', content: prompt },
{ role: 'system', content: summaryPrompt },
{ role: 'user', content: text },
],
reasoning: { effort: 'low' },
response_format: {
type: 'json_schema',
json_schema: {
name: 'response',
name: 'summary',
strict: true,
schema: {
type: 'object',
required: ['fullAnswerHTMLSnippet', 'mostRelevantSources', 'suggestedSearches'],
required: ['summary', 'mostRelevantSources', 'suggestedSearches'],
properties: {
fullAnswerHTMLSnippet: { type: 'string' },
summary: { type: 'string' },
mostRelevantSources: { type: 'array', items: { type: 'string' } },
suggestedSearches: { type: 'array', items: { type: 'string' } },
},
@@ -239,19 +242,88 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) {
stream: false,
};
// 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', {
const summaryFetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
body: JSON.stringify(summaryParams),
});
const response = await fetchResponse.json();
const summaryResponse = await summaryFetchResponse.json();
await logOpenRouterCall('summarizeFinalAnswer-step1', text, summaryParams, summaryResponse);
const summaryData = parseResponse(summaryResponse);
await logOpenRouterCall('summarizeFinalAnswer', text, params, response);
// Step 2: Enrich the summary with HTML tags and emojis
const enrichmentPrompt = `
You are a content formatter. Take the following summary and enrich it with emojis and HTML tags.
Allowed tags: <b>, <i>, <u>, <pre>, <ul>, <li>, <span style="color:...">, <p>, <div>, <hr/>
Make it visually appealing and easy to read. Keep the same language as the original summary.
Summary to enrich:
${summaryData.data.summary}
`;
const enrichmentParams = {
model: 'kwaipilot/kat-coder-pro-v2',
messages: [
{ role: 'system', content: enrichmentPrompt },
{ role: 'user', content: 'Please enrich the summary with HTML tags and emojis.' },
],
reasoning: { effort: 'low' },
response_format: {
type: 'json_schema',
json_schema: {
name: 'enrichedSummary',
strict: true,
schema: {
type: 'object',
required: ['fullAnswerHTMLSnippet'],
properties: {
fullAnswerHTMLSnippet: { type: 'string' },
},
},
},
},
stream: false,
};
const enrichmentFetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(enrichmentParams),
});
const enrichmentResponse = await enrichmentFetchResponse.json();
await logOpenRouterCall('summarizeFinalAnswer-step2', summaryData.data.summary, enrichmentParams, enrichmentResponse);
const enrichmentData = parseResponse(enrichmentResponse);
return parseResponse(response);
// Combine results with separate cost breakdowns
return {
totalCost: summaryData.cost + enrichmentData.cost,
totalPromptTokens: summaryData.prompt_tokens + enrichmentData.prompt_tokens,
totalCompletionTokens: summaryData.completion_tokens + enrichmentData.completion_tokens,
steps: [
{
name: 'summary',
model: 'openai/gpt-oss-120b:nitro',
cost: summaryData.cost,
promptTokens: summaryData.prompt_tokens,
completionTokens: summaryData.completion_tokens,
},
{
name: 'enrichment',
model: 'kwaipilot/kat-coder-pro-v2',
cost: enrichmentData.cost,
promptTokens: enrichmentData.prompt_tokens,
completionTokens: enrichmentData.completion_tokens,
},
],
data: {
fullAnswerHTMLSnippet: enrichmentData.data.fullAnswerHTMLSnippet,
mostRelevantSources: summaryData.data.mostRelevantSources,
suggestedSearches: summaryData.data.suggestedSearches,
},
};
}

View File

@@ -1,6 +1,6 @@
import { extractContent } from './extractContent.js';
import { logExtraction } from './extractionLogger.js';
import { summarizeFinalAnswer, summarizeSources, rephraseQuestion } from './openRouterService.js';
import { summarizeFinalAnswer, summarizeSources, summarizeDetail, rephraseQuestion } from './openRouterService.js';
import { formatSummarySources } from './searchFormatter.js';
import { get_encoding } from 'tiktoken';
@@ -58,11 +58,22 @@ async function fetchDetailedContents({ exa, question, sources, broadcast }) {
const content = await exa.getContents([source.url], EXA_CONTENT_OPTIONS(question));
console.log(content.results.highlights.join('\n'));
console.log(content.results[0].highlights);
return { url: source.url, content , cost: content.costDollars.total };
const summary = await summarizeDetail({text: content.results.highlights.join('\n'), url: source.url, question})
/*const summary = await summarizeDetail({text: content.results, url: source.url, question})
return { url: source.url, content:summary, cost: content.costDollars.total };
const cost2 = {
type:'openrouter_detail_summary',
amount: summary.cost,
prompt_tokens: summary.prompt_tokens,
completion_tokens: summary.completion_tokens,
model: 'openai/gpt-oss-120b:nitro'
};
const summaryIsLonger = JSON.stringify(summary.data).length > JSON.stringify(content.results).length;
return { url: source.url, content: summaryIsLonger ? content.results : source.summary, cost: content.costDollars.total, cost2 };*/
} catch (error) {
broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning');
return { url: source.url, content: null };
@@ -256,6 +267,9 @@ export function createSearchService({ exa, openrouter, broadcast }) {
completion_tokens: outputTokens
});
}
//if (detailed.cost2) {
// cost.push(detailed.cost2);
//}
}
broadcast('📊 Generating final summary...', 'info');
@@ -268,13 +282,28 @@ export function createSearchService({ exa, openrouter, broadcast }) {
question,
});
finalSummary = finalSummaryResult.data;
cost.push({
type:'openrouter_final_summary',
amount: finalSummaryResult.cost,
prompt_tokens: finalSummaryResult.prompt_tokens,
completion_tokens: finalSummaryResult.completion_tokens,
model: 'openai/gpt-oss-120b:nitro'
});
// Push each step separately for detailed cost tracking
if (finalSummaryResult.steps && Array.isArray(finalSummaryResult.steps)) {
for (const step of finalSummaryResult.steps) {
cost.push({
type: `openrouter_${step.name}`,
amount: step.cost,
prompt_tokens: step.promptTokens,
completion_tokens: step.completionTokens,
model: step.model,
});
}
} else {
// Fallback for backward compatibility
cost.push({
type: 'openrouter_final_summary',
amount: finalSummaryResult.totalCost,
prompt_tokens: finalSummaryResult.totalPromptTokens,
completion_tokens: finalSummaryResult.totalCompletionTokens,
model: 'openai/gpt-oss-120b:nitro',
});
}
} catch (error) {
throw new SearchServiceError('Failed to generate final summary', 500, error);
}