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:
@@ -19,6 +19,7 @@ func seedClassifier(t *testing.T, c *Classifier) {
|
||||
facts := []string{"drank water", "i drank water", "ate lunch", "slept six hours", "had a meal"}
|
||||
notes := []string{"gpu driver fixed the flicker", "prefer backups at three am", "note that the router reboots on tuesday"}
|
||||
queries := []string{"is the backup up", "when did i last eat", "is nginx running", "how much water today"}
|
||||
chats := []string{"what do you think about", "tell me something interesting", "how are you", "расскажи что-нибудь", "что ты думаешь", "как дела"}
|
||||
for _, x := range acts {
|
||||
if err := c.AddExample(ctx, IntentAct, x); err != nil {
|
||||
t.Fatalf("seed act %q: %v", x, err)
|
||||
@@ -44,6 +45,11 @@ func seedClassifier(t *testing.T, c *Classifier) {
|
||||
t.Fatalf("seed query %q: %v", x, err)
|
||||
}
|
||||
}
|
||||
for _, x := range chats {
|
||||
if err := c.AddExample(ctx, IntentChat, x); err != nil {
|
||||
t.Fatalf("seed chat %q: %v", x, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTestRouter(t *testing.T, threshold float64) *Router {
|
||||
@@ -97,6 +103,24 @@ func TestStage0WakeWordFallsThroughOnUnknownAct(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStage0GrammarFiresThroughCyrillicWakeWord(t *testing.T) {
|
||||
// The STT is a Russian model — it transcribes the spoken wake word
|
||||
// phonetically ("Мэйвен"), never as the Latin "maven". Grammars that
|
||||
// don't expect a wake prefix (time/date) must still fire when one is
|
||||
// there, otherwise these fall through to the classifier and get
|
||||
// misrouted into IntentReminder (see stage0.go's wakeToken comment).
|
||||
r := newTestRouter(t, 0.0)
|
||||
r.grammars = append(r.grammars, SystemTimeDateGrammars()...)
|
||||
|
||||
d, err := r.Route(context.Background(), "Мэйвен который час", refNow())
|
||||
if err != nil {
|
||||
t.Fatalf("route: %v", err)
|
||||
}
|
||||
if d.Stage != 0 || d.Intent != IntentSystem {
|
||||
t.Fatalf("want stage0 system, got %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------- stage 1 ---------------------------------------
|
||||
|
||||
func TestStage1ClassifiesAct(t *testing.T) {
|
||||
@@ -300,6 +324,28 @@ func TestClassifierDeterministicOrdering(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStage1ClassifiesChat(t *testing.T) {
|
||||
r := newTestRouter(t, 0.0)
|
||||
cases := []struct {
|
||||
in string
|
||||
want Intent
|
||||
}{
|
||||
{"what do you think about ai", IntentChat},
|
||||
{"tell me something interesting", IntentChat},
|
||||
{"как дела", IntentChat},
|
||||
{"расскажи что-нибудь", IntentChat},
|
||||
}
|
||||
for _, c := range cases {
|
||||
d, err := r.Route(context.Background(), c.in, refNow())
|
||||
if err != nil {
|
||||
t.Fatalf("route %q: %v", c.in, err)
|
||||
}
|
||||
if d.Intent != c.want {
|
||||
t.Errorf("%q: want %s, got %s (conf %f)", c.in, c.want, d.Intent, d.Confidence)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClassifierIntentsSorted(t *testing.T) {
|
||||
emb := NewHashEmbedder(32)
|
||||
c := NewClassifier(emb)
|
||||
|
||||
Reference in New Issue
Block a user