mentedb_cognitive/
stream.rs1use 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#[derive(Debug, Clone)]
37pub struct StreamConfig {
38 pub contradiction_keyword_ratio: f32,
40 pub reinforcement_threshold: f32,
42 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 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 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 if ratio > self.config.contradiction_keyword_ratio && !full_lower.contains(&fact_lower)
126 {
127 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}