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" ) //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. // These are the exact strings that lived in Go before this file existed. var queryFloor = registerFloor(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} градусов, {condition}.", QueryWeatherOff: "погода не настроена.", QueryWeatherWhere: "не знаю, для какого города — задай voice.weather.default_location или назови город.", QueryNetEmpty: "в сети никого не нашла{tail}.", QueryFailPlan: "не получилось собрать план.", QueryFailNotes: "не получилось посмотреть записи.", QueryFailFeeds: "не получилось посмотреть ленты.", QueryFailCalendar: "не получилось проверить календарь.", QueryFailWeather: "не получилось узнать погоду.", QueryFailAnswer: "не получилось найти ответ.", QueryFailPage: "не получилось прочитать страницу.", QueryFailNetscan: "не получилось просканировать сеть.", }) // Queries picks a hand-written Russian query line. Safe for concurrent use. type Queries struct{ d *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 := loadDeck(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, "{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() *deck { if q == nil { return nil } 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) }