d32eae8aac
The gesture was web-only, so the sample was skewing to the turns he happens to type. Voice is where the hard cases are. Half of it already existed: the repair rung has read "нет, это была заметка" since V-455. It taught the classifier and wrote no durable label, so the two paths disagreed about what a correction is. It now writes both. Two sinks and not one on purpose: the classifier seed makes the next turn better today, and the label is what a fitted head trains on after the transcript expires. The trace id is stamped onto the remembered turn after the fact, because the trace is written when the turn ends and recordTurn runs in the middle of it. New: the untargeted half. "нет, не так" writes the negative and redoes nothing, because there is no target to redo it as. Voice needs this more than the web does — naming an intent aloud means saying "заметка" or "факт", which is her vocabulary and not his. repair_negatives is a new closed lexicon set matched against the WHOLE utterance, never as a substring. That is what keeps it apart from repair_markers, where "это не" is a fragment that needs an intent word after it. A member that could appear inside an ordinary sentence does not belong in the set.
215 lines
9.3 KiB
Go
215 lines
9.3 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"
|
|
|
|
"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"
|
|
// ActUnknownTarget — the verb reached a tool and the target did not reach
|
|
// anything. Named rather than run, because the alias match swallowed the verb
|
|
// and handed on the next word of the sentence (V-634).
|
|
ActUnknownTarget = "act_unknown_target"
|
|
// RepairNoted — he said the turn was wrong and did not say what it should
|
|
// have been. She confirms the label landed and does not ask, because the
|
|
// answer would be one of her own intent names (V-636).
|
|
RepairNoted = "repair_noted"
|
|
|
|
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, ActUnknownTarget, RepairNoted,
|
|
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: "тут нужны аргументы, из голоса не соберу. угадывать не буду.",
|
|
RepairNoted: "поняла, отметила, что ответила не так.",
|
|
ActUnknownTarget: "«{name}» — не знаю такой цели. назови её как в системе.",
|
|
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)
|
|
}
|