Class: Ask::Tool
- Inherits:
-
Object
- Object
- Ask::Tool
- Defined in:
- lib/ask/tools/tool.rb
Defined Under Namespace
Class Method Summary collapse
-
.approval_required(value = :_no_arg_given) ⇒ Boolean
Declare that calling this tool requires human approval.
-
.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.
- .build_schema_from_params ⇒ Object
- .deep_stringify_keys(obj) ⇒ Object
- .desc ⇒ Object
- .description(text = nil) ⇒ Object
-
.infer_parameters_from_execute ⇒ Object
Derive parameters from
def execute(project_id:, title:, ...): required keywords become required string parameters. - .inherited(subclass) ⇒ Object
-
.name(custom = :_no_arg_given) ⇒ Object
Declare a custom tool name.
- .param(name, type:, desc: nil, description: nil, required: true) ⇒ Object
-
.parameters ⇒ Object
The tool's declared parameters — or, when none are declared, inferred from the execute signature so a tool is never silently uncallable: a tool whose execute takes keyword arguments but declares no params would otherwise reject every call ("unknown parameters").
- .params(schema = nil, &block) ⇒ Object
- .params_schema ⇒ Object
- .provider_params ⇒ Object
- .resolve_params_schema(definition) ⇒ Object
Instance Method Summary collapse
-
#approval_required? ⇒ Boolean
Whether calling this tool requires human approval.
-
#auto_approvable? ⇒ Boolean
Whether this tool may be auto-approved under a session-level auto-approval rule.
- #call(args = {}, abort_controller = nil) ⇒ Object
- #description ⇒ Object
- #execute(**args) ⇒ Object
- #inspect ⇒ Object
- #name ⇒ Object
- #parameters ⇒ Object
- #params_schema ⇒ Object
- #provider_params ⇒ Object
- #tool_definition ⇒ Object
-
#validate(normalized) ⇒ Object
Validate normalized (symbol-keyed) arguments.
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).
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).
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_params ⇒ Object
146 147 148 149 150 151 152 153 154 155 |
# File 'lib/ask/tools/tool.rb', line 146 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
169 170 171 172 173 174 175 |
# File 'lib/ask/tools/tool.rb', line 169 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 |
.desc ⇒ Object
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 |
.infer_parameters_from_execute ⇒ Object
Derive parameters from def execute(project_id:, title:, ...):
required keywords become required string parameters. Ruby types
aren't introspectable, so everything infers as string (JSON
numbers coerce); tools that need real types declare params.
120 121 122 123 124 125 126 127 128 |
# File 'lib/ask/tools/tool.rb', line 120 def infer_parameters_from_execute @parameters_inferred = true instance_method(:execute).parameters.each do |kind, name| next unless %i[keyreq key opt].include?(kind) next if name.nil? || name == :_abort_controller @parameters[name] = Parameter.new(name: name, type: "string", required: kind == :keyreq) end 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 |
.parameters ⇒ Object
The tool's declared parameters — or, when none are declared,
inferred from the execute signature so a tool is never silently
uncallable: a tool whose execute takes keyword arguments but
declares no params would otherwise reject every call ("unknown
parameters"). Inference is a fallback, never an override: an
explicit params/param declaration wins.
108 109 110 111 112 113 114 |
# File 'lib/ask/tools/tool.rb', line 108 def parameters @parameters ||= {} if @parameters.empty? && @params_schema_definition.nil? && !@parameters_inferred infer_parameters_from_execute end @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_schema ⇒ Object
134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/ask/tools/tool.rb', line 134 def 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_params ⇒ Object
130 131 132 |
# File 'lib/ask/tools/tool.rb', line 130 def provider_params @provider_params ||= {} end |
.resolve_params_schema(definition) ⇒ Object
157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/ask/tools/tool.rb', line 157 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.
222 223 224 |
# File 'lib/ask/tools/tool.rb', line 222 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.
228 229 230 |
# File 'lib/ask/tools/tool.rb', line 228 def auto_approvable? self.class.auto_approvable end |
#call(args = {}, abort_controller = nil) ⇒ Object
232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/ask/tools/tool.rb', line 232 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.}") end |
#description ⇒ Object
213 214 215 |
# File 'lib/ask/tools/tool.rb', line 213 def description self.class.description end |
#execute(**args) ⇒ Object
245 246 247 |
# File 'lib/ask/tools/tool.rb', line 245 def execute(**args) raise NotImplementedError, "#{self.class} must implement #execute(**args)" end |
#inspect ⇒ Object
268 269 270 |
# File 'lib/ask/tools/tool.rb', line 268 def inspect "#<#{self.class.name} name=#{name.inspect}>" end |
#name ⇒ Object
199 200 201 202 203 204 205 206 207 208 209 210 211 |
# File 'lib/ask/tools/tool.rb', line 199 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 |
#parameters ⇒ Object
217 218 219 |
# File 'lib/ask/tools/tool.rb', line 217 def parameters self.class.parameters end |
#params_schema ⇒ Object
249 250 251 252 253 254 255 256 257 258 259 260 |
# File 'lib/ask/tools/tool.rb', line 249 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_params ⇒ Object
195 196 197 |
# File 'lib/ask/tools/tool.rb', line 195 def provider_params self.class.provider_params end |
#tool_definition ⇒ Object
262 263 264 265 266 |
# File 'lib/ask/tools/tool.rb', line 262 def tool_definition defn = { name: name, description: description } defn[:input_schema] = params_schema if params_schema defn end |
#validate(normalized) ⇒ Object
Validate normalized (symbol-keyed) arguments. Returns nil when
valid, or an actionable message — one that names what was expected
— so the model (or a repair pass) can correct the call instead of
guessing. Public: ask-agent's tool-call repair validates through
this. Tools with a declared params block validate against the
resolved schema (required keys + unknown keys when strict).
278 279 280 281 282 283 284 285 286 287 288 289 |
# File 'lib/ask/tools/tool.rb', line 278 def validate(normalized) return validate_against_schema(normalized) if params_schema_definition expected = self.class.parameters.keys missing = expected.select { |name| self.class.parameters[name].required && !normalized.key?(name) } return "missing required parameters: #{missing.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless missing.empty? unknown = normalized.keys - expected return "unknown parameters: #{unknown.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless unknown.empty? nil end |