28a940ebbe
- Add LLMRouter: grammar-constrained LFM call for intent classification after stage-0, before classifier cascade. Errors fall through gracefully. - Add IntentChat: conversational intent with no store side-effect, routed through LLM -> phraser chat endpoint. - Extract slots for Chat: no structured slots, full utterance is payload. - Extend stage-0 grammars to fire through Cyrillic wake-word spellings (Мэйвен/Мейвен/Майвен/etc.) produced by Russian STT model. - StripWakeToken helper strips leading wake in any script so time/date grammars still match when wake is present. - Add classifier examples for chat utterances (EN + RU). - Wire LLMRouter into Router.Config; optional, nil-safe.
75 lines
2.3 KiB
Go
75 lines
2.3 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"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 }
|
|
|
|
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")
|
|
}
|
|
}
|