// 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, } }