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                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// ============================================================================
205// Variable usage representations
206// ============================================================================
207
208/// Serializable representation of variable usages.
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct VariableUsagesRepr {
211    pub readers: Vec<UsageInfoRepr>,
212    pub writers: Vec<UsageInfoRepr>,
213}
214
215/// Serializable representation of a usage location.
216#[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// ============================================================================
273// Full export types (for JSON/GraphQL)
274// ============================================================================
275
276/// Full graph export for external consumption.
277#[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/// Node representation for full export.
289#[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/// Edge representation for full export.
301#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct GraphExportEdge {
303    pub source: usize,
304    pub target: usize,
305    pub edge_type: String,
306}
307
308/// Span representation.
309#[derive(Debug, Clone, Serialize, Deserialize)]
310pub struct SpanRepr {
311    pub start: usize,
312    pub end: usize,
313}
314
315/// Topic information for export.
316#[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/// Action information within a topic.
327#[derive(Debug, Clone, Serialize, Deserialize)]
328pub struct ActionExportInfo {
329    pub name: String,
330    pub target: Option<String>,
331}
332
333/// Statistics for export.
334#[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/// Validation results for export.
345#[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
352// ============================================================================
353// Builder for full export
354// ============================================================================
355
356impl GraphExport {
357    /// Build a full export from a RefGraph.
358    pub fn from_graph(graph: &RefGraph) -> Self {
359        let inner = graph.inner();
360
361        // Build nodes
362        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        // Build edges
384        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        // Build topic info
394        let mut topic_info: Vec<TopicExportInfo> = Vec::new();
395
396        // Add start_agent first
397        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        // Collect topic information
407        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            // Find topic's actions and transitions from edges
413            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            // Find actions defined in this topic
439            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        // Update start_agent transitions
467        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        // Get stats and validation
480        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}