use serde::{Deserialize, Serialize};
use super::types::{Emotion, MoodVector};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EmotionalMemory {
pub tag: String,
pub mood: MoodVector,
pub intensity: f32,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EmotionalMemoryBank {
memories: Vec<EmotionalMemory>,
capacity: usize,
}
impl EmotionalMemoryBank {
#[must_use]
pub fn new(capacity: usize) -> Self {
Self {
memories: Vec::new(),
capacity: capacity.max(1),
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn record(&mut self, tag: impl Into<String>, mood: &MoodVector, intensity: f32) {
let tag = tag.into();
if let Some(existing) = self.memories.iter_mut().find(|m| m.tag == tag) {
existing.mood = mood.clone();
existing.intensity = intensity.clamp(0.0, 1.0);
} else {
if self.memories.len() >= self.capacity {
if let Some(weakest) = self
.memories
.iter()
.enumerate()
.min_by(|a, b| {
a.1.intensity
.partial_cmp(&b.1.intensity)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(i, _)| i)
{
self.memories.swap_remove(weakest);
}
}
self.memories.push(EmotionalMemory {
tag,
mood: mood.clone(),
intensity: intensity.clamp(0.0, 1.0),
});
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn recall(&self, tag: &str) -> Option<MoodVector> {
self.memories.iter().find(|m| m.tag == tag).map(|m| {
let mut recalled = m.mood.clone();
for &e in Emotion::ALL {
recalled.set(e, recalled.get(e) * m.intensity);
}
recalled
})
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn decay(&mut self, rate: f32) {
let r = rate.clamp(0.0, 1.0);
self.memories.retain_mut(|m| {
m.intensity *= 1.0 - r;
m.intensity > 0.01
});
}
#[must_use]
pub fn len(&self) -> usize {
self.memories.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.memories.is_empty()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn recall_congruent(
&self,
current_mood: &MoodVector,
top_n: usize,
) -> Vec<&EmotionalMemory> {
let mut scored: Vec<(&EmotionalMemory, f32)> = self
.memories
.iter()
.map(|m| {
let similarity = mood_cosine(current_mood, &m.mood);
let score = similarity * m.intensity;
(m, score)
})
.collect();
scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
scored.into_iter().take(top_n).map(|(m, _)| m).collect()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn recall_biased(&self, tag: &str, current_mood: &MoodVector) -> Option<MoodVector> {
self.memories.iter().find(|m| m.tag == tag).map(|m| {
let congruence = mood_cosine(current_mood, &m.mood);
let amplifier = 0.5 + congruence.clamp(-1.0, 1.0) * 0.5;
let effective_intensity = (m.intensity * amplifier).clamp(0.0, 1.0);
let mut recalled = m.mood.clone();
for &e in Emotion::ALL {
recalled.set(e, recalled.get(e) * effective_intensity);
}
recalled
})
}
}
fn mood_cosine(a: &MoodVector, b: &MoodVector) -> f32 {
let mut dot = 0.0f32;
let mut mag_a = 0.0f32;
let mut mag_b = 0.0f32;
for &e in Emotion::ALL {
let va = a.get(e);
let vb = b.get(e);
dot += va * vb;
mag_a += va * va;
mag_b += vb * vb;
}
let denom = mag_a.sqrt() * mag_b.sqrt();
if denom < f32::EPSILON {
return 0.0;
}
dot / denom
}