bac8673f05
Every token of "что у меня сегодня?" is frame, so ownContent left nothing, needsRoute returned false and no route was computed at all. The parked reminder then read "сегодня" as its time and the question was answered nowhere. A question shape now gets routed even when it leaves no content of its own, and a role that fills nothing she asked about is decided by the route. A statement he makes mid-flow gets a role of its own, roleAside, so a note or a fact is stored and the question comes back instead of being dropped in silence. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
62 lines
2.5 KiB
Go
62 lines
2.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"time"
|
|
|
|
"github.com/kami/maven/internal/lexicon"
|
|
"github.com/kami/maven/internal/phraser"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// actionReminder handles router.IntentReminder: parse the time when stage-0
|
|
// skipped the extractor, then create the reminder.
|
|
func (h *reactiveHandler) actionReminder(ctx context.Context, dec router.Decision) string {
|
|
if !dec.Slots.HasTime {
|
|
// Stage-0 (reminder-wakeword grammar) skips the extractor, so the
|
|
// time wasn't parsed. Run the parser as a fallback.
|
|
if dec.Stage == 0 && h.timeParser != nil {
|
|
t, ok, err := h.timeParser.Parse(ctx, dec.Utterance, h.now())
|
|
// Same gate as the extractor (V-577, V-579): a request that named
|
|
// no hour gets asked about, never completed from the clock.
|
|
if err == nil && ok && router.NamesAnHour(dec.Utterance) {
|
|
dec.Slots.Time = t
|
|
dec.Slots.HasTime = true
|
|
}
|
|
}
|
|
if !dec.Slots.HasTime {
|
|
return phraser.Ack(phraser.FailReminderTime, nil)
|
|
}
|
|
}
|
|
// The body is what she says at the hour, so the marker and the time come
|
|
// out of it: the fire time is already a column, and "напомни" is an
|
|
// instruction that has been carried out (Vikunja #469).
|
|
payload := `{"text":` + jsonString(reminderBody(dec.Utterance, dec.Slots.Text)) + `}`
|
|
if _, err := h.api.CreateReminder(ctx, dec.Slots.Time, payload, ""); err != nil {
|
|
log.Printf("voice: create reminder: %v", err)
|
|
return phraser.Ack(phraser.FailReminder, nil)
|
|
}
|
|
// Phrased from the row, never from the utterance (Vikunja #507). The
|
|
// replier only ever saw Slots.Text, so it named whatever hour the sentence
|
|
// contained — including one the parser had rejected or read differently.
|
|
// A confirmation naming an hour no row holds is worse than a clarify,
|
|
// because he stops thinking about it.
|
|
return reminderConfirm(dec.Slots.Time, h.now())
|
|
}
|
|
|
|
// reminderConfirm — the confirmation for a reminder that exists, naming the
|
|
// stored fire time. Deterministic on purpose: the one sentence that must match
|
|
// a database row is not one to hand to a 1.7B.
|
|
func reminderConfirm(fire, now time.Time) string {
|
|
when := dayPrefix(now, fire)
|
|
if when == "это" {
|
|
// Further out than the day words reach — say the date instead of a
|
|
// word that would be wrong.
|
|
date := fmt.Sprintf("%d %s", fire.Day(), lexicon.MonthGenitive(int(fire.Month())))
|
|
return "хорошо, напомню " + date + " в " + fire.Format("15:04") + "."
|
|
}
|
|
return "хорошо, напомню " + when + " в " + fire.Format("15:04") + "."
|
|
}
|