Reports that portfolio AI analysis was coming back in English even for IT-toggled users. Traced the chain (DB user.lang IS set to it, router passes it into the payload, parse_request reads it, build_prompt appends respond_in_clause), so the wiring is correct end-to-end. The model was simply ignoring the single-sentence tail nudge: when the system prompt is hundreds of lines of English and the user message adds more English context, "Respond in Italian." at the end is easy to drop on the floor. Add a new services/i18n.language_directive_lead() that returns a strong, explicit top-of-prompt block — "# LANGUAGE — write everything in <X>" plus the verbatim-tickers-and-numbers carve-out — meant to be PREPENDED so the model anchors on the target language before it reads the bulk of the instructions. Combined with the existing tail clause it's belt-and-suspenders: top + bottom of the prompt both say "in this language". Applied to portfolio_analysis.build_prompt() and chat.py — the two surfaces that generate user-facing prose in real time (the strategic log + indicator summaries get post-hoc translation via translate(), so the directive isn't needed there). Empty-string return for en / unknown lang means callers can wire it in unconditionally; no extra plumbing in i18n callsites. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
76 lines
3 KiB
Python
76 lines
3 KiB
Python
"""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 <Language>."``
|
|
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"
|
|
)
|