1use chrono::{DateTime, Duration, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::context::Context;
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct TemporalQuery {
14 pub reference_time: DateTime<Utc>,
16 pub max_age: Option<Duration>,
18 pub min_age: Option<Duration>,
20 pub window_start: Option<DateTime<Utc>>,
22 pub window_end: Option<DateTime<Utc>>,
24 pub apply_decay: bool,
26 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, }
41 }
42}
43
44impl TemporalQuery {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn with_max_age(mut self, hours: i64) -> Self {
52 self.max_age = Some(Duration::hours(hours));
53 self
54 }
55
56 pub fn with_min_age(mut self, hours: i64) -> Self {
58 self.min_age = Some(Duration::hours(hours));
59 self
60 }
61
62 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 pub fn recent(hours: i64) -> Self {
71 Self::new().with_max_age(hours)
72 }
73
74 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 pub fn this_week() -> Self {
89 Self::new().with_max_age(24 * 7)
90 }
91
92 pub fn matches(&self, ctx: &Context) -> bool {
94 let age = self.reference_time - ctx.created_at;
95
96 if let Some(max) = self.max_age {
98 if age > max {
99 return false;
100 }
101 }
102
103 if let Some(min) = self.min_age {
105 if age < min {
106 return false;
107 }
108 }
109
110 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 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 let decay_factor = 0.5_f64.powf(age_hours / self.decay_half_life_hours);
137
138 let importance = ctx.metadata.importance as f64;
140
141 0.7 * decay_factor + 0.3 * importance
143 }
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct TemporalStats {
149 pub count: usize,
151 pub oldest: Option<DateTime<Utc>>,
153 pub newest: Option<DateTime<Utc>>,
155 pub avg_age_hours: f64,
157 pub distribution: TimeDistribution,
159}
160
161#[derive(Debug, Clone, Default, Serialize, Deserialize)]
163pub struct TimeDistribution {
164 pub last_hour: usize,
166 pub last_day: usize,
168 pub last_week: usize,
170 pub last_month: usize,
172 pub older: usize,
174}
175
176impl TemporalStats {
177 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 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 let age_hours = ctx.age_hours();
205 total_age_hours += age_hours;
206
207 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
231pub 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 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); }
278}