139 lines
5.4 KiB
Go
139 lines
5.4 KiB
Go
package phraser
|
|
|
|
// The phrasing fallbacks — what she says when the model gave her nothing usable.
|
|
//
|
|
// 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
|
|
// rewording one was a Go edit, a rebuild and a redeploy for what is product copy.
|
|
//
|
|
// 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
|
|
// words she has: every accessor falls back to the literal it replaced.
|
|
|
|
import (
|
|
_ "embed"
|
|
"log"
|
|
"math/rand"
|
|
"sync"
|
|
)
|
|
|
|
//go:embed fallbacks_ru_v1.json
|
|
var fallbackJSON []byte
|
|
|
|
// FallbackSchemaVersion — the version this code understands. Its own constant,
|
|
// not shared with the nudge templates or the eval fixtures: two files that change
|
|
// on different days cannot be versioned by one number (Vikunja #397).
|
|
const FallbackSchemaVersion = 1
|
|
|
|
// The entry keys. Every one of them is read by a method below, so a typo in the
|
|
// file is caught at load rather than at the moment she needs the words.
|
|
const (
|
|
fbChat = "chat"
|
|
fbQueryUnknown = "query_unknown"
|
|
fbQuerySources = "query_sources"
|
|
fbWorldGap = "world_gap"
|
|
)
|
|
|
|
// fbKeys — every key the code requires the file to define.
|
|
var fbKeys = []string{fbChat, fbQueryUnknown, fbQuerySources, fbWorldGap}
|
|
|
|
// 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.
|
|
var hardFloor = registerFloor(map[string]string{
|
|
fbChat: "даже не знаю, что сказать.",
|
|
fbQueryUnknown: "не знаю.",
|
|
fbQuerySources: "вот что я нашла: {sources}",
|
|
fbWorldGap: "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.",
|
|
})
|
|
|
|
// Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use.
|
|
type Fallbacks struct{ d *deck }
|
|
|
|
// LoadFallbacks reads the embedded file. Pass a source to make the picking
|
|
// reproducible in tests; nil seeds from the clock.
|
|
func LoadFallbacks(src rand.Source) (*Fallbacks, error) {
|
|
d, err := loadDeck(fallbackJSON, FallbackSchemaVersion, fbKeys, hardFloor, src)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// query_sources is the one entry whose whole job is to read something back.
|
|
if err := d.requirePlaceholder(fbQuerySources, "{sources}"); err != nil {
|
|
return nil, err
|
|
}
|
|
return &Fallbacks{d: d}, nil
|
|
}
|
|
|
|
// deck reads through a nil *Fallbacks, which is the unloadable-file case.
|
|
func (f *Fallbacks) deck() *deck {
|
|
if f == nil {
|
|
return nil
|
|
}
|
|
return f.d
|
|
}
|
|
|
|
// Chat — nothing usable came back on the chat path.
|
|
func (f *Fallbacks) Chat() string { return f.deck().text(fbChat, nil) }
|
|
|
|
// Unknown — a question she cannot answer and will not guess at.
|
|
func (f *Fallbacks) Unknown() string { return f.deck().text(fbQueryUnknown, nil) }
|
|
|
|
// FromSources — read back what she was handed, because phrasing it failed.
|
|
func (f *Fallbacks) FromSources(sources string) string {
|
|
return f.deck().text(fbQuerySources, map[string]string{"sources": sources})
|
|
}
|
|
|
|
// 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
|
|
// would let "the big model is asleep" drift into "I don't know".
|
|
func (f *Fallbacks) WorldGap() string { return f.deck().text(fbWorldGap, nil) }
|
|
|
|
// Variants returns every line the file can produce, for the persona scorer.
|
|
func (f *Fallbacks) Variants() []string { return f.deck().variants() }
|
|
|
|
// 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
|
|
// one — and because a template file that is embedded and validated at load has
|
|
// nothing per-instance to configure.
|
|
var (
|
|
fallbackOnce sync.Once
|
|
fallbacks *Fallbacks
|
|
)
|
|
|
|
// DefaultFallbacks returns the shared instance, loading it on first use. A
|
|
// broken file logs once and leaves a nil *Fallbacks, which still answers from
|
|
// hardFloor — a daemon must not fail to boot over its own copy deck.
|
|
func DefaultFallbacks() *Fallbacks {
|
|
fallbackOnce.Do(func() {
|
|
fb, err := LoadFallbacks(nil)
|
|
if err != nil {
|
|
log.Printf("phraser: fallbacks unavailable, using the built-in lines: %v", err)
|
|
return
|
|
}
|
|
fallbacks = fb
|
|
})
|
|
return fallbacks
|
|
}
|
|
|
|
// ChatFallback — what she says when the chat path produced nothing.
|
|
func ChatFallback() string { return DefaultFallbacks().Chat() }
|
|
|
|
// UnknownFallback — what she says when she has no answer and will not invent one.
|
|
func UnknownFallback() string { return DefaultFallbacks().Unknown() }
|
|
|
|
// SourcesFallback — read the sources back rather than ship a broken fragment.
|
|
func SourcesFallback(sources string) string { return DefaultFallbacks().FromSources(sources) }
|
|
|
|
// WorldGap — what he hears when the world model is configured and unreachable.
|
|
func WorldGap() string { return DefaultFallbacks().WorldGap() }
|
|
|
|
// 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.
|
|
func IsUnknownFallback(text string) bool {
|
|
return DefaultFallbacks().deck().matches(fbQueryUnknown, nil, text)
|
|
}
|
|
|
|
// IsSourcesFallback reports whether text is sources read back verbatim.
|
|
func IsSourcesFallback(text, sources string) bool {
|
|
return DefaultFallbacks().deck().matches(fbQuerySources, map[string]string{"sources": sources}, text)
|
|
}
|