1b76fa8205
askClarify parked "Что сделать?" whatever was on the other end. With an empty allowlist that question has no answer: she asks, fails, asks again and gives up, three turns spent on a request she could have declined in the first one. Empty allowlist now names the gap and parks nothing. A non-empty one still asks, and names what she can run, capped at six, so the question is answerable.
142 lines
5.7 KiB
Go
142 lines
5.7 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"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: {
|
|
"Что записать?",
|
|
"Что именно отметить?",
|
|
"Назови, что записать — например, «выпил воды».",
|
|
},
|
|
dialogue.SlotFn: {
|
|
"Что сделать?",
|
|
"Какое действие выполнить?",
|
|
"Назови действие — я умею только то, что ты мне разрешил.",
|
|
},
|
|
}
|
|
|
|
// namedActsCap bounds how many capability names one question may recite. Six is
|
|
// what a spoken sentence carries; past that the list stops being an answer and
|
|
// becomes a wall he has to hold in his head.
|
|
const namedActsCap = 6
|
|
|
|
// fnClarify decides what to say about an act whose capability is missing, and
|
|
// whether the request is worth parking (Vikunja #556).
|
|
//
|
|
// The old deck asked "Что сделать?" whatever was on the other end. With an empty
|
|
// allowlist that question has no answer: nothing he says can match, so she asks,
|
|
// fails, asks again and gives up — three turns spent on a request she could have
|
|
// declined in the first one. So an empty allowlist names the gap and parks
|
|
// nothing, and a non-empty one asks a question he can actually answer by naming
|
|
// what she has.
|
|
func fnClarify(allow []string, attempt int) (reply string, ask bool) {
|
|
if len(allow) == 0 {
|
|
return "Я пока ничего не умею делать — мне не разрешён ни один инструмент.", false
|
|
}
|
|
q, ok := clarifyQuestionFor(dialogue.SlotFn, attempt)
|
|
if !ok {
|
|
return "", false
|
|
}
|
|
named := allow
|
|
tail := ""
|
|
if len(named) > namedActsCap {
|
|
named, tail = named[:namedActsCap], "…"
|
|
}
|
|
return q + " Я умею: " + strings.Join(named, ", ") + tail + ".", true
|
|
}
|
|
|
|
// 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)
|
|
}
|