Fix: Echte UTF-8-Umlaute in KI-generierten Inhalten erzwingen

- 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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-03-05 17:28:36 +01:00
Ursprung b604a80842
Commit d274a9c9b6
4 geänderte Dateien mit 58 neuen und 14 gelöschten Zeilen

Datei anzeigen

@@ -11,6 +11,7 @@ logger = logging.getLogger("osint.analyzer")
ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System. ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
HEUTIGES DATUM: {today} HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
VORFALL: {title} VORFALL: {title}
KONTEXT: {description} 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. Du erstellst ein strukturiertes Briefing für eine Hintergrundrecherche.
HEUTIGES DATUM: {today} HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
THEMA: {title} THEMA: {title}
KONTEXT: {description} 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. INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE = """Du bist ein OSINT-Analyse-Agent für ein Lagemonitoring-System.
HEUTIGES DATUM: {today} HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
VORFALL: {title} VORFALL: {title}
KONTEXT: {description} 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. Du aktualisierst ein strukturiertes Briefing für eine Hintergrundrecherche.
HEUTIGES DATUM: {today} HEUTIGES DATUM: {today}
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
THEMA: {title} THEMA: {title}
KONTEXT: {description} KONTEXT: {description}

Datei anzeigen

@@ -56,7 +56,12 @@ async def call_claude(prompt: str, tools: str | None = "WebSearch,WebFetch", mod
process = await asyncio.create_subprocess_exec( process = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, *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: try:
stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=CLAUDE_TIMEOUT) stdout, stderr = await asyncio.wait_for(process.communicate(), timeout=CLAUDE_TIMEOUT)

Datei anzeigen

@@ -9,6 +9,7 @@ logger = logging.getLogger("osint.factchecker")
FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System. FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
VORFALL: {title} 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. RESEARCH_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für eine Hintergrundrecherche in einem OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
THEMA: {title} 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. INCREMENTAL_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für ein OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
VORFALL: {title} 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. INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE = """Du bist ein Faktencheck-Agent für eine Hintergrundrecherche in einem OSINT-Lagemonitoring-System.
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
THEMA: {title} THEMA: {title}

Datei anzeigen

@@ -9,6 +9,7 @@ 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.
AUSGABESPRACHE: {output_language} AUSGABESPRACHE: {output_language}
WICHTIG: Verwende IMMER echte UTF-8-Umlaute (ä, ö, ü, ß) — NIEMALS Umschreibungen (ae, oe, ue, ss).
AUFTRAG: Suche nach aktuellen Informationen zu folgendem Vorfall: AUFTRAG: Suche nach aktuellen Informationen zu folgendem Vorfall:
Titel: {title} 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. DEEP_RESEARCH_PROMPT_TEMPLATE = """Du bist ein OSINT-Tiefenrecherche-Agent für ein Lagemonitoring-System.
AUSGABESPRACHE: {output_language} 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 Hintergrundrecherche durch zu:
Titel: {title} 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" 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} LAGE: {title}
KONTEXT: {description} KONTEXT: {description}
@@ -98,7 +100,18 @@ REGELN:
- Bei "Internationale Quellen: Nein": Keine internationalen Feeds auswählen - Bei "Internationale Quellen: Nein": Keine internationalen Feeds auswählen
- Allgemeine Nachrichtenfeeds (tagesschau, Spiegel etc.) sind fast immer relevant - 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. - 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: class ResearcherAgent:
@@ -110,13 +123,13 @@ class ResearcherAgent:
description: str, description: str,
international: bool, international: bool,
feeds_metadata: list[dict], 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. """Lässt Claude die relevanten Feeds für eine Lage vorauswählen.
Nutzt Haiku (CLAUDE_MODEL_FAST) für diese einfache Aufgabe. Nutzt Haiku (CLAUDE_MODEL_FAST) für diese einfache Aufgabe.
Returns: 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-Liste als nummerierte Übersicht formatieren
feed_lines = [] feed_lines = []
@@ -135,13 +148,31 @@ class ResearcherAgent:
try: try:
result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST) result, usage = await call_claude(prompt, tools=None, model=CLAUDE_MODEL_FAST)
# JSON-Array aus Antwort extrahieren # Neues Format: JSON-Objekt mit "feeds" und "keywords"
match = re.search(r'\[[\d\s,]+\]', result) keywords = None
if not match: indices = None
logger.warning("Feed-Selektion: Kein JSON-Array in Antwort, nutze alle Feeds")
return feeds_metadata, usage # 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 = [] selected = []
for idx in indices: for idx in indices:
if isinstance(idx, int) and 1 <= idx <= len(feeds_metadata): if isinstance(idx, int) and 1 <= idx <= len(feeds_metadata):
@@ -149,16 +180,16 @@ class ResearcherAgent:
if not selected: if not selected:
logger.warning("Feed-Selektion: Keine gültigen Indizes, nutze alle Feeds") logger.warning("Feed-Selektion: Keine gültigen Indizes, nutze alle Feeds")
return feeds_metadata, usage return feeds_metadata, keywords, usage
logger.info( logger.info(
f"Feed-Selektion: {len(selected)} von {len(feeds_metadata)} Feeds ausgewählt" f"Feed-Selektion: {len(selected)} von {len(feeds_metadata)} Feeds ausgewählt"
) )
return selected, usage return selected, keywords, usage
except Exception as e: except Exception as e:
logger.warning(f"Feed-Selektion fehlgeschlagen ({e}), nutze alle Feeds") 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]: 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.""" """Sucht nach Informationen zu einem Vorfall."""