From f68d25dbce3189f0ef11c41bc0dfe342911d6fbb Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 13 May 2026 20:54:28 +0000 Subject: [PATCH] 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). --- src/agents/analyzer.py | 14 +++++------ src/agents/factchecker.py | 16 ++++++------ src/agents/orchestrator.py | 13 +++++++++- src/agents/researcher.py | 51 +++++++++++++++++++++++++++----------- src/config.py | 6 +++-- 5 files changed, 68 insertions(+), 32 deletions(-) diff --git a/src/agents/analyzer.py b/src/agents/analyzer.py index 9bb45e6..88db2be 100644 --- a/src/agents/analyzer.py +++ b/src/agents/analyzer.py @@ -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: diff --git a/src/agents/factchecker.py b/src/agents/factchecker.py index 2f5bff2..ef15113 100644 --- a/src/agents/factchecker.py +++ b/src/agents/factchecker.py @@ -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, diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index e8bb457..1211d10 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -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) diff --git a/src/agents/researcher.py b/src/agents/researcher.py index 734b62d..64db182 100644 --- a/src/agents/researcher.py +++ b/src/agents/researcher.py @@ -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) diff --git a/src/config.py b/src/config.py index 1b39ea5..9acf64d 100644 --- a/src/config.py +++ b/src/config.py @@ -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