Class: RobotLab::Tool

Inherits:
RubyLLM::Tool
  • Object
show all
Defined in:
lib/robot_lab/tool.rb

Overview

A tool that robots can use, built on RubyLLM::Tool.

Provides two patterns for defining tools:

  1. **Subclass pattern** — for reusable, robot-aware tools:

class GetWeather < RobotLab::Tool
  description "Get weather for a location"
  param :location, type: "string", desc: "City name"

  def execute(location:)
    WeatherService.fetch(location)
  end
end
  1. **Factory pattern** — for dynamic/inline tools:

tool = RobotLab::Tool.create(
  name: "get_time",
  description: "Get the current time"
) { |args| Time.now.to_s }

Subclasses have access to the owning robot via an accessor, enabling tools that modify their robot’s state (temperature, system prompt, spawning, etc.).

Direct Known Subclasses

AskUser

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(robot: nil) ⇒ Tool

Creates a new Tool instance.

Parameters:

  • robot (Robot, nil) (defaults to: nil)

    the owning robot



81
82
83
84
# File 'lib/robot_lab/tool.rb', line 81

def initialize(robot: nil)
  super()
  @robot = robot
end

Class Attribute Details

.raise_on_error=(value) ⇒ Boolean (writeonly)

When true, exceptions from #execute propagate instead of being caught. Default: false (graceful error handling).

Returns:

  • (Boolean)


44
45
46
# File 'lib/robot_lab/tool.rb', line 44

def raise_on_error=(value)
  @raise_on_error = value
end

Instance Attribute Details

#mcpObject (readonly)

Returns the value of attribute mcp.



37
38
39
# File 'lib/robot_lab/tool.rb', line 37

def mcp
  @mcp
end

#robotRobot?

Returns the robot that owns this tool.

Returns:

  • (Robot, nil)

    the robot that owns this tool



33
34
35
# File 'lib/robot_lab/tool.rb', line 33

def robot
  @robot
end

Class Method Details

.create(name:, description: nil, parameters: nil, mcp: nil, robot: nil) {|args| ... } ⇒ Tool

Factory for dynamic tools (MCP wrappers, inline tools).

Examples:

Simple factory tool

tool = RobotLab::Tool.create(
  name: "get_time",
  description: "Get the current time"
) { |args| Time.now.to_s }

MCP tool wrapper

tool = RobotLab::Tool.create(
  name: "search",
  description: "Search the web",
  parameters: { type: "object", properties: { q: { type: "string" } }, required: ["q"] },
  mcp: "brave_search"
) { |args| mcp_client.call_tool("search", args) }

Parameters:

  • name (String, Symbol)

    the tool name

  • description (String, nil) (defaults to: nil)

    what the tool does

  • parameters (Hash, nil) (defaults to: nil)

    JSON Schema parameter definition

  • mcp (String, nil) (defaults to: nil)

    MCP server name

  • robot (Robot, nil) (defaults to: nil)

    the owning robot

Yields:

  • (args)

    block that executes the tool logic

Returns:

  • (Tool)

    a new tool instance



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/robot_lab/tool.rb', line 150

def self.create(name:, description: nil, parameters: nil, mcp: nil, robot: nil, &handler)
  desc_text = description
  params_hash = parameters
  block = handler

  tool_class = Class.new(self) do
    description(desc_text) if desc_text

    if params_hash.is_a?(Hash) && params_hash[:properties]
      required_list = Array(params_hash[:required]).map(&:to_s)
      params_hash[:properties].each do |pname, pdef|
        param pname.to_sym,
              type: pdef[:type] || "string",
              desc: pdef[:description],
              required: required_list.include?(pname.to_s)
      end
    end

    define_method(:execute) do |**args|
      block.call(args)
    end
  end

  instance = tool_class.new(robot: robot)
  instance.instance_variable_set(:@custom_name, name.to_s)
  instance.instance_variable_set(:@mcp, mcp)
  instance
end

.ractor_safe(value = nil) ⇒ Boolean Also known as: ractor_safe?

Declare that this tool class is safe to run inside a Ractor.

Ractor-safe tools must be stateless — no captured mutable closures and no non-shareable class-level state. The tool is instantiated fresh inside the Ractor worker for each call.

With no argument, acts as a getter (walks the inheritance chain). With a Boolean argument, sets the value.

Parameters:

  • value (Boolean, nil) (defaults to: nil)

Returns:

  • (Boolean)


61
62
63
64
65
66
67
68
69
70
71
72
73
# File 'lib/robot_lab/tool.rb', line 61

def ractor_safe(value = nil)
  if value.nil?
    if instance_variable_defined?(:@ractor_safe)
      @ractor_safe
    elsif superclass.respond_to?(:ractor_safe)
      superclass.ractor_safe
    else
      false
    end
  else
    @ractor_safe = value
  end
end

.raise_on_error?Boolean

Returns:

  • (Boolean)


46
47
48
# File 'lib/robot_lab/tool.rb', line 46

def raise_on_error?
  defined?(@raise_on_error) ? @raise_on_error : false
end

Instance Method Details

#call(args) ⇒ Object

Invokes the tool, routing through the Ractor worker pool if ractor_safe.

For Ractor-safe tools with a resolvable class name: submits to RobotLab.ractor_pool and blocks for the frozen result. Anonymous classes (name.nil?) fall through to the inline path.

For non-Ractor-safe tools: runs execute directly in the calling thread.

Parameters:

  • args (Hash)

    the tool arguments from the LLM

Returns:

  • (Object)

    the tool result or an error string



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/robot_lab/tool.rb', line 96

def call(args)
  if self.class.ractor_safe? && !self.class.name.nil?
    RobotLab.ractor_pool.submit(self.class.name, args)
  else
    super
  end
rescue RobotLab::ToolError => e
  raise if self.class.raise_on_error?
  "Error (#{name}): #{e.message}"
rescue StandardError => e
  raise if self.class.raise_on_error?

  RobotLab.config.logger.warn("Tool '#{name}' error: #{e.class}: #{e.message}")
  "Error (#{name}): #{e.message}"
end

#mcp?Boolean

Check if this is an MCP-provided tool.

Returns:

  • (Boolean)


122
123
124
# File 'lib/robot_lab/tool.rb', line 122

def mcp?
  !@mcp.nil?
end

#nameString

Override name to support explicit names for dynamic/MCP tools.

Returns:

  • (String)

    the tool name



115
116
117
# File 'lib/robot_lab/tool.rb', line 115

def name
  defined?(@custom_name) && @custom_name ? @custom_name : super
end

#to_hHash

Hash representation.

Returns:

  • (Hash)


195
196
197
198
199
200
201
# File 'lib/robot_lab/tool.rb', line 195

def to_h
  {
    name: name,
    description: description,
    mcp: mcp
  }.compact
end

#to_json(*args) ⇒ String

JSON representation.

Parameters:

  • args (Array)

    arguments passed to to_json

Returns:

  • (String)


207
208
209
# File 'lib/robot_lab/tool.rb', line 207

def to_json(*args)
  to_h.to_json(*args)
end

#to_json_schemaHash

Convert to JSON Schema for LLM function calling. Used by RobotLab adapters for provider-specific formatting.

Returns:

  • (Hash)

    JSON Schema representation



183
184
185
186
187
188
189
190
# File 'lib/robot_lab/tool.rb', line 183

def to_json_schema
  schema = params_schema || { "type" => "object", "properties" => {}, "required" => [] }
  {
    name: name,
    description: description,
    parameters: deep_symbolize_keys(schema)
  }.compact
end