router: wire SelectCapability into Router.Route pipeline (slice 6b)
Call SelectCapability after each cascade path (grammar, heads, LLM, classifier) and propagate the result via applyCapabilityToSlots. Remove LLM text capability backfill from fillSlots — SelectCapability now owns that path. fillMatchedSlots retains raw extractor capability extraction for backward compatibility with stage-0 grammars. gateLLMDecision now reads CapabilitySelection.Resolved instead of Slots.HasFn for the act-intent confidence thinning check.
This commit is contained in:
+31
-14
@@ -105,6 +105,11 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
|||||||
// The grammar decided the intent; the extractor fills the slots it did
|
// The grammar decided the intent; the extractor fills the slots it did
|
||||||
// not match (V-572). See fillMatchedSlots for why every grammar gets it.
|
// not match (V-572). See fillMatchedSlots for why every grammar gets it.
|
||||||
r.fillMatchedSlots(ctx, &d, now)
|
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)
|
r.noteGrammarOutcomes(ctx, i+1, declinedBuild, g.Name, d.Intent)
|
||||||
return d, nil
|
return d, nil
|
||||||
}
|
}
|
||||||
@@ -142,6 +147,7 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
|||||||
Producer: RouteProducerHeads,
|
Producer: RouteProducerHeads,
|
||||||
}
|
}
|
||||||
r.fillSlots(ctx, &d, now)
|
r.fillSlots(ctx, &d, now)
|
||||||
|
applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts))
|
||||||
// The clarify head relearned the English assumption that one word
|
// The clarify head relearned the English assumption that one word
|
||||||
// cannot be a sentence. Russian verbs carry subject and tense, and a
|
// cannot be a sentence. Russian verbs carry subject and tense, and a
|
||||||
// deterministic fact parser which also found a key gives both halves
|
// deterministic fact parser which also found a key gives both halves
|
||||||
@@ -184,6 +190,13 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
|||||||
d.Utterance = utterance
|
d.Utterance = utterance
|
||||||
d.Producer = RouteProducerLLM
|
d.Producer = RouteProducerLLM
|
||||||
r.fillSlots(ctx, &d, now)
|
r.fillSlots(ctx, &d, now)
|
||||||
|
// Capability selection: after fillSlots populates Text, SelectCapability
|
||||||
|
// tries the LLM-cleaned text against the allowlist when the raw
|
||||||
|
// utterance did not match. This replaces the former LLM text backfill
|
||||||
|
// that was embedded in fillSlots. Must run before gateLLMDecision so
|
||||||
|
// the gate sees the correct resolution state for acts.
|
||||||
|
sel := SelectCapability(d, r.extractor.Acts)
|
||||||
|
applyCapabilityToSlots(&d, sel)
|
||||||
before := d.Confidence
|
before := d.Confidence
|
||||||
r.gateLLMDecision(&d)
|
r.gateLLMDecision(&d)
|
||||||
// The classifier is the floor and it never ran, which is the whole
|
// The classifier is the floor and it never ran, which is the whole
|
||||||
@@ -244,6 +257,12 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
|||||||
Slots: r.extractor.Extract(ctx, best.Intent, utterance, now),
|
Slots: r.extractor.Extract(ctx, best.Intent, utterance, now),
|
||||||
Producer: RouteProducerClassifier,
|
Producer: RouteProducerClassifier,
|
||||||
}
|
}
|
||||||
|
// Capability selection: for the classifier path, the raw extractor
|
||||||
|
// already filled Slots.Fn/Args via Extract(IntentAct). SelectCapability
|
||||||
|
// records this as a deterministic bypass. If the raw match missed,
|
||||||
|
// there is no LLM text to fall back to (Text == Utterance), so the
|
||||||
|
// selection remains unresolved.
|
||||||
|
applyCapabilityToSlots(&d, SelectCapability(d, r.extractor.Acts))
|
||||||
|
|
||||||
// stage 3 — confidence gate. Below threshold ⇒ clarify, don't guess.
|
// stage 3 — confidence gate. Below threshold ⇒ clarify, don't guess.
|
||||||
if d.Confidence < r.threshold {
|
if d.Confidence < r.threshold {
|
||||||
@@ -302,27 +321,22 @@ func (r *Router) fillMatchedSlots(ctx context.Context, d *Decision, now time.Tim
|
|||||||
if !d.Slots.HasKey && ex.HasKey {
|
if !d.Slots.HasKey && ex.HasKey {
|
||||||
d.Slots.Key, d.Slots.Value, d.Slots.HasKey = ex.Key, ex.Value, ex.HasKey
|
d.Slots.Key, d.Slots.Value, d.Slots.HasKey = ex.Key, ex.Value, ex.HasKey
|
||||||
}
|
}
|
||||||
|
// Capability selection (Fn/Args/HasFn) is now handled by
|
||||||
|
// SelectCapability, not here. The extractor still fills them via
|
||||||
|
// Extract(IntentAct), but the authoritative record lives in
|
||||||
|
// Decision.CapabilitySelection. See capability.go.
|
||||||
if !d.Slots.HasFn && ex.HasFn {
|
if !d.Slots.HasFn && ex.HasFn {
|
||||||
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn, d.Slots.ResolvedBy = ex.Fn, ex.Args, ex.HasFn, ex.ResolvedBy
|
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn, d.Slots.ResolvedBy = ex.Fn, ex.Args, ex.HasFn, ex.ResolvedBy
|
||||||
}
|
}
|
||||||
return ex
|
return ex
|
||||||
}
|
}
|
||||||
|
|
||||||
// fillSlots — fillMatchedSlots for an LLM decision, plus the two backfills that
|
// fillSlots — fillMatchedSlots for an LLM decision, plus the text backfill.
|
||||||
// only make sense there. The LLM wins where it answered: it saw the sentence,
|
// Capability selection (Fn/Args) is now handled by SelectCapability, called
|
||||||
// the parsers are keyword tables.
|
// after fillSlots in the routing pipeline. This function fills only time, key,
|
||||||
|
// and text slots.
|
||||||
func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
||||||
ex := r.fillMatchedSlots(ctx, d, now)
|
ex := r.fillMatchedSlots(ctx, d, now)
|
||||||
// For an act the model returns the verb in Text ("restart nginx"), which is
|
|
||||||
// often cleaner than the raw utterance ("maven, could you restart nginx").
|
|
||||||
// Try it too when the utterance did not match the allowlist.
|
|
||||||
if d.Intent == IntentAct && !d.Slots.HasFn && r.extractor.Acts != nil &&
|
|
||||||
d.Slots.Text != "" && d.Slots.Text != d.Utterance {
|
|
||||||
if fn, args, ok := r.extractor.Acts.Match(d.Slots.Text); ok {
|
|
||||||
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn = fn, args, true
|
|
||||||
d.Slots.ResolvedBy = ActionResolutionExtractorLLMText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The extractor's Text is the raw utterance, which is the payload for a
|
// The extractor's Text is the raw utterance, which is the payload for a
|
||||||
// note, a query or a chat turn but not for a reminder — there Text is the
|
// note, a query or a chat turn but not for a reminder — there Text is the
|
||||||
// subject, what she says at the hour. Backfilling it made Text impossible
|
// subject, what she says at the hour. Backfilling it made Text impossible
|
||||||
@@ -354,11 +368,14 @@ func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
|||||||
// Anything below threshold gets the exact same Clarify=true treatment the
|
// Anything below threshold gets the exact same Clarify=true treatment the
|
||||||
// classifier path already produces — same field, same daemon-side consumer
|
// classifier path already produces — same field, same daemon-side consumer
|
||||||
// (cmd/mavend/clarify.go), nothing new to wire.
|
// (cmd/mavend/clarify.go), nothing new to wire.
|
||||||
|
//
|
||||||
|
// The act check now reads CapabilitySelection.Resolved (set by
|
||||||
|
// SelectCapability before this function runs) rather than Slots.HasFn.
|
||||||
func (r *Router) gateLLMDecision(d *Decision) {
|
func (r *Router) gateLLMDecision(d *Decision) {
|
||||||
if d.Intent == IntentFact && !d.Slots.HasKey && d.Confidence > llmThinConfidence {
|
if d.Intent == IntentFact && !d.Slots.HasKey && d.Confidence > llmThinConfidence {
|
||||||
d.Confidence = llmThinConfidence
|
d.Confidence = llmThinConfidence
|
||||||
}
|
}
|
||||||
if d.Intent == IntentAct && !d.Slots.HasFn && d.Confidence > llmThinConfidence {
|
if d.Intent == IntentAct && !d.CapabilitySelection.Resolved && d.Confidence > llmThinConfidence {
|
||||||
d.Confidence = llmThinConfidence
|
d.Confidence = llmThinConfidence
|
||||||
}
|
}
|
||||||
// A reminder with no subject: she knows when but not what to say then.
|
// A reminder with no subject: she knows when but not what to say then.
|
||||||
|
|||||||
Reference in New Issue
Block a user