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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren