c8444813e2
Behavioural memory, narrowed on purpose. internal/memory/behavior.go builds a profile out of self-facts — distinct days per weekday, median time of day — and reads it back in RU; router.ParseHabitQuery finds the weekday deterministically; a `habits` query source answers the question. Three things the plan doc asks for are deliberately absent, and the doc now records why: - The profile is COUNTED, not LLM-generated. A 1.7B asked to summarise a year of habits writes fluent claims about the owner's life that no row supports, and a wrong claim about him is the most expensive kind of wrong maven can be. - No cached profile fact, so no "update on fact write" machinery. It is recomputed on the question; a cache that can disagree with its own rows is two truths. - No proactive daily plan nudge. A dispatcher proposal at 08:00 every day is the definition of a nag. The path from "she noticed a pattern" to "she acts on it" already exists in internal/pattern with the proposal queue on /routines, and it goes through him. A one-off is not a habit: an activity needs two distinct days before she will call it usual, and until then she says she does not know yet. Only self-facts count — env rows are the world, config rows are her own tuning state. The typical time is a median so one 03:00 outlier cannot move a morning habit into the night. An unrecognised fact key is read back verbatim rather than glossed into something she made up. The source sits before "calendar" in querySources, and its matcher requires a habit marker, so "что я делаю в среду?" still reaches the calendar — answering a question about this coming Wednesday with a statistical average would be answering a different question. Verified: make build and make test both exit 0.
74 lines
3.0 KiB
Go
74 lines
3.0 KiB
Go
package router
|
|
|
|
import "time"
|
|
|
|
// Habit queries — "что я обычно делаю по вторникам?" (Vikunja #254).
|
|
//
|
|
// Deterministic matching, like the calendar and plan matchers: the LLM router
|
|
// classifies the intent, but WHICH weekday was asked about is a lookup, not a
|
|
// generation. A model that answers "по вторникам" for a question about Thursday
|
|
// gives a confidently wrong account of the owner's own life.
|
|
|
|
// HabitQuery — a parsed "what do I usually do" question. Weekday is set only
|
|
// when the utterance names one; otherwise the answer covers the whole week.
|
|
type HabitQuery struct {
|
|
Weekday time.Weekday
|
|
HasWeekday bool
|
|
}
|
|
|
|
// habitMarkers — the words that make a question about habit rather than about
|
|
// today. Without one of these, "что я делаю" is a question about right now, and
|
|
// the recall path owns it.
|
|
var habitMarkers = []string{
|
|
"обычно", "обычное", "чаще", "постоянно", "привычки", "привычка", "привычках",
|
|
"регулярно", "каждый", "каждую", "каждое", "usually", "habits", "habit",
|
|
"typically", "normally",
|
|
}
|
|
|
|
// weekdayWords — every form of a weekday name maven needs to recognise,
|
|
// including the "по …ам" plural the question is usually phrased in.
|
|
var weekdayWords = map[string]time.Weekday{
|
|
"понедельник": time.Monday, "понедельникам": time.Monday,
|
|
"вторник": time.Tuesday, "вторникам": time.Tuesday,
|
|
"среда": time.Wednesday, "среду": time.Wednesday, "средам": time.Wednesday,
|
|
"четверг": time.Thursday, "четвергам": time.Thursday,
|
|
"пятница": time.Friday, "пятницу": time.Friday, "пятницам": time.Friday,
|
|
"суббота": time.Saturday, "субботу": time.Saturday, "субботам": time.Saturday,
|
|
"воскресенье": time.Sunday, "воскресеньям": time.Sunday,
|
|
"monday": time.Monday, "mondays": time.Monday,
|
|
"tuesday": time.Tuesday, "tuesdays": time.Tuesday,
|
|
"wednesday": time.Wednesday, "wednesdays": time.Wednesday,
|
|
"thursday": time.Thursday, "thursdays": time.Thursday,
|
|
"friday": time.Friday, "fridays": time.Friday,
|
|
"saturday": time.Saturday, "saturdays": time.Saturday,
|
|
"sunday": time.Sunday, "sundays": time.Sunday,
|
|
}
|
|
|
|
// ParseHabitQuery reports whether an utterance asks what the owner usually
|
|
// does, and on which weekday if it names one.
|
|
//
|
|
// A habit marker is required. "что я делаю в среду?" without one is a question
|
|
// about this coming Wednesday — the calendar's job — and answering it with a
|
|
// statistical average would be answering a different question.
|
|
func ParseHabitQuery(text string) (HabitQuery, bool) {
|
|
toks := planTokens(text)
|
|
marked := false
|
|
for _, t := range toks {
|
|
for _, m := range habitMarkers {
|
|
if t == m {
|
|
marked = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if !marked {
|
|
return HabitQuery{}, false
|
|
}
|
|
for _, t := range toks {
|
|
if wd, ok := weekdayWords[t]; ok {
|
|
return HabitQuery{Weekday: wd, HasWeekday: true}, true
|
|
}
|
|
}
|
|
return HabitQuery{}, true
|
|
}
|