Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/workflows/_test-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ permissions:
jobs:
test:
name: Test (${{ matrix.ruby-version }} / ${{ matrix.rails-version }})
runs-on: ubuntu-latest
# ubuntu-26.04 has no Ruby 3.1 or JRuby 10.0.2.0 build in ruby/setup-ruby, so pin to 24.04.
runs-on: ubuntu-24.04
env:
BUNDLE_PATH: vendor/bundle
BUNDLE_DISABLE_SHARED_GEMS: "true"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ RubyLLM.moderate "Check if this text is safe"
```ruby
# Let AI use your code
class Weather < RubyLLM::Tool
desc "Get current weather"
description "Get current weather"

def execute(latitude:, longitude:)
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
Expand Down
2 changes: 1 addition & 1 deletion docs/_advanced/agent-handoffs.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ If you would rather the agent decide mid-conversation, give it a single handoff
```ruby
class Handoff < RubyLLM::Tool
description "Hand the conversation to the specialist who should answer next"
param :specialist, desc: "billing or technical"
parameter :specialist, description: "billing or technical"
def execute(specialist:) = specialist
end

Expand Down
2 changes: 1 addition & 1 deletion docs/_advanced/rag.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ end
```ruby
class DocumentSearch < RubyLLM::Tool
description "Searches knowledge base for relevant information"
param :query, desc: "Search query"
parameter :query, description: "Search query"

def execute(query:)
embedding = RubyLLM.embed(query).vectors
Expand Down
2 changes: 1 addition & 1 deletion docs/_advanced/rails-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ Tools are Ruby classes that the AI can call. While the tool classes themselves a
```ruby
class Weather < RubyLLM::Tool
description "Gets current weather for a location"
param :city, desc: "City name"
parameter :city, description: "City name"

def execute(city:)
"The weather in #{city} is sunny and 22°C."
Expand Down
2 changes: 1 addition & 1 deletion docs/_core_features/chat-request-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ The same idea applies to tool definitions:
```ruby
class ChangelogTool < RubyLLM::Tool
description "Formats commits into human-readable changelog entries."
param :commits, type: :array, desc: "List of commits to summarize"
parameter :commits, type: :array, description: "List of commits to summarize"

with_params cache_control: { type: 'ephemeral' }

Expand Down
2 changes: 1 addition & 1 deletion docs/_core_features/citations.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ When your tools fetch documents, e.g. from a vector store, Google Drive, a wiki,
```ruby
class KnowledgeBase < RubyLLM::Tool
description "Searches the company knowledge base"
param :query, desc: "What to look for"
parameter :query, description: "What to look for"

def execute(query:)
docs = MyVectorStore.search(query)
Expand Down
50 changes: 26 additions & 24 deletions docs/_core_features/tool-parameters.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ description: Declare tool arguments - from inferred signatures to full JSON Sche
After reading this guide, you will know:

* How RubyLLM infers parameters from an `execute` signature.
* When to reach for the `param` helper versus the `params` DSL.
* When to reach for the `parameter` helper versus the `parameters` DSL.
* How to supply your own JSON Schema for full control.
* How to return rich content, such as images and documents, from a tool.
* How to inject dependencies through custom initialization.
Expand All @@ -36,18 +36,20 @@ The model only knows what arguments a tool accepts from the schema you give it.
RubyLLM ships with three complementary approaches:

* **Signature inference** for simple flat arguments.
* The **`param` helper** for quick, flat argument lists. (v1.0+)
* The **`params` DSL** for expressive, structured inputs. (v1.9+)
* The **`parameter` helper** for quick, flat argument lists. (v1.0+)
* The **`parameters` DSL** for expressive, structured inputs. (v1.9+)

Start with the method signature. Add `param` when a flat argument needs a description, type, or optionality that is not obvious from Ruby alone. Use the `params` DSL whenever you need nested objects, arrays, enums, or union types.
The 1.x names (`param`, `desc:`, `params`, and class-level `desc`) still work on this fork.

Start with the method signature. Add `parameter` when a flat argument needs a description, type, or optionality that is not obvious from Ruby alone. Use the `parameters` DSL whenever you need nested objects, arrays, enums, or union types.

### Signature Inference

When a tool has no `param` or `params` declaration, RubyLLM builds a JSON Schema from `execute` keyword arguments:
When a tool has no `parameter` or `parameters` declaration, RubyLLM builds a JSON Schema from `execute` keyword arguments:

