Examples¶
Complete working examples demonstrating RobotLab features.
Overview¶
These examples show how to use RobotLab for common scenarios, from simple chatbots to complex multi-robot systems.
Examples¶
| Example | Description |
|---|---|
| Basic Chat | Simple conversational robot |
| Multi-Robot Network | Customer service with routing |
| Tool Usage | External API integration |
| MCP Server | Connecting a robot to external MCP servers |
| Message Bus | Bidirectional robot communication with convergence |
| Spawning Robots | Dynamic specialist creation at runtime |
Rails example — see robot_lab-rails for a full Rails integration example.
Quick Links¶
Simple Examples¶
Advanced Examples¶
- Streaming Responses
- Persistent Conversations
- MCP Integration
- Message Bus Communication
- Spawning Robots
Hello World¶
require "robot_lab"
# Configuration is handled automatically via MywayConfig.
# Set API keys via environment variables:
# ROBOT_LAB_RUBY_LLM__ANTHROPIC_API_KEY=sk-ant-...
# Or via config files (~/.config/robot_lab/robot_lab.yml — the filename
# repeats the app name; ~/.config/robot_lab/config.yml is never read)
robot = RobotLab.build(
name: "greeter",
system_prompt: "You are a friendly greeter. Say hello warmly."
)
result = robot.run("Hi there!")
puts result.last_text_content
Robot with Tools¶
Give the LLM a fixed set of operations rather than an expression evaluator —
never eval a string the model produced.
class CalculatorTool < RubyLLM::Tool
description "Performs basic arithmetic operations"
param :operation, type: "string", desc: "add, subtract, multiply, or divide"
param :a, type: "number", desc: "First operand"
param :b, type: "number", desc: "Second operand"
def execute(operation:, a:, b:)
case operation
when "add" then a + b
when "subtract" then a - b
when "multiply" then a * b
when "divide" then a.to_f / b
else "Unknown operation: #{operation}"
end
end
end
robot = RobotLab.build(
name: "calculator",
system_prompt: "You help with calculations.",
local_tools: [CalculatorTool]
)
# tools: :inherit is REQUIRED — run() defaults to tools: :none
result = robot.run("What's 25 * 4?", tools: :inherit)
puts result.last_text_content
[!WARNING]
Robot#rundefaults totools: :noneandmcp: :none. A plainrobot.run("...")sends the LLM no tools at all, even whenlocal_tools:were attached at build time. Passtools: :inheriton the call that should be able to use them.For a standalone robot like this one, do not pass
tools: :inheritat build time — there the parent level is the global default (:none), so it produces an allowlist that matches nothing. Leavetools:unset in the constructor. (Inside a network the opposite holds: build-time:inheritis how a robot opts into the allowlist on the network'sconfig:. See Configuration.)
See examples/02_tools.rb
for a second tool definition (FortuneCookie) alongside the calculator.
Network with Routing¶
Routing is not configured declaratively — a robot performs it. Subclass
RobotLab::Robot, override #call, and activate one of the depends_on: :optional
tasks based on what the LLM returned.
class ClassifierRobot < RobotLab::Robot
def call(result)
run_context = extract_run_context(result)
message = run_context.delete(:message)
robot_result = run(message, **run_context)
new_result = result
.with_context(@name.to_sym, robot_result)
.continue(robot_result)
case robot_result.reply.to_s.strip.downcase
when /billing/ then new_result.activate(:billing)
when /technical/ then new_result.activate(:technical)
else new_result.activate(:general)
end
end
end
classifier = ClassifierRobot.new(
name: "classifier",
system_prompt: "Classify the request as BILLING, TECHNICAL, or GENERAL. Respond with only the category."
)
billing = RobotLab.build(name: "billing", system_prompt: "You handle billing questions.")
tech = RobotLab.build(name: "technical", system_prompt: "You handle technical issues.")
general = RobotLab.build(name: "general", system_prompt: "You handle everything else.")
network = RobotLab.create_network(name: "support") do
task :classifier, classifier, depends_on: :none
task :billing, billing, depends_on: :optional
task :technical, tech, depends_on: :optional
task :general, general, depends_on: :optional
end
result = network.run(message: "I was charged twice for my subscription")
# Access individual robot results via context
puts result.context[:classifier].last_text_content
puts result.value.last_text_content # the specialist that was activated
[!WARNING]
result.contextis keyed by the robot's name (with_context(@name.to_sym, ...)), not the task name, andactivate(:name)takes a task name. Keep the two identical — activating a task name that was never declared raisesArgumentErrorand aborts the run (Step :classifier attempted to activate unknown step :billing). The same happens for a task that was declared withoutdepends_on: :optional.
Full version: examples/03_network.rb.
Chaining Configuration¶
Robots support with_* methods that return self for chaining:
robot = RobotLab.build(name: "assistant")
.with_instructions("You are a helpful coding assistant.")
.with_temperature(0.3)
.with_model("gpt-4o")
result = robot.run("Explain Ruby blocks.")
puts result.last_text_content
[!NOTE] The chainable set is exactly what
RubyLLM::Chatexposes —with_context,with_headers,with_instructions,with_model,with_params,with_schema,with_temperature,with_thinking,with_tool,with_tools— plus RobotLab'swith_templateandwith_bus. There is nowith_max_tokens,with_top_p, orwith_top_k; use a constructor kwarg orwith_params(max_tokens: 2000).
Using Templates¶
Templates are .md files with optional YAML front matter, managed by prompt_manager.
The body is rendered with ERB — interpolate with <%= var %>.
# Template file: prompts/support.md
# ---
# model: claude-sonnet-4
# temperature: 0.5
# parameters:
# company_name: null
# ---
# You are a support assistant for <%= company_name %>.
robot = RobotLab.build(
name: "support",
template: :support,
context: { company_name: "Acme Corp" }
)
result = robot.run("How do I reset my password?")
puts result.last_text_content
[!NOTE]
{{ var }}is not interpolated — it passes through to the LLM verbatim. Of the LLM keys accepted in front matter, onlymodel:andtemperature:are actually applied to the chat.top_p,top_k,max_tokens,presence_penalty,frequency_penalty, andstopare parsed and then silently dropped; supply those as constructor kwargs or via aRunConfiginstead.
Running Examples¶
-
Install dependencies:
-
Set API key:
-
Run example:
Or use the provided rake tasks:
bundle exec rake examples:all # Run all examples
bundle exec rake examples:run[1] # Run specific example by number
Shared Example Setup (examples/common.rb)¶
Most numbered examples (01_*.rb through 35_*.rb) pull in a shared setup file:
The line sits below each example's header comment rather than at the very top of
the file, and three examples do without it entirely —
32_newsletter_reader.rb, 33_stock_generator.rb, and 33_stock_predictor.rb.
common.rb handles the shared boilerplate so individual examples stay focused:
LLMhash — frozen lookup of provider/model pairs accessible asLLM[:default],LLM[:local],LLM[:anthropic]. Each entry is aLlmConfig = Data.define(:provider, :model)value, so you access the model string asLLM[:default].model.RubyLLM.configure— sets a null logger andLLM[:default].modelas thedefault_model.RobotLab.configure— sets a null logger.- Output helpers —
banner(title),section(title),hr, andshow_code(ruby_string, label:)(Rouge-highlighted) for consistent terminal formatting.
Template Path via direnv¶
Examples that bundle their own prompts/ directory ship with a .envrc file:
examples/.envrc
examples/14_rusty_circuit/.envrc
examples/15_memory_network_and_bus/.envrc
examples/16_writers_room/.envrc
Each sets ROBOT_LAB_TEMPLATE_PATH to the local prompts/ directory when direnv is active. common.rb also sets this variable as a fallback if direnv has not loaded the .envrc:
This means examples work correctly whether you run them from the project root with rake tasks or directly from inside the example's own directory.
Message Bus¶
Robots can communicate bidirectionally via a message bus, enabling convergence loops and negotiation patterns. This example demonstrates a comedy critic tasking a comedian to generate jokes until one passes:
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
require "robot_lab"
MAX_ATTEMPTS = 5
class Comedian < RobotLab::Robot
TEMP_START = 0.2
TEMP_STEP = 0.2
def initialize(bus:)
super(name: "bob", template: :comedian, bus: bus, temperature: TEMP_START)
@attempts = 0
on_message do |message|
@attempts += 1
temp = [TEMP_START + TEMP_STEP * (@attempts - 1), 1.0].min
with_temperature(temp)
joke = run(message.content.to_s).reply.strip
send_reply(to: message.from.to_sym, content: joke, in_reply_to: message.key)
end
end
attr_reader :attempts
end
class ComedyCritic < RobotLab::Robot
def initialize(bus:)
super(name: "alice", template: :comedy_critic, bus: bus)
@accepted = false
@rounds = 0
on_message do |message|
@rounds += 1
verdict = run("Evaluate this joke:\n\n#{message.content}").reply.strip
@accepted = verdict.start_with?("FUNNY")
# The @rounds guard is what terminates the loop — without it the critic
# keeps sending Bob back forever.
send_message(to: :bob, content: "Not funny enough. Try again.") unless @accepted || @rounds >= MAX_ATTEMPTS
end
end
attr_reader :accepted
end
bus = TypedBus::MessageBus.new
bob = Comedian.new(bus: bus)
alice = ComedyCritic.new(bus: bus)
alice.send_message(to: :bob, content: "Tell me a funny robot joke.")
puts "Attempts: #{bob.attempts} / #{MAX_ATTEMPTS}"
puts "Accepted: #{alice.accepted}"
Key patterns demonstrated:
- Robot subclasses with templates for prompt management
- Auto-ack via 1-arg
on_messageblocks send_reply(to:, content:, in_reply_to:)for correlated responses- Temperature ramping (0.2 → 1.0) for increasing creativity
- Convergence loop that terminates when the critic approves or
MAX_ATTEMPTSis reached
[!WARNING]
MAX_ATTEMPTSonly bounds the loop because the critic checks it before sending Bob back. Declaring the constant without testing it leaves the two robots messaging each other indefinitely.
Run: bundle exec ruby examples/12_message_bus.rb
Spawning Robots¶
Robots can create new specialist robots at runtime using spawn. A dispatcher receives questions, decides what kind of specialist is needed, and spawns one on the fly. The bus is created lazily — no explicit setup required:
ENV['ROBOT_LAB_TEMPLATE_PATH'] ||= File.join(__dir__, "prompts")
require "robot_lab"
QUESTIONS = [
"Why did the Roman Empire fall?",
"Write a haiku about recursion.",
"What is the square root of 144?",
].freeze
class Dispatcher < RobotLab::Robot
attr_reader :spawned
def initialize(bus: nil)
super(name: "dispatcher", template: :dispatcher, bus: bus)
@spawned = {}
@pending = {}
on_message do |message|
puts " Dispatcher <- :#{message.from} replied"
puts " | #{message.content.to_s.lines.first&.strip}"
@pending.delete(message.from)
end
end
def dispatch(question)
plan = run(question).last_text_content.strip
role, instruction = plan.split("\n", 2)
role = role.strip.downcase.gsub(/\s+/, "_")
instruction = instruction&.strip || "You are a helpful #{role}."
specialist = @spawned[role] ||= spawn(
name: role,
system_prompt: instruction
)
@pending[role] = question
specialist.send_message(to: :dispatcher, content:
specialist.run(question).last_text_content.strip
)
end
end
dispatcher = Dispatcher.new
QUESTIONS.each_with_index do |question, i|
puts "\nQuestion #{i + 1}: #{question}"
dispatcher.dispatch(question)
end
puts "\nSpecialists spawned: #{dispatcher.spawned.keys.join(', ')}"
Key patterns demonstrated:
spawnfor dynamic robot creation (bus created lazily)on_messagefor reply handling- LLM-driven delegation — the dispatcher asks its LLM what specialist to create
- Specialist reuse — spawned robots are cached and reused across questions
Run: bundle exec ruby examples/13_spawn.rb