1use crate::step::{Step, StepType};
2use crate::types::JsonValue;
3use serde::{Deserialize, Serialize};
4
5#[cfg(test)]
6#[path = "workflow_test.rs"]
7mod workflow_test;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "lowercase")]
12pub enum WorkflowState {
13 Pending,
14 Running,
15 Completed,
16 Failed,
17}
18
19pub struct Workflow {
21 pub id: String,
22 pub steps: Vec<Box<dyn Step>>,
23 pub initial_input: JsonValue,
24 pub state: WorkflowState,
25}
26
27impl Workflow {
28 pub fn builder() -> WorkflowBuilder {
29 WorkflowBuilder::new()
30 }
31
32 pub fn to_mermaid(&self) -> String {
34 let mut diagram = String::from("flowchart TD\n");
35 let mut node_counter = 0;
36
37 diagram.push_str(" Start([Start])\n");
39
40 if self.steps.is_empty() {
41 diagram.push_str(" Start --> End\n");
42 } else {
43 let first_node = format!("N{}", node_counter);
45 diagram.push_str(&format!(" Start --> {}\n", first_node));
46
47 let last_node =
49 self.generate_mermaid_steps(&mut diagram, &mut node_counter, &first_node, 0);
50
51 diagram.push_str(&format!(" {} --> End\n", last_node));
53 }
54
55 diagram.push_str(" End([End])\n");
56
57 diagram.push('\n');
59 diagram.push_str(" classDef agentStyle fill:#e1f5ff,stroke:#01579b,stroke-width:2px\n");
60 diagram
61 .push_str(" classDef transformStyle fill:#f3e5f5,stroke:#4a148c,stroke-width:2px\n");
62 diagram.push_str(
63 " classDef conditionalStyle fill:#fff3e0,stroke:#e65100,stroke-width:2px\n",
64 );
65 diagram.push_str(
66 " classDef subworkflowStyle fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px\n",
67 );
68 diagram
69 .push_str(" classDef convergeStyle fill:#f5f5f5,stroke:#757575,stroke-width:1px\n");
70
71 diagram
72 }
73
74 fn generate_mermaid_steps(
76 &self,
77 diagram: &mut String,
78 node_counter: &mut usize,
79 entry_node: &str,
80 step_index: usize,
81 ) -> String {
82 if step_index >= self.steps.len() {
83 return entry_node.to_string();
84 }
85
86 let step = &self.steps[step_index];
87 let step_type = step.step_type();
88
89 match step_type {
90 StepType::Conditional => {
91 if let Some((then_step, else_step)) = step.get_branches() {
93 let conditional_node = entry_node;
94
95 let step_name = step.name();
97 diagram.push_str(&format!(
98 " {}{{{{\"{}\"}}}}:::conditionalStyle\n",
99 conditional_node, step_name
100 ));
101
102 *node_counter += 1;
104 let then_node = format!("N{}", node_counter);
105 let then_exit_node;
106
107 if then_step.step_type() == StepType::SubWorkflow {
109 if let Some(sub_wf) = then_step.get_sub_workflow() {
110 let (_entry, exit) = self.generate_subworkflow_inline(
112 diagram,
113 node_counter,
114 &then_node,
115 sub_wf,
116 then_step.name(),
117 );
118 then_exit_node = exit;
119 } else {
120 self.generate_step_node(diagram, &then_node, then_step);
121 then_exit_node = then_node.clone();
122 }
123 } else {
124 self.generate_step_node(diagram, &then_node, then_step);
125 then_exit_node = then_node.clone();
126 }
127
128 diagram.push_str(&format!(
129 " {} -->|\"✓ TRUE\"| {}\n",
130 conditional_node, then_node
131 ));
132
133 *node_counter += 1;
135 let else_node = format!("N{}", node_counter);
136 let else_exit_node;
137
138 if else_step.step_type() == StepType::SubWorkflow {
140 if let Some(sub_wf) = else_step.get_sub_workflow() {
141 let (_entry, exit) = self.generate_subworkflow_inline(
142 diagram,
143 node_counter,
144 &else_node,
145 sub_wf,
146 else_step.name(),
147 );
148 else_exit_node = exit;
149 } else {
150 self.generate_step_node(diagram, &else_node, else_step);
151 else_exit_node = else_node.clone();
152 }
153 } else {
154 self.generate_step_node(diagram, &else_node, else_step);
155 else_exit_node = else_node.clone();
156 }
157
158 diagram.push_str(&format!(
159 " {} -->|\"✗ FALSE\"| {}\n",
160 conditional_node, else_node
161 ));
162
163 *node_counter += 1;
165 let converge_node = format!("N{}", node_counter);
166 diagram.push_str(&format!(" {}(( )):::convergeStyle\n", converge_node));
167 diagram.push_str(&format!(" {} --> {}\n", then_exit_node, converge_node));
168 diagram.push_str(&format!(" {} --> {}\n", else_exit_node, converge_node));
169
170 if step_index + 1 < self.steps.len() {
172 let next_step = &self.steps[step_index + 1];
174 if next_step.step_type() == StepType::SubWorkflow {
175 return self.generate_mermaid_steps(
177 diagram,
178 node_counter,
179 &converge_node,
180 step_index + 1,
181 );
182 } else {
183 *node_counter += 1;
184 let next_node = format!("N{}", node_counter);
185 diagram.push_str(&format!(" {} --> {}\n", converge_node, next_node));
186 return self.generate_mermaid_steps(
187 diagram,
188 node_counter,
189 &next_node,
190 step_index + 1,
191 );
192 }
193 } else {
194 return converge_node;
195 }
196 }
197 }
198 StepType::SubWorkflow => {
199 if let Some(sub_wf) = step.get_sub_workflow() {
201 let sub_start_node = entry_node;
202
203 let (_entry, sub_exit_node) = self.generate_subworkflow_inline(
205 diagram,
206 node_counter,
207 sub_start_node,
208 sub_wf,
209 step.name(),
210 );
211
212 return self.generate_mermaid_steps(
214 diagram,
215 node_counter,
216 &sub_exit_node,
217 step_index + 1,
218 );
219 }
220 }
221 _ => {
222 let current_node = entry_node;
224 self.generate_step_node(diagram, current_node, step.as_ref());
225
226 if step_index + 1 < self.steps.len() {
228 let next_step = &self.steps[step_index + 1];
230 if next_step.step_type() == StepType::SubWorkflow {
231 return self.generate_mermaid_steps(
233 diagram,
234 node_counter,
235 current_node,
236 step_index + 1,
237 );
238 } else {
239 *node_counter += 1;
240 let next_node = format!("N{}", node_counter);
241 diagram.push_str(&format!(" {} --> {}\n", current_node, next_node));
242 return self.generate_mermaid_steps(
243 diagram,
244 node_counter,
245 &next_node,
246 step_index + 1,
247 );
248 }
249 } else {
250 return current_node.to_string();
252 }
253 }
254 }
255
256 entry_node.to_string()
257 }
258
259 fn generate_subworkflow_inline(
262 &self,
263 diagram: &mut String,
264 node_counter: &mut usize,
265 entry_point: &str,
266 sub_wf: Workflow,
267 step_name: &str,
268 ) -> (String, String) {
269 let subgraph_id = *node_counter;
270
271 diagram.push_str(&format!(
273 " subgraph SUB{}[\"📦 {}\"]\n",
274 subgraph_id, step_name
275 ));
276
277 *node_counter += 1;
278 let sub_entry = format!("N{}", node_counter);
279 diagram.push_str(&format!(" {}([Start])\n", sub_entry));
280
281 let sub_exit =
283 sub_wf.generate_mermaid_steps_in_subgraph(diagram, node_counter, &sub_entry, 0);
284
285 *node_counter += 1;
286 let sub_end = format!("N{}", node_counter);
287 diagram.push_str(&format!(" {}([End])\n", sub_end));
288 diagram.push_str(&format!(" {} --> {}\n", sub_exit, sub_end));
289
290 diagram.push_str(" end\n");
291
292 diagram.push_str(&format!(" {} --> {}\n", entry_point, sub_entry));
294
295 (entry_point.to_string(), sub_end)
297 }
298
299 fn generate_mermaid_steps_in_subgraph(
301 &self,
302 diagram: &mut String,
303 node_counter: &mut usize,
304 entry_node: &str,
305 step_index: usize,
306 ) -> String {
307 if step_index >= self.steps.len() {
308 return entry_node.to_string();
309 }
310
311 let step = &self.steps[step_index];
312 let step_type = step.step_type();
313
314 match step_type {
315 StepType::Conditional => {
316 if let Some((then_step, else_step)) = step.get_branches() {
317 let conditional_node = entry_node;
318
319 diagram.push_str(&format!(
320 " {}{{{{\"{}\"}}}}:::conditionalStyle\n",
321 conditional_node,
322 step.name()
323 ));
324
325 *node_counter += 1;
326 let then_node = format!("N{}", node_counter);
327 self.generate_step_node_indented(diagram, &then_node, then_step);
328 diagram.push_str(&format!(
329 " {} -->|\"✓\"| {}\n",
330 conditional_node, then_node
331 ));
332
333 *node_counter += 1;
334 let else_node = format!("N{}", node_counter);
335 self.generate_step_node_indented(diagram, &else_node, else_step);
336 diagram.push_str(&format!(
337 " {} -->|\"✗\"| {}\n",
338 conditional_node, else_node
339 ));
340
341 *node_counter += 1;
342 let converge_node = format!("N{}", node_counter);
343 diagram.push_str(&format!(" {}(( )):::convergeStyle\n", converge_node));
344 diagram.push_str(&format!(" {} --> {}\n", then_node, converge_node));
345 diagram.push_str(&format!(" {} --> {}\n", else_node, converge_node));
346
347 *node_counter += 1;
348 let next_node = format!("N{}", node_counter);
349 diagram.push_str(&format!(" {} --> {}\n", converge_node, next_node));
350
351 return self.generate_mermaid_steps_in_subgraph(
352 diagram,
353 node_counter,
354 &next_node,
355 step_index + 1,
356 );
357 }
358 }
359 StepType::SubWorkflow => {
360 if let Some(nested_wf) = step.get_sub_workflow() {
362 let nested_entry = entry_node;
363
364 let nested_id = *node_counter;
365 diagram.push_str(&format!(
366 " subgraph NESTED{}[\"📦 {}\"]\n",
367 nested_id,
368 step.name()
369 ));
370
371 *node_counter += 1;
372 let nested_start = format!("N{}", node_counter);
373 diagram.push_str(&format!(" {}([Start])\n", nested_start));
374
375 let nested_exit = nested_wf.generate_mermaid_steps_in_nested_subgraph(
376 diagram,
377 node_counter,
378 &nested_start,
379 0,
380 );
381
382 *node_counter += 1;
383 let nested_end = format!("N{}", node_counter);
384 diagram.push_str(&format!(" {}([End])\n", nested_end));
385 diagram.push_str(&format!(" {} --> {}\n", nested_exit, nested_end));
386 diagram.push_str(" end\n");
387
388 diagram.push_str(&format!(" {} --> {}\n", nested_entry, nested_start));
389
390 *node_counter += 1;
391 let next_node = format!("N{}", node_counter);
392 diagram.push_str(&format!(" {} --> {}\n", nested_end, next_node));
393
394 return self.generate_mermaid_steps_in_subgraph(
395 diagram,
396 node_counter,
397 &next_node,
398 step_index + 1,
399 );
400 }
401 }
402 _ => {
403 let current_node = entry_node;
404 self.generate_step_node_indented(diagram, current_node, step.as_ref());
405
406 *node_counter += 1;
407 let next_node = format!("N{}", node_counter);
408 diagram.push_str(&format!(" {} --> {}\n", current_node, next_node));
409
410 return self.generate_mermaid_steps_in_subgraph(
411 diagram,
412 node_counter,
413 &next_node,
414 step_index + 1,
415 );
416 }
417 }
418
419 entry_node.to_string()
420 }
421
422 fn generate_mermaid_steps_in_nested_subgraph(
424 &self,
425 diagram: &mut String,
426 node_counter: &mut usize,
427 entry_node: &str,
428 step_index: usize,
429 ) -> String {
430 if step_index >= self.steps.len() {
431 return entry_node.to_string();
432 }
433
434 let step = &self.steps[step_index];
435 let current_node = entry_node;
436
437 let step_name = step.name();
439 let step_type = step.step_type();
440
441 let (node_def, style) = match step_type {
442 StepType::Agent => (
443 format!(" {}[\"{}\"]", current_node, step_name),
444 ":::agentStyle",
445 ),
446 StepType::Transform => (
447 format!(" {}[/\"{}\"/]", current_node, step_name),
448 ":::transformStyle",
449 ),
450 StepType::Conditional => (
451 format!(" {}{{\"{}\"}}", current_node, step_name),
452 ":::conditionalStyle",
453 ),
454 _ => (
455 format!(" {}[\"{}\"]", current_node, step_name),
456 "",
457 ),
458 };
459
460 diagram.push_str(&format!("{}{}\n", node_def, style));
461
462 *node_counter += 1;
463 let next_node = format!("N{}", node_counter);
464 diagram.push_str(&format!(" {} --> {}\n", current_node, next_node));
465
466 self.generate_mermaid_steps_in_nested_subgraph(
467 diagram,
468 node_counter,
469 &next_node,
470 step_index + 1,
471 )
472 }
473
474 fn generate_step_node(&self, diagram: &mut String, node_id: &str, step: &dyn Step) {
476 let step_name = step.name();
477 let step_type = step.step_type();
478
479 let (node_def, style) = match step_type {
480 StepType::Agent => (
481 format!(" {}[\"{}\"]", node_id, step_name),
482 ":::agentStyle",
483 ),
484 StepType::Transform => (
485 format!(" {}[/\"{}\"/]", node_id, step_name),
486 ":::transformStyle",
487 ),
488 StepType::SubWorkflow => (
489 format!(" {}[[\"{}\"]", node_id, step_name),
490 ":::subworkflowStyle",
491 ),
492 _ => (format!(" {}[\"{}\"]", node_id, step_name), ""),
493 };
494
495 diagram.push_str(&format!("{}{}\n", node_def, style));
496 }
497
498 fn generate_step_node_indented(&self, diagram: &mut String, node_id: &str, step: &dyn Step) {
500 let step_name = step.name();
501 let step_type = step.step_type();
502
503 let (node_def, style) = match step_type {
504 StepType::Agent => (
505 format!(" {}[\"{}\"]", node_id, step_name),
506 ":::agentStyle",
507 ),
508 StepType::Transform => (
509 format!(" {}[/\"{}\"/]", node_id, step_name),
510 ":::transformStyle",
511 ),
512 _ => (format!(" {}[\"{}\"]", node_id, step_name), ""),
513 };
514
515 diagram.push_str(&format!("{}{}\n", node_def, style));
516 }
517}
518
519pub struct WorkflowBuilder {
521 steps: Vec<Box<dyn Step>>,
522 initial_input: Option<JsonValue>,
523}
524
525impl WorkflowBuilder {
526 pub fn new() -> Self {
527 Self {
528 steps: Vec::new(),
529 initial_input: None,
530 }
531 }
532
533 pub fn step(mut self, step: Box<dyn Step>) -> Self {
535 self.steps.push(step);
536 self
537 }
538
539 pub fn initial_input(mut self, input: JsonValue) -> Self {
541 self.initial_input = Some(input);
542 self
543 }
544
545 pub fn build(self) -> Workflow {
546 Workflow {
547 id: format!("wf_{}", uuid::Uuid::new_v4()),
548 steps: self.steps,
549 initial_input: self.initial_input.unwrap_or(serde_json::json!({})),
550 state: WorkflowState::Pending,
551 }
552 }
553}
554
555impl Default for WorkflowBuilder {
556 fn default() -> Self {
557 Self::new()
558 }
559}
560
561#[derive(Debug, Clone, Serialize, Deserialize)]
563pub struct WorkflowRun {
564 pub workflow_id: String,
565 pub state: WorkflowState,
566 pub steps: Vec<WorkflowStepRecord>,
567 pub final_output: Option<JsonValue>,
568
569 #[serde(skip_serializing_if = "Option::is_none")]
571 pub parent_workflow_id: Option<String>,
572}
573
574impl WorkflowRun {
575 pub fn to_mermaid_with_results(&self) -> String {
577 let mut diagram = String::from("flowchart TD\n");
578
579 let start_style = match self.state {
581 WorkflowState::Completed => ":::successStyle",
582 WorkflowState::Failed => ":::failureStyle",
583 _ => "",
584 };
585 diagram.push_str(&format!(" Start([Start]){} \n", start_style));
586
587 if !self.steps.is_empty() {
589 diagram.push_str(" Start --> Step0\n");
590 }
591
592 for (i, step) in self.steps.iter().enumerate() {
594 let node_id = format!("Step{}", i);
595 let step_name = &step.step_name;
596 let step_type = &step.step_type;
597 let exec_time = step.execution_time_ms.unwrap_or(0);
598
599 let has_output = step.output.is_some();
601 let style_class = if has_output {
602 ":::successStyle"
603 } else {
604 ":::failureStyle"
605 };
606
607 let node_def = if step_type.contains("Agent") {
609 format!(
610 " {}[\"{}<br/><i>{}</i><br/>{}ms\"]{}",
611 node_id, step_name, step_type, exec_time, style_class
612 )
613 } else if step_type.contains("Transform") {
614 format!(
615 " {}[/\"{}<br/><i>{}</i><br/>{}ms\"/]{}",
616 node_id, step_name, step_type, exec_time, style_class
617 )
618 } else if step_type.contains("Conditional") {
619 format!(
620 " {}{{\"{}<br/><i>{}</i><br/>{}ms\"}}{}",
621 node_id, step_name, step_type, exec_time, style_class
622 )
623 } else if step_type.contains("SubWorkflow") {
624 format!(
625 " {}[[\"{}<br/><i>{}</i><br/>{}ms\"]]{}",
626 node_id, step_name, step_type, exec_time, style_class
627 )
628 } else {
629 format!(
630 " {}[\"{}<br/><i>{}</i><br/>{}ms\"]{}",
631 node_id, step_name, step_type, exec_time, style_class
632 )
633 };
634
635 diagram.push_str(&node_def);
636 diagram.push('\n');
637
638 if i < self.steps.len() - 1 {
640 diagram.push_str(&format!(" {} --> Step{}\n", node_id, i + 1));
641 }
642 }
643
644 if !self.steps.is_empty() {
646 let last_step = self.steps.len() - 1;
647 diagram.push_str(&format!(" Step{} --> End\n", last_step));
648 } else {
649 diagram.push_str(" Start --> End\n");
650 }
651
652 let end_style = match self.state {
653 WorkflowState::Completed => ":::successStyle",
654 WorkflowState::Failed => ":::failureStyle",
655 _ => "",
656 };
657 diagram.push_str(&format!(" End([End]){}\n", end_style));
658
659 diagram.push('\n');
661 diagram
662 .push_str(" classDef successStyle fill:#c8e6c9,stroke:#2e7d32,stroke-width:3px\n");
663 diagram
664 .push_str(" classDef failureStyle fill:#ffcdd2,stroke:#c62828,stroke-width:3px\n");
665
666 diagram
667 }
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize)]
672pub struct WorkflowStepRecord {
673 pub step_index: usize,
674 pub step_name: String,
675 pub step_type: String,
676 pub input: JsonValue,
677 pub output: Option<JsonValue>,
678 pub execution_time_ms: Option<u64>,
679}