phraser: put the act and smart-home replies in a versioned json (V-504)
What she says when a capability ran, refused, or could not be reached. Around forty literals across ecosystem_acts.go, actions_act.go and smarthome.go. "It ran", "it was refused", "the ecosystem is down" and "I could not work out what you meant" keep four entries. One variant set across them would let a failure report itself as a success, which is the only failure mode this family has. The lines that report an act as done are fixed rather than varied. A success report that rewords itself is harder to trust when he is listening for it, and the confirmations are fixed for the same reason: they carry an instruction. internal/smarthome/ha.go keeps its own "готово". It is a device driver, and wiring the copy deck into one is the wrong dependency — the daemon relays that word, it does not speak it.
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
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.
|
||||
// These are the exact strings that lived in Go before this file existed.
|
||||
var actFloor = registerFloor(map[string]string{
|
||||
ActDone: "готово.",
|
||||
ActDoneOut: "готово: {out}",
|
||||
ActDoneEntity: "команда выполнена для {name}.",
|
||||
ActConfirm: "выполнить «{name}»? скажи «да» или «нет».",
|
||||
ActConfirmEntity: "выполнить «{name}» для {entity}? скажи «да» или «нет».",
|
||||
ActWhich: "какую команду для {name}: {items}?",
|
||||
ActFail: "не получилось выполнить команду.",
|
||||
ActFailOut: "не получилось выполнить команду: {out}",
|
||||
ActFailEntity: "не получилось выполнить команду для {name}.",
|
||||
ActServerDown: "этот инструмент включён, но сервер, который его выполняет, сейчас не подключён.",
|
||||
ActWithdrawn: "сервер больше не предлагает этот инструмент — я сняла его с разрешённых, посмотри на /tools.",
|
||||
ActNeedsArgs: "этому инструменту нужны аргументы, которые я из голоса не соберу — я не буду угадывать.",
|
||||
|
||||
EcoDenied: "экосистема отклоняет доступ, проверь токен.",
|
||||
EcoDown: "экосистема недоступна, попробуй ещё раз.",
|
||||
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, "{entity}"},
|
||||
{ActWhich, "{name}"}, {ActWhich, "{items}"},
|
||||
{EcoAmbiguous, "{items}"}, {EcoRecall, "{items}"},
|
||||
{AttentionList, "{items}"}, {ChangesList, "{items}"}, {HomeOn, "{items}"},
|
||||
{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 nil
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "russian act and smart-home replies v1",
|
||||
"notes": [
|
||||
"What she says when a capability ran, refused, or could not be reached. Edit the wording here, no Go changes needed.",
|
||||
"Rules: she is feminine about herself, he is a man addressed as ты. Never вы/вас/ваш, never он/его about him. No pet names.",
|
||||
"\"it ran\", \"it was refused\", \"the ecosystem is down\" and \"I could not work out what you meant\" are four different truths. They keep four entries, because one variant set would let a failure report itself as a success.",
|
||||
"Placeholders: {name} an entity or capability the caller resolved, {out} the command's own output, {items} a joined list, {count} a number. Entity names and capability ids are interpolated Go-side.",
|
||||
"fixed: true means exactly one variant and no picking. Used where the wording carries an instruction he has to act on — a confirmation, a pointer at /tools — and for the two lines that report an act as done, because a success report that reworded itself is harder to trust and harder to test."
|
||||
],
|
||||
"entries": {
|
||||
"act_done": {
|
||||
"fixed": true,
|
||||
"variants": ["готово."]
|
||||
},
|
||||
"act_done_out": {
|
||||
"variants": ["готово: {out}", "сделала: {out}"]
|
||||
},
|
||||
"act_done_entity": {
|
||||
"fixed": true,
|
||||
"variants": ["команда выполнена для {name}."]
|
||||
},
|
||||
"act_confirm": {
|
||||
"fixed": true,
|
||||
"variants": ["выполнить «{name}»? скажи «да» или «нет»."]
|
||||
},
|
||||
"act_confirm_entity": {
|
||||
"fixed": true,
|
||||
"variants": ["выполнить «{name}» для {entity}? скажи «да» или «нет»."]
|
||||
},
|
||||
"act_which": {
|
||||
"variants": ["какую команду для {name}: {items}?"]
|
||||
},
|
||||
"act_fail": {
|
||||
"variants": ["не получилось выполнить команду.", "команда не выполнилась."]
|
||||
},
|
||||
"act_fail_out": {
|
||||
"variants": ["не получилось выполнить команду: {out}"]
|
||||
},
|
||||
"act_fail_entity": {
|
||||
"variants": ["не получилось выполнить команду для {name}.", "команда для {name} не выполнилась."]
|
||||
},
|
||||
"act_server_down": {
|
||||
"variants": ["этот инструмент включён, но сервер, который его выполняет, сейчас не подключён."]
|
||||
},
|
||||
"act_withdrawn": {
|
||||
"fixed": true,
|
||||
"variants": ["сервер больше не предлагает этот инструмент — я сняла его с разрешённых, посмотри на /tools."]
|
||||
},
|
||||
"act_needs_args": {
|
||||
"variants": ["этому инструменту нужны аргументы, которые я из голоса не соберу — я не буду угадывать."]
|
||||
},
|
||||
"eco_denied": {
|
||||
"variants": ["экосистема отклоняет доступ, проверь токен."]
|
||||
},
|
||||
"eco_down": {
|
||||
"variants": ["экосистема недоступна, попробуй ещё раз.", "экосистема не отвечает, попробуй ещё раз."]
|
||||
},
|
||||
"eco_ambiguous": {
|
||||
"variants": ["уточни, что именно: {items}?", "что именно из этого: {items}?"]
|
||||
},
|
||||
"eco_unknown_entity": {
|
||||
"variants": ["не знаю такой сущности.", "такой сущности у меня нет."]
|
||||
},
|
||||
"eco_no_nexus": {
|
||||
"variants": ["не могу связать это с сущностью — Nexus не настроен."]
|
||||
},
|
||||
"eco_about_what": {
|
||||
"variants": ["про что именно спросить?", "про что спросить?"]
|
||||
},
|
||||
"eco_recall": {
|
||||
"variants": ["я помню: {items}"]
|
||||
},
|
||||
"attention_none": {
|
||||
"variants": ["ничего не требует внимания.", "внимания сейчас ничего не требует."]
|
||||
},
|
||||
"attention_list": {
|
||||
"variants": ["требует внимания: {items}"]
|
||||
},
|
||||
"attention_fail": {
|
||||
"variants": ["не могу сейчас узнать, что требует внимания."]
|
||||
},
|
||||
"attention_none_entity": {
|
||||
"variants": ["по «{name}» ничего нет.", "по «{name}» пока пусто."]
|
||||
},
|
||||
"attention_list_entity": {
|
||||
"variants": ["по «{name}»: {items}"]
|
||||
},
|
||||
"attention_fail_entity": {
|
||||
"variants": ["не могу сейчас узнать, что требует внимания по «{name}»."]
|
||||
},
|
||||
"changes_none": {
|
||||
"variants": ["нет изменений.", "изменений нет."]
|
||||
},
|
||||
"changes_list": {
|
||||
"variants": ["изменения: {items}"]
|
||||
},
|
||||
"changes_fail": {
|
||||
"variants": ["не могу сейчас узнать об изменениях."]
|
||||
},
|
||||
"home_unreachable": {
|
||||
"variants": ["не смогла достучаться до дома.", "дом не отвечает."]
|
||||
},
|
||||
"home_empty": {
|
||||
"variants": ["дом ничего не отдаёт.", "дом молчит."]
|
||||
},
|
||||
"home_on": {
|
||||
"variants": ["включено: {items}"]
|
||||
},
|
||||
"home_dark": {
|
||||
"variants": ["дом молчит: {count} {word} не отвечают."]
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user