Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 105 additions & 42 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,100 +47,163 @@ Here's a simple example to get you started:
```ruby
require 'mars'

# Define agents
class Agent1 < MARS::Agent
# Define a RubyLLM agent
class ResolveCountryAgent < RubyLLM::Agent
instructions "Answer with only the country name."
end

class Agent2 < MARS::Agent
# Wrap the agent in a MARS step
class ResolveCountry < MARS::AgentStep
agent ResolveCountryAgent
end

class Agent3 < MARS::Agent
# Plain Ruby steps subclass MARS::Step
class ResearchFood < MARS::Step
def run(input, ctx: {})
result(value: "Typical food of #{input.value}")
end
end

# Create agents
agent1 = Agent1.new
agent2 = Agent2.new
agent3 = Agent3.new
class ResearchSports < MARS::Step
def run(input, ctx: {})
result(value: "Popular sports of #{input.value}")
end
end

class BuildReport < MARS::Aggregator
def run(results, ctx: {})
result(
value: {
country: ctx[:resolve_country].value,
food: results[0].value,
sports: results[1].value
}
)
end
end

# Create a sequential workflow
workflow = MARS::Workflows::Sequential.new(
"My First Workflow",
steps: [agent1, agent2, agent3]
"Country Report",
steps: [
ResolveCountry.new,
MARS::Workflows::Parallel.new(
"country_details",
steps: [
ResearchFood.new,
ResearchSports.new
],
aggregator: BuildReport.new
)
]
)

# Run the workflow
result = workflow.run("Your input here")
pp result.value
```

## Core Concepts

### Agents
### Steps

Agents are the basic building blocks of MARS. They represent individual units of work:
Every executable object in MARS responds to `run`. Plain Ruby steps subclass `MARS::Step`:

```ruby
class CustomAgent < MARS::Agent
def system_prompt
"You are a helpful assistant"
class NormalizeQuestion < MARS::Step
def run(input, ctx: {})
result(value: input.value.strip)
end
end

agent = CustomAgent.new(
options: { model: "gpt-4o" }
)
step = NormalizeQuestion.new
```

### Agent Steps

`MARS::AgentStep` is a thin wrapper around a configured `RubyLLM::Agent`:

```ruby
class CountryAgent < RubyLLM::Agent
instructions "Answer with only the country name."
end

class ResolveCountry < MARS::AgentStep
agent CountryAgent
end
```

### Sequential Workflows

Execute agents one after another, passing outputs as inputs:
Sequential workflows execute steps one after another, passing the previous output to the next step:

```ruby
sequential = MARS::Workflows::Sequential.new(
workflow = MARS::Workflows::Sequential.new(
"Sequential Pipeline",
steps: [agent1, agent2, agent3]
steps: [ResolveCountry.new, NormalizeQuestion.new]
)
```

### Parallel Workflows

Run multiple agents concurrently and aggregate their results:
Parallel workflows use ordered `steps:`. Without an aggregator they return an array of step outputs. With an aggregator they return a single value:

```ruby
aggregator = MARS::Aggregator.new(
"Results Aggregator",
operation: lambda { |results| results.join(", ") }
)
class BuildReport < MARS::Aggregator
def run(results, ctx: {})
result(
value: {
country: ctx[:resolve_country].value,
food: results[0].value,
sports: results[1].value
}
)
end
end

parallel = MARS::Workflows::Parallel.new(
"Parallel Pipeline",
steps: [agent1, agent2, agent3],
aggregator: aggregator
steps: [
ResearchFood.new,
ResearchSports.new
],
aggregator: BuildReport.new
)
```

### Gates

Gates act as guards that either let the workflow continue or divert to a fallback path:
Gates branch out of the happy path when a condition matches. If the `check` returns `nil`, the workflow continues normally. If it returns a branch key, the selected branch runs and the current workflow stops:

```ruby
class TooBroad < MARS::Step
def run(input, ctx: {})
result(
value: {
error: "Please ask about one country",
resolved_value: input.value
}
)
end
end

gate = MARS::Gate.new(
"Validation Gate",
check: ->(input) { :failure unless input[:score] > 0.5 },
fallbacks: {
failure: failure_workflow
"country_guard",
check: ->(input, _ctx) { :too_broad if input.value.split.size > 5 },
branches: {
too_broad: TooBroad.new
}
)
```

