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:
@@ -2,6 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -21,6 +22,11 @@ type Config struct {
|
||||
// spec leaves this open (defines how often maven asks vs guesses on free-
|
||||
// form input; the whole reactive mvp feel rides on it). The daemon sets it.
|
||||
Threshold float64
|
||||
// LLM — optional agentic router. When set, Route consults it after stage-0
|
||||
// and before the classifier cascade, classifying the utterance via a
|
||||
// grammar-constrained LFM call. On any error/parse failure, falls through
|
||||
// to the classifier (never fails the turn on the model).
|
||||
LLM *LLMRouter
|
||||
}
|
||||
|
||||
// Router — the deterministic cascade. Route never guesses: stage 0 wins
|
||||
@@ -31,6 +37,7 @@ type Router struct {
|
||||
classifier *Classifier
|
||||
extractor Extractor
|
||||
threshold float64
|
||||
llm *LLMRouter
|
||||
}
|
||||
|
||||
func New(cfg Config) *Router {
|
||||
@@ -39,6 +46,7 @@ func New(cfg Config) *Router {
|
||||
classifier: cfg.Classifier,
|
||||
extractor: cfg.Extractor,
|
||||
threshold: cfg.Threshold,
|
||||
llm: cfg.LLM,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +61,15 @@ func New(cfg Config) *Router {
|
||||
// worse than a gap).
|
||||
func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (Decision, error) {
|
||||
// stage 0 — exact match / grammar. First match wins; grammars are ordered.
|
||||
// Grammars like time/date/reminder don't expect a wake-word prefix, but
|
||||
// the STT often includes one (transcribed phonetically, any script) — try
|
||||
// the wake-stripped utterance too so those grammars still fire.
|
||||
stripped, hadWake := StripWakeToken(utterance)
|
||||
for _, g := range r.grammars {
|
||||
m := g.Pattern.FindStringSubmatch(utterance)
|
||||
if m == nil && hadWake {
|
||||
m = g.Pattern.FindStringSubmatch(stripped)
|
||||
}
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
@@ -66,6 +81,18 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// stage 1a — LLM router (when wired). It reasons over the utterance instead
|
||||
// of nearest-centroid guessing. On any error/parse-fail, fall through to the
|
||||
// classifier cascade (never fail the turn on the model).
|
||||
if r.llm != nil {
|
||||
if d, ok, err := r.llm.Route(ctx, utterance, now); err == nil && ok {
|
||||
d.Utterance = utterance
|
||||
return d, nil
|
||||
} else if err != nil {
|
||||
log.Printf("router: llm route fell back to classifier: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// stage 1 — intent classifier.
|
||||
results, err := r.classifier.Classify(ctx, utterance)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user