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:
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user