package router import ( "regexp" "strings" ) // Grammar — one stage-0 exact-match pattern. Wake-word + known command grammar // hits the allowlist directly, skips the classifier (lowest latency — the vosk // command path). Boring high-frequency acts for free. // // A Grammar returns a fully-formed Decision (intent + slots) at confidence 1.0 // when its pattern matches AND its Build returns ok=true; the router stops the // cascade. Grammar rules are code, not config — same boundary as rules-as-code // in the loop. The tool registry populates the verb set at daemon wiring time. type Grammar struct { Name string Pattern *regexp.Regexp // matched against the raw utterance Build func(match []string) (Decision, bool) } // wakeWordAct — "maven, restart nginx" / "maven restart nginx" → the remainder // 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[,: ]+(.+)$`) // 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 // list). Returns nil grammars if no matcher is wired (the daemon always wires // one — the guard is for tests that only exercise the classifier). func DefaultGrammars(actMatcher ActMatcher) []Grammar { if actMatcher == nil { return nil } return []Grammar{ { Name: "wakeword-act", Pattern: wakeWordAct, Build: func(m []string) (Decision, bool) { rest := strings.TrimSpace(m[1]) fn, args, ok := actMatcher.Match(rest) if !ok { return Decision{}, false // fall through to classifier } return Decision{ Stage: 0, Intent: IntentAct, Confidence: 1.0, Slots: Slots{Fn: fn, Args: args, HasFn: true, Text: rest}, }, true }, }, } }