Fix: researcher.py korrupte Datei repariert (base64-Transfer)

Dieser Commit ist enthalten in:
claude-dev
2026-03-04 20:32:31 +01:00
Ursprung 3d9a827bc8
Commit 23ac6d6fd7

Datei anzeigen

@@ -8,5 +8,233 @@ from config import CLAUDE_MODEL_FAST
logger = logging.getLogger("osint.researcher")
RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Recherche-Agent für ein Lagemonitoring-System.
AUSPSÑNˆÈÝ]]Û[™ÝXYÙ_B<42>UQ•<14>ˆÝXÚH˜Y[[ˆ[™ÜX][Û™[ˆ<1E>H›ÛÙ[™[H›Ü™˜[‚•][ˆÝ]_BÛÛ<C39B>^ˆÙ\ØÜš\[ÛŸBQÑSHÝXÚH<1B>\ˆ™ZHÙ\špíœÙ[ˆ˜šXÚ[œ]Y[[ˆ
˜šXÚ[˜YÙ[<5B>\™[]X[]0éÞ™Z][™Ù[0í™™™[<5B>XÚ2â \™XÚHYYY[™pí±œ¡•¹(¤(´<>-%8<EFBFBD><EFBFBD>¥…°<EFBFBD>5•¥„€¡QÝ¥ÑѕȽ`°<><>•‰½½¬°<C2AC>%¹ÍÑ…<EFBFBD>É…´°<EFBFBD>­Q½¬°<EFBFBD>I•¥Ð¤(´<>-%9<14> ½Õ±•Ù…É‘µ•‘¥•¸€¡ ¥±<C2A5>°<EFBFBD>¸°<C2B8>…¥±ä<C2B1>5…¥°<C2A5>•ÑŒ¸¤)í±…¹<E280A6>Õ…<C395>•}¥¹ÍÑÉÕ<C389>Ñ¥½¹ô(´<>­Ñ•¹‰…Í¥•ÉÐ<C389>Õ¹<C395><C2B9>¹•ÕÑÉ…°€´<E282AC>­•¥¹”<C2B9>MÁ•­Õ±…Ñ¥½¹•¸(´<>9ÕÑé”<C3A9>]•‰•Ñ<E280A2> <EFBFBD>Õ´<C395>¥”€Ì´Ô<C2B4>Ý¥<C39D>¡Ñ¥<C391>ÍÑ•¸<E280A2>ÉÑ¥­•°<E280A2>Ù½±±ÍÓ¹¥œ<C2A5>…‰éÕÉÕ™•¸<E280A2>Õ¹<C395><C2B9>…ÕÍñ¡É±¥<C2B1>¡•É”<C389>iÕÍ…µµ•¹™…ÍÍÕ¹¡•¸<E280A2>éÔ<C3A9>•ÉÍÑ•±±•¸(´<>9ÕÑé”<C3A9>É•µ½Ù•Á…åÝ…±±Ì¹<C38C>½´<C2BD>ñÈ<C3B1>A…åÝ…±°µ<C2B0>•Í<E280A2>£ñÑé±¥<C2B1>¡”<C2A1>ÉÑ¥­•°€¡è¹¸<>MÁ¥•<C2A5>•°¬°<C2AC>i•¥Ð¬°<C2AC>Mh¬§Žˆ<C5BD>¡ÑÑÁÌè¼½ÝÝܹɕµ½Ù•Á…åÝ…±±Ì¹<C38C>½´½Í•…É<E280A6> ýÕɰõIQ%-1}UI0()¥ˆ<C2A5>¥”<C2A5>É<>•‰¹¥ÍÍ”<C38D>UMM
AUSGABESPRACHE: {output_language}
AUFTRAG: Suche nach aktuellen Informationen zu folgendem Vorfall:
Titel: {title}
Kontext: {description}
REGELN:
- Suche nur bei seriösen Nachrichtenquellen (Nachrichtenagenturen, Qualitätszeitungen, öffentlich-rechtliche Medien, Behörden)
- KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit)
- KEINE Boulevardmedien (Bild, Sun, Daily Mail etc.)
{language_instruction}
- Faktenbasiert und neutral - keine Spekulationen
- Nutze removepaywalls.com für Paywall-geschützte Artikel (z.B. Spiegel+, Zeit+, SZ+): https://www.removepaywalls.com/search?url=ARTIKEL_URL
- Nutze WebFetch um die 3-5 wichtigsten Artikel vollständig abzurufen und zusammenzufassen
Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach.
Jedes Element hat diese Felder:
- "headline": Originale Überschrift
- "headline_de": Übersetzung in Ausgabesprache (falls Originalsprache abweicht)
- "source": Name der Quelle (z.B. "Reuters", "tagesschau")
- "source_url": URL des Artikels
- "content_summary": Zusammenfassung des Inhalts (3-5 Sätze, in Ausgabesprache)
- "language": Sprache des Originals (z.B. "de", "en", "fr")
- "published_at": Veröffentlichungsdatum falls bekannt (ISO-Format)
Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung."""
DEEP_RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Tiefenrecherche-Agent für ein Lagemonitoring-System.
AUSGABESPRACHE: {output_language}
AUFTRAG: Führe eine umfassende Hintergrundrecherche durch zu:
Titel: {title}
Kontext: {description}
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)
AUSSCHLUSS:
- KEIN Social Media (Twitter/X, Facebook, Instagram, TikTok, Reddit)
- KEINE Boulevardmedien
- KEINE Meinungsblogs ohne Quellenbelege
Gib die Ergebnisse AUSSCHLIESSLICH als JSON-Array zurück, ohne Erklärungen davor oder danach.
Jedes Element hat diese Felder:
- "headline": Originale Überschrift
- "headline_de": Übersetzung in Ausgabesprache (falls Originalsprache abweicht)
- "source": Name der Quelle (z.B. "netzpolitik.org", "Handelsblatt")
- "source_url": URL des Artikels
- "content_summary": Ausführliche Zusammenfassung des Inhalts (5-8 Sätze, in Ausgabesprache)
- "language": Sprache des Originals (z.B. "de", "en", "fr")
- "published_at": Veröffentlichungsdatum falls bekannt (ISO-Format)
Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung."""
# Sprach-Anweisungen
LANG_INTERNATIONAL = "- Suche in Deutsch UND Englisch für internationale Abdeckung"
LANG_GERMAN_ONLY = "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen"
LANG_DEEP_INTERNATIONAL = "- Suche in Deutsch, Englisch und weiteren relevanten Sprachen"
LANG_DEEP_GERMAN_ONLY = "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen"
FEED_SELECTION_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyst. Wähle aus dieser Feed-Liste die Feeds aus, die für die Lage relevant sein könnten.
LAGE: {title}
KONTEXT: {description}
INTERNATIONALE QUELLEN: {international}
FEEDS:
{feed_list}
REGELN:
- Wähle alle Feeds die thematisch oder regional relevant sein könnten
- Lieber einen Feed zu viel als zu wenig auswählen
- Bei "Internationale Quellen: Nein": Keine internationalen Feeds auswählen
- Allgemeine Nachrichtenfeeds (tagesschau, Spiegel etc.) sind fast immer relevant
- Antworte NUR mit einem JSON-Array der Nummern, z.B. [1, 2, 5, 12]"""
class ResearcherAgent:
"""Führt OSINT-Recherchen über Claude CLI WebSearch durch."""
async def select_relevant_feeds(
self,
title: str,
description: str,
international: bool,
feeds_metadata: list[dict],
) -> tuple[list[dict], ClaudeUsage | None]:
"""Lässt Claude die relevanten Feeds für eine Lage vorauswählen.
Nutzt Haiku (CLAUDE_MODEL_FAST) für diese einfache Aufgabe.
Returns:
(ausgewählte Feeds, usage) — Bei Fehler: (alle Feeds, None)
"""
# Feed-Liste als nummerierte Übersicht formatieren
feed_lines = []
for i, feed in enumerate(feeds_metadata, 1):
feed_lines.append(
f"{i}. {feed['name']} ({feed['domain']}) [{feed['category']}]"
)
prompt = FEED_SELECTION_PROMPT_TEMPLATE.format(
title=title,
description=description or "Keine weitere Beschreibung",
international="Ja" if international else "Nein",
feed_list="\n".join(feed_lines),
)
try:
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST)
# JSON-Array aus Antwort extrahieren
match = re.search(r'\[[\d\s,]+\]', result)
if not match:
logger.warning("Feed-Selektion: Kein JSON-Array in Antwort, nutze alle Feeds")
return feeds_metadata, usage
indices = json.loads(match.group())
selected = []
for idx in indices:
if isinstance(idx, int) and 1 <= idx <= len(feeds_metadata):
selected.append(feeds_metadata[idx - 1])
if not selected:
logger.warning("Feed-Selektion: Keine gültigen Indizes, nutze alle Feeds")
return feeds_metadata, usage
logger.info(
f"Feed-Selektion: {len(selected)} von {len(feeds_metadata)} Feeds ausgewählt"
)
return selected, usage
except Exception as e:
logger.warning(f"Feed-Selektion fehlgeschlagen ({e}), nutze alle Feeds")
return feeds_metadata, None
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True) -> 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
prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format(
title=title, description=description, language_instruction=lang_instruction,
output_language=OUTPUT_LANGUAGE,
)
else:
lang_instruction = LANG_INTERNATIONAL if international else LANG_GERMAN_ONLY
prompt = RESEARCH_PROMPT_TEMPLATE.format(
title=title, description=description, language_instruction=lang_instruction,
output_language=OUTPUT_LANGUAGE,
)
try:
result, usage = await call_claude(prompt)
articles = self._parse_response(result)
# Ausgeschlossene Quellen dynamisch aus DB laden
excluded_sources = await self._get_excluded_sources()
# Ausgeschlossene Quellen filtern
filtered = []
for article in articles:
source = article.get("source", "").lower()
source_url = article.get("source_url", "").lower()
excluded = False
for excl in excluded_sources:
if excl in source or excl in source_url:
excluded = True
break
if not excluded:
# Bei nur-deutsch: nicht-deutsche Ergebnisse nachfiltern
if not international and article.get("language", "de") != "de":
continue
filtered.append(article)
logger.info(f"Recherche ergab {len(filtered)} Artikel (von {len(articles)} gefundenen, international={international})")
return filtered, usage
except Exception as e:
logger.error(f"Recherche-Fehler: {e}")
return [], None
async def _get_excluded_sources(self) -> list[str]:
"""Lädt ausgeschlossene Quellen aus der Datenbank."""
try:
from source_rules import get_source_rules
rules = await get_source_rules()
return rules.get("excluded_domains", [])
except Exception as e:
logger.warning(f"Fallback auf config.py für Excluded Sources: {e}")
from config import EXCLUDED_SOURCES
return list(EXCLUDED_SOURCES)
def _parse_response(self, response: str) -> list[dict]:
"""Parst die Claude-Antwort als JSON-Array."""
# Versuche JSON direkt zu parsen
try:
data = json.loads(response)
if isinstance(data, list):
return data
except json.JSONDecodeError:
pass
# Versuche JSON aus der Antwort zu extrahieren (zwischen [ und ])
match = re.search(r'\[.*\]', response, re.DOTALL)
if match:
try:
data = json.loads(match.group())
if isinstance(data, list):
return data
except json.JSONDecodeError:
pass
logger.warning("Konnte Claude-Antwort nicht als JSON parsen")
return []