context_mcp/
temporal.rs

1//! Temporal reasoning support for context retrieval
2//!
3//! Provides time-based querying and relevance scoring for
4//! enhanced temporal reasoning in RAG operations.
5
6use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::context::Context;
10
11/// Temporal query parameters
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TemporalQuery {
14    /// Reference time (defaults to now)
15    pub reference_time: DateTime<Utc>,
16    /// Maximum age for results
17    pub max_age: Option<Duration>,
18    /// Minimum age for results  
19    pub min_age: Option<Duration>,
20    /// Time window start
21    pub window_start: Option<DateTime<Utc>>,
22    /// Time window end
23    pub window_end: Option<DateTime<Utc>>,
24    /// Apply temporal decay to relevance scoring
25    pub apply_decay: bool,
26    /// Decay half-life in hours
27    pub decay_half_life_hours: f64,
28}
29
30impl Default for TemporalQuery {
31    fn default() -> Self {
32        Self {
33            reference_time: Utc::now(),
34            max_age: None,
35            min_age: None,
36            window_start: None,
37            window_end: None,
38            apply_decay: true,
39            decay_half_life_hours: 24.0, // 1 day half-life
40        }
41    }
42}
43
44impl TemporalQuery {
45    /// Create a new temporal query
46    pub fn new() -> Self {
47        Self::default()
48    }
49
50    /// Set maximum age
51    pub fn with_max_age(mut self, hours: i64) -> Self {
52        self.max_age = Some(Duration::hours(hours));
53        self
54    }
55
56    /// Set minimum age
57    pub fn with_min_age(mut self, hours: i64) -> Self {
58        self.min_age = Some(Duration::hours(hours));
59        self
60    }
61
62    /// Set time window
63    pub fn with_window(mut self, start: DateTime<Utc>, end: DateTime<Utc>) -> Self {
64        self.window_start = Some(start);
65        self.window_end = Some(end);
66        self
67    }
68
69    /// Query for recent contexts (last N hours)
70    pub fn recent(hours: i64) -> Self {
71        Self::new().with_max_age(hours)
72    }
73
74    /// Query for contexts from today
75    pub fn today() -> Self {
76        let now = Utc::now();
77        let start_of_day = now
78            .date_naive()
79            .and_hms_opt(0, 0, 0)
80            .unwrap()
81            .and_utc();
82        
83        Self {
84            reference_time: now,
85            window_start: Some(start_of_day),
86            window_end: Some(now),
87            ..Default::default()
88        }
89    }
90
91    /// Query for contexts from this week
92    pub fn this_week() -> Self {
93        Self::new().with_max_age(24 * 7)
94    }
95
96    /// Check if a context matches temporal criteria
97    pub fn matches(&self, ctx: &Context) -> bool {
98        let age = self.reference_time - ctx.created_at;
99
100        // Check max age
101        if let Some(max) = self.max_age {
102            if age > max {
103                return false;
104            }
105        }
106
107        // Check min age
108        if let Some(min) = self.min_age {
109            if age < min {
110                return false;
111            }
112        }
113
114        // Check time window
115        if let Some(start) = self.window_start {
116            if ctx.created_at < start {
117                return false;
118            }
119        }
120
121        if let Some(end) = self.window_end {
122            if ctx.created_at > end {
123                return false;
124            }
125        }
126
127        true
128    }
129
130    /// Calculate temporal relevance score (0.0 to 1.0)
131    /// Uses exponential decay based on age
132    pub fn relevance_score(&self, ctx: &Context) -> f64 {
133        if !self.apply_decay {
134            return 1.0;
135        }
136
137        let age_hours = ctx.age_hours();
138        
139        // Exponential decay: score = 0.5^(age/half_life)
140        let decay_factor = 0.5_f64.powf(age_hours / self.decay_half_life_hours);
141        
142        // Combine with importance
143        let importance = ctx.metadata.importance as f64;
144        
145        // Weighted combination (70% temporal, 30% importance)
146        0.7 * decay_factor + 0.3 * importance
147    }
148}
149
150/// Temporal statistics for a set of contexts
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct TemporalStats {
153    /// Number of contexts
154    pub count: usize,
155    /// Oldest context timestamp
156    pub oldest: Option<DateTime<Utc>>,
157    /// Newest context timestamp  
158    pub newest: Option<DateTime<Utc>>,
159    /// Average age in hours
160    pub avg_age_hours: f64,
161    /// Distribution by time bucket
162    pub distribution: TimeDistribution,
163}
164
165/// Distribution of contexts over time
166#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct TimeDistribution {
168    /// Last hour
169    pub last_hour: usize,
170    /// Last 24 hours
171    pub last_day: usize,
172    /// Last week
173    pub last_week: usize,
174    /// Last month
175    pub last_month: usize,
176    /// Older
177    pub older: usize,
178}
179
180impl TemporalStats {
181    /// Compute temporal statistics from contexts
182    pub fn from_contexts(contexts: &[Context]) -> Self {
183        if contexts.is_empty() {
184            return Self {
185                count: 0,
186                oldest: None,
187                newest: None,
188                avg_age_hours: 0.0,
189                distribution: TimeDistribution::default(),
190            };
191        }
192
193        let mut oldest: Option<DateTime<Utc>> = None;
194        let mut newest: Option<DateTime<Utc>> = None;
195        let mut total_age_hours = 0.0;
196        let mut distribution = TimeDistribution::default();
197
198        for ctx in contexts {
199            // Update oldest/newest
200            if oldest.map(|o| ctx.created_at < o).unwrap_or(true) {
201                oldest = Some(ctx.created_at);
202            }
203            if newest.map(|n| ctx.created_at > n).unwrap_or(true) {
204                newest = Some(ctx.created_at);
205            }
206
207            // Accumulate age
208            let age_hours = ctx.age_hours();
209            total_age_hours += age_hours;
210
211            // Update distribution
212            if age_hours < 1.0 {
213                distribution.last_hour += 1;
214            } else if age_hours < 24.0 {
215                distribution.last_day += 1;
216            } else if age_hours < 24.0 * 7.0 {
217                distribution.last_week += 1;
218            } else if age_hours < 24.0 * 30.0 {
219                distribution.last_month += 1;
220            } else {
221                distribution.older += 1;
222            }
223        }
224
225        Self {
226            count: contexts.len(),
227            oldest,
228            newest,
229            avg_age_hours: total_age_hours / contexts.len() as f64,
230            distribution,
231        }
232    }
233}
234
235/// Human-readable time formatting for context age
236pub fn format_age(ctx: &Context) -> String {
237    let age_secs = ctx.age_seconds();
238    
239    if age_secs < 60 {
240        format!("{}s ago", age_secs)
241    } else if age_secs < 3600 {
242        format!("{}m ago", age_secs / 60)
243    } else if age_secs < 86400 {
244        format!("{}h ago", age_secs / 3600)
245    } else {
246        format!("{}d ago", age_secs / 86400)
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253    use crate::context::ContextDomain;
254
255    #[test]
256    fn test_temporal_query_recent() {
257        let query = TemporalQuery::recent(24);
258        assert!(query.max_age.is_some());
259    }
260
261    #[test]
262    fn test_relevance_decay() {
263        let query = TemporalQuery::new();
264        let ctx = Context::new("Test", ContextDomain::General);
265        
266        let score = query.relevance_score(&ctx);
267        // Fresh context should have high score
268        assert!(score > 0.9);
269    }
270
271    #[test]
272    fn test_temporal_stats() {
273        let contexts = vec![
274            Context::new("Test 1", ContextDomain::General),
275            Context::new("Test 2", ContextDomain::Code),
276        ];
277
278        let stats = TemporalStats::from_contexts(&contexts);
279        assert_eq!(stats.count, 2);
280        assert!(stats.avg_age_hours < 1.0); // Just created
281    }
282}