package main import ( "context" "fmt" "log" "strings" "time" ) // Command history — "что я тебе говорил?", "что ты записала сегодня?" // (Vikunja #456). // // Read-only over the facts that already exist. No new mechanism and no new // storage: everything he tapped in is already a row with a source and a // timestamp, and this only reads them back. // historyMarkers — the ways he asks what he told her. Each entry is a pair of // substrings that must BOTH appear, because either half alone is a different // question: "что я говорил про сервер" is a recall question the notes pass // answers better, and "что ты записала" with no "что" is not a question at all. var historyMarkers = [][2]string{ {"что я", "говорил"}, {"что я", "сказал"}, {"что я", "рассказ"}, {"что ты", "записал"}, {"что ты", "запомнил"}, {"что я", "отмечал"}, {"что я", "отметил"}, {"what did i", "tell"}, {"what did you", "record"}, } // historyRecall — the word that turns a history question into a recall // question. "что я говорил про сервер" names a topic, and the notes pass // answers a topic far better than a list of the last five facts does. var historyRecall = []string{" про ", " об ", " о ", " about "} // isHistoryQuery reports whether he is asking what he told her. func isHistoryQuery(u string) bool { s := " " + strings.ToLower(strings.TrimSpace(u)) + " " if s == " " { return false } for _, r := range historyRecall { if strings.Contains(s, r) { return false } } for _, pair := range historyMarkers { if strings.Contains(s, pair[0]) && strings.Contains(s, pair[1]) { return true } } return false } // historyScan — how many recent facts are read before filtering. Deliberately // larger than historyReadOut: a poller writing every few minutes would // otherwise push everything he said out of the window, the same way his own // notes used to crowd out the feed headlines. const historyScan = 100 // historyReadOut — how many she says out loud. Five is what fits in one spoken // breath; the rest are on /history, which is the surface for reading a list. const historyReadOut = 5 // historyWindow — how far back "recently" reaches. A day, because the question // is about this conversation and not about the archive. const historyWindow = 24 * time.Hour // queryHistory answers what he told her, from the facts he tapped in. // // Only "tap:" sources. A fact written by a poller, an inference or the ambient // relay is a thing she learned, not a thing he said, and reading those back // under "что я тебе говорил?" would put words in his mouth. // // Placed with the other sources that read his own rows and above the recall // pass: the notes pass would otherwise answer this from whatever note happens // to be nearest, which reads as an answer and is not one. func (h *reactiveHandler) queryHistory(ctx context.Context, t *queryTurn) (string, bool) { if !isHistoryQuery(t.dec.Utterance) { return "", false } facts, err := h.api.RecentFacts(ctx, historyScan) if err != nil { log.Printf("voice: history: recent facts: %v", err) return "не получилось посмотреть, что ты говорил.", true } cutoff := h.now().Add(-historyWindow) var said []string for _, f := range facts { if !strings.HasPrefix(f.Source, "tap:") || f.Ts.Before(cutoff) { continue } said = append(said, historyLine(f.Key, f.Value, f.Ts)) if len(said) == historyReadOut { break } } if len(said) == 0 { // Claim the turn rather than fall through. "ничего не говорил" is the // true answer, and recall would answer it with an old note instead. return "за последние сутки ты мне ничего такого не говорил.", true } return "ты говорил: " + strings.Join(said, "; "), true } // historyLine — one fact as she says it. The hour and minute, because the day // is already bounded by historyWindow and a date would be noise. func historyLine(key, value string, ts time.Time) string { what := key if value != "" { what = key + " — " + value } return fmt.Sprintf("%s (%s)", what, ts.Local().Format("15:04")) }