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" "github.com/kami/maven/internal/say" ) //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 = map[string]string{ fbChat: "даже не знаю, что сказать.", fbQueryUnknown: "не знаю.", fbQuerySources: "вот что я нашла: {sources}", fbWorldGap: "сейчас не могу ответить — большая модель недоступна, а придумывать не хочу.", } // Fallbacks picks a hand-written Russian fallback line. Safe for concurrent use. type Fallbacks struct{ d *say.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 := say.Load(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() *say.Deck { if f == nil { return say.FloorDeck(hardFloor) } 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) }