// Package memory provides temporal context tracking with recency-weighted scoring. package memory import ( "math" "sort" "time" ) // TemporalContext provides time-aware context for memory retrieval. // It tracks temporal relationships and applies recency-based scoring. type TemporalContext struct { // UserID identifies the user UserID string // SessionID identifies the current session SessionID string // QueryTime is when the query was issued QueryTime time.Time // LastInteraction is the time of the last user interaction LastInteraction time.Time // Timezone is the user's timezone Timezone string // RecencyBias is the exponential decay factor (lambda) // Higher values = faster decay = stronger preference for recent // Typical range: 0.01 (slow decay) to 0.5 (fast decay) RecencyBias float64 // TemporalAnchors are specific time points of interest TemporalAnchors []time.Time // TimeOfDay context (morning, afternoon, evening, night) TimeOfDay string // DayOfWeek for weekly patterns DayOfWeek time.Weekday // SeasonalContext for seasonal patterns SeasonalContext string } // NewTemporalContext creates a new temporal context func NewTemporalContext(userID, sessionID string) *TemporalContext { now := time.Now() return &TemporalContext{ UserID: userID, SessionID: sessionID, QueryTime: now, LastInteraction: now, RecencyBias: 0.1, // Default decay rate TimeOfDay: getTimeOfDay(now), DayOfWeek: now.Weekday(), } } // CalculateRecencyScore computes a recency-weighted relevance score // using exponential decay: score = confidence * e^(-λ * age_hours) func (tc *TemporalContext) CalculateRecencyScore(entry *MemoryEntry) float64 { age := tc.QueryTime.Sub(entry.Timestamp).Hours() // Prevent negative ages (future timestamps) if age < 0 { age = 0 } // Exponential decay: score = e^(-λ * age) recencyScore := math.Exp(-tc.RecencyBias * age) // Combine with confidence return entry.Confidence * recencyScore } // CalculateCombinedScore combines multiple scoring factors func (tc *TemporalContext) CalculateCombinedScore(entry *MemoryEntry, similarityScore float64) float64 { recencyScore := tc.CalculateRecencyScore(entry) // Weighted combination // similarity: 0.6, recency: 0.3, confidence: 0.1 weights := struct { similarity float64 recency float64 confidence float64 }{0.6, 0.3, 0.1} combined := weights.similarity*similarityScore + weights.recency*recencyScore + weights.confidence*entry.Confidence return combined } // ApplyTemporalScoring applies recency scoring to a slice of entries func (tc *TemporalContext) ApplyTemporalScoring(entries []*MemoryEntry) []*MemoryEntry { for _, entry := range entries { entry.Score = tc.CalculateRecencyScore(entry) } return entries } // SortByRecency sorts entries by recency score (highest first) func (tc *TemporalContext) SortByRecency(entries []*MemoryEntry) []*MemoryEntry { tc.ApplyTemporalScoring(entries) sort.Slice(entries, func(i, j int) bool { return entries[i].Score > entries[j].Score }) return entries } // FilterByTimeRange filters entries to a specific time range func (tc *TemporalContext) FilterByTimeRange(entries []*MemoryEntry, start, end time.Time) []*MemoryEntry { var filtered []*MemoryEntry for _, entry := range entries { if entry.Timestamp.After(start) && entry.Timestamp.Before(end) { filtered = append(filtered, entry) } } return filtered } // FilterByMaxAge filters entries to those within maxAge of query time func (tc *TemporalContext) FilterByMaxAge(entries []*MemoryEntry, maxAge time.Duration) []*MemoryEntry { cutoff := tc.QueryTime.Add(-maxAge) var filtered []*MemoryEntry for _, entry := range entries { if entry.Timestamp.After(cutoff) { filtered = append(filtered, entry) } } return filtered } // GroupByTimePeriod groups entries by time period func (tc *TemporalContext) GroupByTimePeriod(entries []*MemoryEntry, period TimePeriod) map[string][]*MemoryEntry { groups := make(map[string][]*MemoryEntry) for _, entry := range entries { key := getTimePeriodKey(entry.Timestamp, period) groups[key] = append(groups[key], entry) } return groups } // TimePeriod represents a time grouping unit type TimePeriod int const ( PeriodHour TimePeriod = iota PeriodDay PeriodWeek PeriodMonth ) func getTimePeriodKey(t time.Time, period TimePeriod) string { switch period { case PeriodHour: return t.Format("2006-01-02-15") case PeriodDay: return t.Format("2006-01-02") case PeriodWeek: year, week := t.ISOWeek() return t.Format("2006") + "-W" + padInt(week, 2) + "-" + padInt(year, 4) case PeriodMonth: return t.Format("2006-01") default: return t.Format("2006-01-02") } } func padInt(n, width int) string { s := "" for i := 0; i < width; i++ { s = "0" + s } return s[len(s)-width:] } func getTimeOfDay(t time.Time) string { hour := t.Hour() switch { case hour >= 5 && hour < 12: return "morning" case hour >= 12 && hour < 17: return "afternoon" case hour >= 17 && hour < 21: return "evening" default: return "night" } } // TemporalPattern represents a detected temporal pattern type TemporalPattern struct { // PatternType describes the pattern // Values: "daily", "weekly", "hourly", "seasonal" PatternType string // Description explains the pattern Description string // Confidence in the pattern Confidence float64 // Frequency of occurrence Frequency int // TimeSlots are the typical times this pattern occurs TimeSlots []string // AssociatedTags are commonly associated with this pattern AssociatedTags []string } // DetectPatterns analyzes entries for temporal patterns func (tc *TemporalContext) DetectPatterns(entries []*MemoryEntry) []TemporalPattern { if len(entries) < 5 { return nil // Need minimum entries for pattern detection } var patterns []TemporalPattern // Detect daily patterns dailyGroups := tc.GroupByTimePeriod(entries, PeriodDay) if len(dailyGroups) >= 3 { patterns = append(patterns, tc.analyzeDailyPatterns(dailyGroups)) } // Detect hourly patterns hourlyDistribution := make(map[int]int) for _, entry := range entries { hour := entry.Timestamp.Hour() hourlyDistribution[hour]++ } if pattern := tc.analyzeHourlyPatterns(hourlyDistribution); pattern != nil { patterns = append(patterns, *pattern) } return patterns } func (tc *TemporalContext) analyzeDailyPatterns(groups map[string][]*MemoryEntry) TemporalPattern { avgPerDay := 0 for _, entries := range groups { avgPerDay += len(entries) } avgPerDay /= len(groups) return TemporalPattern{ PatternType: "daily", Description: "Regular daily interaction pattern detected", Confidence: 0.7, Frequency: avgPerDay, } } func (tc *TemporalContext) analyzeHourlyPatterns(distribution map[int]int) *TemporalPattern { if len(distribution) < 3 { return nil } // Find peak hours maxCount := 0 peakHours := []string{} for hour, count := range distribution { if count > maxCount { maxCount = count peakHours = []string{getHourLabel(hour)} } else if count == maxCount { peakHours = append(peakHours, getHourLabel(hour)) } } return &TemporalPattern{ PatternType: "hourly", Description: "Peak activity hours detected", Confidence: 0.65, TimeSlots: peakHours, } } func getHourLabel(hour int) string { if hour == 0 { return "12am" } else if hour < 12 { return string(rune('0'+hour%10)) + "am" } else if hour == 12 { return "12pm" } else { return string(rune('0'+(hour-12)%10)) + "pm" } } // TimeAwareRanker combines temporal and semantic ranking type TimeAwareRanker struct { temporal *TemporalContext recencyWeight float64 similarityWeight float64 confidenceWeight float64 } // NewTimeAwareRanker creates a new time-aware ranker func NewTimeAwareRanker(temporal *TemporalContext) *TimeAwareRanker { return &TimeAwareRanker{ temporal: temporal, recencyWeight: 0.3, similarityWeight: 0.6, confidenceWeight: 0.1, } } // SetWeights configures the ranking weights func (r *TimeAwareRanker) SetWeights(recency, similarity, confidence float64) { total := recency + similarity + confidence r.recencyWeight = recency / total r.similarityWeight = similarity / total r.confidenceWeight = confidence / total } // Rank applies combined scoring and sorting func (r *TimeAwareRanker) Rank(entries []*MemoryEntry, similarityScores map[string]float64) []*MemoryEntry { for _, entry := range entries { similarity := 0.0 if s, ok := similarityScores[entry.ID]; ok { similarity = s } recency := r.temporal.CalculateRecencyScore(entry) entry.Score = r.recencyWeight*recency + r.similarityWeight*similarity + r.confidenceWeight*entry.Confidence } sort.Slice(entries, func(i, j int) bool { return entries[i].Score > entries[j].Score }) return entries }