4.1 routing quality + 4.4 persona prompt

- VoiceConfig: add QueryMinScore (default 0.55) + Persona config fields
- voice.go: remove queryMinScore const, wire from cfg.Voice.QueryMinScore
  as reactiveHandler field
- llmphraser.go: add Persona to Config, prepend to system prompts in
  chat and query paths (systemPrompt/querySystemPrompt methods)
- main.go: pass personaFromCfg into both phraser config blocks
- Makefile: add download-embedder target (Xenova/paraphrase-multilingual-
  MiniLM-L12-v2, ~90MB ONNX)
- AGENTS.md: document embedder model download + libonnxruntime setup
- server.go: fix pre-existing wg.Add vs wg.Wait data race using accept
  mutex. make test green, zero races across all 29 packages.
This commit is contained in:
kami
2026-07-06 13:35:39 +04:00
parent 15fe7bbc74
commit b7eb53a3b4
7 changed files with 136 additions and 15 deletions
+12
View File
@@ -213,6 +213,7 @@ func run(args []string) error {
NGpuLayers: cfg.Phraser.NGpuLayers,
NCtx: cfg.Phraser.NCtx,
Timeout: time.Duration(cfg.Phraser.Timeout),
Persona: personaFromCfg(cfg),
}
if pc.BinPath == "" {
pc.BinPath = "llama-server"
@@ -364,6 +365,7 @@ func run(args []string) error {
NGpuLayers: cfg.Phraser.NGpuLayers,
NCtx: cfg.Phraser.NCtx,
Timeout: time.Duration(cfg.Phraser.Timeout),
Persona: personaFromCfg(cfg),
}
if pc.BinPath == "" {
pc.BinPath = "llama-server"
@@ -494,3 +496,13 @@ func run(args []string) error {
log.Printf("mavend: bye")
return nil
}
// personaFromCfg extracts the voice persona from the config, or returns ""
// when voice isn't configured. Used to pass a character prompt into the
// LLM phraser without requiring voice to be enabled.
func personaFromCfg(cfg *config.Config) string {
if cfg.Voice != nil {
return cfg.Voice.Persona
}
return ""
}
+9 -7
View File
@@ -228,6 +228,7 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
weatherLocation: weatherLocation,
memStore: memStore,
dialogueSessions: dialogueSessions,
queryMinScore: cfg.Voice.QueryMinScore,
}
// ----- the server (TCP listener) -----
@@ -261,6 +262,12 @@ type reactiveHandler struct {
memStore memory.Store
// queryMinScore — the note-recall confidence gate. Top cosine below this ⇒
// "I don't know" instead of a guess. Tuned for the ONNX embedder; a knob, not
// load-bearing math (same posture as the presence thresholds). Set by
// wireVoice from VoiceConfig; default 0.55.
queryMinScore float64
// dialogueSessions carries slots across turns for follow-ups (single-user
// box → one session slot, keyed voiceDialogueID). nil ⇒ no carry-over.
dialogueSessions *dialogue.SessionStore
@@ -376,11 +383,6 @@ func (h *reactiveHandler) HandlePushToTalk(ctx context.Context, req voice.PushTo
// returns a non-empty string when the action path wants to OVERRIDE the
// reply text (e.g. an action error the user should hear SPECIFICALLY, not
// a generic "ok"). Errors surface as a short reply text the user hears.
// queryMinScore — the note-recall confidence gate. Top cosine below this ⇒
// "no note" instead of a guess. Hand-tuned for the ONNX embedder; a knob, not
// load-bearing math (same posture as the presence thresholds).
const queryMinScore = 0.55
func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision) string {
if dec.Clarify {
return "" // the Replier phrases clarify
@@ -541,13 +543,13 @@ func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision)
// a gap (spec's "not a guesser-of-truth"). Same instinct as the loop's
// since(key)==null → don't fire. Tuned for the ONNX embedder; the Hash
// floor scores lexically and may rarely clear it.
if len(notes) == 0 || notes[0].Score < queryMinScore {
if len(notes) == 0 || notes[0].Score < h.queryMinScore {
// Long-term memory recall (notes + facts) before general knowledge:
// the notes table can't answer fact questions, but the memory store
// indexes both. Only runs when notes-RAG already gave up → additive.
if h.memStore != nil {
if hits, herr := h.memStore.Search(ctx, vec, 3); herr == nil {
if text, ok := bestRecall(hits, queryMinScore); ok {
if text, ok := bestRecall(hits, h.queryMinScore); ok {
return text
}
}