feat(pipeline): output_language pro Org durch die Pipeline reichen

- OUTPUT_LANGUAGE Konstante aus config.py entfernt (jetzt pro Org in
  organization_settings).
- Orchestrator laedt output_language einmal pro Refresh aus der Org-Sprache.
- researcher.search(), analyzer.analyze/.analyze_incremental/.generate_latest_developments,
  factchecker.check/.check_incremental/.check_incremental_twophase bekommen
  output_language als Parameter (Default Deutsch).
- LANG_INTERNATIONAL / LANG_GERMAN_ONLY (+ Deep-Varianten) sind Funktionen,
  die je nach output_language die Sprachanweisung erzeugen (Deutsch | English
  | Fallback).
- Sprachfilter in researcher.search ist org-relativ: bei nicht-international
  werden Artikel mit Sprache != output_language_iso gefiltert.

Phase 2 von 8 (eng_demo / Org-Sprache). Bestandsorgs unveraendert, weil
Default-Setting weiterhin de (siehe Phase-1-Migration).
Dieser Commit ist enthalten in:
Claude Code
2026-05-13 20:54:28 +00:00
Ursprung d27d586003
Commit f68d25dbce
5 geänderte Dateien mit 68 neuen und 32 gelöschten Zeilen

Datei anzeigen

@@ -396,14 +396,13 @@ class AnalyzerAgent:
articles_text += f"Inhalt: {content[:800]}\n" articles_text += f"Inhalt: {content[:800]}\n"
return articles_text return articles_text
async def analyze(self, title: str, description: str, articles: list[dict], incident_type: str = "adhoc", fact_context_block: str = "") -> tuple[dict | None, ClaudeUsage | None]: async def analyze(self, title: str, description: str, articles: list[dict], incident_type: str = "adhoc", fact_context_block: str = "", output_language: str = "Deutsch") -> tuple[dict | None, ClaudeUsage | None]:
"""Erstanalyse: Analysiert alle Meldungen zu einem Vorfall (erster Refresh).""" """Erstanalyse: Analysiert alle Meldungen zu einem Vorfall (erster Refresh)."""
if not articles: if not articles:
return None, None return None, None
articles_text = self._format_articles_text(articles) articles_text = self._format_articles_text(articles)
from config import OUTPUT_LANGUAGE
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y") today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
template = BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else ANALYSIS_PROMPT_TEMPLATE template = BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else ANALYSIS_PROMPT_TEMPLATE
prompt = template.format( prompt = template.format(
@@ -411,7 +410,7 @@ class AnalyzerAgent:
description=description or "Keine weiteren Details", description=description or "Keine weiteren Details",
articles_text=articles_text, articles_text=articles_text,
today=today, today=today,
output_language=OUTPUT_LANGUAGE, output_language=output_language,
fact_context_block=fact_context_block, fact_context_block=fact_context_block,
) )
@@ -435,6 +434,7 @@ class AnalyzerAgent:
previous_sources_json: str | None, previous_sources_json: str | None,
incident_type: str = "adhoc", incident_type: str = "adhoc",
fact_context_block: str = "", fact_context_block: str = "",
output_language: str = "Deutsch",
) -> tuple[dict | None, ClaudeUsage | None]: ) -> tuple[dict | None, ClaudeUsage | None]:
"""Inkrementelle Analyse: Aktualisiert das Lagebild mit nur den neuen Artikeln. """Inkrementelle Analyse: Aktualisiert das Lagebild mit nur den neuen Artikeln.
@@ -465,7 +465,6 @@ class AnalyzerAgent:
except (json.JSONDecodeError, TypeError): except (json.JSONDecodeError, TypeError):
previous_sources_text = "Fehler beim Laden der bisherigen Quellen" previous_sources_text = "Fehler beim Laden der bisherigen Quellen"
from config import OUTPUT_LANGUAGE
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y") today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
template = INCREMENTAL_BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE template = INCREMENTAL_BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else INCREMENTAL_ANALYSIS_PROMPT_TEMPLATE
@@ -476,7 +475,7 @@ class AnalyzerAgent:
previous_sources_text=previous_sources_text, previous_sources_text=previous_sources_text,
new_articles_text=new_articles_text, new_articles_text=new_articles_text,
today=today, today=today,
output_language=OUTPUT_LANGUAGE, output_language=output_language,
fact_context_block=fact_context_block, fact_context_block=fact_context_block,
) )
@@ -580,6 +579,7 @@ class AnalyzerAgent:
summary: str, summary: str,
recent_articles: list[dict], recent_articles: list[dict],
previous_developments: str | None = None, previous_developments: str | None = None,
output_language: str = "Deutsch",
) -> tuple[str | None, ClaudeUsage | None]: ) -> tuple[str | None, ClaudeUsage | None]:
"""Generiert die Kachel 'Neueste Entwicklungen' aus dem Lagebild. """Generiert die Kachel 'Neueste Entwicklungen' aus dem Lagebild.
@@ -598,7 +598,7 @@ class AnalyzerAgent:
if not recent_articles: if not recent_articles:
return prev, None return prev, None
from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST from config import CLAUDE_MODEL_FAST
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y") today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
# Kompakter Artikel-Block: nur die für Zeitstempel/Quellen nötigen Felder. # Kompakter Artikel-Block: nur die für Zeitstempel/Quellen nötigen Felder.
@@ -629,7 +629,7 @@ class AnalyzerAgent:
summary=summary.strip(), summary=summary.strip(),
articles_text=articles_text, articles_text=articles_text,
today=today, today=today,
output_language=OUTPUT_LANGUAGE, output_language=output_language,
) )
try: try:

