Tool Usage¶
Robots with external capabilities through tools.
Overview¶
This example demonstrates how to give robots access to external systems through tools. Tools are defined as RubyLLM::Tool subclasses or RobotLab::Tool instances and passed to robots via the local_tools: parameter.
RubyLLM::Tool Subclass Pattern¶
The primary way to define tools is by subclassing RubyLLM::Tool:
#!/usr/bin/env ruby
# examples/tool_usage.rb
require "bundler/setup"
require "robot_lab"
# Define tools as RubyLLM::Tool subclasses
class Calculator < RubyLLM::Tool
description "Performs basic arithmetic operations"
param :operation,
type: "string",
desc: "The operation to perform (add, subtract, multiply, 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
class FortuneCookie < RubyLLM::Tool
description "Get a fortune cookie message with wisdom and lucky numbers"
param :category,
type: "string",
desc: "The category of fortune (wisdom, love, career, adventure)"
FORTUNES = {
"wisdom" => [
"The obstacle in the path becomes the path.",
"A journey of a thousand miles begins with a single step."
],
"career" => [
"Opportunity dances with those already on the dance floor.",
"Your work is your signature. Sign it with excellence."
]
}.freeze
def execute(category:)
{
category: category,
fortune: FORTUNES.fetch(category, FORTUNES["wisdom"]).sample,
lucky_numbers: Array.new(6) { rand(1..49) }.sort
}
end
end
# Create robot with tools via local_tools
robot = RobotLab.build(
name: "assistant",
system_prompt: "You help with math and dispense fortune cookies.",
local_tools: [Calculator, FortuneCookie],
model: "claude-sonnet-4"
)
# Run the robot
result = robot.run("What is 15 multiplied by 7? Also, give me a career fortune.")
# Display results
puts "Response: #{result.last_text_content}"
if result.tool_calls.any?
puts "\nTool calls made:"
result.tool_calls.each do |tc|
tool_info = tc.respond_to?(:tool) ? tc.tool : tc
puts " #{tool_info[:name] || tool_info}"
end
end
RobotLab::Tool.create Pattern¶
For simpler tools that do not need their own class, use RobotLab::Tool.create:
require "robot_lab"
# Define an inline tool
get_time = RobotLab::Tool.create(
name: "get_time",
description: "Get the current time"
) { |_args| Time.now.to_s }
# Define a tool with parameters (JSON Schema)
weather_tool = RobotLab::Tool.create(
name: "get_weather",
description: "Get weather for a city",
parameters: {
type: "object",
properties: {
city: { type: "string", description: "City name" }
},
required: ["city"]
}
) { |args| { city: args[:city], temperature: "72F", condition: "sunny" } }
robot = RobotLab.build(
name: "weather_bot",
system_prompt: "You provide weather and time information.",
local_tools: [get_time, weather_tool],
model: "claude-sonnet-4"
)
result = robot.run("What time is it and what's the weather in New York?")
puts result.last_text_content
Weather API Integration¶
#!/usr/bin/env ruby
# examples/weather_assistant.rb
require "bundler/setup"
require "robot_lab"
require "http"
require "json"
class GetWeather < RubyLLM::Tool
description "Get current weather for a city"
param :city,
type: "string",
desc: "City name (e.g., 'New York', 'London')"
def execute(city:)
response = HTTP.get(
"https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
)
if response.status.success?
data = JSON.parse(response.body)
current = data["current_condition"].first
{
city: city,
temperature_f: current["temp_F"],
temperature_c: current["temp_C"],
condition: current["weatherDesc"].first["value"],
humidity: current["humidity"],
wind_mph: current["windspeedMiles"]
}
else
{ error: "Could not fetch weather for #{city}" }
end
rescue HTTP::Error => e
{ error: "Network error: #{e.message}" }
end
end
class GetForecast < RubyLLM::Tool
description "Get weather forecast for upcoming days"
param :city, type: "string", desc: "City name"
param :days, type: "integer", desc: "Number of days (default 3)"
def execute(city:, days: 3)
response = HTTP.get(
"https://wttr.in/#{URI.encode_www_form_component(city)}?format=j1"
)
if response.status.success?
data = JSON.parse(response.body)
data["weather"].take(days).map do |day|
{
date: day["date"],
high_f: day["maxtempF"],
low_f: day["mintempF"],
condition: day["hourly"].first["weatherDesc"].first["value"]
}
end
else
{ error: "Could not fetch forecast" }
end
rescue HTTP::Error => e
{ error: "Network error: #{e.message}" }
end
end
# Create weather assistant
weather_bot = RobotLab.build(
name: "weather_assistant",
description: "Provides weather information",
system_prompt: <<~PROMPT,
You are a helpful weather assistant. Use your tools to look up weather.
Always provide temperatures in both Fahrenheit and Celsius.
Include relevant advice based on conditions (umbrella, sunscreen, etc).
PROMPT
local_tools: [GetWeather, GetForecast],
model: "claude-sonnet-4"
)
# Interactive session
puts "Weather Assistant (type 'quit' to exit)"
puts "-" * 50
loop do
print "\nYou: "
input = gets&.chomp
break if input.nil? || input.downcase == "quit"
next if input.empty?
result = weather_bot.run(input)
puts "\nAssistant: #{result.last_text_content}"
end
puts "\nGoodbye!"
Database Integration¶
# examples/order_assistant.rb
require "robot_lab"
# Mock database
ORDERS = {
"ORD001" => { id: "ORD001", status: "shipped", items: ["Widget"], total: 29.99 },
"ORD002" => { id: "ORD002", status: "processing", items: ["Gadget", "Gizmo"], total: 89.99 }
}
class GetOrder < RubyLLM::Tool
description "Look up an order by ID"
param :order_id, type: "string", desc: "The order ID to look up"
def execute(order_id:)
order = ORDERS[order_id.upcase]
order || { error: "Order not found" }
end
end
class ListOrders < RubyLLM::Tool
description "List recent orders"
param :limit, type: "integer", desc: "Maximum number of orders to return"
def execute(limit: 5)
ORDERS.values.take(limit)
end
end
class CancelOrder < RubyLLM::Tool
description "Cancel an order"
param :order_id, type: "string", desc: "The order ID to cancel"
param :reason, type: "string", desc: "Reason for cancellation"
def execute(order_id:, reason: nil)
order = ORDERS[order_id.upcase]
if order.nil?
{ success: false, error: "Order not found" }
elsif order[:status] == "shipped"
{ success: false, error: "Cannot cancel shipped orders" }
else
order[:status] = "cancelled"
order[:cancel_reason] = reason
{ success: true, message: "Order #{order_id} cancelled" }
end
end
end
order_bot = RobotLab.build(
name: "order_assistant",
system_prompt: "You help customers check and manage their orders.",
local_tools: [GetOrder, ListOrders, CancelOrder],
model: "claude-sonnet-4"
)
# Run with a question
result = order_bot.run("What's the status of order ORD001?")
puts result.last_text_content
Tool Call Callbacks¶
Use on_tool_call and on_tool_result to monitor tool execution:
robot = RobotLab.build(
name: "monitored_bot",
system_prompt: "You help with calculations.",
local_tools: [Calculator],
model: "claude-sonnet-4",
on_tool_call: ->(tool_call) {
puts "[Tool Call] #{tool_call.name}: #{tool_call.arguments}"
},
on_tool_result: ->(tool_call, result) {
puts "[Tool Result] #{tool_call.name}: #{result}"
}
)
result = robot.run("What is 42 * 17?")
Running¶
export ANTHROPIC_API_KEY="your-key"
# Tool usage example
ruby examples/tool_usage.rb
# Weather assistant
ruby examples/weather_assistant.rb
# Order lookup
ruby examples/order_assistant.rb
Interactive User Input¶
Use the built-in RobotLab::AskUser tool to let robots ask the user questions during execution:
require "robot_lab"
robot = RobotLab.build(
name: "interviewer",
system_prompt: <<~PROMPT,
You are a project setup assistant. Interview the user to understand their
needs, then summarize the project plan. Use the ask_user tool to gather
information one question at a time.
PROMPT
local_tools: [RobotLab::AskUser],
model: "claude-sonnet-4"
)
result = robot.run("Help me plan a new web application")
puts "\nProject Plan:\n#{result.last_text_content}"
The robot will ask questions interactively:
[interviewer] What programming language would you like to use?
1. Ruby
2. Python
3. TypeScript
> 1
[interviewer] Will you need a database?
> [yes]
[interviewer] What's the main purpose of the application?
> Customer support portal
For testing, inject StringIO objects:
Key Concepts¶
- RubyLLM::Tool subclass: Define a class with
description,param, andexecutemethod - RobotLab::Tool subclass: Same DSL plus
robotaccessor for robot-aware tools - RobotLab::Tool.create: Use
RobotLab::Tool.create(name:, description:, &block)for dynamic tools - Built-in tools:
RobotLab::AskUserfor interactive terminal input - local_tools: Pass tool classes/instances via
local_tools:parameter toRobotLab.buildorRobot.new - Frontmatter tools: Declare tool class names in template YAML front matter (
tools: [Calculator]) for self-contained templates - Error Handling: Return error hashes (e.g.,
{ error: "message" }) for graceful failures - Callbacks: Use
on_tool_call:andon_tool_result:for monitoring - Result Access: Check
result.tool_callsfor tool call history,result.last_text_contentfor the final response