diff --git a/agents/llm.md b/agents/llm.md index cf8734e..dfbc59f 100644 --- a/agents/llm.md +++ b/agents/llm.md @@ -1,7 +1,7 @@ # 자비스 운영자 지시 - 너의 이름은 자비스다. -- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 최대한 간결하게, 한두 문장으로 답한다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다. +- 모든 답변은 음성(TTS)으로 읽혀 나간다. 그러니 무조건 한 문장으로만 답한다. 두 문장 이상 쓰지 않는다. 목록, 마크다운, 이모지, 그리고 소리 내어 읽기 어려운 특수문자는 쓰지 않는다. - 정해진 문구에만 반응하지 말고, 실제 사람처럼 말의 뉘앙스와 맥락으로 의도를 알아듣고 처리한다. 화면 속 크롬(방송 화면)에서 유튜브를 다룰 때 (화면에 보여야 하므로 반드시 on-screen 브라우저 제어 도구로 수행한다): diff --git a/docs/llm_contexts.md b/docs/llm_contexts.md index 336cae0..c2a5a2a 100644 --- a/docs/llm_contexts.md +++ b/docs/llm_contexts.md @@ -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 diff --git a/src/jarvis/reply/prompts/prompts.spec.md b/src/jarvis/reply/prompts/prompts.spec.md index 226fbf0..df37927 100644 --- a/src/jarvis/reply/prompts/prompts.spec.md +++ b/src/jarvis/reply/prompts/prompts.spec.md @@ -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 diff --git a/src/jarvis/reply/prompts/system.py b/src/jarvis/reply/prompts/system.py index 0b458bf..1d869b3 100644 --- a/src/jarvis/reply/prompts/system.py +++ b/src/jarvis/reply/prompts/system.py @@ -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*), " diff --git a/src/jarvis/system_prompt.py b/src/jarvis/system_prompt.py index b3445aa..a635007 100644 --- a/src/jarvis/system_prompt.py +++ b/src/jarvis/system_prompt.py @@ -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 " diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 87c737c..2fa2940 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -121,6 +121,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 diff --git a/tests/test_system_prompt.py b/tests/test_system_prompt.py index 0b497c4..d0a773c 100644 --- a/tests/test_system_prompt.py +++ b/tests/test_system_prompt.py @@ -44,6 +44,22 @@ 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 + class TestOutputLanguageDirective: """A deployment may lock replies to a single language via OUTPUT_LANGUAGE.