mars_rb 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +38 -0
- data/README.md +183 -12
- data/examples/complex_llm_workflow/diagram.md +32 -0
- data/examples/complex_llm_workflow/generator.rb +112 -0
- data/examples/complex_workflow/diagram.md +47 -0
- data/examples/complex_workflow/generator.rb +108 -0
- data/examples/parallel_workflow/diagram.md +21 -0
- data/examples/parallel_workflow/generator.rb +33 -0
- data/examples/simple_workflow/diagram.md +26 -0
- data/examples/simple_workflow/generator.rb +48 -0
- data/lib/mars/agent_step.rb +15 -0
- data/lib/mars/aggregator.rb +24 -0
- data/lib/mars/execution_context.rb +35 -0
- data/lib/mars/formatter.rb +13 -0
- data/lib/mars/gate.rb +52 -0
- data/lib/mars/hooks.rb +35 -0
- data/lib/mars/rendering/graph/agent_step.rb +18 -0
- data/lib/mars/rendering/graph/aggregator.rb +17 -0
- data/lib/mars/rendering/graph/base.rb +32 -0
- data/lib/mars/rendering/graph/builder.rb +43 -0
- data/lib/mars/rendering/graph/gate.rb +22 -0
- data/lib/mars/rendering/graph/node.rb +22 -0
- data/lib/mars/rendering/graph/parallel_workflow.rb +34 -0
- data/lib/mars/rendering/graph/runnable.rb +18 -0
- data/lib/mars/rendering/graph/sequential_workflow.rb +39 -0
- data/lib/mars/rendering/graph/subgraph.rb +17 -0
- data/lib/mars/rendering/graph.rb +16 -0
- data/lib/mars/rendering/html.rb +37 -0
- data/lib/mars/rendering/mermaid.rb +71 -0
- data/lib/mars/runnable.rb +26 -2
- data/lib/mars/version.rb +2 -2
- data/lib/mars/workflows/aggregate_error.rb +14 -0
- data/lib/mars/workflows/parallel.rb +61 -0
- data/lib/mars/workflows/sequential.rb +26 -8
- data/lib/mars.rb +12 -3
- data/lib/mars_rb.rb +3 -0
- data/sig/mars.rbs +1 -1
- metadata +81 -8
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "../../lib/mars"
|
|
5
|
+
|
|
6
|
+
# Define the LLMs
|
|
7
|
+
class Agent1 < MARS::AgentStep
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class Agent2 < MARS::AgentStep
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class Agent3 < MARS::AgentStep
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
class Agent4 < MARS::AgentStep
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Create the LLMs
|
|
20
|
+
llm1 = Agent1.new
|
|
21
|
+
llm2 = Agent2.new
|
|
22
|
+
llm3 = Agent3.new
|
|
23
|
+
llm4 = Agent4.new
|
|
24
|
+
|
|
25
|
+
# Create the failure workflow (LLM 3)
|
|
26
|
+
failure_workflow = MARS::Workflows::Sequential.new(
|
|
27
|
+
"Failure workflow",
|
|
28
|
+
steps: [llm4]
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Create the gate that decides between exit or continue
|
|
32
|
+
gate = MARS::Gate.new(
|
|
33
|
+
check: ->(input) { input[:result] },
|
|
34
|
+
fallbacks: {
|
|
35
|
+
failure: failure_workflow
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Create the main workflow: LLM 1 -> Gate
|
|
40
|
+
main_workflow = MARS::Workflows::Sequential.new(
|
|
41
|
+
"Main Pipeline",
|
|
42
|
+
steps: [llm1, gate, llm2, llm3]
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Generate and save the diagram
|
|
46
|
+
diagram = MARS::Rendering::Mermaid.new(main_workflow).render
|
|
47
|
+
File.write("examples/simple_workflow/diagram.md", diagram)
|
|
48
|
+
puts "Simple workflow diagram saved to: examples/simple_workflow/diagram.md"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
class AgentStep < Runnable
|
|
5
|
+
class << self
|
|
6
|
+
def agent(klass = nil)
|
|
7
|
+
klass ? @agent_class = klass : @agent_class
|
|
8
|
+
end
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def run(context)
|
|
12
|
+
self.class.agent.new.ask(context.current_input).content
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
class Aggregator < Runnable
|
|
5
|
+
attr_reader :operation
|
|
6
|
+
|
|
7
|
+
def initialize(name = "Aggregator", operation: nil, **kwargs)
|
|
8
|
+
super(name: name, **kwargs)
|
|
9
|
+
|
|
10
|
+
@operation = operation || ->(inputs) { inputs }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def run(context)
|
|
14
|
+
context = ensure_context(context)
|
|
15
|
+
operation.call(context.current_input)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def ensure_context(input)
|
|
21
|
+
input.is_a?(ExecutionContext) ? input : ExecutionContext.new(input: input)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
class ExecutionContext
|
|
5
|
+
attr_reader :outputs, :global_state
|
|
6
|
+
attr_accessor :current_input
|
|
7
|
+
|
|
8
|
+
def initialize(input: nil, global_state: {})
|
|
9
|
+
@current_input = input
|
|
10
|
+
@outputs = {}
|
|
11
|
+
@global_state = global_state
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def [](step_name)
|
|
15
|
+
outputs[step_name.to_sym]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def record(step_name, output)
|
|
19
|
+
@outputs[step_name.to_sym] = output
|
|
20
|
+
@current_input = output
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fork(input: current_input, state: {})
|
|
24
|
+
self.class.new(input: input, global_state: global_state.merge(state))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def merge(child_contexts)
|
|
28
|
+
child_contexts.each do |child|
|
|
29
|
+
@outputs.merge!(child.outputs)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
self
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/mars/gate.rb
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
class Gate < Runnable
|
|
5
|
+
class << self
|
|
6
|
+
def check(&block)
|
|
7
|
+
@check_block = block
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :check_block
|
|
11
|
+
|
|
12
|
+
def fallback(key, runnable)
|
|
13
|
+
fallbacks_map[key] = runnable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fallbacks_map
|
|
17
|
+
@fallbacks_map ||= {}
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(name = "Gate", check: nil, fallbacks: nil, **kwargs)
|
|
22
|
+
super(name: name, **kwargs)
|
|
23
|
+
|
|
24
|
+
@check = check || self.class.check_block
|
|
25
|
+
@fallbacks = fallbacks || self.class.fallbacks_map
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def run(context)
|
|
29
|
+
context = ensure_context(context)
|
|
30
|
+
result = check.call(context)
|
|
31
|
+
|
|
32
|
+
return context if result.nil? || result == :default
|
|
33
|
+
|
|
34
|
+
branch = fallbacks[result]
|
|
35
|
+
raise ArgumentError, "No fallback registered for #{result.inspect}" unless branch
|
|
36
|
+
|
|
37
|
+
resolve_branch(branch).run(context)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
attr_reader :check, :fallbacks
|
|
43
|
+
|
|
44
|
+
def resolve_branch(branch)
|
|
45
|
+
branch.is_a?(Class) ? branch.new : branch
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ensure_context(input)
|
|
49
|
+
input.is_a?(ExecutionContext) ? input : ExecutionContext.new(input: input)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/mars/hooks.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Hooks
|
|
5
|
+
def self.included(base)
|
|
6
|
+
base.extend(ClassMethods)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module ClassMethods
|
|
10
|
+
def before_run(&block)
|
|
11
|
+
before_run_hooks << block
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def after_run(&block)
|
|
15
|
+
after_run_hooks << block
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def before_run_hooks
|
|
19
|
+
@before_run_hooks ||= []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def after_run_hooks
|
|
23
|
+
@after_run_hooks ||= []
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run_before_hooks(context)
|
|
28
|
+
self.class.before_run_hooks.each { |hook| hook.call(context, self) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def run_after_hooks(context, result)
|
|
32
|
+
self.class.after_run_hooks.each { |hook| hook.call(context, result, self) }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module AgentStep
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_node(node_id, name, Node::STEP)
|
|
11
|
+
builder.add_edge(parent_id, node_id, value)
|
|
12
|
+
|
|
13
|
+
[node_id]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module Aggregator
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_edge(parent_id, node_id, value)
|
|
11
|
+
|
|
12
|
+
[node_id]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module Base
|
|
7
|
+
def build_graph(builder = MARS::Rendering::Graph::Builder.new)
|
|
8
|
+
builder.add_node("in", "In", Node::INPUT)
|
|
9
|
+
builder.add_node("out", "Out", Node::OUTPUT)
|
|
10
|
+
|
|
11
|
+
sink_nodes = to_graph(builder, parent_id: "in")
|
|
12
|
+
|
|
13
|
+
sink_nodes.each do |sink_node|
|
|
14
|
+
builder.add_edge(sink_node, "out")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
[builder.adjacency, builder.nodes, builder.subgraphs]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def node_id
|
|
21
|
+
@node_id ||= sanitize(name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def sanitize(name)
|
|
27
|
+
name.to_s.gsub(/[^a-zA-Z0-9]/, "_").downcase
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
class Builder
|
|
7
|
+
attr_reader :adjacency, :nodes, :subgraphs
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@adjacency = Hash.new { |h, k| h[k] = [] }
|
|
11
|
+
@nodes = {}
|
|
12
|
+
@subgraphs = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def add_edge(from, to, value = nil)
|
|
16
|
+
return unless from && to
|
|
17
|
+
|
|
18
|
+
# can we avoid visiting the node twice instead?
|
|
19
|
+
adjacency[from] << [to, value] unless adjacency[from].include?([to, value])
|
|
20
|
+
adjacency[to] = [] unless adjacency[to]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_node(id, value, type)
|
|
24
|
+
return if nodes.key?(id)
|
|
25
|
+
|
|
26
|
+
nodes[id] = Node.new(id, value, type)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def add_subgraph(id, name)
|
|
30
|
+
return if subgraphs.key?(id)
|
|
31
|
+
|
|
32
|
+
subgraphs[id] = Subgraph.new(id, name, [])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def add_node_to_subgraph(id, node_id)
|
|
36
|
+
return if subgraphs[id]&.nodes&.include?(node_id)
|
|
37
|
+
|
|
38
|
+
subgraphs[id].nodes << node_id
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module Gate
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_node(node_id, name, Node::GATE)
|
|
11
|
+
builder.add_edge(parent_id, node_id, value)
|
|
12
|
+
|
|
13
|
+
sink_nodes = fallbacks.map do |fallback_key, branch|
|
|
14
|
+
branch.to_graph(builder, parent_id: node_id, value: fallback_key)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sink_nodes.flatten
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
class Node
|
|
7
|
+
STEP = :step
|
|
8
|
+
OUTPUT = :output
|
|
9
|
+
INPUT = :input
|
|
10
|
+
GATE = :gate
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :name, :type
|
|
13
|
+
|
|
14
|
+
def initialize(id, name, type)
|
|
15
|
+
@id = id
|
|
16
|
+
@name = name
|
|
17
|
+
@type = type
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module ParallelWorkflow
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_subgraph(node_id, name) if steps.any?
|
|
11
|
+
builder.add_node(aggregator.node_id, aggregator.name, Node::STEP)
|
|
12
|
+
|
|
13
|
+
build_steps_graph(builder, parent_id, value)
|
|
14
|
+
|
|
15
|
+
[aggregator.node_id]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def build_steps_graph(builder, parent_id, value)
|
|
21
|
+
steps.each do |step|
|
|
22
|
+
sink_nodes = step.to_graph(builder, parent_id: parent_id, value: value)
|
|
23
|
+
|
|
24
|
+
builder.add_node_to_subgraph(node_id, step.node_id)
|
|
25
|
+
|
|
26
|
+
sink_nodes.each do |sink_node|
|
|
27
|
+
aggregator.to_graph(builder, parent_id: sink_node)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module Runnable
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_node(node_id, name, Node::STEP)
|
|
11
|
+
builder.add_edge(parent_id, node_id, value)
|
|
12
|
+
|
|
13
|
+
[node_id]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
module SequentialWorkflow
|
|
7
|
+
include Base
|
|
8
|
+
|
|
9
|
+
def to_graph(builder, parent_id: nil, value: nil)
|
|
10
|
+
builder.add_subgraph(node_id, name) if steps.any?
|
|
11
|
+
|
|
12
|
+
parent_id, value, sink_nodes = build_steps_graph(builder, parent_id, value)
|
|
13
|
+
|
|
14
|
+
builder.add_edge(parent_id, "out", value) if sink_nodes.empty?
|
|
15
|
+
|
|
16
|
+
sink_nodes.flatten
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_steps_graph(builder, parent_id, value)
|
|
22
|
+
sink_nodes = []
|
|
23
|
+
|
|
24
|
+
steps.each do |step|
|
|
25
|
+
sink_nodes = step.to_graph(builder, parent_id: parent_id, value: value)
|
|
26
|
+
value = nil # We don't want to pass the value to subsequent steps
|
|
27
|
+
parent_id = step.node_id
|
|
28
|
+
|
|
29
|
+
builder.add_node_to_subgraph(node_id, step.node_id)
|
|
30
|
+
|
|
31
|
+
sink_nodes.each { |sink_node| builder.add_node_to_subgraph(node_id, sink_node) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
[parent_id, value, sink_nodes]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
module Graph
|
|
6
|
+
def self.include_extensions
|
|
7
|
+
MARS::Runnable.include(Runnable)
|
|
8
|
+
MARS::AgentStep.include(AgentStep)
|
|
9
|
+
MARS::Gate.include(Gate)
|
|
10
|
+
MARS::Workflows::Sequential.include(SequentialWorkflow)
|
|
11
|
+
MARS::Workflows::Parallel.include(ParallelWorkflow)
|
|
12
|
+
MARS::Aggregator.include(Aggregator)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
class Html
|
|
6
|
+
MERMAID_CDN = "https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"
|
|
7
|
+
|
|
8
|
+
def initialize(obj)
|
|
9
|
+
@mermaid = Mermaid.new(obj)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render
|
|
13
|
+
diagram = @mermaid.graph_mermaid.join("\n")
|
|
14
|
+
direction = "LR"
|
|
15
|
+
|
|
16
|
+
<<~HTML
|
|
17
|
+
<!DOCTYPE html>
|
|
18
|
+
<html lang="en">
|
|
19
|
+
<head>
|
|
20
|
+
<meta charset="UTF-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
22
|
+
<title>MARS Workflow</title>
|
|
23
|
+
<script src="#{MERMAID_CDN}"></script>
|
|
24
|
+
</head>
|
|
25
|
+
<body>
|
|
26
|
+
<pre class="mermaid">
|
|
27
|
+
flowchart #{direction}
|
|
28
|
+
#{diagram}
|
|
29
|
+
</pre>
|
|
30
|
+
<script>mermaid.initialize({ startOnLoad: true });</script>
|
|
31
|
+
</body>
|
|
32
|
+
</html>
|
|
33
|
+
HTML
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Rendering
|
|
5
|
+
class Mermaid
|
|
6
|
+
attr_reader :obj, :graph, :nodes, :subgraphs
|
|
7
|
+
|
|
8
|
+
def initialize(obj)
|
|
9
|
+
@obj = obj
|
|
10
|
+
@graph, @nodes, @subgraphs = obj.build_graph
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render(options = {})
|
|
14
|
+
direction = options.fetch(:direction, "LR")
|
|
15
|
+
mermaid = graph_mermaid.join("\n")
|
|
16
|
+
|
|
17
|
+
<<~MERMAID
|
|
18
|
+
```mermaid
|
|
19
|
+
flowchart #{direction}
|
|
20
|
+
#{mermaid}
|
|
21
|
+
```
|
|
22
|
+
MERMAID
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def graph_mermaid
|
|
26
|
+
nodes_mermaid + subgraphs_mermaid + edges_mermaid
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def nodes_mermaid
|
|
30
|
+
nodes.keys.map { |node_id| "#{node_id}#{shape(node_id)}" }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def subgraphs_mermaid
|
|
34
|
+
subgraphs.values.reverse.map do |subgraph|
|
|
35
|
+
node_names = subgraph.nodes
|
|
36
|
+
"subgraph #{subgraph.id}[\"#{subgraph.name}\"]\n #{node_names.join("\n ")}\nend"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def edges_mermaid
|
|
41
|
+
edges = []
|
|
42
|
+
graph.each do |from, tos|
|
|
43
|
+
tos.each do |to|
|
|
44
|
+
node_id, value = to
|
|
45
|
+
edges << "#{from} -->#{edge_value(value)} #{node_id}"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
edges
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def shape(node_id)
|
|
52
|
+
node = nodes[node_id]
|
|
53
|
+
|
|
54
|
+
case node.type
|
|
55
|
+
when Graph::Node::INPUT, Graph::Node::OUTPUT
|
|
56
|
+
"((#{node.name}))"
|
|
57
|
+
when Graph::Node::GATE
|
|
58
|
+
"{#{node.name}}"
|
|
59
|
+
else
|
|
60
|
+
"[#{node.name}]"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def edge_value(value)
|
|
65
|
+
return "" unless value
|
|
66
|
+
|
|
67
|
+
"|#{value}|"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
data/lib/mars/runnable.rb
CHANGED
|
@@ -1,8 +1,32 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
module
|
|
3
|
+
module MARS
|
|
4
4
|
class Runnable
|
|
5
|
-
|
|
5
|
+
include Hooks
|
|
6
|
+
|
|
7
|
+
attr_reader :name, :formatter
|
|
8
|
+
attr_accessor :state
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def step_name
|
|
12
|
+
return @step_name if defined?(@step_name)
|
|
13
|
+
return unless name
|
|
14
|
+
|
|
15
|
+
name.split("::").last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def formatter(klass = nil)
|
|
19
|
+
klass ? @formatter_class = klass : @formatter_class
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(name: self.class.step_name, state: {}, formatter: nil)
|
|
24
|
+
@name = name
|
|
25
|
+
@state = state
|
|
26
|
+
@formatter = formatter || self.class.formatter&.new || Formatter.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def run(context)
|
|
6
30
|
raise NotImplementedError
|
|
7
31
|
end
|
|
8
32
|
end
|
data/lib/mars/version.rb
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MARS
|
|
4
|
+
module Workflows
|
|
5
|
+
class AggregateError < StandardError
|
|
6
|
+
attr_reader :errors
|
|
7
|
+
|
|
8
|
+
def initialize(errors)
|
|
9
|
+
@errors = errors
|
|
10
|
+
super(errors.map { |error| "#{error[:step_name]}: #{error[:error].message}" }.join("\n"))
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|