c8a5b5416e
V-560 classified the side query correctly and then dropped the request behind it, saying "Прошлую просьбу отпускаю." The owner rejected that on sight: he asked about the weather in the middle of setting a reminder, and being told the reminder was let go reports a loss he did not ask for. It had not been lost — there was simply nowhere to put it. There is now. ClarifyStore grew a bounded stack in V-559 and nothing called Push; this is the caller it was built for. A side query leaves the question parked exactly as it is, the words are answered as themselves, and the question comes back on the end of the same reply — one utterance, two acts. The resumed question is not the first one again. "Когда?" works in the same breath as "напомни позвонить маме" and does not work after a turn about Rome, so the deck has a second form per slot that names the request: "На какое время поставить напоминание?". No attempt is spent, because he answered the side query and not the parked question, and charging a retry for a turn that was never an answer is the V-554 shape. clarifyDropped stays for new_request and cancel, where something really does die. Two things can now die at once, so TakeExpired reports a count instead of a bool and the expiry notice has a plural wording — "прошлую просьбу" when two were lost would be a lie about the number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
156 lines
6.9 KiB
Go
156 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"github.com/kami/maven/internal/dialogue"
|
|
"github.com/kami/maven/internal/router"
|
|
)
|
|
|
|
// The clarify copy deck (Vikunja #457).
|
|
//
|
|
// Every clarify turn used to say one sentence per gap, and a re-ask repeated
|
|
// that sentence word for word. A question he already failed to answer is the
|
|
// worst one to ask again unchanged: the second wording is the one that tells
|
|
// him which part she missed.
|
|
//
|
|
// Fixed templates, not model output, for the reason clarifyQuestions has always
|
|
// given: the resident model would wander, and a question whose wording changes
|
|
// at random is harder to answer than a blunt one. What changes here is that the
|
|
// wording varies with the attempt rather than with a die roll — the first ask is
|
|
// short, the second names the gap, the third spells it out.
|
|
//
|
|
// No schema_version, unlike internal/phraser/nudge_templates.go. These are Go
|
|
// constants compiled into the daemon, so there is no file that can drift out of
|
|
// step with the code that reads it.
|
|
//
|
|
// Persona holds: infinitive and imperative questions, so there is no gender
|
|
// agreement to get wrong, "ты" throughout, and no pet names.
|
|
var clarifyQuestionVariants = map[dialogue.Slot][]string{
|
|
dialogue.SlotTime: {
|
|
"Когда?",
|
|
"Во сколько напомнить?",
|
|
"Скажи время — например, «в семь вечера» или «через час».",
|
|
},
|
|
dialogue.SlotText: {
|
|
"О чём напомнить?",
|
|
"Что сказать тебе в это время?",
|
|
"Скажи одной фразой, о чём напомнить.",
|
|
},
|
|
dialogue.SlotKey: {
|
|
"Что записать?",
|
|
"Что именно отметить?",
|
|
"Назови, что записать — например, «выпил воды».",
|
|
},
|
|
// Not spoken since Vikunja #556: askClarify answers actNotRecognized for a
|
|
// missing capability rather than asking. Kept because clarifyQuestion still
|
|
// reports the gap, and a re-ask deck with a hole in it is harder to read.
|
|
dialogue.SlotFn: {
|
|
"Что сделать?",
|
|
"Какое действие выполнить?",
|
|
"Назови действие — я умею только то, что ты мне разрешил.",
|
|
},
|
|
}
|
|
|
|
// clarifyResumedVariants — the wording for a question coming BACK after a side
|
|
// query took the turn away from it (Vikunja #561).
|
|
//
|
|
// It is not the first question again. "Когда?" works in the same breath as
|
|
// "напомни позвонить маме", because the thing it is about was just said. After
|
|
// a turn about the weather in Rome it does not: he has been thinking about
|
|
// something else, and a bare "Когда?" asks him to remember what she is holding.
|
|
// So the resumed form names the request — "напоминание", "заметка" — and the
|
|
// first form stays short.
|
|
//
|
|
// One wording per slot, not a rotation and not an attempt ladder. A resume does
|
|
// not spend an attempt (that is the point of suspending rather than re-asking),
|
|
// so there is no attempt number to vary on, and this line is heard once per
|
|
// interruption rather than repeatedly.
|
|
//
|
|
// Persona holds: infinitive, so no gender agreement, "ты" nowhere needed, no pet
|
|
// names.
|
|
var clarifyResumedVariants = map[dialogue.Slot]string{
|
|
dialogue.SlotTime: "На какое время поставить напоминание?",
|
|
dialogue.SlotText: "Так о чём напомнить?",
|
|
dialogue.SlotKey: "Так что записать?",
|
|
dialogue.SlotFn: "Так какое действие выполнить?",
|
|
}
|
|
|
|
// clarifyResumedFor gives the resumed wording for a slot. ("", false) when the
|
|
// slot has none, and the caller then resumes nothing rather than inventing a
|
|
// question — a flow it cannot re-ask is one it must not claim to be holding.
|
|
func clarifyResumedFor(slot dialogue.Slot) (string, bool) {
|
|
q, ok := clarifyResumedVariants[slot]
|
|
return q, ok
|
|
}
|
|
|
|
// actNotRecognized is what an act she cannot run gets (Vikunja #556).
|
|
//
|
|
// The deck used to ask "Что сделать?" instead. That question has no answer he
|
|
// can give: he already said what he wanted, and nothing he repeats will match a
|
|
// capability that is not there. So she asked, failed, asked again and gave up —
|
|
// three turns spent on one refusal. She says it once now, and parks nothing.
|
|
//
|
|
// It does not recite the allowlist. A list of names he did not ask about is not
|
|
// an answer to the thing he did ask about.
|
|
const actNotRecognized = "Такую команду я не знаю."
|
|
|
|
// clarifyQuestionFor picks the wording for this attempt. attempt is 1-based, as
|
|
// PendingQuestion.Attempts counts it; anything past the list uses the last and
|
|
// most explicit phrasing rather than wrapping round to the short one, because
|
|
// wrapping would ask the same short question he has already not answered.
|
|
//
|
|
// Deterministic on purpose, unlike clarifyExpiredLine: an expiry notice is the
|
|
// same statement however it is worded, and a re-ask is not.
|
|
func clarifyQuestionFor(slot dialogue.Slot, attempt int) (string, bool) {
|
|
variants, ok := clarifyQuestionVariants[slot]
|
|
if !ok || len(variants) == 0 {
|
|
return "", false
|
|
}
|
|
i := attempt - 1
|
|
if i < 0 {
|
|
i = 0
|
|
}
|
|
if i >= len(variants) {
|
|
i = len(variants) - 1
|
|
}
|
|
return variants[i], true
|
|
}
|
|
|
|
// clarifyMissedVariants — she is asking for the whole utterance again, because
|
|
// the gate fired on an intent with nothing identifiable to ask about (note,
|
|
// query, chat, system are not in wantedSlots).
|
|
//
|
|
// Rotated like the expiry lines and for the same reason: this is the line he
|
|
// hears whenever she misses him completely, so it is a line that repeats, and
|
|
// the same sentence every time is what makes a house assistant sound like a
|
|
// kiosk. All of them say the same two things — she did not catch it, and he
|
|
// should say it again — because the wording may vary and the meaning may not.
|
|
var clarifyMissedVariants = []string{
|
|
"Не совсем поняла — скажи, пожалуйста, ещё раз.",
|
|
"Я тебя не разобрала. Повтори, пожалуйста.",
|
|
"Не уловила. Скажи это по-другому?",
|
|
"Прости, не поняла — попробуй сказать иначе.",
|
|
}
|
|
|
|
// clarifyMissedFor picks a wording by the utterance itself, so the same words
|
|
// asked twice get the same answer and two different misses sound different.
|
|
//
|
|
// A hash, not rand: a test that drives an utterance twice must not depend on a
|
|
// die roll, and the point of rotating is only that consecutive misses differ.
|
|
func clarifyMissedFor(utterance string) string {
|
|
var sum int
|
|
for _, r := range utterance {
|
|
sum += int(r)
|
|
}
|
|
return clarifyMissedVariants[sum%len(clarifyMissedVariants)]
|
|
}
|
|
|
|
// clarifyMissedLine is the canned reply for a clarify decision she cannot turn
|
|
// into a question. Returns "" for a decision that is not a clarify, so the
|
|
// caller keeps its own reply.
|
|
func clarifyMissedLine(dec router.Decision) string {
|
|
if !dec.Clarify {
|
|
return ""
|
|
}
|
|
return clarifyMissedFor(dec.Utterance)
|
|
}
|