feat(brain): make OUTPUT_LANGUAGE lock robust on small models

Harden the reply-language lock so qwen2.5:3b reliably stays in the locked
language instead of leaking the query language back in:

- reply_language_directive(): single resolver with clear precedence —
  explicit OUTPUT_LANGUAGE lock wins over the Piper/Chatterbox English-only
  fallback (this deployment's actual TTS is Korean MeloTTS, so the legacy
  English lock was both wrong and contradicting the Korean lock).
- Stronger, override-explicit directive wording, inserted near the FRONT of
  the system prompt so a small model gives it primacy over the persona.
- build_system_prompt(output_language=...): rewrite the persona's "in the
  user's language" clause to the locked language so the persona stops
  fighting the lock.
- docs/llm_contexts.md: document the resolver, precedence, and placement.

Live-verified on the running brain (qwen2.5:3b): Korean voice-style input
and a cold English query both return fully Korean replies with no CJK/Hanja
leak. Tests cover unset/set/agnostic/whitespace + precedence + persona rewrite.
This commit is contained in:
javis-bot
2026-06-12 21:18:47 +09:00
parent 006a32276a
commit 8a2a109d5e
4 changed files with 117 additions and 24 deletions

View File

@@ -81,14 +81,26 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
)
def build_system_prompt(assistant_name: str = "Jarvis") -> str:
def build_system_prompt(
assistant_name: str = "Jarvis", output_language: Optional[str] = None
) -> str:
"""Render the persona prompt with the configured assistant name.
The name comes from the user's wake word (capitalised); defaults to
"Jarvis" when no config is available (tests, eval harnesses).
When ``output_language`` is set (a single-language deployment), the
persona's "reply in the user's language" clause is rewritten to that
language so the persona does not contradict the OUTPUT_LANGUAGE lock — a
small model otherwise honours the persona's instruction to mirror the
query language and leaks the other language back in.
"""
name = (assistant_name or "Jarvis").strip() or "Jarvis"
return _SYSTEM_PROMPT_TEMPLATE.format(name=name)
prompt = _SYSTEM_PROMPT_TEMPLATE.format(name=name)
lang = (output_language or "").strip()
if lang:
prompt = prompt.replace("in the user's language", f"in {lang}")
return prompt
def output_language_directive(language: Optional[str]) -> Optional[str]:
@@ -108,7 +120,41 @@ def output_language_directive(language: Optional[str]) -> Optional[str]:
if not lang:
return None
return (
f"Always respond only in {lang}, regardless of the language the user "
f"writes in. Do not mix in words, characters, or punctuation from any "
f"other language or script."
f"CRITICAL OUTPUT RULE: write your ENTIRE reply only in {lang}. Even if "
f"the user writes in English or any other language, you must still reply "
f"only in {lang}. This rule overrides every other instruction about "
f"matching or using the user's language. Never mix in words, characters, "
f"or punctuation from any other language or script."
)
# TTS engines that can only synthesise English. Replying in another language
# with one of these produces garbled audio, so those deployments force English.
_TTS_ENGLISH_ONLY = frozenset({"piper", "chatterbox"})
# Kept verbatim for backward compatibility with anything asserting on the wording.
ENGLISH_ONLY_DIRECTIVE = (
"Always respond in English regardless of the language the user speaks in."
)
def reply_language_directive(
output_language: Optional[str], tts_engine: Optional[str]
) -> Optional[str]:
"""Resolve the reply-language instruction for the chat loop, or None.
Precedence:
1. An explicit ``output_language`` lock wins — the deployment serves a
single language and owns a TTS voice that can speak it (e.g. Korean
MeloTTS). This intentionally overrides the English-only fallback.
2. Otherwise, a Piper/Chatterbox TTS engine can only synthesise English,
so force English to avoid garbled audio.
3. Otherwise (multilingual TTS, no lock) → None: the assistant replies in
the user's own language.
"""
forced = output_language_directive(output_language)
if forced:
return forced
if (tts_engine or "piper").strip().lower() in _TTS_ENGLISH_ONLY:
return ENGLISH_ONLY_DIRECTIVE
return None