Files
Maven/internal/pattern/extractor.go
T
kami 4c40a183cf feat: event store, pattern inference, and routine proposals
- 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.
2026-07-10 15:49:03 +04:00

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