From b8704b0cb0efa868454b70e3f8e60fdbea9ec543 Mon Sep 17 00:00:00 2001 From: Santiago Bartesaghi Date: Sun, 1 Mar 2026 19:07:05 -0300 Subject: [PATCH 1/3] Adjust examples --- examples/complex_llm_workflow/diagram.md | 32 ------------------ examples/complex_llm_workflow/generator.rb | 1 + examples/complex_workflow/diagram.html | 25 +++++--------- examples/complex_workflow/diagram.md | 25 +++++--------- examples/complex_workflow/generator.rb | 1 + examples/parallel_workflow/diagram.html | 38 ++++++++++++++++++++++ examples/parallel_workflow/diagram.md | 9 ++--- examples/parallel_workflow/generator.rb | 1 + examples/simple_workflow/diagram.html | 38 ++++++++++++++++++++++ examples/simple_workflow/diagram.md | 13 +++++--- examples/simple_workflow/generator.rb | 1 + lib/mars/rendering/graph/builder.rb | 8 ++++- lib/mars/rendering/mermaid.rb | 13 +++++--- 13 files changed, 123 insertions(+), 82 deletions(-) delete mode 100644 examples/complex_llm_workflow/diagram.md create mode 100644 examples/parallel_workflow/diagram.html create mode 100644 examples/simple_workflow/diagram.html diff --git a/examples/complex_llm_workflow/diagram.md b/examples/complex_llm_workflow/diagram.md deleted file mode 100644 index 6051243..0000000 --- a/examples/complex_llm_workflow/diagram.md +++ /dev/null @@ -1,32 +0,0 @@ -```mermaid -flowchart LR -in((In)) -out((Out)) -agent1[Agent1] -gate{Gate} -parallel_workflow_aggregator[Parallel workflow Aggregator] -agent2[Agent2] -agent3[Agent3] -agent4[Agent4] -subgraph parallel_workflow["Parallel workflow"] - agent2 - agent3 - agent4 -end -subgraph sequential_workflow["Sequential workflow"] - agent1 - gate - parallel_workflow - parallel_workflow_aggregator -end -in --> agent1 -agent1 --> gate -gate -->|failure| out -gate --> agent2 -gate --> agent3 -gate --> agent4 -agent2 --> parallel_workflow_aggregator -parallel_workflow_aggregator --> out -agent3 --> parallel_workflow_aggregator -agent4 --> parallel_workflow_aggregator -``` diff --git a/examples/complex_llm_workflow/generator.rb b/examples/complex_llm_workflow/generator.rb index 0bfe4b6..a8f8c8d 100755 --- a/examples/complex_llm_workflow/generator.rb +++ b/examples/complex_llm_workflow/generator.rb @@ -109,6 +109,7 @@ class WeatherStep < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(sequential_workflow).render File.write("examples/complex_llm_workflow/diagram.md", diagram) +MARS::Rendering::Html.new(sequential_workflow).write("examples/complex_llm_workflow/diagram.html") puts "Complex workflow diagram saved to: examples/complex_llm_workflow/diagram.md" # Run the workflow diff --git a/examples/complex_workflow/diagram.html b/examples/complex_workflow/diagram.html index 53b7070..6b569a5 100644 --- a/examples/complex_workflow/diagram.html +++ b/examples/complex_workflow/diagram.html @@ -18,30 +18,21 @@

Main Pipeline

