Add a voice.llm_router flag, default off
Wires cmd/mavend/voice.go to build the LLM router when the operator asks for it. Default false, so nothing changes on the deploy box. Look at pickLLMRouter: the flag on with no llama-server logs one line and keeps the classifier, it never fails a turn. The default stays off until the router can refuse (#359) and the extractor runs on LLM decisions — both noted as TODOs in config.go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
This commit is contained in:
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user