package phraser // The act and smart-home replies — what she says when a capability ran, refused, // or could not be reached. // // Fourth family on the shared deck (deck.go). They were literals in // ecosystem_acts.go, actions_act.go and smarthome.go, where a reworded line was // a rebuild of the daemon that executes his house. // // The four outcomes stay four entries. Reporting a refusal with the wording of // a success is the one failure mode this family can have, and a shared variant // set is how it would happen. import ( _ "embed" "log" "math/rand" "sync" "github.com/kami/maven/internal/say" ) //go:embed acts_ru_v1.json var actJSON []byte // ActSchemaVersion — this family's own version. const ActSchemaVersion = 1 // The entry keys. const ( ActDone = "act_done" ActDoneOut = "act_done_out" ActDoneEntity = "act_done_entity" ActConfirm = "act_confirm" ActConfirmEntity = "act_confirm_entity" ActWhich = "act_which" ActFail = "act_fail" ActFailOut = "act_fail_out" ActFailEntity = "act_fail_entity" ActServerDown = "act_server_down" ActWithdrawn = "act_withdrawn" ActNeedsArgs = "act_needs_args" // ActNeedsAuthedSurface — the irreversible tier, local row or Hexis // capability alike. Not a failure and not a refusal to help: a spoken "да" // is the only authority the voice path can offer, and this is the one act // it is not enough for (Vikunja #449, #523). ActNeedsAuthedSurface = "act_needs_authed_surface" EcoDenied = "eco_denied" EcoDown = "eco_down" EcoAmbiguous = "eco_ambiguous" EcoUnknownEntity = "eco_unknown_entity" EcoNoNexus = "eco_no_nexus" EcoAboutWhat = "eco_about_what" EcoRecall = "eco_recall" AttentionNone = "attention_none" AttentionList = "attention_list" AttentionFail = "attention_fail" AttentionNoneEntity = "attention_none_entity" AttentionListEntity = "attention_list_entity" AttentionFailEntity = "attention_fail_entity" // AttentionDegraded and AttentionNoSources — the two ways an empty // attention list is not an all-clear (ECOSYSTEM-SPEC §2.6, Vikunja #540). AttentionDegraded = "attention_degraded" AttentionNoSources = "attention_no_sources" ChangesNone = "changes_none" ChangesList = "changes_list" ChangesFail = "changes_fail" HomeUnreachable = "home_unreachable" HomeEmpty = "home_empty" HomeOn = "home_on" HomeDark = "home_dark" ) var actKeys = []string{ ActDone, ActDoneOut, ActDoneEntity, ActConfirm, ActConfirmEntity, ActWhich, ActFail, ActFailOut, ActFailEntity, ActServerDown, ActWithdrawn, ActNeedsArgs, ActNeedsAuthedSurface, EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall, AttentionNone, AttentionList, AttentionFail, AttentionNoneEntity, AttentionListEntity, AttentionFailEntity, AttentionDegraded, AttentionNoSources, ChangesNone, ChangesList, ChangesFail, HomeUnreachable, HomeEmpty, HomeOn, HomeDark, } // actFloor — the literal each key falls back to when the file is unusable. It // started as the exact strings that lived in Go before this file existed and now // tracks the file's first variant instead, because a floor that keeps the // wording review threw out would say it back on the one turn nobody is watching. var actFloor = map[string]string{ ActDone: "готово.", ActDoneOut: "готово: {out}", ActDoneEntity: "готово: {name}.", ActConfirm: "выполнить «{name}»? да или нет.", ActConfirmEntity: "выполнить «{name}» для {name_entity}? да или нет.", ActWhich: "какую команду для {name}: {items}?", ActFail: "не получилось выполнить команду.", ActFailOut: "не получилось выполнить команду: {out}", ActFailEntity: "не получилось выполнить команду для {name}.", ActServerDown: "инструмент есть, но сервер не подключён.", ActWithdrawn: "сервер больше не отдаёт этот инструмент — сняла его с разрешённых, посмотри /tools.", ActNeedsArgs: "тут нужны аргументы, из голоса не соберу. угадывать не буду.", ActNeedsAuthedSurface: "это из голоса не выполню — после него ничего не вернуть. запусти сам.", EcoDenied: "{name} отклоняет доступ, проверь токен.", EcoDown: "{name} не отвечает, попробуй ещё раз.", EcoAmbiguous: "что именно: {items}?", EcoUnknownEntity: "не знаю, что это.", EcoNoNexus: "не с чем связать — Nexus не настроен.", EcoAboutWhat: "про что именно?", EcoRecall: "я помню: {items}", AttentionNone: "ничего не требует внимания.", AttentionList: "требует внимания: {items}", AttentionFail: "не могу сейчас узнать, что требует внимания.", AttentionNoneEntity: "по «{name}» ничего нет.", AttentionListEntity: "по «{name}»: {items}", AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».", AttentionDegraded: "за всё не отвечу — источники молчат: {items}.", AttentionNoSources: "мне пока нечего смотреть — у Praxis нет источников.", ChangesNone: "изменений нет.", ChangesList: "изменения: {items}", ChangesFail: "не могу сейчас узнать об изменениях.", HomeUnreachable: "дом не отвечает.", HomeEmpty: "дом ничего не отдаёт.", HomeOn: "включено: {items}", HomeDark: "не отвечают: {count} {word}.", } // Acts picks a hand-written Russian act reply. Safe for concurrent use. type Acts struct{ d *say.Deck } // LoadActs reads the embedded file. Pass a source to make the picking // reproducible in tests; nil seeds from the clock. func LoadActs(src rand.Source) (*Acts, error) { d, err := say.Load(actJSON, ActSchemaVersion, actKeys, actFloor, src) if err != nil { return nil, err } // The entries that name what ran or what he has to choose between. A // variant that dropped the name would confirm an act without saying which. for _, req := range []struct{ key, ph string }{ {ActDoneOut, "{out}"}, {ActDoneEntity, "{name}"}, {ActFailOut, "{out}"}, {ActFailEntity, "{name}"}, {ActConfirm, "{name}"}, {ActConfirmEntity, "{name}"}, {ActConfirmEntity, "{name_entity}"}, {ActWhich, "{name}"}, {ActWhich, "{items}"}, {EcoAmbiguous, "{items}"}, {EcoRecall, "{items}"}, {AttentionList, "{items}"}, {ChangesList, "{items}"}, {HomeOn, "{items}"}, {EcoDenied, "{name}"}, {EcoDown, "{name}"}, {HomeDark, "{count}"}, {HomeDark, "{word}"}, {AttentionNoneEntity, "{name}"}, {AttentionListEntity, "{name}"}, {AttentionListEntity, "{items}"}, {AttentionFailEntity, "{name}"}, } { if err := d.RequirePlaceholder(req.key, req.ph); err != nil { return nil, err } } return &Acts{d: d}, nil } // deck reads through a nil *Acts, which is the unloadable-file case. func (a *Acts) deck() *say.Deck { if a == nil { return say.FloorDeck(actFloor) } return a.d } // Say returns one line for key, with the names filled into the frame. func (a *Acts) 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 *Acts) Variants() []string { return a.deck().Variants() } var ( actOnce sync.Once actsDeck *Acts ) // DefaultActs returns the shared instance, loading it on first use. A broken // file logs once and leaves a nil *Acts, which still answers from actFloor. func DefaultActs() *Acts { actOnce.Do(func() { a, err := LoadActs(nil) if err != nil { log.Printf("phraser: act replies unavailable, using the built-in lines: %v", err) return } actsDeck = a }) return actsDeck } // A — one act reply, the way every caller says it. func A(key string, vars map[string]string) string { return DefaultActs().Say(key, vars) } // IsA reports whether text is a line key could have produced, for the tests. func IsA(key string, vars map[string]string, text string) bool { return DefaultActs().deck().Matches(key, vars, text) }