Skip to main content

shodh_memory/memory/
context.rs

1//! Context Management - Building and merging rich contexts
2
3use super::types::*;
4use anyhow::Result;
5use chrono::{Datelike, Timelike, Utc};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9/// Context builder for creating rich contexts
10///
11/// Public API for external callers to construct RichContext objects.
12/// Used internally by ContextManager and available for custom context creation.
13#[allow(unused)] // Public API
14pub struct ContextBuilder {
15    context: RichContext,
16}
17
18#[allow(unused)] // Public API methods
19impl Default for ContextBuilder {
20    fn default() -> Self {
21        Self::new()
22    }
23}
24
25#[allow(unused)] // Public API methods
26impl ContextBuilder {
27    /// Create new context builder
28    pub fn new() -> Self {
29        Self {
30            context: RichContext {
31                id: ContextId(Uuid::new_v4()),
32                conversation: ConversationContext::default(),
33                user: UserContext::default(),
34                project: ProjectContext::default(),
35                temporal: TemporalContext::default(),
36                semantic: SemanticContext::default(),
37                code: CodeContext::default(),
38                document: DocumentContext::default(),
39                environment: EnvironmentContext::default(),
40                // SHO-104: Richer context encoding
41                emotional: EmotionalContext::default(),
42                source: SourceContext::default(),
43                episode: EpisodeContext::default(),
44                parent: None,
45                embeddings: None,
46                decay_rate: 0.1, // Default decay
47                created_at: Utc::now(),
48                updated_at: Utc::now(),
49            },
50        }
51    }
52
53    /// Set conversation context
54    pub fn with_conversation(mut self, conv: ConversationContext) -> Self {
55        self.context.conversation = conv;
56        self
57    }
58
59    /// Set user context
60    pub fn with_user(mut self, user: UserContext) -> Self {
61        self.context.user = user;
62        self
63    }
64
65    /// Set project context
66    pub fn with_project(mut self, project: ProjectContext) -> Self {
67        self.context.project = project;
68        self
69    }
70
71    /// Set code context
72    pub fn with_code(mut self, code: CodeContext) -> Self {
73        self.context.code = code;
74        self
75    }
76
77    /// Set semantic context
78    pub fn with_semantic(mut self, semantic: SemanticContext) -> Self {
79        self.context.semantic = semantic;
80        self
81    }
82
83    /// Set document context
84    pub fn with_document(mut self, doc: DocumentContext) -> Self {
85        self.context.document = doc;
86        self
87    }
88
89    /// Set parent context
90    pub fn with_parent(mut self, parent: RichContext) -> Self {
91        self.context.parent = Some(Box::new(parent));
92        self
93    }
94
95    /// Set decay rate
96    pub fn with_decay_rate(mut self, rate: f32) -> Self {
97        self.context.decay_rate = rate;
98        self
99    }
100
101    /// Set temporal context
102    pub fn with_temporal(mut self, temporal: TemporalContext) -> Self {
103        self.context.temporal = temporal;
104        self
105    }
106
107    // SHO-104: Builder methods for richer context encoding
108
109    /// Set emotional context (valence, arousal, dominant emotion)
110    pub fn with_emotional(mut self, emotional: EmotionalContext) -> Self {
111        self.context.emotional = emotional;
112        self
113    }
114
115    /// Set source context (source type, credibility)
116    pub fn with_source(mut self, source: SourceContext) -> Self {
117        self.context.source = source;
118        self
119    }
120
121    /// Set episode context (episode ID, sequence, temporal chain)
122    pub fn with_episode(mut self, episode: EpisodeContext) -> Self {
123        self.context.episode = episode;
124        self
125    }
126
127    /// Build the context
128    pub fn build(self) -> RichContext {
129        self.context
130    }
131}
132
133/// Context manager for automatic context capture
134///
135/// Public API for managing conversation context across sessions.
136/// Tracks user profile, project state, and temporal patterns.
137#[allow(unused)] // Public API
138pub struct ContextManager {
139    /// Current active context
140    current_context: Option<RichContext>,
141
142    /// Context history (sliding window)
143    context_history: Vec<RichContext>,
144    max_history: usize,
145
146    /// User profile (accumulated over time)
147    user_profile: UserContext,
148
149    /// Project state
150    project_state: HashMap<String, ProjectContext>,
151}
152
153#[allow(unused)] // Public API
154impl Default for ContextManager {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160#[allow(unused)] // Public API methods
161impl ContextManager {
162    /// Create new context manager
163    pub fn new() -> Self {
164        Self {
165            current_context: None,
166            context_history: Vec::new(),
167            max_history: 50,
168            user_profile: UserContext::default(),
169            project_state: HashMap::new(),
170        }
171    }
172
173    /// Capture current context automatically
174    pub fn capture_context(&mut self) -> Result<RichContext> {
175        let mut builder = ContextBuilder::new();
176
177        // Capture temporal context
178        let now = Utc::now();
179        let temporal = TemporalContext {
180            time_of_day: Some(format!("{:02}:{:02}", now.hour(), now.minute())),
181            day_of_week: Some(now.weekday().to_string()),
182            session_duration_minutes: self.calculate_session_duration(),
183            time_since_last_interaction: self.calculate_time_since_last(),
184            patterns: self.detect_temporal_patterns(),
185            trends: Vec::new(),
186        };
187
188        builder = builder
189            .with_temporal(temporal)
190            .with_user(self.user_profile.clone());
191
192        // Add parent context if available
193        if let Some(parent) = &self.current_context {
194            builder = builder.with_parent(parent.clone());
195        }
196
197        // Build context
198        let context = builder.build();
199
200        // Update current context
201        self.current_context = Some(context.clone());
202
203        // Add to history
204        self.context_history.push(context.clone());
205        if self.context_history.len() > self.max_history {
206            self.context_history.remove(0);
207        }
208
209        Ok(context)
210    }
211
212    /// Update user profile based on interactions
213    pub fn update_user_profile(&mut self, experience: &Experience) {
214        // Extract entities mentioned
215        for entity in &experience.entities {
216            if !self.user_profile.expertise.contains(entity) {
217                // Check if mentioned frequently
218                let count = self
219                    .context_history
220                    .iter()
221                    .filter(|ctx| ctx.conversation.mentioned_entities.contains(entity))
222                    .count();
223
224                if count > 5 {
225                    self.user_profile.expertise.push(entity.clone());
226                }
227            }
228        }
229
230        // Extract preferences from metadata
231        if let Some(ctx) = &experience.context {
232            for (key, value) in &ctx.user.preferences {
233                self.user_profile
234                    .preferences
235                    .insert(key.clone(), value.clone());
236            }
237        }
238    }
239
240    /// Update conversation context
241    pub fn update_conversation(
242        &mut self,
243        conv_id: String,
244        message: String,
245        topic: Option<String>,
246    ) -> Result<()> {
247        if let Some(ref mut ctx) = self.current_context {
248            ctx.conversation.conversation_id = Some(conv_id);
249            ctx.conversation.recent_messages.push(message.clone());
250
251            // Keep only last 10 messages
252            if ctx.conversation.recent_messages.len() > 10 {
253                ctx.conversation.recent_messages.remove(0);
254            }
255
256            if let Some(t) = topic {
257                ctx.conversation.topic = Some(t);
258            }
259
260            ctx.updated_at = Utc::now();
261        }
262        Ok(())
263    }
264
265    /// Update code context
266    pub fn update_code_context(&mut self, file: String, scope: Option<String>) -> Result<()> {
267        if let Some(ref mut ctx) = self.current_context {
268            ctx.code.current_file = Some(file.clone());
269            ctx.code.current_scope = scope;
270
271            // Track recent edits
272            if !ctx.code.recent_edits.contains(&file) {
273                ctx.code.recent_edits.push(file);
274            }
275
276            // Keep only last 20 edits
277            if ctx.code.recent_edits.len() > 20 {
278                ctx.code.recent_edits.remove(0);
279            }
280
281            ctx.updated_at = Utc::now();
282        }
283        Ok(())
284    }
285
286    /// Update project context
287    pub fn update_project_context(
288        &mut self,
289        project_id: String,
290        project: ProjectContext,
291    ) -> Result<()> {
292        self.project_state
293            .insert(project_id.clone(), project.clone());
294
295        if let Some(ref mut ctx) = self.current_context {
296            ctx.project = project;
297            ctx.updated_at = Utc::now();
298        }
299        Ok(())
300    }
301
302    /// Get current context
303    pub fn get_current_context(&self) -> Option<&RichContext> {
304        self.current_context.as_ref()
305    }
306
307    /// Merge multiple contexts
308    pub fn merge_contexts(&self, contexts: Vec<RichContext>) -> RichContext {
309        if contexts.is_empty() {
310            return ContextBuilder::new().build();
311        }
312
313        let mut merged = contexts[0].clone();
314
315        for ctx in contexts.iter().skip(1) {
316            // Merge conversation contexts
317            merged
318                .conversation
319                .recent_messages
320                .extend(ctx.conversation.recent_messages.clone());
321            merged
322                .conversation
323                .mentioned_entities
324                .extend(ctx.conversation.mentioned_entities.clone());
325            merged
326                .conversation
327                .active_intents
328                .extend(ctx.conversation.active_intents.clone());
329
330            // Merge code contexts
331            merged
332                .code
333                .related_files
334                .extend(ctx.code.related_files.clone());
335            merged
336                .code
337                .recent_edits
338                .extend(ctx.code.recent_edits.clone());
339            merged.code.patterns.extend(ctx.code.patterns.clone());
340
341            // Merge semantic contexts
342            merged
343                .semantic
344                .concepts
345                .extend(ctx.semantic.concepts.clone());
346            merged
347                .semantic
348                .related_concepts
349                .extend(ctx.semantic.related_concepts.clone());
350            merged.semantic.tags.extend(ctx.semantic.tags.clone());
351
352            // Deduplicate
353            merged.conversation.mentioned_entities.sort();
354            merged.conversation.mentioned_entities.dedup();
355            merged.code.related_files.sort();
356            merged.code.related_files.dedup();
357            merged.semantic.concepts.sort();
358            merged.semantic.concepts.dedup();
359        }
360
361        merged.updated_at = Utc::now();
362        merged
363    }
364
365    /// Find similar contexts from history
366    pub fn find_similar_contexts(
367        &self,
368        query_context: &RichContext,
369        top_k: usize,
370    ) -> Vec<RichContext> {
371        let mut scored_contexts: Vec<(f32, RichContext)> = self
372            .context_history
373            .iter()
374            .map(|ctx| {
375                let score = self.calculate_context_similarity(query_context, ctx);
376                (score, ctx.clone())
377            })
378            .collect();
379
380        scored_contexts.sort_by(|a, b| b.0.total_cmp(&a.0));
381        scored_contexts
382            .into_iter()
383            .take(top_k)
384            .map(|(_, ctx)| ctx)
385            .collect()
386    }
387
388    /// Calculate similarity between two contexts
389    fn calculate_context_similarity(&self, ctx1: &RichContext, ctx2: &RichContext) -> f32 {
390        let mut score = 0.0;
391
392        // Conversation similarity
393        let conv_overlap = ctx1
394            .conversation
395            .mentioned_entities
396            .iter()
397            .filter(|e| ctx2.conversation.mentioned_entities.contains(e))
398            .count();
399        score += conv_overlap as f32 * 0.2;
400
401        // Project similarity
402        if ctx1.project.project_id == ctx2.project.project_id {
403            score += 0.3;
404        }
405
406        // Code similarity
407        let code_overlap = ctx1
408            .code
409            .related_files
410            .iter()
411            .filter(|f| ctx2.code.related_files.contains(f))
412            .count();
413        score += code_overlap as f32 * 0.15;
414
415        // Semantic similarity
416        let concept_overlap = ctx1
417            .semantic
418            .concepts
419            .iter()
420            .filter(|c| ctx2.semantic.concepts.contains(c))
421            .count();
422        score += concept_overlap as f32 * 0.25;
423
424        // Temporal decay
425        let time_diff = (Utc::now() - ctx2.created_at).num_hours() as f32;
426        let decay_factor = (-ctx2.decay_rate * time_diff / 24.0).exp();
427        score *= decay_factor;
428
429        score
430    }
431
432    /// Helper: Calculate session duration
433    fn calculate_session_duration(&self) -> Option<u32> {
434        self.context_history.first().map(|first| {
435            let duration = Utc::now() - first.created_at;
436            duration.num_minutes() as u32
437        })
438    }
439
440    /// Helper: Calculate time since last interaction
441    fn calculate_time_since_last(&self) -> Option<i64> {
442        self.context_history
443            .last()
444            .map(|last| (Utc::now() - last.updated_at).num_seconds())
445    }
446
447    /// Helper: Detect temporal patterns
448    fn detect_temporal_patterns(&self) -> Vec<TimePattern> {
449        // Simple pattern detection - can be enhanced
450        Vec::new()
451    }
452
453    /// Get user expertise areas
454    pub fn get_user_expertise(&self) -> &Vec<String> {
455        &self.user_profile.expertise
456    }
457
458    /// Add entity to user expertise
459    pub fn add_user_expertise(&mut self, entity: String) {
460        if !self.user_profile.expertise.contains(&entity) {
461            self.user_profile.expertise.push(entity);
462        }
463    }
464}
465
466#[cfg(test)]
467mod tests {
468    use super::*;
469
470    #[test]
471    fn test_context_builder() {
472        let ctx = ContextBuilder::new()
473            .with_conversation(ConversationContext {
474                topic: Some("Testing".to_string()),
475                ..Default::default()
476            })
477            .build();
478
479        assert_eq!(ctx.conversation.topic, Some("Testing".to_string()));
480    }
481
482    #[test]
483    fn test_context_manager() {
484        let mut manager = ContextManager::new();
485        let ctx = manager.capture_context().unwrap();
486
487        assert!(ctx.temporal.time_of_day.is_some());
488    }
489}