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 ValidationError::InvalidPropertyAccess { .. } => "invalid_property_access",
195 }
196 .to_string(),
197 message: error.message(),
198 span_start: span.map(|s| s.0),
199 span_end: span.map(|s| s.1),
200 }
201 }
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct VariableUsagesRepr {
211 pub readers: Vec<UsageInfoRepr>,
212 pub writers: Vec<UsageInfoRepr>,
213}
214
215#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct UsageInfoRepr {
218 pub location: String,
219 pub node_type: String,
220 pub topic: Option<String>,
221 pub context: Option<String>,
222}
223
224impl UsageInfoRepr {
225 pub fn from_node(node: &RefNode) -> Self {
226 match node {
227 RefNode::ActionDef { name, topic, .. } => UsageInfoRepr {
228 location: name.clone(),
229 node_type: "action_def".to_string(),
230 topic: Some(topic.clone()),
231 context: None,
232 },
233 RefNode::ReasoningAction {
234 name,
235 topic,
236 target,
237 ..
238 } => UsageInfoRepr {
239 location: name.clone(),
240 node_type: "reasoning_action".to_string(),
241 topic: Some(topic.clone()),
242 context: target.clone(),
243 },
244 RefNode::Topic { name, .. } => UsageInfoRepr {
245 location: name.clone(),
246 node_type: "topic".to_string(),
247 topic: Some(name.clone()),
248 context: None,
249 },
250 RefNode::StartAgent { .. } => UsageInfoRepr {
251 location: "start_agent".to_string(),
252 node_type: "start_agent".to_string(),
253 topic: None,
254 context: None,
255 },
256 RefNode::Variable { name, .. } => UsageInfoRepr {
257 location: name.clone(),
258 node_type: "variable".to_string(),
259 topic: None,
260 context: None,
261 },
262 RefNode::Connection { name, .. } => UsageInfoRepr {
263 location: name.clone(),
264 node_type: "connection".to_string(),
265 topic: None,
266 context: None,
267 },
268 }
269 }
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct GraphExport {
279 pub version: String,
280 pub nodes: Vec<GraphExportNode>,
281 pub edges: Vec<GraphExportEdge>,
282 pub topics: Vec<TopicExportInfo>,
283 pub variables: Vec<String>,
284 pub stats: StatsExport,
285 pub validation: ValidationExport,
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct GraphExportNode {
291 pub id: usize,
292 pub node_type: String,
293 pub name: Option<String>,
294 pub topic: Option<String>,
295 pub target: Option<String>,
296 pub mutable: Option<bool>,
297 pub span: SpanRepr,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct GraphExportEdge {
303 pub source: usize,
304 pub target: usize,
305 pub edge_type: String,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct SpanRepr {
311 pub start: usize,
312 pub end: usize,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct TopicExportInfo {
318 pub name: String,
319 pub description: Option<String>,
320 pub is_entry: bool,
321 pub transitions_to: Vec<String>,
322 pub delegates_to: Vec<String>,
323 pub actions: Vec<ActionExportInfo>,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ActionExportInfo {
329 pub name: String,
330 pub target: Option<String>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct StatsExport {
336 pub total_nodes: usize,
337 pub total_edges: usize,
338 pub topics: usize,
339 pub variables: usize,
340 pub action_defs: usize,
341 pub reasoning_actions: usize,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct ValidationExport {
347 pub is_valid: bool,
348 pub errors: Vec<ValidationErrorRepr>,
349 pub warnings: Vec<ValidationErrorRepr>,
350}
351
352impl GraphExport {
357 pub fn from_graph(graph: &RefGraph) -> Self {
359 let inner = graph.inner();
360
361 let nodes: Vec<GraphExportNode> = inner
363 .node_indices()
364 .filter_map(|idx| {
365 graph.get_node(idx).map(|node| {
366 let repr = NodeRepr::from(node);
367 GraphExportNode {
368 id: idx.index(),
369 node_type: repr.node_type,
370 name: repr.name,
371 topic: repr.topic,
372 target: repr.target,
373 mutable: repr.mutable,
374 span: SpanRepr {
375 start: repr.span_start,
376 end: repr.span_end,
377 },
378 }
379 })
380 })
381 .collect();
382
383 let edges: Vec<GraphExportEdge> = inner
385 .edge_references()
386 .map(|e| GraphExportEdge {
387 source: e.source().index(),
388 target: e.target().index(),
389 edge_type: e.weight().label().to_string(),
390 })
391 .collect();
392
393 let mut topic_info: Vec<TopicExportInfo> = Vec::new();
395
396 topic_info.push(TopicExportInfo {
398 name: "start_agent".to_string(),
399 description: None,
400 is_entry: true,
401 transitions_to: Vec::new(),
402 delegates_to: Vec::new(),
403 actions: Vec::new(),
404 });
405
406 for topic_name in graph.topic_names() {
408 let mut transitions = Vec::new();
409 let mut delegates = Vec::new();
410 let mut actions = Vec::new();
411
412 for edge in inner.edge_references() {
414 let edge_type = edge.weight().label();
415 if let (Some(src), Some(tgt)) =
416 (graph.get_node(edge.source()), graph.get_node(edge.target()))
417 {
418 match (src, edge_type) {
419 (RefNode::Topic { name: src_name, .. }, "transitions_to")
420 if src_name == topic_name =>
421 {
422 if let RefNode::Topic { name: tgt_name, .. } = tgt {
423 transitions.push(tgt_name.clone());
424 }
425 }
426 (RefNode::Topic { name: src_name, .. }, "delegates")
427 if src_name == topic_name =>
428 {
429 if let RefNode::Topic { name: tgt_name, .. } = tgt {
430 delegates.push(tgt_name.clone());
431 }
432 }
433 _ => {}
434 }
435 }
436 }
437
438 for idx in inner.node_indices() {
440 if let Some(RefNode::ReasoningAction {
441 name,
442 topic,
443 target,
444 ..
445 }) = graph.get_node(idx)
446 {
447 if topic == topic_name {
448 actions.push(ActionExportInfo {
449 name: name.clone(),
450 target: target.clone(),
451 });
452 }
453 }
454 }
455
456 topic_info.push(TopicExportInfo {
457 name: topic_name.to_string(),
458 description: None,
459 is_entry: false,
460 transitions_to: transitions,
461 delegates_to: delegates,
462 actions,
463 });
464 }
465
466 for edge in inner.edge_references() {
468 if edge.weight().label() == "routes" {
469 if let Some(RefNode::StartAgent { .. }) = graph.get_node(edge.source()) {
470 if let Some(RefNode::Topic { name, .. }) = graph.get_node(edge.target()) {
471 if let Some(start) = topic_info.get_mut(0) {
472 start.transitions_to.push(name.clone());
473 }
474 }
475 }
476 }
477 }
478
479 let stats = graph.stats();
481 let validation = graph.validate();
482
483 GraphExport {
484 version: env!("CARGO_PKG_VERSION").to_string(),
485 nodes,
486 edges,
487 topics: topic_info,
488 variables: graph.variable_names().map(|s| s.to_string()).collect(),
489 stats: StatsExport {
490 total_nodes: stats.total_definitions(),
491 total_edges: stats.total_edges(),
492 topics: stats.topics,
493 variables: stats.variables,
494 action_defs: stats.action_defs,
495 reasoning_actions: stats.reasoning_actions,
496 },
497 validation: ValidationExport {
498 is_valid: validation.is_ok(),
499 errors: validation
500 .errors
501 .iter()
502 .map(ValidationErrorRepr::from)
503 .collect(),
504 warnings: validation
505 .warnings
506 .iter()
507 .map(ValidationErrorRepr::from)
508 .collect(),
509 },
510 }
511 }
512}