76 lines
3.0 KiB
Go
76 lines
3.0 KiB
Go
package router
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
)
|
|
|
|
// FastPathResult is the outcome of the stage-0 deterministic fast path.
|
|
// Matched is true when a grammar accepted the input. Decision carries the
|
|
// full routing result for a match. When Matched is false the caller falls
|
|
// through to the general cascade (heads → LLM → classifier → extraction → gate).
|
|
type FastPathResult struct {
|
|
Matched bool
|
|
Decision Decision
|
|
}
|
|
|
|
// TryFastPath runs the stage-0 exact-match grammars. It owns the ordered
|
|
// grammar list, first-match-wins semantics, wake-token alternate handling,
|
|
// slot filling, capability selection, and per-grammar trace recording.
|
|
//
|
|
// When no grammar matches, Matched is false and the caller falls through
|
|
// to the general cascade.
|
|
//
|
|
// TryFastPath receives NormalizedInput so later recognizers can opt into
|
|
// MatchText, but no existing grammar switches in this slice — they all
|
|
// receive input.Text (the raw utterance) exactly as they do today.
|
|
func (r *Router) TryFastPath(
|
|
ctx context.Context,
|
|
input NormalizedInput,
|
|
now time.Time,
|
|
) (FastPathResult, 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(input.Text)
|
|
// declinedBuild — the grammars that matched the shape and refused the
|
|
// content, kept for the decision record (V-564) so a reader can tell that
|
|
// rule from one whose pattern never fired.
|
|
var declinedBuild map[int]bool
|
|
for i, g := range r.grammars {
|
|
d, matched, ok := g.Evaluate(input.Text)
|
|
if !matched && hadWake {
|
|
d, matched, ok = g.Evaluate(stripped)
|
|
}
|
|
if !matched {
|
|
continue
|
|
}
|
|
if !ok {
|
|
if declinedBuild == nil {
|
|
declinedBuild = map[int]bool{}
|
|
}
|
|
declinedBuild[i] = true
|
|
continue // grammar matched shape but not content → fall through
|
|
}
|
|
d.Utterance = input.Text
|
|
d.Producer = RouteProducerGrammar
|
|
// A literal pattern named that destination, which is the one provenance
|
|
// allowed to take the personal boundary off a turn (V-666). Set here and
|
|
// nowhere else, so no other arm of the cascade can claim it.
|
|
d.SourceAnchored = d.Source != SourceUnknown
|
|
// The grammar decided the intent; the extractor fills the slots it did
|
|
// not match (V-572). See fillMatchedSlots for why every grammar gets it.
|
|
r.fillMatchedSlots(ctx, &d, now)
|
|
// Capability selection: deterministic grammars that already resolved Fn
|
|
// (wakeword-act, praxis, task-status) bypass the general selector.
|
|
// SelectCapability records the existing result with
|
|
// InputKind=SelectionDeterministic.
|
|
applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts))
|
|
r.noteGrammarOutcomes(ctx, i+1, declinedBuild, g.Name, d.Intent)
|
|
return FastPathResult{Matched: true, Decision: d}, nil
|
|
}
|
|
r.noteGrammarOutcomes(ctx, len(r.grammars), declinedBuild, "", "")
|
|
return FastPathResult{}, nil
|
|
}
|