feat: LLM router with chat intent and Cyrillic wake-word support
- 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.
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user