Class: RubyLLM::Tool
- Inherits:
-
Object
- Object
- RubyLLM::Tool
- 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
-
.approval_resolver ⇒ Object
readonly
:nodoc:.
-
.parameters_schema_definition ⇒ Object
readonly
:nodoc:.
Class Method Summary collapse
-
.declared_parameters ⇒ Object
:nodoc:.
-
.description(text = nil) ⇒ Object
:call-seq: description(text) -> text description -> string or nil.
-
.inherited(subclass) ⇒ Object
:nodoc:.
-
.parameter(name, **options) ⇒ Object
Declares a parameter for the tool.
-
.parameters(schema = nil, &block) ⇒ Object
Sets the JSON Schema for the tool's arguments.
-
.provider_options(options = (get = true)) ⇒ Object
:call-seq: provider_options(options) -> self provider_options -> hash.
-
.requires_approval(&resolver) ⇒ Object
Declares that this tool must be approved before it executes.
-
.requires_approval? ⇒ Boolean
:nodoc:.
-
.split_result(result) ⇒ Object
:nodoc:.
-
.tool_name ⇒ Object
Returns the name the model calls this tool by, derived from the class name: underscored, reduced to ASCII, with a trailing "_tool" removed.
Instance Method Summary collapse
-
#approval_resolver ⇒ Object
:nodoc:.
-
#call(tool_call: nil, **arguments) ⇒ Object
Runs the tool with keyword arguments, validating them against the #execute signature before invoking it.
-
#declared_parameters ⇒ Object
:nodoc:.
-
#description ⇒ Object
Returns the tool description declared on the class with ::description.
-
#execute ⇒ Object
Runs the tool with the arguments chosen by the model.
-
#name ⇒ Object
Returns the name the model calls this tool by, delegating to ::tool_name.
-
#parameters_schema ⇒ Object
Returns the JSON Schema for the tool's arguments, whether declared explicitly or inferred from the
executesignature. -
#provider_options ⇒ Object
Returns the provider-specific tool metadata declared on the class.
-
#requires_approval? ⇒ Boolean
Returns whether this tool was declared with ::requires_approval.
Class Attribute Details
.approval_resolver ⇒ Object (readonly)
:nodoc:
56 57 58 |
# File 'lib/ruby_llm/tool.rb', line 56 def approval_resolver @approval_resolver end |
.parameters_schema_definition ⇒ Object (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_parameters ⇒ Object
: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, **) declared_parameters[name] = Parameter.new(name, **) 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:
() -> self
-> 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.
cache_control: { type: "ephemeral" }
Raises ArgumentError if options is nil.
190 191 192 193 194 195 196 |
# File 'lib/ruby_llm/tool.rb', line 190 def ( = (get = true)) return ||= {} if get raise ArgumentError, 'provider_options does not accept nil' if .nil? = .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_name ⇒ Object
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_resolver ⇒ Object
: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_parameters ⇒ Object
:nodoc:
252 253 254 |
# File 'lib/ruby_llm/tool.rb', line 252 def declared_parameters # :nodoc: self.class.declared_parameters end |
#description ⇒ Object
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 |
#execute ⇒ Object
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
312 313 314 |
# File 'lib/ruby_llm/tool.rb', line 312 def execute(...) raise NotImplementedError, 'Subclasses must implement #execute' end |
#name ⇒ Object
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_schema ⇒ Object
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_options ⇒ Object
Returns the provider-specific tool metadata declared on the class.
257 258 259 |
# File 'lib/ruby_llm/tool.rb', line 257 def self.class. 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 |