feat(brain): add OUTPUT_LANGUAGE reply-language lock

Add an optional OUTPUT_LANGUAGE env var that forces every reply into a
single language. When set, output_language_directive() injects a "respond
only in <language>" instruction (also forbidding other scripts) into the
chat loop's system prompt, next to the existing TTS English-only lock.
Empty (default) keeps the multilingual "reply in the user's language"
behaviour, so upstream is unaffected.

For the Korean-only deployment this also suppresses the occasional trailing
CJK/Hanja fragment qwen2.5:3b leaks on free-form chit-chat.

- system_prompt.py: language-agnostic output_language_directive() helper
- engine.py: read OUTPUT_LANGUAGE, append directive in _build_initial_system_message
- docker-compose.yml + .env.example: document/pass the new var
- docs/llm_contexts.md: note the new gating on the main reply context
- tests: cover unset/set/agnostic/whitespace cases
This commit is contained in:
javis-bot
2026-06-12 21:08:44 +09:00
parent 40877b65b3
commit 006a32276a
6 changed files with 77 additions and 3 deletions

View File

@@ -5,7 +5,7 @@ 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
from jarvis.system_prompt import build_system_prompt, output_language_directive
class TestBuildSystemPrompt:
@@ -26,3 +26,35 @@ class TestBuildSystemPrompt:
assert "named Jarvis" in build_system_prompt("")
assert "named Jarvis" in build_system_prompt(" ")
assert "named Jarvis" in build_system_prompt(None) # type: ignore[arg-type]
class TestOutputLanguageDirective:
"""A deployment may lock replies to a single language via OUTPUT_LANGUAGE.
Unset (the default) must keep the assistant's multilingual behaviour of
replying in the user's own language, so the helper returns None and no
directive is injected.
"""
def test_unset_returns_none(self):
assert output_language_directive(None) is None
assert output_language_directive("") is None
assert output_language_directive(" ") is None
def test_set_language_is_named_and_exclusive(self):
directive = output_language_directive("Korean")
assert directive is not None
assert "Korean" in directive
# Must force exclusivity, not merely prefer the language.
assert "only" in directive.lower()
def test_language_agnostic(self):
# The helper takes any language string — no hardcoded single language.
assert "French" in (output_language_directive("French") or "")
assert "日本語" in (output_language_directive("日本語") or "")
def test_strips_surrounding_whitespace(self):
directive = output_language_directive(" Korean ")
assert directive is not None
assert "Korean" in directive
assert " Korean" not in directive