1use std::fs;
13use std::io::Write;
14use std::path::Path;
15use std::time::{SystemTime, UNIX_EPOCH};
16
17use crate::thought_graph::{ThoughtGraphState, PATTERN_BOOST_WEIGHT};
18
19const MAX_PREDICTIONS: usize = 50; const MAX_BLOCKS_PER_PREDICTION: usize = 30;
23const HIT_REWARD: f32 = 0.3; const MISS_PENALTY: f32 = 0.05; const PREDICTION_DECAY: f32 = 0.98; const MIN_CONFIDENCE: f32 = 0.1; const MIN_PATTERN_FREQ: u32 = 3; #[derive(Clone, Debug)]
33pub struct Prediction {
34 pub predicted_query_hash: u64,
35 pub blocks: Vec<u32>,
36 pub confidence: f32,
37 pub pattern_id: u32,
38 pub created_ms: u64,
39}
40
41#[derive(Clone, Debug, Default)]
44pub struct CacheStats {
45 pub total_predictions: u32,
46 pub total_hits: u32,
47 pub total_misses: u32,
48 pub total_partial_hits: u32,
49 pub current_predictions: usize,
50 pub avg_confidence: f32,
51}
52
53impl CacheStats {
54 pub fn hit_rate(&self) -> f32 {
55 let total = self.total_hits + self.total_misses + self.total_partial_hits;
56 if total == 0 {
57 return 0.0;
58 }
59 (self.total_hits as f32 + self.total_partial_hits as f32 * 0.5) / total as f32
60 }
61}
62
63#[derive(Clone)]
66pub struct PredictiveCache {
67 pub predictions: Vec<Prediction>,
68 pub stats: CacheStats,
69}
70
71impl PredictiveCache {
72 pub fn load_or_init(output_dir: &Path) -> Self {
73 let path = output_dir.join("predictive_cache.bin");
74 if path.exists() {
75 load_cache(&path)
76 } else {
77 Self {
78 predictions: Vec::new(),
79 stats: CacheStats::default(),
80 }
81 }
82 }
83
84 pub fn check(&self, query_hash: u64) -> Option<(Vec<u32>, f32)> {
87 self.predictions
88 .iter()
89 .find(|p| p.predicted_query_hash == query_hash && p.confidence >= MIN_CONFIDENCE)
90 .map(|p| (p.blocks.clone(), p.confidence))
91 }
92
93 pub fn evaluate(
97 &mut self,
98 query_hash: u64,
99 actual_results: &[u32],
100 thought_graph: &mut ThoughtGraphState,
101 ) -> (&'static str, usize) {
102 let prediction = self
103 .predictions
104 .iter()
105 .find(|p| p.predicted_query_hash == query_hash);
106
107 let prediction = match prediction {
108 Some(p) => p.clone(),
109 None => return ("none", 0),
110 };
111
112 let overlap = prediction
114 .blocks
115 .iter()
116 .filter(|b| actual_results.contains(b))
117 .count();
118
119 let hit_type = if overlap == 0 {
120 self.stats.total_misses += 1;
122 if let Some(pattern) = thought_graph
124 .patterns
125 .iter_mut()
126 .find(|p| p.id == prediction.pattern_id)
127 {
128 pattern.strength = (pattern.strength - MISS_PENALTY).max(0.0);
129 }
130 if let Some(pred) = self
132 .predictions
133 .iter_mut()
134 .find(|p| p.predicted_query_hash == query_hash)
135 {
136 pred.confidence *= 0.5; }
138 "miss"
139 } else if overlap >= prediction.blocks.len() / 2 || overlap >= 3 {
140 self.stats.total_hits += 1;
142 if let Some(pattern) = thought_graph
144 .patterns
145 .iter_mut()
146 .find(|p| p.id == prediction.pattern_id)
147 {
148 pattern.strength = (pattern.strength + HIT_REWARD).min(5.0);
149 }
150 if let Some(pred) = self
152 .predictions
153 .iter_mut()
154 .find(|p| p.predicted_query_hash == query_hash)
155 {
156 pred.confidence = (pred.confidence + 0.2).min(1.0);
157 }
158 "hit"
159 } else {
160 self.stats.total_partial_hits += 1;
162 let reward = HIT_REWARD * (overlap as f32 / prediction.blocks.len() as f32);
163 if let Some(pattern) = thought_graph
164 .patterns
165 .iter_mut()
166 .find(|p| p.id == prediction.pattern_id)
167 {
168 pattern.strength = (pattern.strength + reward).min(5.0);
169 }
170 "partial"
171 };
172
173 (hit_type, overlap)
174 }
175
176 pub fn predict_next(&mut self, thought_graph: &ThoughtGraphState) {
179 for pred in &mut self.predictions {
181 pred.confidence *= PREDICTION_DECAY;
182 }
183 self.predictions.retain(|p| p.confidence >= MIN_CONFIDENCE);
184
185 let session_hashes: Vec<u64> = thought_graph
186 .nodes
187 .iter()
188 .filter(|n| n.session_id == thought_graph.current_session_id)
189 .map(|n| n.query_hash)
190 .collect();
191
192 if session_hashes.is_empty() {
193 return;
194 }
195
196 let now_ms = now_epoch_ms();
197
198 for pattern in &thought_graph.patterns {
199 if pattern.frequency < MIN_PATTERN_FREQ {
200 continue;
201 }
202 if pattern.result_blocks.is_empty() {
203 continue;
204 }
205
206 let seq = &pattern.sequence;
207
208 for prefix_len in 1..seq.len() {
211 if session_hashes.len() < prefix_len {
212 continue;
213 }
214
215 let trail_start = session_hashes.len() - prefix_len;
216 let trail = &session_hashes[trail_start..];
217
218 if trail == &seq[..prefix_len] {
219 let predicted_hash = seq[prefix_len];
220
221 if self
223 .predictions
224 .iter()
225 .any(|p| p.predicted_query_hash == predicted_hash)
226 {
227 continue;
228 }
229
230 let confidence = pattern.strength
231 * PATTERN_BOOST_WEIGHT
232 * (prefix_len as f32 / seq.len() as f32);
233
234 let blocks: Vec<u32> = pattern
235 .result_blocks
236 .iter()
237 .take(MAX_BLOCKS_PER_PREDICTION)
238 .copied()
239 .collect();
240
241 self.predictions.push(Prediction {
242 predicted_query_hash: predicted_hash,
243 blocks,
244 confidence: confidence.min(1.0),
245 pattern_id: pattern.id,
246 created_ms: now_ms,
247 });
248
249 self.stats.total_predictions += 1;
250 }
251 }
252 }
253
254 if self.predictions.len() > MAX_PREDICTIONS {
256 self.predictions
257 .sort_by(|a, b| b.confidence.total_cmp(&a.confidence));
258 self.predictions.truncate(MAX_PREDICTIONS);
259 }
260
261 if !self.predictions.is_empty() {
263 self.stats.avg_confidence = self.predictions.iter().map(|p| p.confidence).sum::<f32>()
264 / self.predictions.len() as f32;
265 }
266 self.stats.current_predictions = self.predictions.len();
267 }
268
269 pub fn export_stats(&self) -> (u32, u32, u32, f32) {
271 (
272 self.stats.total_predictions,
273 self.stats.total_hits,
274 self.stats.total_misses,
275 self.stats.hit_rate(),
276 )
277 }
278
279 pub fn merge_stats(&mut self, remote_predictions: u32, remote_hits: u32, remote_misses: u32) {
281 self.stats.total_predictions += remote_predictions;
282 self.stats.total_hits += remote_hits;
283 self.stats.total_misses += remote_misses;
284 }
285
286 pub fn dream_cleanup(&mut self) {
288 self.predictions.retain(|p| p.confidence > 0.1);
289 self.stats.current_predictions = self.predictions.len();
290 }
291
292 pub fn save(&self, output_dir: &Path) -> Result<(), String> {
294 save_cache(&output_dir.join("predictive_cache.bin"), self)
295 }
296
297 pub fn remap_indexes(&mut self, old_to_new: &[u32]) {
299 for pred in &mut self.predictions {
300 pred.blocks = pred
301 .blocks
302 .iter()
303 .filter_map(|&idx| {
304 if (idx as usize) < old_to_new.len() {
305 let new = old_to_new[idx as usize];
306 if new != u32::MAX {
307 Some(new)
308 } else {
309 None
310 }
311 } else {
312 None
313 }
314 })
315 .collect();
316 }
317 self.predictions.retain(|p| !p.blocks.is_empty());
319 }
320}
321
322fn now_epoch_ms() -> u64 {
325 SystemTime::now()
326 .duration_since(UNIX_EPOCH)
327 .unwrap_or_default()
328 .as_millis() as u64
329}
330
331fn save_cache(path: &Path, cache: &PredictiveCache) -> Result<(), String> {
332 let mut buf = Vec::with_capacity(256);
333
334 buf.write_all(b"PRC1").map_err(|e| e.to_string())?;
336 buf.write_all(&(cache.predictions.len() as u32).to_le_bytes())
337 .map_err(|e| e.to_string())?;
338
339 buf.write_all(&cache.stats.total_predictions.to_le_bytes())
341 .map_err(|e| e.to_string())?;
342 buf.write_all(&cache.stats.total_hits.to_le_bytes())
343 .map_err(|e| e.to_string())?;
344 buf.write_all(&cache.stats.total_misses.to_le_bytes())
345 .map_err(|e| e.to_string())?;
346 buf.write_all(&cache.stats.total_partial_hits.to_le_bytes())
347 .map_err(|e| e.to_string())?;
348
349 for p in &cache.predictions {
351 buf.write_all(&p.predicted_query_hash.to_le_bytes())
352 .map_err(|e| e.to_string())?;
353 buf.write_all(&p.confidence.to_le_bytes())
354 .map_err(|e| e.to_string())?;
355 buf.write_all(&p.pattern_id.to_le_bytes())
356 .map_err(|e| e.to_string())?;
357 buf.write_all(&p.created_ms.to_le_bytes())
358 .map_err(|e| e.to_string())?;
359 buf.write_all(&(p.blocks.len() as u16).to_le_bytes())
360 .map_err(|e| e.to_string())?;
361 for &b in &p.blocks {
362 buf.write_all(&b.to_le_bytes()).map_err(|e| e.to_string())?;
363 }
364 }
365
366 let tmp_path = path.with_extension("bin.tmp");
367 fs::write(&tmp_path, &buf).map_err(|e| e.to_string())?;
368 fs::rename(&tmp_path, path).map_err(|e| e.to_string())
369}
370
371fn load_cache(path: &Path) -> PredictiveCache {
372 let data = match fs::read(path) {
373 Ok(d) => d,
374 Err(_) => {
375 return PredictiveCache {
376 predictions: Vec::new(),
377 stats: CacheStats::default(),
378 }
379 }
380 };
381
382 if data.len() < 24 || &data[0..4] != b"PRC1" {
383 return PredictiveCache {
384 predictions: Vec::new(),
385 stats: CacheStats::default(),
386 };
387 }
388
389 let pred_count = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
390
391 let total_predictions = u32::from_le_bytes([data[8], data[9], data[10], data[11]]);
392 let total_hits = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
393 let total_misses = u32::from_le_bytes([data[16], data[17], data[18], data[19]]);
394 let total_partial_hits = u32::from_le_bytes([data[20], data[21], data[22], data[23]]);
395
396 let mut offset = 24;
397 let mut predictions = Vec::with_capacity(pred_count);
398
399 for _ in 0..pred_count {
400 if offset + 22 > data.len() {
401 break;
402 }
403
404 let predicted_query_hash = u64::from_le_bytes([
405 data[offset],
406 data[offset + 1],
407 data[offset + 2],
408 data[offset + 3],
409 data[offset + 4],
410 data[offset + 5],
411 data[offset + 6],
412 data[offset + 7],
413 ]);
414 offset += 8;
415
416 let confidence = f32::from_le_bytes([
417 data[offset],
418 data[offset + 1],
419 data[offset + 2],
420 data[offset + 3],
421 ]);
422 offset += 4;
423
424 let pattern_id = u32::from_le_bytes([
425 data[offset],
426 data[offset + 1],
427 data[offset + 2],
428 data[offset + 3],
429 ]);
430 offset += 4;
431
432 let created_ms = u64::from_le_bytes([
433 data[offset],
434 data[offset + 1],
435 data[offset + 2],
436 data[offset + 3],
437 data[offset + 4],
438 data[offset + 5],
439 data[offset + 6],
440 data[offset + 7],
441 ]);
442 offset += 8;
443
444 if offset + 2 > data.len() {
445 break;
446 }
447 let block_count = u16::from_le_bytes([data[offset], data[offset + 1]]) as usize;
448 offset += 2;
449
450 if offset + block_count * 4 > data.len() {
451 break;
452 }
453 let mut blocks = Vec::with_capacity(block_count);
454 for _ in 0..block_count {
455 let b = u32::from_le_bytes([
456 data[offset],
457 data[offset + 1],
458 data[offset + 2],
459 data[offset + 3],
460 ]);
461 blocks.push(b);
462 offset += 4;
463 }
464
465 predictions.push(Prediction {
466 predicted_query_hash,
467 blocks,
468 confidence,
469 pattern_id,
470 created_ms,
471 });
472 }
473
474 let current_predictions = predictions.len();
475 let avg_confidence = if predictions.is_empty() {
476 0.0
477 } else {
478 predictions.iter().map(|p| p.confidence).sum::<f32>() / predictions.len() as f32
479 };
480
481 PredictiveCache {
482 predictions,
483 stats: CacheStats {
484 total_predictions,
485 total_hits,
486 total_misses,
487 total_partial_hits,
488 current_predictions,
489 avg_confidence,
490 },
491 }
492}
493
494#[cfg(test)]
497mod tests {
498 use super::*;
499 use crate::thought_graph::{ThoughtGraphState, ThoughtPattern};
500
501 fn make_tg() -> ThoughtGraphState {
502 ThoughtGraphState::load_or_init(Path::new("/nonexistent"))
503 }
504
505 #[test]
506 fn test_check_empty() {
507 let cache = PredictiveCache {
508 predictions: Vec::new(),
509 stats: CacheStats::default(),
510 };
511 assert!(cache.check(0xAA).is_none());
512 }
513
514 #[test]
515 fn test_check_hit() {
516 let cache = PredictiveCache {
517 predictions: vec![Prediction {
518 predicted_query_hash: 0xAA,
519 blocks: vec![10, 20, 30],
520 confidence: 0.8,
521 pattern_id: 0,
522 created_ms: 0,
523 }],
524 stats: CacheStats::default(),
525 };
526 let result = cache.check(0xAA);
527 assert!(result.is_some());
528 let (blocks, conf) = result.unwrap();
529 assert_eq!(blocks, vec![10, 20, 30]);
530 assert!((conf - 0.8).abs() < 0.001);
531 }
532
533 #[test]
534 fn test_evaluate_hit() {
535 let mut cache = PredictiveCache {
536 predictions: vec![Prediction {
537 predicted_query_hash: 0xAA,
538 blocks: vec![10, 20, 30],
539 confidence: 0.5,
540 pattern_id: 0,
541 created_ms: 0,
542 }],
543 stats: CacheStats::default(),
544 };
545 let mut tg = make_tg();
546 tg.patterns.push(ThoughtPattern {
547 id: 0,
548 sequence: vec![0xBB, 0xAA],
549 frequency: 5,
550 strength: 1.0,
551 last_seen_ms: 0,
552 result_blocks: vec![10, 20, 30],
553 });
554
555 let actual = vec![10u32, 20, 30, 40];
556 let (hit_type, overlap) = cache.evaluate(0xAA, &actual, &mut tg);
557 assert_eq!(hit_type, "hit");
558 assert_eq!(overlap, 3);
559 assert_eq!(cache.stats.total_hits, 1);
560 assert!(tg.patterns[0].strength > 1.0);
562 }
563
564 #[test]
565 fn test_evaluate_miss() {
566 let mut cache = PredictiveCache {
567 predictions: vec![Prediction {
568 predicted_query_hash: 0xAA,
569 blocks: vec![10, 20, 30],
570 confidence: 0.5,
571 pattern_id: 0,
572 created_ms: 0,
573 }],
574 stats: CacheStats::default(),
575 };
576 let mut tg = make_tg();
577 tg.patterns.push(ThoughtPattern {
578 id: 0,
579 sequence: vec![0xBB, 0xAA],
580 frequency: 5,
581 strength: 1.0,
582 last_seen_ms: 0,
583 result_blocks: vec![10, 20, 30],
584 });
585
586 let actual = vec![100u32, 200, 300]; let (hit_type, overlap) = cache.evaluate(0xAA, &actual, &mut tg);
588 assert_eq!(hit_type, "miss");
589 assert_eq!(overlap, 0);
590 assert_eq!(cache.stats.total_misses, 1);
591 assert!(tg.patterns[0].strength < 1.0);
593 }
594
595 #[test]
596 fn test_evaluate_no_prediction() {
597 let mut cache = PredictiveCache {
598 predictions: Vec::new(),
599 stats: CacheStats::default(),
600 };
601 let mut tg = make_tg();
602 let (hit_type, _) = cache.evaluate(0xAA, &[10, 20], &mut tg);
603 assert_eq!(hit_type, "none");
604 }
605
606 #[test]
607 fn test_predict_next() {
608 let mut cache = PredictiveCache {
609 predictions: Vec::new(),
610 stats: CacheStats::default(),
611 };
612 let mut tg = make_tg();
613
614 tg.current_session_id = 1;
616 tg.nodes.push(crate::thought_graph::ThoughtNode {
617 timestamp_ms: 1000,
618 query_hash: 0xAA,
619 session_id: 1,
620 result_count: 3,
621 dominant_layer: 1,
622 centroid_hash: 0,
623 });
624 tg.patterns.push(ThoughtPattern {
625 id: 0,
626 sequence: vec![0xAA, 0xBB],
627 frequency: 5,
628 strength: 2.0,
629 last_seen_ms: 1000,
630 result_blocks: vec![10, 20, 30],
631 });
632
633 cache.predict_next(&tg);
634
635 assert_eq!(cache.predictions.len(), 1);
636 assert_eq!(cache.predictions[0].predicted_query_hash, 0xBB);
637 assert_eq!(cache.predictions[0].blocks, vec![10, 20, 30]);
638 assert!(cache.predictions[0].confidence > 0.0);
639 }
640
641 #[test]
642 fn test_predict_decay() {
643 let mut cache = PredictiveCache {
644 predictions: vec![Prediction {
645 predicted_query_hash: 0xAA,
646 blocks: vec![10],
647 confidence: MIN_CONFIDENCE + 0.01,
648 pattern_id: 0,
649 created_ms: 0,
650 }],
651 stats: CacheStats::default(),
652 };
653 let tg = make_tg();
654
655 for _ in 0..20 {
657 cache.predict_next(&tg);
658 }
659 assert!(cache.predictions.is_empty());
660 }
661
662 #[test]
663 fn test_hit_rate() {
664 let mut stats = CacheStats::default();
665 assert_eq!(stats.hit_rate(), 0.0);
666
667 stats.total_hits = 7;
668 stats.total_misses = 3;
669 assert!((stats.hit_rate() - 0.7).abs() < 0.001);
670
671 stats.total_partial_hits = 2;
672 assert!((stats.hit_rate() - 0.6667).abs() < 0.01);
674 }
675
676 #[test]
677 fn test_save_load_roundtrip() {
678 let dir = tempfile::tempdir().unwrap();
679
680 let cache = PredictiveCache {
681 predictions: vec![
682 Prediction {
683 predicted_query_hash: 0xAA,
684 blocks: vec![10, 20],
685 confidence: 0.75,
686 pattern_id: 1,
687 created_ms: 12345,
688 },
689 Prediction {
690 predicted_query_hash: 0xBB,
691 blocks: vec![30, 40, 50],
692 confidence: 0.5,
693 pattern_id: 2,
694 created_ms: 67890,
695 },
696 ],
697 stats: CacheStats {
698 total_predictions: 10,
699 total_hits: 5,
700 total_misses: 3,
701 total_partial_hits: 2,
702 current_predictions: 2,
703 avg_confidence: 0.625,
704 },
705 };
706
707 cache.save(dir.path()).unwrap();
708 let loaded = PredictiveCache::load_or_init(dir.path());
709
710 assert_eq!(loaded.predictions.len(), 2);
711 assert_eq!(loaded.predictions[0].predicted_query_hash, 0xAA);
712 assert_eq!(loaded.predictions[0].blocks, vec![10, 20]);
713 assert!((loaded.predictions[0].confidence - 0.75).abs() < 0.001);
714 assert_eq!(loaded.predictions[1].blocks, vec![30, 40, 50]);
715 assert_eq!(loaded.stats.total_hits, 5);
716 assert_eq!(loaded.stats.total_misses, 3);
717 assert_eq!(loaded.stats.total_partial_hits, 2);
718 }
719}