converge_knowledge/agentic/
temporal.rs1use chrono::{DateTime, Datelike, Timelike, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct TimeCrystal {
22 pub id: Uuid,
24
25 pub name: String,
27
28 pub period: TemporalPeriod,
30
31 pub occurrences: Vec<TemporalOccurrence>,
33
34 pub distribution: Vec<f32>,
36
37 pub confidence: f32,
39
40 pub created_at: DateTime<Utc>,
42
43 pub updated_at: DateTime<Utc>,
45}
46
47impl TimeCrystal {
48 pub fn new(name: impl Into<String>, period: TemporalPeriod) -> Self {
50 let bins = period.bin_count();
51 let now = Utc::now();
52
53 Self {
54 id: Uuid::new_v4(),
55 name: name.into(),
56 period,
57 occurrences: Vec::new(),
58 distribution: vec![0.0; bins],
59 confidence: 0.0,
60 created_at: now,
61 updated_at: now,
62 }
63 }
64
65 pub fn record(&mut self, timestamp: DateTime<Utc>, value: f32) {
67 let bin = self.period.time_to_bin(×tamp);
68 self.occurrences.push(TemporalOccurrence {
69 timestamp,
70 bin,
71 value,
72 });
73 self.updated_at = Utc::now();
74 self.recompute_distribution();
75 }
76
77 pub fn record_now(&mut self, value: f32) {
79 self.record(Utc::now(), value);
80 }
81
82 pub fn predict(&self, timestamp: &DateTime<Utc>) -> f32 {
84 let bin = self.period.time_to_bin(timestamp);
85 self.distribution.get(bin).copied().unwrap_or(0.0)
86 }
87
88 pub fn predict_now(&self) -> f32 {
90 self.predict(&Utc::now())
91 }
92
93 pub fn best_time(&self) -> usize {
95 self.distribution
96 .iter()
97 .enumerate()
98 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
99 .map(|(i, _)| i)
100 .unwrap_or(0)
101 }
102
103 pub fn is_anomalous(&self, timestamp: &DateTime<Utc>, value: f32, threshold: f32) -> bool {
105 let expected = self.predict(timestamp);
106 (value - expected).abs() > threshold
107 }
108
109 fn recompute_distribution(&mut self) {
111 let bins = self.period.bin_count();
112 let mut counts = vec![0.0f32; bins];
113 let mut totals = vec![0.0f32; bins];
114
115 for occ in &self.occurrences {
116 if occ.bin < bins {
117 counts[occ.bin] += 1.0;
118 totals[occ.bin] += occ.value;
119 }
120 }
121
122 for i in 0..bins {
124 self.distribution[i] = if counts[i] > 0.0 {
125 totals[i] / counts[i]
126 } else {
127 0.0
128 };
129 }
130
131 let total_samples: f32 = counts.iter().sum();
133 self.confidence = (total_samples / (bins as f32 * 3.0)).min(1.0);
134 }
135
136 pub fn period_description(&self) -> String {
138 match &self.period {
139 TemporalPeriod::Hourly => "hourly (by minute)".to_string(),
140 TemporalPeriod::Daily => "daily (by hour)".to_string(),
141 TemporalPeriod::Weekly => "weekly (by day)".to_string(),
142 TemporalPeriod::Monthly => "monthly (by day)".to_string(),
143 TemporalPeriod::Custom { name, .. } => format!("custom: {}", name),
144 }
145 }
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct TemporalOccurrence {
151 pub timestamp: DateTime<Utc>,
153
154 pub bin: usize,
156
157 pub value: f32,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize)]
163pub enum TemporalPeriod {
164 Hourly,
166
167 Daily,
169
170 Weekly,
172
173 Monthly,
175
176 Custom {
178 name: String,
180 bins: usize,
182 duration_minutes: u64,
184 },
185}
186
187impl TemporalPeriod {
188 pub fn bin_count(&self) -> usize {
190 match self {
191 TemporalPeriod::Hourly => 60,
192 TemporalPeriod::Daily => 24,
193 TemporalPeriod::Weekly => 7,
194 TemporalPeriod::Monthly => 31,
195 TemporalPeriod::Custom { bins, .. } => *bins,
196 }
197 }
198
199 pub fn time_to_bin(&self, timestamp: &DateTime<Utc>) -> usize {
201 match self {
202 TemporalPeriod::Hourly => timestamp.minute() as usize,
203 TemporalPeriod::Daily => timestamp.hour() as usize,
204 TemporalPeriod::Weekly => timestamp.weekday().num_days_from_monday() as usize,
205 TemporalPeriod::Monthly => (timestamp.day() - 1) as usize,
206 TemporalPeriod::Custom {
207 duration_minutes,
208 bins,
209 ..
210 } => {
211 let minutes_since_epoch = timestamp.timestamp() as u64 / 60;
212 ((minutes_since_epoch % duration_minutes) * (*bins as u64) / duration_minutes)
213 as usize
214 }
215 }
216 }
217
218 pub fn bin_label(&self, bin: usize) -> String {
220 match self {
221 TemporalPeriod::Hourly => format!(":{:02}", bin),
222 TemporalPeriod::Daily => format!("{:02}:00", bin),
223 TemporalPeriod::Weekly => {
224 let day = match bin {
225 0 => "Monday",
226 1 => "Tuesday",
227 2 => "Wednesday",
228 3 => "Thursday",
229 4 => "Friday",
230 5 => "Saturday",
231 6 => "Sunday",
232 _ => "Unknown",
233 };
234 day.to_string()
235 }
236 TemporalPeriod::Monthly => format!("Day {}", bin + 1),
237 TemporalPeriod::Custom { name, .. } => format!("{} bin {}", name, bin),
238 }
239 }
240}
241
242pub struct TemporalMemory {
244 crystals: HashMap<String, TimeCrystal>,
245}
246
247impl TemporalMemory {
248 pub fn new() -> Self {
250 Self {
251 crystals: HashMap::new(),
252 }
253 }
254
255 pub fn get_or_create(&mut self, name: &str, period: TemporalPeriod) -> &mut TimeCrystal {
257 self.crystals
258 .entry(name.to_string())
259 .or_insert_with(|| TimeCrystal::new(name, period))
260 }
261
262 pub fn record(&mut self, pattern_name: &str, period: TemporalPeriod, value: f32) {
264 let crystal = self.get_or_create(pattern_name, period);
265 crystal.record_now(value);
266 }
267
268 pub fn predict(&self, pattern_name: &str) -> Option<f32> {
270 self.crystals.get(pattern_name).map(|c| c.predict_now())
271 }
272
273 pub fn list_patterns(&self) -> Vec<&str> {
275 self.crystals.keys().map(|s| s.as_str()).collect()
276 }
277
278 pub fn get(&self, name: &str) -> Option<&TimeCrystal> {
280 self.crystals.get(name)
281 }
282
283 pub fn len(&self) -> usize {
285 self.crystals.len()
286 }
287
288 pub fn is_empty(&self) -> bool {
290 self.crystals.is_empty()
291 }
292}
293
294impl Default for TemporalMemory {
295 fn default() -> Self {
296 Self::new()
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303 use chrono::Duration;
304
305 #[test]
313 fn test_daily_pattern() {
314 let mut crystal = TimeCrystal::new("coding_activity", TemporalPeriod::Daily);
315
316 for hour in 0..24 {
318 let value = if hour >= 9 && hour < 17 {
319 1.0 } else if hour >= 22 || hour < 6 {
321 0.0 } else {
323 0.3 };
325
326 let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
328 crystal.record(timestamp, value);
329 }
330
331 let work_hour = crystal.predict(&Utc::now().with_hour(10).unwrap().with_minute(0).unwrap());
333 let night_hour = crystal.predict(&Utc::now().with_hour(2).unwrap().with_minute(0).unwrap());
334
335 assert!(work_hour > night_hour);
336 assert!(work_hour >= 0.9); let best = crystal.best_time();
340 assert!(best >= 9 && best < 17);
341 }
342
343 #[test]
350 fn test_weekly_pattern() {
351 let mut crystal = TimeCrystal::new("deploy_activity", TemporalPeriod::Weekly);
352
353 for day in 0..7 {
355 let value = if day == 1 || day == 3 { 1.0 } else { 0.0 };
356 let days_offset = day as i64 - Utc::now().weekday().num_days_from_monday() as i64;
358 let timestamp = Utc::now() + Duration::days(days_offset);
359 crystal.record(timestamp, value);
360 }
361
362 assert!(crystal.distribution[1] > 0.5); assert!(crystal.distribution[3] > 0.5); assert!(crystal.distribution[0] < 0.1); assert!(crystal.distribution[5] < 0.1); }
368
369 #[test]
376 fn test_anomaly_detection() {
377 let mut crystal = TimeCrystal::new("system_load", TemporalPeriod::Daily);
378
379 for _ in 0..10 {
381 for hour in 0..24 {
382 let normal_value = if hour >= 9 && hour < 17 { 0.8 } else { 0.1 };
383 let timestamp = Utc::now().with_hour(hour).unwrap().with_minute(0).unwrap();
384 crystal.record(timestamp, normal_value);
385 }
386 }
387
388 let night_time = Utc::now().with_hour(3).unwrap();
390 assert!(crystal.is_anomalous(&night_time, 0.9, 0.3));
391
392 let day_time = Utc::now().with_hour(10).unwrap();
394 assert!(!crystal.is_anomalous(&day_time, 0.8, 0.3));
395 }
396
397 #[test]
404 fn test_temporal_memory() {
405 let mut memory = TemporalMemory::new();
406
407 memory.record("coding", TemporalPeriod::Daily, 1.0);
409 memory.record("meetings", TemporalPeriod::Daily, 0.5);
410 memory.record("deploys", TemporalPeriod::Weekly, 1.0);
411
412 assert_eq!(memory.len(), 3);
413 assert!(memory.predict("coding").is_some());
414 assert!(memory.predict("unknown").is_none());
415
416 let patterns = memory.list_patterns();
417 assert!(patterns.contains(&"coding"));
418 assert!(patterns.contains(&"meetings"));
419 assert!(patterns.contains(&"deploys"));
420 }
421}