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