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:
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren