Small local models (qwen2.5-vl-3b, gemma-4-e4b) narrate actions ('Sure,
I'll pause the music') without emitting the tool call once the full
system prompt and 26-tool set are in context. Measured: qwen emitted
tools in 2/8 realistic intents, gemma 5/8; single-tool tests pass 100%,
so this is context-induced drift, not plumbing.
- agent/intent_router.py: deterministic forced-call router for
high-frequency intents — media controls, now_playing/list_library,
'remember that ...' memory saves, explicit YouTube Music searches,
and personal-fact recall (memory_recall/search_notes/skill_recall,
excluded for weather/time questions). Fires only when the tool is
available; guards against how-do-I questions.
- agent.py: generalize _ForcedToolCallStream to any tool; route through
intent_router after the dispatch check. System prompt adds a hard
'call, don't narrate' rule and reconditions 'answer directly'.
- tests/test_intent_router.py: 30 unit tests pinning router behavior.
Verified against the live LLM with the full fidelity harness: combined
forced-or-emitted coverage 8/8 on qwen2.5-vl-3b (was 2/8) and 8/8 on
gemma-4-e4b (was 5/8). Full suite: 79 passed.
Hope test suite
End-to-end tests that drive the actual running container — no mocks. They run on the host and hit the live web UI, token endpoint, LLM API, MCP tool servers, and agent process.
Prerequisites
- The container is up:
docker compose up -d(and healthy) - Host deps installed:
pip install -r tests/requirements.txt(oruv pip install -r tests/requirements.txt) - The LLM at
LLM_BASE_URL(defaulthttp://192.168.86.2:3005/api, OpenWebUI) is reachable - Internet access for the weather tool tests (wttr.in)
Run
cd /home/shane/dev/voice
python -m pytest tests/ -v # everything
python -m pytest tests/ -v -m "not slow" # skip LLM/network-heavy tests
What each file covers
| File | Target | Notes |
|---|---|---|
test_web_ui.py |
nginx HTTPS UI (:9178) + token endpoint (:8091) | static assets, /token (direct + via nginx), /livekit/ proxy |
test_llm_api.py |
LLM API directly | completion, tool calling, enable_thinking=false latency, streaming |
test_mcp_tools.py |
MCP servers via docker exec |
weather/time, authenticated remote memory save/recall/delete, and local skills — probes are cleaned up |
test_media.py |
media server API + player UI + media MCP | library/player state flow, Range proxy, static mp3 Range, live YT Music search (-m slow), gated full download (HOPE_RUN_YTDLP_DOWNLOAD=1) |
test_agent_integration.py |
running container | agent process alive, worker registered with LiveKit, all 4 supervised processes up, container health, LiveKit HTTP API |
test_compaction.py |
HopeLLM._compact_context in-container |
compaction triggers >24 items, system prompt preserved, short context untouched |
Notes
- LLM and MCP tests are marked
@pytest.mark.slow(1–30s each). - The token endpoint is POST
/token→{"token": "<JWT>"}. - Process checks use
/proc/*/cmdlinebecause the container image has nops, and the supervisorctl unix socket is not exposed in this build.