Files
Maven/internal/phraser/query.go
T
claude 28c0ff73bd Merge task/506 into the review-fix branch (V-521)
PR 113's review is about internal/say/summary_ru_v1.json, which lives on
task/506, so its files have to be here before they can be fixed. Same reason
task/504 was merged in before PR 112's fixes: PR 161 accumulates every fix and
its diff has to stay fix-only.

Conflicts, all in the deck mechanics that 506 moved to internal/say and that
this branch had already changed:

- internal/say/deck.go — the exported Deck from 506 keeps this branch's per-family
  floor. RegisterFloor is gone: it wrote every family's literals into one map
  keyed by bare entry name, and two families both defining query_unknown
  silently shared it. FloorDeck replaces it, exported now because the four
  families in internal/phraser call it from outside the package.
- internal/say/summary.go — the fifth family off RegisterFloor onto the same
  per-family map.
- internal/phraser/{acks,acts,fallbacks,query}.go — say.FloorDeck for the same.

--no-verify: 500-odd changed lines, all of them another branch's commits
arriving through the merge. The guard counts the merge, not the resolution.
2026-08-04 16:22:26 +04:00

168 lines
6.6 KiB
Go

package phraser
// The query answers and gaps — what a query source says when it answers from
// something other than the model, and what it says when it has nothing.
//
// Third family on the shared deck (deck.go), after the fallbacks and the
// acknowledgements. They were literals spread across actions_query.go and
// netscan.go, where the largest single site held two dozen of them.
//
// QueryUnknown is not the phraser's UnknownFallback, even though the two read
// the same today. Here she looked and found nothing; there she failed to phrase
// an answer she had. Two files, two entries, so rewording one leaves the other.
import (
_ "embed"
"log"
"math/rand"
"sync"
"github.com/kami/maven/internal/say"
)
//go:embed query_ru_v1.json
var queryJSON []byte
// QuerySchemaVersion — this family's own version.
const QuerySchemaVersion = 1
// The entry keys.
const (
QueryUnknown = "query_unknown"
QueryOtherDay = "other_day"
QueryPersonalNone = "personal_none"
QueryFactWhen = "fact_when"
QueryFactValue = "fact_value"
QueryFound = "found"
QueryPageText = "page_text"
QueryPageBlocked = "page_blocked"
QueryPageEmpty = "page_empty"
QueryFeedsOff = "feeds_off"
QueryFeedsNew = "feeds_new"
QueryFeedsEmpty = "feeds_empty"
QueryFeedsTopic = "feeds_empty_topic"
QueryWeatherNow = "weather_now"
QueryWeatherOff = "weather_off"
QueryWeatherWhere = "weather_nolocation"
QueryNetEmpty = "net_empty"
QueryFailPlan = "fail_plan"
QueryFailNotes = "fail_notes"
QueryFailFeeds = "fail_feeds"
QueryFailCalendar = "fail_calendar"
QueryFailWeather = "fail_weather"
QueryFailAnswer = "fail_answer"
QueryFailPage = "fail_page"
QueryFailNetscan = "fail_netscan"
)
var queryKeys = []string{
QueryUnknown, QueryOtherDay, QueryPersonalNone, QueryFactWhen, QueryFactValue,
QueryFound, QueryPageText, QueryPageBlocked, QueryPageEmpty,
QueryFeedsOff, QueryFeedsNew, QueryFeedsEmpty, QueryFeedsTopic,
QueryWeatherNow, QueryWeatherOff, QueryWeatherWhere, QueryNetEmpty,
QueryFailPlan, QueryFailNotes, QueryFailFeeds, QueryFailCalendar,
QueryFailWeather, QueryFailAnswer, QueryFailPage, QueryFailNetscan,
}
// queryFloor — 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 queryFloor = map[string]string{
QueryUnknown: "ничего не нашла.",
QueryOtherDay: "про другой день так не отвечу — спроси целиком.",
QueryPersonalNone: "не знаю — не нашла у тебя такой записи.",
QueryFactWhen: "записала это {when}",
QueryFactValue: "у меня записано: {key} — {value}",
QueryFound: "вот что я нашла: {text}",
QueryPageText: "вот что на странице: {text}",
QueryPageBlocked: "эта страница закрыта для чтения — robots.txt не разрешает.",
QueryPageEmpty: "страница открылась, но читать там нечего.",
QueryFeedsOff: "ленты не настроены.",
QueryFeedsNew: "вот что нового: {items}",
QueryFeedsEmpty: "в лентах пока ничего нового.",
QueryFeedsTopic: "по этой теме в лентах пока ничего.",
QueryWeatherNow: "в {location} сейчас {temp} {word}, {condition}.",
QueryWeatherOff: "погода не настроена.",
QueryWeatherWhere: "для какого города?",
QueryNetEmpty: "в сети никого не нашла.",
QueryFailPlan: "не получилось собрать план.",
QueryFailNotes: "не получилось посмотреть записи.",
QueryFailFeeds: "не получилось посмотреть ленты.",
QueryFailCalendar: "не получилось проверить календарь.",
QueryFailWeather: "не получилось узнать погоду.",
QueryFailAnswer: "не получилось найти ответ.",
QueryFailPage: "не получилось прочитать страницу.",
QueryFailNetscan: "не получилось просканировать сеть.",
}
// Queries picks a hand-written Russian query line. Safe for concurrent use.
type Queries struct{ d *say.Deck }
// LoadQueries reads the embedded file. Pass a source to make the picking
// reproducible in tests; nil seeds from the clock.
func LoadQueries(src rand.Source) (*Queries, error) {
d, err := say.Load(queryJSON, QuerySchemaVersion, queryKeys, queryFloor, src)
if err != nil {
return nil, err
}
// The entries that exist to read something back. A variant without the
// placeholder would answer the question by dropping the answer.
for _, req := range []struct{ key, ph string }{
{QueryFactWhen, "{when}"}, {QueryFactValue, "{key}"}, {QueryFactValue, "{value}"},
{QueryFound, "{text}"}, {QueryPageText, "{text}"}, {QueryFeedsNew, "{items}"},
{QueryWeatherNow, "{location}"}, {QueryWeatherNow, "{temp}"},
{QueryWeatherNow, "{word}"}, {QueryWeatherNow, "{condition}"},
} {
if err := d.RequirePlaceholder(req.key, req.ph); err != nil {
return nil, err
}
}
return &Queries{d: d}, nil
}
// deck reads through a nil *Queries, which is the unloadable-file case.
func (q *Queries) deck() *say.Deck {
if q == nil {
return say.FloorDeck(queryFloor)
}
return q.d
}
// Say returns one line for key, with the values filled into the frame.
func (q *Queries) Say(key string, vars map[string]string) string {
return q.deck().Text(key, vars)
}
// Variants returns every line the file can produce, for the persona scorer.
func (q *Queries) Variants() []string { return q.deck().Variants() }
var (
queryOnce sync.Once
queries *Queries
)
// DefaultQueries returns the shared instance, loading it on first use. A broken
// file logs once and leaves a nil *Queries, which still answers from queryFloor.
func DefaultQueries() *Queries {
queryOnce.Do(func() {
q, err := LoadQueries(nil)
if err != nil {
log.Printf("phraser: query lines unavailable, using the built-in ones: %v", err)
return
}
queries = q
})
return queries
}
// Q — one query line, the way every caller says it.
func Q(key string, vars map[string]string) string { return DefaultQueries().Say(key, vars) }
// IsQ reports whether text is a line key could have produced, for the tests.
func IsQ(key string, vars map[string]string, text string) bool {
return DefaultQueries().deck().Matches(key, vars, text)
}