Merge the confirmation strings family (#174)
This commit is contained in:
+14
-12
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/kami/maven/internal/phraser"
|
||||
"github.com/kami/maven/internal/router"
|
||||
)
|
||||
|
||||
@@ -118,13 +119,13 @@ func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolve
|
||||
//
|
||||
// Acceptance itself is recorded by /routines, and the tick
|
||||
// loop nudges on the interval from there (Vikunja #366).
|
||||
return "поняла — подтверди на странице рутин, и начну напоминать."
|
||||
return phraser.C(phraser.ConfirmRoutineAuthed, nil)
|
||||
},
|
||||
no: func() string {
|
||||
if err := h.dataStore.DismissProposedRoutine(ctx, pr.routineID); err != nil {
|
||||
log.Printf("voice: dismiss proposed routine: %v", err)
|
||||
}
|
||||
return "хорошо, не буду."
|
||||
return phraser.C(phraser.ConfirmRoutineNo, nil)
|
||||
},
|
||||
},
|
||||
// Hexis execution confirm. Bound to the exact capability + target that
|
||||
@@ -137,7 +138,7 @@ func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolve
|
||||
yes: func() string {
|
||||
return h.execHexis(ctx, hx.capabilityID, hx.capName, hx.entityID, hx.displayName)
|
||||
},
|
||||
no: func() string { return "отменила." },
|
||||
no: func() string { return phraser.C(phraser.ConfirmCancelled, nil) },
|
||||
},
|
||||
// Tool confirm.
|
||||
{
|
||||
@@ -150,16 +151,16 @@ func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolve
|
||||
if err != nil {
|
||||
log.Printf("voice: tool %s (confirmed): %v", p.fn, err)
|
||||
if out != "" {
|
||||
return "не получилось выполнить команду: " + firstLine(out)
|
||||
return phraser.A(phraser.ActFailOut, map[string]string{"out": firstLine(out)})
|
||||
}
|
||||
return "не получилось выполнить команду."
|
||||
return phraser.A(phraser.ActFail, nil)
|
||||
}
|
||||
if out != "" {
|
||||
return "готово: " + firstLine(out)
|
||||
return phraser.A(phraser.ActDoneOut, map[string]string{"out": firstLine(out)})
|
||||
}
|
||||
return "готово."
|
||||
return phraser.A(phraser.ActDone, nil)
|
||||
},
|
||||
no: func() string { return "отменила." },
|
||||
no: func() string { return phraser.C(phraser.ConfirmCancelled, nil) },
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -170,17 +171,18 @@ func (h *reactiveHandler) confirmResolvers(ctx context.Context) []confirmResolve
|
||||
func (h *reactiveHandler) proposeGap(ctx context.Context, dec router.Decision) string {
|
||||
name := firstWord(stripWake(dec.Utterance))
|
||||
if name == "" {
|
||||
return "не разобрала команду — попробуй иначе."
|
||||
return phraser.C(phraser.ProposeNoVerb, nil)
|
||||
}
|
||||
vars := map[string]string{"name": name}
|
||||
newly, err := h.api.ProposeTool(ctx, name, dec.Utterance, "", h.now())
|
||||
if err != nil {
|
||||
log.Printf("voice: propose tool %q: %v", name, err)
|
||||
return "команды «" + name + "» нет в списке разрешённых."
|
||||
return phraser.C(phraser.ProposeFailed, vars)
|
||||
}
|
||||
if newly {
|
||||
return "команды «" + name + "» нет в списке. Предложила её добавить — включи через клиент."
|
||||
return phraser.C(phraser.ProposeNew, vars)
|
||||
}
|
||||
return "команды «" + name + "» пока нет в списке — она уже предложена, включи через клиент."
|
||||
return phraser.C(phraser.ProposeAlready, vars)
|
||||
}
|
||||
|
||||
// confirmVerdict — the parse of a y/n confirm answer.
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package phraser
|
||||
|
||||
// The confirmation answers — what she says once he has answered a confirm, and
|
||||
// what she says when an act names a verb she may not run.
|
||||
//
|
||||
// Fifth family on the shared deck (deck.go). They were literals in
|
||||
// cmd/mavend/confirm.go.
|
||||
//
|
||||
// The prompt is deliberately not here. act_confirm and act_confirm_entity are in
|
||||
// acts_ru_v1.json, where they belong: the sentence he has to hear before he says
|
||||
// yes is an act line, and it already loads with its {name} placeholder required.
|
||||
// Vikunja #505 asked whether this family should move at all. It moves, but only
|
||||
// the answers, and every entry is fixed. He answered a question about one
|
||||
// specific thing, so variety here buys nothing and costs the one property that
|
||||
// matters: the same act reports the same outcome every time.
|
||||
//
|
||||
// The tool-confirm success and failure lines are not here either. They said
|
||||
// "готово." and "не получилось выполнить команду." in two places, which is the
|
||||
// acts family word for word, so the confirmed run now reports through ActDone
|
||||
// and ActFail rather than keeping a second copy that can drift.
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"log"
|
||||
"math/rand"
|
||||
"sync"
|
||||
|
||||
"github.com/kami/maven/internal/say"
|
||||
)
|
||||
|
||||
//go:embed confirm_ru_v1.json
|
||||
var confirmJSON []byte
|
||||
|
||||
// ConfirmSchemaVersion — this family's own version.
|
||||
const ConfirmSchemaVersion = 1
|
||||
|
||||
// The entry keys.
|
||||
const (
|
||||
// ConfirmCancelled — he said no. It names no capability, because the
|
||||
// prompt he answered named one and nothing ran.
|
||||
ConfirmCancelled = "confirm_cancelled"
|
||||
// ConfirmRoutineAuthed — he said yes to a proposed routine out loud, which
|
||||
// is not an acceptance. A room mic cannot hand the tick loop a standing new
|
||||
// reason to speak, so the row stays proposed and this line points at the
|
||||
// page where the accept button is gated (Vikunja #367).
|
||||
ConfirmRoutineAuthed = "confirm_routine_authed"
|
||||
ConfirmRoutineNo = "confirm_routine_no"
|
||||
|
||||
// The propose-gap lines: an act whose verb is not on the allowlist. She
|
||||
// drafts the registration and says so. She never enables it.
|
||||
ProposeNoVerb = "propose_no_verb"
|
||||
ProposeFailed = "propose_failed"
|
||||
ProposeNew = "propose_new"
|
||||
ProposeAlready = "propose_already"
|
||||
)
|
||||
|
||||
var confirmKeys = []string{
|
||||
ConfirmCancelled, ConfirmRoutineAuthed, ConfirmRoutineNo,
|
||||
ProposeNoVerb, ProposeFailed, ProposeNew, ProposeAlready,
|
||||
}
|
||||
|
||||
// confirmFloor — the literal each key falls back to when the file is unusable.
|
||||
// These are the exact strings that lived in cmd/mavend/confirm.go.
|
||||
var confirmFloor = map[string]string{
|
||||
ConfirmCancelled: "отменила.",
|
||||
ConfirmRoutineAuthed: "поняла — подтверди на странице рутин, и начну напоминать.",
|
||||
ConfirmRoutineNo: "хорошо, не буду.",
|
||||
|
||||
ProposeNoVerb: "не разобрала команду — попробуй иначе.",
|
||||
ProposeFailed: "команды «{name}» нет в списке разрешённых.",
|
||||
ProposeNew: "команды «{name}» нет в списке. Предложила её добавить — включи через клиент.",
|
||||
ProposeAlready: "команды «{name}» пока нет в списке — она уже предложена, включи через клиент.",
|
||||
}
|
||||
|
||||
// Confirms picks a hand-written Russian confirmation answer. Safe for
|
||||
// concurrent use.
|
||||
type Confirms struct{ d *say.Deck }
|
||||
|
||||
// LoadConfirms reads the embedded file. Pass a source to make the picking
|
||||
// reproducible in tests; nil seeds from the clock.
|
||||
func LoadConfirms(src rand.Source) (*Confirms, error) {
|
||||
d, err := say.Load(confirmJSON, ConfirmSchemaVersion, confirmKeys, confirmFloor, src)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Every propose line that names the verb must keep naming it. A variant that
|
||||
// dropped {name} would tell him a command is not allowed without saying
|
||||
// which one, and the whole point of the line is that he goes and enables it.
|
||||
for _, key := range []string{ProposeFailed, ProposeNew, ProposeAlready} {
|
||||
if err := d.RequirePlaceholder(key, "{name}"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &Confirms{d: d}, nil
|
||||
}
|
||||
|
||||
// deck reads through a nil *Confirms, which is the unloadable-file case.
|
||||
func (c *Confirms) deck() *say.Deck {
|
||||
if c == nil {
|
||||
return say.FloorDeck(confirmFloor)
|
||||
}
|
||||
return c.d
|
||||
}
|
||||
|
||||
// Say returns the line for key, with the verb filled into the frame.
|
||||
func (c *Confirms) Say(key string, vars map[string]string) string {
|
||||
return c.deck().Text(key, vars)
|
||||
}
|
||||
|
||||
// Variants returns every line the file can produce, for the persona scorer.
|
||||
func (c *Confirms) Variants() []string { return c.deck().Variants() }
|
||||
|
||||
var (
|
||||
confirmOnce sync.Once
|
||||
confirmsDeck *Confirms
|
||||
)
|
||||
|
||||
// DefaultConfirms returns the shared instance, loading it on first use. A broken
|
||||
// file logs once and leaves a nil *Confirms, which still answers from
|
||||
// confirmFloor: a daemon must not fail to boot over its own copy deck.
|
||||
func DefaultConfirms() *Confirms {
|
||||
confirmOnce.Do(func() {
|
||||
c, err := LoadConfirms(nil)
|
||||
if err != nil {
|
||||
log.Printf("phraser: confirmation answers unavailable, using the built-in lines: %v", err)
|
||||
return
|
||||
}
|
||||
confirmsDeck = c
|
||||
})
|
||||
return confirmsDeck
|
||||
}
|
||||
|
||||
// C — one confirmation answer, the way every caller says it.
|
||||
func C(key string, vars map[string]string) string { return DefaultConfirms().Say(key, vars) }
|
||||
|
||||
// IsC reports whether text is a line key could have produced, for the tests.
|
||||
func IsC(key string, vars map[string]string, text string) bool {
|
||||
return DefaultConfirms().deck().Matches(key, vars, text)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package phraser
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The confirmation family has the strictest constraints of the six, so they are
|
||||
// tested rather than left to the doc comment (Vikunja #505).
|
||||
func TestConfirmFamilyLoads(t *testing.T) {
|
||||
c, err := LoadConfirms(rand.NewSource(1))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfirms: %v", err)
|
||||
}
|
||||
if got := len(c.Variants()); got != len(confirmKeys) {
|
||||
t.Errorf("variants %d, want %d: every entry is fixed at one wording", got, len(confirmKeys))
|
||||
}
|
||||
}
|
||||
|
||||
// A propose line that lost {name} would tell him a command is not allowed
|
||||
// without saying which one, which is the whole content of the line.
|
||||
func TestEveryProposeLineNamesTheVerb(t *testing.T) {
|
||||
for _, key := range []string{ProposeFailed, ProposeNew, ProposeAlready} {
|
||||
got := C(key, map[string]string{"name": "перезагрузи"})
|
||||
if !strings.Contains(got, "перезагрузи") {
|
||||
t.Errorf("%s: %q does not name the verb", key, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A spoken yes does not accept a routine. The line has to keep saying where the
|
||||
// acceptance happens, or he hears agreement and gets no reminders.
|
||||
func TestRoutineYesStillPointsAtThePage(t *testing.T) {
|
||||
got := C(ConfirmRoutineAuthed, nil)
|
||||
if !strings.Contains(got, "рутин") {
|
||||
t.Errorf("%q does not name the routines page", got)
|
||||
}
|
||||
}
|
||||
|
||||
// The floor answers when the file will not load, so the deck can never leave a
|
||||
// confirmed act with nothing to say.
|
||||
func TestConfirmFloorAnswersWithoutTheFile(t *testing.T) {
|
||||
var c *Confirms
|
||||
if got := c.Say(ConfirmCancelled, nil); got != confirmFloor[ConfirmCancelled] {
|
||||
t.Errorf("nil deck: %q, want the floor line", got)
|
||||
}
|
||||
if got := c.Say(ProposeFailed, map[string]string{"name": "стоп"}); !strings.Contains(got, "стоп") {
|
||||
t.Errorf("nil deck: %q does not name the verb", got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "confirm_ru_v1",
|
||||
"notes": [
|
||||
"What she says once a confirmation has been answered, and what she says when an act names a verb she is not allowed to run.",
|
||||
"The prompt itself is not here. act_confirm and act_confirm_entity live in acts_ru_v1.json, because the sentence he has to hear before he says yes is an act line.",
|
||||
"Every entry is fixed. He answered a question about one specific thing, so the answer names what happened to that thing and does not get reworded for variety.",
|
||||
"confirm_routine_authed is the strictest of them. A spoken yes does not accept a routine, so this line has to keep pointing at the page that does."
|
||||
],
|
||||
"entries": {
|
||||
"confirm_cancelled": {
|
||||
"fixed": true,
|
||||
"variants": ["отменила."]
|
||||
},
|
||||
"confirm_routine_authed": {
|
||||
"fixed": true,
|
||||
"variants": ["поняла — подтверди на странице рутин, и начну напоминать."]
|
||||
},
|
||||
"confirm_routine_no": {
|
||||
"fixed": true,
|
||||
"variants": ["хорошо, не буду."]
|
||||
},
|
||||
"propose_no_verb": {
|
||||
"fixed": true,
|
||||
"variants": ["не разобрала команду — попробуй иначе."]
|
||||
},
|
||||
"propose_failed": {
|
||||
"fixed": true,
|
||||
"variants": ["команды «{name}» нет в списке разрешённых."]
|
||||
},
|
||||
"propose_new": {
|
||||
"fixed": true,
|
||||
"variants": ["команды «{name}» нет в списке. Предложила её добавить — включи через клиент."]
|
||||
},
|
||||
"propose_already": {
|
||||
"fixed": true,
|
||||
"variants": ["команды «{name}» пока нет в списке — она уже предложена, включи через клиент."]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,11 @@ func TestFallbackPersona(t *testing.T) {
|
||||
t.Fatalf("LoadActs: %v", err)
|
||||
}
|
||||
variants = append(variants, act.Variants()...)
|
||||
con, err := phraser.LoadConfirms(rand.NewSource(20260804))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfirms: %v", err)
|
||||
}
|
||||
variants = append(variants, con.Variants()...)
|
||||
sum, err := say.LoadSummaries(rand.NewSource(20260804))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadSummaries: %v", err)
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The other four families, held to the rule internal/say holds the fifth to:
|
||||
// The five embedded families, held to the rule internal/say holds the summary to:
|
||||
// one variant means fixed. Reported per family, because a failure that names
|
||||
// "some file" is a failure nobody acts on.
|
||||
func TestEverySingleVariantEntryIsFixed(t *testing.T) {
|
||||
@@ -25,11 +25,16 @@ func TestEverySingleVariantEntryIsFixed(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("LoadActs: %v", err)
|
||||
}
|
||||
confirms, err := LoadConfirms(rand.NewSource(1))
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfirms: %v", err)
|
||||
}
|
||||
for name, keys := range map[string][]string{
|
||||
"fallbacks": f.d.UnfixedSingles(),
|
||||
"acks": a.d.UnfixedSingles(),
|
||||
"queries": q.d.UnfixedSingles(),
|
||||
"acts": acts.d.UnfixedSingles(),
|
||||
"confirms": confirms.d.UnfixedSingles(),
|
||||
} {
|
||||
if len(keys) > 0 {
|
||||
t.Errorf("%s: single-variant entries not marked fixed: %v", name, keys)
|
||||
|
||||
Reference in New Issue
Block a user