package phraser // The capture acknowledgements — what she says after storing something he said, // and what she says when storing it failed. // // They were string literals in eight files under cmd/mavend plus the stub // replier in internal/voice. He hears them many times a day, which is exactly // why they need variants and exactly why rewording one must not be a rebuild. // Same shape as fallbacks_ru_v1.json, on the shared deck (deck.go). // // His data stays Go-side. The file holds "отметила: {key} = {value}"; the key // and the value are interpolated here, so nothing he said lives in the copy. import ( _ "embed" "log" "math/rand" "strings" "sync" "github.com/kami/maven/internal/say" ) //go:embed ack_ru_v1.json var ackJSON []byte // AckSchemaVersion — this family's own version. A file that changes on a // different day than the fallbacks cannot share their number (Vikunja #397). const AckSchemaVersion = 1 // The entry keys. Namespaced by family, because the floor map behind a nil deck // is process-wide. const ( AckFact = "ack_fact" AckFactKey = "ack_fact_key" AckFactValue = "ack_fact_kv" AckFactEcho = "ack_fact_echo" AckNote = "ack_note" AckReminder = "ack_reminder" AckAct = "ack_act" AckTask = "ack_task" AckTaskUrgent = "ack_task_urgent" AckTaskDuplicate = "ack_task_duplicate" AckNudge = "ack_nudge" AckSnooze = "ack_snooze" AckGeneric = "ack_generic" AckQuietOn = "quiet_on" AckQuietOff = "quiet_off" FailFact = "fail_fact" FailFactUnparsed = "fail_fact_unparsed" FailNote = "fail_note" FailReminder = "fail_reminder" FailReminderTime = "fail_reminder_time" FailTask = "fail_task" FailAck = "fail_ack" FailSnooze = "fail_snooze" FailQuiet = "fail_quiet" ) // ackKeys — every key the code requires the file to define. var ackKeys = []string{ AckFact, AckFactKey, AckFactValue, AckFactEcho, AckNote, AckReminder, AckAct, AckTask, AckTaskUrgent, AckTaskDuplicate, AckNudge, AckSnooze, AckGeneric, AckQuietOn, AckQuietOff, FailFact, FailFactUnparsed, FailNote, FailReminder, FailReminderTime, FailTask, FailAck, FailSnooze, FailQuiet, } // ackFloor — the literal each key falls back to when the file is unusable. // These are the exact strings that lived in Go before this file existed. var ackFloor = map[string]string{ AckFact: "записала факт.", AckFactKey: "отметила: {key}", AckFactValue: "отметила: {key} = {value}", AckFactEcho: "записала: {text}", AckNote: "сохранила заметку.", AckReminder: "напомню.", AckAct: "ок, записала действие: {fn}", AckTask: "записала: {text}", AckTaskUrgent: "поняла, беру в работу: {text}", AckTaskDuplicate: "это уже в списке.", AckNudge: "отлично, отметила.", AckSnooze: "хорошо, вернусь к этому позже.", AckGeneric: "приняла.", AckQuietOn: "тихий режим включён. буду реже напоминать.", AckQuietOff: "тихий режим выключен.", FailFact: "не получилось сохранить факт.", FailFactUnparsed: "не разобрала, что записать — попробуй иначе.", FailNote: "не получилось сохранить заметку.", FailReminder: "не получилось поставить напоминание.", FailReminderTime: "не получилось разобрать время напоминания.", FailTask: "не получилось записать задачу.", FailAck: "не получилось отметить.", FailSnooze: "не получилось отложить.", FailQuiet: "не получилось переключить тихий режим.", } // Acks picks a hand-written Russian acknowledgement. Safe for concurrent use. type Acks struct{ d *say.Deck } // LoadAcks reads the embedded file. Pass a source to make the picking // reproducible in tests; nil seeds from the clock. func LoadAcks(src rand.Source) (*Acks, error) { d, err := say.Load(ackJSON, AckSchemaVersion, ackKeys, ackFloor, src) if err != nil { return nil, err } // The three entries that exist to read his own words back. A variant // without the placeholder would confirm the capture and drop what was // captured, which reads as a successful save of nothing. for _, req := range []struct{ key, ph string }{ {AckFactKey, "{key}"}, {AckFactValue, "{key}"}, {AckFactValue, "{value}"}, {AckFactEcho, "{text}"}, {AckAct, "{fn}"}, {AckTask, "{text}"}, {AckTaskUrgent, "{text}"}, } { if err := d.RequirePlaceholder(req.key, req.ph); err != nil { return nil, err } } return &Acks{d: d}, nil } // deck reads through a nil *Acks, which is the unloadable-file case. func (a *Acks) deck() *say.Deck { if a == nil { return say.FloorDeck(ackFloor) } return a.d } // Say returns one line for key, with his data filled into the frame. Pass nil // when the entry takes none. func (a *Acks) Say(key string, vars map[string]string) string { return a.deck().Text(key, vars) } // Variants returns every line the file can produce, for the persona scorer. func (a *Acks) Variants() []string { return a.deck().Variants() } var ( ackOnce sync.Once acks *Acks ) // DefaultAcks returns the shared instance, loading it on first use. A broken // file logs once and leaves a nil *Acks, which still answers from ackFloor. func DefaultAcks() *Acks { ackOnce.Do(func() { a, err := LoadAcks(nil) if err != nil { log.Printf("phraser: acknowledgements unavailable, using the built-in lines: %v", err) return } acks = a }) return acks } // Ack — one acknowledgement line, the way every caller says it. func Ack(key string, vars map[string]string) string { return DefaultAcks().Say(key, vars) } // FactAck — the confirmation for a captured fact, in the words he used (V-592). // // It is a deck line with his sentence dropped into it, and there is no // generation anywhere on this path. Asking a 1.7B to say his sentence back // produced "Проверила, что ты выпел стакан воды" for "я выпил воды": a non-word // for the verb, a glass he never mentioned — lifted straight out of the example // in ReplySystemPrompt — and a claim to have checked something. The fact store // held key=water value="drank" throughout, so nothing was mis-captured and // everything after the capture was invented. // // An empty utterance falls back to the contentless line rather than confirming // a capture of nothing. func FactAck(utterance string) string { utterance = strings.TrimSpace(utterance) if utterance == "" { return Ack(AckFact, nil) } return Ack(AckFactEcho, map[string]string{"text": utterance}) } // IsAck reports whether text is a line key could have produced. For the daemon // tests, which can no longer compare against one literal. func IsAck(key string, vars map[string]string, text string) bool { return DefaultAcks().deck().Matches(key, vars, text) }