a4abcdefa3
embedder.heads_path is empty by default and deploy/mavend.json sets it. A missing or broken weights file logs and leaves the heads nil, because refusing to start over a routing accelerator would trade a working box for a better one. TestONNXRoutingHeads is the same cascade TestONNXBaseline scores with one arm added, so the two are directly comparable. It also checks the Go tokenizer against the Python one, since the heads were trained through transformers and are read through a hand-written tokenizer: a mismatch shows up here as a score below what Python measured on the same weights, and nowhere else. That is how the reversed word pieces were found. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013ptwopxyo3Z2kwFckHkLvN
257 lines
11 KiB
Go
257 lines
11 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// The blocks nested under voice: the two worker seams, the embedder the
|
|
// classifier scores with, the weather provider and the act allowlist. They live
|
|
// here because none of them is reachable except through a voice block.
|
|
|
|
// WorkerConfig — a unix-socket worker module connection. Used by Stt and
|
|
// (via TtsConfig embedding the same fields) by Tts. Socket is the unix
|
|
// socket path the worker module listens on (e.g.
|
|
// /run/user/$UID/maven/stt.sock). Lang overrides the surface default for
|
|
// this module when the user wants different langs for stt vs tts (rare).
|
|
type WorkerConfig struct {
|
|
Socket string `json:"socket,omitempty"`
|
|
Lang string `json:"lang,omitempty"`
|
|
}
|
|
|
|
// TtsConfig — the tts worker module connection + tts-specific Voice field
|
|
// (a named voice when the worker supports multiple; "" ⇒ the worker's
|
|
// configured default).
|
|
type TtsConfig struct {
|
|
Socket string `json:"socket,omitempty"`
|
|
Lang string `json:"lang,omitempty"`
|
|
Voice string `json:"voice,omitempty"`
|
|
}
|
|
|
|
// EmbedderConfig — paths for the ONNX multilingual embedder. The daemon
|
|
// constructs an in-process ONNX embedder when all three paths are non-empty;
|
|
// the router's classifier then uses real sentence embeddings instead of the
|
|
// floor HashEmbedder stub. Model_path is the ONNX model file, tokenizer_path
|
|
// is tokenizer.json (Unigram), lib_path is the ONNX Runtime shared library.
|
|
type EmbedderConfig struct {
|
|
ModelPath string `json:"model_path,omitempty"`
|
|
TokenizerPath string `json:"tokenizer_path,omitempty"`
|
|
LibPath string `json:"lib_path,omitempty"`
|
|
|
|
// HeadsPath — the routing heads graph, which is a fine-tuned COPY of the
|
|
// model above with four linear heads on its pooled output (V-664). Empty
|
|
// means no heads, and the cascade runs exactly as it did before they
|
|
// existed. It shares LibPath and TokenizerPath, and router_heads.json is
|
|
// read from the same directory.
|
|
//
|
|
// It must never be pointed at ModelPath. Memory recall depends on the
|
|
// resident copy scoring what it scored, and the fine-tuned one does not.
|
|
HeadsPath string `json:"heads_path,omitempty"`
|
|
}
|
|
|
|
// WeatherConfig configures the weather provider for voice queries.
|
|
type WeatherConfig struct {
|
|
Provider string `json:"provider,omitempty"` // "open-meteo" or "" → stub
|
|
DefaultLocation string `json:"default_location,omitempty"` // e.g. "Moscow"
|
|
}
|
|
|
|
// ToolConfig — one enabled tool. Name is the spoken verb ("restart"); Cmd is
|
|
// the fixed argv prefix (["systemctl","restart"]); Destructive marks acts that
|
|
// must not fire from the voice path (they need a confirm on an authed surface).
|
|
//
|
|
// Aliases are the spoken phrases that reach this tool, Russian included. They
|
|
// are config data rather than a pattern in code, and they match as exact leading
|
|
// tokens, so an imperative reaches the tool and the past tense of the same verb
|
|
// does not.
|
|
type ToolConfig struct {
|
|
Name string `json:"name"`
|
|
Scope string `json:"scope,omitempty"`
|
|
Cmd []string `json:"cmd"`
|
|
Destructive bool `json:"destructive,omitempty"`
|
|
Aliases []string `json:"aliases,omitempty"`
|
|
}
|
|
|
|
// Voice defaults, applied in normaliseVoice.
|
|
const (
|
|
DefaultRouterThreshold = 0.55
|
|
DefaultQueryMinScore = 0.55
|
|
// Read off the margin sweep in internal/memory/recalleval on the e5
|
|
// embedder: 0.008 answers 68% of real questions (down from 72%) and cuts
|
|
// false recall from 5/5 to 1/5. Every larger delta costs real recall
|
|
// without removing that last one until 0.020, which drops recall to 44%.
|
|
DefaultQueryMinMargin = 0.008
|
|
// DefaultClarifyMaxAttempts — see dialogue.DefaultMaxAttempts.
|
|
DefaultClarifyMaxAttempts = 3
|
|
DefaultToolTimeout = 30 * time.Second
|
|
// DefaultLLMRouter — route with the resident model unless told otherwise.
|
|
DefaultLLMRouter = true
|
|
)
|
|
|
|
// UseLLMRouter reports whether to route with the resident model. Unset means
|
|
// on; only an explicit false in the config turns it off.
|
|
func (v *VoiceConfig) UseLLMRouter() bool {
|
|
if v == nil || v.LLMRouter == nil {
|
|
return DefaultLLMRouter
|
|
}
|
|
return *v.LLMRouter
|
|
}
|
|
|
|
// normaliseVoice applies the block's defaults. An absent block stays nil: the
|
|
// surface is off and there is nothing to tune.
|
|
func (c *Config) normaliseVoice() {
|
|
if c.Voice == nil {
|
|
return
|
|
}
|
|
if c.Voice.RouterThreshold <= 0 {
|
|
c.Voice.RouterThreshold = DefaultRouterThreshold
|
|
}
|
|
if c.Voice.QueryMinScore <= 0 {
|
|
c.Voice.QueryMinScore = DefaultQueryMinScore
|
|
}
|
|
// Unset ⇒ default. Negative is how you turn the margin off on purpose,
|
|
// so it is clamped to 0 rather than replaced by the default.
|
|
switch {
|
|
case c.Voice.QueryMinMargin == 0:
|
|
c.Voice.QueryMinMargin = DefaultQueryMinMargin
|
|
case c.Voice.QueryMinMargin < 0:
|
|
c.Voice.QueryMinMargin = 0
|
|
}
|
|
if c.Voice.ClarifyMaxAttempts <= 0 {
|
|
c.Voice.ClarifyMaxAttempts = DefaultClarifyMaxAttempts
|
|
}
|
|
if c.Voice.ToolTimeout <= 0 {
|
|
c.Voice.ToolTimeout = Duration(DefaultToolTimeout)
|
|
}
|
|
if c.Voice.LLMRouter == nil {
|
|
on := DefaultLLMRouter
|
|
c.Voice.LLMRouter = &on
|
|
}
|
|
}
|
|
|
|
// validateVoice refuses a surface that would listen nowhere, and an embedder
|
|
// block with only some of its three paths filled in.
|
|
func (c *Config) validateVoice() error {
|
|
if c.Voice == nil || !c.Voice.Enabled {
|
|
return nil
|
|
}
|
|
if c.Voice.Bind == "" {
|
|
return errors.New("voice.enabled set but voice.bind is empty — refusing to start a voice surface with no bind address")
|
|
}
|
|
if e := c.Voice.Embedder; e != nil {
|
|
if e.ModelPath == "" || e.TokenizerPath == "" || e.LibPath == "" {
|
|
return errors.New("voice.embedder: all three of model_path, tokenizer_path, lib_path must be set, or remove embedder to use the floor stub")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// VoiceConfig — the client↔core TCP surface + the stt/tts worker-module
|
|
// seams.
|
|
//
|
|
// Enabled gates wiring; Bind is the TCP address (inside the wg tunnel in
|
|
// production; "127.0.0.1:9100" for the local smoke). Lang is the default
|
|
// language hint passed to both stt and tts (per-call overrides later).
|
|
//
|
|
// Stt and Tts are the worker-module seams. nil Stt ⇒ daemon wires the
|
|
// in-process stt.Stub (the "no models on disk" floor — the loop is
|
|
// exercisable end-to-end with a deterministic no-model transcriber).
|
|
// non-nil Stt with Socket ⇒ daemon wires stt.Remote dialing that unix
|
|
// socket (cmd/mavsttd serves the other end; production swaps in a
|
|
// faster-whisper handler in cmd/mavsttd, no daemon or stt-package
|
|
// change). Tts mirrors for tts.Remote + cmd/mavttsd.
|
|
//
|
|
// Embedder configures the router's sentence embedder. When all three
|
|
// paths are set, the daemon constructs an ONNX multilingual embedder
|
|
// (in-process); when nil, it falls back to the floor HashEmbedder stub
|
|
// (deterministic, no model files required — good for CI and smoke).
|
|
//
|
|
// The daemon refuses to start if Voice.Enabled but Bind is empty — the
|
|
// bind is the one operational config the surface can't default (127.0.0.1
|
|
// is too relaxed for production, a wg-tunnel address is the user's);
|
|
// surfacing the gap explicitly beats an idle listener the user thinks is
|
|
// wired but isn't reachable.
|
|
type VoiceConfig struct {
|
|
Enabled bool `json:"enabled,omitempty"`
|
|
Bind string `json:"bind,omitempty"`
|
|
Lang string `json:"lang,omitempty"`
|
|
Stt *WorkerConfig `json:"stt,omitempty"`
|
|
Tts *TtsConfig `json:"tts,omitempty"`
|
|
Embedder *EmbedderConfig `json:"embedder,omitempty"`
|
|
|
|
// RouterThreshold — the minimum confidence score for the intent classifier
|
|
// (stage 3 gate). Below this → clarify, don't guess. 0 ⇒
|
|
// DefaultRouterThreshold (0.55), tuned for the ONNX embedder; the
|
|
// HashEmbedder floor scores lexically and may need a lower value.
|
|
//
|
|
// There is no way to ask for "permissive, never clarify" through this
|
|
// field: normaliseVoice replaces anything ≤ 0 with the default, so a
|
|
// written 0 is the default and a written negative is too.
|
|
RouterThreshold float64 `json:"router_threshold,omitempty"`
|
|
|
|
// LLMRouter — route with the resident model instead of the embedding
|
|
// classifier. On by default since Vikunja #320.
|
|
//
|
|
// Measured on the held-out fixture (docs/evals/2026-07-31-routing.md): 63.2% of
|
|
// intents right against the classifier's 50.0%, and no route errors. It
|
|
// costs about 1s per turn instead of 30ms.
|
|
//
|
|
// It is safe to leave on. The model can refuse — it answers "unknown" when
|
|
// it cannot route, and the turn drops to the classifier and its clarify
|
|
// gate. Any LLM error does the same, so a turn never breaks on the model.
|
|
// Slot extraction runs on LLM decisions too, so acts get their Fn and
|
|
// reminders their Time.
|
|
//
|
|
// Set it false to go back to the classifier, e.g. on a box with no
|
|
// llama-server or when 1s a turn is too slow.
|
|
//
|
|
// It is a pointer so that "missing from the file" and "explicitly false"
|
|
// are different things: missing means on, false means off. Read it with
|
|
// UseLLMRouter(), not directly.
|
|
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
|
|
// default if unset.
|
|
QueryMinScore float64 `json:"query_min_score,omitempty"`
|
|
|
|
// QueryMinMargin — the second half of the recall gate: the top hit must
|
|
// beat the runner-up by more than this. The absolute score above cannot do
|
|
// the job on its own, because the e5 embedder puts every cosine in one
|
|
// narrow high band, so a made-up question scores as high as a real one.
|
|
// The margin asks whether one note is clearly the best instead.
|
|
// Negative ⇒ off. 0 ⇒ the default below.
|
|
QueryMinMargin float64 `json:"query_min_margin,omitempty"`
|
|
|
|
// ClarifyMaxAttempts — how many clarifying questions she may ask about one
|
|
// request before she gives up and says she did not understand. Default 3.
|
|
ClarifyMaxAttempts int `json:"clarify_max_attempts,omitempty"`
|
|
|
|
// Persona — optional prompt prefix that tunes maven's character. Prepended
|
|
// to every LLM system prompt (nudge phrasing, note queries, general
|
|
// knowledge). Empty string ⇒ current hardcoded persona (feminine-gendered
|
|
// Russian self-reference). Example: "Be formal and answer in English only."
|
|
Persona string `json:"persona,omitempty"`
|
|
|
|
// OwnerName / City — optional facts about the owner, added to the shared
|
|
// context block (internal/persona). Empty is fine: the block still states
|
|
// who he is grammatically (a man, addressed as "ты") and the current time.
|
|
// Nothing about correct behaviour may depend on these being filled in.
|
|
OwnerName string `json:"owner_name,omitempty"`
|
|
City string `json:"city,omitempty"`
|
|
|
|
// Weather — the weather provider config. nil ⇒ the daemon wires
|
|
// the stub provider (returns ErrNotConfigured — "погода не настроена").
|
|
// Set provider to "open-meteo" to use the keyless Open-Meteo API.
|
|
Weather *WeatherConfig `json:"weather,omitempty"`
|
|
|
|
// Tools — the enabled act allowlist. Each is a spoken verb → argv the
|
|
// executor runs (args from the utterance appended). Editing this set is the
|
|
// human-only "enable" act (per spec); maven can't add to it from a request.
|
|
// Empty ⇒ every act is refused (nothing enabled).
|
|
Tools []ToolConfig `json:"tools,omitempty"`
|
|
|
|
// ToolTimeout bounds each tool invocation. Zero ⇒ executor default (30s).
|
|
ToolTimeout Duration `json:"tool_timeout,omitempty"`
|
|
}
|