4c40a183cf
- Add events table (migration #4): stores normalized (action, object, ts) triples extracted from facts, indexed for recurrence detection. - Add proposed_routines table: stores inferred recurring patterns with proposed/accepted/dismissed status and optional linked reminder. - Add pattern package: Extractor normalizes fact text into (action, object) pairs with TTS normalization; Detector groups events to find recurring patterns and proposes routines. - Add internal/ttsnorm: text normalization pipeline for Russian/English (lowercase, punctuation strip, number normalization, stopword removal). - Add chat seed file for LLM phraser.
86 lines
3.0 KiB
Go
86 lines
3.0 KiB
Go
// Package pattern extracts normalized events from facts and detects recurring
|
|
// patterns to propose as routines (recurring reminders).
|
|
//
|
|
// The pipeline: fact write → extractor (action+object) → detector (intervals) →
|
|
// proposed routine → human confirms → recurring reminder.
|
|
package pattern
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Event is a normalized, derived observation — the output of the extractor
|
|
// and the input to the pattern detector.
|
|
type Event struct {
|
|
FactID int64
|
|
Action string // normalized action, e.g. "refill"
|
|
Object string // normalized object, e.g. "cat_water_fountain"
|
|
Ts time.Time
|
|
}
|
|
|
|
// actionLexicon maps observed value words to canonical actions. The key is the
|
|
// canonical form; the values are observed inflections/alternatives (lowercase).
|
|
// Pure additive — unrecognized values silently produce no event (false
|
|
// negative), which is harmless.
|
|
var actionLexicon = map[string][]string{
|
|
"refill": {"refilled", "refill", "fills", "filling", "заправил", "налил", "долил", "пополнил"},
|
|
"feed": {"fed", "feed", "feeds", "feeding", "покормил", "кормил", "покормить"},
|
|
"change": {"changed", "change", "changes", "changing", "поменял", "сменил", "заменил"},
|
|
"clean": {"cleaned", "clean", "cleans", "cleaning", "почистил", "убрал", "убирал", "помыл", "мыл"},
|
|
"take": {"took", "take", "takes", "taking", "принял", "выпил", "пил", "съел", "ел"},
|
|
"walk": {"walked", "walk", "walks", "walking", "гулял", "выгулял", "прогулка"},
|
|
"water": {"watered", "water", "waters", "watering", "полил", "поливал"},
|
|
}
|
|
|
|
// invertLexicon builds a fast map from any variant → canonical action.
|
|
var variantToAction map[string]string
|
|
|
|
func init() {
|
|
variantToAction = make(map[string]string)
|
|
for canonical, variants := range actionLexicon {
|
|
for _, v := range variants {
|
|
variantToAction[v] = canonical
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extract attempts to normalize a fact (key + value) into an Event.
|
|
// Returns nil when the fact doesn't describe a recognizable action —
|
|
// structured JSON values, empty values, and unrecognized actions all
|
|
// produce no event (false negative by design).
|
|
//
|
|
// Rules:
|
|
// - If value starts with '{' or '[' (JSON), skip — it's structured data,
|
|
// not an action statement.
|
|
// - The value (trimmed, lowered) is looked up in the action lexicon.
|
|
// - The key (trimmed, lowered) becomes the object.
|
|
// - Both action and object must be non-empty.
|
|
func Extract(factID int64, key, value string, ts time.Time) *Event {
|
|
v := strings.TrimSpace(value)
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
// Skip structured JSON values — measurements, config, etc.
|
|
if v[0] == '{' || v[0] == '[' {
|
|
return nil
|
|
}
|
|
|
|
action, ok := variantToAction[strings.ToLower(v)]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
object := strings.TrimSpace(strings.ToLower(key))
|
|
if object == "" {
|
|
return nil
|
|
}
|
|
|
|
return &Event{
|
|
FactID: factID,
|
|
Action: action,
|
|
Object: object,
|
|
Ts: ts,
|
|
}
|
|
}
|