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" QueryNetOff = "net_off" QueryPageOff = "page_off" 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, QueryNetOff, QueryPageOff, 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: "эта страница закрыта для чтения — сам сайт это запрещает.", QueryPageEmpty: "страница открылась, но читать там нечего.", QueryFeedsOff: "ленты не настроены.", QueryFeedsNew: "вот что нового: {items}", QueryFeedsEmpty: "в лентах пока ничего нового.", QueryFeedsTopic: "по этой теме в лентах пока ничего.", QueryWeatherNow: "в {location} сейчас {temp} {word}, {condition}.", QueryWeatherOff: "погода не настроена.", QueryWeatherWhere: "для какого города?", QueryNetEmpty: "в сети никого не нашла.", QueryNetOff: "сканирование сети не настроено.", QueryPageOff: "я не читаю страницы — это не настроено.", 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) }