rag_plusplus_core/trajectory/chain/
manager.rs1use std::collections::HashMap;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8use crate::trajectory::branch::{BranchStateMachine, BranchError};
9use crate::trajectory::graph::{TrajectoryGraph, NodeId};
10
11pub type ChainId = String;
13
14#[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
23fn 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#[derive(Debug, Clone)]
35pub struct ChainMetadata {
36 pub id: ChainId,
38 pub title: Option<String>,
40 pub created_at: i64,
42 pub last_accessed_at: i64,
44 pub node_count: usize,
46 pub branch_count: usize,
48 pub is_active: bool,
50 pub tags: Vec<String>,
52}
53
54impl ChainMetadata {
55 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 pub fn touch(&mut self) {
72 self.last_accessed_at = current_timestamp();
73 }
74
75 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#[derive(Debug, Clone)]
84pub struct ChainManagerConfig {
85 pub max_chains: usize,
87 pub inactivity_threshold_secs: u64,
89 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, auto_cleanup: false,
99 }
100 }
101}
102
103#[derive(Debug, Clone, Default)]
105pub struct ChainManagerStats {
106 pub total_chains: usize,
108 pub active_chains: usize,
110 pub total_nodes: usize,
112 pub total_branches: usize,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq)]
118pub enum ChainManagerError {
119 ChainNotFound(ChainId),
121 ChainAlreadyExists(ChainId),
123 MaxChainsReached,
125 BranchError(String),
127 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
151pub struct ChainManager {
176 chains: HashMap<ChainId, BranchStateMachine>,
178 metadata: HashMap<ChainId, ChainMetadata>,
180 active_chain: Option<ChainId>,
182 config: ChainManagerConfig,
184}
185
186impl Default for ChainManager {
187 fn default() -> Self {
188 Self::new()
189 }
190}
191
192impl ChainManager {
193 pub fn new() -> Self {
195 Self::with_config(ChainManagerConfig::default())
196 }
197
198 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 pub fn create_chain(&mut self, chain_id: Option<ChainId>) -> Result<ChainId, ChainManagerError> {
217 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 let graph = TrajectoryGraph::new();
235 let machine = BranchStateMachine::from_graph(graph);
236
237 let metadata = ChainMetadata::new(id.clone());
239
240 self.chains.insert(id.clone(), machine);
241 self.metadata.insert(id.clone(), metadata);
242
243 if self.active_chain.is_none() {
245 self.active_chain = Some(id.clone());
246 }
247
248 Ok(id)
249 }
250
251 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 pub fn get_chain(&self, chain_id: &ChainId) -> Option<&BranchStateMachine> {
279 self.chains.get(chain_id)
280 }
281
282 pub fn get_chain_mut(&mut self, chain_id: &ChainId) -> Option<&mut BranchStateMachine> {
284 if let Some(meta) = self.metadata.get_mut(chain_id) {
286 meta.touch();
287 }
288 self.chains.get_mut(chain_id)
289 }
290
291 pub fn get_metadata(&self, chain_id: &ChainId) -> Option<&ChainMetadata> {
293 self.metadata.get(chain_id)
294 }
295
296 pub fn get_metadata_mut(&mut self, chain_id: &ChainId) -> Option<&mut ChainMetadata> {
298 self.metadata.get_mut(chain_id)
299 }
300
301 pub fn contains(&self, chain_id: &ChainId) -> bool {
303 self.chains.contains_key(chain_id)
304 }
305
306 pub fn chain_ids(&self) -> impl Iterator<Item = &ChainId> {
308 self.chains.keys()
309 }
310
311 pub fn chain_count(&self) -> usize {
313 self.chains.len()
314 }
315
316 pub fn active_chain(&self) -> Option<&ChainId> {
322 self.active_chain.as_ref()
323 }
324
325 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 pub fn get_active_chain(&self) -> Option<&BranchStateMachine> {
336 self.active_chain.as_ref().and_then(|id| self.chains.get(id))
337 }
338
339 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 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 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 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 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 for branch in from_chain.all_branches() {
393 let _ = branch; }
397
398 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 if self.active_chain.as_ref() == Some(from) {
407 self.active_chain = Some(into.clone());
408 }
409
410 Ok(())
411 }
412
413 pub fn split_chain(
417 &mut self,
418 chain_id: &ChainId,
419 node_id: NodeId,
420 ) -> Result<ChainId, ChainManagerError> {
421 let chain = self.chains.get_mut(chain_id)
423 .ok_or_else(|| ChainManagerError::ChainNotFound(chain_id.clone()))?;
424
425 let split_result = chain.split(node_id)?;
427
428 let new_chain_id = generate_chain_id();
430
431 let mut metadata = ChainMetadata::new(new_chain_id.clone());
434 metadata.title = Some(format!("Split from {} at node {}", chain_id, node_id));
435
436 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; Ok(new_chain_id)
447 }
448
449 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 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 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 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 pub fn all_metadata(&self) -> impl Iterator<Item = &ChainMetadata> {
515 self.metadata.values()
516 }
517
518 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 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 assert_eq!(manager.active_chain(), Some(&chain_id));
594
595 let chain_id2 = manager.create_chain(None).unwrap();
597
598 assert_eq!(manager.active_chain(), Some(&chain_id));
600
601 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 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 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}