Merge the llm_router flag and the kill-maven fix

This commit is contained in:
kami
2026-07-31 02:17:00 +04:00
6 changed files with 99 additions and 7 deletions
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"testing"
"time"
"github.com/kami/maven/internal/llm"
)
func TestPickLLMRouterOff(t *testing.T) {
if r := pickLLMRouter(false, llm.New("http://127.0.0.1:1", time.Second)); r != nil {
t.Error("flag off should give no LLM router")
}
}
// The operator can turn the flag on without an LLM phraser configured. That must
// leave the classifier running, not panic.
func TestPickLLMRouterOnWithoutClient(t *testing.T) {
if r := pickLLMRouter(true, nil); r != nil {
t.Error("no llama-server should give no LLM router")
}
}
func TestPickLLMRouterOn(t *testing.T) {
if r := pickLLMRouter(true, llm.New("http://127.0.0.1:1", time.Second)); r == nil {
t.Error("flag on with a client should give an LLM router")
}
}
+20 -3
View File
@@ -199,8 +199,6 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
if lp, ok := phr.(*phraser.LLMPhraser); ok {
llmClient = llm.New(lp.BaseURL(), 60*time.Second)
}
// LLM router disabled — the classifier handles routing reliably.
// ----- router (the cascade; floor examples seed the classifier) -----
// The act matcher's allowlist is exactly the enabled tool names — the
// router only matches acts the executor can run (one source of truth).
@@ -208,7 +206,11 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
if threshold <= 0 {
threshold = config.DefaultRouterThreshold
}
rtr := buildRouter(emb, matcher, threshold, nil) // LLM router disabled
// Both routing paths are weak on held-out utterances — the classifier gets
// 36.8% of intents right, the resident model 50.0% and much slower. Off by
// default (see config.VoiceConfig.LLMRouter); the classifier always stays
// wired as the fallback, so a model error never breaks a turn.
rtr := buildRouter(emb, matcher, threshold, pickLLMRouter(cfg.Voice.LLMRouter, llmClient))
// ----- sessions registry (shared with voicesink) -----
sessions := voice.NewSessions()
@@ -1048,6 +1050,21 @@ func (h *reactiveHandler) reply(ctx context.Context, text string, _ []string) (v
return voice.PushToTalkResp{ReplyText: text, ReplyAudio: audioOut}, nil
}
// pickLLMRouter returns the LLM router when the operator asked for it and there
// is a llama-server to talk to, and nil otherwise. nil is safe: the cascade then
// routes with the classifier, so an unusable setting costs accuracy, not turns.
func pickLLMRouter(enabled bool, c *llm.Client) *router.LLMRouter {
if !enabled {
return nil
}
if c == nil {
log.Printf("voice: voice.llm_router is on but there is no llama-server to route with (the phraser is not an LLM phraser) — using the classifier instead")
return nil
}
log.Printf("voice: LLM router enabled")
return router.NewLLMRouter(c)
}
// buildRouter constructs the reactive-path router with the given embedder
// and confidence threshold.
// - stage-0 grammars from DefaultActMatcher whose fn allowlist is exactly
+1
View File
@@ -40,6 +40,7 @@
"tokenizer_path": "/opt/maven/models/embedder/tokenizer.json",
"lib_path": "/opt/maven/lib/libonnxruntime.so"
},
"llm_router": false,
"tool_timeout": "30s",
"tools": [
{ "name": "status", "cmd": ["systemctl", "status"], "scope": "homelab", "destructive": false },
+14
View File
@@ -257,6 +257,20 @@ type VoiceConfig struct {
// Default 0.35 if unset.
RouterThreshold float64 `json:"router_threshold,omitempty"`
// LLMRouter — route with the resident model instead of the embedding
// classifier. Measured on the held-out fixture (ROUTING-EVAL-31-07-2026.md)
// the model gets 50.0% of intents right against the classifier's 36.8%, but
// it costs about 800ms per turn instead of 30ms.
//
// TODO: the default stays false until two things land.
// 1. The LLM router cannot refuse. LLMRouter.Route hardcodes
// Confidence: 1.0, so the stage-3 clarify gate never fires and an
// unclear utterance becomes a confident wrong action (Vikunja #359).
// 2. Extractor.Extract never runs on an LLM decision, so acts arrive with
// no Fn and reminders with no Time.
// Turning this on today makes routing more accurate and less safe.
LLMRouter bool `json:"llm_router,omitempty"`
// QueryMinScore — the note-recall confidence gate. Top cosine below this
// ⇒ "I don't know" instead of a guess. Tuned for the ONNX embedder (0.55);
// the HashEmbedder floor scores lexically and may never clear it. 0.55
+22
View File
@@ -171,6 +171,28 @@ func TestWeatherConfigNilOK(t *testing.T) {
}
}
func TestLLMRouterDefaultsOff(t *testing.T) {
p := writeConfig(t, `{"voice":{"enabled":true,"bind":"127.0.0.1:9100"}}`)
c, err := Load(p)
if err != nil {
t.Fatalf("Load: %v", err)
}
if c.Voice.LLMRouter {
t.Error("voice.llm_router absent should mean false")
}
}
func TestLLMRouterRead(t *testing.T) {
p := writeConfig(t, `{"voice":{"enabled":true,"bind":"127.0.0.1:9100","llm_router":true}}`)
c, err := Load(p)
if err != nil {
t.Fatalf("Load: %v", err)
}
if !c.Voice.LLMRouter {
t.Error("voice.llm_router true was not read")
}
}
func TestDurationRoundTrip(t *testing.T) {
d := Duration(15 * time.Minute)
b, err := d.MarshalJSON()
+14 -4
View File
@@ -7,12 +7,22 @@
set -euo pipefail
# The llama-server the phraser spawns has NO "maven" in its command line (its
# args are `-m /path/to/LFM2.5-...gguf --port ...`), so a `llama-server.*maven`
# args are `-m /path/to/<model>.gguf --port ...`), so a `llama-server.*maven`
# pattern matches nothing and leaks it — the exact bug that let orphans pile up
# and OOM the box. Match the model instead. Override MODEL if you change it.
MODEL="${MODEL:-LFM2}"
# and OOM the box.
#
# We used to match the model name, defaulting to LFM2. The deploy now runs
# Qwen3.5-0.8B, so that default matched nothing and the server survived every
# kill. Match any llama-server serving a .gguf instead, so swapping the model in
# deploy/mavend.json cannot break this script again. Set MODEL to narrow it if
# some other llama-server on this box must be left alone.
MODEL="${MODEL:-}"
PAT='mavend|mavsttd|mavttsd|mavweb|mavpoll|mavenclient'
LLM="llama-server.*${MODEL}"
if [ -n "$MODEL" ]; then
LLM="llama-server.*${MODEL}"
else
LLM='llama-server.*\.gguf'
fi
echo "--- Sending graceful SIGTERM to Maven services ---"
pkill -TERM -f "$PAT" || true