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#[derive(Debug, Clone)]
51pub struct WriteInferenceConfig {
52 pub contradiction_threshold: f32,
54 pub obsolete_threshold: f32,
56 pub related_min: f32,
58 pub related_max: f32,
60 pub correction_threshold: f32,
62 pub confidence_decay_factor: f32,
64 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; 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 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 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 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 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 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}