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:
@@ -396,14 +396,13 @@ class AnalyzerAgent:
|
||||
articles_text += f"Inhalt: {content[:800]}\n"
|
||||
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)."""
|
||||
if not articles:
|
||||
return None, None
|
||||
|
||||
articles_text = self._format_articles_text(articles)
|
||||
|
||||
from config import OUTPUT_LANGUAGE
|
||||
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
|
||||
template = BRIEFING_PROMPT_TEMPLATE if incident_type == "research" else ANALYSIS_PROMPT_TEMPLATE
|
||||
prompt = template.format(
|
||||
@@ -411,7 +410,7 @@ class AnalyzerAgent:
|
||||
description=description or "Keine weiteren Details",
|
||||
articles_text=articles_text,
|
||||
today=today,
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
fact_context_block=fact_context_block,
|
||||
)
|
||||
|
||||
@@ -435,6 +434,7 @@ class AnalyzerAgent:
|
||||
previous_sources_json: str | None,
|
||||
incident_type: str = "adhoc",
|
||||
fact_context_block: str = "",
|
||||
output_language: str = "Deutsch",
|
||||
) -> tuple[dict | None, ClaudeUsage | None]:
|
||||
"""Inkrementelle Analyse: Aktualisiert das Lagebild mit nur den neuen Artikeln.
|
||||
|
||||
@@ -465,7 +465,6 @@ class AnalyzerAgent:
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
previous_sources_text = "Fehler beim Laden der bisherigen Quellen"
|
||||
|
||||
from config import OUTPUT_LANGUAGE
|
||||
today = datetime.now(TIMEZONE).strftime("%d.%m.%Y")
|
||||
|
||||
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,
|
||||
new_articles_text=new_articles_text,
|
||||
today=today,
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
fact_context_block=fact_context_block,
|
||||
)
|
||||
|
||||
@@ -580,6 +579,7 @@ class AnalyzerAgent:
|
||||
summary: str,
|
||||
recent_articles: list[dict],
|
||||
previous_developments: str | None = None,
|
||||
output_language: str = "Deutsch",
|
||||
) -> tuple[str | None, ClaudeUsage | None]:
|
||||
"""Generiert die Kachel 'Neueste Entwicklungen' aus dem Lagebild.
|
||||
|
||||
@@ -598,7 +598,7 @@ class AnalyzerAgent:
|
||||
if not recent_articles:
|
||||
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")
|
||||
|
||||
# Kompakter Artikel-Block: nur die für Zeitstempel/Quellen nötigen Felder.
|
||||
@@ -629,7 +629,7 @@ class AnalyzerAgent:
|
||||
summary=summary.strip(),
|
||||
articles_text=articles_text,
|
||||
today=today,
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -462,19 +462,18 @@ class FactCheckerAgent:
|
||||
lines.append(line)
|
||||
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)."""
|
||||
if not articles:
|
||||
return [], None
|
||||
|
||||
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
|
||||
prompt = template.format(
|
||||
title=title,
|
||||
articles_text=articles_text,
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -494,6 +493,7 @@ class FactCheckerAgent:
|
||||
new_articles: list[dict],
|
||||
existing_facts: list[dict],
|
||||
incident_type: str = "adhoc",
|
||||
output_language: str = "Deutsch",
|
||||
) -> tuple[list[dict], ClaudeUsage | None]:
|
||||
"""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)
|
||||
existing_facts_text = self._format_existing_facts(existing_facts)
|
||||
|
||||
from config import OUTPUT_LANGUAGE
|
||||
if incident_type == "research":
|
||||
template = INCREMENTAL_RESEARCH_FACTCHECK_PROMPT_TEMPLATE
|
||||
else:
|
||||
@@ -516,7 +515,7 @@ class FactCheckerAgent:
|
||||
title=title,
|
||||
articles_text=articles_text,
|
||||
existing_facts_text=existing_facts_text,
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -536,6 +535,7 @@ class FactCheckerAgent:
|
||||
new_articles: list[dict],
|
||||
existing_facts: list[dict],
|
||||
incident_type: str = "adhoc",
|
||||
output_language: str = "Deutsch",
|
||||
) -> tuple[list[dict], ClaudeUsage | None]:
|
||||
"""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)
|
||||
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(
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
fact_count=len(existing_facts),
|
||||
existing_facts_text=triage_facts_text,
|
||||
article_count=len(new_articles),
|
||||
@@ -619,7 +619,7 @@ class FactCheckerAgent:
|
||||
template = VERIFY_GROUP_PROMPT_TEMPLATE
|
||||
|
||||
prompt = template.format(
|
||||
output_language=OUTPUT_LANGUAGE,
|
||||
output_language=output_language,
|
||||
theme=theme,
|
||||
facts_text=facts_text,
|
||||
new_claims_text=new_claims_text,
|
||||
|
||||
@@ -743,6 +743,10 @@ class AgentOrchestrator:
|
||||
visibility = incident["visibility"] if "visibility" in incident.keys() else "public"
|
||||
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
|
||||
# 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_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
|
||||
@@ -923,6 +927,8 @@ class AgentOrchestrator:
|
||||
international=international, user_id=user_id,
|
||||
existing_articles=existing_for_context,
|
||||
preferred_sources=preferred_sources,
|
||||
output_language=output_language,
|
||||
output_language_iso=output_language_iso,
|
||||
)
|
||||
logger.info(
|
||||
f"Claude-Recherche: {len(results)} Ergebnisse"
|
||||
@@ -1308,12 +1314,14 @@ class AgentOrchestrator:
|
||||
title, description, new_articles_for_analysis,
|
||||
previous_summary, previous_sources_json, incident_type,
|
||||
fact_context_block=fact_context_block,
|
||||
output_language=output_language,
|
||||
)
|
||||
else:
|
||||
logger.info("Erstanalyse: Alle Artikel werden analysiert")
|
||||
return await analyzer.analyze(
|
||||
title, description, all_articles_preloaded, incident_type,
|
||||
fact_context_block=fact_context_block,
|
||||
output_language=output_language,
|
||||
)
|
||||
|
||||
# --- Faktencheck-Task ---
|
||||
@@ -1327,6 +1335,7 @@ class AgentOrchestrator:
|
||||
)
|
||||
return await factchecker.check_incremental_twophase(
|
||||
title, new_articles_for_analysis, existing_facts, incident_type,
|
||||
output_language=output_language,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
@@ -1335,6 +1344,7 @@ class AgentOrchestrator:
|
||||
)
|
||||
return await factchecker.check_incremental(
|
||||
title, new_articles_for_analysis, existing_facts, incident_type,
|
||||
output_language=output_language,
|
||||
)
|
||||
else:
|
||||
# Alle Artikel laden falls nicht vorab geladen (Henne-Ei-Problem:
|
||||
@@ -1346,7 +1356,7 @@ class AgentOrchestrator:
|
||||
(incident_id,),
|
||||
)
|
||||
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
|
||||
# Faktenkontext fuer das Lagebild, damit dieses auf geprueftem Stand
|
||||
@@ -1573,6 +1583,7 @@ class AgentOrchestrator:
|
||||
dev_analyzer = AnalyzerAgent()
|
||||
dev_text, dev_usage = await dev_analyzer.generate_latest_developments(
|
||||
title, description, dev_summary_source, dev_articles, previous_developments,
|
||||
output_language=output_language,
|
||||
)
|
||||
if dev_usage:
|
||||
usage_acc.add(dev_usage)
|
||||
|
||||
@@ -153,12 +153,37 @@ Jedes Element hat diese Felder:
|
||||
|
||||
Antworte NUR mit dem JSON-Array. Keine Einleitung, keine Erklärung."""
|
||||
|
||||
# Sprach-Anweisungen
|
||||
LANG_INTERNATIONAL = "- Suche in Deutsch UND Englisch für internationale Abdeckung"
|
||||
LANG_GERMAN_ONLY = "- Suche NUR auf Deutsch bei deutschsprachigen Quellen (Deutschland, Österreich, Schweiz)\n- KEINE englischsprachigen oder anderssprachigen Quellen"
|
||||
# Sprach-Anweisungen (org-sprach-relativ; primary_display = "Deutsch" | "English")
|
||||
def lang_international(primary_display: str) -> str:
|
||||
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.
|
||||
@@ -392,7 +417,7 @@ class ResearcherAgent:
|
||||
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
|
||||
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.
|
||||
|
||||
Returns:
|
||||
@@ -400,8 +425,6 @@ class ResearcherAgent:
|
||||
das JSON aber nicht extrahierbar war. So kann der Orchestrator zwischen
|
||||
"echt keine Treffer" und "kaputte Antwort" unterscheiden.
|
||||
"""
|
||||
from config import OUTPUT_LANGUAGE
|
||||
|
||||
# Bevorzugte Web-Quellen als Prompt-Block (optional)
|
||||
preferred_sources_block = ""
|
||||
if preferred_sources:
|
||||
@@ -422,7 +445,7 @@ class ResearcherAgent:
|
||||
)
|
||||
|
||||
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
|
||||
existing_context = ""
|
||||
if existing_articles:
|
||||
@@ -439,11 +462,11 @@ class ResearcherAgent:
|
||||
)
|
||||
prompt = DEEP_RESEARCH_PROMPT_TEMPLATE.format(
|
||||
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,
|
||||
)
|
||||
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
|
||||
existing_context = ""
|
||||
if existing_articles:
|
||||
@@ -458,7 +481,7 @@ class ResearcherAgent:
|
||||
)
|
||||
prompt = RESEARCH_PROMPT_TEMPLATE.format(
|
||||
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,
|
||||
)
|
||||
|
||||
@@ -486,8 +509,8 @@ class ResearcherAgent:
|
||||
excluded = True
|
||||
break
|
||||
if not excluded:
|
||||
# Bei nur-deutsch: nicht-deutsche Ergebnisse nachfiltern
|
||||
if not international and article.get("language", "de") != "de":
|
||||
# Bei nur-primary: andersprachige Ergebnisse nachfiltern
|
||||
if not international and article.get("language", output_language_iso) != output_language_iso:
|
||||
continue
|
||||
filtered.append(article)
|
||||
|
||||
|
||||
@@ -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_STANDARD = "claude-opus-4-7" # Standard-Opus für Recherche, Analyse, Faktencheck
|
||||
|
||||
# Ausgabesprache (Lagebilder, Faktenchecks, Zusammenfassungen)
|
||||
OUTPUT_LANGUAGE = "Deutsch"
|
||||
# Ausgabesprache wird pro Organisation gesteuert -- siehe services/org_settings.py
|
||||
# (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)
|
||||
# In Kundenversion auf False setzen oder Env-Variable entfernen
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren