Class: RubyLLM::Tool

Inherits:
Object
  • Object
show all
Defined in:
lib/ruby_llm/tool.rb

Overview

A Tool is an action an AI model can call during a chat. Subclasses describe themselves with ::description, declare their arguments, and implement #execute:

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

def execute(latitude:, longitude:)
  response = Faraday.get "https://api.open-meteo.com/v1/forecast",
                         latitude: latitude, longitude: longitude,
                         current: "temperature_2m,wind_speed_10m"
  JSON.parse(response.body)
end
end

chat.with_tools(Weather).ask "What's the weather in Berlin?"

When no parameters are declared, the argument schema is inferred from #execute's keyword arguments: required keywords become required string parameters and optional keywords become optional ones. Use ::parameter or ::parameters when arguments need explicit types, descriptions, or structure.

Defined Under Namespace

Classes: SchemaDefinition

Constant Summary collapse

KEYWORD_PARAMETER_KINDS =

:nodoc:

i[keyreq key].freeze
TOOL_CALL_KEYWORD =

:nodoc:

:tool_call

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.approval_resolverObject (readonly)

:nodoc:



56
57
58
# File 'lib/ruby_llm/tool.rb', line 56

def approval_resolver
  @approval_resolver
end

.parameters_schema_definitionObject (readonly)

:nodoc:



56
57
58
# File 'lib/ruby_llm/tool.rb', line 56

def parameters_schema_definition
  @parameters_schema_definition
end

Class Method Details

.declared_parametersObject

:nodoc:



114
115
116
# File 'lib/ruby_llm/tool.rb', line 114

def declared_parameters # :nodoc:
  @declared_parameters ||= {}
end

.description(text = nil) ⇒ Object

:call-seq:

description(text) -> text
description -> string or nil

Sets the description the model sees for this tool, or returns the current description when called without an argument.

class Weather < RubyLLM::Tool
description "Gets current weather for a location"
end


93
94
95
96
97
# File 'lib/ruby_llm/tool.rb', line 93

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

  @description = text
end

.inherited(subclass) ⇒ Object

:nodoc:



58
59
60
61
62
63
64
65
66
67
68
# File 'lib/ruby_llm/tool.rb', line 58

def inherited(subclass) # :nodoc:
  super
  DUPED_INHERITED_CONFIG.each do |ivar, default|
    value = instance_variable_defined?(ivar) ? instance_variable_get(ivar) : default
    subclass.instance_variable_set(ivar, value.dup)
  end

  COPIED_INHERITED_CONFIG.each do |ivar|
    subclass.instance_variable_set(ivar, instance_variable_get(ivar))
  end
end

.parameter(name, **options) ⇒ Object

Declares a parameter for the tool. options accepts type: (defaults to 'string'), description:, and required: (defaults to true).

class Distance < RubyLLM::Tool
description "Calculates distance between two cities"
parameter :origin, description: "Origin city name"
parameter :destination, description: "Destination city name"
parameter :units, type: :string, description: "metric or imperial", required: false
end


110
111
112
# File 'lib/ruby_llm/tool.rb', line 110

def parameter(name, **options)
  declared_parameters[name] = Parameter.new(name, **options)
end

.parameters(schema = nil, &block) ⇒ Object

Sets the JSON Schema for the tool's arguments. Accepts a schema hash, a Schematist::Schema class or instance, or a block written in the schematist DSL. Returns self.

class Scheduler < RubyLLM::Tool
description "Books a meeting"

parameters do
  object :window, description: "Time window to reserve" do
    string :start, description: "ISO8601 start time"
    string :finish, description: "ISO8601 end time"
  end
  array :participants, of: :string
end
end

Raises ArgumentError when called without a schema or a block.



135
136
137
138
139
140
141
142
# File 'lib/ruby_llm/tool.rb', line 135

def parameters(schema = nil, &block)
  if schema.nil? && block.nil?
    raise ArgumentError, 'parameters requires a schema or a block; declare single arguments with parameter'
  end

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

.provider_options(options = (get = true)) ⇒ Object

:call-seq:

provider_options(options) -> self
provider_options -> hash

Sets provider-specific metadata, such as Anthropic's cache_control hints, merged verbatim into the tool payload sent to the provider. Without an argument, returns the current options.

provider_options cache_control: { type: "ephemeral" }

Raises ArgumentError if options is nil.

Raises:

  • (ArgumentError)


190
191
192
193
194
195
196
# File 'lib/ruby_llm/tool.rb', line 190

def provider_options(options = (get = true))
  return @provider_options ||= {} if get
  raise ArgumentError, 'provider_options does not accept nil' if options.nil?

  @provider_options = options.to_h
  self
end

.requires_approval(&resolver) ⇒ Object

Declares that this tool must be approved before it executes. The conversation loop pauses the tool call until a decision is recorded with Chat#approve or Chat#deny, so Chat#complete returns cleanly and can be called again once the decision exists. In Rails the decision persists on the tool call record and survives process restarts.

class IssueRefund < RubyLLM::Tool
requires_approval

def execute(order_id:)
  Refunds.issue!(order_id)
end
end

Pass a block to resolve the decision yourself instead of using the recorded one. The block receives the ToolCall and returns true to execute, false to deny, or nil while the decision is pending.

The block never runs at class definition. The loop consults it whenever it needs the decision, which can be several times while the call is pending, including after a crashed job resumes, so write it as an idempotent read. If it also creates the approval request, make that a find-or-create.

requires_approval { |tool_call| Approvals.status(tool_call.id) }


170
171
172
173
# File 'lib/ruby_llm/tool.rb', line 170

def requires_approval(&resolver)
  @requires_approval = true
  @approval_resolver = resolver
end

.requires_approval?Boolean

:nodoc:



175
176
177
# File 'lib/ruby_llm/tool.rb', line 175

def requires_approval? # :nodoc:
  @requires_approval || false
end

.split_result(result) ⇒ Object

:nodoc:



198
199
200
201
202
203
204
# File 'lib/ruby_llm/tool.rb', line 198

def split_result(result) # :nodoc:
  case result
  when Attachment then ['', [result]]
  when Array then split_array_result(result)
  else [result_content(result), []]
  end
end

.tool_nameObject

Returns the name the model calls this tool by, derived from the class name: underscored, reduced to ASCII, with a trailing "_tool" removed. Override this method to choose a different name.

WeatherLookup.tool_name  # => "weather_lookup"


76
77
78
79
80
# File 'lib/ruby_llm/tool.rb', line 76

def tool_name
  normalized = name.to_s.dup.force_encoding('UTF-8').unicode_normalize(:nfkd)
  ascii_name = normalized.encode('ASCII', replace: '').gsub(/[^a-zA-Z0-9_-]/, '-')
  Support::Utils.underscore(ascii_name).delete_suffix('_tool')
end

Instance Method Details

#approval_resolverObject

:nodoc:



248
249
250
# File 'lib/ruby_llm/tool.rb', line 248

def approval_resolver # :nodoc:
  self.class.approval_resolver
end

#call(tool_call: nil, **arguments) ⇒ Object

Runs the tool with keyword arguments, validating them against the #execute signature before invoking it. RubyLLM supplies tool_call: when the call comes from a chat; pass it yourself only when you need the ToolCall in a direct invocation.

Weather.new.call(latitude: 52.52, longitude: 13.405)


285
286
287
288
289
290
291
292
293
294
295
# File 'lib/ruby_llm/tool.rb', line 285

def call(tool_call: nil, **arguments)
  normalized_args = arguments.transform_keys(&:to_sym)
  validation_error = validate_keyword_arguments(normalized_args)
  return { error: "Invalid tool arguments: #{validation_error}" } if validation_error

  RubyLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
  normalized_args[TOOL_CALL_KEYWORD] = tool_call if execute_accepts_tool_call?
  result = execute(**normalized_args)
  RubyLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
  result
end

#declared_parametersObject

:nodoc:



252
253
254
# File 'lib/ruby_llm/tool.rb', line 252

def declared_parameters # :nodoc:
  self.class.declared_parameters
end

#descriptionObject

Returns the tool description declared on the class with ::description.



239
240
241
# File 'lib/ruby_llm/tool.rb', line 239

def description
  self.class.description
end

#executeObject

Runs the tool with the arguments chosen by the model. Subclasses must implement this method; the base implementation raises NotImplementedError. The return value is sent back to the model. Return a Hash like { error: "..." } to report a recoverable failure.

Declare an optional tool_call: keyword to receive the ToolCall being executed. The keyword is reserved: it never appears in the tool's argument schema and is filled in by RubyLLM, not by the model.

def execute(query:, tool_call: nil)
AuditLog.create!(tool_call_id: tool_call&.id)
Search.run(query)
end

Raises:

  • (NotImplementedError)


312
313
314
# File 'lib/ruby_llm/tool.rb', line 312

def execute(...)
  raise NotImplementedError, 'Subclasses must implement #execute'
end

#nameObject

Returns the name the model calls this tool by, delegating to ::tool_name. Override either one to choose a different name.

WeatherLookup.new.name  # => "weather_lookup"


234
235
236
# File 'lib/ruby_llm/tool.rb', line 234

def name
  self.class.tool_name
end

#parameters_schemaObject

Returns the JSON Schema for the tool's arguments, whether declared explicitly or inferred from the execute signature.



263
264
265
266
267
268
269
270
271
272
273
274
275
276
# File 'lib/ruby_llm/tool.rb', line 263

def parameters_schema
  return @parameters_schema if defined?(@parameters_schema)

  @parameters_schema = begin
    definition = self.class.parameters_schema_definition
    if definition&.present?
      definition.json_schema
    elsif declared_parameters.any?
      SchemaDefinition.from_parameters(declared_parameters)&.json_schema
    else
      SchemaDefinition.from_parameters(inferred_parameters, allow_empty: true)&.json_schema
    end
  end
end

#provider_optionsObject

Returns the provider-specific tool metadata declared on the class.



257
258
259
# File 'lib/ruby_llm/tool.rb', line 257

def provider_options
  self.class.provider_options
end

#requires_approval?Boolean

Returns whether this tool was declared with ::requires_approval.



244
245
246
# File 'lib/ruby_llm/tool.rb', line 244

def requires_approval?
  self.class.requires_approval?
end