Skip to main content

lc_langgraph/compiled/
visualize.rs

1// crates/lc-langgraph/src/compiled/visualize.rs
2//! CompiledGraph visualization and serialization methods:
3//! visualize_ascii, visualize_mermaid, visualize_json, to_definition
4
5use super::graph::CompiledGraph;
6use crate::edge::GraphEdge;
7use crate::persistence::{EdgeDefinition, GraphDefinition, NodeDefinition, NodeType};
8use crate::state::StateSchema;
9
10impl<S: StateSchema> CompiledGraph<S> {
11    /// Visualize the graph structure in ASCII format
12    pub fn visualize_ascii(&self) -> String {
13        let mut output = String::new();
14        output.push_str("┌─────────────────────────────────────┐\n");
15        output.push_str("│         LangGraph Structure         │\n");
16        output.push_str("└─────────────────────────────────────┘\n\n");
17
18        output.push_str(&format!("Entry Point: {}\n\n", self.entry_point));
19
20        output.push_str("Nodes:\n");
21        for name in self.nodes.keys() {
22            output.push_str(&format!("  • {}\n", name));
23        }
24
25        output.push_str("\nEdges:\n");
26        for edge in &self.edges {
27            match edge {
28                GraphEdge::Fixed { source, target } => {
29                    output.push_str(&format!("  {} → {}\n", source, target));
30                }
31                GraphEdge::Conditional {
32                    source,
33                    router_name,
34                    targets,
35                    ..
36                } => {
37                    output.push_str(&format!("  {} → [{}]\n", source, router_name));
38                    for (route, target) in targets {
39                        output.push_str(&format!("    {} → {}\n", route, target));
40                    }
41                }
42                GraphEdge::FanOut { source, targets } => {
43                    output.push_str(&format!("  {} → [FanOut]\n", source));
44                    for target in targets {
45                        output.push_str(&format!("    → {}\n", target));
46                    }
47                }
48                GraphEdge::FanIn { sources, target } => {
49                    output.push_str(&format!("  [FanIn] → {}\n", target));
50                    for source in sources {
51                        output.push_str(&format!("    {} →\n", source));
52                    }
53                }
54            }
55        }
56
57        if !self.conditional_routers.is_empty() {
58            output.push_str("\nRouters:\n");
59            for name in self.conditional_routers.keys() {
60                output.push_str(&format!("  • {}\n", name));
61            }
62        }
63
64        output.push_str(&format!("\nRecursion Limit: {}\n", self.recursion_limit));
65
66        output
67    }
68
69    /// Visualize the graph structure in Mermaid format
70    pub fn visualize_mermaid(&self) -> String {
71        let mut output = String::new();
72        output.push_str("```mermaid\n");
73        output.push_str("graph TD\n");
74
75        output.push_str("  START[\"START\"]\n");
76        output.push_str("  END[\"END\"]\n");
77
78        for name in self.nodes.keys() {
79            output.push_str(&format!("  {}[\"{}\"]\n", name, name));
80        }
81
82        for edge in &self.edges {
83            match edge {
84                GraphEdge::Fixed { source, target } => {
85                    output.push_str(&format!("  {} --> {}\n", source, target));
86                }
87                GraphEdge::Conditional {
88                    source,
89                    router_name,
90                    targets,
91                    ..
92                } => {
93                    for (route, target) in targets {
94                        output.push_str(&format!("  {} -->|{}| {}\n", source, route, target));
95                    }
96                    let _ = router_name; // router name not rendered in Mermaid edge syntax
97                }
98                GraphEdge::FanOut { source, targets } => {
99                    output.push_str(&format!("  {} --> {{\n", source));
100                    for target in targets {
101                        output.push_str(&format!("    --> {}\n", target));
102                    }
103                    output.push_str("  }\n");
104                }
105                GraphEdge::FanIn { sources, target } => {
106                    for source in sources {
107                        output.push_str(&format!("  {} --> {}\n", source, target));
108                    }
109                }
110            }
111        }
112
113        output.push_str("```\n");
114        output
115    }
116
117    /// Visualize the graph structure as JSON
118    pub fn visualize_json(&self) -> serde_json::Value {
119        let nodes: Vec<String> = self.nodes.keys().cloned().collect();
120
121        let edges: Vec<serde_json::Value> = self
122            .edges
123            .iter()
124            .map(|edge| match edge {
125                GraphEdge::Fixed { source, target } => {
126                    serde_json::json!({
127                        "type": "fixed",
128                        "source": source,
129                        "target": target
130                    })
131                }
132                GraphEdge::Conditional {
133                    source,
134                    router_name,
135                    targets,
136                    default_target,
137                } => {
138                    serde_json::json!({
139                        "type": "conditional",
140                        "source": source,
141                        "router": router_name,
142                        "targets": targets,
143                        "default": default_target
144                    })
145                }
146                GraphEdge::FanOut { source, targets } => {
147                    serde_json::json!({
148                        "type": "fanout",
149                        "source": source,
150                        "targets": targets
151                    })
152                }
153                GraphEdge::FanIn { sources, target } => {
154                    serde_json::json!({
155                        "type": "fanin",
156                        "sources": sources,
157                        "target": target
158                    })
159                }
160            })
161            .collect();
162
163        let routers: Vec<String> = self.conditional_routers.keys().cloned().collect();
164
165        serde_json::json!({
166            "entry_point": self.entry_point,
167            "nodes": nodes,
168            "edges": edges,
169            "routers": routers,
170            "recursion_limit": self.recursion_limit
171        })
172    }
173
174    pub fn to_definition(&self) -> GraphDefinition {
175        let mut definition = GraphDefinition::new(self.entry_point.clone())
176            .with_recursion_limit(self.recursion_limit);
177
178        for node_name in self.nodes.keys() {
179            definition.add_node(NodeDefinition {
180                name: node_name.clone(),
181                node_type: NodeType::Sync,
182                config: serde_json::json!({}),
183            });
184        }
185
186        for edge in &self.edges {
187            let edge_def = match edge {
188                GraphEdge::Fixed { source, target } => {
189                    EdgeDefinition::fixed(source.clone(), target.clone())
190                }
191                GraphEdge::Conditional {
192                    source,
193                    router_name,
194                    targets,
195                    default_target,
196                } => EdgeDefinition::conditional(
197                    source.clone(),
198                    router_name.clone(),
199                    targets.clone(),
200                    default_target.clone(),
201                ),
202                GraphEdge::FanOut { source, targets } => {
203                    EdgeDefinition::fan_out(source.clone(), targets.clone())
204                }
205                GraphEdge::FanIn { sources, target } => {
206                    EdgeDefinition::fan_in(sources.clone(), target.clone())
207                }
208            };
209            definition.add_edge(edge_def);
210        }
211
212        definition
213    }
214}