Control halt scope — `:local` (default) stops only the parent workflow, `:global` propagates to the root:
### Context And Result

Steps receive a shared `ctx:` object and workflows always return `MARS::Result`:

```ruby
gate = MARS::Gate.new(
"Critical Gate",
check: ->(input) { :error unless input[:valid] },
fallbacks: { error: error_workflow },
halt_scope: :global
)
result = workflow.run("Which is the largest country in Europe?")

result.value # final workflow output
result.outputs[:research_food] # output captured for a step
result.stopped? # whether a gate branched out of the happy path
```

### Visualization
Expand Down
110 changes: 66 additions & 44 deletions examples/complex_llm_workflow/generator.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "json"
require "net/http"
require "uri"
require_relative "../../lib/mars"

RubyLLM.configure do |config|
Expand Down Expand Up @@ -37,75 +40,94 @@ def execute(latitude:, longitude:)
end
end

# Define LLMs
class Agent1 < MARS::AgentStep
def system_prompt
"You are a helpful assistant that can answer questions.
When asked about a country, only answer with its name."
end
class ResolveCountryAgent < RubyLLM::Agent
instructions "Answer with only the country name."
end

class Agent2 < MARS::AgentStep
def system_prompt
"You are a helpful assistant that can answer questions and help with tasks.
Return information about the typical food of the country."
end
class TypicalFoodAgent < RubyLLM::Agent
instructions "Return information about the typical food of the country."
end

class Agent3 < MARS::AgentStep
def system_prompt
"You are a helpful assistant that can answer questions and help with tasks.
Return information about the popular sports of the country."
end
class PopularSportsAgent < RubyLLM::Agent
instructions "Return information about the popular sports of the country."
schema SportsSchema.new
end

def schema
SportsSchema.new
end
class CapitalWeatherAgent < RubyLLM::Agent
instructions "Return the current weather of the country's capital."
tools Weather.new
end

class Agent4 < MARS::AgentStep
def system_prompt
"You are a helpful assistant that can answer questions and help with tasks.
Return the current weather of the country's capital."
end
class ResolveCountry < MARS::AgentStep
agent ResolveCountryAgent
end

class TypicalFood < MARS::AgentStep
agent TypicalFoodAgent
end

def tools
[Weather.new]
class PopularSports < MARS::AgentStep
agent PopularSportsAgent
end

class CapitalWeather < MARS::AgentStep
agent CapitalWeatherAgent
end

class TooBroad < MARS::Step
def run(input, ctx: {})
result(
value: {
error: "Please ask about one country",
resolved_value: input.value
}
)
end
end

# Create the LLMs
llm1 = Agent1.new
llm2 = Agent2.new
llm3 = Agent3.new
llm4 = Agent4.new
class BuildReport < MARS::Aggregator
def run(results, ctx: {})
result(
value: {
country: ctx[:resolve_country].value,
food: results[0].value,
sports: results[1].value,
weather: results[2].value
}
)
end
end

parallel_workflow = MARS::Workflows::Parallel.new(
"Parallel workflow",
steps: [llm2, llm3, llm4]
)

error_workflow = MARS::Workflows::Sequential.new(
"Error workflow",
steps: []
steps: [
TypicalFood.new,
PopularSports.new,
CapitalWeather.new
],
aggregator: BuildReport.new
)

gate = MARS::Gate.new(
check: ->(input) { :failure unless input.split.length < 10 },
fallbacks: {
failure: error_workflow
"country_guard",
check: ->(input, _ctx) { :failure unless input.value.split.length < 10 },
branches: {
failure: TooBroad.new
}
)

sequential_workflow = MARS::Workflows::Sequential.new(
"Sequential workflow",
steps: [llm1, gate, parallel_workflow]
steps: [
ResolveCountry.new,
gate,
parallel_workflow
]
)

# Generate and save the diagram
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"

# Run the workflow
puts sequential_workflow.run("Which is the largest country in Europe?")
result = sequential_workflow.run("Which is the largest country in Europe?")
pp result.value
Loading
Loading