-
Notifications
You must be signed in to change notification settings - Fork 4
Tools
Tools enable agents to perform actions and retrieve information from external systems. When an agent has tools available, the LLM can decide to call them to gather information or perform operations before providing a final response.
Tools inherit from RubyLLM::Tool and use a DSL to define their behavior:
class SearchTool < RubyLLM::Tool
description "Search for products by query"
param :query, desc: "Search query", required: true
param :limit, desc: "Maximum results to return", default: 10
def execute(query:, limit: 10)
Product.search(query).limit(limit).map(&:to_s).join("\n")
end
endDescribes what the tool does. This is shown to the LLM to help it decide when to use the tool:
class WeatherTool < RubyLLM::Tool
description "Get current weather for a location. Returns temperature, conditions, and forecast."
endDefine parameters the tool accepts:
class SearchTool < RubyLLM::Tool
# Required parameter
param :query, desc: "Search query text", required: true
# Optional with default
param :limit, desc: "Max results", default: 10
# Optional without default (will be nil)
param :category, desc: "Category filter"
# Array type
param :colors, desc: "Colors to filter by", type: :array
# Boolean type
param :in_stock_only, desc: "Only show in-stock items", type: :boolean
endParameter Options:
| Option | Description |
|---|---|
:desc |
Description shown to LLM |
:required |
Whether parameter is required (default: false) |
:default |
Default value if not provided |
:type |
Parameter type (:array, :boolean, etc.) |
The main method that performs the tool's action:
def execute(query:, limit: 10, **filters)
# Perform the action
results = search(query, filters)
# Return a string for the LLM
format_results(results)
endImportant: The execute method should return a string that the LLM can understand and use.
Register tools at the class level using the tools DSL:
class ProductAgent < ApplicationAgent
tools [SearchTool, GetProductTool, CompareTool]
system "You are a helpful shopping assistant."
user "Help the user with: {query}"
endFor runtime tool selection, override tools as an instance method:
class SmartAgent < ApplicationAgent
param :query, required: true
param :user_role
user "{query}"
def tools
base_tools = [SearchTool, GetInfoTool]
# Add admin tools for admin users
if user_role == "admin"
base_tools + [DeleteTool, UpdateTool]
else
base_tools
end
end
endWhen an agent with tools is called:
- Initial Request - Agent sends prompt to LLM with tool definitions
- Tool Decision - LLM analyzes the request and may decide to call tools
- Tool Execution - RubyLLM automatically executes requested tools
- Result Return - Tool results are sent back to the LLM
- Iteration - LLM may call more tools or provide final answer
- Completion - Loop ends when LLM provides a final response
User Request
|
v
+----------+ tool_call +-----------+
| LLM | --------------> | Tool |
| | <-------------- | |
+----------+ result +-----------+
|
| (may repeat)
v
Final Response
After execution, you can inspect which tools were called:
result = ProductAgent.call(query: "Find red shoes under $100")
result.tool_calls # Array of tool call records
result.tool_calls_count # Number of tools called
result.has_tool_calls? # Boolean - were any tools called?
# Each tool call contains:
result.tool_calls.each do |call|
puts call["name"] # Tool name
puts call["arguments"] # Arguments passed
endHere's a full example with a base tool class and multiple tools:
# app/tools/base_tool.rb
class BaseTool < RubyLLM::Tool
private
def format_error(message)
"Error: #{message}"
end
def format_results(items)
return "No results found." if items.empty?
items.map(&:to_s).join("\n\n")
end
end# app/tools/product/search_tool.rb
class Product::SearchTool < BaseTool
description "Search products with advanced filtering. Supports text search, price ranges, categories, and more."
param :query, desc: "Search text (semantic search)", required: false
param :category, desc: "Product category (tops, bottoms, shoes, accessories)", required: false
param :price_min, desc: "Minimum price in USD", required: false
param :price_max, desc: "Maximum price in USD", required: false
param :colors, desc: "Array of colors to filter by", type: :array
param :sizes, desc: "Array of sizes to filter by", type: :array
param :in_stock_only, desc: "Only show in-stock products", type: :boolean
param :limit, desc: "Maximum results to return", default: 20
def execute(query: nil, limit: 20, **filters)
products = Product.all
# Apply filters
products = products.search(query) if query.present?
products = products.where(category: filters[:category]) if filters[:category]
products = products.where("price >= ?", filters[:price_min]) if filters[:price_min]
products = products.where("price <= ?", filters[:price_max]) if filters[:price_max]
products = products.where(color: filters[:colors]) if filters[:colors].present?
products = products.where(size: filters[:sizes]) if filters[:sizes].present?
products = products.in_stock if filters[:in_stock_only]
format_results(products.limit(limit))
rescue => e
format_error(e.message)
end
end# app/tools/product/get_tool.rb
class Product::GetTool < BaseTool
description "Get detailed information about a specific product by ID"
param :id, desc: "Product ID", required: true
def execute(id:)
product = Product.find(id)
product.to_detailed_string
rescue ActiveRecord::RecordNotFound
format_error("Product not found with ID: #{id}")
end
end# app/agents/shopping_agent.rb
class ShoppingAgent < ApplicationAgent
model "gpt-4o"
tools [Product::SearchTool, Product::GetTool]
param :user_id
system do
<<~S
You are a helpful shopping assistant. Use the available tools to:
- Search for products matching user requests
- Get detailed product information
- Compare products when asked
Always be helpful and provide specific product recommendations.
S
end
user "{query}"
def metadata
{ user_id: user_id }
end
endresult = ShoppingAgent.call(
query: "I'm looking for red sneakers under $150",
user_id: current_user.id
)
puts result.content
# => "I found 3 great options for red sneakers under $150..."
puts result.tool_calls_count
# => 1 (SearchTool was called)Format tool output for LLM comprehension:
def execute(id:)
product = Product.find(id)
<<~OUTPUT
Product: #{product.name}
Price: $#{product.price}
Category: #{product.category}
In Stock: #{product.in_stock? ? 'Yes' : 'No'}
Description: #{product.description}
OUTPUT
endHelp the LLM understand when to use each tool:
# Good - specific and actionable
description "Search products by text query, with optional filters for price, category, and availability"
# Bad - vague
description "Search stuff"Return error messages the LLM can understand:
def execute(id:)
product = Product.find(id)
format_product(product)
rescue ActiveRecord::RecordNotFound
"Product with ID #{id} was not found. Please check the ID and try again."
rescue => e
"Unable to retrieve product: #{e.message}"
endOne tool should do one thing well:
# Good - separate concerns
class SearchTool < RubyLLM::Tool
description "Search for products"
end
class CreateOrderTool < RubyLLM::Tool
description "Create a new order"
end
# Bad - too many responsibilities
class ProductTool < RubyLLM::Tool
description "Search, create, update, delete products and orders"
endSpecify types for non-string parameters:
param :tags, desc: "Filter tags", type: :array
param :active, desc: "Only active items", type: :boolean
param :count, desc: "Number of items", type: :integerAny agent can be used as a tool in another agent's tools list. The orchestrating agent's LLM sees the sub-agent as a callable tool and can invoke it with the sub-agent's declared params.
class ResearchAgent < ApplicationAgent
description "Researches a topic and returns key findings"
model "gpt-4o"
param :query, required: true, desc: "Topic to research"
user "Research the following topic thoroughly: {query}"
end
class WriterAgent < ApplicationAgent
description "Writes articles using research from specialist agents"
model "gpt-4o"
# Pass agent classes directly — they're automatically wrapped as tools
tools [ResearchAgent]
param :topic, required: true
system "You are a content writer. Use the research tool to gather information, then write an article."
user "Write an article about: {topic}"
end
result = WriterAgent.call(topic: "quantum computing")When WriterAgent runs, the LLM sees ResearchAgent as a tool named "research" (derived from the class name by removing the Agent suffix and snake_casing). It can call the tool with a query parameter, which triggers a full ResearchAgent.call(query: ...) under the hood.
-
Name derivation —
ResearchAgentbecomes"research",CodeReviewAgentbecomes"code_review" -
Param mapping — The sub-agent's
paramdeclarations become the tool's parameter schema -
Description — The sub-agent's
descriptionbecomes the tool's description shown to the LLM -
Execution — When the LLM calls the tool,
AgentToolcalls the sub-agent class via.call()and returns its content as a string
You can mix agent classes and regular RubyLLM::Tool subclasses in the same tools list:
class OrchestratorAgent < ApplicationAgent
tools [ResearchAgent, CalculatorTool, SummarizerAgent]
endUse an instance method to select tools at runtime:
class SmartOrchestratorAgent < ApplicationAgent
param :mode, default: "full"
def tools
base = [ResearchAgent]
mode == "full" ? base + [FactCheckerAgent, EditorAgent] : base
end
endWhen an agent is invoked as a tool, the execution is automatically linked to the parent:
-
parent_execution_id— points to the calling agent's execution -
root_execution_id— points to the top-level execution in the chain
This lets you trace the full call tree in the dashboard and in queries:
# Find all child executions of a parent
RubyLLM::Agents::Execution.where(parent_execution_id: parent.id)
# Find the full execution tree from the root
RubyLLM::Agents::Execution.where(root_execution_id: root.id)See Execution Tracking for more on hierarchy queries.
To prevent infinite recursion (e.g., Agent A calls Agent B which calls Agent A), there is a maximum nesting depth of 5 levels. If exceeded, the tool returns an error message to the LLM instead of executing.
When the parent agent runs in a multi-tenant context, the tenant is automatically propagated to the sub-agent. The sub-agent inherits the parent's tenant for budget tracking and isolation.
-
Write clear descriptions — The LLM uses the agent's
descriptionto decide when to call it -
Use
desc:on params — Param descriptions help the LLM provide correct arguments - Keep sub-agents focused — Each sub-agent should do one thing well
- Watch the depth — Deep nesting increases latency and token usage; prefer flat composition when possible
- Agent DSL - Full agent configuration reference
- Result Object - Accessing tool call data
- Execution Tracking - Tool calls in execution logs
- Examples - Agent composition examples