ask-tools 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/ask/tools/tool.rb +78 -157
- data/lib/ask/version.rb +3 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1a85f7d9d66c347416e813fa6f74b5f1d6f44f75107422d6d2de79f31318aa21
|
|
4
|
+
data.tar.gz: deab76f0fdde304174f3d6d11de03c556024b23e3163f0b4b51d2efbf74d834d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 20ea10459f3cef2feb1d59633d661a0fdd935c372d06d63967055a08723635999272cb5ff6f50115b06c797bb3a00e5fba61f5d580f365fe4de919d7a23aa9c2
|
|
7
|
+
data.tar.gz: f6e37198ee07a2bf183d6c46ee6c6d51fd638e48f71333c25cb3cb5870725da9ef36efc84439396f392132c3889e5c42835db771dfbafd5f220d3a95435ee0d5
|
data/lib/ask/tools/tool.rb
CHANGED
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "ask-schema"
|
|
4
|
+
|
|
3
5
|
module Ask
|
|
4
|
-
# Base class for defining tools that LLMs can call.
|
|
5
|
-
#
|
|
6
|
-
# Subclass +Ask::Tool+, use the DSL to declare metadata and parameters,
|
|
7
|
-
# and implement +#execute+ to perform the work.
|
|
8
|
-
#
|
|
9
|
-
# class Greeter < Ask::Tool
|
|
10
|
-
# description "Greets a person by name"
|
|
11
|
-
# param :name, type: :string, desc: "The person's name", required: true
|
|
12
|
-
#
|
|
13
|
-
# def execute(name:)
|
|
14
|
-
# Ask::Result.ok(data: "Hello, #{name}!")
|
|
15
|
-
# end
|
|
16
|
-
# end
|
|
17
|
-
#
|
|
18
|
-
# Greeter.new.name # => "greeter"
|
|
19
|
-
# Greeter.new.call(name: "World")
|
|
20
|
-
# # => #<Ask::Result ok=true output="Hello, World!">
|
|
21
|
-
#
|
|
22
6
|
class Tool
|
|
23
|
-
# Raised (or returned from +#call+) to signal the conversation loop
|
|
24
|
-
# should stop rather than continuing after this tool's result.
|
|
25
7
|
class Halt < StandardError
|
|
26
8
|
attr_reader :content
|
|
27
|
-
|
|
28
9
|
def initialize(content)
|
|
29
10
|
@content = content
|
|
30
11
|
super(content.to_s)
|
|
@@ -32,81 +13,65 @@ module Ask
|
|
|
32
13
|
end
|
|
33
14
|
|
|
34
15
|
class << self
|
|
35
|
-
# @api private
|
|
36
16
|
def inherited(subclass)
|
|
37
17
|
super
|
|
38
18
|
@parameters = {} if @parameters.nil?
|
|
39
19
|
subclass.instance_variable_set(:@description, nil)
|
|
40
20
|
subclass.instance_variable_set(:@parameters, {})
|
|
21
|
+
subclass.instance_variable_set(:@params_schema_definition, nil)
|
|
41
22
|
end
|
|
42
23
|
|
|
43
|
-
# Set or retrieve the tool's human-readable description.
|
|
44
|
-
#
|
|
45
|
-
# @param text [String, nil] when provided, sets the description
|
|
46
|
-
# @return [String, nil]
|
|
47
24
|
def description(text = nil)
|
|
48
25
|
return @description unless text
|
|
49
|
-
|
|
50
26
|
@description = text
|
|
51
27
|
end
|
|
52
28
|
alias desc description
|
|
53
29
|
|
|
54
|
-
# Declare a parameter the tool accepts.
|
|
55
|
-
#
|
|
56
|
-
# @param name [Symbol] parameter name
|
|
57
|
-
# @param type [Symbol] JSON Schema type (+:string+, +:integer+, +:number+,
|
|
58
|
-
# +:boolean+, +:array+, +:object+)
|
|
59
|
-
# @param desc [String] human-readable description of the parameter
|
|
60
|
-
# @param required [Boolean] whether the parameter is mandatory
|
|
61
|
-
# @return [void]
|
|
62
30
|
def param(name, type:, desc: nil, description: nil, required: true)
|
|
63
31
|
type = type.to_s.downcase.to_sym
|
|
64
32
|
validate_param_type!(type, name)
|
|
65
33
|
parameters[name] = Parameter.new(
|
|
66
|
-
name: name,
|
|
67
|
-
|
|
68
|
-
description: desc || description,
|
|
69
|
-
required: required
|
|
34
|
+
name: name, type: map_type(type),
|
|
35
|
+
description: desc || description, required: required
|
|
70
36
|
)
|
|
71
37
|
end
|
|
72
38
|
|
|
73
|
-
# @api private
|
|
74
|
-
# @return [Hash{Symbol => Ask::Tool::Parameter}]
|
|
75
|
-
def parameters
|
|
76
|
-
@parameters ||= {}
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
# @api private
|
|
80
|
-
# Define tool parameters using the {Ask::Schema} DSL.
|
|
81
|
-
#
|
|
82
|
-
# When a block is provided, it takes precedence over individual
|
|
83
|
-
# +param+ declarations for schema generation.
|
|
84
|
-
#
|
|
85
|
-
# @example
|
|
86
|
-
# params do
|
|
87
|
-
# string :location, description: "City name"
|
|
88
|
-
# string :unit, enum: %w[celsius fahrenheit]
|
|
89
|
-
# end
|
|
90
|
-
#
|
|
91
|
-
# @param schema [Ask::Schema, Class<Ask::Schema>, Hash, nil] A pre-built schema
|
|
92
|
-
# @param block [Proc] DSL block evaluated by Ask::Schema
|
|
93
|
-
# @return [void]
|
|
94
39
|
def params(schema = nil, &block)
|
|
95
40
|
@params_schema_definition = schema || block
|
|
96
41
|
end
|
|
97
42
|
|
|
43
|
+
def parameters
|
|
44
|
+
@parameters ||= {}
|
|
45
|
+
end
|
|
46
|
+
|
|
98
47
|
def provider_params
|
|
99
48
|
@provider_params ||= {}
|
|
100
49
|
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def validate_param_type!(type, name)
|
|
54
|
+
return if VALID_JSON_SCHEMA_TYPES.include?(type)
|
|
55
|
+
raise ArgumentError,
|
|
56
|
+
"Invalid type #{type.inspect} for parameter #{name.inspect}. " \
|
|
57
|
+
"Valid types: #{VALID_JSON_SCHEMA_TYPES.map(&:inspect).join(', ')}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def map_type(type)
|
|
61
|
+
case type
|
|
62
|
+
when :int then "integer"
|
|
63
|
+
when :float, :double then "number"
|
|
64
|
+
else type.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def provider_params
|
|
70
|
+
self.class.provider_params
|
|
101
71
|
end
|
|
102
72
|
|
|
103
|
-
# Auto-derive the tool name from the class name.
|
|
104
|
-
# Converts CamelCase to snake_case and strips a trailing +_tool+ suffix.
|
|
105
|
-
#
|
|
106
|
-
# @return [String]
|
|
107
73
|
def name
|
|
108
|
-
|
|
109
|
-
klass_name = self.class.name.to_s.split("::").last || self.class.name.to_s
|
|
74
|
+
klass_name = self.class.name.to_s || ""
|
|
110
75
|
normalized = klass_name.dup.force_encoding("UTF-8").unicode_normalize(:nfkd)
|
|
111
76
|
normalized.encode("ASCII", replace: "")
|
|
112
77
|
.gsub(/[^a-zA-Z0-9_-]/, "-")
|
|
@@ -116,157 +81,113 @@ module Ask
|
|
|
116
81
|
.delete_suffix("_tool")
|
|
117
82
|
end
|
|
118
83
|
|
|
119
|
-
# @return [String, nil] the tool's description
|
|
120
84
|
def description
|
|
121
85
|
self.class.description
|
|
122
86
|
end
|
|
123
87
|
|
|
124
|
-
# @return [Hash{Symbol => Ask::Tool::Parameter}]
|
|
125
88
|
def parameters
|
|
126
89
|
self.class.parameters
|
|
127
90
|
end
|
|
128
91
|
|
|
129
|
-
# Call the tool with the given arguments.
|
|
130
|
-
#
|
|
131
|
-
# Normalizes keys to symbols, validates required parameters,
|
|
132
|
-
# and delegates to +#execute+.
|
|
133
|
-
#
|
|
134
|
-
# @param args [Hash, nil] keyword arguments for the tool
|
|
135
|
-
# @return [Ask::Result] the tool's result
|
|
136
92
|
def call(args = {})
|
|
137
93
|
normalized = normalize_args(args)
|
|
138
94
|
validation = validate(normalized)
|
|
139
|
-
return Ask::Result.
|
|
140
|
-
|
|
95
|
+
return Ask::Result.failure(validation) if validation
|
|
141
96
|
execute(**normalized)
|
|
142
97
|
rescue Halt => e
|
|
143
98
|
Ask::Result.ok(data: e.content, metadata: { halted: true })
|
|
144
99
|
rescue StandardError => e
|
|
145
|
-
Ask::Result.
|
|
100
|
+
Ask::Result.failure("#{self.class.name.split('::').last} raised #{e.class}: #{e.message}")
|
|
146
101
|
end
|
|
147
102
|
|
|
148
|
-
# Subclasses must implement this method.
|
|
149
|
-
#
|
|
150
|
-
# @param args [Hash] normalized keyword arguments
|
|
151
|
-
# @return [Ask::Result] the tool's result
|
|
152
103
|
def execute(**)
|
|
153
104
|
raise NotImplementedError, "#{self.class} must implement #execute(**args)"
|
|
154
105
|
end
|
|
155
106
|
|
|
156
|
-
# Generate a JSON Schema hash describing this tool's parameters.
|
|
157
|
-
# Suitable for LLM function-calling APIs (OpenAI, Anthropic, etc.).
|
|
158
|
-
#
|
|
159
|
-
# @return [Hash]
|
|
160
107
|
def params_schema
|
|
161
108
|
return @params_schema if defined?(@params_schema)
|
|
162
|
-
|
|
163
109
|
@params_schema = begin
|
|
164
|
-
if
|
|
165
|
-
|
|
110
|
+
if params_schema_definition
|
|
111
|
+
deep_stringify_keys(resolve_params_schema(params_schema_definition))
|
|
112
|
+
elsif parameters.any?
|
|
113
|
+
build_schema_from_params
|
|
166
114
|
else
|
|
167
|
-
|
|
168
|
-
schema = { type: param.type }
|
|
169
|
-
schema[:description] = param.description if param.description
|
|
170
|
-
schema[:items] = { type: "string" } if param.type == "array"
|
|
171
|
-
[param.name.to_s, schema]
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
required = parameters.select { |_, p| p.required }.keys.map(&:to_s)
|
|
175
|
-
|
|
176
|
-
{
|
|
177
|
-
type: "object",
|
|
178
|
-
properties: properties,
|
|
179
|
-
required: required,
|
|
180
|
-
additionalProperties: false
|
|
181
|
-
}
|
|
115
|
+
nil
|
|
182
116
|
end
|
|
183
117
|
end
|
|
184
118
|
end
|
|
185
119
|
|
|
186
|
-
# Full tool definition hash for LLM API calls.
|
|
187
|
-
#
|
|
188
|
-
# @return [Hash]
|
|
189
120
|
def tool_definition
|
|
190
|
-
defn = {
|
|
191
|
-
name: name,
|
|
192
|
-
description: description
|
|
193
|
-
}
|
|
121
|
+
defn = { name: name, description: description }
|
|
194
122
|
defn[:input_schema] = params_schema if params_schema
|
|
195
123
|
defn
|
|
196
124
|
end
|
|
197
125
|
|
|
198
|
-
# @return [String] inspect string
|
|
199
126
|
def inspect
|
|
200
127
|
"#<#{self.class.name} name=#{name.inspect}>"
|
|
201
128
|
end
|
|
202
129
|
|
|
203
130
|
private
|
|
204
131
|
|
|
132
|
+
def params_schema_definition
|
|
133
|
+
self.class.instance_variable_get(:@params_schema_definition)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def resolve_params_schema(definition)
|
|
137
|
+
case definition
|
|
138
|
+
when Proc
|
|
139
|
+
schema_class = Ask::Schema.create(&definition)
|
|
140
|
+
schema_class.new.to_json_schema.dig(:schema)
|
|
141
|
+
when Hash then definition
|
|
142
|
+
when ->(d) { d.respond_to?(:to_json_schema) }
|
|
143
|
+
definition.to_json_schema.dig(:schema)
|
|
144
|
+
else nil
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def build_schema_from_params
|
|
149
|
+
properties = parameters.to_h do |_name, param|
|
|
150
|
+
schema = { type: param.type }
|
|
151
|
+
schema[:description] = param.description if param.description
|
|
152
|
+
schema[:items] = { type: "string" } if param.type == "array"
|
|
153
|
+
[param.name.to_s, schema]
|
|
154
|
+
end
|
|
155
|
+
required = parameters.select { |_, p| p.required }.keys.map(&:to_s)
|
|
156
|
+
{ type: "object", properties: properties, required: required, additionalProperties: false }
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def deep_stringify_keys(obj)
|
|
160
|
+
case obj
|
|
161
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
|
|
162
|
+
when Array then obj.map { |v| deep_stringify_keys(v) }
|
|
163
|
+
else obj
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
205
167
|
def normalize_args(args)
|
|
206
168
|
return {} if args.nil?
|
|
207
|
-
|
|
208
169
|
args.respond_to?(:transform_keys) ? args.transform_keys(&:to_sym) : {}
|
|
209
170
|
end
|
|
210
171
|
|
|
211
172
|
def validate(normalized)
|
|
173
|
+
return nil if params_schema_definition
|
|
212
174
|
missing = self.class.parameters.select { |_, p| p.required && !normalized.key?(p.name) }
|
|
213
175
|
return "missing required parameters: #{missing.keys.map(&:inspect).join(', ')}" unless missing.empty?
|
|
214
|
-
|
|
215
176
|
unknown = normalized.keys - self.class.parameters.keys
|
|
216
177
|
return "unknown parameters: #{unknown.map(&:inspect).join(', ')}" unless unknown.empty?
|
|
217
|
-
|
|
218
178
|
nil
|
|
219
179
|
end
|
|
220
180
|
|
|
221
181
|
VALID_JSON_SCHEMA_TYPES = %i[string integer number boolean array object].freeze
|
|
222
182
|
|
|
223
|
-
def self.validate_param_type!(type, name)
|
|
224
|
-
return if VALID_JSON_SCHEMA_TYPES.include?(type)
|
|
225
|
-
|
|
226
|
-
raise ArgumentError,
|
|
227
|
-
"Invalid type #{type.inspect} for parameter #{name.inspect}. " \
|
|
228
|
-
"Valid types: #{VALID_JSON_SCHEMA_TYPES.map(&:inspect).join(', ')}"
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
def self.map_type(type)
|
|
232
|
-
case type
|
|
233
|
-
when :int then "integer"
|
|
234
|
-
when :float, :double then "number"
|
|
235
|
-
else type.to_s
|
|
236
|
-
end
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Internal value object for parameter metadata.
|
|
240
183
|
class Parameter
|
|
241
|
-
|
|
242
|
-
attr_reader :name
|
|
243
|
-
|
|
244
|
-
# @return [String] JSON Schema type string
|
|
245
|
-
attr_reader :type
|
|
246
|
-
|
|
247
|
-
# @return [String, nil]
|
|
248
|
-
attr_reader :description
|
|
249
|
-
|
|
250
|
-
# @return [Boolean]
|
|
251
|
-
attr_reader :required
|
|
252
|
-
|
|
184
|
+
attr_reader :name, :type, :description, :required
|
|
253
185
|
alias required? required
|
|
254
|
-
|
|
255
186
|
def initialize(name:, type:, description: nil, required: true)
|
|
256
|
-
@name = name
|
|
257
|
-
@type = type
|
|
258
|
-
@description = description
|
|
259
|
-
@required = required
|
|
187
|
+
@name = name; @type = type; @description = description; @required = required
|
|
260
188
|
end
|
|
261
|
-
|
|
262
|
-
# @return [Hash]
|
|
263
189
|
def to_h
|
|
264
|
-
{
|
|
265
|
-
name: name,
|
|
266
|
-
type: type,
|
|
267
|
-
description: description,
|
|
268
|
-
required: required
|
|
269
|
-
}
|
|
190
|
+
{ name: name, type: type, description: description, required: required }
|
|
270
191
|
end
|
|
271
192
|
end
|
|
272
193
|
end
|
data/lib/ask/version.rb
CHANGED