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.date_naive().and_hms_opt(0, 0, 0).unwrap().and_utc();
78
79        Self {
80            reference_time: now,
81            window_start: Some(start_of_day),
82            window_end: Some(now),
83            ..Default::default()
84        }
85    }
86
87    /// Query for contexts from this week
88    pub fn this_week() -> Self {
89        Self::new().with_max_age(24 * 7)
90    }
91
92    /// Check if a context matches temporal criteria
93    pub fn matches(&self, ctx: &Context) -> bool {
94        let age = self.reference_time - ctx.created_at;
95
96        // Check max age
97        if let Some(max) = self.max_age {
98            if age > max {
99                return false;
100            }
101        }
102
103        // Check min age
104        if let Some(min) = self.min_age {
105            if age < min {
106                return false;
107            }
108        }
109
110        // Check time window
111        if let Some(start) = self.window_start {
112            if ctx.created_at < start {
113                return false;
114            }
115        }
116
117        if let Some(end) = self.window_end {
118            if ctx.created_at > end {
119                return false;
120            }
121        }
122
123        true
124    }
125
126    /// Calculate temporal relevance score (0.0 to 1.0)
127    /// Uses exponential decay based on age
128    pub fn relevance_score(&self, ctx: &Context) -> f64 {
129        if !self.apply_decay {
130            return 1.0;
131        }
132
133        let age_hours = ctx.age_hours();
134
135        // Exponential decay: score = 0.5^(age/half_life)
136        let decay_factor = 0.5_f64.powf(age_hours / self.decay_half_life_hours);
137
138        // Combine with importance
139        let importance = ctx.metadata.importance as f64;
140
141        // Weighted combination (70% temporal, 30% importance)
142        0.7 * decay_factor + 0.3 * importance
143    }
144}
145
146/// Temporal statistics for a set of contexts
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct TemporalStats {
149    /// Number of contexts
150    pub count: usize,
151    /// Oldest context timestamp
152    pub oldest: Option<DateTime<Utc>>,
153    /// Newest context timestamp  
154    pub newest: Option<DateTime<Utc>>,
155    /// Average age in hours
156    pub avg_age_hours: f64,
157    /// Distribution by time bucket
158    pub distribution: TimeDistribution,
159}
160
161/// Distribution of contexts over time
162#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163pub struct TimeDistribution {
164    /// Last hour
165    pub last_hour: usize,
166    /// Last 24 hours
167    pub last_day: usize,
168    /// Last week
169    pub last_week: usize,
170    /// Last month
171    pub last_month: usize,
172    /// Older
173    pub older: usize,
174}
175
176impl TemporalStats {
177    /// Compute temporal statistics from contexts
178    pub fn from_contexts(contexts: &[Context]) -> Self {
179        if contexts.is_empty() {
180            return Self {
181                count: 0,
182                oldest: None,
183                newest: None,
184                avg_age_hours: 0.0,
185                distribution: TimeDistribution::default(),
186            };
187        }
188
189        let mut oldest: Option<DateTime<Utc>> = None;
190        let mut newest: Option<DateTime<Utc>> = None;
191        let mut total_age_hours = 0.0;
192        let mut distribution = TimeDistribution::default();
193
194        for ctx in contexts {
195            // Update oldest/newest
196            if oldest.map(|o| ctx.created_at < o).unwrap_or(true) {
197                oldest = Some(ctx.created_at);
198            }
199            if newest.map(|n| ctx.created_at > n).unwrap_or(true) {
200                newest = Some(ctx.created_at);
201            }
202
203            // Accumulate age
204            let age_hours = ctx.age_hours();
205            total_age_hours += age_hours;
206
207            // Update distribution
208            if age_hours < 1.0 {
209                distribution.last_hour += 1;
210            } else if age_hours < 24.0 {
211                distribution.last_day += 1;
212            } else if age_hours < 24.0 * 7.0 {
213                distribution.last_week += 1;
214            } else if age_hours < 24.0 * 30.0 {
215                distribution.last_month += 1;
216            } else {
217                distribution.older += 1;
218            }
219        }
220
221        Self {
222            count: contexts.len(),
223            oldest,
224            newest,
225            avg_age_hours: total_age_hours / contexts.len() as f64,
226            distribution,
227        }
228    }
229}
230
231/// Human-readable time formatting for context age
232pub fn format_age(ctx: &Context) -> String {
233    let age_secs = ctx.age_seconds();
234
235    if age_secs < 60 {
236        format!("{}s ago", age_secs)
237    } else if age_secs < 3600 {
238        format!("{}m ago", age_secs / 60)
239    } else if age_secs < 86400 {
240        format!("{}h ago", age_secs / 3600)
241    } else {
242        format!("{}d ago", age_secs / 86400)
243    }
244}
245
246#[cfg(test)]
247mod tests {
248    use super::*;
249    use crate::context::ContextDomain;
250
251    #[test]
252    fn test_temporal_query_recent() {
253        let query = TemporalQuery::recent(24);
254        assert!(query.max_age.is_some());
255    }
256
257    #[test]
258    fn test_relevance_decay() {
259        let query = TemporalQuery::new();
260        let ctx = Context::new("Test", ContextDomain::General);
261
262        let score = query.relevance_score(&ctx);
263        // Fresh context should have high score
264        assert!(score > 0.9);
265    }
266
267    #[test]
268    fn test_temporal_stats() {
269        let contexts = vec![
270            Context::new("Test 1", ContextDomain::General),
271            Context::new("Test 2", ContextDomain::Code),
272        ];
273
274        let stats = TemporalStats::from_contexts(&contexts);
275        assert_eq!(stats.count, 2);
276        assert!(stats.avg_age_hours < 1.0); // Just created
277    }
278}