Class: Ask::Tool

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

Overview

Base class for defining tools that LLMs can call.

Subclass Ask::Tool, use the DSL to declare metadata and parameters, and implement #execute to perform the work.

class Greeter < Ask::Tool
  description "Greets a person by name"
  param :name, type: :string, desc: "The person's name", required: true

  def execute(name:)
    Ask::Result.ok(data: "Hello, #{name}!")
  end
end

Greeter.new.name         # => "greeter"
Greeter.new.call(name: "World")
# => #<Ask::Result ok=true output="Hello, World!">

Defined Under Namespace

Classes: Halt, Parameter

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.descString?

Set or retrieve the tool’s human-readable description.

Parameters:

  • text (String, nil)

    when provided, sets the description

Returns:

  • (String, nil)


52
53
54
55
56
# File 'lib/ask/tools/tool.rb', line 52

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

  @description = text
end

.description(text = nil) ⇒ String?

Set or retrieve the tool’s human-readable description.

Parameters:

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

    when provided, sets the description

Returns:

  • (String, nil)


47
48
49
50
51
# File 'lib/ask/tools/tool.rb', line 47

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

  @description = text
end

.inherited(subclass) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



36
37
38
39
40
41
# File 'lib/ask/tools/tool.rb', line 36

def inherited(subclass)
  super
  @parameters = {} if @parameters.nil?
  subclass.instance_variable_set(:@description, nil)
  subclass.instance_variable_set(:@parameters, {})
end

.map_type(type) ⇒ Object



231
232
233
234
235
236
237
# File 'lib/ask/tools/tool.rb', line 231

def self.map_type(type)
  case type
  when :int then "integer"
  when :float, :double then "number"
  else type.to_s
  end
end

.param(name, type:, desc: nil, description: nil, required: true) ⇒ void

This method returns an undefined value.

Declare a parameter the tool accepts.

Parameters:

  • name (Symbol)

    parameter name

  • type (Symbol)

    JSON Schema type (:string, :integer, :number, :boolean, :array, :object)

  • desc (String) (defaults to: nil)

    human-readable description of the parameter

  • required (Boolean) (defaults to: true)

    whether the parameter is mandatory



62
63
64
65
66
67
68
69
70
71
# File 'lib/ask/tools/tool.rb', line 62

def param(name, type:, desc: nil, description: nil, required: true)
  type = type.to_s.downcase.to_sym
  validate_param_type!(type, name)
  parameters[name] = Parameter.new(
    name: name,
    type: map_type(type),
    description: desc || description,
    required: required
  )
end

.parametersHash{Symbol => Ask::Tool::Parameter}

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:



75
76
77
# File 'lib/ask/tools/tool.rb', line 75

def parameters
  @parameters ||= {}
end

.params(schema = nil, &block) ⇒ void

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method returns an undefined value.

Define tool parameters using the Schema DSL.

When a block is provided, it takes precedence over individual param declarations for schema generation.

Examples:

params do
  string :location, description: "City name"
  string :unit, enum: %w[celsius fahrenheit]
end

Parameters:

  • schema (Ask::Schema, Class<Ask::Schema>, Hash, nil) (defaults to: nil)

    A pre-built schema

  • block (Proc)

    DSL block evaluated by Ask::Schema



94
95
96
# File 'lib/ask/tools/tool.rb', line 94

def params(schema = nil, &block)
  @params_schema_definition = schema || block
end

.provider_paramsObject



98
99
100
# File 'lib/ask/tools/tool.rb', line 98

def provider_params
  @provider_params ||= {}
end

.validate_param_type!(type, name) ⇒ Object

Raises:

  • (ArgumentError)


223
224
225
226
227
228
229
# File 'lib/ask/tools/tool.rb', line 223

def self.validate_param_type!(type, name)
  return if VALID_JSON_SCHEMA_TYPES.include?(type)

  raise ArgumentError,
        "Invalid type #{type.inspect} for parameter #{name.inspect}. " \
        "Valid types: #{VALID_JSON_SCHEMA_TYPES.map(&:inspect).join(', ')}"
end

Instance Method Details

#call(args = {}) ⇒ Ask::Result

Call the tool with the given arguments.

Normalizes keys to symbols, validates required parameters, and delegates to #execute.

Parameters:

  • args (Hash, nil) (defaults to: {})

    keyword arguments for the tool

Returns:



136
137
138
139
140
141
142
143
144
145
146
# File 'lib/ask/tools/tool.rb', line 136

def call(args = {})
  normalized = normalize_args(args)
  validation = validate(normalized)
  return Ask::Result.error(message: validation) if validation

  execute(**normalized)
rescue Halt => e
  Ask::Result.ok(data: e.content, metadata: { halted: true })
rescue StandardError => e
  Ask::Result.error(message: "#{self.class.name.split('::').last} raised #{e.class}: #{e.message}")
end

#descriptionString?

Returns the tool’s description.

Returns:

  • (String, nil)

    the tool’s description



120
121
122
# File 'lib/ask/tools/tool.rb', line 120

def description
  self.class.description
end

#executeAsk::Result

Subclasses must implement this method.

Parameters:

  • args (Hash)

    normalized keyword arguments

Returns:

Raises:

  • (NotImplementedError)


152
153
154
# File 'lib/ask/tools/tool.rb', line 152

def execute(**)
  raise NotImplementedError, "#{self.class} must implement #execute(**args)"
end

#inspectString

Returns inspect string.

Returns:

  • (String)

    inspect string



199
200
201
# File 'lib/ask/tools/tool.rb', line 199

def inspect
  "#<#{self.class.name} name=#{name.inspect}>"
end

#nameString

Auto-derive the tool name from the class name. Converts CamelCase to snake_case and strips a trailing _tool suffix.

Returns:

  • (String)


107
108
109
110
111
112
113
114
115
116
117
# File 'lib/ask/tools/tool.rb', line 107

def name
  # Use only the class name (last segment), ignoring module nesting
  klass_name = self.class.name.to_s.split("::").last || self.class.name.to_s
  normalized = klass_name.dup.force_encoding("UTF-8").unicode_normalize(:nfkd)
  normalized.encode("ASCII", replace: "")
            .gsub(/[^a-zA-Z0-9_-]/, "-")
            .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
            .gsub(/([a-z\d])([A-Z])/, '\1_\2')
            .downcase
            .delete_suffix("_tool")
end

#parametersHash{Symbol => Ask::Tool::Parameter}

Returns:



125
126
127
# File 'lib/ask/tools/tool.rb', line 125

def parameters
  self.class.parameters
end

#params_schemaHash

Generate a JSON Schema hash describing this tool’s parameters. Suitable for LLM function-calling APIs (OpenAI, Anthropic, etc.).

Returns:

  • (Hash)


160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/ask/tools/tool.rb', line 160

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

  @params_schema = begin
    if parameters.empty?
      nil
    else
      properties = parameters.to_h do |_name, param|
        schema = { type: param.type }
        schema[:description] = param.description if param.description
        schema[:items] = { type: "string" } if param.type == "array"
        [param.name.to_s, schema]
      end

      required = parameters.select { |_, p| p.required }.keys.map(&:to_s)

      {
        type: "object",
        properties: properties,
        required: required,
        additionalProperties: false
      }
    end
  end
end

#tool_definitionHash

Full tool definition hash for LLM API calls.

Returns:

  • (Hash)


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

def tool_definition
  defn = {
    name: name,
    description: description
  }
  defn[:input_schema] = params_schema if params_schema
  defn
end