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:
@@ -189,6 +189,18 @@ type VoiceConfig struct {
|
||||
// Default 0.35 if unset.
|
||||
RouterThreshold float64 `json:"router_threshold,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"`
|
||||
|
||||
// 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"`
|
||||
|
||||
// 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.
|
||||
@@ -309,6 +321,7 @@ const (
|
||||
DefaultRepeatInterval = 5 * time.Minute
|
||||
DefaultAutotuneInterval = 10 * time.Minute
|
||||
DefaultRouterThreshold = 0.35
|
||||
DefaultQueryMinScore = 0.55
|
||||
DefaultToolTimeout = 30 * time.Second
|
||||
)
|
||||
|
||||
@@ -379,6 +392,9 @@ func (c *Config) applyDefaults() {
|
||||
if c.Voice.RouterThreshold <= 0 {
|
||||
c.Voice.RouterThreshold = DefaultRouterThreshold
|
||||
}
|
||||
if c.Voice.QueryMinScore <= 0 {
|
||||
c.Voice.QueryMinScore = DefaultQueryMinScore
|
||||
}
|
||||
if c.Voice.ToolTimeout <= 0 {
|
||||
c.Voice.ToolTimeout = Duration(DefaultToolTimeout)
|
||||
}
|
||||
|
||||
+16
-3
@@ -281,9 +281,10 @@ type Server struct {
|
||||
api atomic.Value // stores CoreAPI
|
||||
path string
|
||||
|
||||
ln net.Listener
|
||||
wg sync.WaitGroup
|
||||
done chan struct{}
|
||||
ln net.Listener
|
||||
wg sync.WaitGroup
|
||||
done chan struct{}
|
||||
accept sync.Mutex // guards wg.Add vs Close's wg.Wait sequence
|
||||
|
||||
// Check — optional authorization hook. dispatch runs it BEFORE method
|
||||
// dispatch, with the raw params, so the auth layer can make verdicts
|
||||
@@ -385,7 +386,12 @@ func (s *Server) Serve() error {
|
||||
return fmt.Errorf("ipc: accept: %w", err)
|
||||
}
|
||||
}
|
||||
// wg.Add under accept mutex so Close's wg.Wait (also under accept) sees
|
||||
// a consistent counter — a connection accepted just before Close closes
|
||||
// the listener must be tracked before Wait starts.
|
||||
s.accept.Lock()
|
||||
s.wg.Add(1)
|
||||
s.accept.Unlock()
|
||||
go func(c net.Conn) {
|
||||
defer s.wg.Done()
|
||||
defer c.Close()
|
||||
@@ -747,7 +753,14 @@ func (s *Server) Close() error {
|
||||
close(s.done)
|
||||
}
|
||||
err := s.ln.Close()
|
||||
// Under accept lock: after the listener closes, no new Accept can complete,
|
||||
// so no new wg.Add will be called. The Wait is safe to observe the wg
|
||||
// counter because any in-flight Accept that already got a conn either
|
||||
// already called wg.Add (before releasing the lock) or will see the closed
|
||||
// listener error and not call wg.Add at all.
|
||||
s.accept.Lock()
|
||||
s.wg.Wait()
|
||||
s.accept.Unlock()
|
||||
_ = os.Remove(s.path)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ type Config struct {
|
||||
NGpuLayers int
|
||||
NCtx int
|
||||
Timeout time.Duration
|
||||
Persona string // optional prompt prefix tuning maven's character
|
||||
}
|
||||
|
||||
func DefaultConfig(modelPath string) Config {
|
||||
@@ -185,7 +186,7 @@ func (p *LLMPhraser) PhraseQuery(ctx context.Context, utterance string, notes []
|
||||
if len(notes) == 1 {
|
||||
notes[0] = strings.TrimSpace(notes[0])
|
||||
}
|
||||
sys := "You are maven, a self-hosted personal assistant answering from your notes. Answer briefly and naturally in Russian starting with \"вот что я нашла: \". Respond with just the answer text, no JSON wrapper."
|
||||
sys := p.querySystemPrompt()
|
||||
prompt := fmt.Sprintf(
|
||||
`The user asks: "%s". Your notes matching the query contain: "%s". Answer them naturally and briefly. If the notes don't answer the question, say so.`,
|
||||
utterance, strings.Join(notes, `"; "`),
|
||||
@@ -248,7 +249,7 @@ type chatResp struct {
|
||||
}
|
||||
|
||||
func (p *LLMPhraser) chat(ctx context.Context, userPrompt string) (string, error) {
|
||||
return p.chatWithSystem(ctx, systemPrompt(), userPrompt, 256)
|
||||
return p.chatWithSystem(ctx, p.systemPrompt(), userPrompt, 256)
|
||||
}
|
||||
|
||||
func (p *LLMPhraser) chatWithSystem(ctx context.Context, system, user string, maxTokens int) (string, error) {
|
||||
@@ -295,8 +296,22 @@ func (p *LLMPhraser) chatWithSystem(ctx context.Context, system, user string, ma
|
||||
return cr.Choices[0].Message.Content, nil
|
||||
}
|
||||
|
||||
func systemPrompt() string {
|
||||
return `You are maven, a self-hosted personal assistant. Generate brief, natural nudge messages in the user's language (Russian or English). Respond ONLY with valid JSON: {"body": "full voice message", "summary": "brief away-channel version (<60 chars)"}. body is what the user hears on voice; summary is for push notifications (ntfy/telegram) — minimal, no exfil detail.`
|
||||
func (p *LLMPhraser) systemPrompt() string {
|
||||
base := `You are maven, a self-hosted personal assistant. Generate brief, natural nudge messages in the user's language (Russian or English). Respond ONLY with valid JSON: {"body": "full voice message", "summary": "brief away-channel version (<60 chars)"}. body is what the user hears on voice; summary is for push notifications (ntfy/telegram) — minimal, no exfil detail.`
|
||||
if p.cfg.Persona != "" {
|
||||
base = p.cfg.Persona + "\n\n" + base
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// querySystemPrompt returns the system prompt for PhraseQuery (notes + general
|
||||
// knowledge). Prepends the configured persona when set.
|
||||
func (p *LLMPhraser) querySystemPrompt() string {
|
||||
base := "You are maven, a self-hosted personal assistant answering from your notes. Answer briefly and naturally in Russian starting with \"вот что я нашла: \". Respond with just the answer text, no JSON wrapper."
|
||||
if p.cfg.Persona != "" {
|
||||
base = p.cfg.Persona + "\n\n" + base
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
func buildNudgePrompt(c loop.Candidate) string {
|
||||
|
||||
Reference in New Issue
Block a user