Skip to main content

mentedb_cognitive/
interference.rs

1use 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    /// Reorder memories so interference pairs are never adjacent
82    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        // Greedy: pick the first non-conflicting memory from remaining
99        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                // Find a non-conflicting one
110                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); // put conflicting back
116                        found = true;
117                        break;
118                    }
119                }
120                if !found {
121                    // No choice, just place it
122                    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        // a and b should interfere (high similarity, different content)
160        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        // ids[0] and ids[1] should not be adjacent
176        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}