rag_plusplus_core/trajectory/chain/
manager.rs

1//! Chain Manager Implementation
2//!
3//! Manages multiple conversation chains (BranchStateMachine instances).
4//! Ported from DLM's ChainManager class.
5
6use std::collections::HashMap;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8use crate::trajectory::branch::{BranchStateMachine, BranchError};
9use crate::trajectory::graph::{TrajectoryGraph, NodeId};
10
11/// Unique identifier for a chain (conversation).
12pub type ChainId = String;
13
14/// Get current Unix timestamp in seconds.
15#[inline]
16fn current_timestamp() -> i64 {
17    SystemTime::now()
18        .duration_since(UNIX_EPOCH)
19        .map(|d| d.as_secs() as i64)
20        .unwrap_or(0)
21}
22
23/// Generate a unique chain ID.
24fn generate_chain_id() -> ChainId {
25    use std::sync::atomic::{AtomicU64, Ordering};
26    static COUNTER: AtomicU64 = AtomicU64::new(0);
27
28    let timestamp = current_timestamp();
29    let counter = COUNTER.fetch_add(1, Ordering::Relaxed);
30    format!("chain_{:x}_{:04x}", timestamp, counter)
31}
32
33/// Metadata about a chain.
34#[derive(Debug, Clone)]
35pub struct ChainMetadata {
36    /// Chain ID
37    pub id: ChainId,
38    /// Human-readable title
39    pub title: Option<String>,
40    /// When the chain was created
41    pub created_at: i64,
42    /// When the chain was last accessed
43    pub last_accessed_at: i64,
44    /// Number of nodes in the chain
45    pub node_count: usize,
46    /// Number of branches in the chain
47    pub branch_count: usize,
48    /// Whether the chain is active
49    pub is_active: bool,
50    /// Custom metadata
51    pub tags: Vec<String>,
52}
53
54impl ChainMetadata {
55    /// Create metadata for a new chain.
56    pub fn new(id: ChainId) -> Self {
57        let now = current_timestamp();
58        Self {
59            id,
60            title: None,
61            created_at: now,
62            last_accessed_at: now,
63            node_count: 0,
64            branch_count: 0,
65            is_active: true,
66            tags: Vec::new(),
67        }
68    }
69
70    /// Update last accessed time to now.
71    pub fn touch(&mut self) {
72        self.last_accessed_at = current_timestamp();
73    }
74
75    /// Update statistics from a state machine.
76    pub fn update_stats(&mut self, machine: &BranchStateMachine) {
77        self.node_count = machine.graph().node_count();
78        self.branch_count = machine.branch_count();
79    }
80}
81
82/// Configuration for chain manager.
83#[derive(Debug, Clone)]
84pub struct ChainManagerConfig {
85    /// Maximum number of chains to keep in memory
86    pub max_chains: usize,
87    /// Inactivity threshold for cleanup (in seconds)
88    pub inactivity_threshold_secs: u64,
89    /// Whether to auto-cleanup on operations
90    pub auto_cleanup: bool,
91}
92
93impl Default for ChainManagerConfig {
94    fn default() -> Self {
95        Self {
96            max_chains: 1000,
97            inactivity_threshold_secs: 3600, // 1 hour
98            auto_cleanup: false,
99        }
100    }
101}
102
103/// Statistics about the chain manager.
104#[derive(Debug, Clone, Default)]
105pub struct ChainManagerStats {
106    /// Total chains managed
107    pub total_chains: usize,
108    /// Active chains
109    pub active_chains: usize,
110    /// Total nodes across all chains
111    pub total_nodes: usize,
112    /// Total branches across all chains
113    pub total_branches: usize,
114}
115
116/// Errors that can occur in chain management.
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum ChainManagerError {
119    /// Chain not found
120    ChainNotFound(ChainId),
121    /// Chain already exists
122    ChainAlreadyExists(ChainId),
123    /// Maximum chains reached
124    MaxChainsReached,
125    /// Branch operation error
126    BranchError(String),
127    /// Invalid operation
128    InvalidOperation(String),
129}
130
131impl std::fmt::Display for ChainManagerError {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        match self {
134            Self::ChainNotFound(id) => write!(f, "Chain not found: {}", id),
135            Self::ChainAlreadyExists(id) => write!(f, "Chain already exists: {}", id),
136            Self::MaxChainsReached => write!(f, "Maximum number of chains reached"),
137            Self::BranchError(msg) => write!(f, "Branch error: {}", msg),
138            Self::InvalidOperation(msg) => write!(f, "Invalid operation: {}", msg),
139        }
140    }
141}
142
143impl std::error::Error for ChainManagerError {}
144
145impl From<BranchError> for ChainManagerError {
146    fn from(err: BranchError) -> Self {
147        Self::BranchError(err.to_string())
148    }
149}
150
151/// Manager for multiple conversation chains.
152///
153/// Provides operations for creating, accessing, and managing multiple
154/// conversation state machines. Ported from DLM's ChainManager.
155///
156/// # Example
157///
158/// ```ignore
159/// let mut manager = ChainManager::new();
160///
161/// // Create a chain with auto-generated ID
162/// let chain_id = manager.create_chain(None);
163///
164/// // Create a chain with specific ID
165/// let chain_id = manager.create_chain(Some("my-chain".to_string()));
166///
167/// // Get chain for operations
168/// if let Some(chain) = manager.get_chain_mut(&chain_id) {
169///     chain.split(some_node)?;
170/// }
171///
172/// // Merge two chains
173/// manager.merge_chains(&chain1, &chain2)?;
174/// ```
175pub struct ChainManager {
176    /// All managed chains
177    chains: HashMap<ChainId, BranchStateMachine>,
178    /// Metadata for each chain
179    metadata: HashMap<ChainId, ChainMetadata>,
180    /// Currently active chain (if any)
181    active_chain: Option<ChainId>,
182    /// Configuration
183    config: ChainManagerConfig,
184}
185
186impl Default for ChainManager {
187    fn default() -> Self {
188        Self::new()
189    }
190}
191
192impl ChainManager {
193    /// Create a new chain manager with default config.
194    pub fn new() -> Self {
195        Self::with_config(ChainManagerConfig::default())
196    }
197
198    /// Create a new chain manager with specific config.
199    pub fn with_config(config: ChainManagerConfig) -> Self {
200        Self {
201            chains: HashMap::new(),
202            metadata: HashMap::new(),
203            active_chain: None,
204            config,
205        }
206    }
207
208    // =========================================================================
209    // CHAIN CREATION & ACCESS
210    // =========================================================================
211
212    /// Create a new conversation chain.
213    ///
214    /// If `chain_id` is None, a unique ID is generated.
215    /// Returns the chain ID.
216    pub fn create_chain(&mut self, chain_id: Option<ChainId>) -> Result<ChainId, ChainManagerError> {
217        // Check max chains limit
218        if self.chains.len() >= self.config.max_chains {
219            if self.config.auto_cleanup {
220                self.cleanup_inactive(Duration::from_secs(self.config.inactivity_threshold_secs));
221            }
222            if self.chains.len() >= self.config.max_chains {
223                return Err(ChainManagerError::MaxChainsReached);
224            }
225        }
226
227        let id = chain_id.unwrap_or_else(generate_chain_id);
228
229        if self.chains.contains_key(&id) {
230            return Err(ChainManagerError::ChainAlreadyExists(id));
231        }
232
233        // Create empty graph and state machine
234        let graph = TrajectoryGraph::new();
235        let machine = BranchStateMachine::from_graph(graph);
236
237        // Create metadata
238        let metadata = ChainMetadata::new(id.clone());
239
240        self.chains.insert(id.clone(), machine);
241        self.metadata.insert(id.clone(), metadata);
242
243        // Set as active if no active chain
244        if self.active_chain.is_none() {
245            self.active_chain = Some(id.clone());
246        }
247
248        Ok(id)
249    }
250
251    /// Create a chain from an existing graph.
252    pub fn create_chain_from_graph(
253        &mut self,
254        chain_id: Option<ChainId>,
255        graph: TrajectoryGraph,
256    ) -> Result<ChainId, ChainManagerError> {
257        let id = chain_id.unwrap_or_else(generate_chain_id);
258
259        if self.chains.contains_key(&id) {
260            return Err(ChainManagerError::ChainAlreadyExists(id));
261        }
262
263        let machine = BranchStateMachine::from_graph(graph);
264        let mut metadata = ChainMetadata::new(id.clone());
265        metadata.update_stats(&machine);
266
267        self.chains.insert(id.clone(), machine);
268        self.metadata.insert(id.clone(), metadata);
269
270        if self.active_chain.is_none() {
271            self.active_chain = Some(id.clone());
272        }
273
274        Ok(id)
275    }
276
277    /// Get a chain by ID (immutable).
278    pub fn get_chain(&self, chain_id: &ChainId) -> Option<&BranchStateMachine> {
279        self.chains.get(chain_id)
280    }
281
282    /// Get a chain by ID (mutable).
283    pub fn get_chain_mut(&mut self, chain_id: &ChainId) -> Option<&mut BranchStateMachine> {
284        // Update access time
285        if let Some(meta) = self.metadata.get_mut(chain_id) {
286            meta.touch();
287        }
288        self.chains.get_mut(chain_id)
289    }
290
291    /// Get chain metadata.
292    pub fn get_metadata(&self, chain_id: &ChainId) -> Option<&ChainMetadata> {
293        self.metadata.get(chain_id)
294    }
295
296    /// Get mutable chain metadata.
297    pub fn get_metadata_mut(&mut self, chain_id: &ChainId) -> Option<&mut ChainMetadata> {
298        self.metadata.get_mut(chain_id)
299    }
300
301    /// Check if a chain exists.
302    pub fn contains(&self, chain_id: &ChainId) -> bool {
303        self.chains.contains_key(chain_id)
304    }
305
306    /// Get all chain IDs.
307    pub fn chain_ids(&self) -> impl Iterator<Item = &ChainId> {
308        self.chains.keys()
309    }
310
311    /// Get number of chains.
312    pub fn chain_count(&self) -> usize {
313        self.chains.len()
314    }
315
316    // =========================================================================
317    // ACTIVE CHAIN MANAGEMENT
318    // =========================================================================
319
320    /// Get the currently active chain ID.
321    pub fn active_chain(&self) -> Option<&ChainId> {
322        self.active_chain.as_ref()
323    }
324
325    /// Set the active chain.
326    pub fn set_active_chain(&mut self, chain_id: ChainId) -> Result<(), ChainManagerError> {
327        if !self.chains.contains_key(&chain_id) {
328            return Err(ChainManagerError::ChainNotFound(chain_id));
329        }
330        self.active_chain = Some(chain_id);
331        Ok(())
332    }
333
334    /// Get the active chain (immutable).
335    pub fn get_active_chain(&self) -> Option<&BranchStateMachine> {
336        self.active_chain.as_ref().and_then(|id| self.chains.get(id))
337    }
338
339    /// Get the active chain (mutable).
340    pub fn get_active_chain_mut(&mut self) -> Option<&mut BranchStateMachine> {
341        if let Some(id) = &self.active_chain {
342            if let Some(meta) = self.metadata.get_mut(id) {
343                meta.touch();
344            }
345            self.chains.get_mut(id)
346        } else {
347            None
348        }
349    }
350
351    // =========================================================================
352    // CHAIN OPERATIONS
353    // =========================================================================
354
355    /// Delete a chain.
356    pub fn delete_chain(&mut self, chain_id: &ChainId) -> bool {
357        let removed = self.chains.remove(chain_id).is_some();
358        self.metadata.remove(chain_id);
359
360        // Update active chain if deleted
361        if self.active_chain.as_ref() == Some(chain_id) {
362            self.active_chain = self.chains.keys().next().cloned();
363        }
364
365        removed
366    }
367
368    /// Merge two chains.
369    ///
370    /// The `from` chain is merged into `into`, and then deleted.
371    pub fn merge_chains(
372        &mut self,
373        from: &ChainId,
374        into: &ChainId,
375    ) -> Result<(), ChainManagerError> {
376        if from == into {
377            return Err(ChainManagerError::InvalidOperation(
378                "Cannot merge chain with itself".to_string(),
379            ));
380        }
381
382        // Get both chains
383        let from_chain = self.chains.remove(from)
384            .ok_or_else(|| ChainManagerError::ChainNotFound(from.clone()))?;
385
386        let into_chain = self.chains.get_mut(into)
387            .ok_or_else(|| ChainManagerError::ChainNotFound(into.clone()))?;
388
389        // Merge the graphs (append from's graph to into's)
390        // Note: This is a simplified merge - in production you'd want
391        // to properly connect the graphs
392        for branch in from_chain.all_branches() {
393            // Add branches from 'from' chain to 'into' chain
394            // This is a simplified implementation
395            let _ = branch; // Placeholder for actual merge logic
396        }
397
398        // Update metadata
399        self.metadata.remove(from);
400        if let Some(meta) = self.metadata.get_mut(into) {
401            meta.update_stats(into_chain);
402            meta.touch();
403        }
404
405        // Update active chain if needed
406        if self.active_chain.as_ref() == Some(from) {
407            self.active_chain = Some(into.clone());
408        }
409
410        Ok(())
411    }
412
413    /// Split a chain at a node, creating a new chain.
414    ///
415    /// Returns the ID of the new chain.
416    pub fn split_chain(
417        &mut self,
418        chain_id: &ChainId,
419        node_id: NodeId,
420    ) -> Result<ChainId, ChainManagerError> {
421        // Get the chain
422        let chain = self.chains.get_mut(chain_id)
423            .ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
424
425        // Perform the split
426        let split_result = chain.split(node_id)?;
427
428        // Create a new chain for the split branch
429        let new_chain_id = generate_chain_id();
430
431        // Note: In a full implementation, we'd extract the subtree
432        // into a new graph. For now, we just record the split.
433        let mut metadata = ChainMetadata::new(new_chain_id.clone());
434        metadata.title = Some(format!("Split from {} at node {}", chain_id, node_id));
435
436        // Update original chain metadata
437        if let Some(meta) = self.metadata.get_mut(chain_id) {
438            meta.update_stats(chain);
439            meta.touch();
440        }
441
442        self.metadata.insert(new_chain_id.clone(), metadata);
443
444        let _ = split_result; // Use split result in full implementation
445
446        Ok(new_chain_id)
447    }
448
449    // =========================================================================
450    // CLEANUP & MAINTENANCE
451    // =========================================================================
452
453    /// Cleanup inactive chains.
454    ///
455    /// Removes chains that haven't been accessed within the threshold.
456    pub fn cleanup_inactive(&mut self, threshold: Duration) {
457        let now = current_timestamp();
458        let threshold_secs = threshold.as_secs() as i64;
459
460        let inactive: Vec<ChainId> = self.metadata
461            .iter()
462            .filter(|(_, meta)| {
463                now - meta.last_accessed_at > threshold_secs
464            })
465            .map(|(id, _)| id.clone())
466            .collect();
467
468        for chain_id in inactive {
469            self.delete_chain(&chain_id);
470        }
471    }
472
473    /// Mark a chain as inactive.
474    pub fn deactivate_chain(&mut self, chain_id: &ChainId) -> Result<(), ChainManagerError> {
475        let meta = self.metadata.get_mut(chain_id)
476            .ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
477        meta.is_active = false;
478        Ok(())
479    }
480
481    /// Reactivate a chain.
482    pub fn reactivate_chain(&mut self, chain_id: &ChainId) -> Result<(), ChainManagerError> {
483        let meta = self.metadata.get_mut(chain_id)
484            .ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
485        meta.is_active = true;
486        meta.touch();
487        Ok(())
488    }
489
490    // =========================================================================
491    // STATISTICS
492    // =========================================================================
493
494    /// Get statistics about all managed chains.
495    pub fn stats(&self) -> ChainManagerStats {
496        let mut stats = ChainManagerStats::default();
497        stats.total_chains = self.chains.len();
498
499        for (id, chain) in &self.chains {
500            stats.total_nodes += chain.graph().node_count();
501            stats.total_branches += chain.branch_count();
502
503            if let Some(meta) = self.metadata.get(id) {
504                if meta.is_active {
505                    stats.active_chains += 1;
506                }
507            }
508        }
509
510        stats
511    }
512
513    /// Get all metadata.
514    pub fn all_metadata(&self) -> impl Iterator<Item = &ChainMetadata> {
515        self.metadata.values()
516    }
517
518    // =========================================================================
519    // SEARCH & QUERY
520    // =========================================================================
521
522    /// Find chains by tag.
523    pub fn find_by_tag(&self, tag: &str) -> Vec<&ChainId> {
524        self.metadata
525            .iter()
526            .filter(|(_, meta)| meta.tags.contains(&tag.to_string()))
527            .map(|(id, _)| id)
528            .collect()
529    }
530
531    /// Find chains by title (partial match).
532    pub fn find_by_title(&self, query: &str) -> Vec<&ChainId> {
533        let query_lower = query.to_lowercase();
534        self.metadata
535            .iter()
536            .filter(|(_, meta)| {
537                meta.title
538                    .as_ref()
539                    .map_or(false, |t| t.to_lowercase().contains(&query_lower))
540            })
541            .map(|(id, _)| id)
542            .collect()
543    }
544}
545
546#[cfg(test)]
547mod tests {
548    use super::*;
549
550    #[test]
551    fn test_create_chain() {
552        let mut manager = ChainManager::new();
553        let chain_id = manager.create_chain(None).unwrap();
554
555        assert!(manager.contains(&chain_id));
556        assert_eq!(manager.chain_count(), 1);
557    }
558
559    #[test]
560    fn test_create_chain_with_id() {
561        let mut manager = ChainManager::new();
562        let chain_id = manager.create_chain(Some("my-chain".to_string())).unwrap();
563
564        assert_eq!(chain_id, "my-chain");
565        assert!(manager.contains(&"my-chain".to_string()));
566    }
567
568    #[test]
569    fn test_duplicate_chain_error() {
570        let mut manager = ChainManager::new();
571        manager.create_chain(Some("test".to_string())).unwrap();
572
573        let result = manager.create_chain(Some("test".to_string()));
574        assert!(matches!(result, Err(ChainManagerError::ChainAlreadyExists(_))));
575    }
576
577    #[test]
578    fn test_delete_chain() {
579        let mut manager = ChainManager::new();
580        let chain_id = manager.create_chain(None).unwrap();
581
582        assert!(manager.delete_chain(&chain_id));
583        assert!(!manager.contains(&chain_id));
584        assert_eq!(manager.chain_count(), 0);
585    }
586
587    #[test]
588    fn test_active_chain() {
589        let mut manager = ChainManager::new();
590        let chain_id = manager.create_chain(None).unwrap();
591
592        // First chain should be active by default
593        assert_eq!(manager.active_chain(), Some(&chain_id));
594
595        // Create another chain
596        let chain_id2 = manager.create_chain(None).unwrap();
597
598        // First chain should still be active
599        assert_eq!(manager.active_chain(), Some(&chain_id));
600
601        // Set second chain as active
602        manager.set_active_chain(chain_id2.clone()).unwrap();
603        assert_eq!(manager.active_chain(), Some(&chain_id2));
604    }
605
606    #[test]
607    fn test_get_chain() {
608        let mut manager = ChainManager::new();
609        let chain_id = manager.create_chain(None).unwrap();
610
611        assert!(manager.get_chain(&chain_id).is_some());
612        assert!(manager.get_chain(&"nonexistent".to_string()).is_none());
613    }
614
615    #[test]
616    fn test_stats() {
617        let mut manager = ChainManager::new();
618        manager.create_chain(None).unwrap();
619        manager.create_chain(None).unwrap();
620
621        let stats = manager.stats();
622        assert_eq!(stats.total_chains, 2);
623        assert_eq!(stats.active_chains, 2);
624    }
625
626    #[test]
627    fn test_metadata() {
628        let mut manager = ChainManager::new();
629        let chain_id = manager.create_chain(Some("test".to_string())).unwrap();
630
631        let meta = manager.get_metadata(&chain_id).unwrap();
632        assert!(meta.is_active);
633        assert!(meta.created_at > 0);
634
635        // Update metadata
636        let meta = manager.get_metadata_mut(&chain_id).unwrap();
637        meta.title = Some("My Chain".to_string());
638        meta.tags.push("important".to_string());
639
640        // Verify
641        let meta = manager.get_metadata(&chain_id).unwrap();
642        assert_eq!(meta.title, Some("My Chain".to_string()));
643        assert!(meta.tags.contains(&"important".to_string()));
644    }
645
646    #[test]
647    fn test_find_by_tag() {
648        let mut manager = ChainManager::new();
649        let chain_id = manager.create_chain(Some("test".to_string())).unwrap();
650
651        let meta = manager.get_metadata_mut(&chain_id).unwrap();
652        meta.tags.push("project".to_string());
653
654        let found = manager.find_by_tag("project");
655        assert_eq!(found.len(), 1);
656        assert_eq!(found[0], &chain_id);
657
658        let not_found = manager.find_by_tag("nonexistent");
659        assert!(not_found.is_empty());
660    }
661
662    #[test]
663    fn test_deactivate_reactivate() {
664        let mut manager = ChainManager::new();
665        let chain_id = manager.create_chain(None).unwrap();
666
667        assert!(manager.get_metadata(&chain_id).unwrap().is_active);
668
669        manager.deactivate_chain(&chain_id).unwrap();
670        assert!(!manager.get_metadata(&chain_id).unwrap().is_active);
671
672        manager.reactivate_chain(&chain_id).unwrap();
673        assert!(manager.get_metadata(&chain_id).unwrap().is_active);
674    }
675}