4c95b200e4
The owner's wording from the PR 112 review, and the placeholder fixes under it.
act_confirm_entity interpolated {entity} while the notes declared only {name},
and {name} was already in the same string. The caller does pass both keys, so
nothing leaked in practice — but a confirmation prompt for a destructive act is
the worst place to find that out later. Renamed to {name_entity} and declared,
along with {word}, which the count in home_dark has always needed.
Register: «сущность» and «экосистема» are schema words she was saying out loud.
act_done_entity stops reporting in the passive and matches «готово.», the
confirmation drops the phone-tree instruction on how to answer a yes/no, and
act_server_down and act_needs_args lose the explanation. «угадывать не буду»
stays exactly as it was.
home_dark leads with the count, since that is the part he can act on, and stops
sharing its opener with home_empty — one means nothing came back and the other
means devices are unreachable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
189 lines
7.5 KiB
Go
189 lines
7.5 KiB
Go
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"
|
|
)
|
|
|
|
//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"
|
|
|
|
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"
|
|
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,
|
|
EcoDenied, EcoDown, EcoAmbiguous, EcoUnknownEntity, EcoNoNexus, EcoAboutWhat, EcoRecall,
|
|
AttentionNone, AttentionList, AttentionFail,
|
|
AttentionNoneEntity, AttentionListEntity, AttentionFailEntity,
|
|
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: "тут нужны аргументы, из голоса не соберу. угадывать не буду.",
|
|
|
|
EcoDenied: "{name} отклоняет доступ, проверь токен.",
|
|
EcoDown: "{name} не отвечает, попробуй ещё раз.",
|
|
EcoAmbiguous: "что именно: {items}?",
|
|
EcoUnknownEntity: "не знаю, что это.",
|
|
EcoNoNexus: "не с чем связать — Nexus не настроен.",
|
|
EcoAboutWhat: "про что именно?",
|
|
EcoRecall: "я помню: {items}",
|
|
|
|
AttentionNone: "ничего не требует внимания.",
|
|
AttentionList: "требует внимания: {items}",
|
|
AttentionFail: "не могу сейчас узнать, что требует внимания.",
|
|
AttentionNoneEntity: "по «{name}» ничего нет.",
|
|
AttentionListEntity: "по «{name}»: {items}",
|
|
AttentionFailEntity: "не могу сейчас узнать, что требует внимания по «{name}».",
|
|
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 *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 := loadDeck(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() *deck {
|
|
if a == nil {
|
|
return 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)
|
|
}
|