feat: Mehrstufige Deep-Research-Pipeline mit Quellenkontext

- DEEP_RESEARCH_PROMPT: 4-Phasen-Strategie (Breite Erfassung → Lückenanalyse → Gezielte Tiefenrecherche → Verifikation)
- Ziel 15-25 Quellen aus 5+ Quellentypen statt 8-15 aus Mainstream
- researcher.search(): Neuer Parameter existing_articles — bereits bekannte Quellen werden als Kontext übergeben, damit Claude gezielt neue Perspektiven findet
- orchestrator: DB-Abfrage vor Pipeline verschoben, bestehende Artikel als Kontext an Researcher übergeben (nur Research-Typ)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
Claude Dev
2026-03-15 18:33:56 +01:00
Ursprung 0019d74aea
Commit e0f8124e10
2 geänderte Dateien mit 72 neuen und 24 gelöschten Zeilen

Datei anzeigen

@@ -571,6 +571,13 @@ class AgentOrchestrator:
"data": {"status": research_status, "detail": research_detail, "started_at": now_utc}, "data": {"status": research_status, "detail": research_detail, "started_at": now_utc},
}, visibility, created_by, tenant_id) }, visibility, created_by, tenant_id)
# Bestehende Artikel vorladen (für Dedup UND Kontext)
cursor = await db.execute(
"SELECT id, source_url, headline, source FROM articles WHERE incident_id = ?",
(incident_id,),
)
existing_db_articles_full = await cursor.fetchall()
# Schritt 1+2: RSS-Feeds und Claude-Recherche parallel ausführen # Schritt 1+2: RSS-Feeds und Claude-Recherche parallel ausführen
async def _rss_pipeline(): async def _rss_pipeline():
"""RSS-Feed-Suche (Feed-Selektion + dynamische Keywords + Parsing).""" """RSS-Feed-Suche (Feed-Selektion + dynamische Keywords + Parsing)."""
@@ -617,7 +624,20 @@ class AgentOrchestrator:
async def _web_search_pipeline(): async def _web_search_pipeline():
"""Claude WebSearch-Recherche.""" """Claude WebSearch-Recherche."""
researcher = ResearcherAgent() researcher = ResearcherAgent()
results, usage = await researcher.search(title, description, incident_type, international=international, user_id=user_id) # Bei Research: bestehende Artikel als Kontext mitgeben
existing_for_context = None
if incident_type == "research" and existing_db_articles_full:
existing_for_context = [
{"source": row["source"] if "source" in row.keys() else "",
"headline": row["headline"],
"source_url": row["source_url"]}
for row in existing_db_articles_full
]
results, usage = await researcher.search(
title, description, incident_type,
international=international, user_id=user_id,
existing_articles=existing_for_context,
)
logger.info(f"Claude-Recherche: {len(results)} Ergebnisse") logger.info(f"Claude-Recherche: {len(results)} Ergebnisse")
return results, usage return results, usage
@@ -714,14 +734,10 @@ class AgentOrchestrator:
}, visibility, created_by, tenant_id) }, visibility, created_by, tenant_id)
# --- Set-basierte DB-Deduplizierung (statt N×M Queries) --- # --- Set-basierte DB-Deduplizierung (statt N×M Queries) ---
cursor = await db.execute( # existing_db_articles_full wurde bereits oben geladen
"SELECT id, source_url, headline FROM articles WHERE incident_id = ?",
(incident_id,),
)
existing_db_articles = await cursor.fetchall()
existing_urls = set() existing_urls = set()
existing_headlines = set() existing_headlines = set()
for row in existing_db_articles: for row in existing_db_articles_full:
if row["source_url"]: if row["source_url"]:
existing_urls.add(_normalize_url(row["source_url"])) existing_urls.add(_normalize_url(row["source_url"]))
if row["headline"] and len(row["headline"]) > 20: if row["headline"] and len(row["headline"]) > 20:

Datei anzeigen