const diagram = `flowchart LR in((In)) out((Out)) -agent1[agent1] -gate{Gate} -agent4[agent4] -parallel_workflow_aggregator[Parallel workflow Aggregator] -agent2[agent2] -agent3[agent3] -parallel_workflow_2_aggregator[Parallel workflow 2 Aggregator] -agent5[agent5] subgraph main_pipeline["Main Pipeline"] - agent1 - gate - parallel_workflow_aggregator + agent1[agent1] + gate{Gate} subgraph parallel_workflow_2["Parallel workflow 2"] subgraph sequential_workflow["Sequential workflow"] - agent4 + agent4[agent4] subgraph parallel_workflow["Parallel workflow"] - agent2 - agent3 + agent2[agent2] + agent3[agent3] end - parallel_workflow_aggregator + parallel_workflow_aggregator[Parallel workflow Aggregator] end - agent5 + agent5[agent5] end - parallel_workflow_2_aggregator + parallel_workflow_2_aggregator[Parallel workflow 2 Aggregator] end in --> agent1 agent1 --> gate diff --git a/examples/complex_workflow/diagram.md b/examples/complex_workflow/diagram.md index dd8fc85..7469b72 100644 --- a/examples/complex_workflow/diagram.md +++ b/examples/complex_workflow/diagram.md @@ -2,30 +2,21 @@ flowchart LR in((In)) out((Out)) -agent1[agent1] -gate{Gate} -agent4[agent4] -parallel_workflow_aggregator[Parallel workflow Aggregator] -agent2[agent2] -agent3[agent3] -parallel_workflow_2_aggregator[Parallel workflow 2 Aggregator] -agent5[agent5] subgraph main_pipeline["Main Pipeline"] - agent1 - gate - parallel_workflow_aggregator + agent1[agent1] + gate{Gate} subgraph parallel_workflow_2["Parallel workflow 2"] subgraph sequential_workflow["Sequential workflow"] - agent4 + agent4[agent4] subgraph parallel_workflow["Parallel workflow"] - agent2 - agent3 + agent2[agent2] + agent3[agent3] end - parallel_workflow_aggregator + parallel_workflow_aggregator[Parallel workflow Aggregator] end - agent5 + agent5[agent5] end - parallel_workflow_2_aggregator + parallel_workflow_2_aggregator[Parallel workflow 2 Aggregator] end in --> agent1 agent1 --> gate diff --git a/examples/complex_workflow/generator.rb b/examples/complex_workflow/generator.rb index d32a2fa..9e39178 100755 --- a/examples/complex_workflow/generator.rb +++ b/examples/complex_workflow/generator.rb @@ -62,4 +62,5 @@ class Agent5 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(main_workflow).render File.write("examples/complex_workflow/diagram.md", diagram) +MARS::Rendering::Html.new(main_workflow).write("examples/complex_workflow/diagram.html") puts "Complex workflow diagram saved to: examples/complex_workflow/diagram.md" diff --git a/examples/parallel_workflow/diagram.html b/examples/parallel_workflow/diagram.html new file mode 100644 index 0000000..b8d26ac --- /dev/null +++ b/examples/parallel_workflow/diagram.html @@ -0,0 +1,38 @@ + + + + + + Parallel workflow + + + +

Parallel workflow

+
+ + + diff --git a/examples/parallel_workflow/diagram.md b/examples/parallel_workflow/diagram.md index 8265347..6bbbaaf 100644 --- a/examples/parallel_workflow/diagram.md +++ b/examples/parallel_workflow/diagram.md @@ -3,13 +3,10 @@ flowchart LR in((In)) out((Out)) aggregator[Aggregator] -agent1[Agent1] -agent2[Agent2] -agent3[Agent3] subgraph parallel_workflow["Parallel workflow"] - agent1 - agent2 - agent3 + agent1[agent1] + agent2[agent2] + agent3[agent3] end in --> agent1 in --> agent2 diff --git a/examples/parallel_workflow/generator.rb b/examples/parallel_workflow/generator.rb index 66378bd..160fa11 100755 --- a/examples/parallel_workflow/generator.rb +++ b/examples/parallel_workflow/generator.rb @@ -30,4 +30,5 @@ class Agent3 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(parallel_workflow).render File.write("examples/parallel_workflow/diagram.md", diagram) +MARS::Rendering::Html.new(parallel_workflow).write("examples/parallel_workflow/diagram.html") puts "Parallel workflow diagram saved to: examples/parallel_workflow/diagram.md" diff --git a/examples/simple_workflow/diagram.html b/examples/simple_workflow/diagram.html new file mode 100644 index 0000000..1f9ffd9 --- /dev/null +++ b/examples/simple_workflow/diagram.html @@ -0,0 +1,38 @@ + + + + + + Main Pipeline + + + +

Main Pipeline

+
+ + + diff --git a/examples/simple_workflow/diagram.md b/examples/simple_workflow/diagram.md index 4bab18d..5891e06 100644 --- a/examples/simple_workflow/diagram.md +++ b/examples/simple_workflow/diagram.md @@ -2,14 +2,17 @@ flowchart LR in((In)) out((Out)) -agent1[Agent1] -gate{Gate} -agent2[Agent2] -agent3[Agent3] +subgraph main_pipeline["Main Pipeline"] + agent1[agent1] + gate{Gate} +end +subgraph success_workflow["Success workflow"] + agent2[agent2] + agent3[agent3] +end in --> agent1 agent1 --> gate gate -->|success| agent2 -gate -->|default| out agent2 --> agent3 agent3 --> out ``` diff --git a/examples/simple_workflow/generator.rb b/examples/simple_workflow/generator.rb index b1a0351..60bde50 100755 --- a/examples/simple_workflow/generator.rb +++ b/examples/simple_workflow/generator.rb @@ -41,4 +41,5 @@ class Agent3 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(main_workflow).render File.write("examples/simple_workflow/diagram.md", diagram) +MARS::Rendering::Html.new(main_workflow).write("examples/simple_workflow/diagram.html") puts "Simple workflow diagram saved to: examples/simple_workflow/diagram.md" diff --git a/lib/mars/rendering/graph/builder.rb b/lib/mars/rendering/graph/builder.rb index 9e5db74..5598570 100644 --- a/lib/mars/rendering/graph/builder.rb +++ b/lib/mars/rendering/graph/builder.rb @@ -33,10 +33,16 @@ def add_subgraph(id, name) end def add_node_to_subgraph(id, node_id) - return if subgraphs[id]&.nodes&.include?(node_id) + return if node_in_any_subgraph?(node_id) subgraphs[id].nodes << node_id end + + private + + def node_in_any_subgraph?(node_id) + subgraphs.values.any? { |sg| sg.nodes.include?(node_id) } + end end end end diff --git a/lib/mars/rendering/mermaid.rb b/lib/mars/rendering/mermaid.rb index 3f1914a..ef5cf57 100644 --- a/lib/mars/rendering/mermaid.rb +++ b/lib/mars/rendering/mermaid.rb @@ -23,11 +23,16 @@ def render(options = {}) end def graph_mermaid - nodes_mermaid + subgraphs_mermaid + edges_mermaid + top_level_nodes_mermaid + subgraphs_mermaid + edges_mermaid end - def nodes_mermaid - nodes.keys.map { |node_id| "#{node_id}#{shape(node_id)}" } + def top_level_nodes_mermaid + subgraph_node_ids = subgraphs.values.flat_map(&:nodes).to_set + nodes.keys.reject { |id| subgraph_node_ids.include?(id) }.map { |id| node_definition(id) } + end + + def node_definition(node_id) + "#{node_id}#{shape(node_id)}" end def subgraphs_mermaid @@ -87,7 +92,7 @@ def render_subgraph_node(node_id, indent) if subgraphs.key?(node_id) render_subgraph(node_id, "#{indent} ") else - "#{indent} #{node_id}" + "#{indent} #{node_definition(node_id)}" end end end From 36dde677c1c1ff5ec7d8b474fff89ab791337f32 Mon Sep 17 00:00:00 2001 From: Santiago Bartesaghi Date: Mon, 2 Mar 2026 10:44:57 -0300 Subject: [PATCH 2/3] Silence warning --- lib/mars.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mars.rb b/lib/mars.rb index 4d0dffd..19a0cf2 100644 --- a/lib/mars.rb +++ b/lib/mars.rb @@ -7,6 +7,7 @@ loader = Zeitwerk::Loader.for_gem loader.inflector.inflect("mars" => "MARS") +loader.ignore("#{__dir__}/mars_rb.rb") loader.setup module MARS From 9cf5ad6b1017b8348cf427979e9aff8320b1df96 Mon Sep 17 00:00:00 2001 From: Santiago Bartesaghi Date: Mon, 2 Mar 2026 11:37:10 -0300 Subject: [PATCH 3/3] Improvements --- examples/complex_llm_workflow/diagram.html | 45 +++++++++++++++++++ examples/complex_llm_workflow/diagram.md | 25 +++++++++++ examples/complex_llm_workflow/generator.rb | 3 +- examples/complex_workflow/diagram.html | 3 +- examples/complex_workflow/diagram.md | 3 +- examples/complex_workflow/generator.rb | 3 +- examples/parallel_workflow/generator.rb | 3 +- examples/simple_workflow/generator.rb | 3 +- lib/mars/rendering/graph/builder.rb | 24 +++++++++- .../rendering/graph/sequential_workflow.rb | 19 ++++++-- 10 files changed, 117 insertions(+), 14 deletions(-) create mode 100644 examples/complex_llm_workflow/diagram.html create mode 100644 examples/complex_llm_workflow/diagram.md diff --git a/examples/complex_llm_workflow/diagram.html b/examples/complex_llm_workflow/diagram.html new file mode 100644 index 0000000..90467eb --- /dev/null +++ b/examples/complex_llm_workflow/diagram.html @@ -0,0 +1,45 @@ + + + + + + Sequential workflow + + + +

