"""Language registry + prompt helpers for localized AI output. Two surfaces consume this module: - Per-user LLM call sites (portfolio analysis only at this stage) call ``respond_in_clause(user.lang)`` and append the result to their system prompt. - The settings dropdown + its PATCH endpoint consult ``ACTIVE_LANGUAGES`` to decide which options are selectable. The strategic-log and digest translation fan-outs also consult it to decide which languages to spend tokens on. Adding Spanish/French/German support later is a one-line constant change: extend ``ACTIVE_LANGUAGES`` to include the new code. No other code change is required — the rest of the system already treats them as first-class via ``LANGUAGES``. """ from __future__ import annotations # Display labels for every language the system knows about. ES/FR/DE # are kept here so labels still render in the dropdown (as disabled # options) without requiring code changes to enable them later. LANGUAGES: dict[str, str] = { "en": "English", "it": "Italian", "es": "Spanish", "fr": "French", "de": "German", } # Languages users can actually select. Settings POST validates against # this; the strategic-log + digest translation fan-outs only consider # these. ACTIVE_LANGUAGES: set[str] = {"en", "it"} def respond_in_clause(lang: str | None) -> str: """Suffix appended to per-user LLM system prompts. Returns an empty string for ``en`` (no nudge needed), an unknown code, or ``None``/empty input — those callers want the default English path. Otherwise returns ``"\\n\\nRespond in ."`` keyed off ``LANGUAGES``. """ if not lang or lang == "en" or lang not in LANGUAGES: return "" return f"\n\nRespond in {LANGUAGES[lang]}." def language_directive_lead(lang: str | None) -> str: """Strong, top-of-prompt language directive for callers that generate user-facing prose in real time (portfolio analysis, chat) and need the output to actually land in the user's preferred language. A single tail clause like ``respond_in_clause`` is easy for the model to ignore when the rest of the prompt + user message are entirely in English; this leads with an explicit "all output in X" block, kept verbatim rules for symbols/numbers, and is intended to be prepended to the system prompt so the model anchors on the target language before reading the rest. Combined with respond_in_clause at the tail it gives a belt-and-suspenders defence. Empty string for English or unknown codes so callers can paste it in unconditionally. """ if not lang or lang == "en" or lang not in LANGUAGES: return "" language = LANGUAGES[lang] return ( f"# LANGUAGE — write everything in {language}\n" f"All output — section headers, prose, lists, and any inline " f"labels — must be written in {language}. Do NOT mix English in. " f"Ticker symbols (AAPL, MSFT, VOD.L), ISO currency codes " f"(USD, EUR, GBP), and numeric values stay unchanged.\n\n" )