de4c47459a
"расскажи про Байкал" was refused as his by 0.0052. Every world seed opened with an interrogative, so a world question phrased as an order landed nearer "я тебе рассказывал об этом?" — the same verb about his own words. Four narrative seeds on the world side. TestONNXPersonalBoundary 25/25 -> 29/29 on held-out utterances, and the control "я рассказывал тебе про байкал?" is still his. TestONNXTopics unchanged at 34/34.
116 lines
5.1 KiB
Go
116 lines
5.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// A handler with no embedder never loads the seeds, so the boundary falls back
|
|
// to the possession markers. That is the offline floor and it must keep working
|
|
// — an embedder that fails to load must not open the boundary.
|
|
func TestBoundaryFallsBackToMarkersWithNoEmbedder(t *testing.T) {
|
|
h := personalHandler()
|
|
if !h.isPersonalTurn(context.Background(), &queryTurn{
|
|
dec: router.Decision{Utterance: "во сколько у меня встреча"},
|
|
}) {
|
|
t.Error("no embedder: a possession question must still be personal")
|
|
}
|
|
if h.isPersonalTurn(context.Background(), &queryTurn{
|
|
dec: router.Decision{Utterance: "почему небо синее"},
|
|
}) {
|
|
t.Error("no embedder: a world question must still pass")
|
|
}
|
|
}
|
|
|
|
// TestONNXPersonalBoundary — the number that matters, scored against the
|
|
// embedder homesrv actually runs. Opt-in via MAVEN_ONNX_LIB, exactly like
|
|
// TestONNXRecall in internal/memory/recalleval.
|
|
//
|
|
// Every case here is held out: none of these strings is a seed. The #495
|
|
// regression is the first row — "что я говорил про бэкапы?" reached SearXNG and
|
|
// was answered from a Habr article, and no possession word appears in it.
|
|
func TestONNXPersonalBoundary(t *testing.T) {
|
|
lib := os.Getenv("MAVEN_ONNX_LIB")
|
|
if lib == "" {
|
|
t.Skip("MAVEN_ONNX_LIB unset — see AGENTS.md § Embedder model for intent routing")
|
|
}
|
|
dir := filepath.Join("../..", "models/embedder/multilingual-e5-small")
|
|
emb, err := router.NewONNXEmbedder(filepath.Join(dir, "model_quantized.onnx"), filepath.Join(dir, "tokenizer.json"), lib)
|
|
if err != nil {
|
|
t.Skipf("onnx embedder unavailable: %v", err)
|
|
}
|
|
defer emb.Close()
|
|
|
|
cases := []struct {
|
|
utterance string
|
|
personal bool
|
|
}{
|
|
{"что я говорил про бэкапы?", true},
|
|
{"что я сказал вчера про отпуск", true},
|
|
{"я писал что-нибудь про сервер", true},
|
|
{"я упоминал про конференцию?", true},
|
|
{"что я отмечал по поводу переезда", true},
|
|
{"я рассказывал тебе про новую работу?", true},
|
|
{"во сколько у меня встреча", true},
|
|
{"когда мой следующий отпуск", true},
|
|
{"what did i say about backups", true},
|
|
{"did i tell you about the doctor", true},
|
|
{"как я говорил, почему небо синее", false},
|
|
{"как уже я говорил, какая столица франции", false},
|
|
{"почему трава зелёная", false},
|
|
{"столица франции", false},
|
|
{"как мне сварить борщ", false},
|
|
{"что мне посмотреть вечером", false},
|
|
{"я хочу узнать про рим", false},
|
|
{"кто такой гагарин", false},
|
|
{"how do i boil an egg", false},
|
|
// Asking when a public thing happens (Vikunja #553). "во сколько закат
|
|
// сегодня" was answered "не знаю — не нашла у тебя такой записи",
|
|
// because the frame lived only on the personal side. The pair above it
|
|
// is the control: "во сколько у меня встреча" is the same frame about
|
|
// something that IS his, and it has to stay personal.
|
|
{"во сколько закат сегодня", false},
|
|
{"когда сегодня заканчивается концерт", false},
|
|
{"во сколько завтра открывается аптека", false},
|
|
// The "какой сегодня X" frame. These clear the weather topic after the
|
|
// V-553 seeds and were then refused here, which is the same defect one
|
|
// source further down the chain.
|
|
{"какой сегодня праздник", false},
|
|
{"что интересного произошло сегодня в мире", false},
|
|
{"кто выиграл вчера матч", false},
|
|
// The narrative shape, held out from the seeds above (Vikunja #554).
|
|
// The control is the row after them: the same verb about his own words
|
|
// is still his.
|
|
{"расскажи про эверест", false},
|
|
{"расскажи про войну 1812 года", false},
|
|
{"объясни что такое инфляция", false},
|
|
{"я рассказывал тебе про байкал?", true},
|
|
}
|
|
|
|
h := &reactiveHandler{recall: recallWiring{embedder: emb}}
|
|
ctx := context.Background()
|
|
wrong := 0
|
|
for _, c := range cases {
|
|
vec, err := router.EmbedQuery(ctx, emb, c.utterance)
|
|
if err != nil {
|
|
t.Fatalf("embed %q: %v", c.utterance, err)
|
|
}
|
|
turn := &queryTurn{dec: router.Decision{Utterance: c.utterance}, vec: vec}
|
|
got := h.isPersonalTurn(ctx, turn)
|
|
p, w, ok := h.recall.boundary.score(vec)
|
|
if !ok {
|
|
t.Fatal("seeds did not load with a working embedder")
|
|
}
|
|
if got != c.personal {
|
|
wrong++
|
|
t.Errorf("%q: personal=%v want %v (personal %.4f world %.4f)", c.utterance, got, c.personal, p, w)
|
|
}
|
|
t.Logf("personal=%-5v personal %.4f world %.4f delta %+.4f %s", got, p, w, p-w, c.utterance)
|
|
}
|
|
t.Logf("personal boundary: %d/%d held-out utterances correct", len(cases)-wrong, len(cases))
|
|
}
|