Sequential workflow

+
+ + + diff --git a/examples/complex_llm_workflow/diagram.md b/examples/complex_llm_workflow/diagram.md new file mode 100644 index 0000000..eb60ca2 --- /dev/null +++ b/examples/complex_llm_workflow/diagram.md @@ -0,0 +1,25 @@ +```mermaid +flowchart LR +in((In)) +out((Out)) +subgraph sequential_workflow["Sequential workflow"] + country[Country] + gate{Gate} + subgraph parallel_workflow["Parallel workflow"] + food[Food] + sports[Sports] + weather[Weather] + end + parallel_workflow_aggregator[Parallel workflow Aggregator] +end +in --> country +country --> gate +gate -->|failure| out +gate --> food +gate --> sports +gate --> weather +food --> parallel_workflow_aggregator +parallel_workflow_aggregator --> out +sports --> parallel_workflow_aggregator +weather --> parallel_workflow_aggregator +``` diff --git a/examples/complex_llm_workflow/generator.rb b/examples/complex_llm_workflow/generator.rb index a8f8c8d..b23492d 100755 --- a/examples/complex_llm_workflow/generator.rb +++ b/examples/complex_llm_workflow/generator.rb @@ -109,8 +109,9 @@ class WeatherStep < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(sequential_workflow).render File.write("examples/complex_llm_workflow/diagram.md", diagram) -MARS::Rendering::Html.new(sequential_workflow).write("examples/complex_llm_workflow/diagram.html") puts "Complex workflow diagram saved to: examples/complex_llm_workflow/diagram.md" +MARS::Rendering::Html.new(sequential_workflow).write("examples/complex_llm_workflow/diagram.html") +puts "Complex workflow beautiful mermaid diagram saved to: examples/complex_llm_workflow/diagram.html" # Run the workflow puts sequential_workflow.run("Which is the largest country in Europe?") diff --git a/examples/complex_workflow/diagram.html b/examples/complex_workflow/diagram.html index 6b569a5..3da8bb0 100644 --- a/examples/complex_workflow/diagram.html +++ b/examples/complex_workflow/diagram.html @@ -39,12 +39,11 @@

