6316354518
The day total rolls over at midnight and the poller had nothing to write until the first spend of the new day, so at 09:00 the latest money_today fact was yesterday's spending and looked perfectly fresh. The value now carries the first instant of the window it covers, and a today question that the stored window does not cover is refused rather than answered with yesterday's number. Staleness was measured off the fact timestamp, which only moved when the figure moved, so a quiet month was reported as data from three days ago while being current. The value now carries when it was last read and the poller writes on every read. Amounts in an instrument the window diff never named were spoken with a numeric instrument id as the currency. Instruments are resolved from one cursor-zero diff, cached for the process, and an amount still unnamed is dropped from speech rather than recited wrongly. "сколько я потратил вчера" was answered with the month total, a real number to a different question, and is now refused by naming the two windows she keeps. Income questions led with the spending. Found in review of #62.
105 lines
4.1 KiB
Go
105 lines
4.1 KiB
Go
package router
|
||
|
||
import "strings"
|
||
|
||
// Money questions, matched deterministically (Vikunja #125).
|
||
//
|
||
// No new intent, for the same reason as tasks: the intent enum is a contract
|
||
// with the relabelling prompt. "сколько я потратил?" is a query; which figure
|
||
// it asks for is a lookup, not something to ask a 1.7B — and a model asked to
|
||
// invent a spending total will happily do it.
|
||
|
||
// MoneyWindow — which period a money question asks about.
|
||
type MoneyWindow int
|
||
|
||
const (
|
||
MoneyNone MoneyWindow = iota
|
||
MoneyToday
|
||
MoneyMonth
|
||
// MoneyUnsupported — a money question over a window nothing is stored for
|
||
// ("вчера", "на прошлой неделе"). Claimed, not answered: the poller keeps
|
||
// today and the month, and answering a question about yesterday with the
|
||
// month-to-date total is worse than saying she does not keep it.
|
||
MoneyUnsupported
|
||
)
|
||
|
||
// MoneyQuery — a parsed money question. Income is set when he asked what he
|
||
// EARNED rather than what he spent; the two read the same fact and differ only
|
||
// in which half of it leads the answer.
|
||
type MoneyQuery struct {
|
||
Window MoneyWindow
|
||
Income bool
|
||
}
|
||
|
||
// incomeNouns — the words that make a money question be about income.
|
||
var incomeNouns = []string{"заработал", "заработала", "получил", "доход", "доходы", "earned", "income"}
|
||
|
||
// moneyNouns — the words that make a question be about his money.
|
||
var moneyNouns = []string{
|
||
"потратил", "потратила", "тратил", "траты", "трат", "расходы", "расходов",
|
||
"заработал", "заработала", "доход", "доходы", "потрачено", "денег",
|
||
"spend", "spent", "expenses", "earned", "income",
|
||
}
|
||
|
||
// ParseMoneyQuery reports whether an utterance asks about spending or income,
|
||
// and over which window. Defaults to the month: "сколько я потратил?" without a
|
||
// period is the month-to-date question, which is the one worth answering.
|
||
//
|
||
// Narrow on purpose. A money noun alone is not enough — "я потратил весь день
|
||
// на это" is him talking about his day, so an amount word or an explicit
|
||
// question word has to be there too. The two evidence halves are INDEPENDENT:
|
||
// "траты" and "расходы" used to sit in both lists, so either word alone
|
||
// satisfied the whole gate and "у меня в этом месяце большие траты", a
|
||
// statement, came back with a figure.
|
||
func ParseMoneyQuery(text string) (MoneyQuery, bool) {
|
||
toks := planTokens(text)
|
||
if len(toks) == 0 {
|
||
return MoneyQuery{}, false
|
||
}
|
||
hasNoun := false
|
||
for _, t := range toks {
|
||
for _, n := range moneyNouns {
|
||
if t == n {
|
||
hasNoun = true
|
||
}
|
||
}
|
||
}
|
||
if !hasNoun {
|
||
return MoneyQuery{}, false
|
||
}
|
||
// "весь день", "время", "силы" — spending that is not money.
|
||
for _, t := range toks {
|
||
switch t {
|
||
case "день", "дня", "время", "времени", "силы", "сил", "нервы":
|
||
return MoneyQuery{}, false
|
||
}
|
||
}
|
||
asking := hasTok(toks, "сколько") || hasTok(toks, "какие") || hasTok(toks, "покажи") ||
|
||
hasTok(toks, "how") || hasTok(toks, "much") || hasTok(toks, "my") ||
|
||
hasTok(toks, "мои")
|
||
if !asking {
|
||
return MoneyQuery{}, false
|
||
}
|
||
income := false
|
||
for _, t := range toks {
|
||
for _, n := range incomeNouns {
|
||
if t == n {
|
||
income = true
|
||
}
|
||
}
|
||
}
|
||
lower := strings.ToLower(text)
|
||
switch {
|
||
// Windows nothing is stored for, named explicitly so they are refused
|
||
// rather than silently answered with the month.
|
||
case hasTok(toks, "вчера") || hasTok(toks, "позавчера") || strings.Contains(lower, "yesterday"),
|
||
hasTok(toks, "неделю") || hasTok(toks, "неделе") || hasTok(toks, "неделя") ||
|
||
strings.Contains(lower, "week"),
|
||
hasTok(toks, "год") || hasTok(toks, "году") || strings.Contains(lower, "year"):
|
||
return MoneyQuery{Window: MoneyUnsupported, Income: income}, true
|
||
case hasTok(toks, "сегодня") || strings.Contains(lower, "today"):
|
||
return MoneyQuery{Window: MoneyToday, Income: income}, true
|
||
}
|
||
return MoneyQuery{Window: MoneyMonth, Income: income}, true
|
||
}
|