Spaces:
Build error
Build error
| // 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 | |
| } | |