Merge commit '4383844' into overnight-jul31
# Conflicts: # deploy/mavend.json # internal/config/config.go
This commit is contained in:
@@ -8,16 +8,13 @@ import "github.com/kami/maven/internal/memory"
|
||||
// that can answer "when did I last …?" from a captured fact). A note hit here
|
||||
// is redundant with the notes-RAG path — by design; the two indexes can diverge
|
||||
// once the backend is swapped for a persistent/external store. ok=false when
|
||||
// there's no hit above the threshold or the hit carries no text.
|
||||
func bestRecall(results []memory.Result, min float64) (string, bool) {
|
||||
if len(results) == 0 {
|
||||
// the hit fails the confidence gate (see memory.Confident: an absolute floor
|
||||
// plus a margin over the runner-up) or carries no text.
|
||||
func bestRecall(results []memory.Result, minScore, minMargin float64) (string, bool) {
|
||||
if !memory.Confident(results, minScore, minMargin) {
|
||||
return "", false
|
||||
}
|
||||
top := results[0]
|
||||
if top.Score < min {
|
||||
return "", false
|
||||
}
|
||||
text := top.Meta["text"]
|
||||
text := results[0].Meta["text"]
|
||||
if text == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -8,23 +8,24 @@ import (
|
||||
|
||||
func TestBestRecall(t *testing.T) {
|
||||
const min = 0.55
|
||||
const margin = 0.008
|
||||
|
||||
t.Run("empty results", func(t *testing.T) {
|
||||
if _, ok := bestRecall(nil, min); ok {
|
||||
if _, ok := bestRecall(nil, min, margin); ok {
|
||||
t.Error("empty results returned ok")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("top below threshold", func(t *testing.T) {
|
||||
res := []memory.Result{{Score: 0.4, Meta: map[string]string{"text": "выпил воды"}}}
|
||||
if _, ok := bestRecall(res, min); ok {
|
||||
if _, ok := bestRecall(res, min, margin); ok {
|
||||
t.Error("below-threshold hit returned ok")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("hit without text meta", func(t *testing.T) {
|
||||
res := []memory.Result{{Score: 0.9, Meta: map[string]string{"type": "fact"}}}
|
||||
if _, ok := bestRecall(res, min); ok {
|
||||
if _, ok := bestRecall(res, min, margin); ok {
|
||||
t.Error("textless hit returned ok")
|
||||
}
|
||||
})
|
||||
@@ -34,7 +35,7 @@ func TestBestRecall(t *testing.T) {
|
||||
{Score: 0.82, Meta: map[string]string{"text": "выпил воды в три часа", "type": "fact"}},
|
||||
{Score: 0.60, Meta: map[string]string{"text": "другое"}},
|
||||
}
|
||||
got, ok := bestRecall(res, min)
|
||||
got, ok := bestRecall(res, min, margin)
|
||||
if !ok {
|
||||
t.Fatal("clearing hit not returned")
|
||||
}
|
||||
@@ -42,4 +43,16 @@ func TestBestRecall(t *testing.T) {
|
||||
t.Errorf("wrong text: %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
// The runner-up is almost as close, so the embedder cannot tell the two
|
||||
// notes apart. Silence beats reading back a coin flip.
|
||||
t.Run("runner-up too close", func(t *testing.T) {
|
||||
res := []memory.Result{
|
||||
{Score: 0.860, Meta: map[string]string{"text": "выпил воды в три часа"}},
|
||||
{Score: 0.858, Meta: map[string]string{"text": "другое"}},
|
||||
}
|
||||
if _, ok := bestRecall(res, min, margin); ok {
|
||||
t.Error("thin-margin hit returned ok")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
+16
-7
@@ -257,6 +257,7 @@ func wireVoice(cfg *config.Config, coreAPI ipc.CoreAPI, phr phraser.Phraser, mem
|
||||
clarifyStore: clarifyStore,
|
||||
extractor: router.Extractor{Time: timeParser, Acts: matcher, Facts: router.DefaultFactParser{}},
|
||||
queryMinScore: cfg.Voice.QueryMinScore,
|
||||
queryMinMargin: cfg.Voice.QueryMinMargin,
|
||||
timeParser: timeParser,
|
||||
ecosystem: eco,
|
||||
}
|
||||
@@ -300,6 +301,9 @@ type reactiveHandler struct {
|
||||
// load-bearing math (same posture as the presence thresholds). Set by
|
||||
// wireVoice from VoiceConfig; default 0.55.
|
||||
queryMinScore float64
|
||||
// queryMinMargin — the second half of that gate: how far the top hit must
|
||||
// beat the runner-up. 0 ⇒ margin off.
|
||||
queryMinMargin float64
|
||||
|
||||
// timeParser — used as a fallback for stage-0 reminder grammar matches
|
||||
// (where the extractor didn't run). Shared with the router's extractor.
|
||||
@@ -760,18 +764,23 @@ func (h *reactiveHandler) applyAction(ctx context.Context, dec router.Decision)
|
||||
log.Printf("voice: query notes: %v", err)
|
||||
return "не получилось найти ответ."
|
||||
}
|
||||
// Confidence gate: below threshold, say "I don't know" rather than read
|
||||
// back the least-unrelated note — a confident wrong recall is worse than
|
||||
// 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 < h.queryMinScore {
|
||||
// Confidence gate: below it, say "I don't know" rather than read back
|
||||
// the least-unrelated note — a confident wrong recall is worse than a
|
||||
// gap (spec's "not a guesser-of-truth"). Same instinct as the loop's
|
||||
// since(key)==null → don't fire. Two parts: an absolute cosine floor,
|
||||
// and a margin over the runner-up, which is the part that works with
|
||||
// the e5 embedder's narrow score band. See memory.Confident.
|
||||
noteScores := make([]float64, len(notes))
|
||||
for i, n := range notes {
|
||||
noteScores[i] = n.Score
|
||||
}
|
||||
if !memory.ConfidentScores(noteScores, h.queryMinScore, h.queryMinMargin) {
|
||||
// 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, h.queryMinScore); ok {
|
||||
if text, ok := bestRecall(hits, h.queryMinScore, h.queryMinMargin); ok {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user