1use 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#[async_trait]
17pub trait GraphPersistence: Send + Sync {
18 async fn save(&self, id: &str, definition: &GraphDefinition) -> Result<(), PersistenceError>;
20
21 async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError>;
23 async fn delete(&self, id: &str) -> Result<(), PersistenceError>;
25
26 async fn exists(&self, id: &str) -> Result<bool, PersistenceError>;
28
29 async fn list(&self) -> Result<Vec<String>, PersistenceError>;
31}
32
33#[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#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct GraphDefinition {
61 pub id: String,
63
64 pub name: Option<String>,
66
67 pub entry_point: String,
69
70 pub nodes: Vec<NodeDefinition>,
72
73 pub edges: Vec<EdgeDefinition>,
75
76 pub routers: Vec<RouterDefinition>,
78
79 pub recursion_limit: usize,
81
82 pub created_at: DateTime<Utc>,
84
85 pub updated_at: DateTime<Utc>,
87
88 pub metadata: HashMap<String, serde_json::Value>,
90}
91
92impl GraphDefinition {
93 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 pub fn with_id(mut self, id: String) -> Self {
112 self.id = id;
113 self
114 }
115
116 pub fn with_name(mut self, name: String) -> Self {
118 self.name = Some(name);
119 self
120 }
121
122 pub fn with_recursion_limit(mut self, limit: usize) -> Self {
124 self.recursion_limit = limit;
125 self
126 }
127
128 pub fn add_node(&mut self, node: NodeDefinition) {
130 self.nodes.push(node);
131 self.updated_at = Utc::now();
132 }
133
134 pub fn add_edge(&mut self, edge: EdgeDefinition) {
136 self.edges.push(edge);
137 self.updated_at = Utc::now();
138 }
139
140 pub fn add_router(&mut self, router: RouterDefinition) {
142 self.routers.push(router);
143 self.updated_at = Utc::now();
144 }
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct NodeDefinition {
150 pub name: String,
152
153 pub node_type: NodeType,
155
156 pub config: serde_json::Value,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
162pub enum NodeType {
163 Sync,
164 Async,
165 Subgraph,
166 Custom,
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub struct EdgeDefinition {
172 pub edge_type: EdgeType,
174
175 pub source: String,
177
178 pub target: Option<String>,
180
181 pub targets: Option<Vec<String>>,
183
184 pub router_name: Option<String>,
186
187 pub conditional_targets: Option<HashMap<String, String>>,
189
190 pub default_target: Option<String>,
192
193 pub sources: Option<Vec<String>>,
195}
196
197impl EdgeDefinition {
198 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
262pub enum EdgeType {
263 Fixed,
265
266 Conditional,
268
269 FanOut,
271
272 FanIn,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct RouterDefinition {
279 pub name: String,
281
282 pub router_type: String,
284
285 pub routes: Vec<String>,
287
288 pub config: serde_json::Value,
290}
291
292pub struct MemoryPersistence {
294 graphs: Mutex<HashMap<String, GraphDefinition>>,
295}
296
297impl MemoryPersistence {
298 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
347pub struct FilePersistence {
349 directory: PathBuf,
350}
351
352impl FilePersistence {
353 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 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#[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#[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 pub struct MongoConfig {
480 pub uri: String,
482
483 pub database: String,
485
486 pub collection: String,
488 }
489
490 impl MongoConfig {
491 pub fn new(uri: String, database: String, collection: String) -> Self {
493 Self {
494 uri,
495 database,
496 collection,
497 }
498 }
499
500 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 pub struct MongoPersistence {
523 client: Client,
524 collection: Collection<Document>,
525 database_name: String,
526 collection_name: String,
527 }
528
529 impl MongoPersistence {
530 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 pub async fn from_env() -> Result<Self, PersistenceError> {
565 let config = MongoConfig::from_env()?;
566 Self::new(config).await
567 }
568
569 pub fn client(&self) -> &Client {
571 &self.client
572 }
573
574 pub fn collection_name(&self) -> &str {
576 &self.collection_name
577 }
578
579 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 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}