phraser: move the fallbacks onto the deck (V-502)
This commit is contained in:
@@ -13,16 +13,7 @@ import (
|
|||||||
// fallbacks_ru_v1.json break Go tests, which is the coupling this file removed.
|
// fallbacks_ru_v1.json break Go tests, which is the coupling this file removed.
|
||||||
func isFallback(t *testing.T, key, sources, got string) bool {
|
func isFallback(t *testing.T, key, sources, got string) bool {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
e, ok := DefaultFallbacks().file.Entries[key]
|
return DefaultFallbacks().deck().matches(key, map[string]string{"sources": sources}, got)
|
||||||
if !ok {
|
|
||||||
t.Fatalf("no fallback entry %q", key)
|
|
||||||
}
|
|
||||||
for _, v := range e.Variants {
|
|
||||||
if strings.ReplaceAll(v, "{sources}", sources) == got {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// A dead server must be distinguishable from bad phrasing. Both PhraseChat and
|
// A dead server must be distinguishable from bad phrasing. Both PhraseChat and
|
||||||
|
|||||||
+23
-123
@@ -5,8 +5,6 @@ package phraser
|
|||||||
// They were four string literals spread across phraser.go, llmphraser.go and
|
// They were four string literals spread across phraser.go, llmphraser.go and
|
||||||
// cmd/mavend/worldmodel.go. Every one of them is a line he hears out loud, so
|
// cmd/mavend/worldmodel.go. Every one of them is a line he hears out loud, so
|
||||||
// rewording one was a Go edit, a rebuild and a redeploy for what is product copy.
|
// rewording one was a Go edit, a rebuild and a redeploy for what is product copy.
|
||||||
// This is the same shape nudges_ru_v1.json already uses for nudges: embedded,
|
|
||||||
// schema-versioned, several variants, and never the same variant twice running.
|
|
||||||
//
|
//
|
||||||
// The floor under the floor is deliberate. These strings exist because something
|
// The floor under the floor is deliberate. These strings exist because something
|
||||||
// already failed, so a broken template file must not be able to take the last
|
// already failed, so a broken template file must not be able to take the last
|
||||||
@@ -14,13 +12,9 @@ package phraser
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed fallbacks_ru_v1.json
|
//go:embed fallbacks_ru_v1.json
|
||||||
@@ -45,132 +39,56 @@ var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}
|
|||||||
|
|
||||||
// hardFloor — the literal each key falls back to when the file is unusable.
|
// hardFloor — 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.
|
// These are the exact strings that lived in Go before this file existed.
|
||||||
var hardFloor = map[string]string{
|
var hardFloor = registerFloor(map[string]string{
|
||||||
fbChat: "даже не знаю, что сказать.",
|
fbChat: "даже не знаю, что сказать.",
|
||||||
fbQueryUnknown: "не знаю.",
|
fbQueryUnknown: "не знаю.",
|
||||||
fbQuerySources: "вот что я нашла: {sources}",
|
fbQuerySources: "вот что я нашла: {sources}",
|
||||||
fbWorldGap: "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.",
|
fbWorldGap: "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.",
|
||||||
}
|
})
|
||||||
|
|
||||||
type fallbackEntry struct {
|
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
|
||||||
// Fixed — one variant, never picked between. For wording that must not drift
|
type Fallbacks struct{ d *deck }
|
||||||
// from turn to turn, like the gap phrase that names an unavailable model.
|
|
||||||
Fixed bool `json:"fixed"`
|
|
||||||
Variants []string `json:"variants"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type fallbackFile struct {
|
|
||||||
SchemaVersion int `json:"schema_version"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Notes []string `json:"notes"`
|
|
||||||
Entries map[string]fallbackEntry `json:"entries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallbacks picks a hand-written Russian fallback line.
|
|
||||||
//
|
|
||||||
// Safe for concurrent use. Never the same variant twice in a row for the same
|
|
||||||
// entry: hearing the identical words every time a request fails is how a failure
|
|
||||||
// stops registering as one.
|
|
||||||
type Fallbacks struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
rnd *rand.Rand
|
|
||||||
last map[string]string
|
|
||||||
file fallbackFile
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadFallbacks reads the embedded file. Pass a source to make the picking
|
// LoadFallbacks reads the embedded file. Pass a source to make the picking
|
||||||
// reproducible in tests; nil seeds from the clock.
|
// reproducible in tests; nil seeds from the clock.
|
||||||
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
|
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
|
||||||
var f fallbackFile
|
d, err := loadDeck(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
||||||
if err := json.Unmarshal(fallbackJSON, &f); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fallbacks: parse: %w", err)
|
return nil, err
|
||||||
}
|
}
|
||||||
if f.SchemaVersion != FallbackSchemaVersion {
|
// query_sources is the one entry whose whole job is to read something back.
|
||||||
return nil, fmt.Errorf("fallbacks: schema_version %d, want %d",
|
if err := d.requirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
||||||
f.SchemaVersion, FallbackSchemaVersion)
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, k := range fbKeys {
|
return &Fallbacks{d: d}, nil
|
||||||
e, ok := f.Entries[k]
|
|
||||||
if !ok || len(e.Variants) == 0 {
|
|
||||||
return nil, fmt.Errorf("fallbacks: entry %q is missing or empty", k)
|
|
||||||
}
|
|
||||||
if e.Fixed && len(e.Variants) != 1 {
|
|
||||||
return nil, fmt.Errorf("fallbacks: entry %q is fixed but has %d variants", k, len(e.Variants))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// query_sources is the one entry whose whole job is to read something back,
|
|
||||||
// so a variant without the placeholder would silently drop the sources.
|
|
||||||
for _, v := range f.Entries[fbQuerySources].Variants {
|
|
||||||
if !strings.Contains(v, "{sources}") {
|
|
||||||
return nil, fmt.Errorf("fallbacks: %q variant %q does not use {sources}", fbQuerySources, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if src == nil {
|
|
||||||
src = rand.NewSource(time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
return &Fallbacks{rnd: rand.New(src), last: map[string]string{}, file: f}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// text returns one variant for key, with {sources} filled in. A nil receiver is
|
// deck reads through a nil *Fallbacks, which is the unloadable-file case.
|
||||||
// the unloadable-file case and answers from hardFloor, so the caller never has
|
func (f *Fallbacks) deck() *deck {
|
||||||
// to check whether the templates loaded.
|
if f == nil {
|
||||||
func (f *Fallbacks) text(key, sources string) string {
|
return nil
|
||||||
tmpl := hardFloor[key]
|
|
||||||
if f != nil {
|
|
||||||
if e, ok := f.file.Entries[key]; ok && len(e.Variants) > 0 {
|
|
||||||
tmpl = f.pick(key, e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return strings.ReplaceAll(tmpl, "{sources}", sources)
|
return f.d
|
||||||
}
|
|
||||||
|
|
||||||
// pick chooses at random, skipping whatever this entry said last time.
|
|
||||||
func (f *Fallbacks) pick(key string, e fallbackEntry) string {
|
|
||||||
f.mu.Lock()
|
|
||||||
defer f.mu.Unlock()
|
|
||||||
|
|
||||||
choices := e.Variants
|
|
||||||
if len(choices) > 1 {
|
|
||||||
fresh := make([]string, 0, len(choices))
|
|
||||||
for _, v := range choices {
|
|
||||||
if v != f.last[key] {
|
|
||||||
fresh = append(fresh, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(fresh) > 0 {
|
|
||||||
choices = fresh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got := choices[f.rnd.Intn(len(choices))]
|
|
||||||
f.last[key] = got
|
|
||||||
return got
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chat — nothing usable came back on the chat path.
|
// Chat — nothing usable came back on the chat path.
|
||||||
func (f *Fallbacks) Chat() string { return f.text(fbChat, "") }
|
func (f *Fallbacks) Chat() string { return f.deck().text(fbChat, nil) }
|
||||||
|
|
||||||
// Unknown — a question she cannot answer and will not guess at.
|
// Unknown — a question she cannot answer and will not guess at.
|
||||||
func (f *Fallbacks) Unknown() string { return f.text(fbQueryUnknown, "") }
|
func (f *Fallbacks) Unknown() string { return f.deck().text(fbQueryUnknown, nil) }
|
||||||
|
|
||||||
// FromSources — read back what she was handed, because phrasing it failed.
|
// FromSources — read back what she was handed, because phrasing it failed.
|
||||||
func (f *Fallbacks) FromSources(sources string) string {
|
func (f *Fallbacks) FromSources(sources string) string {
|
||||||
return f.text(fbQuerySources, sources)
|
return f.deck().text(fbQuerySources, map[string]string{"sources": sources})
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorldGap — the world model is the one configured to answer and it is not
|
// WorldGap — the world model is the one configured to answer and it is not
|
||||||
// answering. Fixed wording: it names a specific gap, and a variant set here
|
// answering. Fixed wording: it names a specific gap, and a variant set here
|
||||||
// would let "the big model is asleep" drift into "I don't know".
|
// would let "the big model is asleep" drift into "I don't know".
|
||||||
func (f *Fallbacks) WorldGap() string { return f.text(fbWorldGap, "") }
|
func (f *Fallbacks) WorldGap() string { return f.deck().text(fbWorldGap, nil) }
|
||||||
|
|
||||||
// Variants returns every line the file can produce, for the persona scorer.
|
// Variants returns every line the file can produce, for the persona scorer.
|
||||||
// Order is stable so a failure names the same variant twice running.
|
func (f *Fallbacks) Variants() []string { return f.deck().variants() }
|
||||||
func (f *Fallbacks) Variants() []string {
|
|
||||||
var out []string
|
|
||||||
for _, k := range fbKeys {
|
|
||||||
out = append(out, f.file.Entries[k].Variants...)
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
// The process-wide instance. Package-level because these lines are needed on
|
// The process-wide instance. Package-level because these lines are needed on
|
||||||
// paths that have no phraser to hand — cmd/mavend names the world gap without
|
// paths that have no phraser to hand — cmd/mavend names the world gap without
|
||||||
@@ -208,31 +126,13 @@ func SourcesFallback(sources string) string { return DefaultFallbacks().FromSour
|
|||||||
// WorldGap — what he hears when the world model is configured and unreachable.
|
// WorldGap — what he hears when the world model is configured and unreachable.
|
||||||
func WorldGap() string { return DefaultFallbacks().WorldGap() }
|
func WorldGap() string { return DefaultFallbacks().WorldGap() }
|
||||||
|
|
||||||
// matches reports whether text is a line the given entry could have produced.
|
|
||||||
// A caller that has to recognise a fallback cannot compare against one literal
|
|
||||||
// any more, because the entry picks between variants.
|
|
||||||
func (f *Fallbacks) matches(key, sources, text string) bool {
|
|
||||||
if strings.ReplaceAll(hardFloor[key], "{sources}", sources) == text {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if f == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, v := range f.file.Entries[key].Variants {
|
|
||||||
if strings.ReplaceAll(v, "{sources}", sources) == text {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUnknownFallback reports whether text is one of her "I do not know" lines.
|
// IsUnknownFallback reports whether text is one of her "I do not know" lines.
|
||||||
// The daemon tests read it to tell an answer from a shrug.
|
// The daemon tests read it to tell an answer from a shrug.
|
||||||
func IsUnknownFallback(text string) bool {
|
func IsUnknownFallback(text string) bool {
|
||||||
return DefaultFallbacks().matches(fbQueryUnknown, "", text)
|
return DefaultFallbacks().deck().matches(fbQueryUnknown, nil, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSourcesFallback reports whether text is sources read back verbatim.
|
// IsSourcesFallback reports whether text is sources read back verbatim.
|
||||||
func IsSourcesFallback(text, sources string) bool {
|
func IsSourcesFallback(text, sources string) bool {
|
||||||
return DefaultFallbacks().matches(fbQuerySources, sources, text)
|
return DefaultFallbacks().deck().matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user