busbar_sf_agentscript/graph/render/
ascii.rs1use super::super::{RefGraph, RefNode};
6use petgraph::visit::EdgeRef;
7use std::collections::{HashMap, HashSet};
8
9pub fn render_topic_flow(graph: &RefGraph) -> String {
13 let inner = graph.inner();
14
15 let mut topics: Vec<String> = vec!["start_agent".to_string()];
17 let mut topic_idx: HashMap<String, usize> = HashMap::new();
18 topic_idx.insert("start_agent".to_string(), 0);
19
20 for name in graph.topic_names() {
21 topic_idx.insert(name.to_string(), topics.len());
22 topics.push(name.to_string());
23 }
24
25 let mut edges: HashMap<usize, Vec<(usize, String)>> = HashMap::new();
27
28 for edge in inner.edge_references() {
29 let edge_type = edge.weight().label();
30 if edge_type == "transitions_to" || edge_type == "delegates" || edge_type == "routes" {
31 let source_node = graph.get_node(edge.source());
32 let target_node = graph.get_node(edge.target());
33
34 if let (Some(src), Some(tgt)) = (source_node, target_node) {
35 let src_name = get_topic_name(src);
36 let tgt_name = get_topic_name(tgt);
37
38 if let (Some(&src_id), Some(&tgt_id)) =
39 (topic_idx.get(&src_name), topic_idx.get(&tgt_name))
40 {
41 edges
42 .entry(src_id)
43 .or_default()
44 .push((tgt_id, edge_type.to_string()));
45 }
46 }
47 }
48 }
49
50 render_ascii_tree(&topics, &edges)
51}
52
53pub fn render_actions_view(graph: &RefGraph) -> String {
57 let inner = graph.inner();
58 let mut labels: Vec<String> = Vec::new();
59 let mut node_map: HashMap<usize, usize> = HashMap::new();
60
61 for idx in inner.node_indices() {
62 if let Some(node) = graph.get_node(idx) {
63 let label = match node {
64 RefNode::StartAgent { .. } => Some("start_agent".to_string()),
65 RefNode::Topic { name, .. } => Some(format!("[{}]", name)),
66 RefNode::ActionDef { name, .. } => Some(name.clone()),
67 RefNode::ReasoningAction { name, target, .. } => {
68 if let Some(t) = target {
69 Some(format!("{}→{}", name, t.split("://").last().unwrap_or(t)))
70 } else {
71 Some(name.clone())
72 }
73 }
74 _ => None,
75 };
76
77 if let Some(lbl) = label {
78 node_map.insert(idx.index(), labels.len());
79 labels.push(lbl);
80 }
81 }
82 }
83
84 let mut edges: HashMap<usize, Vec<(usize, String)>> = HashMap::new();
86
87 for edge in inner.edge_references() {
88 let edge_type = edge.weight().label();
89 if edge_type == "invokes" || edge_type == "transitions_to" || edge_type == "delegates" {
90 if let (Some(&src_id), Some(&tgt_id)) =
91 (node_map.get(&edge.source().index()), node_map.get(&edge.target().index()))
92 {
93 edges
94 .entry(src_id)
95 .or_default()
96 .push((tgt_id, edge_type.to_string()));
97 }
98 }
99 }
100
101 render_ascii_tree(&labels, &edges)
102}
103
104pub fn render_full_view(graph: &RefGraph) -> String {
108 let inner = graph.inner();
109 let mut output = String::new();
110
111 struct TopicInfo {
112 actions: Vec<String>,
113 reasoning: Vec<String>,
114 transitions: Vec<String>,
115 delegates: Vec<String>,
116 }
117
118 let mut topics: HashMap<String, TopicInfo> = HashMap::new();
119 let mut start_routes: Vec<String> = Vec::new();
120 let mut variables: Vec<(String, bool)> = Vec::new();
121
122 for idx in inner.node_indices() {
124 if let Some(node) = graph.get_node(idx) {
125 match node {
126 RefNode::Variable { name, mutable, .. } => {
127 variables.push((name.clone(), *mutable));
128 }
129 RefNode::Topic { name, .. } => {
130 topics.entry(name.clone()).or_insert(TopicInfo {
131 actions: Vec::new(),
132 reasoning: Vec::new(),
133 transitions: Vec::new(),
134 delegates: Vec::new(),
135 });
136 }
137 RefNode::ActionDef { name, topic, .. } => {
138 if let Some(t) = topics.get_mut(topic) {
139 t.actions.push(name.clone());
140 }
141 }
142 RefNode::ReasoningAction {
143 name,
144 topic,
145 target,
146 ..
147 } => {
148 if let Some(t) = topics.get_mut(topic) {
149 let desc = if let Some(tgt) = target {
150 format!("{} → {}", name, tgt.split("://").last().unwrap_or(tgt))
151 } else {
152 name.clone()
153 };
154 t.reasoning.push(desc);
155 }
156 }
157 _ => {}
158 }
159 }
160 }
161
162 for edge in inner.edge_references() {
164 let edge_type = edge.weight().label();
165 let source_node = graph.get_node(edge.source());
166 let target_node = graph.get_node(edge.target());
167
168 if let (Some(src), Some(tgt)) = (source_node, target_node) {
169 match (src, tgt, edge_type) {
170 (RefNode::StartAgent { .. }, RefNode::Topic { name, .. }, "routes") => {
171 start_routes.push(name.clone());
172 }
173 (
174 RefNode::Topic { name: src_name, .. },
175 RefNode::Topic { name: tgt_name, .. },
176 "transitions_to",
177 ) => {
178 if let Some(t) = topics.get_mut(src_name) {
179 if !t.transitions.contains(tgt_name) {
180 t.transitions.push(tgt_name.clone());
181 }
182 }
183 }
184 (
185 RefNode::Topic { name: src_name, .. },
186 RefNode::Topic { name: tgt_name, .. },
187 "delegates",
188 ) => {
189 if let Some(t) = topics.get_mut(src_name) {
190 if !t.delegates.contains(tgt_name) {
191 t.delegates.push(tgt_name.clone());
192 }
193 }
194 }
195 _ => {}
196 }
197 }
198 }
199
200 output.push_str("┌─────────────────────────────────────────────────────────────┐\n");
202 output.push_str("│ AGENT EXECUTION FLOW │\n");
203 output.push_str("└─────────────────────────────────────────────────────────────┘\n\n");
204
205 if !variables.is_empty() {
207 output.push_str("VARIABLES:\n");
208 let mutable: Vec<_> = variables
209 .iter()
210 .filter(|(_, m)| *m)
211 .map(|(n, _)| n.as_str())
212 .collect();
213 let linked: Vec<_> = variables
214 .iter()
215 .filter(|(_, m)| !*m)
216 .map(|(n, _)| n.as_str())
217 .collect();
218 if !mutable.is_empty() {
219 output.push_str(&format!(" Mutable: {}\n", mutable.join(", ")));
220 }
221 if !linked.is_empty() {
222 output.push_str(&format!(" Linked: {}\n", linked.join(", ")));
223 }
224 output.push('\n');
225 }
226
227 output.push_str("ENTRY POINT:\n");
229 output.push_str(" start_agent\n");
230 if !start_routes.is_empty() {
231 output.push_str(&format!(" routes to: {}\n", start_routes.join(", ")));
232 }
233 output.push('\n');
234
235 output.push_str("TOPICS:\n");
237 for (name, info) in &topics {
238 output.push_str(&format!("\n ┌─ {} ─────────────────────────────\n", name));
239
240 if !info.actions.is_empty() {
241 output.push_str(" │ Actions:\n");
242 for action in &info.actions {
243 output.push_str(&format!(" │ • {}\n", action));
244 }
245 }
246
247 if !info.reasoning.is_empty() {
248 output.push_str(" │ Reasoning:\n");
249 for r in &info.reasoning {
250 output.push_str(&format!(" │ ◆ {}\n", r));
251 }
252 }
253
254 if !info.transitions.is_empty() {
255 output.push_str(&format!(" │ Transitions → {}\n", info.transitions.join(", ")));
256 }
257
258 if !info.delegates.is_empty() {
259 output.push_str(&format!(" │ Delegates ⇒ {}\n", info.delegates.join(", ")));
260 }
261
262 output.push_str(" └────────────────────────────────────────\n");
263 }
264
265 output
266}
267
268pub fn render_ascii_tree(
270 labels: &[String],
271 edges: &HashMap<usize, Vec<(usize, String)>>,
272) -> String {
273 let mut output = String::new();
274 let mut visited: HashSet<usize> = HashSet::new();
275
276 let mut has_incoming: HashSet<usize> = HashSet::new();
278 for targets in edges.values() {
279 for (target, _) in targets {
280 has_incoming.insert(*target);
281 }
282 }
283
284 let roots: Vec<usize> = (0..labels.len())
285 .filter(|i| !has_incoming.contains(i))
286 .collect();
287
288 let start_nodes = if roots.is_empty() { vec![0] } else { roots };
290
291 for (i, &root) in start_nodes.iter().enumerate() {
292 if i > 0 {
293 output.push('\n');
294 }
295 render_node(&mut output, labels, edges, root, "", true, &mut visited);
296 }
297
298 output
299}
300
301fn render_node(
302 output: &mut String,
303 labels: &[String],
304 edges: &HashMap<usize, Vec<(usize, String)>>,
305 node: usize,
306 prefix: &str,
307 is_last: bool,
308 visited: &mut HashSet<usize>,
309) {
310 let connector = if prefix.is_empty() {
311 ""
312 } else if is_last {
313 "└── "
314 } else {
315 "├── "
316 };
317
318 let label = labels.get(node).map(|s| s.as_str()).unwrap_or("?");
319
320 if visited.contains(&node) {
322 output.push_str(prefix);
323 output.push_str(connector);
324 output.push_str(label);
325 output.push_str(" ↩\n");
326 return;
327 }
328
329 output.push_str(prefix);
330 output.push_str(connector);
331 output.push_str(label);
332 output.push('\n');
333
334 visited.insert(node);
335
336 if let Some(children) = edges.get(&node) {
337 let new_prefix = if prefix.is_empty() {
338 "".to_string()
339 } else if is_last {
340 format!("{} ", prefix)
341 } else {
342 format!("{}│ ", prefix)
343 };
344
345 for (i, (child, edge_type)) in children.iter().enumerate() {
346 let child_is_last = i == children.len() - 1;
347
348 let edge_indicator = match edge_type.as_str() {
350 "transitions_to" => "→ ",
351 "delegates" => "⇒ ",
352 "routes" => "⊳ ",
353 "invokes" => "◆ ",
354 "reads" => "◇ ",
355 "writes" => "◈ ",
356 _ => "",
357 };
358
359 if !edge_indicator.is_empty() {
360 output.push_str(&new_prefix);
361 output.push_str(if child_is_last { "└" } else { "├" });
362 output.push_str(edge_indicator);
363
364 let child_label = labels.get(*child).map(|s| s.as_str()).unwrap_or("?");
365 if visited.contains(child) {
366 output.push_str(child_label);
367 output.push_str(" ↩\n");
368 } else {
369 output.push_str(child_label);
370 output.push('\n');
371
372 let deeper_prefix = if child_is_last {
374 format!("{} ", new_prefix)
375 } else {
376 format!("{}│ ", new_prefix)
377 };
378
379 visited.insert(*child);
380 if let Some(grandchildren) = edges.get(child) {
381 for (j, (grandchild, _gc_edge)) in grandchildren.iter().enumerate() {
382 let gc_is_last = j == grandchildren.len() - 1;
383 render_node(
384 output,
385 labels,
386 edges,
387 *grandchild,
388 &deeper_prefix,
389 gc_is_last,
390 visited,
391 );
392 }
393 }
394 }
395 } else {
396 render_node(output, labels, edges, *child, &new_prefix, child_is_last, visited);
397 }
398 }
399 }
400}
401
402fn get_topic_name(node: &RefNode) -> String {
403 match node {
404 RefNode::StartAgent { .. } => "start_agent".to_string(),
405 RefNode::Topic { name, .. } => name.clone(),
406 _ => "unknown".to_string(),
407 }
408}