1use super::error::{MemoryError, Result};
40use super::math_engine::{MathEngine, MemoryParameters};
41use super::models::*;
42use chrono::Utc;
43use pgvector::Vector;
44use serde::{Deserialize, Serialize};
45use sqlx::postgres::types::PgInterval;
46use std::collections::HashMap;
47use tracing::{info, warn};
48use uuid::Uuid;
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CognitiveConsolidationConfig {
53 pub alpha: f64,
55
56 pub beta: f64,
58
59 pub context_weight: f64,
61
62 pub clustering_threshold: f64,
64
65 pub min_spacing_hours: f64,
67
68 pub max_strength: f64,
70
71 pub difficulty_scaling: f64,
73}
74
75impl Default for CognitiveConsolidationConfig {
76 fn default() -> Self {
77 Self {
78 alpha: 0.3, beta: 1.5, context_weight: 0.2, clustering_threshold: 0.75, min_spacing_hours: 0.5, max_strength: 15.0, difficulty_scaling: 1.2, }
86 }
87}
88
89#[derive(Debug, Clone)]
91pub struct RetrievalContext {
92 pub query_embedding: Option<Vector>,
93 pub environmental_factors: HashMap<String, f64>,
94 pub retrieval_latency_ms: u64,
95 pub confidence_score: f64,
96 pub related_memories: Vec<Uuid>,
97}
98
99#[derive(Debug, Clone)]
101pub struct CognitiveConsolidationResult {
102 pub new_consolidation_strength: f64,
103 pub strength_increment: f64,
104 pub recall_probability: f64,
105 pub spacing_bonus: f64,
106 pub difficulty_bonus: f64,
107 pub context_similarity: f64,
108 pub calculation_time_ms: u64,
109 pub cognitive_factors: CognitiveFactors,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct CognitiveFactors {
115 pub spacing_effect_strength: f64,
116 pub testing_effect_strength: f64,
117 pub semantic_clustering_bonus: f64,
118 pub context_dependent_boost: f64,
119 pub interference_penalty: f64,
120}
121
122pub struct CognitiveConsolidationEngine {
124 config: CognitiveConsolidationConfig,
125 math_engine: MathEngine,
126}
127
128impl CognitiveConsolidationEngine {
129 pub fn new(config: CognitiveConsolidationConfig) -> Self {
130 Self {
131 config,
132 math_engine: MathEngine::new(),
133 }
134 }
135
136 pub async fn calculate_cognitive_consolidation(
145 &self,
146 memory: &Memory,
147 context: &RetrievalContext,
148 similar_memories: &[Memory],
149 ) -> Result<CognitiveConsolidationResult> {
150 let start_time = std::time::Instant::now();
151
152 let params = MemoryParameters {
154 consolidation_strength: memory.consolidation_strength,
155 decay_rate: memory.decay_rate,
156 last_accessed_at: memory.last_accessed_at,
157 created_at: memory.created_at,
158 access_count: memory.access_count,
159 importance_score: memory.importance_score,
160 };
161
162 let _base_recall = self.math_engine.calculate_recall_probability(¶ms)?;
163
164 let spacing_effect = self.calculate_spacing_effect(memory)?;
166
167 let testing_effect = self.calculate_testing_effect(context)?;
169
170 let clustering_bonus =
172 self.calculate_semantic_clustering_bonus(memory, similar_memories)?;
173
174 let context_boost = self.calculate_context_boost(memory, context)?;
176
177 let interference_penalty = self.calculate_interference_penalty(memory, similar_memories)?;
179
180 let strength_increment = self.calculate_enhanced_strength_increment(
182 memory,
183 spacing_effect,
184 testing_effect,
185 clustering_bonus,
186 context_boost,
187 interference_penalty,
188 )?;
189
190 let new_strength = (memory.consolidation_strength + strength_increment)
191 .min(self.config.max_strength)
192 .max(0.1);
193
194 let enhanced_params = MemoryParameters {
196 consolidation_strength: new_strength,
197 ..params
198 };
199
200 let enhanced_recall = self
201 .math_engine
202 .calculate_recall_probability(&enhanced_params)?;
203
204 let calculation_time = start_time.elapsed().as_millis() as u64;
205
206 Ok(CognitiveConsolidationResult {
207 new_consolidation_strength: new_strength,
208 strength_increment,
209 recall_probability: enhanced_recall.recall_probability,
210 spacing_bonus: spacing_effect,
211 difficulty_bonus: testing_effect,
212 context_similarity: context_boost,
213 calculation_time_ms: calculation_time,
214 cognitive_factors: CognitiveFactors {
215 spacing_effect_strength: spacing_effect,
216 testing_effect_strength: testing_effect,
217 semantic_clustering_bonus: clustering_bonus,
218 context_dependent_boost: context_boost,
219 interference_penalty,
220 },
221 })
222 }
223
224 fn calculate_spacing_effect(&self, memory: &Memory) -> Result<f64> {
229 let current_time = Utc::now();
230
231 let last_access = memory.last_accessed_at.unwrap_or(memory.created_at);
233 let interval_hours = current_time
234 .signed_duration_since(last_access)
235 .num_seconds() as f64
236 / 3600.0;
237
238 if interval_hours < self.config.min_spacing_hours {
240 return Ok(0.1);
242 }
243
244 let optimal_interval = memory.consolidation_strength * 24.0; let spacing_ratio = interval_hours / optimal_interval;
250 let spacing_effect = if spacing_ratio < 0.5 {
251 spacing_ratio * 2.0
253 } else if spacing_ratio <= 2.0 {
254 1.0 + (spacing_ratio - 1.0) * 0.5
256 } else {
257 1.5 * (2.0 / spacing_ratio).min(1.0)
259 };
260
261 Ok(spacing_effect.max(0.1).min(2.0))
262 }
263
264 fn calculate_testing_effect(&self, context: &RetrievalContext) -> Result<f64> {
269 let difficulty = match context.retrieval_latency_ms {
271 0..=500 => 0.2, 501..=2000 => 1.0, 2001..=5000 => 1.5, _ => 0.8, };
276
277 let confidence_factor = 1.0 + (1.0 - context.confidence_score) * 0.5;
279
280 let testing_effect = difficulty * confidence_factor * self.config.difficulty_scaling;
281
282 Ok(testing_effect.max(0.1).min(2.0))
283 }
284
285 fn calculate_semantic_clustering_bonus(
290 &self,
291 memory: &Memory,
292 similar_memories: &[Memory],
293 ) -> Result<f64> {
294 if similar_memories.is_empty() || memory.embedding.is_none() {
295 return Ok(0.0);
296 }
297
298 let memory_embedding = memory.embedding.as_ref().unwrap();
299 let mut similarity_sum = 0.0;
300 let mut high_similarity_count = 0;
301
302 for similar_memory in similar_memories {
303 if let Some(similar_embedding) = &similar_memory.embedding {
304 let similarity =
306 self.calculate_cosine_similarity(memory_embedding, similar_embedding)?;
307
308 if similarity > self.config.clustering_threshold {
309 high_similarity_count += 1;
310 similarity_sum += similarity;
311 }
312 }
313 }
314
315 if high_similarity_count == 0 {
316 return Ok(0.0);
317 }
318
319 let avg_similarity = similarity_sum / high_similarity_count as f64;
321 let density_bonus = (high_similarity_count as f64).ln() / 10.0; let clustering_bonus = avg_similarity * density_bonus;
324
325 Ok(clustering_bonus.max(0.0).min(1.0))
326 }
327
328 fn calculate_context_boost(&self, memory: &Memory, context: &RetrievalContext) -> Result<f64> {
333 let memory_context = memory
335 .metadata
336 .get("environmental_context")
337 .and_then(|v| v.as_object())
338 .map(|obj| {
339 obj.iter()
340 .filter_map(|(k, v)| v.as_f64().map(|val| (k.clone(), val)))
341 .collect::<HashMap<String, f64>>()
342 })
343 .unwrap_or_default();
344
345 if memory_context.is_empty() || context.environmental_factors.is_empty() {
346 return Ok(0.0);
347 }
348
349 let mut context_similarity = 0.0;
351 let mut matching_factors = 0;
352
353 for (factor, current_value) in &context.environmental_factors {
354 if let Some(memory_value) = memory_context.get(factor) {
355 let factor_similarity = 1.0 - (current_value - memory_value).abs().min(1.0);
356 context_similarity += factor_similarity;
357 matching_factors += 1;
358 }
359 }
360
361 if matching_factors == 0 {
362 return Ok(0.0);
363 }
364
365 let avg_context_similarity = context_similarity / matching_factors as f64;
366 let context_boost = avg_context_similarity * self.config.context_weight;
367
368 Ok(context_boost.max(0.0).min(0.5))
369 }
370
371 fn calculate_interference_penalty(
376 &self,
377 memory: &Memory,
378 similar_memories: &[Memory],
379 ) -> Result<f64> {
380 if similar_memories.is_empty() || memory.embedding.is_none() {
381 return Ok(0.0);
382 }
383
384 let memory_embedding = memory.embedding.as_ref().unwrap();
385 let mut interference_total = 0.0;
386
387 for similar_memory in similar_memories {
388 if similar_memory.id == memory.id {
389 continue; }
391
392 if let Some(similar_embedding) = &similar_memory.embedding {
393 let similarity =
394 self.calculate_cosine_similarity(memory_embedding, similar_embedding)?;
395
396 let strength_ratio =
398 similar_memory.consolidation_strength / memory.consolidation_strength;
399 let interference_strength = similarity * strength_ratio.min(2.0);
400
401 interference_total += interference_strength;
402 }
403 }
404
405 let interference_penalty = (1.0 + interference_total).ln() / 10.0;
407
408 Ok(interference_penalty.max(0.0).min(0.3))
409 }
410
411 fn calculate_enhanced_strength_increment(
413 &self,
414 memory: &Memory,
415 spacing_effect: f64,
416 testing_effect: f64,
417 clustering_bonus: f64,
418 context_boost: f64,
419 interference_penalty: f64,
420 ) -> Result<f64> {
421 let base_increment = if let Some(last_access) = memory.last_accessed_at {
423 let hours_since_access =
424 Utc::now().signed_duration_since(last_access).num_seconds() as f64 / 3600.0;
425 let base = (1.0 - (-self.config.beta * hours_since_access).exp())
426 / (1.0 + (-self.config.beta * hours_since_access).exp());
427 self.config.alpha * base
428 } else {
429 self.config.alpha * 0.5 };
431
432 let cognitive_multiplier = spacing_effect
434 * testing_effect
435 * (1.0 + clustering_bonus + context_boost - interference_penalty);
436
437 let enhanced_increment = base_increment * cognitive_multiplier;
438
439 Ok(enhanced_increment.max(0.01).min(2.0))
441 }
442
443 fn calculate_cosine_similarity(&self, vec1: &Vector, vec2: &Vector) -> Result<f64> {
445 let slice1 = vec1.as_slice();
446 let slice2 = vec2.as_slice();
447
448 if slice1.len() != slice2.len() {
449 return Err(MemoryError::InvalidRequest {
450 message: "Vector dimensions must match for similarity calculation".to_string(),
451 });
452 }
453
454 let dot_product: f64 = slice1
455 .iter()
456 .zip(slice2.iter())
457 .map(|(a, b)| (*a as f64) * (*b as f64))
458 .sum();
459
460 let norm1: f64 = slice1
461 .iter()
462 .map(|x| (*x as f64).powi(2))
463 .sum::<f64>()
464 .sqrt();
465 let norm2: f64 = slice2
466 .iter()
467 .map(|x| (*x as f64).powi(2))
468 .sum::<f64>()
469 .sqrt();
470
471 if norm1 == 0.0 || norm2 == 0.0 {
472 return Ok(0.0);
473 }
474
475 Ok(dot_product / (norm1 * norm2))
476 }
477
478 pub async fn apply_consolidation_results(
480 &self,
481 memory: &mut Memory,
482 result: &CognitiveConsolidationResult,
483 repository: &crate::memory::repository::MemoryRepository,
484 ) -> Result<()> {
485 let previous_strength = memory.consolidation_strength;
486 let previous_probability = memory.recall_probability;
487
488 memory.consolidation_strength = result.new_consolidation_strength;
490 memory.recall_probability = Some(result.recall_probability);
491 memory.access_count += 1;
492 memory.last_accessed_at = Some(Utc::now());
493
494 let recall_interval = if let Some(last_access) = memory.last_accessed_at {
496 let duration = Utc::now().signed_duration_since(last_access);
497 PgInterval {
498 months: 0,
499 days: duration.num_days() as i32,
500 microseconds: (duration.num_microseconds().unwrap_or(0) % (24 * 60 * 60 * 1000000)),
501 }
502 } else {
503 PgInterval {
504 months: 0,
505 days: 0,
506 microseconds: 0,
507 }
508 };
509
510 memory.last_recall_interval = Some(recall_interval);
511
512 let context = serde_json::json!({
514 "cognitive_factors": result.cognitive_factors,
515 "spacing_bonus": result.spacing_bonus,
516 "difficulty_bonus": result.difficulty_bonus,
517 "context_similarity": result.context_similarity,
518 "calculation_time_ms": result.calculation_time_ms
519 });
520
521 repository
522 .log_consolidation_event(
523 memory.id,
524 "cognitive_consolidation",
525 previous_strength,
526 result.new_consolidation_strength,
527 previous_probability,
528 Some(result.recall_probability),
529 Some(recall_interval),
530 context,
531 )
532 .await?;
533
534 info!(
535 "Applied cognitive consolidation to memory {}: strength {:.3} -> {:.3}, recall {:.3}",
536 memory.id,
537 previous_strength,
538 result.new_consolidation_strength,
539 result.recall_probability
540 );
541
542 Ok(())
543 }
544}
545
546pub struct CognitiveConsolidationService {
548 engine: CognitiveConsolidationEngine,
549 repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
550}
551
552impl CognitiveConsolidationService {
553 pub fn new(
554 config: CognitiveConsolidationConfig,
555 repository: std::sync::Arc<crate::memory::repository::MemoryRepository>,
556 ) -> Self {
557 Self {
558 engine: CognitiveConsolidationEngine::new(config),
559 repository,
560 }
561 }
562
563 pub async fn process_batch_consolidation(
565 &self,
566 memory_ids: &[Uuid],
567 context: &RetrievalContext,
568 ) -> Result<Vec<CognitiveConsolidationResult>> {
569 let mut results = Vec::with_capacity(memory_ids.len());
570
571 for &memory_id in memory_ids {
572 match self
573 .process_single_memory_consolidation(memory_id, context)
574 .await
575 {
576 Ok(result) => results.push(result),
577 Err(e) => {
578 warn!(
579 "Failed to process consolidation for memory {}: {}",
580 memory_id, e
581 );
582 }
584 }
585 }
586
587 Ok(results)
588 }
589
590 async fn process_single_memory_consolidation(
591 &self,
592 memory_id: Uuid,
593 context: &RetrievalContext,
594 ) -> Result<CognitiveConsolidationResult> {
595 let mut memory = self.repository.get_memory(memory_id).await?;
597
598 let similar_memories = self.find_similar_memories(&memory).await?;
600
601 let result = self
603 .engine
604 .calculate_cognitive_consolidation(&memory, context, &similar_memories)
605 .await?;
606
607 self.engine
609 .apply_consolidation_results(&mut memory, &result, &self.repository)
610 .await?;
611
612 Ok(result)
613 }
614
615 async fn find_similar_memories(&self, memory: &Memory) -> Result<Vec<Memory>> {
616 if memory.embedding.is_none() {
617 return Ok(Vec::new());
618 }
619
620 let search_request = SearchRequest {
622 query_embedding: Some(memory.embedding.as_ref().unwrap().as_slice().to_vec()),
623 search_type: Some(SearchType::Semantic),
624 similarity_threshold: Some(0.7),
625 limit: Some(20),
626 tier: None, ..Default::default()
628 };
629
630 let search_response = self.repository.search_memories(search_request).await?;
631
632 Ok(search_response
633 .results
634 .into_iter()
635 .filter(|result| result.memory.id != memory.id) .map(|result| result.memory)
637 .collect())
638 }
639}
640
641#[cfg(test)]
642mod tests {
643 use super::*;
644 use chrono::Duration;
645
646 fn create_test_memory() -> Memory {
647 let mut memory = Memory::default();
648 memory.consolidation_strength = 2.0;
649 memory.access_count = 3;
650 memory.last_accessed_at = Some(Utc::now() - Duration::hours(2));
651 memory.importance_score = 0.7;
652 memory
653 }
654
655 fn create_test_context() -> RetrievalContext {
656 RetrievalContext {
657 query_embedding: None,
658 environmental_factors: HashMap::new(),
659 retrieval_latency_ms: 1500, confidence_score: 0.8,
661 related_memories: Vec::new(),
662 }
663 }
664
665 #[test]
666 fn test_spacing_effect_calculation() {
667 let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
668 let memory = create_test_memory();
669
670 let spacing_effect = engine.calculate_spacing_effect(&memory).unwrap();
671
672 assert!(spacing_effect > 0.0);
674 assert!(spacing_effect <= 2.0);
675 }
676
677 #[test]
678 fn test_testing_effect_calculation() {
679 let engine = CognitiveConsolidationEngine::new(CognitiveConsolidationConfig::default());
680 let context = create_test_context();
681
682 let testing_effect = engine.calculate_testing_effect(&context).unwrap();
683
684 assert!(testing_effect > 0.0);
686 assert!(testing_effect <= 2.0);
687 }
688
689 #[test]
690 fn test_cognitive_factors_bounds() {
691 let config = CognitiveConsolidationConfig::default();
692
693 assert!(config.alpha > 0.0 && config.alpha < 1.0);
695 assert!(config.beta > 0.0 && config.beta < 5.0);
696 assert!(config.context_weight >= 0.0 && config.context_weight <= 1.0);
697 assert!(config.clustering_threshold > 0.5 && config.clustering_threshold <= 1.0);
698 }
699}