feca776077
Two defects in the deck, both of which reach him as a broken answer.
An optional placeholder had no rule. net_empty carries {tail} for the case
where a scan stopped short of the whole range, and a scan that finished has
nothing to put there — so the answer went out with the braces in it, or with
nothing at all if the variant was all placeholder. The picker now narrows to
the variants this call can actually fill, and prefers, among those, the ones
using the most of what the caller supplied, so a caveat he was given is never
dropped for a shorter wording. Nothing fillable still says the line, because a
visible placeholder beats silence.
The floor literals lived in one global map keyed by bare entry name, and two
families both define an entry called query_unknown: the query answers, where
she looked and found nothing, and the phrasing fallbacks, where she failed to
say an answer she had. Whichever registered last answered for both, so the
distinction those two files exist for disappeared exactly when a file failed to
load. Each family now carries its own map, and an unloadable file leaves a
floor-only deck behind instead of a nil one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XGTGCWX33aX8SMBSRz9VmS
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 = 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 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)
|
|
}
|