mentedb_cognitive/
interference.rs1use mentedb_core::MemoryNode;
2use mentedb_core::types::MemoryId;
3
4#[derive(Debug, Clone)]
5pub struct InterferencePair {
6 pub memory_a: MemoryId,
7 pub memory_b: MemoryId,
8 pub similarity: f32,
9 pub disambiguation: String,
10}
11
12pub struct InterferenceDetector {
13 similarity_threshold: f32,
14 truncation_length: usize,
15}
16
17fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
18 if a.len() != b.len() || a.is_empty() {
19 return 0.0;
20 }
21 let mut dot = 0.0f32;
22 let mut norm_a = 0.0f32;
23 let mut norm_b = 0.0f32;
24 for i in 0..a.len() {
25 dot += a[i] * b[i];
26 norm_a += a[i] * a[i];
27 norm_b += b[i] * b[i];
28 }
29 let denom = norm_a.sqrt() * norm_b.sqrt();
30 if denom == 0.0 { 0.0 } else { dot / denom }
31}
32
33fn truncate_content(s: &str, max_len: usize) -> &str {
34 if s.len() <= max_len {
35 s
36 } else {
37 &s[..s.floor_char_boundary(max_len)]
38 }
39}
40
41impl InterferenceDetector {
42 pub fn new(similarity_threshold: f32) -> Self {
43 Self {
44 similarity_threshold,
45 truncation_length: 80,
46 }
47 }
48
49 pub fn with_truncation_length(mut self, truncation_length: usize) -> Self {
50 self.truncation_length = truncation_length;
51 self
52 }
53
54 pub fn detect_interference(&self, memories: &[MemoryNode]) -> Vec<InterferencePair> {
55 let mut pairs = Vec::new();
56 for i in 0..memories.len() {
57 for j in (i + 1)..memories.len() {
58 let sim = cosine_similarity(&memories[i].embedding, &memories[j].embedding);
59 if sim > self.similarity_threshold && memories[i].content != memories[j].content {
60 pairs.push(InterferencePair {
61 memory_a: memories[i].id,
62 memory_b: memories[j].id,
63 similarity: sim,
64 disambiguation: self.generate_disambiguation(&memories[i], &memories[j]),
65 });
66 }
67 }
68 }
69 pairs
70 }
71
72 pub fn generate_disambiguation(&self, a: &MemoryNode, b: &MemoryNode) -> String {
73 let a_content = truncate_content(&a.content, self.truncation_length);
74 let b_content = truncate_content(&b.content, self.truncation_length);
75 format!(
76 "Note: Memory A: \"{}\" (created {}), Memory B: \"{}\" (created {}). Do not confuse.",
77 a_content, a.created_at, b_content, b.created_at
78 )
79 }
80
81 pub fn arrange_with_separation(
83 memories: Vec<MemoryId>,
84 pairs: &[InterferencePair],
85 ) -> Vec<MemoryId> {
86 if memories.is_empty() || pairs.is_empty() {
87 return memories;
88 }
89
90 let conflicts: ahash::AHashSet<(MemoryId, MemoryId)> = pairs
91 .iter()
92 .flat_map(|p| [(p.memory_a, p.memory_b), (p.memory_b, p.memory_a)])
93 .collect();
94
95 let mut result: Vec<MemoryId> = Vec::with_capacity(memories.len());
96 let mut remaining: std::collections::VecDeque<MemoryId> = memories.into();
97
98 while let Some(first) = remaining.pop_front() {
100 if result.is_empty() {
101 result.push(first);
102 continue;
103 }
104
105 let last = *result.last().unwrap();
106 if !conflicts.contains(&(last, first)) {
107 result.push(first);
108 } else {
109 let mut found = false;
111 for i in 0..remaining.len() {
112 if !conflicts.contains(&(last, remaining[i])) {
113 let item = remaining.remove(i).unwrap();
114 result.push(item);
115 remaining.push_front(first); found = true;
117 break;
118 }
119 }
120 if !found {
121 result.push(first);
123 }
124 }
125 }
126
127 result
128 }
129}
130
131impl Default for InterferenceDetector {
132 fn default() -> Self {
133 Self::new(0.8)
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140 use mentedb_core::memory::MemoryType;
141
142 fn make_memory(content: &str, embedding: Vec<f32>) -> MemoryNode {
143 MemoryNode::new(
144 uuid::Uuid::new_v4(),
145 MemoryType::Semantic,
146 content.to_string(),
147 embedding,
148 )
149 }
150
151 #[test]
152 fn test_detect_interference() {
153 let a = make_memory("Project Alpha uses React", vec![1.0, 0.0, 0.0]);
154 let b = make_memory("Project Beta uses Vue", vec![0.99, 0.1, 0.0]);
155 let c = make_memory("Cooking recipe for pasta", vec![0.0, 0.0, 1.0]);
156
157 let detector = InterferenceDetector::default();
158 let pairs = detector.detect_interference(&[a, b, c]);
159 assert_eq!(pairs.len(), 1);
161 assert!(pairs[0].disambiguation.contains("Do not confuse"));
162 }
163
164 #[test]
165 fn test_arrange_separation() {
166 let ids: Vec<MemoryId> = (0..4).map(|_| uuid::Uuid::new_v4()).collect();
167 let pairs = vec![InterferencePair {
168 memory_a: ids[0],
169 memory_b: ids[1],
170 similarity: 0.9,
171 disambiguation: String::new(),
172 }];
173
174 let arranged = InterferenceDetector::arrange_with_separation(ids.clone(), &pairs);
175 for w in arranged.windows(2) {
177 assert!(
178 !(w[0] == ids[0] && w[1] == ids[1]) && !(w[0] == ids[1] && w[1] == ids[0]),
179 "Interference pair should not be adjacent"
180 );
181 }
182 }
183}