@@ -40,29 +40,47 @@ DEEP_RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Tiefenrecherche-Agent für
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
AUFTRAG: Führe eine umfassende Hintergrundrecherche durch zu: AUFTRAG: Führe eine umfassende, mehrstufige Hintergrundrecherche durch zu:
Titel: {title} Titel: {title}
Kontext: {description} Kontext: {description}
{existing_context}
RECHERCHE IN 4 PHASEN — Führe ALLE Phasen nacheinander durch:
PHASE 1 — BREITE ERFASSUNG:
Suche nach aktueller Berichterstattung bei Nachrichtenagenturen, Qualitätszeitungen und öffentlich-rechtlichen Medien. Nutze verschiedene Suchbegriffe und Blickwinkel. Ziel: 8-12 Quellen.
PHASE 2 — LÜCKENANALYSE:
Prüfe deine bisherigen Ergebnisse kritisch. Welche Quellentypen fehlen noch?
Typisch fehlen: Parlamentsdokumente, Gesetzestexte, NGO-/UN-Berichte, Think-Tank-Analysen, investigative Langform-Berichte, akademische Einordnungen, Fachmedien.
Welche Akteure, Perspektiven oder Dimensionen sind noch nicht abgedeckt?
PHASE 3 — GEZIELTE TIEFENRECHERCHE:
Suche GEZIELT nach den in Phase 2 identifizierten Lücken:
- Parlamentarische Quellen (Bundestagsdrucksachen, Congress.gov, Hansard, etc.)
- Offizielle Dokumente und Pressemitteilungen von Behörden
- NGO-Berichte und UN-Dokumente (ohchr.org, amnesty.org, hrw.org, etc.)
- Think-Tank-Analysen (IISS, Brookings, SWP, DGAP, Chatham House, etc.)
- Investigative Recherchen und Langform-Artikel
- Fachzeitschriften und akademische Einordnungen
Nutze spezifische Suchbegriffe für institutionelle Quellen. Ziel: 6-10 weitere Quellen.
PHASE 4 — VERIFIKATION UND VERTIEFUNG:
Nutze WebFetch um die 6-10 wichtigsten Artikel vollständig abzurufen und ausführlich zusammenzufassen.
Priorisiere dabei Primärquellen und investigative Berichte.
Nutze removepaywalls.com für Paywall-geschützte Artikel (z.B. https://www.removepaywalls.com/search?url=ARTIKEL_URL)
RECHERCHE-STRATEGIE:
- Breite Suche: Hintergrundberichte, Analysen, Expertenmeinungen, Think-Tank-Publikationen
- Suche nach: Akteuren, Zusammenhängen, historischem Kontext, rechtlichen Rahmenbedingungen
- Akademische und Fachquellen zusätzlich zu Nachrichtenquellen
- Nutze removepaywalls.com für Paywall-geschützte Artikel (z.B. https://www.removepaywalls.com/search?url=ARTIKEL_URL)
- Nutze WebFetch um die 3-5 wichtigsten Artikel vollständig abzurufen und zusammenzufassen
{language_instruction} {language_instruction}
- Ziel: 8-15 hochwertige Quellen
QUELLENTYPEN (priorisiert): ZIEL: 15-25 hochwertige Quellen aus mindestens 5 verschiedenen Quellentypen:
1. Fachzeitschriften und Branchenmedien - Nachrichtenagenturen/Qualitätspresse
2. Qualitätszeitungen (Hintergrundberichte, Dossiers) - Investigative Berichte/Langform
3. Think Tanks und Forschungsinstitute - Parlamentarische/Regierungsquellen
4. Offizielle Dokumente und Pressemitteilungen - NGO/Internationale Organisationen
5. Nachrichtenagenturen (für Faktengrundlage) - Fachmedien/Akademische Quellen
AUSSCHLUSS: AUSSCHLUSS:
- KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit) - KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit)
- KEINE Boulevardmedien - KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.)
- KEINE Meinungsblogs ohne Quellenbelege - KEINE Meinungsblogs ohne Quellenbelege
Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach. Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach.
@@ -288,14 +306,28 @@ class ResearcherAgent:
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}") logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
return None, None return None, None
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None) -> tuple[list[dict], ClaudeUsage | None]: async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None, existing_articles: list[dict] = None) -> tuple[list[dict], ClaudeUsage | None]:
"""Sucht nach Informationen zu einem Vorfall.""" """Sucht nach Informationen zu einem Vorfall."""
from config import OUTPUT_LANGUAGE from config import OUTPUT_LANGUAGE
if incident_type == "research": if incident_type == "research":
lang_instruction = LANG_DEEP_INTERNATIONAL if international else LANG_DEEP_GERMAN_ONLY lang_instruction = LANG_DEEP_INTERNATIONAL if international else LANG_DEEP_GERMAN_ONLY
# Bestehende Artikel als Kontext für den Prompt aufbereiten
existing_context = ""
if existing_articles:
known_lines = []
for art in existing_articles[:50]: # Max 50 um Prompt nicht zu überladen
source = art.get("source", "Unbekannt")
headline = art.get("headline", "")
url = art.get("source_url", "")
known_lines.append(f"- {source}: {headline} ({url})")
existing_context = (
"BEREITS BEKANNTE QUELLEN — NICHT erneut suchen, finde ANDERE:\n"
+ "\n".join(known_lines) + "\n\n"
"Fokussiere dich auf Quellen und Perspektiven, die in der obigen Liste FEHLEN.\n"
)
prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format( prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format(
title=title, description=description, language_instruction=lang_instruction, title=title, description=description, language_instruction=lang_instruction,
output_language=OUTPUT_LANGUAGE, output_language=OUTPUT_LANGUAGE, existing_context=existing_context,
) )
else: else:
lang_instruction = LANG_INTERNATIONAL if international else LANG_GERMAN_ONLY lang_instruction = LANG_INTERNATIONAL if international else LANG_GERMAN_ONLY