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