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
index 6051243..eb60ca2 100644
--- a/examples/complex_llm_workflow/diagram.md
+++ b/examples/complex_llm_workflow/diagram.md
@@ -2,31 +2,24 @@
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
+ country[Country]
+ gate{Gate}
+ subgraph parallel_workflow["Parallel workflow"]
+ food[Food]
+ sports[Sports]
+ weather[Weather]
+ end
+ parallel_workflow_aggregator[Parallel workflow Aggregator]
end
-in --> agent1
-agent1 --> gate
+in --> country
+country --> gate
gate -->|failure| out
-gate --> agent2
-gate --> agent3
-gate --> agent4
-agent2 --> parallel_workflow_aggregator
+gate --> food
+gate --> sports
+gate --> weather
+food --> parallel_workflow_aggregator
parallel_workflow_aggregator --> out
-agent3 --> parallel_workflow_aggregator
-agent4 --> parallel_workflow_aggregator
+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 0bfe4b6..b23492d 100755
--- a/examples/complex_llm_workflow/generator.rb
+++ b/examples/complex_llm_workflow/generator.rb
@@ -110,6 +110,8 @@ class WeatherStep < MARS::AgentStep
diagram = MARS::Rendering::Mermaid.new(sequential_workflow).render
File.write("examples/complex_llm_workflow/diagram.md", diagram)
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 53b7070..3da8bb0 100644
--- a/examples/complex_workflow/diagram.html
+++ b/examples/complex_workflow/diagram.html
@@ -18,42 +18,32 @@ 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
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 dd8fc85..c95456d 100644
--- a/examples/complex_workflow/diagram.md
+++ b/examples/complex_workflow/diagram.md
@@ -2,42 +2,32 @@
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
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 d32a2fa..aa36cfb 100755
--- a/examples/complex_workflow/generator.rb
+++ b/examples/complex_workflow/generator.rb
@@ -63,3 +63,5 @@ class Agent5 < MARS::AgentStep
diagram = MARS::Rendering::Mermaid.new(main_workflow).render
File.write("examples/complex_workflow/diagram.md", diagram)
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/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..1cc3992 100755
--- a/examples/parallel_workflow/generator.rb
+++ b/examples/parallel_workflow/generator.rb
@@ -31,3 +31,5 @@ class Agent3 < MARS::AgentStep
diagram = MARS::Rendering::Mermaid.new(parallel_workflow).render
File.write("examples/parallel_workflow/diagram.md", diagram)
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/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..60e246f 100755
--- a/examples/simple_workflow/generator.rb
+++ b/examples/simple_workflow/generator.rb
@@ -42,3 +42,5 @@ class Agent3 < MARS::AgentStep
diagram = MARS::Rendering::Mermaid.new(main_workflow).render
File.write("examples/simple_workflow/diagram.md", diagram)
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.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
diff --git a/lib/mars/rendering/graph/builder.rb b/lib/mars/rendering/graph/builder.rb
index 9e5db74..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
@@ -33,10 +36,33 @@ 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
+
+ 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
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