Datei anzeigen

@@ -462,19 +462,18 @@ class FactCheckerAgent:
lines.append(line) lines.append(line)
return "\n".join(lines) return "\n".join(lines)
async def check(self, title: str, articles: list[dict], incident_type: str = "adhoc") -> tuple[list[dict], ClaudeUsage | None]: async def check(self, title: str, articles: list[dict], incident_type: str = "adhoc", output_language: str = "Deutsch") -> tuple[list[dict], ClaudeUsage | None]:
"""Führt vollständigen Faktencheck durch (erster Refresh).""" """Führt vollständigen Faktencheck durch (erster Refresh)."""
if not articles: if not articles:
return [], None return [], None
articles_text = self._format_articles_text(articles) articles_text = self._format_articles_text(articles)
from config import OUTPUT_LANGUAGE
template = RESEARCH_FACTCHECK_PROMPT_TEMPLATE if incident_type == "research" else FACTCHECK_PROMPT_TEMPLATE template = RESEARCH_FACTCHECK_PROMPT_TEMPLATE if incident_type == "research" else FACTCHECK_PROMPT_TEMPLATE
prompt = template.format( prompt = template.format(
title=title, title=title,
articles_text=articles_text, articles_text=articles_text,
output_language=OUTPUT_LANGUAGE, output_language=output_language,
) )
try: try:
@@ -494,6 +493,7 @@ class FactCheckerAgent:
new_articles: list[dict], new_articles: list[dict],
existing_facts: list[dict], existing_facts: list[dict],
incident_type: str = "adhoc", incident_type: str = "adhoc",
output_language: str = "Deutsch",
) -> tuple[list[dict], ClaudeUsage | None]: ) -> tuple[list[dict], ClaudeUsage | None]:
"""Inkrementeller Faktencheck: Prüft nur neue Artikel gegen bestehende Fakten. """Inkrementeller Faktencheck: Prüft nur neue Artikel gegen bestehende Fakten.
@@ -506,7 +506,6 @@ class FactCheckerAgent:
articles_text = self._format_articles_text(new_articles, max_articles=15) articles_text = self._format_articles_text(new_articles, max_articles=15)
existing_facts_text = self._format_existing_facts(existing_facts) existing_facts_text = self._format_existing_facts(existing_facts)
from config import OUTPUT_LANGUAGE
if incident_type == "research": if incident_type == "research":
template = INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE template = INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE
else: else:
@@ -516,7 +515,7 @@ class FactCheckerAgent:
title=title, title=title,
articles_text=articles_text, articles_text=articles_text,
existing_facts_text=existing_facts_text, existing_facts_text=existing_facts_text,
output_language=OUTPUT_LANGUAGE, output_language=output_language,
) )
try: try:
@@ -536,6 +535,7 @@ class FactCheckerAgent:
new_articles: list[dict], new_articles: list[dict],
existing_facts: list[dict], existing_facts: list[dict],
incident_type: str = "adhoc", incident_type: str = "adhoc",
output_language: str = "Deutsch",
) -> tuple[list[dict], ClaudeUsage | None]: ) -> tuple[list[dict], ClaudeUsage | None]:
"""Zwei-Phasen inkrementeller Faktencheck: Haiku-Triage + parallele Opus-Verifikation. """Zwei-Phasen inkrementeller Faktencheck: Haiku-Triage + parallele Opus-Verifikation.
@@ -556,9 +556,9 @@ class FactCheckerAgent:
triage_facts_text = self._format_facts_for_triage(existing_facts) triage_facts_text = self._format_facts_for_triage(existing_facts)
articles_text = self._format_articles_text(new_articles, max_articles=15) articles_text = self._format_articles_text(new_articles, max_articles=15)
from config import OUTPUT_LANGUAGE, CLAUDE_MODEL_FAST from config import CLAUDE_MODEL_FAST
triage_prompt = TRIAGE_PROMPT_TEMPLATE.format( triage_prompt = TRIAGE_PROMPT_TEMPLATE.format(
output_language=OUTPUT_LANGUAGE, output_language=output_language,
fact_count=len(existing_facts), fact_count=len(existing_facts),
existing_facts_text=triage_facts_text, existing_facts_text=triage_facts_text,
article_count=len(new_articles), article_count=len(new_articles),
@@ -619,7 +619,7 @@ class FactCheckerAgent:
template = VERIFY_GROUP_PROMPT_TEMPLATE template = VERIFY_GROUP_PROMPT_TEMPLATE
prompt = template.format( prompt = template.format(
output_language=OUTPUT_LANGUAGE, output_language=output_language,
theme=theme, theme=theme,
facts_text=facts_text, facts_text=facts_text,
new_claims_text=new_claims_text, new_claims_text=new_claims_text,

Datei anzeigen

@@ -743,6 +743,10 @@ class AgentOrchestrator:
visibility = incident["visibility"] if "visibility" in incident.keys() else "public" visibility = incident["visibility"] if "visibility" in incident.keys() else "public"
created_by = incident["created_by"] if "created_by" in incident.keys() else None created_by = incident["created_by"] if "created_by" in incident.keys() else None
tenant_id = incident["tenant_id"] if "tenant_id" in incident.keys() else None tenant_id = incident["tenant_id"] if "tenant_id" in incident.keys() else None
# Org-Sprache fuer alle KI-Agenten (Lagebild, Faktencheck, Recherche)
from services.org_settings import get_org_language, language_display
output_language_iso = await get_org_language(db, tenant_id) if tenant_id else "de"
output_language = language_display(output_language_iso)
previous_summary = incident["summary"] or "" previous_summary = incident["summary"] or ""
previous_sources_json = incident["sources_json"] if "sources_json" in incident.keys() else None previous_sources_json = incident["sources_json"] if "sources_json" in incident.keys() else None
previous_developments = incident["latest_developments"] if "latest_developments" in incident.keys() else None previous_developments = incident["latest_developments"] if "latest_developments" in incident.keys() else None
@@ -923,6 +927,8 @@ class AgentOrchestrator:
international=international, user_id=user_id, international=international, user_id=user_id,
existing_articles=existing_for_context, existing_articles=existing_for_context,
preferred_sources=preferred_sources, preferred_sources=preferred_sources,
output_language=output_language,
output_language_iso=output_language_iso,
) )
logger.info( logger.info(
f"Claude-Recherche: {len(results)} Ergebnisse" f"Claude-Recherche: {len(results)} Ergebnisse"
@@ -1308,12 +1314,14 @@ class AgentOrchestrator:
title, description, new_articles_for_analysis, title, description, new_articles_for_analysis,
previous_summary, previous_sources_json, incident_type, previous_summary, previous_sources_json, incident_type,
fact_context_block=fact_context_block, fact_context_block=fact_context_block,
output_language=output_language,
) )
else: else:
logger.info("Erstanalyse: Alle Artikel werden analysiert") logger.info("Erstanalyse: Alle Artikel werden analysiert")
return await analyzer.analyze( return await analyzer.analyze(
title, description, all_articles_preloaded, incident_type, title, description, all_articles_preloaded, incident_type,
fact_context_block=fact_context_block, fact_context_block=fact_context_block,
output_language=output_language,
) )
# --- Faktencheck-Task --- # --- Faktencheck-Task ---
@@ -1327,6 +1335,7 @@ class AgentOrchestrator:
) )
return await factchecker.check_incremental_twophase( return await factchecker.check_incremental_twophase(
title, new_articles_for_analysis, existing_facts, incident_type, title, new_articles_for_analysis, existing_facts, incident_type,
output_language=output_language,
) )
else: else:
logger.info( logger.info(
@@ -1335,6 +1344,7 @@ class AgentOrchestrator:
) )
return await factchecker.check_incremental( return await factchecker.check_incremental(
title, new_articles_for_analysis, existing_facts, incident_type, title, new_articles_for_analysis, existing_facts, incident_type,
output_language=output_language,
) )
else: else:
# Alle Artikel laden falls nicht vorab geladen (Henne-Ei-Problem: # Alle Artikel laden falls nicht vorab geladen (Henne-Ei-Problem:
@@ -1346,7 +1356,7 @@ class AgentOrchestrator:
(incident_id,), (incident_id,),
) )
articles_for_check = [dict(row) for row in await cursor.fetchall()] articles_for_check = [dict(row) for row in await cursor.fetchall()]
return await factchecker.check(title, articles_for_check, incident_type) return await factchecker.check(title, articles_for_check, incident_type, output_language=output_language)
# Pipeline-Schritt 6: Faktencheck zuerst (sequenziell). Liefert den # Pipeline-Schritt 6: Faktencheck zuerst (sequenziell). Liefert den
# Faktenkontext fuer das Lagebild, damit dieses auf geprueftem Stand # Faktenkontext fuer das Lagebild, damit dieses auf geprueftem Stand
@@ -1573,6 +1583,7 @@ class AgentOrchestrator:
dev_analyzer = AnalyzerAgent() dev_analyzer = AnalyzerAgent()
dev_text, dev_usage = await dev_analyzer.generate_latest_developments( dev_text, dev_usage = await dev_analyzer.generate_latest_developments(
title, description, dev_summary_source, dev_articles, previous_developments, title, description, dev_summary_source, dev_articles, previous_developments,
output_language=output_language,
) )
if dev_usage: if dev_usage:
usage_acc.add(dev_usage) usage_acc.add(dev_usage)

