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:
@@ -5,7 +5,12 @@ wake word to e.g. "Friday" produces a butler named Friday, not one still
|
||||
hardcoded to Jarvis.
|
||||
"""
|
||||
|
||||
from jarvis.system_prompt import build_system_prompt, output_language_directive
|
||||
from jarvis.system_prompt import (
|
||||
build_system_prompt,
|
||||
output_language_directive,
|
||||
reply_language_directive,
|
||||
ENGLISH_ONLY_DIRECTIVE,
|
||||
)
|
||||
|
||||
|
||||
class TestBuildSystemPrompt:
|
||||
@@ -27,6 +32,17 @@ class TestBuildSystemPrompt:
|
||||
assert "named Jarvis" in build_system_prompt(" ")
|
||||
assert "named Jarvis" in build_system_prompt(None) # type: ignore[arg-type]
|
||||
|
||||
def test_default_keeps_user_language_clause(self):
|
||||
# Without a lock, the persona still mirrors the user's language.
|
||||
assert "in the user's language" in build_system_prompt("Jarvis")
|
||||
|
||||
def test_language_lock_rewrites_user_language_clause(self):
|
||||
# With a lock, the contradicting "user's language" clause is rewritten
|
||||
# so the persona does not fight the OUTPUT_LANGUAGE directive.
|
||||
prompt = build_system_prompt("Jarvis", "Korean")
|
||||
assert "in the user's language" not in prompt
|
||||
assert "in Korean" in prompt
|
||||
|
||||
|
||||
class TestOutputLanguageDirective:
|
||||
"""A deployment may lock replies to a single language via OUTPUT_LANGUAGE.
|
||||
@@ -58,3 +74,37 @@ class TestOutputLanguageDirective:
|
||||
assert directive is not None
|
||||
assert "Korean" in directive
|
||||
assert " Korean" not in directive
|
||||
|
||||
|
||||
class TestReplyLanguageDirective:
|
||||
"""Precedence: explicit OUTPUT_LANGUAGE lock > English-only TTS > free.
|
||||
|
||||
The lock must override the Piper/Chatterbox English fallback, because a
|
||||
deployment that sets OUTPUT_LANGUAGE (e.g. Korean) also runs a TTS voice
|
||||
that can speak it. Without this, the English lock and the Korean lock
|
||||
contradict each other and the model reverts to English.
|
||||
"""
|
||||
|
||||
def test_lock_overrides_english_only_tts(self):
|
||||
directive = reply_language_directive("Korean", "piper")
|
||||
assert directive is not None
|
||||
assert "Korean" in directive
|
||||
assert directive != ENGLISH_ONLY_DIRECTIVE
|
||||
|
||||
def test_english_only_tts_forces_english_without_lock(self):
|
||||
assert reply_language_directive(None, "piper") == ENGLISH_ONLY_DIRECTIVE
|
||||
assert reply_language_directive("", "chatterbox") == ENGLISH_ONLY_DIRECTIVE
|
||||
|
||||
def test_no_lock_multilingual_tts_is_free(self):
|
||||
# A non-English-only engine (e.g. melo) with no lock → reply in the
|
||||
# user's own language, so no directive.
|
||||
assert reply_language_directive(None, "melo") is None
|
||||
|
||||
def test_unknown_tts_defaults_to_english_only(self):
|
||||
# Preserves the original getattr(cfg, 'tts_engine', 'piper') default:
|
||||
# an unknown/missing engine is treated conservatively as English-only.
|
||||
assert reply_language_directive(None, None) == ENGLISH_ONLY_DIRECTIVE
|
||||
|
||||
def test_lock_wins_even_with_multilingual_tts(self):
|
||||
directive = reply_language_directive("Korean", "melo")
|
||||
assert directive is not None and "Korean" in directive
|
||||
|
||||
Reference in New Issue
Block a user