Skip to main content

mentedb_cognitive/
stream.rs

1use std::collections::VecDeque;
2
3use mentedb_core::types::MemoryId;
4use parking_lot::Mutex;
5
6#[derive(Debug, Clone)]
7pub enum TokenEvent {
8    Token(String),
9    EndOfTurn,
10    Correction(MemoryId, String),
11    Flush,
12}
13
14#[derive(Debug, Clone)]
15pub enum StreamAlert {
16    Contradiction {
17        memory_id: MemoryId,
18        ai_said: String,
19        stored: String,
20    },
21    Forgotten {
22        memory_id: MemoryId,
23        summary: String,
24    },
25    Correction {
26        memory_id: MemoryId,
27        old: String,
28        new: String,
29    },
30    Reinforcement {
31        memory_id: MemoryId,
32    },
33}
34
35/// Configuration for stream-based cognition alerts.
36#[derive(Debug, Clone)]
37pub struct StreamConfig {
38    /// Keyword overlap ratio above which a potential contradiction is flagged (default: 0.5).
39    pub contradiction_keyword_ratio: f32,
40    /// Keyword overlap ratio above which a reinforcement alert is emitted (default: 0.8).
41    pub reinforcement_threshold: f32,
42    /// Maximum number of tokens held in the ring buffer (default: 1000).
43    pub buffer_size: usize,
44}
45
46impl Default for StreamConfig {
47    fn default() -> Self {
48        Self {
49            contradiction_keyword_ratio: 0.5,
50            reinforcement_threshold: 0.8,
51            buffer_size: 1000,
52        }
53    }
54}
55
56struct StreamState {
57    buffer: VecDeque<String>,
58    accumulated: String,
59    buffer_size: usize,
60}
61
62pub struct CognitionStream {
63    state: Mutex<StreamState>,
64    config: StreamConfig,
65}
66
67impl CognitionStream {
68    pub fn new(buffer_size: usize) -> Self {
69        Self::with_config(StreamConfig {
70            buffer_size,
71            ..StreamConfig::default()
72        })
73    }
74
75    pub fn with_config(config: StreamConfig) -> Self {
76        Self {
77            state: Mutex::new(StreamState {
78                buffer: VecDeque::with_capacity(config.buffer_size),
79                accumulated: String::new(),
80                buffer_size: config.buffer_size,
81            }),
82            config,
83        }
84    }
85
86    pub fn feed_token(&self, token: &str) {
87        let mut state = self.state.lock();
88        if state.buffer.len() >= state.buffer_size {
89            // Drain oldest token into accumulated text before evicting
90            if let Some(old) = state.buffer.pop_front() {
91                state.accumulated.push_str(&old);
92            }
93        }
94        state.buffer.push_back(token.to_string());
95    }
96
97    pub fn check_alerts(&self, known_facts: &[(MemoryId, String)]) -> Vec<StreamAlert> {
98        let state = self.state.lock();
99        let mut full_text = state.accumulated.clone();
100        for t in &state.buffer {
101            full_text.push_str(t);
102        }
103        let full_lower = full_text.to_lowercase();
104
105        let mut alerts = Vec::new();
106        for (memory_id, fact) in known_facts {
107            let fact_lower = fact.to_lowercase();
108            // Extract significant keywords (3+ chars) from the stored fact
109            let keywords: Vec<&str> = fact_lower
110                .split_whitespace()
111                .filter(|w| w.len() >= 3)
112                .collect();
113
114            if keywords.is_empty() {
115                continue;
116            }
117
118            let matched = keywords
119                .iter()
120                .filter(|kw| full_lower.contains(*kw))
121                .count();
122            let ratio = matched as f32 / keywords.len() as f32;
123
124            // High keyword overlap but not identical text = potential contradiction
125            if ratio > self.config.contradiction_keyword_ratio && !full_lower.contains(&fact_lower)
126            {
127                // Check for negation patterns that suggest contradiction
128                let has_negation = full_lower.contains("not ")
129                    || full_lower.contains("never ")
130                    || full_lower.contains("don't ")
131                    || full_lower.contains("doesn't ")
132                    || full_lower.contains("isn't ")
133                    || full_lower.contains("actually ")
134                    || full_lower.contains("instead ");
135
136                if has_negation {
137                    alerts.push(StreamAlert::Contradiction {
138                        memory_id: *memory_id,
139                        ai_said: full_text.clone(),
140                        stored: fact.clone(),
141                    });
142                } else if ratio > self.config.reinforcement_threshold {
143                    alerts.push(StreamAlert::Reinforcement {
144                        memory_id: *memory_id,
145                    });
146                }
147            } else if ratio > self.config.reinforcement_threshold {
148                alerts.push(StreamAlert::Reinforcement {
149                    memory_id: *memory_id,
150                });
151            }
152        }
153        alerts
154    }
155
156    pub fn drain_buffer(&self) -> String {
157        let mut state = self.state.lock();
158        let mut result = std::mem::take(&mut state.accumulated);
159        for t in state.buffer.drain(..) {
160            result.push_str(&t);
161        }
162        result
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_feed_and_drain() {
172        let stream = CognitionStream::new(100);
173        stream.feed_token("hello ");
174        stream.feed_token("world");
175        assert_eq!(stream.drain_buffer(), "hello world");
176        assert_eq!(stream.drain_buffer(), "");
177    }
178
179    #[test]
180    fn test_ring_buffer_eviction() {
181        let stream = CognitionStream::new(2);
182        stream.feed_token("a");
183        stream.feed_token("b");
184        stream.feed_token("c");
185        let result = stream.drain_buffer();
186        assert_eq!(result, "abc");
187    }
188
189    #[test]
190    fn test_contradiction_alert() {
191        let stream = CognitionStream::new(100);
192        let mid = MemoryId::new();
193        stream.feed_token("The system does not use PostgreSQL, actually it uses MySQL");
194
195        let facts = vec![(mid, "The system uses PostgreSQL for storage".to_string())];
196        let alerts = stream.check_alerts(&facts);
197        assert!(
198            alerts
199                .iter()
200                .any(|a| matches!(a, StreamAlert::Contradiction { .. })),
201            "Expected contradiction alert, got: {:?}",
202            alerts
203        );
204    }
205}