Skip to main content

st/mem8/
conversation.rs

1//! Conversation Memory System for Smart Tree
2//! Intelligently detects, parses, and stores conversation data in MEM|8 format
3//!
4//! "Every conversation is a wave pattern waiting to be preserved" - Omni
5
6use anyhow::{Context, Result};
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value};
9use std::fs;
10use std::path::PathBuf;
11use std::time::SystemTime;
12
13use super::wave::{MemoryWave, WaveGrid};
14
15/// Conversation memory manager
16pub struct ConversationMemory {
17    /// Base directory for storing conversations (~/.mem8/conversations/)
18    base_path: PathBuf,
19    /// Wave grid for memory storage (lazy-initialized to avoid 34GB allocation!)
20    wave_grid: Option<WaveGrid>,
21    /// Smart structure analyzer
22    analyzer: ConversationAnalyzer,
23}
24
25impl ConversationMemory {
26    /// Create a new conversation memory system
27    pub fn new() -> Result<Self> {
28        let home_dir = dirs::home_dir().context("Failed to get home directory")?;
29
30        let base_path = home_dir.join(".mem8").join("conversations");
31
32        // Create directory if it doesn't exist
33        fs::create_dir_all(&base_path)?;
34
35        Ok(Self {
36            base_path,
37            wave_grid: None, // Don't allocate 34GB until actually needed!
38            analyzer: ConversationAnalyzer::new(),
39        })
40    }
41
42    /// Ensure wave grid is initialized (lazy initialization)
43    fn ensure_wave_grid(&mut self) {
44        if self.wave_grid.is_none() {
45            self.wave_grid = Some(WaveGrid::new());
46        }
47    }
48
49    /// Intelligently detect and save conversation from JSON
50    pub fn save_conversation(
51        &mut self,
52        json_data: &Value,
53        source: Option<&str>,
54    ) -> Result<PathBuf> {
55        // Ensure wave grid is initialized before using it
56        self.ensure_wave_grid();
57
58        // Analyze the JSON structure to understand conversation format
59        let analysis = self.analyzer.analyze(json_data)?;
60
61        // Generate a unique filename based on content
62        let timestamp = SystemTime::now()
63            .duration_since(SystemTime::UNIX_EPOCH)?
64            .as_secs();
65
66        let filename = format!(
67            "conv_{}_{}_{}.m8",
68            analysis.conversation_type.as_str(),
69            source.unwrap_or("unknown"),
70            timestamp
71        );
72
73        let file_path = self.base_path.join(&filename);
74
75        // Convert conversation to wave patterns
76        let waves = self.conversation_to_waves(&analysis)?;
77
78        // Store in wave grid
79        if let Some(ref mut grid) = self.wave_grid {
80            for (idx, wave) in waves.iter().enumerate() {
81                let x = (idx % 256) as u8;
82                let y = ((idx / 256) % 256) as u8;
83                let z = (idx / (256 * 256)) as u16;
84                grid.store(x, y, z, wave.clone());
85            }
86        }
87
88        // For now, save directly as JSON until M8Writer is properly implemented
89        // TODO: Implement proper M8Writer integration
90
91        // Also save a JSON companion file for easy retrieval
92        let json_path = file_path.with_extension("json");
93        fs::write(&json_path, serde_json::to_string_pretty(json_data)?)?;
94
95        println!("🧠 Conversation saved to MEM|8: {}", filename);
96        println!("   Type: {:?}", analysis.conversation_type);
97        println!("   Messages: {}", analysis.message_count);
98        println!("   Participants: {}", analysis.participants.join(", "));
99
100        Ok(file_path)
101    }
102
103    /// Convert conversation analysis to wave patterns
104    fn conversation_to_waves(&self, analysis: &ConversationAnalysis) -> Result<Vec<MemoryWave>> {
105        let mut waves = Vec::new();
106
107        for message in &analysis.messages {
108            // Map message emotion to frequency
109            let frequency = match message.emotion.as_str() {
110                "happy" | "excited" => 100.0,    // High energy
111                "sad" | "worried" => 20.0,       // Low energy
112                "angry" | "frustrated" => 150.0, // Intense
113                "neutral" | "thinking" => 50.0,  // Balanced
114                _ => 44.1,                       // Default to audio baseline
115            };
116
117            // Create wave with message characteristics
118            let mut wave = MemoryWave::new(frequency, message.importance as f32);
119            wave.phase = message.timestamp as f32;
120            wave.valence = match message.emotion.as_str() {
121                "happy" | "excited" => 0.8,
122                "sad" | "worried" => -0.5,
123                "angry" | "frustrated" => -0.8,
124                _ => 0.0,
125            };
126            wave.arousal = message.importance as f32 / 10.0;
127
128            waves.push(wave);
129        }
130
131        Ok(waves)
132    }
133
134    /// List all saved conversations
135    pub fn list_conversations(&self) -> Result<Vec<ConversationSummary>> {
136        let mut summaries = Vec::new();
137
138        if !self.base_path.exists() {
139            return Ok(summaries);
140        }
141
142        for entry in fs::read_dir(&self.base_path)? {
143            let entry = entry?;
144            let path = entry.path();
145
146            if path.extension() == Some(std::ffi::OsStr::new("m8")) {
147                // Read the companion JSON for quick summary
148                let json_path = path.with_extension("json");
149                if json_path.exists() {
150                    let json_str = fs::read_to_string(&json_path)?;
151                    let json_data: Value = serde_json::from_str(&json_str)?;
152
153                    let analysis = self.analyzer.analyze(&json_data)?;
154                    summaries.push(ConversationSummary {
155                        file_name: path.file_name().unwrap().to_string_lossy().to_string(),
156                        conversation_type: analysis.conversation_type,
157                        message_count: analysis.message_count,
158                        participants: analysis.participants,
159                        timestamp: entry.metadata()?.modified()?,
160                    });
161                }
162            }
163        }
164
165        Ok(summaries)
166    }
167}
168
169/// Smart conversation structure analyzer
170pub struct ConversationAnalyzer {
171    /// Known conversation patterns
172    patterns: Vec<ConversationPattern>,
173}
174
175impl Default for ConversationAnalyzer {
176    fn default() -> Self {
177        Self::new()
178    }
179}
180
181impl ConversationAnalyzer {
182    pub fn new() -> Self {
183        Self {
184            patterns: Self::default_patterns(),
185        }
186    }
187
188    /// Analyze JSON to understand conversation structure
189    pub fn analyze(&self, json_data: &Value) -> Result<ConversationAnalysis> {
190        // Try to detect the conversation format
191        let conversation_type = self.detect_type(json_data);
192
193        // Extract messages based on detected type
194        let messages = self.extract_messages(json_data, &conversation_type)?;
195
196        // Identify participants
197        let participants = self.extract_participants(&messages);
198
199        // Get message count before moving messages
200        let message_count = messages.len();
201
202        // Build metadata
203        let mut metadata = Map::new();
204        metadata.insert(
205            "type".to_string(),
206            Value::String(conversation_type.to_string()),
207        );
208        metadata.insert("version".to_string(), Value::String("1.0".to_string()));
209
210        Ok(ConversationAnalysis {
211            conversation_type,
212            messages,
213            participants,
214            message_count,
215            metadata,
216        })
217    }
218
219    /// Detect conversation type from JSON structure
220    fn detect_type(&self, json_data: &Value) -> ConversationType {
221        // Check for common conversation patterns
222        if json_data.get("messages").is_some() {
223            ConversationType::ChatGPT
224        } else if json_data.get("conversation").is_some() {
225            ConversationType::Claude
226        } else if json_data.get("history").is_some() {
227            ConversationType::Generic
228        } else if json_data.is_array() {
229            ConversationType::MessageArray
230        } else {
231            ConversationType::Unknown
232        }
233    }
234
235    /// Extract messages from JSON based on type
236    fn extract_messages(
237        &self,
238        json_data: &Value,
239        conv_type: &ConversationType,
240    ) -> Result<Vec<Message>> {
241        let mut messages = Vec::new();
242
243        match conv_type {
244            ConversationType::ChatGPT => {
245                if let Some(msgs) = json_data.get("messages").and_then(|m| m.as_array()) {
246                    for (idx, msg) in msgs.iter().enumerate() {
247                        messages.push(Message {
248                            content: msg
249                                .get("content")
250                                .and_then(|c| c.as_str())
251                                .unwrap_or("")
252                                .to_string(),
253                            role: msg
254                                .get("role")
255                                .and_then(|r| r.as_str())
256                                .unwrap_or("unknown")
257                                .to_string(),
258                            timestamp: idx as u64,
259                            emotion: self.detect_emotion(msg),
260                            importance: self.calculate_importance(msg),
261                        });
262                    }
263                }
264            }
265            ConversationType::MessageArray => {
266                if let Some(msgs) = json_data.as_array() {
267                    for (idx, msg) in msgs.iter().enumerate() {
268                        messages.push(Message {
269                            content: msg
270                                .get("text")
271                                .or_else(|| msg.get("content"))
272                                .and_then(|c| c.as_str())
273                                .unwrap_or("")
274                                .to_string(),
275                            role: msg
276                                .get("sender")
277                                .or_else(|| msg.get("role"))
278                                .and_then(|r| r.as_str())
279                                .unwrap_or("unknown")
280                                .to_string(),
281                            timestamp: idx as u64,
282                            emotion: self.detect_emotion(msg),
283                            importance: self.calculate_importance(msg),
284                        });
285                    }
286                }
287            }
288            _ => {
289                // Try to extract any text content
290                self.extract_generic_messages(json_data, &mut messages, 0);
291            }
292        }
293
294        Ok(messages)
295    }
296
297    /// Recursively extract text from generic JSON
298    fn extract_generic_messages(&self, value: &Value, messages: &mut Vec<Message>, depth: usize) {
299        if depth > 10 {
300            return; // Prevent infinite recursion
301        }
302
303        match value {
304            Value::String(s) if s.len() > 20 => {
305                messages.push(Message {
306                    content: s.clone(),
307                    role: "extracted".to_string(),
308                    timestamp: messages.len() as u64,
309                    emotion: "neutral".to_string(),
310                    importance: 5,
311                });
312            }
313            Value::Object(map) => {
314                for (_key, val) in map {
315                    self.extract_generic_messages(val, messages, depth + 1);
316                }
317            }
318            Value::Array(arr) => {
319                for val in arr {
320                    self.extract_generic_messages(val, messages, depth + 1);
321                }
322            }
323            _ => {}
324        }
325    }
326
327    /// Extract unique participants from messages
328    fn extract_participants(&self, messages: &[Message]) -> Vec<String> {
329        let mut participants = Vec::new();
330        for msg in messages {
331            if !participants.contains(&msg.role) {
332                participants.push(msg.role.clone());
333            }
334        }
335        participants
336    }
337
338    /// Detect emotion from message (simple heuristic)
339    fn detect_emotion(&self, _msg: &Value) -> String {
340        // TODO: Implement actual emotion detection
341        "neutral".to_string()
342    }
343
344    /// Calculate message importance (1-10)
345    fn calculate_importance(&self, msg: &Value) -> u8 {
346        // Simple heuristic based on length
347        if let Some(content) = msg.get("content").and_then(|c| c.as_str()) {
348            let len = content.len();
349            if len > 500 {
350                8
351            } else if len > 200 {
352                6
353            } else if len > 50 {
354                5
355            } else {
356                3
357            }
358        } else {
359            5
360        }
361    }
362
363    /// Default conversation patterns
364    fn default_patterns() -> Vec<ConversationPattern> {
365        vec![
366            ConversationPattern {
367                name: "OpenAI".to_string(),
368                message_path: vec!["messages".to_string()],
369                content_field: "content".to_string(),
370                role_field: "role".to_string(),
371            },
372            ConversationPattern {
373                name: "Claude".to_string(),
374                message_path: vec!["conversation".to_string()],
375                content_field: "text".to_string(),
376                role_field: "sender".to_string(),
377            },
378        ]
379    }
380}
381
382/// Conversation type enumeration
383#[derive(Debug, Clone, Serialize, Deserialize)]
384pub enum ConversationType {
385    ChatGPT,
386    Claude,
387    Generic,
388    MessageArray,
389    Unknown,
390}
391
392impl ConversationType {
393    fn as_str(&self) -> &str {
394        match self {
395            Self::ChatGPT => "chatgpt",
396            Self::Claude => "claude",
397            Self::Generic => "generic",
398            Self::MessageArray => "array",
399            Self::Unknown => "unknown",
400        }
401    }
402}
403
404impl std::fmt::Display for ConversationType {
405    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
406        write!(f, "{}", self.as_str())
407    }
408}
409
410/// Analyzed conversation structure
411#[derive(Debug)]
412pub struct ConversationAnalysis {
413    pub conversation_type: ConversationType,
414    pub messages: Vec<Message>,
415    pub participants: Vec<String>,
416    pub message_count: usize,
417    pub metadata: Map<String, Value>,
418}
419
420/// Individual message in a conversation
421#[derive(Debug, Clone)]
422pub struct Message {
423    pub content: String,
424    pub role: String,
425    pub timestamp: u64,
426    pub emotion: String,
427    pub importance: u8,
428}
429
430/// Known conversation pattern
431#[derive(Debug, Clone)]
432pub struct ConversationPattern {
433    pub name: String,
434    pub message_path: Vec<String>,
435    pub content_field: String,
436    pub role_field: String,
437}
438
439/// Conversation summary for listing
440#[derive(Debug, Serialize)]
441pub struct ConversationSummary {
442    pub file_name: String,
443    pub conversation_type: ConversationType,
444    pub message_count: usize,
445    pub participants: Vec<String>,
446    pub timestamp: std::time::SystemTime,
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_conversation_detection() {
455        let analyzer = ConversationAnalyzer::new();
456
457        // Test ChatGPT format
458        let chatgpt_json = serde_json::json!({
459            "messages": [
460                {"role": "user", "content": "Hello"},
461                {"role": "assistant", "content": "Hi there!"}
462            ]
463        });
464
465        let analysis = analyzer.analyze(&chatgpt_json).unwrap();
466        assert!(matches!(
467            analysis.conversation_type,
468            ConversationType::ChatGPT
469        ));
470        assert_eq!(analysis.message_count, 2);
471
472        // Test array format
473        let array_json = serde_json::json!([
474            {"text": "Hello", "sender": "user"},
475            {"text": "Hi!", "sender": "bot"}
476        ]);
477
478        let analysis = analyzer.analyze(&array_json).unwrap();
479        assert!(matches!(
480            analysis.conversation_type,
481            ConversationType::MessageArray
482        ));
483    }
484
485    #[test]
486    fn test_lazy_wave_grid_initialization() {
487        // Test that creating a ConversationMemory doesn't immediately allocate 34GB
488        // This test would fail with the old code that allocated WaveGrid in new()
489        let memory_result = ConversationMemory::new();
490
491        // Should succeed without OOM
492        assert!(memory_result.is_ok());
493
494        let memory = memory_result.unwrap();
495
496        // Wave grid should be None initially (lazy)
497        assert!(memory.wave_grid.is_none());
498
499        // Listing conversations should work without allocating the grid
500        let _list_result = memory.list_conversations();
501        // Don't fail the test if directory doesn't exist, just verify no crash
502    }
503}