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) }