From e0f8124e10dcbfbfd098a5aef5276d8428fc9288 Mon Sep 17 00:00:00 2001 From: Claude Dev Date: Sun, 15 Mar 2026 18:33:56 +0100 Subject: [PATCH] feat: Mehrstufige Deep-Research-Pipeline mit Quellenkontext MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- src/agents/orchestrator.py | 30 +++++++++++++---- src/agents/researcher.py | 66 ++++++++++++++++++++++++++++---------- 2 files changed, 72 insertions(+), 24 deletions(-) diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index 9fbfb43..4b2aaa0 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -571,6 +571,13 @@ class AgentOrchestrator: "data": {"status": research_status, "detail": research_detail, "started_at": now_utc}, }, 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 async def _rss_pipeline(): """RSS-Feed-Suche (Feed-Selektion + dynamische Keywords + Parsing).""" @@ -617,7 +624,20 @@ class AgentOrchestrator: async def _web_search_pipeline(): """Claude WebSearch-Recherche.""" 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") return results, usage @@ -714,14 +734,10 @@ class AgentOrchestrator: }, visibility, created_by, tenant_id) # --- Set-basierte DB-Deduplizierung (statt N×M Queries) --- - cursor = await db.execute( - "SELECT id, source_url, headline FROM articles WHERE incident_id = ?", - (incident_id,), - ) - existing_db_articles = await cursor.fetchall() + # existing_db_articles_full wurde bereits oben geladen existing_urls = set() existing_headlines = set() - for row in existing_db_articles: + for row in existing_db_articles_full: if row["source_url"]: existing_urls.add(_normalize_url(row["source_url"])) if row["headline"] and len(row["headline"]) > 20: diff --git a/src/agents/researcher.py b/src/agents/researcher.py index 93ca331..0428039 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -40,29 +40,47 @@ DEEP_RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Tiefenrecherche-Agent für AUSGABESPRACHE: {output_language} 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} 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} -- Ziel: 8-15 hochwertige Quellen -QUELLENTYPEN (priorisiert): -1. Fachzeitschriften und Branchenmedien -2. Qualitätszeitungen (Hintergrundberichte, Dossiers) -3. Think Tanks und Forschungsinstitute -4. Offizielle Dokumente und Pressemitteilungen -5. Nachrichtenagenturen (für Faktengrundlage) +ZIEL: 15-25 hochwertige Quellen aus mindestens 5 verschiedenen Quellentypen: +- Nachrichtenagenturen/Qualitätspresse +- Investigative Berichte/Langform +- Parlamentarische/Regierungsquellen +- NGO/Internationale Organisationen +- Fachmedien/Akademische Quellen AUSSCHLUSS: - KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit) -- KEINE Boulevardmedien +- KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.) - KEINE Meinungsblogs ohne Quellenbelege 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}") 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.""" from config import OUTPUT_LANGUAGE if incident_type == "research": 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( title=title, description=description, language_instruction=lang_instruction, - output_language=OUTPUT_LANGUAGE, + output_language=OUTPUT_LANGUAGE, existing_context=existing_context, ) else: lang_instruction = LANG_INTERNATIONAL if international else LANG_GERMAN_ONLY