Compare commits
5 Commits
680f5a656a
...
v1.9.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83999a5b0b | ||
|
|
d5fd218c86 | ||
|
|
23b1fe692b | ||
|
|
7bb9718c34 | ||
|
|
7da2fcb5e5 |
@@ -65,6 +65,9 @@ OLLAMA_CHAT_MODEL=qwen2.5:3b
|
||||
# default qwen2.5:3b, which ollama-init pulls automatically. Set it equal to
|
||||
# OLLAMA_CHAT_MODEL to run everything on one resident model instead (saves VRAM
|
||||
# at the cost of slower routing when the chat model is large).
|
||||
# NEVER set this LARGER than OLLAMA_CHAT_MODEL: the auxiliary calls would then
|
||||
# run on the bigger, slower model and add latency to every command (the exact
|
||||
# opposite of the split's purpose). Keep it <= the chat model, blank, or equal.
|
||||
OLLAMA_INTENT_MODEL=
|
||||
OLLAMA_EMBED_MODEL=nomic-embed-text
|
||||
WHISPER_MODEL=small
|
||||
@@ -227,6 +230,7 @@ COMPOSE_FILE=docker-compose.yml:docker-compose.gpu-linux.yml
|
||||
# OLLAMA_CHAT_MODEL=qwen2.5:7b # quality (needs ~5GB VRAM + whisper small)
|
||||
# OLLAMA_CHAT_MODEL=qwen2.5:3b # speed (fits easily, faster on 8GB GPUs)
|
||||
# WHISPER_MODEL=small # small frees VRAM for a bigger LLM; medium=more accurate
|
||||
# STT_BEAM_SIZE=5 # beam search (5) > greedy (1) for accuracy; lower for speed
|
||||
# MELO_DEVICE=cuda # cpu if no GPU on the bot host
|
||||
|
||||
# --- Settings web UI (http://localhost:8765/settings on the bot host) ---
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 자비스 운영자 지시
|
||||
|
||||
- 너의 이름은 자비스다.
|
||||
- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 최대한 간결하게, 한두 문장으로 답한다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다.
|
||||
- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 무조건 한 문장으로만 답한다. 두 문장 이상 쓰지 않는다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다.
|
||||
- 정해진 문구에만 반응하지 말고, 실제 사람처럼 말의 뉘앙스와 맥락으로 의도를 알아듣고 처리한다.
|
||||
|
||||
화면 속 크롬(방송 화면)에서 유튜브를 다룰 때 (화면에 보여야 하므로 반드시 on-screen 브라우저 제어 도구로 수행한다):
|
||||
|
||||
@@ -87,6 +87,17 @@ VAD_MIN_SPEECH_MS = int(os.environ.get("VAD_MIN_SPEECH_MS", "200"))
|
||||
# Korean phrase decoded as Chinese) and shaves a little latency. Empty = auto.
|
||||
STT_LANGUAGE = os.environ.get("STT_LANGUAGE", "ko").strip() or None
|
||||
|
||||
# Whisper decoding accuracy knobs. beam_size=1 is greedy decoding — fast but the
|
||||
# least accurate; beam search (5 is the Whisper default) explores alternatives
|
||||
# and noticeably improves recognition on short, accented, or noisy Discord-mic
|
||||
# speech. condition_on_previous_text=False stops Whisper from feeding a previous
|
||||
# clip's transcript back in as a prompt, which on isolated short utterances
|
||||
# causes repetition loops and drift rather than helping. Both are env-tunable so
|
||||
# accuracy/latency can be traded without a code change (lower STT_BEAM_SIZE for
|
||||
# speed, raise it for accuracy).
|
||||
STT_BEAM_SIZE = max(1, int(os.environ.get("STT_BEAM_SIZE", "5")))
|
||||
STT_CONDITION_ON_PREV = os.environ.get("STT_CONDITION_ON_PREV", "0") in ("1", "true", "True", "yes", "on")
|
||||
|
||||
# TTS engine: "edge" (Microsoft Edge TTS, natural Korean neural voice) is the
|
||||
# primary voice. "melo" (a warm MeloTTS worker) and "piper" remain selectable.
|
||||
def _tts_engine_setting() -> str:
|
||||
@@ -243,7 +254,12 @@ def transcribe(wav_bytes: bytes) -> dict:
|
||||
print("[bridge] no speech detected (VAD) — skipping STT", flush=True)
|
||||
return {"text": "", "language": None, "note": "음성 아님(VAD 차단)"}
|
||||
|
||||
segments, info = _whisper.transcribe(audio, beam_size=1, language=STT_LANGUAGE)
|
||||
segments, info = _whisper.transcribe(
|
||||
audio,
|
||||
beam_size=STT_BEAM_SIZE,
|
||||
language=STT_LANGUAGE,
|
||||
condition_on_previous_text=STT_CONDITION_ON_PREV,
|
||||
)
|
||||
# Second line of defence: drop non-speech / hallucinated segments by
|
||||
# Whisper's own no_speech_prob. The no_speech_prob hard cutoff (plus the VAD
|
||||
# pre-gate above) is what rejects noise/hallucinations. The avg_logprob
|
||||
|
||||
@@ -97,6 +97,9 @@ services:
|
||||
PLANNER_ENABLED: ${PLANNER_ENABLED:-0}
|
||||
# Lock STT to Korean (skip Whisper auto-detect).
|
||||
STT_LANGUAGE: ${STT_LANGUAGE:-ko}
|
||||
# Whisper decode accuracy: beam search (5) over greedy (1) lifts recognition
|
||||
# on short/noisy Discord speech. Lower to 1 for minimum latency.
|
||||
STT_BEAM_SIZE: ${STT_BEAM_SIZE:-5}
|
||||
VOICE_SILENCE_MS: ${VOICE_SILENCE_MS:-600}
|
||||
BRIDGE_URL: http://127.0.0.1:8765
|
||||
# Split-deployment role: full (default, all-in-one), browser (only the
|
||||
|
||||
@@ -19,7 +19,7 @@ Every distinct LLM call in Jarvis, what feeds it, what consumes it, and how it i
|
||||
- Time + location context (re-injected each turn)
|
||||
- Tool schema: native via `generate_tools_json_schema()` ([src/jarvis/tools/registry.py](src/jarvis/tools/registry.py)) or text fallback via `_text_tool_call_guidance()` ([engine.py:68](src/jarvis/reply/engine.py:68))
|
||||
- Tool results from prior turns (raw or digested — see #5)
|
||||
- **Output**: OpenAI-style `{content, tool_calls, thinking}`. Consumed by the tool orchestrator and TTS pipeline. Natural-language content is delivered immediately; no post-turn evaluator runs.
|
||||
- **Output**: OpenAI-style `{content, tool_calls, thinking}`. Consumed by the tool orchestrator and TTS pipeline. Natural-language content is delivered immediately; no post-turn evaluator runs. Spoken-answer length: the persona (`system_prompt.py`) and `voice_style` (`prompts/system.py`) both constrain the reply to a SINGLE sentence — any dry aside must fold into that one sentence as a trailing clause, never a second sentence. This keeps TTS latency down (synth time scales with text length) and matches the `agents/llm.md` operator instruction.
|
||||
- **Limits**: `num_ctx: 8192` (explicit). Output `num_predict: cfg.ollama_num_predict` (default 512, `0`/negative disables) caps generated tokens per turn — a worst-case latency guard for short spoken answers; the headroom stays above tool-call JSON so it does not truncate tool calls (both native and text tool-call paths). Timeout `llm_chat_timeout_sec` (45s). Auto-fallback from native to text tool-calls on HTTP 400 (`ToolsNotSupportedError`), sticky for the session. Risk: `fetch_web_page` truncates at 50,000 chars (~37k tokens) — mitigated for SMALL models by tool-result digest (#5) which compresses the payload before it enters the messages history. LARGE models receive the raw payload and may silently see a truncated context.
|
||||
|
||||
## 2. Intent Judge
|
||||
|
||||
@@ -22,11 +22,15 @@ class ModelSize(Enum):
|
||||
LARGE = "large" # 8b+ - can infer tool usage from context
|
||||
|
||||
|
||||
# Model size patterns - models matching these are considered SMALL
|
||||
# Model size patterns - models matching these are considered SMALL.
|
||||
# Covers every sub-8B size (1b-7b): these models need the explicit, repeated
|
||||
# tool/greeting/instruction constraints and falter on the terse LARGE prompt.
|
||||
# Without 2b/4b/5b/6b here a genuinely small model (e.g. qwen*:4b) was
|
||||
# misclassified as LARGE and given the less-guided prompt set.
|
||||
_SMALL_MODEL_PATTERNS = (
|
||||
":1b", ":3b", ":7b",
|
||||
"-1b", "-3b", "-7b",
|
||||
"_1b", "_3b", "_7b",
|
||||
":1b", ":2b", ":3b", ":4b", ":5b", ":6b", ":7b",
|
||||
"-1b", "-2b", "-3b", "-4b", "-5b", "-6b", "-7b",
|
||||
"_1b", "_2b", "_3b", "_4b", "_5b", "_6b", "_7b",
|
||||
"gemma4", # Gemma 4 - always small regardless of tag
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ This module provides model-size-aware prompt generation for the reply engine.
|
||||
|
||||
### Problem Statement
|
||||
|
||||
Small models (1b, 3b, 7b parameters) lack the reasoning capacity to infer when NOT to use tools. When given prompts like "Proactively use available tools," they may incorrectly call tools for simple greetings like "hello" or "ni hao" because they cannot distinguish between:
|
||||
Small models (every sub-8B size, 1b-7b parameters) lack the reasoning capacity to infer when NOT to use tools. When given prompts like "Proactively use available tools," they may incorrectly call tools for simple greetings like "hello" or "ni hao" because they cannot distinguish between:
|
||||
- Requests that require tools (weather, search, data retrieval)
|
||||
- Simple conversation (greetings, small talk, general knowledge)
|
||||
|
||||
@@ -14,7 +14,7 @@ The module detects model size from the model name and selects appropriate prompt
|
||||
|
||||
| Model Size | Detection Pattern | Tool Prompts |
|
||||
|------------|-------------------|--------------|
|
||||
| SMALL | `:1b`, `:3b`, `:7b`, `gemma4` | Conservative — explicit "DO NOT use tools for greetings" + worked negative examples + repetition |
|
||||
| SMALL | `:1b`-`:7b` (every size 1-7B, all separators), `gemma4` | Conservative — explicit "DO NOT use tools for greetings" + worked negative examples + repetition |
|
||||
| LARGE | All others (8b+) | Proactive — "use tools confidently" + short anti-confabulation + auto-derive clause |
|
||||
|
||||
### Architecture
|
||||
@@ -43,7 +43,7 @@ from jarvis.reply.prompts import (
|
||||
Both model sizes share these base components:
|
||||
- `asr_note`: Voice transcription error handling
|
||||
- `inference_guidance`: Prefer inference over clarification
|
||||
- `voice_style`: Concise, conversational responses
|
||||
- `voice_style`: Single-sentence, conversational responses (spoken aloud, so one sentence only — never more)
|
||||
|
||||
Model-size-specific components:
|
||||
- `tool_incentives`: When/how aggressively to use tools
|
||||
|
||||
@@ -26,8 +26,8 @@ INFERENCE_GUIDANCE = (
|
||||
# Voice assistant communication style - concise, conversational
|
||||
VOICE_STYLE = (
|
||||
"Keep responses concise and conversational since this is a voice assistant. "
|
||||
"Two to three sentences maximum. Prioritize clarity and brevity - users are listening, not reading. "
|
||||
"Avoid unnecessary elaboration unless specifically requested. "
|
||||
"Reply in a SINGLE sentence - never more than one sentence. Prioritize clarity and brevity - users are listening, not reading. "
|
||||
"Avoid unnecessary elaboration. "
|
||||
"Do NOT offer follow-up suggestions or ask if the user wants more info - just respond directly. "
|
||||
"IMPORTANT: Always respond in natural language - never output JSON, code, or structured data as your response. "
|
||||
"NEVER use markdown formatting in your replies: no asterisks for emphasis (**bold**, *italic*), "
|
||||
|
||||
@@ -62,10 +62,11 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"Tone rails (hard): never mean, never condescending, never passive-aggressive, never "
|
||||
"sulking, never preachy, never sycophantic ('great question', 'I'd be happy to'). "
|
||||
"Sarcasm points at the situation, the topic, or mildly at yourself — never at the user. "
|
||||
"Shape for casual, factual, or small-talk replies: state the answer in a sentence, then add "
|
||||
"one short dry observation about it (an understated aside, a raised-eyebrow remark, a gentle "
|
||||
"noticing of the irony). One aside — not two, not a joke opener, not a joke-shaped sentence "
|
||||
"replacing the answer. The aside is a tail, not the head. "
|
||||
"Shape for casual, factual, or small-talk replies: give the answer in a SINGLE sentence. If a "
|
||||
"dry aside fits, fold it into that same sentence as a short trailing clause — never add it as "
|
||||
"a second sentence, never a joke opener, never a joke-shaped sentence replacing the answer. "
|
||||
"Whenever the wit would require a second sentence, drop the wit and keep the one-sentence "
|
||||
"answer. The aside is a tail inside the sentence, not a head and not a new sentence. "
|
||||
"Examples of the MOVE (shape, not wording — never copy these): stating a fact and then noting "
|
||||
"its mild absurdity; giving the weather and then commenting on what it implies for the day; "
|
||||
"answering a trivia question and then offering a wry footnote about the subject; admitting "
|
||||
@@ -79,8 +80,8 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"butler clichés, and never address the user as 'sir', 'madam', 'my liege', or similar. "
|
||||
"Never stack multiple jokes in one reply. "
|
||||
"Be concise, conversational, and actionable. "
|
||||
"This is a spoken voice assistant: answer in ONE short sentence whenever possible "
|
||||
"(two at the very most). No lists, no preamble, no 'is there anything else' offers. "
|
||||
"This is a spoken voice assistant: your ENTIRE reply must be a single short sentence. "
|
||||
"Never write a second sentence. No lists, no preamble, no 'is there anything else' offers. "
|
||||
"When a controlBrowser tool is available, use IT (never webSearch) for anything that "
|
||||
"should happen in the on-screen browser — opening a site, searching on a site "
|
||||
"(controlBrowser action 'search' with the right site), clicking, typing — because only "
|
||||
@@ -119,10 +120,13 @@ _SYSTEM_PROMPT_TEMPLATE: str = (
|
||||
"'tell me a joke', 'chat with me'), never reply with a bare greeting like 'Hey there!', "
|
||||
"'Hi!', 'How can I help you?', or a generic observation about an unrelated topic. "
|
||||
"When the 'Information the user has shared…' section is present, you MUST pick one concrete "
|
||||
"fact from it and build the reply around that fact (e.g. 'You mentioned you box at Trenches "
|
||||
"Gym — how's training going this week?'). Do not talk about things that are not in that "
|
||||
"section. Only when that section is absent may you invent a fresh observation, question, or "
|
||||
"joke. Produce a varied response each time — do not repeat a previous reply verbatim. "
|
||||
"fact from it and build the reply around that fact, opening with a short natural reference to "
|
||||
"it. CRITICAL: use ONLY names, places, activities, and details that literally appear in that "
|
||||
"section — never borrow any name, place, or activity from these instructions or from any "
|
||||
"example wording, and never invent specifics that are not in that section. Do not talk about "
|
||||
"things that are not in that section. Only when that section is absent may you invent a fresh "
|
||||
"observation, question, or joke. Produce a varied response each time — do not repeat a "
|
||||
"previous reply verbatim. "
|
||||
"Banned phrasings: 'I can only tell you what you have shared with me in this conversation', "
|
||||
"'I don't have access to any personal information outside of what you tell me', 'I don't have "
|
||||
"personal details outside of our conversation history', 'I do not store personal details "
|
||||
|
||||
@@ -21,9 +21,15 @@ class TestModelSizeDetection:
|
||||
("gemma:7b", True),
|
||||
("phi3:3b", True),
|
||||
("qwen2:7b", True),
|
||||
# Sub-8B sizes that were previously misclassified as LARGE.
|
||||
("qwen3.5:4b", True), # the deployed model that produced weak, off-tone replies
|
||||
("gemma2:2b", True),
|
||||
("model:5b", True),
|
||||
("model:6b", True),
|
||||
# Various separators
|
||||
("model-3b-instruct", True),
|
||||
("model_1b_chat", True),
|
||||
("model-4b-instruct", True),
|
||||
# Large models (should return LARGE)
|
||||
("gpt-oss:20b", False),
|
||||
("llama3.1:8b", False),
|
||||
@@ -121,6 +127,18 @@ class TestPromptComponents:
|
||||
assert prompts.voice_style, f"{size.value} missing voice_style"
|
||||
assert prompts.tool_guidance, f"{size.value} missing tool_guidance"
|
||||
|
||||
def test_voice_style_enforces_single_sentence(self):
|
||||
"""voice_style must cap replies at one sentence (spoken aloud). The old
|
||||
'Two to three sentences maximum' wording let the model run long, which
|
||||
also slowed TTS since synth time scales with text length."""
|
||||
from jarvis.reply.prompts import get_system_prompts, ModelSize
|
||||
|
||||
for size in [ModelSize.SMALL, ModelSize.LARGE]:
|
||||
voice_style = get_system_prompts(size).voice_style
|
||||
assert "SINGLE sentence" in voice_style, f"{size.value} voice_style not single-sentence"
|
||||
assert "never more than one sentence" in voice_style
|
||||
assert "Two to three" not in voice_style
|
||||
|
||||
def test_to_list_returns_non_empty_strings(self):
|
||||
"""to_list() returns only non-empty prompt strings."""
|
||||
from jarvis.reply.prompts import get_system_prompts, ModelSize
|
||||
|
||||
@@ -44,6 +44,32 @@ class TestBuildSystemPrompt:
|
||||
assert "in the user's language" not in prompt
|
||||
assert "in Korean" in prompt
|
||||
|
||||
def test_persona_enforces_single_sentence(self):
|
||||
# Spoken replies must be one sentence (TTS latency scales with text
|
||||
# length, and the user asked for one-sentence answers). The persona must
|
||||
# state the single-sentence rule and must NOT carry the old "two at the
|
||||
# very most" allowance that let the model run long.
|
||||
prompt = build_system_prompt("Jarvis")
|
||||
assert "single short sentence" in prompt
|
||||
assert "Never write a second sentence" in prompt
|
||||
assert "two at the very most" not in prompt
|
||||
|
||||
def test_persona_aside_does_not_authorise_a_second_sentence(self):
|
||||
# The dry aside must fold into the one sentence, not become a 2nd one.
|
||||
prompt = build_system_prompt("Jarvis")
|
||||
assert "SINGLE sentence" in prompt
|
||||
assert "never add it as " in prompt
|
||||
|
||||
def test_persona_has_no_copyable_proper_noun_examples(self):
|
||||
# A weak model parroted the literal "Trenches Gym" example from the
|
||||
# persona as if it were a real user fact (boxing mangled to tennis).
|
||||
# The persona must not embed copyable personal proper nouns, and must
|
||||
# tell the model to use ONLY facts that literally appear in the memory
|
||||
# section — never borrow names/places from the instructions themselves.
|
||||
prompt = build_system_prompt("Jarvis")
|
||||
assert "Trenches" not in prompt
|
||||
assert "never borrow any name, place, or activity from these instructions" in prompt
|
||||
|
||||
|
||||
class TestOutputLanguageDirective:
|
||||
"""A deployment may lock replies to a single language via OUTPUT_LANGUAGE.
|
||||
|
||||
Reference in New Issue
Block a user