Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
relationship_evolution.goβ€’14.6 kB
// Package temporal - Relationship evolution tracking for dynamic graphs. // // RelationshipEvolution tracks how edge weights change over time using // KalmanVelocity filters. This enables: // - Detecting strengthening relationships (increasing co-access) // - Detecting weakening relationships (decreasing relevance) // - Predicting future relationship strength // - Identifying emerging connections // // Use cases: // - Recommend strengthening edges for pre-fetching // - Prune weakening edges to save memory // - Detect new relationship patterns automatically // - Power dynamic graph visualizations // // Example usage: // // re := temporal.NewRelationshipEvolution(temporal.DefaultRelationshipConfig()) // // // Record co-access (updates edge weight) // re.RecordCoAccess("node-1", "node-2", 1.0) // // // Get relationship trend // trend := re.GetTrend("node-1", "node-2") // fmt.Printf("Relationship is %s (velocity: %.3f)\n", trend.Direction, trend.Velocity) // // // Predict future strength // future := re.PredictStrength("node-1", "node-2", 10) // fmt.Printf("In 10 steps, strength will be: %.3f\n", future) // // # ELI12 (Explain Like I'm 12) // // Think about your friendships. Some get STRONGER over time, some fade: // // πŸ‘« Best friend in 1st grade β†’ Moved away β†’ Don't talk anymore (WEAKENING) // πŸ‘« New kid at school β†’ Hang out more β†’ Now best friends! (STRENGTHENING) // πŸ‘« Neighbor β†’ See them same amount β†’ Just neighbors (STABLE) // // RelationshipEvolution tracks how "connected" two things are, and whether // that connection is growing or shrinking. // // In a database, "relationships" are like friendships between data: // // πŸ“š "JavaScript" ←→ "React" : Strong connection (always accessed together) // πŸ“š "JavaScript" ←→ "Python": Weak connection (rarely together) // // The Kalman filter tracks the TREND: // // Week 1: JS+React accessed together 10 times // Week 2: JS+React accessed together 15 times // Week 3: JS+React accessed together 20 times // β†’ Velocity is POSITIVE! Relationship is STRENGTHENING! πŸ“ˆ // // Week 1: JS+Python accessed together 5 times // Week 2: JS+Python accessed together 3 times // Week 3: JS+Python accessed together 1 time // β†’ Velocity is NEGATIVE! Relationship is WEAKENING! πŸ“‰ // // Why this matters: // // βœ… STRENGTHENING relationships: Pre-fetch! If you access JS, also load React // ❌ WEAKENING relationships: Maybe delete the connection, save memory // 🌱 EMERGING relationships: "Ooh, this is new and growing fast - watch it!" // // The Kalman filter makes this smooth. One weird week doesn't change everything. // It looks for REAL TRENDS, not random noise. package temporal import ( "fmt" "sync" "time" "github.com/orneryd/nornicdb/pkg/filter" ) // RelationshipTrend represents the evolution trend of a relationship. type RelationshipTrend struct { // Direction: "strengthening", "weakening", "stable" Direction string // Velocity: rate of change (positive = strengthening) Velocity float64 // CurrentStrength: current filtered weight CurrentStrength float64 // PredictedStrength: predicted weight in 5 steps PredictedStrength float64 // Confidence: confidence in the trend (0-1) Confidence float64 // ObservationCount: number of weight updates ObservationCount int // LastUpdate: when the relationship was last updated LastUpdate time.Time } // RelationshipConfig holds configuration for relationship evolution tracking. type RelationshipConfig struct { // FilterConfig for the underlying Kalman velocity filter FilterConfig filter.VelocityConfig // MaxTrackedRelationships - maximum relationships to track (LRU eviction) MaxTrackedRelationships int // StrengthenThreshold - velocity above which is "strengthening" StrengthenThreshold float64 // WeakenThreshold - velocity below which is "weakening" WeakenThreshold float64 // MinObservationsForTrend - minimum observations before reporting trend MinObservationsForTrend int // DecayIdleRelationships - decay weight of idle relationships DecayIdleRelationships bool // IdleDecayRate - how much to decay per hour of inactivity IdleDecayRate float64 } // DefaultRelationshipConfig returns sensible defaults. func DefaultRelationshipConfig() RelationshipConfig { return RelationshipConfig{ FilterConfig: filter.VelocityConfig{ ProcessNoisePos: 0.01, ProcessNoiseVel: 0.001, MeasurementNoise: 0.1, InitialPosVariance: 1.0, InitialVelVariance: 0.1, Dt: 1.0, }, MaxTrackedRelationships: 100000, StrengthenThreshold: 0.01, WeakenThreshold: -0.01, MinObservationsForTrend: 3, DecayIdleRelationships: true, IdleDecayRate: 0.01, // 1% per hour } } // RelationshipEvolution tracks edge weight changes over time. type RelationshipEvolution struct { mu sync.RWMutex config RelationshipConfig // Edge trackers (edgeKey -> tracker) edges map[string]*edgeTracker // LRU ordering accessOrder []string // Statistics totalUpdates int64 startTime time.Time } // edgeTracker tracks a single relationship. type edgeTracker struct { sourceID string targetID string // Kalman filter for weight tracking weightFilter *filter.KalmanVelocity // Statistics observations int firstUpdate time.Time lastUpdate time.Time // Cached values lastWeight float64 lastVelocity float64 } // NewRelationshipEvolution creates a new relationship evolution tracker. func NewRelationshipEvolution(cfg RelationshipConfig) *RelationshipEvolution { return &RelationshipEvolution{ config: cfg, edges: make(map[string]*edgeTracker), accessOrder: make([]string, 0, cfg.MaxTrackedRelationships), startTime: time.Now(), } } // edgeKey generates a consistent key for an edge. func edgeKey(sourceID, targetID string) string { // Ensure consistent ordering for undirected relationships if sourceID > targetID { sourceID, targetID = targetID, sourceID } return fmt.Sprintf("%s->%s", sourceID, targetID) } // RecordCoAccess records a co-access event between two nodes. // weight should be 1.0 for simple co-access, or can be weighted. func (re *RelationshipEvolution) RecordCoAccess(sourceID, targetID string, weight float64) { re.RecordCoAccessAt(sourceID, targetID, weight, time.Now()) } // RecordCoAccessAt records co-access at a specific time. func (re *RelationshipEvolution) RecordCoAccessAt(sourceID, targetID string, weight float64, timestamp time.Time) { re.mu.Lock() defer re.mu.Unlock() key := edgeKey(sourceID, targetID) re.totalUpdates++ tracker, exists := re.edges[key] if !exists { tracker = &edgeTracker{ sourceID: sourceID, targetID: targetID, weightFilter: filter.NewKalmanVelocity(re.config.FilterConfig), firstUpdate: timestamp, } re.edges[key] = tracker // Check for eviction if len(re.edges) > re.config.MaxTrackedRelationships { re.evictOldest() } } // Update the filter with the new weight observation filtered := tracker.weightFilter.Process(weight) tracker.lastWeight = filtered tracker.lastVelocity = tracker.weightFilter.Velocity() tracker.lastUpdate = timestamp tracker.observations++ // Update LRU re.updateLRU(key) } // UpdateWeight updates the weight of an existing relationship. // Use this for explicit weight updates (not just co-access). func (re *RelationshipEvolution) UpdateWeight(sourceID, targetID string, newWeight float64) { re.RecordCoAccess(sourceID, targetID, newWeight) } // GetTrend returns the evolution trend for a relationship. func (re *RelationshipEvolution) GetTrend(sourceID, targetID string) *RelationshipTrend { re.mu.RLock() defer re.mu.RUnlock() key := edgeKey(sourceID, targetID) tracker, exists := re.edges[key] if !exists { return nil } return re.calculateTrend(tracker) } // calculateTrend computes the trend from a tracker. func (re *RelationshipEvolution) calculateTrend(tracker *edgeTracker) *RelationshipTrend { velocity := tracker.lastVelocity var direction string if tracker.observations < re.config.MinObservationsForTrend { direction = "unknown" } else if velocity > re.config.StrengthenThreshold { direction = "strengthening" } else if velocity < re.config.WeakenThreshold { direction = "weakening" } else { direction = "stable" } // Calculate confidence based on observations confidence := float64(tracker.observations) / float64(tracker.observations+10) // Predict future strength predicted := tracker.weightFilter.Predict(5) return &RelationshipTrend{ Direction: direction, Velocity: velocity, CurrentStrength: tracker.lastWeight, PredictedStrength: predicted, Confidence: confidence, ObservationCount: tracker.observations, LastUpdate: tracker.lastUpdate, } } // PredictStrength predicts the relationship strength n steps ahead. func (re *RelationshipEvolution) PredictStrength(sourceID, targetID string, steps int) float64 { re.mu.RLock() defer re.mu.RUnlock() key := edgeKey(sourceID, targetID) tracker, exists := re.edges[key] if !exists { return 0 } return tracker.weightFilter.Predict(steps) } // GetStrengtheningRelationships returns relationships that are getting stronger. func (re *RelationshipEvolution) GetStrengtheningRelationships(limit int) []RelationshipTrend { re.mu.RLock() defer re.mu.RUnlock() var results []RelationshipTrend for _, tracker := range re.edges { if tracker.observations >= re.config.MinObservationsForTrend { if tracker.lastVelocity > re.config.StrengthenThreshold { trend := re.calculateTrend(tracker) results = append(results, *trend) } } } // Sort by velocity (descending) for i := 0; i < len(results)-1; i++ { for j := i + 1; j < len(results); j++ { if results[j].Velocity > results[i].Velocity { results[i], results[j] = results[j], results[i] } } } if len(results) > limit { results = results[:limit] } return results } // GetWeakeningRelationships returns relationships that are getting weaker. func (re *RelationshipEvolution) GetWeakeningRelationships(limit int) []RelationshipTrend { re.mu.RLock() defer re.mu.RUnlock() var results []RelationshipTrend for _, tracker := range re.edges { if tracker.observations >= re.config.MinObservationsForTrend { if tracker.lastVelocity < re.config.WeakenThreshold { trend := re.calculateTrend(tracker) results = append(results, *trend) } } } // Sort by velocity (ascending - most negative first) for i := 0; i < len(results)-1; i++ { for j := i + 1; j < len(results); j++ { if results[j].Velocity < results[i].Velocity { results[i], results[j] = results[j], results[i] } } } if len(results) > limit { results = results[:limit] } return results } // GetEmergingRelationships returns new relationships with positive velocity. func (re *RelationshipEvolution) GetEmergingRelationships(limit int) []RelationshipTrend { re.mu.RLock() defer re.mu.RUnlock() minAge := 10 // Minimum observations to consider "emerging" maxAge := 50 // Maximum observations to still be "emerging" var results []RelationshipTrend for _, tracker := range re.edges { if tracker.observations >= minAge && tracker.observations <= maxAge { if tracker.lastVelocity > 0 { trend := re.calculateTrend(tracker) results = append(results, *trend) } } } // Sort by velocity (descending) for i := 0; i < len(results)-1; i++ { for j := i + 1; j < len(results); j++ { if results[j].Velocity > results[i].Velocity { results[i], results[j] = results[j], results[i] } } } if len(results) > limit { results = results[:limit] } return results } // ShouldPrune checks if a relationship should be pruned (very weak and weakening). func (re *RelationshipEvolution) ShouldPrune(sourceID, targetID string, threshold float64) bool { trend := re.GetTrend(sourceID, targetID) if trend == nil { return false } // Prune if weak AND getting weaker return trend.CurrentStrength < threshold && trend.Direction == "weakening" } // DecayIdleRelationships applies decay to relationships not updated recently. func (re *RelationshipEvolution) DecayIdleRelationships(maxIdleHours float64) int { if !re.config.DecayIdleRelationships { return 0 } re.mu.Lock() defer re.mu.Unlock() now := time.Now() decayed := 0 for _, tracker := range re.edges { idleHours := now.Sub(tracker.lastUpdate).Hours() if idleHours > maxIdleHours { // Apply decay decayFactor := 1.0 - (re.config.IdleDecayRate * idleHours) if decayFactor < 0.1 { decayFactor = 0.1 // Minimum decay } newWeight := tracker.lastWeight * decayFactor tracker.weightFilter.Process(newWeight) tracker.lastWeight = newWeight decayed++ } } return decayed } // updateLRU updates LRU order for an edge. func (re *RelationshipEvolution) updateLRU(key string) { // Remove from current position for i, k := range re.accessOrder { if k == key { re.accessOrder = append(re.accessOrder[:i], re.accessOrder[i+1:]...) break } } // Add to end re.accessOrder = append(re.accessOrder, key) } // evictOldest removes the least recently used edge. func (re *RelationshipEvolution) evictOldest() { if len(re.accessOrder) == 0 { return } oldest := re.accessOrder[0] re.accessOrder = re.accessOrder[1:] delete(re.edges, oldest) } // RelationshipStats holds statistics about relationship tracking. type RelationshipStats struct { TrackedRelationships int TotalUpdates int64 Strengthening int Weakening int Stable int UptimeSeconds float64 } // GetStats returns statistics about relationship tracking. func (re *RelationshipEvolution) GetStats() RelationshipStats { re.mu.RLock() defer re.mu.RUnlock() stats := RelationshipStats{ TrackedRelationships: len(re.edges), TotalUpdates: re.totalUpdates, UptimeSeconds: time.Since(re.startTime).Seconds(), } for _, tracker := range re.edges { if tracker.observations >= re.config.MinObservationsForTrend { if tracker.lastVelocity > re.config.StrengthenThreshold { stats.Strengthening++ } else if tracker.lastVelocity < re.config.WeakenThreshold { stats.Weakening++ } else { stats.Stable++ } } } return stats } // Reset clears all relationship data. func (re *RelationshipEvolution) Reset() { re.mu.Lock() defer re.mu.Unlock() re.edges = make(map[string]*edgeTracker) re.accessOrder = make([]string, 0, re.config.MaxTrackedRelationships) re.totalUpdates = 0 re.startTime = time.Now() }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server