Skip to main content

mentedb_cognitive/
write_inference.rs

1use mentedb_core::MemoryNode;
2use mentedb_core::edge::EdgeType;
3use mentedb_core::memory::MemoryType;
4use mentedb_core::types::MemoryId;
5
6#[derive(Debug, Clone)]
7pub enum InferredAction {
8    FlagContradiction {
9        existing: MemoryId,
10        new: MemoryId,
11        reason: String,
12    },
13    MarkObsolete {
14        memory: MemoryId,
15        superseded_by: MemoryId,
16    },
17    CreateEdge {
18        source: MemoryId,
19        target: MemoryId,
20        edge_type: EdgeType,
21        weight: f32,
22    },
23    UpdateConfidence {
24        memory: MemoryId,
25        new_confidence: f32,
26    },
27    PropagateBeliefChange {
28        root: MemoryId,
29        delta: f32,
30    },
31}
32
33fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
34    if a.len() != b.len() || a.is_empty() {
35        return 0.0;
36    }
37    let mut dot = 0.0f32;
38    let mut norm_a = 0.0f32;
39    let mut norm_b = 0.0f32;
40    for i in 0..a.len() {
41        dot += a[i] * b[i];
42        norm_a += a[i] * a[i];
43        norm_b += b[i] * b[i];
44    }
45    let denom = norm_a.sqrt() * norm_b.sqrt();
46    if denom == 0.0 { 0.0 } else { dot / denom }
47}
48
49/// Configuration for write-time inference thresholds.
50#[derive(Debug, Clone)]
51pub struct WriteInferenceConfig {
52    /// Similarity above which two memories may contradict (default: 0.95).
53    pub contradiction_threshold: f32,
54    /// Similarity above which an older memory is marked obsolete (default: 0.85).
55    pub obsolete_threshold: f32,
56    /// Minimum similarity for creating a Related edge (default: 0.6).
57    pub related_min: f32,
58    /// Maximum similarity for creating a Related edge (default: 0.85).
59    pub related_max: f32,
60    /// Minimum similarity for a Correction to supersede (default: 0.5).
61    pub correction_threshold: f32,
62    /// Multiplier applied to original confidence on correction (default: 0.5).
63    pub confidence_decay_factor: f32,
64    /// Minimum confidence after decay (default: 0.1).
65    pub confidence_floor: f32,
66}
67
68impl Default for WriteInferenceConfig {
69    fn default() -> Self {
70        Self {
71            contradiction_threshold: 0.95,
72            obsolete_threshold: 0.85,
73            related_min: 0.6,
74            related_max: 0.85,
75            correction_threshold: 0.5,
76            confidence_decay_factor: 0.5,
77            confidence_floor: 0.1,
78        }
79    }
80}
81
82pub struct WriteInferenceEngine {
83    config: WriteInferenceConfig,
84}
85
86impl WriteInferenceEngine {
87    pub fn new() -> Self {
88        Self {
89            config: WriteInferenceConfig::default(),
90        }
91    }
92
93    pub fn with_config(config: WriteInferenceConfig) -> Self {
94        Self { config }
95    }
96
97    pub fn infer_on_write(
98        &self,
99        new_memory: &MemoryNode,
100        existing_memories: &[MemoryNode],
101        existing_edges: &[(MemoryId, MemoryId, EdgeType)],
102    ) -> Vec<InferredAction> {
103        let _ = existing_edges; // reserved for future graph-aware inference
104        let mut actions = Vec::new();
105
106        for existing in existing_memories {
107            if existing.id == new_memory.id {
108                continue;
109            }
110
111            let sim = cosine_similarity(&new_memory.embedding, &existing.embedding);
112
113            // Very high similarity: potential duplicate or contradiction
114            if sim > self.config.contradiction_threshold
115                && existing.agent_id == new_memory.agent_id
116                && existing.content != new_memory.content
117            {
118                actions.push(InferredAction::FlagContradiction {
119                    existing: existing.id,
120                    new: new_memory.id,
121                    reason: format!(
122                        "High embedding similarity ({:.3}) with different content from same agent",
123                        sim
124                    ),
125                });
126            }
127
128            // High similarity: mark older as obsolete if newer timestamp
129            if sim > self.config.obsolete_threshold && new_memory.created_at > existing.created_at {
130                actions.push(InferredAction::MarkObsolete {
131                    memory: existing.id,
132                    superseded_by: new_memory.id,
133                });
134            }
135
136            // Moderate similarity: create Related edge
137            if sim > self.config.related_min && sim <= self.config.related_max {
138                actions.push(InferredAction::CreateEdge {
139                    source: new_memory.id,
140                    target: existing.id,
141                    edge_type: EdgeType::Related,
142                    weight: sim,
143                });
144            }
145        }
146
147        // Correction type: find the most similar existing memory and supersede it
148        if new_memory.memory_type == MemoryType::Correction
149            && let Some(original) = existing_memories
150                .iter()
151                .filter(|m| m.id != new_memory.id)
152                .max_by(|a, b| {
153                    cosine_similarity(&new_memory.embedding, &a.embedding)
154                        .partial_cmp(&cosine_similarity(&new_memory.embedding, &b.embedding))
155                        .unwrap_or(std::cmp::Ordering::Equal)
156                })
157        {
158            let sim = cosine_similarity(&new_memory.embedding, &original.embedding);
159            if sim > self.config.correction_threshold {
160                actions.push(InferredAction::CreateEdge {
161                    source: new_memory.id,
162                    target: original.id,
163                    edge_type: EdgeType::Supersedes,
164                    weight: 1.0,
165                });
166                actions.push(InferredAction::UpdateConfidence {
167                    memory: original.id,
168                    new_confidence: (original.confidence * self.config.confidence_decay_factor)
169                        .max(self.config.confidence_floor),
170                });
171            }
172        }
173
174        actions
175    }
176}
177
178impl Default for WriteInferenceEngine {
179    fn default() -> Self {
180        Self::new()
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use mentedb_core::memory::MemoryType;
188
189    fn make_memory(content: &str, embedding: Vec<f32>, mem_type: MemoryType) -> MemoryNode {
190        let mut m = MemoryNode::new(
191            uuid::Uuid::new_v4(),
192            mem_type,
193            content.to_string(),
194            embedding,
195        );
196        m.created_at = 1000;
197        m
198    }
199
200    #[test]
201    fn test_flag_contradiction() {
202        let agent = uuid::Uuid::new_v4();
203        let mut existing =
204            make_memory("uses PostgreSQL", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
205        existing.agent_id = agent;
206
207        let mut new_mem = make_memory("uses MySQL", vec![0.99, 0.01, 0.0], MemoryType::Semantic);
208        new_mem.agent_id = agent;
209        new_mem.created_at = 2000;
210
211        let engine = WriteInferenceEngine::new();
212        let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
213        assert!(
214            actions
215                .iter()
216                .any(|a| matches!(a, InferredAction::FlagContradiction { .. })),
217            "Expected FlagContradiction, got: {:?}",
218            actions
219        );
220    }
221
222    #[test]
223    fn test_moderate_similarity_creates_edge() {
224        let existing = make_memory("topic A", vec![1.0, 0.0, 0.0], MemoryType::Semantic);
225        // ~0.7 similarity
226        let new_mem = make_memory("topic B", vec![0.7, 0.714, 0.0], MemoryType::Semantic);
227
228        let engine = WriteInferenceEngine::new();
229        let actions = engine.infer_on_write(&new_mem, &[existing], &[]);
230        assert!(
231            actions.iter().any(|a| matches!(
232                a,
233                InferredAction::CreateEdge {
234                    edge_type: EdgeType::Related,
235                    ..
236                }
237            )),
238            "Expected CreateEdge Related, got: {:?}",
239            actions
240        );
241    }
242}