From 011946bb4be5eb896024f454a2e8aa17329119df Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 17 Apr 2026 15:31:09 +0200 Subject: [PATCH] feat(news-research): add depth=shallow|deep option to research_news tool MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../src/lib/modules/news-research/tools.ts | 75 ++++++++++++++++++- 1 file changed, 73 insertions(+), 2 deletions(-) diff --git a/apps/mana/apps/web/src/lib/modules/news-research/tools.ts b/apps/mana/apps/web/src/lib/modules/news-research/tools.ts index ce9d9c0a2..056363abe 100644 --- a/apps/mana/apps/web/src/lib/modules/news-research/tools.ts +++ b/apps/mana/apps/web/src/lib/modules/news-research/tools.ts @@ -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 1–2 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) {