fix(ai): swap web-research pre-step from deep-research → news-research RSS

Debug log on a "Recherchiere News über Adult Learning" mission showed
the deep-research pipeline (mana-search + LLM synthesis) returned 0
sources but reported success — planner then either hallucinated or
fell back to create_task. The webapp already has a documented
RSS-based research path in news-research/{discoverByQuery,searchFeeds}
that's faster, free (no credits), and matches the companion-flow
contract written in news-research/tools.ts: "research_news → save_news_article".

Now:
- Pre-step calls discoverByQuery + searchFeeds directly.
- Empty discovery / empty results throw an explicit error so the
  failure-injection branch surfaces it to the planner instead of
  silently feeding a "0 sources" success message.
- Injected ResolvedInput now carries explicit instructions ("rufe für
  jeden relevanten Artikel save_news_article auf — erfinde keine
  URLs") so the planner doesn't have to infer the next move.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 22:07:29 +02:00
parent cda70b6a81
commit 98668b69a2

View file

@ -31,7 +31,7 @@ import { getAvailableToolsForAi } from './available-tools';
import { executeTool } from '../../tools/executor';
import { db } from '../../database';
import { decryptRecords } from '../../crypto';
import { researchApi } from '$lib/api/research';
import { discoverByQuery, searchFeeds } from '$lib/modules/news-research/api';
import { isAiDebugEnabled, recordAiDebug, type AiDebugEntry } from './debug';
import { makeAgentActor, LEGACY_AI_PRINCIPAL, type Actor } from '../../events/actor';
import { getAgent } from '../agents/store';
@ -418,41 +418,59 @@ interface WebResearchOutcome {
}
async function runWebResearch(mission: Mission): Promise<WebResearchOutcome | null> {
const result = await researchApi.startSync({
// Tag the run with the mission id so backend logs can correlate.
questionId: `mission:${mission.id}`,
title: mission.objective.slice(0, 500),
description: mission.conceptMarkdown?.slice(0, 4000),
depth: 'quick',
});
if (result.status === 'error' || !result.summary) return null;
// RSS-based news research via news-research module: discoverByQuery
// finds matching feeds, searchFeeds ranks recent articles by relevance.
// Robust (own infra, no external SearXNG dependency), free (no credits),
// and the documented happy-path for the AI companion's news flow.
// Detect language hint from objective: German chars/words → de, else en.
const objective = mission.objective;
const isGerman = /[äöüß]|recherchier|aktuelle|neueste|finde|suche/i.test(objective);
const language = isGerman ? 'de' : 'en';
const sources = await researchApi.listSources(result.id);
const sourcesBlock = sources
.slice(0, 8)
.map((s, i) =>
`[${i + 1}] ${s.title || s.url}\n URL: ${s.url}\n ${s.snippet ?? ''}`.trim()
const discovered = await discoverByQuery(objective, language);
const feedUrls = discovered.feeds.slice(0, 10).map((f) => f.url);
if (feedUrls.length === 0) {
// No feeds discovered — surface as failure so the planner doesn't
// pretend it has data. Caller wraps this in a "research failed"
// ResolvedInput.
throw new Error(
`news-research: keine RSS-Feeds für "${objective}" gefunden (${discovered.searched ?? 0} Quellen abgesucht).`
);
}
const { articles } = await searchFeeds(feedUrls, objective, { limit: 10 });
if (articles.length === 0) {
throw new Error(
`news-research: ${feedUrls.length} Feeds gefunden, aber 0 Artikel matchen "${objective}".`
);
}
const articlesBlock = articles
.map((a, i) =>
`[${i + 1}] ${a.title}\n URL: ${a.url}\n ${a.publishedAt ?? 'unbekannt'} · ${a.feedUrl}\n ${a.excerpt ?? ''}`.trim()
)
.join('\n\n');
const content = [
`Zusammenfassung (Tiefe: ${result.depth}):`,
result.summary,
`Recherche-Ergebnis (RSS, ${feedUrls.length} Feeds, ${articles.length} Treffer):`,
'',
'Quellen (kopiere die URL beim Aufruf von save_news_article):',
sourcesBlock || '(keine Quellen)',
'WICHTIG: Für jeden relevanten Artikel rufe save_news_article(url, title, summary) auf.',
'Erfinde keine URLs — nutze ausschließlich die hier gelisteten.',
'Wähle 3-5 Artikel die am besten zum Mission-Ziel passen.',
'',
articlesBlock,
].join('\n');
return {
input: {
id: result.id,
module: 'research',
table: 'researchResults',
title: 'Web-Recherche zu diesem Auftrag',
id: `news-research-${Date.now()}`,
module: 'news-research',
table: 'rssArticles',
title: 'News-Recherche (RSS) zu diesem Auftrag',
content,
},
sourceCount: sources.length,
summary: result.summary,
sourceCount: articles.length,
summary: `${articles.length} Artikel aus ${feedUrls.length} Feeds.`,
};
}