Class: Ask::Tool

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

Defined Under Namespace

Classes: Halt, Parameter

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.approval_required(value = :_no_arg_given) ⇒ Boolean

Declare that calling this tool requires human approval.

The tool is still registered and described to the LLM normally, but when an agent session runs with an approval queue enabled, calls to it are queued instead of executed — the agent gets a pending result, and the tool only runs after a human approves it.

Called with no argument returns the current value (default false).

Examples:

class SendEmail < Ask::Tool
  approval_required true
  def execute(to:, body:) ... end
end

Parameters:

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

Returns:

  • (Boolean)


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

def approval_required(value = :_no_arg_given)
  if value == :_no_arg_given
    @approval_required == true
  else
    @approval_required = !!value
  end
end

.auto_approvable(value = :_no_arg_given) ⇒ Boolean

Declare that this tool may be auto-approved when the session's approval policy has auto-approval enabled for it. This is a per-action verdict only — the session-level user rule is still the binding gate. A tool that requires approval but is NOT marked auto-approvable always queues for human review.

Called with no argument returns the current value (default false).

Parameters:

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

Returns:

  • (Boolean)


81
82
83
84
85
86
87
# File 'lib/ask/tools/tool.rb', line 81

def auto_approvable(value = :_no_arg_given)
  if value == :_no_arg_given
    @auto_approvable == true
  else
    @auto_approvable = !!value
  end
end

.build_schema_from_paramsObject



122
123
124
125
126
127
128
129
130
131
# File 'lib/ask/tools/tool.rb', line 122

def build_schema_from_params
  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

.deep_stringify_keys(obj) ⇒ Object



145
146
147
148
149
150
151
# File 'lib/ask/tools/tool.rb', line 145

def deep_stringify_keys(obj)
  case obj
  when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
  when Array then obj.map { |v| deep_stringify_keys(v) }
  else obj
  end
end

.descObject



32
33
34
35
# File 'lib/ask/tools/tool.rb', line 32

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

.description(text = nil) ⇒ Object



28
29
30
31
# File 'lib/ask/tools/tool.rb', line 28

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

.inherited(subclass) ⇒ Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/ask/tools/tool.rb', line 17

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

.name(custom = :_no_arg_given) ⇒ Object

Declare a custom tool name. Called with no argument returns the class name (via Module#name). Called with a string stores a custom name for the instance. Example: name "my_custom_tool"



38
39
40
41
42
43
44
# File 'lib/ask/tools/tool.rb', line 38

def name(custom = :_no_arg_given)
  if custom == :_no_arg_given
    super()  # Module#name, returns the Ruby class path
  else
    @tool_name = custom
  end
end

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



89
90
91
92
93
94
95
96
# File 'lib/ask/tools/tool.rb', line 89

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

.parametersObject



102
103
104
# File 'lib/ask/tools/tool.rb', line 102

def parameters
  @parameters ||= {}
end

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



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

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

.params_schemaObject



110
111
112
113
114
115
116
117
118
119
120
# File 'lib/ask/tools/tool.rb', line 110

def params_schema
  @params_schema ||= begin
    if @params_schema_definition
      deep_stringify_keys(resolve_params_schema(@params_schema_definition))
    elsif @parameters && @parameters.any?
      build_schema_from_params
    else
      nil
    end
  end
end

.provider_paramsObject



106
107
108
# File 'lib/ask/tools/tool.rb', line 106

def provider_params
  @provider_params ||= {}
end

.resolve_params_schema(definition) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
# File 'lib/ask/tools/tool.rb', line 133

def resolve_params_schema(definition)
  case definition
  when Proc
    schema_class = Ask::Schema.create(&definition)
    schema_class.new.to_json_schema.dig(:schema)
  when Hash then definition
  when ->(d) { d.respond_to?(:to_json_schema) }
    definition.to_json_schema.dig(:schema)
  else nil
  end
end

Instance Method Details

#approval_required?Boolean

Returns whether calling this tool requires human approval.

Returns:

  • (Boolean)

    whether calling this tool requires human approval



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

def approval_required?
  self.class.approval_required
end

#auto_approvable?Boolean

Returns whether this tool may be auto-approved under a session-level auto-approval rule.

Returns:

  • (Boolean)

    whether this tool may be auto-approved under a session-level auto-approval rule



204
205
206
# File 'lib/ask/tools/tool.rb', line 204

def auto_approvable?
  self.class.auto_approvable
end

#call(args = {}, abort_controller = nil) ⇒ Object



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/ask/tools/tool.rb', line 208

def call(args = {}, abort_controller = nil)
normalized = normalize_args(args)
validation = validate(normalized)
return Ask::Result.failure(validation) if validation
normalized[:_abort_controller] = abort_controller if abort_controller
execute_kwargs = normalized.reject { |k, _| k == :_abort_controller || k == :abort_controller }
execute(**execute_kwargs)
rescue Halt => e
  Ask::Result.ok(data: e.content, metadata: { halted: true })
rescue StandardError => e
  Ask::Result.failure("#{self.class.name.split('::').last} raised #{e.class}: #{e.message}")
end

#descriptionObject



189
190
191
# File 'lib/ask/tools/tool.rb', line 189

def description
  self.class.description
end

#execute(**args) ⇒ Object

Raises:

  • (NotImplementedError)


221
222
223
# File 'lib/ask/tools/tool.rb', line 221

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

#inspectObject



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

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

#nameObject



175
176
177
178
179
180
181
182
183
184
185
186
187
# File 'lib/ask/tools/tool.rb', line 175

def name
  custom = self.class.instance_variable_get(:@tool_name)
  return custom if custom

  klass_name = self.class.name.to_s.split("::").last || ""
  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

#parametersObject



193
194
195
# File 'lib/ask/tools/tool.rb', line 193

def parameters
  self.class.parameters
end

#params_schemaObject



225
226
227
228
229
230
231
232
233
234
235
236
# File 'lib/ask/tools/tool.rb', line 225

def params_schema
  return @params_schema if defined?(@params_schema)
  @params_schema = begin
    if params_schema_definition
      deep_stringify_keys(resolve_params_schema(params_schema_definition))
    elsif parameters.any?
      build_schema_from_params
    else
      nil
    end
  end
end

#provider_paramsObject



171
172
173
# File 'lib/ask/tools/tool.rb', line 171

def provider_params
  self.class.provider_params
end

#tool_definitionObject



238
239
240
241
242
# File 'lib/ask/tools/tool.rb', line 238

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