Merge: a reminder said whole is not asked about (#214)
V-572. "напомни в 11:00 позвонить маме" answered "Когда?". ReminderGrammar builds its slots by hand and the extractor never ran over a stage 0 decision, so HasTime was false however clearly the hour was spoken, and missingFor read the silence as absence. fillMatchedSlots in internal/router/router.go now runs the stage 2 extractor over every stage 0 decision and fills only what the grammar left empty. A matched value always wins. The LLM path had the same hole and the same fix, so both share one function rather than ten grammars re-implementing extraction. Slots.Text is deliberately not filled. A grammar that left Text empty meant it: agendaQueryBuild hands the query chain the sentence itself. Filling it would also make SlotText unaskable, which is the bug V-383 fixed on the LLM side. Enabled for all ten grammars and inert for nine. Extract fills Time for a reminder, Fn for an act and Key for a fact, and nothing for query, system, note or chat. Benchmarked at 20000x with the real date parser: every stage 0 shape stays inside the noise, and the reminder rule gains, because actionReminder was already running that same parse one layer down. TestONNXBaseline 64/91 before and after, no case regressed. The fixture's own "slots deferred to daemon" line went 6 to 0. On the box: "хорошо, напомню сегодня в 11:00." Conflict in internal/router/router.go resolved by hand: V-564's grammar-outcome note and V-572's slot fill both belong, fill first. Full -race suite green. --no-verify: the pre-commit hook refuses master, and the owner asked for straight-to-master merges for this unattended run.
This commit is contained in:
@@ -90,6 +90,9 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
||||
continue // grammar matched shape but not content → fall through
|
||||
}
|
||||
d.Utterance = utterance
|
||||
// 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)
|
||||
r.noteGrammarOutcomes(ctx, i+1, declinedBuild, g.Name, d.Intent)
|
||||
return d, nil
|
||||
}
|
||||
@@ -176,14 +179,37 @@ func (r *Router) Route(ctx context.Context, utterance string, now time.Time) (De
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fillSlots — run stage-2 extraction on an LLM decision and fill only the slots
|
||||
// the model left empty. The LLM wins where it answered: it saw the sentence, the
|
||||
// parsers are keyword tables. Extraction covers what the model cannot produce at
|
||||
// all — a parsed reminder time and an allowlist fn.
|
||||
// fillMatchedSlots — run stage-2 extraction over a decision some earlier
|
||||
// claimant produced, and fill only the slots that claimant left empty. A
|
||||
// matched value always wins: the claimant read the sentence, the extractor
|
||||
// guesses from keyword tables.
|
||||
//
|
||||
// Shared by the stage-0 grammars and the LLM router, which had the same hole
|
||||
// for the same reason. A grammar asserts an intent at confidence 1.0 and says
|
||||
// nothing about the slots, so "напомни в 11:00 позвонить маме" arrived with
|
||||
// HasTime false however plainly the hour was spoken, and the daemon read the
|
||||
// silence as absence and asked "Когда?" (V-572). The alternative was ten
|
||||
// grammars each re-implementing extraction.
|
||||
//
|
||||
// It is applied to every stage-0 decision rather than to a chosen few, because
|
||||
// for every intent but reminder it is inert: Extract fills Time for a reminder,
|
||||
// Fn for an act and Key for a fact, and nothing at all for query, system, note
|
||||
// or chat, which is what the query, clock, agenda, feed, list, task and
|
||||
// narrative rules emit. The act rules — wakeword-act and the Praxis ones —
|
||||
// already carry an Fn or they do not match, so there is nothing left for the
|
||||
// matcher to fill. The reminder rule is the one that gains, and its time parse
|
||||
// is a cost the daemon was already paying one layer down in actionReminder.
|
||||
//
|
||||
// Slots.Text is deliberately NOT filled here. Extract sets it to the raw
|
||||
// utterance, and a grammar that left it empty meant it: agendaQueryBuild hands
|
||||
// the query chain the utterance itself, and narrativeQueryBuild's Text is the
|
||||
// topic, not the sentence.
|
||||
//
|
||||
// If a reminder still has no time, leave it missing. The daemon then says it
|
||||
// could not read the time; inventing one would set a wrong alarm.
|
||||
func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
||||
// Returns what the extractor read, so a caller that wants more of it does not
|
||||
// pay for a second extraction — the reminder parser is the expensive one.
|
||||
func (r *Router) fillMatchedSlots(ctx context.Context, d *Decision, now time.Time) Slots {
|
||||
ex := r.extractor.Extract(ctx, d.Intent, d.Utterance, now)
|
||||
if !d.Slots.HasTime && ex.HasTime {
|
||||
d.Slots.Time, d.Slots.HasTime = ex.Time, ex.HasTime
|
||||
@@ -194,6 +220,14 @@ func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
||||
if !d.Slots.HasFn && ex.HasFn {
|
||||
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn = ex.Fn, ex.Args, ex.HasFn
|
||||
}
|
||||
return ex
|
||||
}
|
||||
|
||||
// fillSlots — fillMatchedSlots for an LLM decision, plus the two backfills that
|
||||
// only make sense there. The LLM wins where it answered: it saw the sentence,
|
||||
// the parsers are keyword tables.
|
||||
func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user