router: a reminder with no subject asks instead of guessing (V-383)
Slots.Text was the raw utterance for every intent, so a reminder could not have an empty subject. StillMissing never reported SlotText, the question "О чём напомнить?" was unaskable, and the branch in PendingQuestion.Answer that fills a text slot could only overwrite the whole request. The LLM path now keeps the model's own text, empty included, and the gate turns a subjectless reminder into a question. The classifier path is unchanged: it has no subject parser, so the utterance is the only signal it has.
This commit is contained in:
@@ -210,7 +210,11 @@ func (lr *LLMRouter) Route(ctx context.Context, utterance string, now time.Time)
|
||||
d.Slots.HasKey = a.Key != ""
|
||||
case IntentReminder:
|
||||
d.Intent = IntentReminder
|
||||
d.Slots.Text = firstNonEmpty(a.Text, utterance)
|
||||
// No utterance fallback here, unlike every other intent below. The
|
||||
// model returning no text for a reminder means it found no subject,
|
||||
// and "напомни в 11" is not a subject. Leaving Text empty is what
|
||||
// lets the gate turn that into a question (Vikunja #383).
|
||||
d.Slots.Text = a.Text
|
||||
case IntentNote:
|
||||
d.Intent = IntentNote
|
||||
d.Slots.Text = firstNonEmpty(a.Text, utterance)
|
||||
|
||||
@@ -356,3 +356,35 @@ func TestRouterLLMFactWithResolvedKeyStaysConfident(t *testing.T) {
|
||||
t.Fatalf("a fact the parser could key must not clarify: %+v", d)
|
||||
}
|
||||
}
|
||||
|
||||
// A reminder with a time and no subject must come back empty and gated, not
|
||||
// backfilled with the raw words. "напомни в 11" carries an hour and nothing to
|
||||
// say at that hour; parking the utterance in Text made the request look
|
||||
// complete, so the daemon set a reminder that fires saying "напомни в 11"
|
||||
// (Vikunja #383).
|
||||
func TestLLMReminderWithoutSubjectAsksInsteadOfGuessing(t *testing.T) {
|
||||
r := newLLMTestRouter(t, `{"intent":"reminder"}`)
|
||||
d, err := r.Route(context.Background(), "напомни в 11", refNow())
|
||||
if err != nil {
|
||||
t.Fatalf("route: %v", err)
|
||||
}
|
||||
if d.Slots.Text != "" {
|
||||
t.Fatalf("subject backfilled from the utterance: %q", d.Slots.Text)
|
||||
}
|
||||
if !d.Clarify {
|
||||
t.Fatalf("a subjectless reminder was accepted, confidence %v", d.Confidence)
|
||||
}
|
||||
}
|
||||
|
||||
// The gate is about the subject, not about reminders in general: one that has
|
||||
// both halves still runs without a question.
|
||||
func TestLLMReminderWithSubjectIsNotGated(t *testing.T) {
|
||||
r := newLLMTestRouter(t, `{"intent":"reminder","text":"позвонить маме"}`)
|
||||
d, err := r.Route(context.Background(), "напомни в 11 позвонить маме", refNow())
|
||||
if err != nil {
|
||||
t.Fatalf("route: %v", err)
|
||||
}
|
||||
if d.Clarify {
|
||||
t.Fatalf("a complete reminder was sent back as a question: %+v", d.Slots)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,15 @@ func (r *Router) fillSlots(ctx context.Context, d *Decision, now time.Time) {
|
||||
d.Slots.Fn, d.Slots.Args, d.Slots.HasFn = fn, args, true
|
||||
}
|
||||
}
|
||||
if d.Slots.Text == "" {
|
||||
// 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
|
||||
// subject, what she says at the hour. Backfilling it made Text impossible
|
||||
// to be empty, so StillMissing never reported SlotText and "О чём
|
||||
// напомнить?" was unaskable; the answer to a question she did manage to
|
||||
// ask then overwrote the whole request instead of filling one gap
|
||||
// (Vikunja #383). A reminder with no subject stays empty and is gated
|
||||
// below into a question.
|
||||
if d.Slots.Text == "" && d.Intent != IntentReminder {
|
||||
d.Slots.Text = ex.Text
|
||||
}
|
||||
// Stage stays 1: it says who decided the route, and that was the LLM.
|
||||
@@ -177,6 +185,12 @@ func (r *Router) gateLLMDecision(d *Decision) {
|
||||
if d.Intent == IntentAct && !d.Slots.HasFn && d.Confidence > llmThinConfidence {
|
||||
d.Confidence = llmThinConfidence
|
||||
}
|
||||
// A reminder with no subject: she knows when but not what to say then.
|
||||
// Setting it anyway fires an empty reminder at the hour, which reads as a
|
||||
// bug to him and cannot be repaired after the fact. Ask (Vikunja #383).
|
||||
if d.Intent == IntentReminder && d.Slots.Text == "" && d.Confidence > llmThinConfidence {
|
||||
d.Confidence = llmThinConfidence
|
||||
}
|
||||
if d.Confidence < r.threshold {
|
||||
d.Clarify = true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user