Skip to main content

busbar_sf_agentscript/graph/
export.rs

1//! Serialization types for graph export.
2//!
3//! This module contains all the data structures used to serialize RefGraph
4//! data for external consumption (JSON, WASM, etc.).
5
6use super::error::ValidationError;
7use super::{RefGraph, RefNode, ValidationResult};
8use petgraph::visit::EdgeRef;
9use serde::{Deserialize, Serialize};
10
11// ============================================================================
12// Basic representations
13// ============================================================================
14
15/// Serializable representation of a RefGraph.
16#[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/// Serializable representation of a RefNode.
55#[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/// Serializable representation of a RefEdge.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct EdgeRepr {
139    pub source: usize,
140    pub target: usize,
141    pub edge_type: String,
142}
143
144// ============================================================================
145// Validation representations
146// ============================================================================
147
148/// Serializable representation of ValidationResult.
149#[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/// Serializable representation of ValidationError.
175#[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// ============================================================================
204// Variable usage representations
205// ============================================================================
206
207/// Serializable representation of variable usages.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct VariableUsagesRepr {
210    pub readers: Vec<UsageInfoRepr>,
211    pub writers: Vec<UsageInfoRepr>,
212}
213
214/// Serializable representation of a usage location.
215#[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// ============================================================================
272// Full export types (for JSON/GraphQL)
273// ============================================================================
274
275/// Full graph export for external consumption.
276#[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/// Node representation for full export.
288#[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/// Edge representation for full export.
300#[derive(Debug, Clone, Serialize, Deserialize)]
301pub struct GraphExportEdge {
302    pub source: usize,
303    pub target: usize,
304    pub edge_type: String,
305}
306
307/// Span representation.
308#[derive(Debug, Clone, Serialize, Deserialize)]
309pub struct SpanRepr {
310    pub start: usize,
311    pub end: usize,
312}
313
314/// Topic information for export.
315#[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/// Action information within a topic.
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct ActionExportInfo {
328    pub name: String,
329    pub target: Option<String>,
330}
331
332/// Statistics for export.
333#[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/// Validation results for export.
344#[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
351// ============================================================================
352// Builder for full export
353// ============================================================================
354
355impl GraphExport {
356    /// Build a full export from a RefGraph.
357    pub fn from_graph(graph: &RefGraph) -> Self {
358        let inner = graph.inner();
359
360        // Build nodes
361        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        // Build edges
383        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        // Build topic info
393        let mut topic_info: Vec<TopicExportInfo> = Vec::new();
394
395        // Add start_agent first
396        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        // Collect topic information
406        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            // Find topic's actions and transitions from edges
412            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            // Find actions defined in this topic
438            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        // Update start_agent transitions
466        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        // Get stats and validation
479        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}