Datei anzeigen

@@ -153,12 +153,37 @@ Jedes Element hat diese Felder:
Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung.""" Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung."""
# Sprach-Anweisungen # Sprach-Anweisungen (org-sprach-relativ; primary_display = "Deutsch" | "English")
LANG_INTERNATIONAL = "- Suche in Deutsch UND Englisch für internationale Abdeckung" def lang_international(primary_display: str) -> str:
LANG_GERMAN_ONLY = "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen" if primary_display == "Deutsch":
return "- Suche in Deutsch UND Englisch für internationale Abdeckung"
if primary_display == "English":
return "- Search in English AND other relevant languages for international coverage"
return f"- Suche in {primary_display} und weiteren relevanten Sprachen"
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" def lang_primary_only(primary_display: str) -> str:
if primary_display == "Deutsch":
return "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen"
if primary_display == "English":
return "- Search ONLY in English-language sources\n- NO sources in other languages"
return f"- Suche NUR auf {primary_display}\n- KEINE Quellen in anderen Sprachen"
def lang_deep_international(primary_display: str) -> str:
if primary_display == "Deutsch":
return "- Suche in Deutsch, Englisch und weiteren relevanten Sprachen"
if primary_display == "English":
return "- Search in English and other relevant languages"
return f"- Suche in {primary_display} und weiteren relevanten Sprachen"
def lang_deep_primary_only(primary_display: str) -> str:
if primary_display == "Deutsch":
return "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen"
if primary_display == "English":
return "- Search ONLY in English-language sources\n- NO sources in other languages"
return f"- Suche NUR auf {primary_display}\n- KEINE Quellen in anderen Sprachen"
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. 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.
@@ -392,7 +417,7 @@ class ResearcherAgent:
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}") logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
return None, None return None, None
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None, existing_articles: list[dict] = None, preferred_sources: list[dict] = None) -> tuple[list[dict], ClaudeUsage | None, bool]: async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None, existing_articles: list[dict] = None, preferred_sources: list[dict] = None, output_language: str = "Deutsch", output_language_iso: str = "de") -> tuple[list[dict], ClaudeUsage | None, bool]:
"""Sucht nach Informationen zu einem Vorfall. """Sucht nach Informationen zu einem Vorfall.
Returns: Returns:
@@ -400,8 +425,6 @@ class ResearcherAgent:
das JSON aber nicht extrahierbar war. So kann der Orchestrator zwischen das JSON aber nicht extrahierbar war. So kann der Orchestrator zwischen
"echt keine Treffer" und "kaputte Antwort" unterscheiden. "echt keine Treffer" und "kaputte Antwort" unterscheiden.
""" """
from config import OUTPUT_LANGUAGE
# Bevorzugte Web-Quellen als Prompt-Block (optional) # Bevorzugte Web-Quellen als Prompt-Block (optional)
preferred_sources_block = "" preferred_sources_block = ""
if preferred_sources: if preferred_sources:
@@ -422,7 +445,7 @@ class ResearcherAgent:
) )
if incident_type == "research": if incident_type == "research":
lang_instruction = LANG_DEEP_INTERNATIONAL if international else LANG_DEEP_GERMAN_ONLY lang_instruction = lang_deep_international(output_language) if international else lang_deep_primary_only(output_language)
# Bestehende Artikel als Kontext für den Prompt aufbereiten # Bestehende Artikel als Kontext für den Prompt aufbereiten
existing_context = "" existing_context = ""
if existing_articles: if existing_articles:
@@ -439,11 +462,11 @@ class ResearcherAgent:
) )
prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format( prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format(
title=title, description=description, language_instruction=lang_instruction, title=title, description=description, language_instruction=lang_instruction,
output_language=OUTPUT_LANGUAGE, existing_context=existing_context, output_language=output_language, existing_context=existing_context,
preferred_sources_block=preferred_sources_block, preferred_sources_block=preferred_sources_block,
) )
else: else:
lang_instruction = LANG_INTERNATIONAL if international else LANG_GERMAN_ONLY lang_instruction = lang_international(output_language) if international else lang_primary_only(output_language)
# Bestehende Artikel als Kontext: bei Folge-Refreshes findet Claude andere Quellen # Bestehende Artikel als Kontext: bei Folge-Refreshes findet Claude andere Quellen
existing_context = "" existing_context = ""
if existing_articles: if existing_articles:
@@ -458,7 +481,7 @@ class ResearcherAgent:
) )
prompt = RESEARCH_PROMPT_TEMPLATE.format( prompt = RESEARCH_PROMPT_TEMPLATE.format(
title=title, description=description, language_instruction=lang_instruction, title=title, description=description, language_instruction=lang_instruction,
output_language=OUTPUT_LANGUAGE, existing_context=existing_context, output_language=output_language, existing_context=existing_context,
preferred_sources_block=preferred_sources_block, preferred_sources_block=preferred_sources_block,
) )
@@ -486,8 +509,8 @@ class ResearcherAgent:
excluded = True excluded = True
break break
if not excluded: if not excluded:
# Bei nur-deutsch: nicht-deutsche Ergebnisse nachfiltern # Bei nur-primary: andersprachige Ergebnisse nachfiltern
if not international and article.get("language", "de") != "de": if not international and article.get("language", output_language_iso) != output_language_iso:
continue continue
filtered.append(article) filtered.append(article)

