1use super::error::ValidationError;
7use super::{RefGraph, RefNode, ValidationResult};
8use petgraph::visit::EdgeRef;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct GraphRepr {
18 pub nodes: Vec<NodeRepr>,
19 pub edges: Vec<EdgeRepr>,
20 pub topics: Vec<String>,
21 pub variables: Vec<String>,
22}
23
24impl From<&RefGraph> for GraphRepr {
25 fn from(graph: &RefGraph) -> Self {
26 let inner = graph.inner();
27
28 let nodes: Vec<NodeRepr> = inner
29 .node_indices()
30 .filter_map(|idx| graph.get_node(idx).map(NodeRepr::from))
31 .collect();
32
33 let edges: Vec<EdgeRepr> = inner
34 .edge_references()
35 .map(|e| EdgeRepr {
36 source: e.source().index(),
37 target: e.target().index(),
38 edge_type: e.weight().label().to_string(),
39 })
40 .collect();
41
42 let topics: Vec<String> = graph.topic_names().map(|s| s.to_string()).collect();
43 let variables: Vec<String> = graph.variable_names().map(|s| s.to_string()).collect();
44
45 Self {
46 nodes,
47 edges,
48 topics,
49 variables,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct NodeRepr {
57 pub node_type: String,
58 pub name: Option<String>,
59 pub topic: Option<String>,
60 pub target: Option<String>,
61 pub mutable: Option<bool>,
62 pub span_start: usize,
63 pub span_end: usize,
64}
65
66impl From<&RefNode> for NodeRepr {
67 fn from(node: &RefNode) -> Self {
68 match node {
69 RefNode::StartAgent { span } => NodeRepr {
70 node_type: "start_agent".to_string(),
71 name: None,
72 topic: None,
73 target: None,
74 mutable: None,
75 span_start: span.0,
76 span_end: span.1,
77 },
78 RefNode::Topic { name, span } => NodeRepr {
79 node_type: "topic".to_string(),
80 name: Some(name.clone()),
81 topic: None,
82 target: None,
83 mutable: None,
84 span_start: span.0,
85 span_end: span.1,
86 },
87 RefNode::ActionDef { name, topic, span } => NodeRepr {
88 node_type: "action_def".to_string(),
89 name: Some(name.clone()),
90 topic: Some(topic.clone()),
91 target: None,
92 mutable: None,
93 span_start: span.0,
94 span_end: span.1,
95 },
96 RefNode::ReasoningAction {
97 name,
98 topic,
99 target,
100 span,
101 } => NodeRepr {
102 node_type: "reasoning_action".to_string(),
103 name: Some(name.clone()),
104 topic: Some(topic.clone()),
105 target: target.clone(),
106 mutable: None,
107 span_start: span.0,
108 span_end: span.1,
109 },
110 RefNode::Variable {
111 name,
112 mutable,
113 span,
114 } => NodeRepr {
115 node_type: "variable".to_string(),
116 name: Some(name.clone()),
117 topic: None,
118 target: None,
119 mutable: Some(*mutable),
120 span_start: span.0,
121 span_end: span.1,
122 },
123 RefNode::Connection { name, span } => NodeRepr {
124 node_type: "connection".to_string(),
125 name: Some(name.clone()),
126 topic: None,
127 target: None,
128 mutable: None,
129 span_start: span.0,
130 span_end: span.1,
131 },
132 }
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct EdgeRepr {
139 pub source: usize,
140 pub target: usize,
141 pub edge_type: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ValidationResultRepr {
151 pub errors: Vec<ValidationErrorRepr>,
152 pub warnings: Vec<ValidationErrorRepr>,
153 pub is_valid: bool,
154}
155
156impl From<&ValidationResult> for ValidationResultRepr {
157 fn from(result: &ValidationResult) -> Self {
158 Self {
159 errors: result
160 .errors
161 .iter()
162 .map(ValidationErrorRepr::from)
163 .collect(),
164 warnings: result
165 .warnings
166 .iter()
167 .map(ValidationErrorRepr::from)
168 .collect(),
169 is_valid: result.is_ok(),
170 }
171 }
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct ValidationErrorRepr {
177 pub error_type: String,
178 pub message: String,
179 pub span_start: Option<usize>,
180 pub span_end: Option<usize>,
181}
182
183impl From<&ValidationError> for ValidationErrorRepr {
184 fn from(error: &ValidationError) -> Self {
185 let span = error.span();
186 Self {
187 error_type: match error {
188 ValidationError::UnresolvedReference { .. } => "unresolved_reference",
189 ValidationError::CycleDetected { .. } => "cycle_detected",
190 ValidationError::UnreachableTopic { .. } => "unreachable_topic",
191 ValidationError::UnusedActionDef { .. } => "unused_action_def",
192 ValidationError::UnusedVariable { .. } => "unused_variable",
193 ValidationError::UninitializedVariable { .. } => "uninitialized_variable",
194 }
195 .to_string(),
196 message: error.message(),
197 span_start: span.map(|s| s.0),
198 span_end: span.map(|s| s.1),
199 }
200 }
201}
202
203#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct VariableUsagesRepr {
210 pub readers: Vec<UsageInfoRepr>,
211 pub writers: Vec<UsageInfoRepr>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
216pub struct UsageInfoRepr {
217 pub location: String,
218 pub node_type: String,
219 pub topic: Option<String>,
220 pub context: Option<String>,
221}
222
223impl UsageInfoRepr {
224 pub fn from_node(node: &RefNode) -> Self {
225 match node {
226 RefNode::ActionDef { name, topic, .. } => UsageInfoRepr {
227 location: name.clone(),
228 node_type: "action_def".to_string(),
229 topic: Some(topic.clone()),
230 context: None,
231 },
232 RefNode::ReasoningAction {
233 name,
234 topic,
235 target,
236 ..
237 } => UsageInfoRepr {
238 location: name.clone(),
239 node_type: "reasoning_action".to_string(),
240 topic: Some(topic.clone()),
241 context: target.clone(),
242 },
243 RefNode::Topic { name, .. } => UsageInfoRepr {
244 location: name.clone(),
245 node_type: "topic".to_string(),
246 topic: Some(name.clone()),
247 context: None,
248 },
249 RefNode::StartAgent { .. } => UsageInfoRepr {
250 location: "start_agent".to_string(),
251 node_type: "start_agent".to_string(),
252 topic: None,
253 context: None,
254 },
255 RefNode::Variable { name, .. } => UsageInfoRepr {
256 location: name.clone(),
257 node_type: "variable".to_string(),
258 topic: None,
259 context: None,
260 },
261 RefNode::Connection { name, .. } => UsageInfoRepr {
262 location: name.clone(),
263 node_type: "connection".to_string(),
264 topic: None,
265 context: None,
266 },
267 }
268 }
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
277pub struct GraphExport {
278 pub version: String,
279 pub nodes: Vec<GraphExportNode>,
280 pub edges: Vec<GraphExportEdge>,
281 pub topics: Vec<TopicExportInfo>,
282 pub variables: Vec<String>,
283 pub stats: StatsExport,
284 pub validation: ValidationExport,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct GraphExportNode {
290 pub id: usize,
291 pub node_type: String,
292 pub name: Option<String>,
293 pub topic: Option<String>,
294 pub target: Option<String>,
295 pub mutable: Option<bool>,
296 pub span: SpanRepr,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct GraphExportEdge {
302 pub source: usize,
303 pub target: usize,
304 pub edge_type: String,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct SpanRepr {
310 pub start: usize,
311 pub end: usize,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
316pub struct TopicExportInfo {
317 pub name: String,
318 pub description: Option<String>,
319 pub is_entry: bool,
320 pub transitions_to: Vec<String>,
321 pub delegates_to: Vec<String>,
322 pub actions: Vec<ActionExportInfo>,
323}
324
325#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ActionExportInfo {
328 pub name: String,
329 pub target: Option<String>,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct StatsExport {
335 pub total_nodes: usize,
336 pub total_edges: usize,
337 pub topics: usize,
338 pub variables: usize,
339 pub action_defs: usize,
340 pub reasoning_actions: usize,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ValidationExport {
346 pub is_valid: bool,
347 pub errors: Vec<ValidationErrorRepr>,
348 pub warnings: Vec<ValidationErrorRepr>,
349}
350
351impl GraphExport {
356 pub fn from_graph(graph: &RefGraph) -> Self {
358 let inner = graph.inner();
359
360 let nodes: Vec<GraphExportNode> = inner
362 .node_indices()
363 .filter_map(|idx| {
364 graph.get_node(idx).map(|node| {
365 let repr = NodeRepr::from(node);
366 GraphExportNode {
367 id: idx.index(),
368 node_type: repr.node_type,
369 name: repr.name,
370 topic: repr.topic,
371 target: repr.target,
372 mutable: repr.mutable,
373 span: SpanRepr {
374 start: repr.span_start,
375 end: repr.span_end,
376 },
377 }
378 })
379 })
380 .collect();
381
382 let edges: Vec<GraphExportEdge> = inner
384 .edge_references()
385 .map(|e| GraphExportEdge {
386 source: e.source().index(),
387 target: e.target().index(),
388 edge_type: e.weight().label().to_string(),
389 })
390 .collect();
391
392 let mut topic_info: Vec<TopicExportInfo> = Vec::new();
394
395 topic_info.push(TopicExportInfo {
397 name: "start_agent".to_string(),
398 description: None,
399 is_entry: true,
400 transitions_to: Vec::new(),
401 delegates_to: Vec::new(),
402 actions: Vec::new(),
403 });
404
405 for topic_name in graph.topic_names() {
407 let mut transitions = Vec::new();
408 let mut delegates = Vec::new();
409 let mut actions = Vec::new();
410
411 for edge in inner.edge_references() {
413 let edge_type = edge.weight().label();
414 if let (Some(src), Some(tgt)) =
415 (graph.get_node(edge.source()), graph.get_node(edge.target()))
416 {
417 match (src, edge_type) {
418 (RefNode::Topic { name: src_name, .. }, "transitions_to")
419 if src_name == topic_name =>
420 {
421 if let RefNode::Topic { name: tgt_name, .. } = tgt {
422 transitions.push(tgt_name.clone());
423 }
424 }
425 (RefNode::Topic { name: src_name, .. }, "delegates")
426 if src_name == topic_name =>
427 {
428 if let RefNode::Topic { name: tgt_name, .. } = tgt {
429 delegates.push(tgt_name.clone());
430 }
431 }
432 _ => {}
433 }
434 }
435 }
436
437 for idx in inner.node_indices() {
439 if let Some(RefNode::ReasoningAction {
440 name,
441 topic,
442 target,
443 ..
444 }) = graph.get_node(idx)
445 {
446 if topic == topic_name {
447 actions.push(ActionExportInfo {
448 name: name.clone(),
449 target: target.clone(),
450 });
451 }
452 }
453 }
454
455 topic_info.push(TopicExportInfo {
456 name: topic_name.to_string(),
457 description: None,
458 is_entry: false,
459 transitions_to: transitions,
460 delegates_to: delegates,
461 actions,
462 });
463 }
464
465 for edge in inner.edge_references() {
467 if edge.weight().label() == "routes" {
468 if let Some(RefNode::StartAgent { .. }) = graph.get_node(edge.source()) {
469 if let Some(RefNode::Topic { name, .. }) = graph.get_node(edge.target()) {
470 if let Some(start) = topic_info.get_mut(0) {
471 start.transitions_to.push(name.clone());
472 }
473 }
474 }
475 }
476 }
477
478 let stats = graph.stats();
480 let validation = graph.validate();
481
482 GraphExport {
483 version: env!("CARGO_PKG_VERSION").to_string(),
484 nodes,
485 edges,
486 topics: topic_info,
487 variables: graph.variable_names().map(|s| s.to_string()).collect(),
488 stats: StatsExport {
489 total_nodes: stats.total_definitions(),
490 total_edges: stats.total_edges(),
491 topics: stats.topics,
492 variables: stats.variables,
493 action_defs: stats.action_defs,
494 reasoning_actions: stats.reasoning_actions,
495 },
496 validation: ValidationExport {
497 is_valid: validation.is_ok(),
498 errors: validation
499 .errors
500 .iter()
501 .map(ValidationErrorRepr::from)
502 .collect(),
503 warnings: validation
504 .warnings
505 .iter()
506 .map(ValidationErrorRepr::from)
507 .collect(),
508 },
509 }
510 }
511}