From 23ac6d6fd7cb8cf3581c7cd79d3d233c76e5df0d Mon Sep 17 00:00:00 2001 From: claude-dev Date: Wed, 4 Mar 2026 20:32:31 +0100 Subject: [PATCH] Fix: researcher.py korrupte Datei repariert (base64-Transfer) --- src/agents/researcher.py | 232 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 230 insertions(+), 2 deletions(-) diff --git a/src/agents/researcher.py b/src/agents/researcher.py index 5d60c8a..9f990b4 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -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. -AUSPSN]][XY_BUQQΈXHXZY[[[ܛX][ۙ[H[[Hܙ[][]_B۝^\ܚ\[۟BQSHXH\ZH\p[XX[]Y[[ -XX[Y[\[]X[]0ޙZ][[0홙[X2 \XXHYYY[p((-%8M5QݥѕȽ`%хɅQQIФ(-%9 ձمɑ Mո5ь)Յ}Սѥ(ѕͥЁչɅMձѥ(9锁]эմ̴ԁݥѥѕѥٽՙչ͛ɱɔiͅչԁѕ(9锁ɕٕ̹݅ȁA͍݅鱥ѥMiЬMh輽ܹɕٕ̹͕݅ɍɰIQ%-1}UI0()ɝ͔UMM !1'e1% ́)M=8Ʌ񍬰ɭչٽȀmȁ))́ЁЁ͔(=ɥq͍ɥ(}U͕չ͝Ʌ́=ɥɅݕФ(ͽɍ9ȁEՕIѕ̈х͍Ԉ(ͽɍ}ɰUI0́ѥ(ѕ}յiͅչ́%̴̀ԁO锰͝Ʌ ȁ]эՙѥ聅͛ɱɔiͅչԴO锤(ՅMɅ́=ɥ̀Ȉ(Չ͡}ЈYٙѱཱչ͑մ́Ѐ%M<ɵФ()ݽє9UHЁ)M=8Ʌ丁-չɭչ()A}IMI !}AI=5AQ}Q5A1Q􀈈ԁЁ=M%9PQɕɍЁȁ1ѽɥMѕ)UMQAIM!UIcΔ \ No newline at end of file +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 []