Fix: researcher.py korrupte Datei repariert (base64-Transfer)
Dieser Commit ist enthalten in:
@@ -8,5 +8,233 @@ from config import CLAUDE_MODEL_FAST
|
|||||||
logger = logging.getLogger("osint.researcher")
|
logger = logging.getLogger("osint.researcher")
|
||||||
|
|
||||||
RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Recherche-Agent für ein Lagemonitoring-System.
|
RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Recherche-Agent für ein Lagemonitoring-System.
|
||||||
AUSPS‘ÑNˆÈÝ]]Û[™ÝXYÙ_B‚<42>UQ•<14>QΈÝXÚH˜XÚZÝY[[ˆ[™›Ü›X][Û™[ˆ<1E>H›ÛÙ[™[H›Ü™˜[‚•][ˆÝ]_B’ÛÛ<C39B>^ˆÙ\ØÜš\[ÛŸB‚”‘QÑS‚‹HÝXÚH<1B>\ˆ™ZHÙ\špíœÙ[ˆ˜XÚšXÚ[œ]Y[[ˆ
|
AUSGABESPRACHE: {output_language}
|
||||||
˜XÚšXÚ[˜YÙ[<5B>\™[‹]X[]0éÞ™Z][™Ù[‹0í™™™[<5B>XÚ2â\™XÚXÚHYYY[‹™pí±œ¡•¹(¤(´<>-%8<EFBFBD>M½<EFBFBD>¥…°<EFBFBD>5•‘¥„€¡QÝ¥ÑѕȽ`°<>…<>•‰½½¬°<C2AC>%¹ÍÑ…<EFBFBD>É…´°<EFBFBD>Q¥Q½¬°<EFBFBD>I•‘‘¥Ð¤(´<>-%9<14> ½Õ±•Ù…É‘µ•‘¥•¸€¡ ¥±<C2A5>°<EFBFBD>MÕ¸°<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
|
|
||||||
|
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 []
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren