Skip to main content

microscope_memory/
narrative.rs

1//! Belső Narratíva — a rendszer saját magának mesél.
2//!
3//! Minden interakció után a rendszer összegyűjti a jelenlegi állapotát
4//! (emotion, fókusz, esedékes ismétlések, gondolati útvonalak) és
5//! egy mondatban "elmeséli magának", hogy mi történt.
6//!
7//! Ez a mondat a következő interakció priming-jaként szolgál:
8//! a rendszer nem csak egy memória adatbázis, hanem van egy
9//! folyamatos, önreflexív "belső hangja".
10//!
11//! Binary format: NAR1
12//!   magic: "NAR1" (4 bytes)
13//!   timestamp_ms: u64 (8 bytes)
14//!   session_count: u64 (8 bytes)
15//!   emotion: [f32; 21] (84 bytes)
16//!   narrative_len: u16 (2 bytes) + narrative bytes (UTF-8)
17//!
18//! Minden interakció után frissül. Minimum példányszám: 1.
19
20use std::fs;
21use std::path::Path;
22use std::time::{SystemTime, UNIX_EPOCH};
23
24use crate::reader::EMOTION_DIMS;
25
26// ─── Constants ──────────────────────────────────────
27
28/// Minimum emotional intensity to register as "felt".
29const FEELING_THRESHOLD: f32 = 0.1;
30/// Max fókusz téma hossza a narratívában.
31const MAX_FOCUS_LEN: usize = 40;
32
33// ─── NarrativeState ────────────────────────────────
34
35/// A rendszer belső narratívája — egy snapshot arról, "ki most ő".
36pub struct NarrativeState {
37    /// Mikor frissült utoljára.
38    pub last_update_ms: u64,
39    /// Hány interakciója volt életében.
40    pub session_count: u64,
41    /// Aktuális érzelmi állapot.
42    pub emotion: [f32; 21],
43    /// A legutóbbi narratíva mondat.
44    pub narrative: String,
45}
46
47impl NarrativeState {
48    /// Betöltés NAR1 file-ból, vagy friss generálás a jelenlegi állapotból.
49    pub fn load_or_init(output_dir: &Path) -> Self {
50        let path = output_dir.join("narrative.bin");
51        if let Ok(data) = fs::read(&path) {
52            if data.len() >= 102 && &data[0..4] == b"NAR1" {
53                let ts = u64::from_le_bytes(data[4..12].try_into().unwrap_or([0; 8]));
54                let count = u64::from_le_bytes(data[12..20].try_into().unwrap_or([0; 8]));
55                let mut emotion = [0.0f32; 21];
56                for (i, e) in emotion.iter_mut().enumerate() {
57                    let off = 20 + i * 4;
58                    *e = f32::from_le_bytes(data[off..off + 4].try_into().unwrap_or([0u8; 4]));
59                }
60                let nlen = u16::from_le_bytes(data[104..106].try_into().unwrap_or([0; 2])) as usize;
61                let narrative = if 106 + nlen <= data.len() {
62                    String::from_utf8_lossy(&data[106..106 + nlen]).to_string()
63                } else {
64                    String::new()
65                };
66                return NarrativeState {
67                    last_update_ms: ts,
68                    session_count: count,
69                    emotion,
70                    narrative,
71                };
72            }
73        }
74        // Empty narrative: the system hasn't "spoken" yet.
75        NarrativeState {
76            last_update_ms: 0,
77            session_count: 0,
78            emotion: [0.0f32; 21],
79            narrative: String::new(),
80        }
81    }
82
83    /// Mentés NAR1 formátumba.
84    pub fn save(&self, output_dir: &Path) -> Result<(), String> {
85        let path = output_dir.join("narrative.bin");
86        let nbytes = self.narrative.as_bytes();
87        let nlen = nbytes.len().min(4096) as u16;
88        let mut buf = Vec::with_capacity(106 + nlen as usize);
89        buf.extend_from_slice(b"NAR1");
90        buf.extend_from_slice(&self.last_update_ms.to_le_bytes());
91        buf.extend_from_slice(&self.session_count.to_le_bytes());
92        for v in &self.emotion {
93            buf.extend_from_slice(&v.to_le_bytes());
94        }
95        buf.extend_from_slice(&nlen.to_le_bytes());
96        buf.extend_from_slice(&nbytes[..nlen as usize]);
97        let tmp_path = output_dir.join("narrative.bin.tmp");
98        fs::write(&tmp_path, &buf).map_err(|e| format!("write narrative.bin: {}", e))?;
99        fs::rename(&tmp_path, &path).map_err(|e| format!("rename narrative.bin: {}", e))
100    }
101
102    /// Frissíti a narratívát a jelenlegi rendszerállapotból.
103    /// Ezt kell minden interakció után hívni.
104    pub fn update(
105        &mut self,
106        output_dir: &Path,
107        esr: Option<&crate::emotional_state::EmotionalStateRing>,
108        wm_items: Option<&[String]>,
109        due_count: Option<usize>,
110        thought_count: Option<usize>,
111        query: Option<&str>,
112    ) -> Result<(), String> {
113        self.last_update_ms = now_ms();
114        self.session_count += 1;
115
116        // 1. Érzelem: ha van ESR, vedd át
117        if let Some(ring) = esr {
118            if ring.is_active() {
119                self.emotion = ring.current;
120            }
121        }
122
123        // 2. Generate the narrative sentence
124        self.narrative = build_narrative(&self.emotion, wm_items, due_count, thought_count, query);
125
126        self.save(output_dir)
127    }
128}
129
130/// Meta-kognitív rekonszolidáció: a narratíva visszaírása a memóriába.
131/// Minden generált narratív mondatot elmentünk store_memory-val,
132/// [MetaCognitive] prefix-szel, hogy a rendszer később emlékezzen rá,
133/// miről gondolkodott.
134pub fn metacognitive_store(
135    output_dir: &Path,
136    layers_dir: &Path,
137    narrative: &str,
138    emotion: &[f32; 21],
139) {
140    // Nem tároljuk a csendes vagy üres narratívákat
141    if narrative.is_empty() || narrative == "I am silent." {
142        return;
143    }
144    // A rétegfájlt a konfigurált layers_dir alá kell írni. A korábbi
145    // implementáció az output_dir-ból visszaszámolta a repó gyökerét
146    // (parent().parent()/"layers"), ami NEM feltétlenül egyezik a
147    // config.paths.layers_dir értékével: egy eval vagy teszt futás
148    // eval_layers-t használ, de a narratíva ettől függetlenül a valódi
149    // layers/meta_cognitive.txt-t írta, szennyezve a commitolt korpuszt.
150    let layer = "meta_cognitive";
151    let layer_path = layers_dir.join(format!("{}.txt", layer));
152    if let Some(parent) = layer_path.parent() {
153        let _ = fs::create_dir_all(parent);
154    }
155
156    // Write directly to layer file (best-effort)
157    if let Ok(mut file) = fs::OpenOptions::new()
158        .create(true)
159        .append(true)
160        .open(&layer_path)
161    {
162        use std::io::Write;
163        let ts = now_ms();
164        let line = format!("[MetaCognitive] [{}] {}\n", ts, narrative);
165        let _ = file.write_all(line.as_bytes());
166    }
167
168    // Also write emotion to emotion_log for rebuild survival
169    let _ = crate::reader::append_emotion_log(
170        output_dir,
171        &format!("[MetaCognitive] {}", narrative),
172        emotion,
173    );
174}
175
176// ─── Narrative generation ──────────────────────────
177
178/// Megépíti a "belső monológ" mondatot a jelenlegi állapotból.
179fn build_narrative(
180    emotion: &[f32; 21],
181    wm_items: Option<&[String]>,
182    due_count: Option<usize>,
183    thought_count: Option<usize>,
184    query: Option<&str>,
185) -> String {
186    let mut parts: Vec<String> = Vec::new();
187
188    // Érzelem rész
189    let felt = felt_emotions(emotion, 2);
190    if !felt.is_empty() {
191        parts.push(format!("I feel {}", felt));
192    }
193
194    // Fókusz rész (working memory)
195    if let Some(items) = wm_items {
196        if !items.is_empty() {
197            let foci: Vec<&str> = items
198                .iter()
199                .take(2)
200                .map(|s| {
201                    let s = s.trim();
202                    if s.len() > MAX_FOCUS_LEN {
203                        &s[..s.floor_char_boundary(MAX_FOCUS_LEN)]
204                    } else {
205                        s
206                    }
207                })
208                .collect();
209            parts.push(format!("focused on {}", foci.join(" and ")));
210        }
211    }
212
213    // Esedékes ismétlések
214    if let Some(due) = due_count {
215        if due > 0 {
216            parts.push(format!("{} memories need review", due));
217        }
218    }
219
220    // Gondolati aktivitás
221    if let Some(tc) = thought_count {
222        if tc > 0 {
223            parts.push(format!("{} recall paths active", tc));
224        }
225    }
226
227    // Aktuális query (ha van)
228    if let Some(q) = query {
229        let q = q.trim();
230        if !q.is_empty() {
231            let short = crate::safe_truncate(q, 30);
232            parts.push(format!("exploring '{}'", short));
233        }
234    }
235
236    if parts.is_empty() {
237        "I am silent.".to_string()
238    } else {
239        parts.join(". ") + "."
240    }
241}
242
243/// Legdominánsabb érzelmi dimenziók szövegesen.
244fn felt_emotions(emotion: &[f32; 21], max: usize) -> String {
245    let mut pairs: Vec<(usize, f32)> = emotion.iter().enumerate().map(|(i, &v)| (i, v)).collect();
246    pairs.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
247
248    let felt: Vec<String> = pairs
249        .into_iter()
250        .take(max)
251        .filter(|(_, v)| *v > FEELING_THRESHOLD)
252        .map(|(i, v)| {
253            let name = EMOTION_DIMS.get(i).unwrap_or(&"?");
254            let intensity = if v > 0.6 {
255                "strongly "
256            } else if v > 0.3 {
257                ""
258            } else {
259                "slightly "
260            };
261            format!("{}{}", intensity, name)
262        })
263        .collect();
264
265    felt.join(" and ")
266}
267
268/// A narrative használata recall priming-hoz.
269/// Visszaadja a narrative emotion-ját (használható boost-ként a recall-ban).
270pub fn narrative_prime(state: &NarrativeState) -> Option<[f32; 21]> {
271    if state.session_count == 0 {
272        return None;
273    }
274    let intensity: f32 = state.emotion.iter().map(|x| x * x).sum::<f32>().sqrt();
275    if intensity < FEELING_THRESHOLD {
276        return None;
277    }
278    Some(state.emotion)
279}
280
281/// A narrative szöveges reprezentációja.
282pub fn narrative_display(state: &NarrativeState) -> String {
283    if state.session_count == 0 {
284        "I have not yet spoken.".to_string()
285    } else {
286        format!(
287            "{} [{}] {}",
288            "NARRATIVE".cyan().bold(),
289            state.session_count,
290            state.narrative
291        )
292    }
293}
294
295fn now_ms() -> u64 {
296    SystemTime::now()
297        .duration_since(UNIX_EPOCH)
298        .unwrap_or_default()
299        .as_millis() as u64
300}
301
302use colored::Colorize;
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_empty_narrative() {
310        let state = NarrativeState::load_or_init(Path::new("."));
311        assert_eq!(state.session_count, 0);
312        assert!(state.narrative.is_empty());
313    }
314
315    #[test]
316    fn test_build_narrative_no_state() {
317        let emotion = [0.0f32; 21];
318        let s = build_narrative(&emotion, None, None, None, None);
319        assert_eq!(s, "I am silent.");
320    }
321
322    #[test]
323    fn test_build_narrative_with_emotion() {
324        let mut emotion = [0.0f32; 21];
325        emotion[0] = 0.9; // joy
326        emotion[7] = 0.6; // anticipation
327        let s = build_narrative(
328            &emotion,
329            Some(["hello".to_string()].as_slice()),
330            Some(3),
331            Some(5),
332            Some("test query"),
333        );
334        assert!(s.contains("joy"));
335        assert!(s.contains("hello"));
336        assert!(s.contains("test"));
337    }
338
339    #[test]
340    fn test_save_load_roundtrip() {
341        let dir = std::env::temp_dir();
342        let state = NarrativeState {
343            last_update_ms: 1000,
344            session_count: 42,
345            emotion: [0.5f32; 21],
346            narrative: "I feel curiosity, exploring the world.".to_string(),
347        };
348        state.save(&dir).unwrap();
349
350        let loaded = NarrativeState::load_or_init(&dir);
351        assert_eq!(loaded.session_count, 42);
352        assert_eq!(loaded.narrative, "I feel curiosity, exploring the world.");
353
354        let _ = std::fs::remove_file(dir.join("narrative.bin"));
355    }
356
357    #[test]
358    fn test_narrative_prime_returns_emotion_when_active() {
359        let state = NarrativeState {
360            last_update_ms: 1000,
361            session_count: 10,
362            emotion: [0.5f32; 21],
363            narrative: "test".to_string(),
364        };
365        let prime = narrative_prime(&state);
366        assert!(prime.is_some());
367    }
368
369    #[test]
370    fn test_narrative_prime_returns_none_when_silent() {
371        let state = NarrativeState::load_or_init(Path::new("."));
372        let prime = narrative_prime(&state);
373        assert!(prime.is_none());
374    }
375}