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

@@ -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."""