feat(news-research): add depth=shallow|deep option to research_news tool

The existing RSS-based path stays the default (shallow, free). `depth=deep`
fans out to two research agents in parallel (Perplexity Sonar first, then
Gemini Grounding if available) via the new mana-research /v1/research/compare
endpoint, merges their answers + citations into a single markdown context
block that the AI can cite from, and attaches the runId so the user can
revisit the comparison in Research Lab later.

AI missions keep calling the tool with no depth arg — they still get
free RSS results. Only explicit depth=deep consumes credits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-17 15:31:09 +02:00
parent 8f0a74b2e7
commit 011946bb4b

View file

@ -12,9 +12,18 @@
import type { ModuleTool } from '$lib/data/tools/types';
import { discoverByQuery, searchFeeds } from './api';
import { compareResearch } from '$lib/modules/research-lab/api';
import type { AgentAnswer } from '$lib/modules/research-lab/types';
const MAX_RESULTS = 15;
const DEEP_AGENT_PREFERENCE = [
'perplexity-sonar',
'gemini-grounding',
'openai-responses',
'claude-web-search',
] as const;
function formatContext(
query: string,
feedCount: number,
@ -45,6 +54,28 @@ function formatContext(
return lines.join('\n');
}
function formatDeepContext(query: string, providers: string[], answers: AgentAnswer[]): string {
const lines = [
`# Deep Research — Query: ${query}`,
`Agents consulted: ${providers.join(', ')}`,
'',
];
for (let i = 0; i < answers.length; i++) {
const provider = providers[i];
const answer = answers[i];
if (!answer) continue;
lines.push(`## ${provider}`, '', answer.answer, '');
if (answer.citations.length > 0) {
lines.push('### Sources');
for (const cit of answer.citations) {
lines.push(`- [${cit.title || cit.url}](${cit.url})`);
}
lines.push('');
}
}
return lines.join('\n');
}
export const newsResearchTools: ModuleTool[] = [
{
name: 'research_news',
@ -58,16 +89,23 @@ export const newsResearchTools: ModuleTool[] = [
description: 'Thema oder Stichworte (z.B. "KI-Regulierung EU")',
required: true,
},
{
name: 'depth',
type: 'string',
description:
'"shallow" (Standard, kostenlos — RSS-Feeds durchsuchen) oder "deep" (kostet Credits — fragt 12 Research-Agents wie Perplexity/Gemini mit web_search).',
required: false,
},
{
name: 'language',
type: 'string',
description: 'Sprache der Feeds (de/en); optional',
description: 'Sprache der Feeds (de/en); optional, nur für shallow',
required: false,
},
{
name: 'limit',
type: 'number',
description: `Maximale Anzahl Treffer (Standard ${MAX_RESULTS})`,
description: `Maximale Anzahl Treffer im shallow-Modus (Standard ${MAX_RESULTS})`,
required: false,
},
],
@ -76,9 +114,42 @@ export const newsResearchTools: ModuleTool[] = [
if (!query) {
return { success: false, message: 'query is required' };
}
const depth = String(params.depth ?? 'shallow').toLowerCase();
const language = typeof params.language === 'string' ? params.language : undefined;
const limit = Math.min(Math.max(Number(params.limit) || MAX_RESULTS, 1), 50);
if (depth === 'deep') {
// Ask 2 research agents in parallel; prefer cheapest-first order.
const providers = [...DEEP_AGENT_PREFERENCE].slice(0, 2);
try {
const res = await compareResearch(query, providers);
const answers = res.results.map((r) => r.data?.answer).filter(Boolean) as AgentAnswer[];
const successful = res.results.filter((r) => r.success);
const context = formatDeepContext(
query,
successful.map((r) => r.provider),
answers
);
return {
success: true,
message: `Deep Research: ${successful.length} / ${res.results.length} Agents geantwortet.`,
data: {
context,
runId: res.runId,
answers: answers.map((a, i) => ({
provider: successful[i]?.provider,
answer: a.answer,
citations: a.citations,
})),
},
};
} catch (err) {
const message = err instanceof Error ? err.message : 'Deep research fehlgeschlagen';
return { success: false, message };
}
}
// shallow (default): RSS discovery + keyword search
const discovered = await discoverByQuery(query, language);
const feedUrls = discovered.feeds.slice(0, 10).map((f) => f.url);
if (feedUrls.length === 0) {