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 use mentedb_core::types::AgentId;
142
143 fn make_memory(content: &str, embedding: Vec<f32>) -> MemoryNode {
144 MemoryNode::new(
145 AgentId::new(),
146 MemoryType::Semantic,
147 content.to_string(),
148 embedding,
149 )
150 }
151
152 #[test]
153 fn test_detect_interference() {
154 let a = make_memory("Project Alpha uses React", vec![1.0, 0.0, 0.0]);
155 let b = make_memory("Project Beta uses Vue", vec![0.99, 0.1, 0.0]);
156 let c = make_memory("Cooking recipe for pasta", vec![0.0, 0.0, 1.0]);
157
158 let detector = InterferenceDetector::default();
159 let pairs = detector.detect_interference(&[a, b, c]);
160 assert_eq!(pairs.len(), 1);
162 assert!(pairs[0].disambiguation.contains("Do not confuse"));
163 }
164
165 #[test]
166 fn test_arrange_separation() {
167 let ids: Vec<MemoryId> = (0..4).map(|_| MemoryId::new()).collect();
168 let pairs = vec![InterferencePair {
169 memory_a: ids[0],
170 memory_b: ids[1],
171 similarity: 0.9,
172 disambiguation: String::new(),
173 }];
174
175 let arranged = InterferenceDetector::arrange_with_separation(ids.clone(), &pairs);
176 for w in arranged.windows(2) {
178 assert!(
179 !(w[0] == ids[0] && w[1] == ids[1]) && !(w[0] == ids[1] && w[1] == ids[0]),
180 "Interference pair should not be adjacent"
181 );
182 }
183 }
184}