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 }