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
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 pub fn this_week() -> Self {
93 Self::new().with_max_age(24 * 7)
94 }
95
96 pub fn matches(&self, ctx: &Context) -> bool {
98 let age = self.reference_time - ctx.created_at;
99
100 if let Some(max) = self.max_age {
102 if age > max {
103 return false;
104 }
105 }
106
107 if let Some(min) = self.min_age {
109 if age < min {
110 return false;
111 }
112 }
113
114 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 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 let decay_factor = 0.5_f64.powf(age_hours / self.decay_half_life_hours);
141
142 let importance = ctx.metadata.importance as f64;
144
145 0.7 * decay_factor + 0.3 * importance
147 }
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct TemporalStats {
153 pub count: usize,
155 pub oldest: Option<DateTime<Utc>>,
157 pub newest: Option<DateTime<Utc>>,
159 pub avg_age_hours: f64,
161 pub distribution: TimeDistribution,
163}
164
165#[derive(Debug, Clone, Default, Serialize, Deserialize)]
167pub struct TimeDistribution {
168 pub last_hour: usize,
170 pub last_day: usize,
172 pub last_week: usize,
174 pub last_month: usize,
176 pub older: usize,
178}
179
180impl TemporalStats {
181 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 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 let age_hours = ctx.age_hours();
209 total_age_hours += age_hours;
210
211 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
235pub 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 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); }
282}