Skip to main content

lc_langgraph/
persistence.rs

1// crates/lc-langgraph/src/persistence.rs
2//! Graph persistence for serialization and storage
3//!
4//! This module provides persistence capabilities for graph definitions,
5//! allowing graphs to be saved, loaded, and shared across sessions.
6
7use async_trait::async_trait;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::path::PathBuf;
12use tokio::sync::Mutex;
13use uuid::Uuid;
14
15/// GraphPersistence trait for storing and loading graph definitions
16#[async_trait]
17pub trait GraphPersistence: Send + Sync {
18    /// Save a graph definition with the given ID
19    async fn save(&self, id: &str, definition: &GraphDefinition) -> Result<(), PersistenceError>;
20
21    /// Load a graph definition by ID
22    async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError>;
23    /// Delete a graph definition by ID
24    async fn delete(&self, id: &str) -> Result<(), PersistenceError>;
25
26    /// Check if a graph definition exists
27    async fn exists(&self, id: &str) -> Result<bool, PersistenceError>;
28
29    /// List all stored graph IDs
30    async fn list(&self) -> Result<Vec<String>, PersistenceError>;
31}
32
33/// Persistence error types
34#[derive(Debug, thiserror::Error)]
35pub enum PersistenceError {
36    #[error("Graph '{0}' not found")]
37    NotFound(String),
38
39    #[error("Serialization error: {0}")]
40    SerializationError(String),
41
42    #[error("Deserialization error: {0}")]
43    DeserializationError(String),
44
45    #[error("IO error: {0}")]
46    IoError(String),
47
48    #[error("Invalid graph definition: {0}")]
49    InvalidDefinition(String),
50
51    #[error("MongoDB error: {0}")]
52    MongoError(String),
53
54    #[error("Connection error: {0}")]
55    ConnectionError(String),
56}
57
58/// GraphDefinition - Serializable graph structure
59#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct GraphDefinition {
61    /// Unique identifier
62    pub id: String,
63
64    /// Human-readable name
65    pub name: Option<String>,
66
67    /// Entry point node name
68    pub entry_point: String,
69
70    /// Node definitions
71    pub nodes: Vec<NodeDefinition>,
72
73    /// Edge definitions
74    pub edges: Vec<EdgeDefinition>,
75
76    /// Router definitions
77    pub routers: Vec<RouterDefinition>,
78
79    /// Maximum recursion limit
80    pub recursion_limit: usize,
81
82    /// Creation timestamp
83    pub created_at: DateTime<Utc>,
84
85    /// Last update timestamp
86    pub updated_at: DateTime<Utc>,
87
88    /// Custom metadata
89    pub metadata: HashMap<String, serde_json::Value>,
90}
91
92impl GraphDefinition {
93    /// Create a new graph definition with the given entry point
94    pub fn new(entry_point: String) -> Self {
95        let now = Utc::now();
96        Self {
97            id: Uuid::new_v4().to_string(),
98            name: None,
99            entry_point,
100            nodes: Vec::new(),
101            edges: Vec::new(),
102            routers: Vec::new(),
103            recursion_limit: 25,
104            created_at: now,
105            updated_at: now,
106            metadata: HashMap::new(),
107        }
108    }
109
110    /// Set a custom ID
111    pub fn with_id(mut self, id: String) -> Self {
112        self.id = id;
113        self
114    }
115
116    /// Set a human-readable name
117    pub fn with_name(mut self, name: String) -> Self {
118        self.name = Some(name);
119        self
120    }
121
122    /// Set recursion limit
123    pub fn with_recursion_limit(mut self, limit: usize) -> Self {
124        self.recursion_limit = limit;
125        self
126    }
127
128    /// Add a node definition
129    pub fn add_node(&mut self, node: NodeDefinition) {
130        self.nodes.push(node);
131        self.updated_at = Utc::now();
132    }
133
134    /// Add an edge definition
135    pub fn add_edge(&mut self, edge: EdgeDefinition) {
136        self.edges.push(edge);
137        self.updated_at = Utc::now();
138    }
139
140    /// Add a router definition
141    pub fn add_router(&mut self, router: RouterDefinition) {
142        self.routers.push(router);
143        self.updated_at = Utc::now();
144    }
145}
146
147/// NodeDefinition - Serializable node structure
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct NodeDefinition {
150    /// Node name
151    pub name: String,
152
153    /// Node type
154    pub node_type: NodeType,
155
156    /// Custom configuration
157    pub config: serde_json::Value,
158}
159
160/// NodeType - Type of node execution
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
162pub enum NodeType {
163    Sync,
164    Async,
165    Subgraph,
166    Custom,
167}
168
169/// EdgeDefinition - Serializable edge structure
170#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct EdgeDefinition {
172    /// Edge type
173    pub edge_type: EdgeType,
174
175    /// Source node name
176    pub source: String,
177
178    /// Target node name (for fixed edges)
179    pub target: Option<String>,
180
181    /// Multiple targets (for fan-out edges)
182    pub targets: Option<Vec<String>>,
183
184    /// Router name (for conditional edges)
185    pub router_name: Option<String>,
186
187    /// Conditional targets mapping (route -> target)
188    pub conditional_targets: Option<HashMap<String, String>>,
189
190    /// Default target for conditional edges
191    pub default_target: Option<String>,
192
193    /// Source nodes for fan-in edges
194    pub sources: Option<Vec<String>>,
195}
196
197impl EdgeDefinition {
198    /// Create a fixed edge
199    pub fn fixed(source: String, target: String) -> Self {
200        Self {
201            edge_type: EdgeType::Fixed,
202            source,
203            target: Some(target),
204            targets: None,
205            router_name: None,
206            conditional_targets: None,
207            default_target: None,
208            sources: None,
209        }
210    }
211
212    /// Create a conditional edge
213    pub fn conditional(
214        source: String,
215        router_name: String,
216        targets: HashMap<String, String>,
217        default_target: Option<String>,
218    ) -> Self {
219        Self {
220            edge_type: EdgeType::Conditional,
221            source,
222            target: None,
223            targets: None,
224            router_name: Some(router_name),
225            conditional_targets: Some(targets),
226            default_target,
227            sources: None,
228        }
229    }
230
231    /// Create a fan-out edge (parallel execution)
232    pub fn fan_out(source: String, targets: Vec<String>) -> Self {
233        Self {
234            edge_type: EdgeType::FanOut,
235            source,
236            target: None,
237            targets: Some(targets),
238            router_name: None,
239            conditional_targets: None,
240            default_target: None,
241            sources: None,
242        }
243    }
244
245    /// Create a fan-in edge (merge from parallel branches)
246    pub fn fan_in(sources: Vec<String>, target: String) -> Self {
247        Self {
248            edge_type: EdgeType::FanIn,
249            source: "__fan_in__".to_string(),
250            target: Some(target),
251            targets: None,
252            router_name: None,
253            conditional_targets: None,
254            default_target: None,
255            sources: Some(sources),
256        }
257    }
258}
259
260/// EdgeType - Type of edge connection
261#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262pub enum EdgeType {
263    /// Fixed transition
264    Fixed,
265
266    /// Conditional routing
267    Conditional,
268
269    /// Parallel fan-out
270    FanOut,
271
272    /// Merge fan-in
273    FanIn,
274}
275
276/// RouterDefinition - Serializable router structure
277#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct RouterDefinition {
279    /// Router name
280    pub name: String,
281
282    /// Router type (e.g., "function", "state_key")
283    pub router_type: String,
284
285    /// Possible routes
286    pub routes: Vec<String>,
287
288    /// Custom configuration
289    pub config: serde_json::Value,
290}
291
292/// MemoryPersistence - In-memory graph storage
293pub struct MemoryPersistence {
294    graphs: Mutex<HashMap<String, GraphDefinition>>,
295}
296
297impl MemoryPersistence {
298    /// Create a new memory persistence instance
299    pub fn new() -> Self {
300        Self {
301            graphs: Mutex::new(HashMap::new()),
302        }
303    }
304}
305
306impl Default for MemoryPersistence {
307    fn default() -> Self {
308        Self::new()
309    }
310}
311
312#[async_trait]
313impl GraphPersistence for MemoryPersistence {
314    async fn save(&self, id: &str, definition: &GraphDefinition) -> Result<(), PersistenceError> {
315        let mut graphs = self.graphs.lock().await;
316        graphs.insert(id.to_string(), definition.clone());
317        Ok(())
318    }
319
320    async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError> {
321        let graphs = self.graphs.lock().await;
322        graphs
323            .get(id)
324            .cloned()
325            .ok_or_else(|| PersistenceError::NotFound(id.to_string()))
326    }
327
328    async fn delete(&self, id: &str) -> Result<(), PersistenceError> {
329        let mut graphs = self.graphs.lock().await;
330        graphs
331            .remove(id)
332            .map(|_| ())
333            .ok_or_else(|| PersistenceError::NotFound(id.to_string()))
334    }
335
336    async fn exists(&self, id: &str) -> Result<bool, PersistenceError> {
337        let graphs = self.graphs.lock().await;
338        Ok(graphs.contains_key(id))
339    }
340
341    async fn list(&self) -> Result<Vec<String>, PersistenceError> {
342        let graphs = self.graphs.lock().await;
343        Ok(graphs.keys().cloned().collect())
344    }
345}
346
347/// FilePersistence - File-based graph storage
348pub struct FilePersistence {
349    directory: PathBuf,
350}
351
352impl FilePersistence {
353    /// Create a new file persistence instance
354    pub fn new(directory: impl Into<PathBuf>) -> Result<Self, PersistenceError> {
355        let dir = directory.into();
356        if !dir.exists() {
357            std::fs::create_dir_all(&dir).map_err(|e| {
358                PersistenceError::IoError(format!(
359                    "Failed to create directory '{}': {}",
360                    dir.display(),
361                    e
362                ))
363            })?;
364        }
365        Ok(Self { directory: dir })
366    }
367
368    fn graph_path(&self, id: &str) -> Result<PathBuf, PersistenceError> {
369        // Sanitize id to prevent path traversal: reject ".." and absolute paths
370        if id.contains("..") || id.contains('/') || id.contains('\\') {
371            return Err(PersistenceError::InvalidDefinition(format!(
372                "Invalid graph id '{}': path traversal detected",
373                id
374            )));
375        }
376        if std::path::Path::new(id).is_absolute() {
377            return Err(PersistenceError::InvalidDefinition(format!(
378                "Invalid graph id '{}': absolute path not allowed",
379                id
380            )));
381        }
382        Ok(self.directory.join(format!("{}.json", id)))
383    }
384}
385
386// NOTE: `Default` is intentionally NOT implemented for `FilePersistence` (Q1).
387// The default constructor would have to create a directory, which is I/O that can
388// fail (read-only cwd, disk full, permissions) — `Default` cannot report that
389// failure, so it would have to panic. Use `FilePersistence::new(...)` which
390// returns a `Result` and surfaces the error instead.
391
392#[async_trait]
393impl GraphPersistence for FilePersistence {
394    async fn save(&self, id: &str, definition: &GraphDefinition) -> Result<(), PersistenceError> {
395        let path = self.graph_path(id)?;
396
397        let json = serde_json::to_string_pretty(definition)
398            .map_err(|e| PersistenceError::SerializationError(e.to_string()))?;
399
400        tokio::fs::write(&path, json)
401            .await
402            .map_err(|e| PersistenceError::IoError(e.to_string()))?;
403
404        Ok(())
405    }
406
407    async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError> {
408        let path = self.graph_path(id)?;
409
410        if !path.exists() {
411            return Err(PersistenceError::NotFound(id.to_string()));
412        }
413
414        let json = tokio::fs::read_to_string(&path)
415            .await
416            .map_err(|e| PersistenceError::IoError(e.to_string()))?;
417
418        let definition: GraphDefinition = serde_json::from_str(&json)
419            .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
420
421        Ok(definition)
422    }
423
424    async fn delete(&self, id: &str) -> Result<(), PersistenceError> {
425        let path = self.graph_path(id)?;
426
427        if path.exists() {
428            tokio::fs::remove_file(&path)
429                .await
430                .map_err(|e| PersistenceError::IoError(e.to_string()))?;
431        }
432
433        Ok(())
434    }
435
436    async fn exists(&self, id: &str) -> Result<bool, PersistenceError> {
437        let path = self.graph_path(id)?;
438        Ok(path.exists())
439    }
440
441    async fn list(&self) -> Result<Vec<String>, PersistenceError> {
442        let mut ids = Vec::new();
443
444        let mut entries = tokio::fs::read_dir(&self.directory)
445            .await
446            .map_err(|e| PersistenceError::IoError(e.to_string()))?;
447
448        while let Some(entry) = entries
449            .next_entry()
450            .await
451            .map_err(|e| PersistenceError::IoError(e.to_string()))?
452        {
453            let path = entry.path();
454            if path.extension().is_some_and(|ext| ext == "json") {
455                if let Some(id) = path.file_stem().and_then(|s| s.to_str()) {
456                    ids.push(id.to_string());
457                }
458            }
459        }
460
461        Ok(ids)
462    }
463}
464
465// ============================================================================
466// MongoDB 持久化实现
467// ============================================================================
468
469#[cfg(feature = "mongodb-persistence")]
470mod mongo_impl {
471    use super::*;
472    use mongodb::{
473        bson::{doc, from_document, to_document, Document},
474        options::{ClientOptions, FindOptions},
475        Client, Collection,
476    };
477
478    /// MongoDB配置
479    pub struct MongoConfig {
480        /// MongoDB连接URI (例如: mongodb://localhost:27017 或 mongodb+srv://user:pass@cluster.mongodb.net)
481        pub uri: String,
482
483        /// 数据库名称
484        pub database: String,
485
486        /// 集合名称
487        pub collection: String,
488    }
489
490    impl MongoConfig {
491        /// 创建新的MongoDB配置
492        pub fn new(uri: String, database: String, collection: String) -> Self {
493            Self {
494                uri,
495                database,
496                collection,
497            }
498        }
499
500        /// 从环境变量创建配置
501        ///
502        /// 环境变量:
503        /// - MONGO_URI: MongoDB连接URI
504        /// - MONGO_DATABASE: 数据库名称 (默认: langgraph)
505        /// - MONGO_COLLECTION: 集合名称 (默认: graph_definitions)
506        pub fn from_env() -> Result<Self, PersistenceError> {
507            Ok(Self {
508                uri: std::env::var("MONGO_URI").map_err(|_| {
509                    PersistenceError::ConnectionError(
510                        "MONGO_URI environment variable not set".to_string(),
511                    )
512                })?,
513                database: std::env::var("MONGO_DATABASE")
514                    .unwrap_or_else(|_| "langgraph".to_string()),
515                collection: std::env::var("MONGO_COLLECTION")
516                    .unwrap_or_else(|_| "graph_definitions".to_string()),
517            })
518        }
519    }
520
521    /// MongoPersistence - MongoDB图存储实现
522    pub struct MongoPersistence {
523        client: Client,
524        collection: Collection<Document>,
525        database_name: String,
526        collection_name: String,
527    }
528
529    impl MongoPersistence {
530        /// 创建新的MongoDB持久化实例
531        ///
532        /// # 参数
533        /// - config: MongoDB配置
534        ///
535        /// # 示例
536        /// ```ignore
537        /// let config = MongoConfig::new(
538        ///     "mongodb://localhost:27017",
539        ///     "langgraph",
540        ///     "graph_definitions"
541        /// );
542        /// let persistence = MongoPersistence::new(config).await?;
543        /// ```
544        pub async fn new(config: MongoConfig) -> Result<Self, PersistenceError> {
545            let client_options = ClientOptions::parse(&config.uri)
546                .await
547                .map_err(|e| PersistenceError::ConnectionError(e.to_string()))?;
548
549            let client = Client::with_options(client_options)
550                .map_err(|e| PersistenceError::ConnectionError(e.to_string()))?;
551
552            let database = client.database(&config.database);
553            let collection = database.collection(&config.collection);
554
555            Ok(Self {
556                client,
557                collection,
558                database_name: config.database,
559                collection_name: config.collection,
560            })
561        }
562
563        /// 从环境变量创建实例
564        pub async fn from_env() -> Result<Self, PersistenceError> {
565            let config = MongoConfig::from_env()?;
566            Self::new(config).await
567        }
568
569        /// 获取MongoDB客户端
570        pub fn client(&self) -> &Client {
571            &self.client
572        }
573
574        /// 获取集合名称
575        pub fn collection_name(&self) -> &str {
576            &self.collection_name
577        }
578
579        /// 获取数据库名称
580        pub fn database_name(&self) -> &str {
581            &self.database_name
582        }
583    }
584
585    #[async_trait]
586    impl GraphPersistence for MongoPersistence {
587        async fn save(
588            &self,
589            id: &str,
590            definition: &GraphDefinition,
591        ) -> Result<(), PersistenceError> {
592            let doc = to_document(definition)
593                .map_err(|e| PersistenceError::SerializationError(e.to_string()))?;
594
595            // 使用 upsert 操作:如果存在则更新,不存在则插入
596            self.collection
597                .update_one(
598                    doc! { "_id": id },
599                    doc! { "$set": doc },
600                    mongodb::options::UpdateOptions::builder()
601                        .upsert(true)
602                        .build(),
603                )
604                .await
605                .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
606
607            Ok(())
608        }
609
610        async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError> {
611            let filter = doc! { "_id": id };
612
613            let result = self
614                .collection
615                .find_one(filter, None)
616                .await
617                .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
618
619            match result {
620                Some(doc) => {
621                    let definition: GraphDefinition = from_document(doc)
622                        .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
623                    Ok(definition)
624                }
625                None => Err(PersistenceError::NotFound(id.to_string())),
626            }
627        }
628
629        async fn delete(&self, id: &str) -> Result<(), PersistenceError> {
630            let filter = doc! { "_id": id };
631
632            let result = self
633                .collection
634                .delete_one(filter, None)
635                .await
636                .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
637
638            if result.deleted_count == 0 {
639                Err(PersistenceError::NotFound(id.to_string()))
640            } else {
641                Ok(())
642            }
643        }
644
645        async fn exists(&self, id: &str) -> Result<bool, PersistenceError> {
646            let filter = doc! { "_id": id };
647
648            let count = self
649                .collection
650                .count_documents(filter, None)
651                .await
652                .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
653
654            Ok(count > 0)
655        }
656
657        async fn list(&self) -> Result<Vec<String>, PersistenceError> {
658            let filter = doc! {};
659            let options = FindOptions::builder().projection(doc! { "_id": 1 }).build();
660
661            let mut cursor = self
662                .collection
663                .find(filter, options)
664                .await
665                .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
666
667            let mut ids = Vec::new();
668            while cursor
669                .advance()
670                .await
671                .map_err(|e| PersistenceError::MongoError(e.to_string()))?
672            {
673                let doc = cursor
674                    .deserialize_current()
675                    .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
676
677                if let Ok(id) = doc.get_str("_id") {
678                    ids.push(id.to_string());
679                }
680            }
681
682            Ok(ids)
683        }
684    }
685}
686
687#[cfg(feature = "mongodb-persistence")]
688pub use mongo_impl::{MongoConfig, MongoPersistence};
689
690#[cfg(test)]
691mod tests {
692    use super::*;
693
694    #[test]
695    fn test_node_type_serialization() {
696        let types = vec![
697            NodeType::Sync,
698            NodeType::Async,
699            NodeType::Subgraph,
700            NodeType::Custom,
701        ];
702
703        for t in types {
704            let json = serde_json::to_string(&t).unwrap();
705            let parsed: NodeType = serde_json::from_str(&json).unwrap();
706            assert_eq!(parsed, t);
707        }
708    }
709
710    #[test]
711    fn test_edge_type_serialization() {
712        let types = vec![
713            EdgeType::Fixed,
714            EdgeType::Conditional,
715            EdgeType::FanOut,
716            EdgeType::FanIn,
717        ];
718
719        for t in types {
720            let json = serde_json::to_string(&t).unwrap();
721            let parsed: EdgeType = serde_json::from_str(&json).unwrap();
722            assert_eq!(parsed, t);
723        }
724    }
725
726    #[test]
727    fn test_graph_definition_builder() {
728        let def = GraphDefinition::new("entry".to_string())
729            .with_id("test-id".to_string())
730            .with_name("Test Graph".to_string())
731            .with_recursion_limit(50);
732
733        assert_eq!(def.id, "test-id");
734        assert_eq!(def.name, Some("Test Graph".to_string()));
735        assert_eq!(def.entry_point, "entry");
736        assert_eq!(def.recursion_limit, 50);
737    }
738
739    #[tokio::test]
740    async fn test_memory_persistence() {
741        let persistence = MemoryPersistence::new();
742        let def = GraphDefinition::new("entry".to_string()).with_id("test-001".to_string());
743
744        persistence.save("test-001", &def).await.unwrap();
745        assert!(persistence.exists("test-001").await.unwrap());
746
747        let loaded = persistence.load("test-001").await.unwrap();
748        assert_eq!(loaded.id, "test-001");
749
750        persistence.delete("test-001").await.unwrap();
751        assert!(!persistence.exists("test-001").await.unwrap());
752    }
753}