diff --git a/.rubocop.yml b/.rubocop.yml index ebb2677..9a742e3 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -34,5 +34,8 @@ RSpec/ExampleLength: RSpec/MultipleExpectations: Enabled: false +RSpec/MultipleMemoizedHelpers: + Enabled: false + RSpec/VerifiedDoubleReference: Enabled: false diff --git a/README.md b/README.md index f0b063e..0c14456 100644 --- a/README.md +++ b/README.md @@ -47,10 +47,20 @@ Here's a simple example to get you started: ```ruby require 'mars' +# Define agents +class Agent1 < Mars::Agent +end + +class Agent2 < Mars::Agent +end + +class Agent3 < Mars::Agent +end + # Create agents -agent1 = Mars::Agent.new(name: "Agent 1") -agent2 = Mars::Agent.new(name: "Agent 2") -agent3 = Mars::Agent.new(name: "Agent 3") +agent1 = Agent1.new +agent2 = Agent2.new +agent3 = Agent3.new # Create a sequential workflow workflow = Mars::Workflows::Sequential.new( @@ -69,9 +79,13 @@ result = workflow.run("Your input here") Agents are the basic building blocks of MARS. They represent individual units of work: ```ruby -agent = Mars::Agent.new( - name: "My Agent", - instructions: "You are a helpful assistant", +class CustomAgent < Mars::Agent + def system_prompt + "You are a helpful assistant" + end +end + +agent = CustomAgent.new( options: { model: "gpt-4o" } ) ``` @@ -110,7 +124,7 @@ Create conditional branching in your workflows: ```ruby gate = Mars::Gate.new( - name: "Decision Gate", + "Decision Gate", condition: ->(input) { input[:score] > 0.5 ? :success : :failure }, branches: { success: success_workflow, diff --git a/examples/complex_llm_workflow/diagram.md b/examples/complex_llm_workflow/diagram.md index 3e86da4..700e749 100644 --- a/examples/complex_llm_workflow/diagram.md +++ b/examples/complex_llm_workflow/diagram.md @@ -2,20 +2,20 @@ flowchart LR in((In)) out((Out)) -llm_1[LLM 1] +agent1[Agent1] gate{Gate} parallel_workflow_aggregator[Parallel workflow Aggregator] -llm_2[LLM 2] -llm_3[LLM 3] -llm_4[LLM 4] -in --> llm_1 -llm_1 --> gate -gate -->|success| llm_2 -gate -->|success| llm_3 -gate -->|success| llm_4 +agent2[Agent2] +agent3[Agent3] +agent4[Agent4] +in --> agent1 +agent1 --> gate +gate -->|success| agent2 +gate -->|success| agent3 +gate -->|success| agent4 gate -->|default| out -llm_2 --> parallel_workflow_aggregator +agent2 --> parallel_workflow_aggregator parallel_workflow_aggregator --> out -llm_3 --> parallel_workflow_aggregator -llm_4 --> parallel_workflow_aggregator +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 ce1b5e6..a7d18a9 100755 --- a/examples/complex_llm_workflow/generator.rb +++ b/examples/complex_llm_workflow/generator.rb @@ -19,8 +19,6 @@ class SportsSchema < RubyLLM::Schema end end -sports_schema = SportsSchema.new - # Define weather tool class Weather < RubyLLM::Tool description "Gets current weather for a location" @@ -39,26 +37,48 @@ def execute(latitude:, longitude:) end end -weather_tool = Weather.new +# Define LLMs +class Agent1 < Mars::Agent + def system_prompt + "You are a helpful assistant that can answer questions. + When asked about a country, only answer with its name." + end +end -# Create the LLMs -llm1 = Mars::Agent.new( - name: "LLM 1", options: { model: "gpt-4o" }, - instructions: "You are a helpful assistant that can answer questions. - When asked about a country, only answer with its name." -) +class Agent2 < Mars::Agent + 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 +end + +class Agent3 < Mars::Agent + 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 + + def schema + SportsSchema.new + end +end -llm2 = Mars::Agent.new(name: "LLM 2", options: { model: "gpt-4o" }, - instructions: "You are a helpful assistant that can answer questions and help with tasks. - Return information about the typical food of the country.") +class Agent4 < Mars::Agent + 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 -llm3 = Mars::Agent.new(name: "LLM 3", options: { model: "gpt-4o" }, schema: sports_schema, - instructions: "You are a helpful assistant that can answer questions and help with tasks. - Return information about the popular sports of the country.") + def tools + [Weather.new] + end +end -llm4 = Mars::Agent.new(name: "LLM 4", options: { model: "gpt-4o" }, tools: [weather_tool], - instructions: "You are a helpful assistant that can answer questions and help with tasks. - Return the current weather of the country's capital.") +# Create the LLMs +llm1 = Agent1.new(options: { model: "gpt-4o" }) +llm2 = Agent2.new(options: { model: "gpt-4o" }) +llm3 = Agent3.new(options: { model: "gpt-4o" }) +llm4 = Agent4.new(options: { model: "gpt-4o" }) parallel_workflow = Mars::Workflows::Parallel.new( "Parallel workflow", @@ -66,7 +86,6 @@ def execute(latitude:, longitude:) ) gate = Mars::Gate.new( - name: "Gate", condition: ->(input) { input.split.length < 10 ? :success : :error }, branches: { success: parallel_workflow @@ -84,4 +103,4 @@ def execute(latitude:, longitude:) 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 South America?") +puts sequential_workflow.run("Which is the largest country in Europe?") diff --git a/examples/complex_workflow/diagram.md b/examples/complex_workflow/diagram.md index 32e44d7..ba7d8b5 100644 --- a/examples/complex_workflow/diagram.md +++ b/examples/complex_workflow/diagram.md @@ -2,28 +2,28 @@ flowchart LR in((In)) out((Out)) -llm_1[LLM 1] +agent1[Agent1] gate{Gate} parallel_workflow_2_aggregator[Parallel workflow 2 Aggregator] -llm_4[LLM 4] +agent4[Agent4] parallel_workflow_aggregator[Parallel workflow Aggregator] -llm_2[LLM 2] -llm_3[LLM 3] -llm_5[LLM 5] -in --> llm_1 -llm_1 --> gate -gate -->|success| llm_4 -gate -->|success| llm_5 -gate -->|warning| llm_4 -gate -->|error| llm_2 -gate -->|error| llm_3 +agent2[Agent2] +agent3[Agent3] +agent5[Agent5] +in --> agent1 +agent1 --> gate +gate -->|success| agent4 +gate -->|success| agent5 +gate -->|warning| agent4 +gate -->|error| agent2 +gate -->|error| agent3 gate -->|default| out -llm_4 --> llm_2 -llm_4 --> llm_3 -llm_2 --> parallel_workflow_aggregator +agent4 --> agent2 +agent4 --> agent3 +agent2 --> parallel_workflow_aggregator parallel_workflow_aggregator --> parallel_workflow_2_aggregator parallel_workflow_aggregator --> out -llm_3 --> parallel_workflow_aggregator +agent3 --> parallel_workflow_aggregator parallel_workflow_2_aggregator --> out -llm_5 --> parallel_workflow_2_aggregator +agent5 --> parallel_workflow_2_aggregator ``` diff --git a/examples/complex_workflow/generator.rb b/examples/complex_workflow/generator.rb index 1acdd55..3a8bb2f 100755 --- a/examples/complex_workflow/generator.rb +++ b/examples/complex_workflow/generator.rb @@ -3,16 +3,28 @@ require_relative "../../lib/mars" -# Create the LLMs -llm1 = Mars::Agent.new(name: "LLM 1") +# Define LLMs +class Agent1 < Mars::Agent +end + +class Agent2 < Mars::Agent +end -llm2 = Mars::Agent.new(name: "LLM 2") +class Agent3 < Mars::Agent +end -llm3 = Mars::Agent.new(name: "LLM 3") +class Agent4 < Mars::Agent +end -llm4 = Mars::Agent.new(name: "LLM 4") +class Agent5 < Mars::Agent +end -llm5 = Mars::Agent.new(name: "LLM 5") +# Create the LLMs +llm1 = Agent1.new +llm2 = Agent2.new +llm3 = Agent3.new +llm4 = Agent4.new +llm5 = Agent5.new # Create a parallel workflow (LLM 2 x LLM 3) parallel_workflow = Mars::Workflows::Parallel.new( @@ -34,7 +46,6 @@ # Create the gate that decides between exit or continue gate = Mars::Gate.new( - name: "Gate", condition: ->(input) { input[:result] }, branches: { success: parallel_workflow2, diff --git a/examples/parallel_workflow/diagram.md b/examples/parallel_workflow/diagram.md index 0efc8cb..4a6b8a2 100644 --- a/examples/parallel_workflow/diagram.md +++ b/examples/parallel_workflow/diagram.md @@ -3,14 +3,14 @@ flowchart LR in((In)) out((Out)) aggregator[Aggregator] -llm_1[LLM 1] -llm_2[LLM 2] -llm_3[LLM 3] -in --> llm_1 -in --> llm_2 -in --> llm_3 -llm_1 --> aggregator +agent1[Agent1] +agent2[Agent2] +agent3[Agent3] +in --> agent1 +in --> agent2 +in --> agent3 +agent1 --> aggregator aggregator --> out -llm_2 --> aggregator -llm_3 --> aggregator +agent2 --> aggregator +agent3 --> aggregator ``` diff --git a/examples/parallel_workflow/generator.rb b/examples/parallel_workflow/generator.rb index 106a385..2beec11 100755 --- a/examples/parallel_workflow/generator.rb +++ b/examples/parallel_workflow/generator.rb @@ -3,12 +3,20 @@ require_relative "../../lib/mars" -# Create the LLMs -llm1 = Mars::Agent.new(name: "LLM 1") +# Define the LLMs +class Agent1 < Mars::Agent +end + +class Agent2 < Mars::Agent +end -llm2 = Mars::Agent.new(name: "LLM 2") +class Agent3 < Mars::Agent +end -llm3 = Mars::Agent.new(name: "LLM 3") +# Create the LLMs +llm1 = Agent1.new +llm2 = Agent2.new +llm3 = Agent3.new aggregator = Mars::Aggregator.new("Aggregator", operation: lambda(&:sum)) diff --git a/examples/simple_workflow/diagram.md b/examples/simple_workflow/diagram.md index a2ccca5..4bab18d 100644 --- a/examples/simple_workflow/diagram.md +++ b/examples/simple_workflow/diagram.md @@ -2,14 +2,14 @@ flowchart LR in((In)) out((Out)) -llm_1[LLM 1] +agent1[Agent1] gate{Gate} -llm_2[LLM 2] -llm_3[LLM 3] -in --> llm_1 -llm_1 --> gate -gate -->|success| llm_2 +agent2[Agent2] +agent3[Agent3] +in --> agent1 +agent1 --> gate +gate -->|success| agent2 gate -->|default| out -llm_2 --> llm_3 -llm_3 --> out +agent2 --> agent3 +agent3 --> out ``` diff --git a/examples/simple_workflow/generator.rb b/examples/simple_workflow/generator.rb index f67a681..6c20731 100755 --- a/examples/simple_workflow/generator.rb +++ b/examples/simple_workflow/generator.rb @@ -3,12 +3,20 @@ require_relative "../../lib/mars" -# Create the LLMs -llm1 = Mars::Agent.new(name: "LLM 1") +# Define the LLMs +class Agent1 < Mars::Agent +end + +class Agent2 < Mars::Agent +end -llm2 = Mars::Agent.new(name: "LLM 2") +class Agent3 < Mars::Agent +end -llm3 = Mars::Agent.new(name: "LLM 3") +# Create the LLMs +llm1 = Agent1.new +llm2 = Agent2.new +llm3 = Agent3.new # Create the success workflow (LLM 2 -> LLM 3) success_workflow = Mars::Workflows::Sequential.new( @@ -18,7 +26,6 @@ # Create the gate that decides between exit or continue gate = Mars::Gate.new( - name: "Gate", condition: ->(input) { input[:result] }, branches: { success: success_workflow diff --git a/lib/mars/agent.rb b/lib/mars/agent.rb index ad3add0..db80a0c 100644 --- a/lib/mars/agent.rb +++ b/lib/mars/agent.rb @@ -2,16 +2,10 @@ module Mars class Agent < Runnable - attr_reader :name - - def initialize(name:, options: {}, tools: [], schema: nil, instructions: nil, **kwargs) + def initialize(options: {}, **kwargs) super(**kwargs) - @name = name - @tools = Array(tools) - @schema = schema @options = options - @instructions = instructions end def run(input) @@ -22,11 +16,11 @@ def run(input) private - attr_reader :tools, :schema, :options, :instructions + attr_reader :options def chat @chat ||= RubyLLM::Chat.new(**options) - .with_instructions(instructions) + .with_instructions(system_prompt) .with_tools(*tools) .with_schema(schema) end @@ -38,5 +32,17 @@ def before_run(input) def after_run(response) response end + + def system_prompt + nil + end + + def tools + [] + end + + def schema + nil + end end end diff --git a/lib/mars/gate.rb b/lib/mars/gate.rb index c975ff2..71ab8ea 100644 --- a/lib/mars/gate.rb +++ b/lib/mars/gate.rb @@ -4,7 +4,7 @@ module Mars class Gate < Runnable attr_reader :name - def initialize(name:, condition:, branches:, **kwargs) + def initialize(name = "Gate", condition:, branches:, **kwargs) super(**kwargs) @name = name diff --git a/lib/mars/rendering/graph/agent.rb b/lib/mars/rendering/graph/agent.rb index e4fd77a..a0b0e34 100644 --- a/lib/mars/rendering/graph/agent.rb +++ b/lib/mars/rendering/graph/agent.rb @@ -12,6 +12,10 @@ def to_graph(builder, parent_id: nil, value: nil) [node_id] end + + def name + self.class.name + end end end end diff --git a/spec/mars/agent_spec.rb b/spec/mars/agent_spec.rb index c59f922..493d00f 100644 --- a/spec/mars/agent_spec.rb +++ b/spec/mars/agent_spec.rb @@ -4,7 +4,7 @@ describe "#run" do subject(:run_agent) { agent.run("input text") } - let(:agent) { described_class.new(name: "TestAgent", options: { model: "test-model" }) } + let(:agent) { described_class.new(options: { model: "test-model" }) } let(:mock_chat_instance) do instance_double("RubyLLM::Chat").tap do |mock| allow(mock).to receive_messages(with_tools: mock, with_schema: mock, with_instructions: mock, @@ -26,7 +26,15 @@ context "when tools are provided" do let(:tools) { [proc { "tool1" }, proc { "tool2" }] } - let(:agent) { described_class.new(name: "TestAgent", tools: tools) } + let(:agent_class) do + Class.new(described_class) do + def tools + [proc { "tool1" }, proc { "tool2" }] + end + end + end + + let(:agent) { agent_class.new } it "configures chat with tools" do run_agent @@ -36,13 +44,20 @@ end context "when schema is provided" do - let(:schema) { { type: "object" } } - let(:agent) { described_class.new(name: "TestAgent", schema: schema) } + let(:agent_class) do + Class.new(described_class) do + def schema + { type: "object" } + end + end + end + + let(:agent) { agent_class.new } it "configures chat with schema" do run_agent - expect(mock_chat_instance).to have_received(:with_schema).with(schema) + expect(mock_chat_instance).to have_received(:with_schema).with({ type: "object" }) end end end diff --git a/spec/mars/gate_spec.rb b/spec/mars/gate_spec.rb index b133277..9ac680c 100644 --- a/spec/mars/gate_spec.rb +++ b/spec/mars/gate_spec.rb @@ -7,7 +7,7 @@ let(:true_branch) { instance_spy(Mars::Runnable) } let(:false_branch) { instance_spy(Mars::Runnable) } let(:branches) { { true => true_branch, false => false_branch } } - let(:gate) { described_class.new(name: "TestGate", condition: condition, branches: branches) } + let(:gate) { described_class.new("TestGate", condition: condition, branches: branches) } it "executes true branch when condition is true" do allow(true_branch).to receive(:run).with(10).and_return("true result") @@ -33,7 +33,7 @@ let(:long_branch) { instance_spy(Mars::Runnable) } let(:short_branch) { instance_spy(Mars::Runnable) } let(:branches) { { "long" => long_branch, "short" => short_branch } } - let(:gate) { described_class.new(name: "LengthGate", condition: condition, branches: branches) } + let(:gate) { described_class.new("LengthGate", condition: condition, branches: branches) } it "routes to correct branch based on string result" do allow(long_branch).to receive(:run).with("longstring").and_return("long result") @@ -58,7 +58,7 @@ let(:condition) { ->(input) { input > 5 ? "high" : "low" } } let(:high_branch) { instance_spy(Mars::Runnable) } let(:branches) { { "high" => high_branch } } - let(:gate) { described_class.new(name: "TestGate", condition: condition, branches: branches) } + let(:gate) { described_class.new("TestGate", condition: condition, branches: branches) } it "executes defined branch when condition matches" do allow(high_branch).to receive(:run).with(10).and_return("high result") @@ -93,7 +93,7 @@ let(:branches) { { "low" => low_branch, "medium" => medium_branch, "high" => high_branch } } it "routes to low branch" do - gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + gate = described_class.new("RangeGate", condition: condition, branches: branches) allow(low_branch).to receive(:run).with(5).and_return("low result") result = gate.run(5) @@ -103,7 +103,7 @@ end it "routes to medium branch" do - gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + gate = described_class.new("RangeGate", condition: condition, branches: branches) allow(medium_branch).to receive(:run).with(25).and_return("medium result") result = gate.run(25) @@ -113,7 +113,7 @@ end it "routes to high branch" do - gate = described_class.new(name: "RangeGate", condition: condition, branches: branches) + gate = described_class.new("RangeGate", condition: condition, branches: branches) allow(high_branch).to receive(:run).with(100).and_return("high result") result = gate.run(100)