```ruby
class Weather < RubyLLM::Tool
desc "Gets current weather for a location"
description "Gets current weather for a location"

def execute(latitude:, longitude:, units: "metric")
# ...
Expand All @@ -59,32 +61,32 @@ Required keywords become required string parameters. Optional keywords become op

Ruby method signatures do not expose reliable JSON Schema types or descriptions, so add explicit declarations when those details matter.

### Using the `param` Helper for Simple Tools
### Using the `parameter` Helper for Simple Tools

If your tool just needs a few scalar arguments with descriptions or non-string types, use the `param` helper. RubyLLM translates these declarations into JSON Schema under the hood.
If your tool just needs a few scalar arguments with descriptions or non-string types, use the `parameter` helper. RubyLLM translates these declarations into JSON Schema under the hood.

```ruby
class Distance < RubyLLM::Tool
desc "Calculates distance between two cities"
param :origin, desc: "Origin city name"
param :destination, description: "Destination city name"
param :units, type: :string, desc: "Unit system (metric or imperial)", required: false
description "Calculates distance between two cities"
parameter :origin, description: "Origin city name"
parameter :destination, description: "Destination city name"
parameter :units, type: :string, description: "Unit system (metric or imperial)", required: false

def execute(origin:, destination:, units: "metric")
# ...
end
end
```

### params DSL
### parameters DSL

When you need nested objects, arrays, enums, or union types, the `params do ... end` DSL produces the JSON Schema that function-calling models expect while staying Ruby-flavoured.
When you need nested objects, arrays, enums, or union types, the `parameters do ... end` DSL produces the JSON Schema that function-calling models expect while staying Ruby-flavoured.

```ruby
class Scheduler < RubyLLM::Tool
desc "Books a meeting"
description "Books a meeting"

