Skip to main content

microscope_memory/
chatgpt.rs

1//! ChatGPT Export Importer — Microscope Memory számára
2//!
3//! Képes:
4//! - ChatGPT konverzációs export JSON feldolgozása
5//! - Beszélgetések betöltése Microscope Memory rétegekbe
6//! - Érzelmi kontextus és asszociációk építése
7//! - Személyazonosítás (user vs AI)
8//!
9//! Használat:
10//!   microscope-mem import-chatgpt <path> --persona Liora
11
12use serde::Deserialize;
13use std::collections::{HashMap, VecDeque};
14use std::fs;
15use std::path::Path;
16
17// ─── ChatGPT Export Adatszerkezetek ─────────────────────────────────────────
18
19#[derive(Debug, Deserialize)]
20pub struct ChatGPTExport {
21    #[serde(default)]
22    pub conversations: Vec<Conversation>,
23}
24
25#[derive(Debug, Deserialize)]
26pub struct Conversation {
27    pub title: Option<String>,
28    pub create_time: Option<f64>,
29    pub update_time: Option<f64>,
30    pub mapping: Option<HashMap<String, MappingNode>>,
31    pub current_node: Option<String>,
32    #[serde(default)]
33    pub moderation_results: Vec<serde_json::Value>,
34    pub plugin_ids: Option<Vec<String>>,
35}
36
37#[derive(Debug, Deserialize, Clone)]
38pub struct MappingNode {
39    pub id: Option<String>,
40    pub message: Option<ChatMessageData>,
41    pub parent: Option<String>,
42    pub children: Option<Vec<String>>,
43}
44
45#[derive(Debug, Deserialize, Clone)]
46pub struct ChatMessageData {
47    pub id: Option<String>,
48    pub author: Option<Author>,
49    pub content: Option<MessageContent>,
50    pub create_time: Option<f64>,
51    pub metadata: Option<serde_json::Value>,
52}
53
54#[derive(Debug, Deserialize, Clone)]
55pub struct Author {
56    pub role: String,
57    pub name: Option<String>,
58    pub metadata: Option<serde_json::Value>,
59}
60
61#[derive(Debug, Deserialize, Clone)]
62pub struct MessageContent {
63    pub content_type: Option<String>,
64    pub parts: Option<Vec<serde_json::Value>>,
65    pub text: Option<String>,
66}
67
68// ─── Feldolgozott Adatszerkezetek ──────────────────────────────────────────
69
70#[derive(Debug, Clone)]
71pub struct ProcessedMessage {
72    pub conversation_title: String,
73    pub role: String,        // "user" | "assistant"
74    pub sender_name: String, // Szilvi vagy Liora
75    pub text: String,
76    pub timestamp_ms: u64,
77    pub message_index: usize,
78    pub message_count: usize,
79}
80
81#[derive(Debug, Clone)]
82pub struct ImportResult {
83    pub conversations_found: usize,
84    pub total_messages: usize,
85    pub user_messages: usize,
86    pub ai_messages: usize,
87    pub total_size_bytes: usize,
88    pub errors: Vec<String>,
89    pub import_duration_ms: u64,
90}
91
92// ─── Importáló Motor ───────────────────────────────────────────────────────
93
94pub struct ChatGPTImporter {
95    pub persona_name: String,
96    pub user_name: String,
97}
98
99impl ChatGPTImporter {
100    pub fn new(persona: &str) -> Self {
101        Self {
102            persona_name: persona.to_string(),
103            user_name: "user".to_string(),
104        }
105    }
106
107    /// JSON fájl beolvasása és feldolgozása
108    pub fn parse_export(&self, path: &str) -> Result<VecDeque<ProcessedMessage>, String> {
109        let content = fs::read_to_string(Path::new(path))
110            .map_err(|e| format!("Nem lehet olvasni a fájlt: {}", e))?;
111
112        // A ChatGPT export lehet tömbfájl vagy a {conversations: [...]} formátum
113        let conversations: Vec<Conversation> = if content.trim().starts_with('[') {
114            serde_json::from_str(&content).map_err(|e| format!("JSON parse hiba (tömb): {}", e))?
115        } else {
116            let export: ChatGPTExport = serde_json::from_str(&content)
117                .map_err(|e| format!("JSON parse hiba (objektum): {}", e))?;
118            export.conversations
119        };
120
121        if conversations.is_empty() {
122            return Err("Nincs egy beszélgetés sem az exportban.".to_string());
123        }
124
125        let mut all_messages = VecDeque::new();
126
127        for conv in &conversations {
128            let title = conv.title.as_deref().unwrap_or("Untitled");
129            let mapping = match &conv.mapping {
130                Some(m) => m,
131                None => continue,
132            };
133
134            // Útvonal a current_node-ig
135            let current = match &conv.current_node {
136                Some(n) => n.clone(),
137                None => continue,
138            };
139
140            // Visszafelé építjük a láncot: current_node → root
141            let mut path = Vec::new();
142            let mut node_id = current.clone();
143            loop {
144                path.push(node_id.clone());
145                match mapping.get(&node_id) {
146                    Some(node) => match &node.parent {
147                        Some(parent) if !parent.is_empty() && parent != &node_id => {
148                            node_id = parent.clone();
149                        }
150                        _ => break,
151                    },
152                    None => break,
153                }
154            }
155            path.reverse();
156
157            // Szűrés: csak a user/assistant üzenetek
158            let conv_messages: Vec<ProcessedMessage> = path
159                .iter()
160                .filter_map(|nid| {
161                    let node = mapping.get(nid)?;
162                    let msg = node.message.as_ref()?;
163                    let author = msg.author.as_ref()?;
164                    let role = author.role.clone();
165
166                    // Csak user és assistant
167                    if role != "user" && role != "assistant" {
168                        // Try "system" or others - only take user/assistant
169                        if role != "system" {
170                            return None;
171                        }
172                    }
173
174                    let content = msg.content.as_ref()?;
175                    let text = content
176                        .parts
177                        .as_ref()
178                        .and_then(|parts| {
179                            parts
180                                .first()
181                                .and_then(|p| p.as_str())
182                                .map(|s| s.to_string())
183                        })
184                        .or_else(|| content.text.clone())
185                        .unwrap_or_default();
186
187                    if text.trim().is_empty() {
188                        return None;
189                    }
190
191                    let ts = (msg.create_time.unwrap_or(0.0) * 1000.0) as u64;
192                    let sender = if role == "assistant" {
193                        self.persona_name.clone()
194                    } else {
195                        self.user_name.clone()
196                    };
197
198                    Some(ProcessedMessage {
199                        conversation_title: title.to_string(),
200                        role,
201                        sender_name: sender,
202                        text: text.trim().to_string(),
203                        timestamp_ms: ts,
204                        message_index: 0,
205                        message_count: path.len(),
206                    })
207                })
208                .collect();
209
210            for msg in conv_messages {
211                all_messages.push_back(msg);
212            }
213        }
214
215        Ok(all_messages)
216    }
217
218    /// Beszélgetések importálása Microscope Memory-ba
219    pub fn import(&self, path: &str, microscope_binary: &str) -> ImportResult {
220        let start = std::time::Instant::now();
221        let mut errors = Vec::new();
222        let mut total_messages = 0;
223        let mut user_msgs = 0;
224        let mut ai_msgs = 0;
225
226        let messages = match self.parse_export(path) {
227            Ok(m) => m,
228            Err(e) => {
229                return ImportResult {
230                    conversations_found: 0,
231                    total_messages: 0,
232                    user_messages: 0,
233                    ai_messages: 0,
234                    total_size_bytes: 0,
235                    errors: vec![e],
236                    import_duration_ms: start.elapsed().as_millis() as u64,
237                };
238            }
239        };
240
241        let mut conv_count = 0;
242        let mut last_title = String::new();
243
244        for msg in &messages {
245            if msg.conversation_title != last_title {
246                conv_count += 1;
247                last_title = msg.conversation_title.clone();
248            }
249
250            // Tárolás Microscope Memory-ba
251            let layer = if msg.role == "assistant" {
252                "long_term"
253            } else {
254                "short_term"
255            };
256            let importance = if msg.role == "assistant" { 8 } else { 6 };
257            let store_text = format!(
258                "[{} | {}] {}: {}",
259                msg.conversation_title,
260                msg.sender_name,
261                if msg.role == "assistant" {
262                    &self.persona_name
263                } else {
264                    &self.user_name
265                },
266                msg.text
267            );
268
269            let cmd = format!(
270                "{} store -l {} -i {} \"{}\"",
271                microscope_binary,
272                layer,
273                importance,
274                store_text.replace('\"', "\\\"")
275            );
276
277            // Futtatás (egyszerű: syscall)
278            if std::process::Command::new("sh")
279                .arg("-c")
280                .arg(&cmd)
281                .output()
282                .is_err()
283            {
284                errors.push(format!(
285                    "Hiba üzenet tárolásakor: {}",
286                    msg.text.chars().take(50).collect::<String>()
287                ));
288            }
289
290            total_messages += 1;
291            if msg.role == "assistant" {
292                ai_msgs += 1;
293            } else {
294                user_msgs += 1;
295            }
296        }
297
298        let file_size = fs::metadata(path).map(|m| m.len() as usize).unwrap_or(0);
299
300        ImportResult {
301            conversations_found: conv_count,
302            total_messages,
303            user_messages: user_msgs,
304            ai_messages: ai_msgs,
305            total_size_bytes: file_size,
306            errors,
307            import_duration_ms: start.elapsed().as_millis() as u64,
308        }
309    }
310}