Skip to main content

hippmem_write/
enrich.rs

1//! Enrich stage: complete strong semantic dimensions in the background
2//! (06 §M6-001).
3
4use hippmem_core::model::understanding::{
5    DecisionFrame, EmotionFrame, GoalFrame, GoalStatus, Polarity, PreferenceFrame,
6};
7use hippmem_core::model::unit::MemoryUnit;
8use hippmem_core::score::UnitScore;
9use hippmem_model::lang::active_locales;
10
11/// Enrich input.
12pub struct EnrichInput {
13    pub unit: MemoryUnit,
14}
15
16/// Enrich output: a MemoryUnit with strong semantic dimensions completed.
17pub struct EnrichOutput {
18    pub unit: MemoryUnit,
19}
20
21/// Background enrich: complete goals/preferences/emotions/decisions using
22/// deterministic rules.
23pub fn enrich_unit(input: EnrichInput) -> EnrichOutput {
24    let mut unit = input.unit;
25    let text = &unit.content.raw.clone();
26    let mut u = unit.understanding.clone();
27
28    // Goals
29    if u.goals.is_empty() {
30        u.goals = extract_goals_enrich(text);
31    }
32    // Preferences
33    if u.preferences.is_empty() {
34        u.preferences = extract_preferences(text);
35    }
36    // Emotions
37    if u.emotions.is_empty() {
38        u.emotions = extract_emotions_enrich(text);
39    }
40    // Decisions
41    if u.decisions.is_empty() {
42        u.decisions = extract_decisions_enrich(text);
43    }
44
45    unit.understanding = u;
46    unit.stage = hippmem_core::model::unit::MemoryStage::Enriched;
47    unit.updated_at = unit.context.local_time;
48
49    EnrichOutput { unit }
50}
51
52fn extract_goals_enrich(text: &str) -> Vec<GoalFrame> {
53    for lang in active_locales() {
54        for m in lang.goal_markers {
55            if text.contains(m) {
56                return vec![GoalFrame {
57                    description: format!("marker:{m}"),
58                    status: GoalStatus::Active,
59                    constraints: vec![],
60                    confidence: UnitScore::new(0.45),
61                }];
62            }
63        }
64    }
65    vec![]
66}
67
68fn extract_preferences(text: &str) -> Vec<PreferenceFrame> {
69    for lang in active_locales() {
70        for w in lang.preference_pos {
71            if text.contains(w) {
72                return vec![PreferenceFrame {
73                    object: format!("preference:{w}"),
74                    polarity: Polarity::Like,
75                    strength: UnitScore::new(0.5),
76                    still_valid: true,
77                    confidence: UnitScore::new(0.5),
78                }];
79            }
80        }
81    }
82    for lang in active_locales() {
83        for w in lang.preference_neg {
84            if text.contains(w) {
85                return vec![PreferenceFrame {
86                    object: format!("preference:{w}"),
87                    polarity: Polarity::Dislike,
88                    strength: UnitScore::new(0.5),
89                    still_valid: true,
90                    confidence: UnitScore::new(0.5),
91                }];
92            }
93        }
94    }
95    vec![]
96}
97
98fn extract_emotions_enrich(text: &str) -> Vec<EmotionFrame> {
99    for lang in active_locales() {
100        for (w, ek) in lang.emotion_keywords {
101            if text.contains(w) {
102                return vec![EmotionFrame {
103                    emotion: *ek,
104                    intensity: UnitScore::new(0.6),
105                    trigger: Some(format!("emotion:{w}")),
106                    confidence: UnitScore::new(0.5),
107                }];
108            }
109        }
110    }
111    vec![]
112}
113
114fn extract_decisions_enrich(text: &str) -> Vec<DecisionFrame> {
115    for lang in active_locales() {
116        for m in lang.decision_markers {
117            if text.contains(m) {
118                return vec![DecisionFrame {
119                    decision: format!("decision:{m}"),
120                    rationale: Some(text.chars().take(100).collect()),
121                    decided_at: None,
122                    reverted: false,
123                    confidence: UnitScore::new(0.45),
124                }];
125            }
126        }
127    }
128    vec![]
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use hippmem_core::ids::MemoryId;
135    use hippmem_core::model::understanding::MemoryUnderstanding;
136    use hippmem_core::model::unit::{
137        ContentType, Language, MemoryContent, MemoryStage, WriteContext,
138    };
139    use hippmem_core::time::Timestamp;
140
141    fn make_unit(text: &str) -> MemoryUnit {
142        MemoryUnit {
143            schema_version: 1,
144            id: MemoryId(1),
145            created_at: Timestamp(0),
146            updated_at: Timestamp(0),
147            content: MemoryContent {
148                raw: text.into(),
149                summary: None,
150                normalized: None,
151                language: Language::Zh,
152                content_type: ContentType::UserStatement,
153            },
154            context: WriteContext {
155                conversation_id: None,
156                session_id: None,
157                project_id: None,
158                task_id: None,
159                user_id: None,
160                local_time: Timestamp(1_700_000_000_000),
161                preceding_memory_ids: vec![],
162                source_refs: vec![],
163            },
164            understanding: MemoryUnderstanding {
165                entities: vec![],
166                events: vec![],
167                goals: vec![],
168                decisions: vec![],
169                preferences: vec![],
170                emotions: vec![],
171                causal_claims: vec![],
172                contradictions: vec![],
173                topics: vec![],
174                importance: UnitScore::new(0.5),
175                confidence: UnitScore::new(0.5),
176            },
177            association_keys: hippmem_core::model::links::AssociationKeys {
178                entity_keys: vec![],
179                temporal_keys: vec![],
180                lexical_signature: hippmem_core::model::links::LexicalSignature { simhash: [0; 4] },
181                semantic_signature: hippmem_core::model::links::SemanticSignature {
182                    lexical_simhash: [0; 4],
183                    dense_embedding_ref: None,
184                    binary_code: [0, 0],
185                    topic_minhash: [0u32; 16],
186                },
187                topic_keys: vec![],
188                emotion_keys: vec![],
189                goal_keys: vec![],
190                event_keys: vec![],
191                causal_keys: vec![],
192            },
193            links: vec![],
194            activation: hippmem_core::model::links::ActivationState {
195                last_retrieved_at: None,
196                retrieval_count: 0,
197                co_activations: vec![],
198                usage_score: UnitScore::new(0.5),
199            },
200            lifecycle: hippmem_core::model::unit::MemoryLifecycle::Active,
201            provenance: hippmem_core::model::unit::Provenance {
202                origin: hippmem_core::model::unit::SourceKind::Conversation,
203                generated_by: hippmem_core::model::unit::GeneratedBy::UserDirect,
204                reliability: UnitScore::new(0.5),
205                evidence_refs: vec![],
206                revision_history: vec![],
207            },
208            stage: MemoryStage::Indexed,
209        }
210    }
211
212    #[test]
213    fn enrich_adds_preferences() {
214        let unit = make_unit("I like programming in Rust");
215        let output = enrich_unit(EnrichInput { unit });
216        assert!(!output.unit.understanding.preferences.is_empty());
217        assert_eq!(output.unit.stage, MemoryStage::Enriched);
218    }
219
220    #[test]
221    fn enrich_adds_goals() {
222        let unit = make_unit("I plan to learn Rust programming");
223        let output = enrich_unit(EnrichInput { unit });
224        assert!(!output.unit.understanding.goals.is_empty());
225    }
226
227    #[test]
228    fn enrich_noop_on_empty_text() {
229        let unit = make_unit("The weather is nice today");
230        let output = enrich_unit(EnrichInput { unit });
231        assert_eq!(output.unit.stage, MemoryStage::Enriched);
232    }
233}