17b47ce206
The router prompt tested "reports current state -> fact" before "wants information -> query", so a question naming a fact key was written as a fact. Query now comes first, plus an explicit question test. Reviewers: the prompt block in llmrouter.go, and the note about the training-side copy of the prompt that needs the same edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CGeSZxh1DCtRxmFVSYVGvJ
95 lines
2.9 KiB
Go
95 lines
2.9 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/llm"
|
|
)
|
|
|
|
type mockLLM struct {
|
|
out string
|
|
err error
|
|
}
|
|
|
|
func (m mockLLM) Complete(_ context.Context, _ llm.Req) (string, error) { return m.out, m.err }
|
|
|
|
// A question naming a fact key used to be stored as a fact because the fact rule
|
|
// was tested first. Keep the query rule above it.
|
|
func TestRoutePromptTestsQueryBeforeFact(t *testing.T) {
|
|
query := strings.Index(routeSystem, "→ query")
|
|
fact := strings.Index(routeSystem, "состояние/событие → fact")
|
|
if query < 0 || fact < 0 {
|
|
t.Fatalf("prompt lost a rule: query=%d fact=%d", query, fact)
|
|
}
|
|
if query > fact {
|
|
t.Fatal("query rule must come before the fact rule")
|
|
}
|
|
if !strings.Contains(routeSystem, "Задаёт вопрос") {
|
|
t.Fatal("prompt lost the explicit question test")
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterFactMapping(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: `{"intent":"fact","key":"water","value":"выпил"}`})
|
|
d, ok, err := lr.Route(context.Background(), "я выпил воду", time.Now())
|
|
if err != nil || !ok {
|
|
t.Fatalf("ok=%v err=%v", ok, err)
|
|
}
|
|
if d.Intent != IntentFact || d.Slots.Key != "water" || !d.Slots.HasKey {
|
|
t.Fatalf("bad decision %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterNoteMapping(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: `{"intent":"note","text":"кофе закончился"}`})
|
|
d, ok, err := lr.Route(context.Background(), "запомни что кофе закончился", time.Now())
|
|
if err != nil || !ok {
|
|
t.Fatalf("ok=%v err=%v", ok, err)
|
|
}
|
|
if d.Intent != IntentNote || d.Slots.Text != "кофе закончился" {
|
|
t.Fatalf("bad decision %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterBadJSONFallsBack(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: `garbage`})
|
|
_, ok, err := lr.Route(context.Background(), "x", time.Now())
|
|
if ok || err == nil {
|
|
t.Fatal("want ok=false, err!=nil on bad json")
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterReminderMapping(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: `{"intent":"reminder","text":"позвонить маме"}`})
|
|
d, ok, err := lr.Route(context.Background(), "напомни позвонить маме", time.Now())
|
|
if err != nil || !ok {
|
|
t.Fatalf("ok=%v err=%v", ok, err)
|
|
}
|
|
if d.Intent != IntentReminder || d.Slots.Text != "позвонить маме" {
|
|
t.Fatalf("bad decision %+v", d)
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterChatFallback(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: `{"intent":"unknown"}`})
|
|
d, ok, err := lr.Route(context.Background(), "как дела?", time.Now())
|
|
if err != nil || !ok {
|
|
t.Fatalf("ok=%v err=%v", ok, err)
|
|
}
|
|
if d.Intent != IntentChat {
|
|
t.Fatalf("unknown intent should default to chat, got %s", d.Intent)
|
|
}
|
|
}
|
|
|
|
func TestLLMRouterLLMError(t *testing.T) {
|
|
lr := NewLLMRouter(mockLLM{out: "", err: fmt.Errorf("llm down")})
|
|
_, ok, err := lr.Route(context.Background(), "x", time.Now())
|
|
if ok || err == nil {
|
|
t.Fatal("want ok=false, err!=nil on llm error")
|
|
}
|
|
}
|