Datei anzeigen

@@ -34,8 +34,10 @@ CLAUDE_MODEL_FAST = "claude-haiku-4-5-20251001" # Für einfache Aufgaben (Feed-
CLAUDE_MODEL_MEDIUM = "claude-sonnet-4-6" # Für qualitätskritische Aufgaben (Netzwerkanalyse) CLAUDE_MODEL_MEDIUM = "claude-sonnet-4-6" # Für qualitätskritische Aufgaben (Netzwerkanalyse)
CLAUDE_MODEL_STANDARD = "claude-opus-4-7" # Standard-Opus für Recherche, Analyse, Faktencheck CLAUDE_MODEL_STANDARD = "claude-opus-4-7" # Standard-Opus für Recherche, Analyse, Faktencheck
# Ausgabesprache (Lagebilder, Faktenchecks, Zusammenfassungen) # Ausgabesprache wird pro Organisation gesteuert -- siehe services/org_settings.py
OUTPUT_LANGUAGE = "Deutsch" # (organization_settings-Tabelle, Key 'output_language', Werte 'de' | 'en').
# Default-Fallback in den Agent-Methoden ist 'Deutsch', sodass Calls ohne
# explizite Org-Bindung weiterhin deutsch produzieren.
# Dev-Modus: ausfuehrliches Logging (DEBUG-Level, HTTP-Request-Log) # Dev-Modus: ausfuehrliches Logging (DEBUG-Level, HTTP-Request-Log)
# In Kundenversion auf False setzen oder Env-Variable entfernen # In Kundenversion auf False setzen oder Env-Variable entfernen