Compare commits
2 Commits
v1.8.0
...
680f5a656a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
680f5a656a | ||
|
|
b52ffd2b18 |
11
.env.example
11
.env.example
@@ -59,11 +59,12 @@ OLLAMA_BASE_URL=http://127.0.0.1:11434
|
||||
# free-form chit-chat. Swap back to qwen3:8b for the strongest tool-calling.
|
||||
OLLAMA_CHAT_MODEL=qwen2.5:3b
|
||||
# Model for the auxiliary small-model calls: intent judge, tool router, weather
|
||||
# place extraction, query decomposition. BLANK (default) reuses OLLAMA_CHAT_MODEL
|
||||
# so the stack runs on one already-warm model. The code's built-in default
|
||||
# (gemma4:e2b) is NOT pulled by this stack, so leaving this unset previously made
|
||||
# every router/extractor call silently fail. Only set this if you also pull the
|
||||
# model into Ollama.
|
||||
# place extraction, query decomposition. These are classification/JSON calls,
|
||||
# NOT the spoken answer, so a small fast model here cuts 2-3 big-model round
|
||||
# trips per command without touching answer quality. BLANK uses the stack
|
||||
# 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).
|
||||
OLLAMA_INTENT_MODEL=
|
||||
OLLAMA_EMBED_MODEL=nomic-embed-text
|
||||
WHISPER_MODEL=small
|
||||
|
||||
@@ -87,7 +87,7 @@ COMPOSE_FILE=docker-compose.yml;docker-compose.gpu-windows.yml
|
||||
> Linux와 Windows는 GPU를 컨테이너에 넣는 방식이 달라서 override 파일이 갈립니다. Linux는 CDI(`devices: nvidia.com/gpu=all`), Windows(Docker Desktop)는 Compose의 `deploy.resources.reservations.devices`(`driver: nvidia`)를 씁니다. 호스트 사전 준비는 아래 "GPU 가속" 절 참고.
|
||||
|
||||
`docker compose up` 한 번이면 자동으로:
|
||||
- Ollama 서버가 뜨고, `ollama-init`이 채팅/임베딩 모델을 **자동 pull**
|
||||
- Ollama 서버가 뜨고, `ollama-init`이 채팅/보조(의도·라우팅)/임베딩 모델을 **자동 pull** (보조 모델 `OLLAMA_INTENT_MODEL`은 기본 `qwen2.5:3b`로, 큰 채팅 모델은 답변에만 쓰고 내부 분류 호출은 이 작은 모델이 처리)
|
||||
- VNC+XFCE 데스크톱 + 크롬 + Python 브릿지가 기동
|
||||
- Whisper STT 모델 / Piper TTS 음성 자동 다운로드(볼륨에 캐시)
|
||||
|
||||
|
||||
@@ -40,6 +40,9 @@ services:
|
||||
environment:
|
||||
OLLAMA_HOST: http://ollama:11434
|
||||
CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-qwen2.5:3b}
|
||||
# Small auxiliary model for intent/router/extraction calls (see javis
|
||||
# service). Pulled here so the split is ready out of the box.
|
||||
INTENT_MODEL: ${OLLAMA_INTENT_MODEL:-qwen2.5:3b}
|
||||
EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text}
|
||||
entrypoint: ["/bin/sh", "-c"]
|
||||
command:
|
||||
@@ -48,6 +51,10 @@ services:
|
||||
until ollama list >/dev/null 2>&1; do sleep 2; done;
|
||||
echo "[ollama-init] pulling $$CHAT_MODEL";
|
||||
ollama pull "$$CHAT_MODEL";
|
||||
if [ -n "$$INTENT_MODEL" ] && [ "$$INTENT_MODEL" != "$$CHAT_MODEL" ]; then
|
||||
echo "[ollama-init] pulling $$INTENT_MODEL (auxiliary intent/router model)";
|
||||
ollama pull "$$INTENT_MODEL";
|
||||
fi;
|
||||
echo "[ollama-init] pulling $$EMBED_MODEL";
|
||||
ollama pull "$$EMBED_MODEL";
|
||||
echo "[ollama-init] models ready.";
|
||||
@@ -62,6 +69,14 @@ services:
|
||||
# Point the brain at the ollama service and the bot at the in-container bridge.
|
||||
OLLAMA_BASE_URL: http://ollama:11434
|
||||
OLLAMA_CHAT_MODEL: ${OLLAMA_CHAT_MODEL:-qwen2.5:3b}
|
||||
# Auxiliary small-model calls (intent judge, tool router, arg extraction,
|
||||
# query decomposition) run on this fast model so the big chat model only
|
||||
# runs for the actual spoken answer. With the GPU on, this is the main
|
||||
# per-turn latency win: a command no longer pays the big model's cost 2-3
|
||||
# times for routing/extraction. Defaults to qwen2.5:3b (the project's
|
||||
# reference small model, clean Korean on classification); set it equal to
|
||||
# OLLAMA_CHAT_MODEL to fold everything back onto one resident model.
|
||||
OLLAMA_INTENT_MODEL: ${OLLAMA_INTENT_MODEL:-qwen2.5:3b}
|
||||
OLLAMA_EMBED_MODEL: ${OLLAMA_EMBED_MODEL:-nomic-embed-text}
|
||||
WHISPER_MODEL: ${WHISPER_MODEL:-medium}
|
||||
WHISPER_DEVICE: ${WHISPER_DEVICE:-cuda}
|
||||
|
||||
@@ -10,12 +10,12 @@ set -euo pipefail
|
||||
: "${OLLAMA_BASE_URL:=http://ollama:11434}"
|
||||
: "${OLLAMA_CHAT_MODEL:=qwen3:8b}"
|
||||
# Auxiliary small-model calls (intent judge, tool router, weather place
|
||||
# extraction, query decomposition). The code default is gemma4:e2b, which this
|
||||
# stack does not pull, so those calls would silently fail and fall open —
|
||||
# crippling tool routing and arg extraction. Reuse the (already warm) chat model
|
||||
# by default so everything runs on one resident model; override if you pull a
|
||||
# dedicated small model.
|
||||
: "${OLLAMA_INTENT_MODEL:=${OLLAMA_CHAT_MODEL}}"
|
||||
# extraction, query decomposition). Default to a small fast model so the big
|
||||
# chat model only runs for the actual spoken answer — the main per-turn latency
|
||||
# win once the GPU is in use, since the 2-3 routing/extraction calls per command
|
||||
# no longer pay the big model's cost. ollama-init pulls this model. Set it equal
|
||||
# to OLLAMA_CHAT_MODEL to fold everything back onto one resident model.
|
||||
: "${OLLAMA_INTENT_MODEL:=qwen2.5:3b}"
|
||||
# Cap chat-model output tokens per turn (worst-case latency guard). Spoken
|
||||
# answers are 1-2 sentences; 512 is safe headroom above tool-call JSON. 0 = off.
|
||||
: "${OLLAMA_NUM_PREDICT:=512}"
|
||||
|
||||
@@ -26,7 +26,7 @@ Every distinct LLM call in Jarvis, what feeds it, what consumes it, and how it i
|
||||
|
||||
- **File**: [src/jarvis/listening/intent_judge.py](src/jarvis/listening/intent_judge.py) — `IntentJudge.evaluate()`.
|
||||
- **Trigger**: on a speech segment *only if* there is an engagement signal (wake word detected, hot-window active, or TTS playing). Pure ambient speech skips it.
|
||||
- **Model / gating**: `cfg.intent_judge_model` (default `gemma4:e2b`, ~2B). Falls back to text-based wake detection if Ollama is unavailable.
|
||||
- **Model / gating**: `cfg.intent_judge_model`. Code-level default `gemma4:e2b` (~2B); the **Docker stack** renders it from `OLLAMA_INTENT_MODEL` (default `qwen2.5:3b`, pulled by `ollama-init`), kept deliberately **separate from `ollama_chat_model`** so this judge and the tool router (#3, #7) run on a small fast model while the big chat model is reserved for the spoken answer. Setting `OLLAMA_INTENT_MODEL = OLLAMA_CHAT_MODEL` folds them back onto one resident model. Falls back to text-based wake detection if Ollama is unavailable.
|
||||
- **Inputs**:
|
||||
- Rolling transcript buffer (last 120s, with timestamps)
|
||||
- Wake-word timestamp (if any), normalised aliases
|
||||
@@ -246,7 +246,7 @@ user input
|
||||
3. Pre-warm the intent-judge model before TTS finishes.
|
||||
4. Cache tool-router (#7) output by query hash.
|
||||
5. Give each digest its own timeout budget rather than sharing `llm_digest_timeout_sec` (today a slow memory digest can starve the max-turn digest).
|
||||
6. Consider single-model deployments: router+planner prefer `intent_judge_model`; loading a second model hurts cold-start latency on small hardware.
|
||||
6. Two-model vs single-model tradeoff: the Docker default keeps a **separate** small `intent_judge_model` (`OLLAMA_INTENT_MODEL=qwen2.5:3b`) so routing/judging/extraction don't pay the big chat model's per-call cost — the main win once the GPU holds both models resident. On VRAM-constrained hardware, fold them onto one model by setting `OLLAMA_INTENT_MODEL = OLLAMA_CHAT_MODEL` (saves a resident model at the cost of slower routing when the chat model is large).
|
||||
7. Narrow `llm_thinking_enabled` to router/planner only, not every context.
|
||||
8. Reduce `intent_judge_timeout_sec` (15s) or race it against text-based wake detection to avoid blocking the audio loop.
|
||||
|
||||
|
||||
62
tests/test_intent_model_split.py
Normal file
62
tests/test_intent_model_split.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""The docker deployment must run auxiliary calls on a small model.
|
||||
|
||||
Latency win: intent judging, tool routing and arg extraction are
|
||||
classification/JSON calls, not the spoken answer. Running them on a small fast
|
||||
model means the big chat model only runs once per command (for the answer),
|
||||
instead of 2-3 times per command for routing/extraction too.
|
||||
|
||||
The wiring is: docker/jarvis-config.template.json renders `intent_judge_model`
|
||||
from `${OLLAMA_INTENT_MODEL}`, and the reply engine's resolver falls through
|
||||
`tool_router_model -> intent_judge_model -> ollama_chat_model`. The template
|
||||
sets no `tool_router_model`, so the auxiliary model is whatever
|
||||
`OLLAMA_INTENT_MODEL` renders to. These tests pin that behaviour end to end.
|
||||
"""
|
||||
|
||||
import json
|
||||
import string
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from jarvis.reply.engine import resolve_tool_router_model
|
||||
|
||||
TEMPLATE = Path(__file__).resolve().parent.parent / "docker" / "jarvis-config.template.json"
|
||||
|
||||
|
||||
def _render(**env) -> dict:
|
||||
raw = TEMPLATE.read_text(encoding="utf-8")
|
||||
return json.loads(string.Template(raw).safe_substitute(**env))
|
||||
|
||||
|
||||
class _Cfg:
|
||||
"""cfg stand-in carrying only the fields the resolver reads. The template
|
||||
does not render `tool_router_model`, so it stays empty here too."""
|
||||
|
||||
def __init__(self, rendered: dict):
|
||||
self.tool_router_model = rendered.get("tool_router_model", "") or ""
|
||||
self.intent_judge_model = rendered.get("intent_judge_model", "") or ""
|
||||
self.ollama_chat_model = rendered.get("ollama_chat_model", "") or ""
|
||||
|
||||
|
||||
def test_template_renders_separate_intent_model():
|
||||
cfg = _render(OLLAMA_CHAT_MODEL="qwen3:8b", OLLAMA_INTENT_MODEL="qwen2.5:3b")
|
||||
assert cfg["ollama_chat_model"] == "qwen3:8b"
|
||||
assert cfg["intent_judge_model"] == "qwen2.5:3b"
|
||||
assert cfg["intent_judge_model"] != cfg["ollama_chat_model"]
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_aux_calls_route_to_small_model_not_chat_model():
|
||||
"""The whole point: with a big chat model and a small intent model, tool
|
||||
routing must resolve to the small model, leaving the big model for answers."""
|
||||
cfg = _Cfg(_render(OLLAMA_CHAT_MODEL="qwen3:8b", OLLAMA_INTENT_MODEL="qwen2.5:3b"))
|
||||
assert resolve_tool_router_model(cfg) == "qwen2.5:3b"
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_folding_intent_onto_chat_model_keeps_one_model():
|
||||
"""Setting OLLAMA_INTENT_MODEL == OLLAMA_CHAT_MODEL folds everything back
|
||||
onto a single resident model (the documented VRAM-saving opt-out)."""
|
||||
cfg = _Cfg(_render(OLLAMA_CHAT_MODEL="qwen2.5:3b", OLLAMA_INTENT_MODEL="qwen2.5:3b"))
|
||||
assert resolve_tool_router_model(cfg) == "qwen2.5:3b"
|
||||
assert cfg.intent_judge_model == cfg.ollama_chat_model
|
||||
Reference in New Issue
Block a user