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
386impl Default for FilePersistence {
387 fn default() -> Self {
388 Self::new(".graph_definitions")
389 .expect("Failed to create default graph definitions directory")
390 }
391}
392
393#[async_trait]
394impl GraphPersistence for FilePersistence {
395 async fn save(&self, id: &str, definition: &GraphDefinition) -> Result<(), PersistenceError> {
396 let path = self.graph_path(id)?;
397
398 let json = serde_json::to_string_pretty(definition)
399 .map_err(|e| PersistenceError::SerializationError(e.to_string()))?;
400
401 tokio::fs::write(&path, json)
402 .await
403 .map_err(|e| PersistenceError::IoError(e.to_string()))?;
404
405 Ok(())
406 }
407
408 async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError> {
409 let path = self.graph_path(id)?;
410
411 if !path.exists() {
412 return Err(PersistenceError::NotFound(id.to_string()));
413 }
414
415 let json = tokio::fs::read_to_string(&path)
416 .await
417 .map_err(|e| PersistenceError::IoError(e.to_string()))?;
418
419 let definition: GraphDefinition = serde_json::from_str(&json)
420 .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
421
422 Ok(definition)
423 }
424
425 async fn delete(&self, id: &str) -> Result<(), PersistenceError> {
426 let path = self.graph_path(id)?;
427
428 if path.exists() {
429 tokio::fs::remove_file(&path)
430 .await
431 .map_err(|e| PersistenceError::IoError(e.to_string()))?;
432 }
433
434 Ok(())
435 }
436
437 async fn exists(&self, id: &str) -> Result<bool, PersistenceError> {
438 let path = self.graph_path(id)?;
439 Ok(path.exists())
440 }
441
442 async fn list(&self) -> Result<Vec<String>, PersistenceError> {
443 let mut ids = Vec::new();
444
445 let mut entries = tokio::fs::read_dir(&self.directory)
446 .await
447 .map_err(|e| PersistenceError::IoError(e.to_string()))?;
448
449 while let Some(entry) = entries
450 .next_entry()
451 .await
452 .map_err(|e| PersistenceError::IoError(e.to_string()))?
453 {
454 let path = entry.path();
455 if path.extension().is_some_and(|ext| ext == "json") {
456 if let Some(id) = path.file_stem().and_then(|s| s.to_str()) {
457 ids.push(id.to_string());
458 }
459 }
460 }
461
462 Ok(ids)
463 }
464}
465
466#[cfg(feature = "mongodb-persistence")]
471mod mongo_impl {
472 use super::*;
473 use mongodb::{
474 bson::{doc, from_document, to_document, Document},
475 options::{ClientOptions, FindOptions},
476 Client, Collection,
477 };
478
479 pub struct MongoConfig {
481 pub uri: String,
483
484 pub database: String,
486
487 pub collection: String,
489 }
490
491 impl MongoConfig {
492 pub fn new(uri: String, database: String, collection: String) -> Self {
494 Self {
495 uri,
496 database,
497 collection,
498 }
499 }
500
501 pub fn from_env() -> Result<Self, PersistenceError> {
508 Ok(Self {
509 uri: std::env::var("MONGO_URI").map_err(|_| {
510 PersistenceError::ConnectionError(
511 "MONGO_URI environment variable not set".to_string(),
512 )
513 })?,
514 database: std::env::var("MONGO_DATABASE")
515 .unwrap_or_else(|_| "langgraph".to_string()),
516 collection: std::env::var("MONGO_COLLECTION")
517 .unwrap_or_else(|_| "graph_definitions".to_string()),
518 })
519 }
520 }
521
522 pub struct MongoPersistence {
524 client: Client,
525 collection: Collection<Document>,
526 database_name: String,
527 collection_name: String,
528 }
529
530 impl MongoPersistence {
531 pub async fn new(config: MongoConfig) -> Result<Self, PersistenceError> {
546 let client_options = ClientOptions::parse(&config.uri)
547 .await
548 .map_err(|e| PersistenceError::ConnectionError(e.to_string()))?;
549
550 let client = Client::with_options(client_options)
551 .map_err(|e| PersistenceError::ConnectionError(e.to_string()))?;
552
553 let database = client.database(&config.database);
554 let collection = database.collection(&config.collection);
555
556 Ok(Self {
557 client,
558 collection,
559 database_name: config.database,
560 collection_name: config.collection,
561 })
562 }
563
564 pub async fn from_env() -> Result<Self, PersistenceError> {
566 let config = MongoConfig::from_env()?;
567 Self::new(config).await
568 }
569
570 pub fn client(&self) -> &Client {
572 &self.client
573 }
574
575 pub fn collection_name(&self) -> &str {
577 &self.collection_name
578 }
579
580 pub fn database_name(&self) -> &str {
582 &self.database_name
583 }
584 }
585
586 #[async_trait]
587 impl GraphPersistence for MongoPersistence {
588 async fn save(
589 &self,
590 id: &str,
591 definition: &GraphDefinition,
592 ) -> Result<(), PersistenceError> {
593 let doc = to_document(definition)
594 .map_err(|e| PersistenceError::SerializationError(e.to_string()))?;
595
596 self.collection
598 .update_one(
599 doc! { "_id": id },
600 doc! { "$set": doc },
601 mongodb::options::UpdateOptions::builder()
602 .upsert(true)
603 .build(),
604 )
605 .await
606 .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
607
608 Ok(())
609 }
610
611 async fn load(&self, id: &str) -> Result<GraphDefinition, PersistenceError> {
612 let filter = doc! { "_id": id };
613
614 let result = self
615 .collection
616 .find_one(filter, None)
617 .await
618 .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
619
620 match result {
621 Some(doc) => {
622 let definition: GraphDefinition = from_document(doc)
623 .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
624 Ok(definition)
625 }
626 None => Err(PersistenceError::NotFound(id.to_string())),
627 }
628 }
629
630 async fn delete(&self, id: &str) -> Result<(), PersistenceError> {
631 let filter = doc! { "_id": id };
632
633 let result = self
634 .collection
635 .delete_one(filter, None)
636 .await
637 .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
638
639 if result.deleted_count == 0 {
640 Err(PersistenceError::NotFound(id.to_string()))
641 } else {
642 Ok(())
643 }
644 }
645
646 async fn exists(&self, id: &str) -> Result<bool, PersistenceError> {
647 let filter = doc! { "_id": id };
648
649 let count = self
650 .collection
651 .count_documents(filter, None)
652 .await
653 .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
654
655 Ok(count > 0)
656 }
657
658 async fn list(&self) -> Result<Vec<String>, PersistenceError> {
659 let filter = doc! {};
660 let options = FindOptions::builder().projection(doc! { "_id": 1 }).build();
661
662 let mut cursor = self
663 .collection
664 .find(filter, options)
665 .await
666 .map_err(|e| PersistenceError::MongoError(e.to_string()))?;
667
668 let mut ids = Vec::new();
669 while cursor
670 .advance()
671 .await
672 .map_err(|e| PersistenceError::MongoError(e.to_string()))?
673 {
674 let doc = cursor
675 .deserialize_current()
676 .map_err(|e| PersistenceError::DeserializationError(e.to_string()))?;
677
678 if let Ok(id) = doc.get_str("_id") {
679 ids.push(id.to_string());
680 }
681 }
682
683 Ok(ids)
684 }
685 }
686}
687
688#[cfg(feature = "mongodb-persistence")]
689pub use mongo_impl::{MongoConfig, MongoPersistence};
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694
695 #[test]
696 fn test_node_type_serialization() {
697 let types = vec![
698 NodeType::Sync,
699 NodeType::Async,
700 NodeType::Subgraph,
701 NodeType::Custom,
702 ];
703
704 for t in types {
705 let json = serde_json::to_string(&t).unwrap();
706 let parsed: NodeType = serde_json::from_str(&json).unwrap();
707 assert_eq!(parsed, t);
708 }
709 }
710
711 #[test]
712 fn test_edge_type_serialization() {
713 let types = vec![
714 EdgeType::Fixed,
715 EdgeType::Conditional,
716 EdgeType::FanOut,
717 EdgeType::FanIn,
718 ];
719
720 for t in types {
721 let json = serde_json::to_string(&t).unwrap();
722 let parsed: EdgeType = serde_json::from_str(&json).unwrap();
723 assert_eq!(parsed, t);
724 }
725 }
726
727 #[test]
728 fn test_graph_definition_builder() {
729 let def = GraphDefinition::new("entry".to_string())
730 .with_id("test-id".to_string())
731 .with_name("Test Graph".to_string())
732 .with_recursion_limit(50);
733
734 assert_eq!(def.id, "test-id");
735 assert_eq!(def.name, Some("Test Graph".to_string()));
736 assert_eq!(def.entry_point, "entry");
737 assert_eq!(def.recursion_limit, 50);
738 }
739
740 #[tokio::test]
741 async fn test_memory_persistence() {
742 let persistence = MemoryPersistence::new();
743 let def = GraphDefinition::new("entry".to_string()).with_id("test-001".to_string());
744
745 persistence.save("test-001", &def).await.unwrap();
746 assert!(persistence.exists("test-001").await.unwrap());
747
748 let loaded = persistence.load("test-001").await.unwrap();
749 assert_eq!(loaded.id, "test-001");
750
751 persistence.delete("test-001").await.unwrap();
752 assert!(!persistence.exists("test-001").await.unwrap());
753 }
754}