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:
kami
2026-07-10 15:48:48 +04:00
parent 6bab68e96d
commit 28a940ebbe
8 changed files with 280 additions and 4 deletions
+26 -3
View File
@@ -23,7 +23,30 @@ type Grammar struct {
// is matched against the act allowlist. A non-match returns ok=false so the
// cascade falls through to the classifier (a wakeword prefix alone doesn't
// guarantee a known command — "maven, i'm tired" is a fact, not an act).
var wakeWordAct = regexp.MustCompile(`(?i)^\s*maven[,: ]+(.+)$`)
var wakeWordAct = regexp.MustCompile(`(?i)^\s*(?:maven|мэйвен|мейвен|майвен|мавена?|мэвен)[,:.!\s]+(.+)$`)
// wakeToken matches a leading wake-word token in any script the STT commonly
// produces for "Maven" — Latin "maven" or a Cyrillic phonetic rendering. The
// STT is a Russian model, so it transcribes the spoken wake word phonetically
// almost every time; matching only the Latin spelling meant stage-0 grammars
// (time/date/reminder) silently missed nearly every wake-worded utterance and
// fell through to the classifier, which misroutes time queries into the
// reminder intent (dense time-vocab centroid, see SystemTimeDateGrammars).
var wakeToken = regexp.MustCompile(`(?i)^\s*(?:maven|мэйвен|мейвен|майвен|мавена?|мэвен)[,:.!\s]*`)
// StripWakeToken removes a leading wake-word token (any script/spelling seen
// in wakeToken) and reports whether one was found.
func StripWakeToken(u string) (string, bool) {
loc := wakeToken.FindStringIndex(u)
if loc == nil {
return u, false
}
rest := strings.TrimSpace(u[loc[1]:])
if rest == "" {
return u, false
}
return rest, true
}
// DefaultGrammars — the wake-word act fast path. The ActMatcher is the same
// allowlist stage-2 act extraction uses (single source of truth for the fn
@@ -106,12 +129,12 @@ func SystemTimeDateGrammars() []Grammar {
},
{
Name: "clock-query",
Pattern: regexp.MustCompile(`(?i)^\s*который\s+(сейчас\s+)?час(\s+у\s+нас|\s+в\s+\w+)?\s*\??\s*$`),
Pattern: regexp.MustCompile(`(?i)^\s*который\s+(сейчас\s+)?час(\s+у\s+нас|\s+в\s+\w+)?\s*[?!.]?\s*$`),
Build: timeDateBuild,
},
{
Name: "date-query",
Pattern: regexp.MustCompile(`(?i)^\s*(?:какой\s+сегодня\s+(?:день|день\s+недели|число)|какое\s+сегодня\s+число)\s*\??\s*$`),
Pattern: regexp.MustCompile(`(?i)^\s*(?:какой\s+сегодня\s+(?:день|день\s+недели|число)|какое\s+сегодня\s+число)\s*[?!.]?\s*$`),
Build: timeDateBuild,
},
}