Main Pipeline

gate -->|warning| agent4 gate -->|error| agent2 gate -->|error| agent3 -gate --> agent4 -gate --> agent5 agent4 --> agent2 agent4 --> agent3 agent2 --> parallel_workflow_aggregator parallel_workflow_aggregator --> parallel_workflow_2_aggregator +parallel_workflow_aggregator --> agent5 agent3 --> parallel_workflow_aggregator parallel_workflow_2_aggregator --> out agent5 --> parallel_workflow_2_aggregator`; diff --git a/examples/complex_workflow/diagram.md b/examples/complex_workflow/diagram.md index 7469b72..c95456d 100644 --- a/examples/complex_workflow/diagram.md +++ b/examples/complex_workflow/diagram.md @@ -23,12 +23,11 @@ agent1 --> gate gate -->|warning| agent4 gate -->|error| agent2 gate -->|error| agent3 -gate --> agent4 -gate --> agent5 agent4 --> agent2 agent4 --> agent3 agent2 --> parallel_workflow_aggregator parallel_workflow_aggregator --> parallel_workflow_2_aggregator +parallel_workflow_aggregator --> agent5 agent3 --> parallel_workflow_aggregator parallel_workflow_2_aggregator --> out agent5 --> parallel_workflow_2_aggregator diff --git a/examples/complex_workflow/generator.rb b/examples/complex_workflow/generator.rb index 9e39178..aa36cfb 100755 --- a/examples/complex_workflow/generator.rb +++ b/examples/complex_workflow/generator.rb @@ -62,5 +62,6 @@ class Agent5 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(main_workflow).render File.write("examples/complex_workflow/diagram.md", diagram) -MARS::Rendering::Html.new(main_workflow).write("examples/complex_workflow/diagram.html") puts "Complex workflow diagram saved to: examples/complex_workflow/diagram.md" +MARS::Rendering::Html.new(main_workflow).write("examples/complex_workflow/diagram.html") +puts "Complex workflow beautiful mermaid diagram saved to: examples/complex_workflow/diagram.html" diff --git a/examples/parallel_workflow/generator.rb b/examples/parallel_workflow/generator.rb index 160fa11..1cc3992 100755 --- a/examples/parallel_workflow/generator.rb +++ b/examples/parallel_workflow/generator.rb @@ -30,5 +30,6 @@ class Agent3 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(parallel_workflow).render File.write("examples/parallel_workflow/diagram.md", diagram) -MARS::Rendering::Html.new(parallel_workflow).write("examples/parallel_workflow/diagram.html") puts "Parallel workflow diagram saved to: examples/parallel_workflow/diagram.md" +MARS::Rendering::Html.new(parallel_workflow).write("examples/parallel_workflow/diagram.html") +puts "Parallel workflow beautiful mermaid diagram saved to: examples/parallel_workflow/diagram.html" diff --git a/examples/simple_workflow/generator.rb b/examples/simple_workflow/generator.rb index 60bde50..60e246f 100755 --- a/examples/simple_workflow/generator.rb +++ b/examples/simple_workflow/generator.rb @@ -41,5 +41,6 @@ class Agent3 < MARS::AgentStep # Generate and save the diagram diagram = MARS::Rendering::Mermaid.new(main_workflow).render File.write("examples/simple_workflow/diagram.md", diagram) -MARS::Rendering::Html.new(main_workflow).write("examples/simple_workflow/diagram.html") puts "Simple workflow diagram saved to: examples/simple_workflow/diagram.md" +MARS::Rendering::Html.new(main_workflow).write("examples/simple_workflow/diagram.html") +puts "Simple workflow beautiful mermaid diagram saved to: examples/simple_workflow/diagram.html" diff --git a/lib/mars/rendering/graph/builder.rb b/lib/mars/rendering/graph/builder.rb index 5598570..4778ab4 100644 --- a/lib/mars/rendering/graph/builder.rb +++ b/lib/mars/rendering/graph/builder.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "set" + module MARS module Rendering module Graph @@ -14,9 +16,10 @@ def initialize def add_edge(from, to, value = nil) return unless from && to + return if adjacency[from].include?([to, value]) + return if reachable?(to, from) - # can we avoid visiting the node twice instead? - adjacency[from] << [to, value] unless adjacency[from].include?([to, value]) + adjacency[from] << [to, value] adjacency[to] = [] unless adjacency[to] end @@ -43,6 +46,23 @@ def add_node_to_subgraph(id, node_id) def node_in_any_subgraph?(node_id) subgraphs.values.any? { |sg| sg.nodes.include?(node_id) } end + + def reachable?(from, target) + visited = Set.new + queue = [from] + + while queue.any? + current = queue.shift + next if visited.include?(current) + + visited << current + return true if current == target + + adjacency[current]&.each { |(to, _)| queue << to } + end + + false + end end end end diff --git a/lib/mars/rendering/graph/sequential_workflow.rb b/lib/mars/rendering/graph/sequential_workflow.rb index 69450f7..fef99a3 100644 --- a/lib/mars/rendering/graph/sequential_workflow.rb +++ b/lib/mars/rendering/graph/sequential_workflow.rb @@ -20,19 +20,30 @@ def to_graph(builder, parent_id: nil, value: nil) def build_steps_graph(builder, parent_id, value) sink_nodes = [] + extra_parents = [] steps.each do |step| sink_nodes = step.to_graph(builder, parent_id: parent_id, value: value) - value = nil # We don't want to pass the value to subsequent steps - parent_id = step.node_id + extra_parents.each { |ep| builder.add_edge(ep, step.node_id) } - builder.add_node_to_subgraph(node_id, step.node_id) + value = nil + parent_id, extra_parents = process_sink_nodes(sink_nodes, step) - sink_nodes.each { |sink_node| builder.add_node_to_subgraph(node_id, sink_node) } + add_to_subgraph(builder, step, sink_nodes) end [parent_id, value, sink_nodes] end + + def process_sink_nodes(sink_nodes, step) + unique_sinks = sink_nodes.uniq + [unique_sinks.first || step.node_id, unique_sinks.drop(1)] + end + + def add_to_subgraph(builder, step, sink_nodes) + builder.add_node_to_subgraph(node_id, step.node_id) + sink_nodes.each { |sink_node| builder.add_node_to_subgraph(node_id, sink_node) } + end end end end