use crate::memory::{MemoryEntry, MemoryTag};
use crate::state::Mood;
use crate::types::{Duration, EntityId, MicrosystemId};
pub const DEFAULT_SALIENCE_HALF_LIFE_DAYS: f32 = 30.0;
pub const WEIGHT_TAG_RELEVANCE: f64 = 0.25;
pub const WEIGHT_PARTICIPANT_MATCH: f64 = 0.20;
pub const WEIGHT_SALIENCE: f64 = 0.15;
pub const WEIGHT_RECENCY: f64 = 0.10;
pub const WEIGHT_MOOD_CONGRUENCE: f64 = 0.10;
pub const WEIGHT_CONTEXT_CONGRUENCE: f64 = 0.10;
pub const WEIGHT_SOURCE_CONFIDENCE: f64 = 0.05;
pub const WEIGHT_BASE_SCORE: f64 = 0.05;
#[derive(Debug, Clone)]
pub struct RetrievalQuery<'a> {
pub tags: Option<Vec<MemoryTag>>,
pub participant: Option<EntityId>,
pub current_mood: Option<&'a Mood>,
pub current_context: Option<MicrosystemId>,
pub limit: usize,
pub current_time: Duration,
}
impl<'a> RetrievalQuery<'a> {
#[must_use]
pub fn new(current_time: Duration) -> Self {
RetrievalQuery {
tags: None,
participant: None,
current_mood: None,
current_context: None,
limit: 10,
current_time,
}
}
#[must_use]
pub fn with_tags(mut self, tags: Vec<MemoryTag>) -> Self {
self.tags = Some(tags);
self
}
#[must_use]
pub fn with_participant(mut self, participant: EntityId) -> Self {
self.participant = Some(participant);
self
}
#[must_use]
pub fn with_mood(mut self, mood: &'a Mood) -> Self {
self.current_mood = Some(mood);
self
}
#[must_use]
pub fn with_context(mut self, context: MicrosystemId) -> Self {
self.current_context = Some(context);
self
}
#[must_use]
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
#[must_use]
pub fn compute_retrieval_score(entry: &MemoryEntry, query: &RetrievalQuery) -> f64 {
let tag_score = if let Some(ref query_tags) = query.tags {
if query_tags.is_empty() {
0.5 } else {
let matching = query_tags.iter().filter(|t| entry.has_tag(**t)).count();
matching as f64 / query_tags.len() as f64
}
} else {
0.5 };
let participant_score = if let Some(ref participant) = query.participant {
if entry.involves_participant(participant) {
1.0
} else {
0.0
}
} else {
0.5 };
let salience_score = entry.salience() as f64;
let recency_score = compute_recency_score(entry.timestamp(), query.current_time);
let mood_score = if let Some(mood) = query.current_mood {
entry
.emotional_snapshot()
.compute_congruence_with_mood(mood) as f64
} else {
0.5 };
let context_score = if let Some(ref context) = query.current_context {
if entry.is_in_context(context) {
1.0
} else {
0.0
}
} else {
0.5 };
let source_score = entry.source().confidence() as f64;
let base_score = 1.0;
tag_score * WEIGHT_TAG_RELEVANCE
+ participant_score * WEIGHT_PARTICIPANT_MATCH
+ salience_score * WEIGHT_SALIENCE
+ recency_score * WEIGHT_RECENCY
+ mood_score * WEIGHT_MOOD_CONGRUENCE
+ context_score * WEIGHT_CONTEXT_CONGRUENCE
+ source_score * WEIGHT_SOURCE_CONFIDENCE
+ base_score * WEIGHT_BASE_SCORE
}
fn compute_recency_score(memory_time: Duration, current_time: Duration) -> f64 {
let age_days = (current_time.as_days() as i64 - memory_time.as_days() as i64).max(0) as f64;
let half_life = DEFAULT_SALIENCE_HALF_LIFE_DAYS as f64;
(0.5_f64).powf(age_days / half_life)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory::EmotionalSnapshot;
#[test]
fn default_half_life_is_30_days() {
assert!((DEFAULT_SALIENCE_HALF_LIFE_DAYS - 30.0).abs() < f32::EPSILON);
}
#[test]
fn retrieval_weights_sum_to_one() {
let sum = WEIGHT_TAG_RELEVANCE
+ WEIGHT_PARTICIPANT_MATCH
+ WEIGHT_SALIENCE
+ WEIGHT_RECENCY
+ WEIGHT_MOOD_CONGRUENCE
+ WEIGHT_CONTEXT_CONGRUENCE
+ WEIGHT_SOURCE_CONFIDENCE
+ WEIGHT_BASE_SCORE;
assert!((sum - 1.0).abs() < f64::EPSILON);
}
#[test]
fn retrieval_query_builder() {
let mood = Mood::new();
let context = MicrosystemId::new("work").unwrap();
let participant = EntityId::new("person").unwrap();
let query = RetrievalQuery::new(Duration::days(100))
.with_tags(vec![MemoryTag::Personal])
.with_participant(participant.clone())
.with_mood(&mood)
.with_context(context.clone())
.with_limit(5);
assert!(query.tags.is_some());
assert_eq!(query.participant, Some(participant));
assert!(query.current_mood.is_some());
assert_eq!(query.current_context, Some(context));
assert_eq!(query.limit, 5);
}
#[test]
fn compute_retrieval_score_basic() {
let entry = MemoryEntry::new(Duration::days(10), "Test")
.with_salience(0.8)
.add_tag(MemoryTag::Personal);
let query = RetrievalQuery::new(Duration::days(10));
let score = compute_retrieval_score(&entry, &query);
assert!(score > 0.0);
assert!(score <= 1.0);
}
#[test]
fn compute_retrieval_score_with_matching_tags() {
let entry = MemoryEntry::new(Duration::days(10), "Test")
.with_salience(0.5)
.add_tag(MemoryTag::Personal);
let query_match =
RetrievalQuery::new(Duration::days(10)).with_tags(vec![MemoryTag::Personal]);
let query_no_match =
RetrievalQuery::new(Duration::days(10)).with_tags(vec![MemoryTag::Mission]);
let score_match = compute_retrieval_score(&entry, &query_match);
let score_no_match = compute_retrieval_score(&entry, &query_no_match);
assert!(score_match > score_no_match);
}
#[test]
fn compute_retrieval_score_with_empty_tags_is_neutral() {
let entry = MemoryEntry::new(Duration::days(10), "Test")
.with_salience(0.5)
.add_tag(MemoryTag::Personal);
let query_empty = RetrievalQuery::new(Duration::days(10)).with_tags(vec![]);
let query_none = RetrievalQuery::new(Duration::days(10));
let score_empty = compute_retrieval_score(&entry, &query_empty);
let score_none = compute_retrieval_score(&entry, &query_none);
assert!((score_empty - score_none).abs() < 0.01);
}
#[test]
fn compute_retrieval_score_with_participant() {
let participant = EntityId::new("person").unwrap();
let entry = MemoryEntry::new(Duration::days(10), "Test")
.with_salience(0.5)
.add_participant(participant.clone());
let query_match =
RetrievalQuery::new(Duration::days(10)).with_participant(participant.clone());
let other = EntityId::new("other").unwrap();
let query_no_match = RetrievalQuery::new(Duration::days(10)).with_participant(other);
let score_match = compute_retrieval_score(&entry, &query_match);
let score_no_match = compute_retrieval_score(&entry, &query_no_match);
assert!(score_match > score_no_match);
}
#[test]
fn compute_retrieval_score_context_congruence() {
let context = MicrosystemId::new("work").unwrap();
let entry = MemoryEntry::new(Duration::days(10), "Test")
.with_salience(0.5)
.with_microsystem_context(context.clone());
let query_match = RetrievalQuery::new(Duration::days(10)).with_context(context.clone());
let other = MicrosystemId::new("home").unwrap();
let query_no_match = RetrievalQuery::new(Duration::days(10)).with_context(other);
let score_match = compute_retrieval_score(&entry, &query_match);
let score_no_match = compute_retrieval_score(&entry, &query_no_match);
assert!(score_match > score_no_match);
}
#[test]
fn compute_retrieval_score_mood_congruence() {
let happy_mood = Mood::new().with_valence_base(0.8);
let sad_mood = Mood::new().with_valence_base(-0.8);
let happy_entry = MemoryEntry::new(Duration::days(10), "Happy memory")
.with_salience(0.5)
.with_emotional_snapshot(EmotionalSnapshot::new(0.8, 0.0, 0.0));
let query_happy = RetrievalQuery::new(Duration::days(10)).with_mood(&happy_mood);
let query_sad = RetrievalQuery::new(Duration::days(10)).with_mood(&sad_mood);
let score_congruent = compute_retrieval_score(&happy_entry, &query_happy);
let score_incongruent = compute_retrieval_score(&happy_entry, &query_sad);
assert!(score_congruent > score_incongruent);
}
#[test]
fn compute_retrieval_score_recency() {
let recent_entry = MemoryEntry::new(Duration::days(95), "Recent").with_salience(0.5);
let old_entry = MemoryEntry::new(Duration::days(10), "Old").with_salience(0.5);
let query = RetrievalQuery::new(Duration::days(100));
let score_recent = compute_retrieval_score(&recent_entry, &query);
let score_old = compute_retrieval_score(&old_entry, &query);
assert!(score_recent > score_old);
}
#[test]
fn compute_retrieval_score_source_confidence() {
use crate::memory::MemorySource;
let self_entry = MemoryEntry::new(Duration::days(10), "Self")
.with_salience(0.5)
.with_source(MemorySource::Self_);
let rumor_entry = MemoryEntry::new(Duration::days(10), "Rumor")
.with_salience(0.5)
.with_source(MemorySource::Rumor);
let query = RetrievalQuery::new(Duration::days(10));
let score_self = compute_retrieval_score(&self_entry, &query);
let score_rumor = compute_retrieval_score(&rumor_entry, &query);
assert!(score_self > score_rumor);
}
#[test]
fn recency_score_at_same_time_is_one() {
let score = compute_recency_score(Duration::days(100), Duration::days(100));
assert!((score - 1.0).abs() < f64::EPSILON);
}
#[test]
fn recency_score_decays_with_time() {
let score_fresh = compute_recency_score(Duration::days(100), Duration::days(100));
let score_half_life = compute_recency_score(Duration::days(70), Duration::days(100));
let score_old = compute_recency_score(Duration::days(40), Duration::days(100));
assert!(score_fresh > score_half_life);
assert!(score_half_life > score_old);
assert!((score_half_life - 0.5).abs() < 0.01);
}
#[test]
fn retrieval_query_debug() {
let query = RetrievalQuery::new(Duration::days(100));
let debug = format!("{:?}", query);
assert!(debug.contains("RetrievalQuery"));
}
#[test]
fn retrieval_query_clone() {
let query = RetrievalQuery::new(Duration::days(100)).with_limit(5);
let cloned = query.clone();
assert_eq!(cloned.limit, 5);
}
}