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 }) {
|
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()}.
|
You are a search result analyst. Today is the date of ${new Date().toLocaleDateString()}.
|
||||||
Based on the following search results for the query "${question}",
|
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/>
|
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 followup searched to the user.
|
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',
|
model: 'openai/gpt-oss-120b:nitro',
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: prompt },
|
{ role: 'system', content: summaryPrompt },
|
||||||
{ role: 'user', content: text },
|
{ role: 'user', content: text },
|
||||||
],
|
],
|
||||||
reasoning: { effort: 'low' },
|
reasoning: { effort: 'low' },
|
||||||
response_format: {
|
response_format: {
|
||||||
type: 'json_schema',
|
type: 'json_schema',
|
||||||
json_schema: {
|
json_schema: {
|
||||||
name: 'response',
|
name: 'summary',
|
||||||
strict: true,
|
strict: true,
|
||||||
schema: {
|
schema: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
required: ['fullAnswerHTMLSnippet', 'mostRelevantSources', 'suggestedSearches'],
|
required: ['summary', 'mostRelevantSources', 'suggestedSearches'],
|
||||||
properties: {
|
properties: {
|
||||||
fullAnswerHTMLSnippet: { type: 'string' },
|
summary: { type: 'string' },
|
||||||
mostRelevantSources: { type: 'array', items: { type: 'string' } },
|
mostRelevantSources: { type: 'array', items: { type: 'string' } },
|
||||||
suggestedSearches: { type: 'array', items: { type: 'string' } },
|
suggestedSearches: { type: 'array', items: { type: 'string' } },
|
||||||
},
|
},
|
||||||
@@ -239,19 +242,88 @@ export async function summarizeFinalAnswer({ openrouter, text, question }) {
|
|||||||
stream: false,
|
stream: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Using direct fetch API instead of OpenRouter SDK
|
const summaryFetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
||||||
const fetchResponse = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${apiKey}`,
|
Authorization: `Bearer ${apiKey}`,
|
||||||
'Content-Type': 'application/json',
|
'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}
|
||||||
|
`;
|
||||||
|
|
||||||
return parseResponse(response);
|
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);
|
||||||
|
|
||||||
|
// 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 { extractContent } from './extractContent.js';
|
||||||
import { logExtraction } from './extractionLogger.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 { formatSummarySources } from './searchFormatter.js';
|
||||||
import { get_encoding } from 'tiktoken';
|
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));
|
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) {
|
} catch (error) {
|
||||||
broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning');
|
broadcast(`⚠️ Could not fetch content for ${source.url}: ${error.message}`, 'warning');
|
||||||
return { url: source.url, content: null };
|
return { url: source.url, content: null };
|
||||||
@@ -256,6 +267,9 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
|||||||
completion_tokens: outputTokens
|
completion_tokens: outputTokens
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
//if (detailed.cost2) {
|
||||||
|
// cost.push(detailed.cost2);
|
||||||
|
//}
|
||||||
}
|
}
|
||||||
|
|
||||||
broadcast('📊 Generating final summary...', 'info');
|
broadcast('📊 Generating final summary...', 'info');
|
||||||
@@ -268,13 +282,28 @@ export function createSearchService({ exa, openrouter, broadcast }) {
|
|||||||
question,
|
question,
|
||||||
});
|
});
|
||||||
finalSummary = finalSummaryResult.data;
|
finalSummary = finalSummaryResult.data;
|
||||||
|
|
||||||
|
// 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({
|
cost.push({
|
||||||
type: 'openrouter_final_summary',
|
type: 'openrouter_final_summary',
|
||||||
amount: finalSummaryResult.cost,
|
amount: finalSummaryResult.totalCost,
|
||||||
prompt_tokens: finalSummaryResult.prompt_tokens,
|
prompt_tokens: finalSummaryResult.totalPromptTokens,
|
||||||
completion_tokens: finalSummaryResult.completion_tokens,
|
completion_tokens: finalSummaryResult.totalCompletionTokens,
|
||||||
model: 'openai/gpt-oss-120b:nitro'
|
model: 'openai/gpt-oss-120b:nitro',
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new SearchServiceError('Failed to generate final summary', 500, error);
|
throw new SearchServiceError('Failed to generate final summary', 500, error);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user