params do
parameters do
object :window, description: "Time window to reserve" do
string :start, description: "ISO8601 start time"
string :finish, description: "ISO8601 end time"
Expand All @@ -108,13 +110,13 @@ RubyLLM bundles the DSL through [`ruby_llm-schema`](https://github.com/danielfri

### Supplying JSON Schema Manually

Prefer to own the JSON Schema yourself? Pass a schema hash (or a class/object responding to `#to_json_schema`) directly to `params`:
Prefer to own the JSON Schema yourself? Pass a schema hash (or a class/object responding to `#to_json_schema`) directly to `parameters`:

```ruby
class Lookup < RubyLLM::Tool
description "Performs catalog lookups"

params type: "object",
parameters type: "object",
properties: {
sku: { type: "string", description: "Product SKU" },
locale: { type: "string", description: "Country code", default: "US" }
Expand All @@ -138,7 +140,7 @@ Tools can return `RubyLLM::Content` objects with file attachments, allowing you
```ruby
class AnalyzeTool < RubyLLM::Tool
description "Analyzes data and returns results with visualizations"
param :query, desc: "Analysis query"
parameter :query, description: "Analysis query"

def execute(query:)
chart_path = generate_chart(query)
Expand Down Expand Up @@ -180,12 +182,12 @@ Tools can have custom initialization:
class DocumentSearch < RubyLLM::Tool
description "Searches documents by relevance"

param :query,
desc: "The search query"
parameter :query,
description: "The search query"

param :limit,
parameter :limit,
type: :integer,
desc: "Maximum number of results",
description: "Maximum number of results",
required: false

def initialize(database)
Expand Down Expand Up @@ -215,7 +217,7 @@ Some providers accept additional metadata alongside the JSON Schema, for example
class TodoTool < RubyLLM::Tool
description "Adds a task to the shared TODO list"

params do
parameters do
string :title, description: "Human-friendly task description"
end

Expand Down
18 changes: 10 additions & 8 deletions docs/_core_features/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Define a tool by creating a class that inherits from `RubyLLM::Tool`.

```ruby
class Weather < RubyLLM::Tool
desc "Gets current weather for a location"
description "Gets current weather for a location"

def execute(latitude:, longitude:)
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
Expand All @@ -65,9 +65,9 @@ end
### Tool Components

1. **Inheritance:** Must inherit from `RubyLLM::Tool`.
2. **`desc` / `description`:** A class method defining what the tool does. Crucial for the AI model to understand its purpose. Keep it clear and concise.
2. **`description`:** A class method defining what the tool does. Crucial for the AI model to understand its purpose. Keep it clear and concise.
3. **`execute` Method:** The instance method containing your Ruby code. RubyLLM v1.15+ infers simple keyword parameters from this signature when no explicit parameter schema is declared.
4. **Parameter declarations:** Optional. Use `param` for simple descriptions and types, or `params` for nested objects, arrays, enums, and full JSON Schema control.
4. **Parameter declarations:** Optional. Use `parameter` for simple descriptions and types, or `parameters` for nested objects, arrays, enums, and full JSON Schema control.

> The tool's class name is automatically converted to a snake_case name used in the API call (e.g., `WeatherLookup` becomes `weather_lookup`). This is how the LLM would call it. You can override this by defining a `name` method in your tool class:
>
Expand All @@ -94,14 +94,16 @@ end
RubyLLM ships with three complementary approaches to telling the model what arguments a tool accepts:

* **Signature inference** for simple flat arguments.
* The **`param` helper** for quick, flat argument lists. (v1.0+)
* The **`params` DSL** for expressive, structured inputs. (v1.9+)
* The **`parameter` helper** for quick, flat argument lists. (v1.0+)
* The **`parameters` DSL** for expressive, structured inputs. (v1.9+)

The simplest case needs nothing extra. When a tool has no `param` or `params` declaration, RubyLLM builds a JSON Schema from the `execute` keyword arguments:
The 1.x names (`param`, `desc:`, `params`, and class-level `desc`) still work on this fork.

The simplest case needs nothing extra. When a tool has no `parameter` or `parameters` declaration, RubyLLM builds a JSON Schema from the `execute` keyword arguments:

```ruby
class Weather < RubyLLM::Tool
desc "Gets current weather for a location"
description "Gets current weather for a location"

def execute(latitude:, longitude:, units: "metric")
# ...
Expand All @@ -111,7 +113,7 @@ end

Required keywords become required string parameters. Optional keywords become optional string parameters. A tool with `def execute` receives an empty object schema.

Ruby method signatures do not expose reliable JSON Schema types or descriptions, so add explicit declarations when those details matter. For the `param` helper, the `params` DSL, and supplying JSON Schema manually, see [Tool Parameters]({% link _core_features/tool-parameters.md %}).
Ruby method signatures do not expose reliable JSON Schema types or descriptions, so add explicit declarations when those details matter. For the `parameter` helper, the `parameters` DSL, and supplying JSON Schema manually, see [Tool Parameters]({% link _core_features/tool-parameters.md %}).

## Using Tools in Chat

Expand Down
2 changes: 1 addition & 1 deletion docs/_getting_started/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Tools allow AI models to call Ruby code during conversations. This powerful feat

```ruby
class Calculator < RubyLLM::Tool
desc "Performs basic arithmetic"
description "Performs basic arithmetic"

def execute(expression:)
{ result: eval(expression) }
Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ RubyLLM.moderate "Check if this text is safe"
```ruby
# Let AI use your code
class Weather < RubyLLM::Tool
desc "Get current weather"
description "Get current weather"

def execute(latitude:, longitude:)
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
Expand Down
61 changes: 51 additions & 10 deletions lib/ruby_llm/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'ruby_llm/schema'

module RubyLLM
# Parameter definition for Tool methods.
# Parameter definition for Tool methods. `desc:` is the 1.x name for `description:`.
class Parameter
attr_reader :name, :type, :description, :required

Expand All @@ -20,26 +20,47 @@ class Tool
POSITIONAL_PARAMETER_KINDS = %i[req opt rest].freeze

class << self
attr_reader :params_schema_definition

def description(text = nil)
return @description unless text

@description = text
end
alias desc description

# RubyLLM 2.0 name. Stores into declared_parameters, as 2.0 does.
def parameter(name, **options)
declared_parameters[name] = Parameter.new(name, **options)
end

# 1.x name for parameter.
def param(name, **options)
parameters[name] = Parameter.new(name, **options)
parameter(name, **options)
end

# RubyLLM 2.0 name for the declared parameter hash (the storage).
def declared_parameters
@declared_parameters ||= {}
end

def parameters
@parameters ||= {}
# Called with a schema or a block: the 2.0 form of params. Called with neither: the 1.x
# getter for the parameter hash (2.0 raises ArgumentError here instead; this fork keeps
# the getter until Supernova no longer calls it).
def parameters(schema = nil, &block)
return declared_parameters if schema.nil? && block.nil?

define_parameters_schema(schema, block)
end

# 1.x name for parameters(schema) / parameters do ... end.
def params(schema = nil, &block)
@params_schema_definition = SchemaDefinition.new(schema:, block:)
self
define_parameters_schema(schema, block)
end

attr_reader :parameters_schema_definition

# 1.x name for parameters_schema_definition.
def params_schema_definition
parameters_schema_definition
end

def with_params(**params)
Expand All @@ -50,6 +71,13 @@ def with_params(**params)
def provider_params
@provider_params ||= {}
end

private

def define_parameters_schema(schema, block)
@parameters_schema_definition = SchemaDefinition.new(schema:, block:)
self
end
end

def name
Expand All @@ -63,18 +91,31 @@ def description
self.class.description
end

# 1.x name for declared_parameters.
def parameters
declared_parameters
end

# RubyLLM 2.0 name. Calls the class through its 1.x getter so a class-level override of
# either name is honored.
def declared_parameters
self.class.parameters
end

def provider_params
self.class.provider_params
end

# 1.x name for parameters_schema.
def params_schema
return @params_schema if defined?(@params_schema)
parameters_schema
end

# RubyLLM 2.0 name; holds what params_schema did before.
def parameters_schema
return @parameters_schema if defined?(@parameters_schema)

@params_schema = begin
@parameters_schema = begin
definition = self.class.params_schema_definition
if definition&.present?
definition.json_schema
Expand Down
Loading
Loading