From d274a9c9b6ab3a1b05386274e04981cd4479c16e Mon Sep 17 00:00:00 2001 From: claude-dev Date: Thu, 5 Mar 2026 17:28:36 +0100 Subject: [PATCH] Fix: Echte UTF-8-Umlaute in KI-generierten Inhalten erzwingen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Claude CLI Umgebung: LANG=C.UTF-8, LC_ALL=C.UTF-8 setzen - Alle 10 Agent-Prompts: Explizite Anweisung für echte Umlaute (ä,ö,ü,ß) statt Umschreibungen (ae,oe,ue,ss) - Betrifft: Researcher, Analyzer, Factchecker (jeweils initial + inkrementell) Co-Authored-By: Claude Opus 4.6 --- src/agents/analyzer.py | 4 +++ src/agents/claude_client.py | 7 ++++- src/agents/factchecker.py | 4 +++ src/agents/researcher.py | 57 ++++++++++++++++++++++++++++--------- 4 files changed, 58 insertions(+), 14 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 1defecb..cdc99b2 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -11,6 +11,7 @@ logger = logging.getLogger("osint.analyzer") ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System. HEUTIGES DATUM: {today} AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). VORFALL: {title} KONTEXT: {description} @@ -50,6 +51,7 @@ BRIEFING_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonit Du erstellst ein strukturiertes Briefing für eine Hintergrundrecherche. HEUTIGES DATUM: {today} AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). THEMA: {title} KONTEXT: {description} @@ -101,6 +103,7 @@ Antworte NUR mit dem JSON-Objekt. Keine Einleitung, keine Erklärung.""" INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System. HEUTIGES DATUM: {today} AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). VORFALL: {title} KONTEXT: {description} @@ -144,6 +147,7 @@ INCREMENTAL_BRIEFING_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für e Du aktualisierst ein strukturiertes Briefing für eine Hintergrundrecherche. HEUTIGES DATUM: {today} AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). THEMA: {title} KONTEXT: {description} diff --git a/src/agents/claude_client.py b/src/agents/claude_client.py index f550d11..3282cba 100644 --- a/src/agents/claude_client.py +++ b/src/agents/claude_client.py @@ -56,7 +56,12 @@ async def call_claude(prompt: str, tools: str | None = "WebSearch,WebFetch", mod process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, - env={"PATH": "/usr/local/bin:/usr/bin:/bin", "HOME": "/home/claude-dev"}, + env={ + "PATH": "/usr/local/bin:/usr/bin:/bin", + "HOME": "/home/claude-dev", + "LANG": "C.UTF-8", + "LC_ALL": "C.UTF-8", + }, ) try: stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=CLAUDE_TIMEOUT) diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index 4dd8b00..6d4d61f 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -9,6 +9,7 @@ logger = logging.getLogger("osint.factchecker") FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System. AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). VORFALL: {title} @@ -46,6 +47,7 @@ Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung.""" RESEARCH_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für eine Hintergrundrecherche in einem OSINT-Lagemonitoring-System. AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). THEMA: {title} @@ -86,6 +88,7 @@ Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung.""" INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System. AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). VORFALL: {title} @@ -126,6 +129,7 @@ Antworte NUR mit dem JSON-Array.""" INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für eine Hintergrundrecherche in einem OSINT-Lagemonitoring-System. AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). THEMA: {title} diff --git a/src/agents/researcher.py b/src/agents/researcher.py index 3354138..5b09914 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -9,6 +9,7 @@ logger = logging.getLogger("osint.researcher") RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Recherche-Agent für ein Lagemonitoring-System. AUSGABESPRACHE: {output_language} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). AUFTRAG: Suche nach aktuellen Informationen zu folgendem Vorfall: Titel: {title} @@ -37,6 +38,7 @@ 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} +WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss). AUFTRAG: Führe eine umfassende Hintergrundrecherche durch zu: Titel: {title} @@ -83,7 +85,7 @@ LANG_DEEP_INTERNATIONAL = "- Suche in Deutsch, Englisch und weiteren relevanten 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. +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. Generiere außerdem optimierte Suchbegriffe für das RSS-Matching. LAGE: {title} KONTEXT: {description} @@ -98,7 +100,18 @@ REGELN: - Bei "Internationale Quellen: Nein": Keine internationalen Feeds auswählen - Allgemeine Nachrichtenfeeds (tagesschau, Spiegel etc.) sind fast immer relevant - QUELLENVIELFALT: Wähle pro Domain maximal 2-3 Feeds. Bevorzuge eine breite Mischung aus verschiedenen Quellen statt vieler Feeds derselben Domain. -- Antworte NUR mit einem JSON-Array der Nummern, z.B. [1, 2, 5, 12]""" + +KEYWORDS-REGELN: +- Generiere 5-10 thematisch relevante Suchbegriffe für das RSS-Matching +- Nur inhaltlich relevante Begriffe (Personen, Orte, Themen, Organisationen) +- KEINE Jahreszahlen (2024, 2025, 2026 etc.) +- KEINE Monatsnamen (Januar, Februar, März etc.) +- KEINE generischen Wörter (aktuell, news, update etc.) +- Begriffe in Kleinbuchstaben +- Sowohl deutsche als auch englische Begriffe wo sinnvoll + +Antworte NUR mit einem JSON-Objekt in diesem Format: +{{"feeds": [1, 2, 5, 12], "keywords": ["begriff1", "begriff2", "begriff3"]}}""" class ResearcherAgent: @@ -110,13 +123,13 @@ class ResearcherAgent: description: str, international: bool, feeds_metadata: list[dict], - ) -> tuple[list[dict], ClaudeUsage | None]: + ) -> tuple[list[dict], list[str] | None, 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) + (ausgewählte Feeds, keywords, usage) — Bei Fehler: (alle Feeds, None, None) """ # Feed-Liste als nummerierte Übersicht formatieren feed_lines = [] @@ -135,13 +148,31 @@ class ResearcherAgent: 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 + # Neues Format: JSON-Objekt mit "feeds" und "keywords" + keywords = None + indices = None + + # Versuche JSON-Objekt zu parsen + obj_match = re.search(r'\{[^{}]*"feeds"\s*:\s*\[[\d\s,]+\][^{}]*\}', result, re.DOTALL) + if obj_match: + try: + obj = json.loads(obj_match.group()) + indices = obj.get("feeds", []) + raw_keywords = obj.get("keywords", []) + if isinstance(raw_keywords, list) and raw_keywords: + keywords = [str(k).lower().strip() for k in raw_keywords if k] + logger.info(f"Feed-Selektion Keywords: {keywords}") + except (json.JSONDecodeError, ValueError): + pass + + # Fallback: altes Array-Format + if indices is None: + arr_match = re.search(r'\[[\d\s,]+\]', result) + if not arr_match: + logger.warning("Feed-Selektion: Kein JSON in Antwort, nutze alle Feeds") + return feeds_metadata, None, usage + indices = json.loads(arr_match.group()) - indices = json.loads(match.group()) selected = [] for idx in indices: if isinstance(idx, int) and 1 <= idx <= len(feeds_metadata): @@ -149,16 +180,16 @@ class ResearcherAgent: if not selected: logger.warning("Feed-Selektion: Keine gültigen Indizes, nutze alle Feeds") - return feeds_metadata, usage + return feeds_metadata, keywords, usage logger.info( f"Feed-Selektion: {len(selected)} von {len(feeds_metadata)} Feeds ausgewählt" ) - return selected, usage + return selected, keywords, usage except Exception as e: logger.warning(f"Feed-Selektion fehlgeschlagen ({e}), nutze alle Feeds") - return feeds_metadata, None + return feeds_metadata, None, 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."""