Deployment
Automated deployment update
4b1daed
Raw
History Blame Contribute Delete
8.9 kB
// 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
}