riffer 0.22.0 → 0.24.0
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/.agents/architecture.md +2 -0
- data/.release-please-manifest.json +1 -1
- data/AGENTS.md +10 -9
- data/CHANGELOG.md +14 -0
- data/Steepfile +5 -2
- data/docs/08_MESSAGES.md +38 -8
- data/docs/10_CONFIGURATION.md +33 -0
- data/docs/providers/01_PROVIDERS.md +6 -0
- data/docs/providers/08_GEMINI.md +142 -0
- data/lib/riffer/agent.rb +22 -3
- data/lib/riffer/config.rb +31 -0
- data/lib/riffer/messages/assistant.rb +4 -3
- data/lib/riffer/messages/base.rb +21 -3
- data/lib/riffer/messages/converter.rb +6 -4
- data/lib/riffer/messages/tool.rb +4 -3
- data/lib/riffer/messages/user.rb +4 -3
- data/lib/riffer/providers/gemini.rb +323 -0
- data/lib/riffer/providers/repository.rb +1 -0
- data/lib/riffer/skills/backend.rb +2 -0
- data/lib/riffer/tool.rb +2 -61
- data/lib/riffer/toolable.rb +140 -0
- data/lib/riffer/version.rb +1 -1
- data/sig/generated/riffer/agent.rbs +4 -0
- data/sig/generated/riffer/config.rbs +33 -0
- data/sig/generated/riffer/messages/assistant.rbs +2 -2
- data/sig/generated/riffer/messages/base.rbs +10 -2
- data/sig/generated/riffer/messages/tool.rbs +2 -2
- data/sig/generated/riffer/messages/user.rbs +2 -2
- data/sig/generated/riffer/providers/gemini.rbs +84 -0
- data/sig/generated/riffer/skills/backend.rbs +2 -0
- data/sig/generated/riffer/tool.rbs +1 -41
- data/sig/generated/riffer/toolable.rbs +99 -0
- metadata +12 -7
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
require "json"
|
|
5
|
+
require "net/http"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
# Google Gemini provider for Gemini models via the Gemini REST API.
|
|
10
|
+
class Riffer::Providers::Gemini < Riffer::Providers::Base
|
|
11
|
+
BASE_URI = URI("https://generativelanguage.googleapis.com") #: URI::Generic
|
|
12
|
+
VALID_MODEL_PATTERN = /\A[a-zA-Z0-9._-]+\z/ #: Regexp
|
|
13
|
+
DEFAULT_OPEN_TIMEOUT = 10 #: Integer
|
|
14
|
+
DEFAULT_READ_TIMEOUT = 60 #: Integer
|
|
15
|
+
|
|
16
|
+
# Initializes the Gemini provider.
|
|
17
|
+
#
|
|
18
|
+
#--
|
|
19
|
+
#: (?api_key: String?, ?open_timeout: Integer?, ?read_timeout: Integer?, **untyped) -> void
|
|
20
|
+
def initialize(api_key: nil, open_timeout: nil, read_timeout: nil, **options)
|
|
21
|
+
api_key ||= Riffer.config.gemini.api_key
|
|
22
|
+
@api_key = api_key
|
|
23
|
+
@open_timeout = open_timeout || Riffer.config.gemini.open_timeout || DEFAULT_OPEN_TIMEOUT
|
|
24
|
+
@read_timeout = read_timeout || Riffer.config.gemini.read_timeout || DEFAULT_READ_TIMEOUT
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
#--
|
|
30
|
+
#: (Array[Riffer::Messages::Base], String?, Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
31
|
+
def build_request_params(messages, model, options)
|
|
32
|
+
partitioned = partition_messages(messages)
|
|
33
|
+
tools = options[:tools]
|
|
34
|
+
structured_output = options[:structured_output]
|
|
35
|
+
|
|
36
|
+
params = {
|
|
37
|
+
model: model,
|
|
38
|
+
contents: partitioned[:contents]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
params[:systemInstruction] = partitioned[:system_instruction] if partitioned[:system_instruction]
|
|
42
|
+
|
|
43
|
+
if tools && !tools.empty?
|
|
44
|
+
params[:tools] = [{
|
|
45
|
+
functionDeclarations: tools.map { |t| convert_tool_to_gemini_format(t) }
|
|
46
|
+
}]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
generation_config = options.except(:tools, :structured_output)
|
|
50
|
+
|
|
51
|
+
if structured_output
|
|
52
|
+
generation_config[:responseMimeType] = "application/json"
|
|
53
|
+
generation_config[:responseSchema] = strip_additional_properties(structured_output.json_schema)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
params[:generationConfig] = generation_config unless generation_config.empty?
|
|
57
|
+
|
|
58
|
+
params
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#--
|
|
62
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
63
|
+
def execute_generate(params)
|
|
64
|
+
model = params[:model]
|
|
65
|
+
body = params.except(:model)
|
|
66
|
+
response = post_request(api_path(model, "generateContent"), body)
|
|
67
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
68
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
#--
|
|
72
|
+
#: (Hash[Symbol, untyped]) -> String
|
|
73
|
+
def extract_content(response)
|
|
74
|
+
parts = response.dig(:candidates, 0, :content, :parts)
|
|
75
|
+
return "" unless parts
|
|
76
|
+
|
|
77
|
+
parts.filter_map { |part| part[:text] }.join
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
#--
|
|
81
|
+
#: (Hash[Symbol, untyped]) -> Array[Riffer::Messages::Assistant::ToolCall]
|
|
82
|
+
def extract_tool_calls(response)
|
|
83
|
+
parts = response.dig(:candidates, 0, :content, :parts)
|
|
84
|
+
return [] unless parts
|
|
85
|
+
|
|
86
|
+
parts.filter_map do |part|
|
|
87
|
+
next unless part[:functionCall]
|
|
88
|
+
|
|
89
|
+
fc = part[:functionCall]
|
|
90
|
+
Riffer::Messages::Assistant::ToolCall.new(
|
|
91
|
+
call_id: "gemini_call_#{SecureRandom.hex(12)}",
|
|
92
|
+
name: fc[:name],
|
|
93
|
+
arguments: encode_tool_arguments(fc[:args])
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
#--
|
|
99
|
+
#: (Hash[Symbol, untyped]) -> Riffer::TokenUsage?
|
|
100
|
+
def extract_token_usage(response)
|
|
101
|
+
usage = response[:usageMetadata]
|
|
102
|
+
return nil unless usage
|
|
103
|
+
|
|
104
|
+
Riffer::TokenUsage.new(
|
|
105
|
+
input_tokens: usage[:promptTokenCount] || 0,
|
|
106
|
+
output_tokens: usage[:candidatesTokenCount] || 0
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
#--
|
|
111
|
+
#: (Hash[Symbol, untyped], Enumerator::Yielder) -> void
|
|
112
|
+
def execute_stream(params, yielder)
|
|
113
|
+
model = params[:model]
|
|
114
|
+
body = params.except(:model)
|
|
115
|
+
|
|
116
|
+
uri = URI("#{BASE_URI}/#{api_path(model, "streamGenerateContent")}?alt=sse")
|
|
117
|
+
request = Net::HTTP::Post.new(uri)
|
|
118
|
+
request["Content-Type"] = "application/json"
|
|
119
|
+
request["x-goog-api-key"] = @api_key
|
|
120
|
+
request.body = body.to_json
|
|
121
|
+
|
|
122
|
+
full_text = +""
|
|
123
|
+
buffer = +""
|
|
124
|
+
|
|
125
|
+
process_chunk = lambda do |chunk|
|
|
126
|
+
buffer << chunk
|
|
127
|
+
|
|
128
|
+
while (match = buffer.match(/\r?\n\r?\n/))
|
|
129
|
+
frame = buffer.slice!(0, match.end(0)).strip
|
|
130
|
+
next unless frame.start_with?("data: ")
|
|
131
|
+
|
|
132
|
+
json_str = frame.delete_prefix("data: ").strip
|
|
133
|
+
next if json_str.empty?
|
|
134
|
+
|
|
135
|
+
parsed = JSON.parse(json_str, symbolize_names: true)
|
|
136
|
+
parts = parsed.dig(:candidates, 0, :content, :parts)
|
|
137
|
+
|
|
138
|
+
parts&.each do |part|
|
|
139
|
+
if part[:text]
|
|
140
|
+
full_text << part[:text]
|
|
141
|
+
yielder << Riffer::StreamEvents::TextDelta.new(part[:text])
|
|
142
|
+
elsif part[:functionCall]
|
|
143
|
+
fc = part[:functionCall]
|
|
144
|
+
call_id = "gemini_call_#{SecureRandom.hex(12)}"
|
|
145
|
+
arguments = encode_tool_arguments(fc[:args])
|
|
146
|
+
yielder << Riffer::StreamEvents::ToolCallDone.new(
|
|
147
|
+
item_id: call_id,
|
|
148
|
+
call_id: call_id,
|
|
149
|
+
name: fc[:name],
|
|
150
|
+
arguments: arguments
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
usage = parsed[:usageMetadata]
|
|
156
|
+
if usage && usage[:candidatesTokenCount]
|
|
157
|
+
yielder << Riffer::StreamEvents::TokenUsageDone.new(
|
|
158
|
+
token_usage: Riffer::TokenUsage.new(
|
|
159
|
+
input_tokens: usage[:promptTokenCount] || 0,
|
|
160
|
+
output_tokens: usage[:candidatesTokenCount] || 0
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) do |http|
|
|
168
|
+
http.request(request) do |response|
|
|
169
|
+
handle_api_error!(response) unless response.is_a?(Net::HTTPSuccess)
|
|
170
|
+
|
|
171
|
+
begin
|
|
172
|
+
response.read_body { |chunk| process_chunk.call(chunk) }
|
|
173
|
+
rescue IOError
|
|
174
|
+
process_chunk.call(response.body)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
yielder << Riffer::StreamEvents::TextDone.new(full_text) unless full_text.empty?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#--
|
|
183
|
+
#: (Array[Riffer::Messages::Base]) -> Hash[Symbol, untyped]
|
|
184
|
+
def partition_messages(messages)
|
|
185
|
+
system_parts = []
|
|
186
|
+
contents = []
|
|
187
|
+
|
|
188
|
+
messages.each do |message|
|
|
189
|
+
case message
|
|
190
|
+
when Riffer::Messages::System
|
|
191
|
+
system_parts << {text: message.content}
|
|
192
|
+
when Riffer::Messages::User
|
|
193
|
+
if message.files.empty?
|
|
194
|
+
contents << {role: "user", parts: [{text: message.content}]}
|
|
195
|
+
else
|
|
196
|
+
parts = [{text: message.content}]
|
|
197
|
+
message.files.each { |file| parts << convert_file_part_to_gemini_format(file) }
|
|
198
|
+
contents << {role: "user", parts: parts}
|
|
199
|
+
end
|
|
200
|
+
when Riffer::Messages::Assistant
|
|
201
|
+
contents << convert_assistant_to_gemini_format(message)
|
|
202
|
+
when Riffer::Messages::Tool
|
|
203
|
+
contents << {
|
|
204
|
+
role: "user",
|
|
205
|
+
parts: [{
|
|
206
|
+
functionResponse: {
|
|
207
|
+
name: message.name,
|
|
208
|
+
response: {result: message.content}
|
|
209
|
+
}
|
|
210
|
+
}]
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
result = {contents: contents}
|
|
216
|
+
result[:system_instruction] = {parts: system_parts} unless system_parts.empty?
|
|
217
|
+
result
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
#--
|
|
221
|
+
#: (Riffer::Messages::Assistant) -> Hash[Symbol, untyped]
|
|
222
|
+
def convert_assistant_to_gemini_format(message)
|
|
223
|
+
parts = []
|
|
224
|
+
parts << {text: message.content} if message.content && !message.content.empty?
|
|
225
|
+
|
|
226
|
+
message.tool_calls.each do |tc|
|
|
227
|
+
parts << {
|
|
228
|
+
functionCall: {
|
|
229
|
+
name: tc.name,
|
|
230
|
+
args: parse_tool_arguments(tc.arguments)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
{role: "model", parts: parts}
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
#--
|
|
239
|
+
#: (Riffer::FilePart) -> Hash[Symbol, untyped]
|
|
240
|
+
def convert_file_part_to_gemini_format(file)
|
|
241
|
+
if file.url?
|
|
242
|
+
raise Riffer::ArgumentError,
|
|
243
|
+
"Gemini provider does not support URL-based file references. Provide base64-encoded data instead."
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
{inlineData: {mimeType: file.media_type, data: file.data}}
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#--
|
|
250
|
+
#: (singleton(Riffer::Tool)) -> Hash[Symbol, untyped]
|
|
251
|
+
def convert_tool_to_gemini_format(tool)
|
|
252
|
+
{
|
|
253
|
+
name: tool.name,
|
|
254
|
+
description: tool.description,
|
|
255
|
+
parameters: strip_additional_properties(tool.parameters_schema)
|
|
256
|
+
}
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
#--
|
|
260
|
+
#: (untyped) -> String
|
|
261
|
+
def encode_tool_arguments(args)
|
|
262
|
+
return "{}" unless args
|
|
263
|
+
|
|
264
|
+
args.is_a?(String) ? args : args.to_json
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
#--
|
|
268
|
+
#: (String, Hash[Symbol, untyped]) -> Net::HTTPResponse
|
|
269
|
+
def post_request(path, body)
|
|
270
|
+
uri = URI("#{BASE_URI}/#{path}")
|
|
271
|
+
request = Net::HTTP::Post.new(uri)
|
|
272
|
+
request["Content-Type"] = "application/json"
|
|
273
|
+
request["x-goog-api-key"] = @api_key
|
|
274
|
+
request.body = body.to_json
|
|
275
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, open_timeout: @open_timeout, read_timeout: @read_timeout) { |http| http.request(request) }
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
#--
|
|
279
|
+
#: (String, String) -> String
|
|
280
|
+
def api_path(model, method)
|
|
281
|
+
validate_model!(model)
|
|
282
|
+
"v1beta/models/#{model}:#{method}"
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
#--
|
|
286
|
+
#: (String) -> void
|
|
287
|
+
def validate_model!(model)
|
|
288
|
+
return if model.match?(VALID_MODEL_PATTERN)
|
|
289
|
+
|
|
290
|
+
raise Riffer::ArgumentError, "Invalid model name: #{model.inspect}. Model must contain only alphanumeric characters, hyphens, dots, and underscores."
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
#--
|
|
294
|
+
#: (Hash[Symbol, untyped]) -> Hash[Symbol, untyped]
|
|
295
|
+
def strip_additional_properties(schema)
|
|
296
|
+
schema = schema.dup
|
|
297
|
+
schema.delete(:additionalProperties)
|
|
298
|
+
|
|
299
|
+
if schema[:properties]
|
|
300
|
+
schema[:properties] = schema[:properties].transform_values do |prop|
|
|
301
|
+
strip_additional_properties(prop)
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
if schema[:items].is_a?(Hash)
|
|
306
|
+
schema[:items] = strip_additional_properties(schema[:items])
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
schema
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
#--
|
|
313
|
+
#: (Net::HTTPResponse) -> void
|
|
314
|
+
def handle_api_error!(response)
|
|
315
|
+
body = begin
|
|
316
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
317
|
+
rescue JSON::ParserError
|
|
318
|
+
{message: response.body}
|
|
319
|
+
end
|
|
320
|
+
error_message = body.dig(:error, :message) || body[:message] || response.body
|
|
321
|
+
raise Riffer::Error, "Gemini API error (#{response.code}): #{error_message}"
|
|
322
|
+
end
|
|
323
|
+
end
|
|
@@ -8,6 +8,7 @@ class Riffer::Providers::Repository
|
|
|
8
8
|
amazon_bedrock: -> { Riffer::Providers::AmazonBedrock },
|
|
9
9
|
anthropic: -> { Riffer::Providers::Anthropic },
|
|
10
10
|
azure_openai: -> { Riffer::Providers::AzureOpenAI },
|
|
11
|
+
gemini: -> { Riffer::Providers::Gemini },
|
|
11
12
|
openai: -> { Riffer::Providers::OpenAI },
|
|
12
13
|
mock: -> { Riffer::Providers::Mock }
|
|
13
14
|
}.freeze #: Hash[Symbol, ^() -> singleton(Riffer::Providers::Base)]
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -24,68 +24,9 @@ require "timeout"
|
|
|
24
24
|
# end
|
|
25
25
|
#
|
|
26
26
|
class Riffer::Tool
|
|
27
|
-
|
|
27
|
+
extend Riffer::Toolable
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
# Gets or sets the tool description.
|
|
32
|
-
#
|
|
33
|
-
#--
|
|
34
|
-
#: (?String?) -> String?
|
|
35
|
-
def self.description(value = nil)
|
|
36
|
-
return @description if value.nil?
|
|
37
|
-
@description = value.to_s
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Gets or sets the tool identifier/name.
|
|
41
|
-
#
|
|
42
|
-
#--
|
|
43
|
-
#: (?String?) -> String
|
|
44
|
-
def self.identifier(value = nil)
|
|
45
|
-
return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
|
|
46
|
-
@identifier = value.to_s
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Alias for identifier - used by providers.
|
|
50
|
-
#
|
|
51
|
-
#--
|
|
52
|
-
#: (?String?) -> String
|
|
53
|
-
def self.name(value = nil)
|
|
54
|
-
return identifier(value) unless value.nil?
|
|
55
|
-
identifier
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Gets or sets the tool timeout in seconds.
|
|
59
|
-
#
|
|
60
|
-
#--
|
|
61
|
-
#: (?(Integer | Float)?) -> (Integer | Float)
|
|
62
|
-
def self.timeout(value = nil)
|
|
63
|
-
return @timeout || DEFAULT_TIMEOUT if value.nil?
|
|
64
|
-
@timeout = value.to_f
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
# Defines parameters using the Params DSL.
|
|
68
|
-
#
|
|
69
|
-
#--
|
|
70
|
-
#: () ?{ () -> void } -> Riffer::Params?
|
|
71
|
-
def self.params(&block)
|
|
72
|
-
return @params_builder if block.nil?
|
|
73
|
-
@params_builder = Riffer::Params.new
|
|
74
|
-
@params_builder.instance_eval(&block)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# Returns the JSON Schema for the tool's parameters.
|
|
78
|
-
#
|
|
79
|
-
#--
|
|
80
|
-
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
81
|
-
def self.parameters_schema(strict: false)
|
|
82
|
-
@params_builder&.to_json_schema(strict: strict) || empty_schema
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def self.empty_schema # :nodoc:
|
|
86
|
-
{type: "object", properties: {}, required: [], additionalProperties: false}
|
|
87
|
-
end
|
|
88
|
-
private_class_method :empty_schema
|
|
29
|
+
kind :tool
|
|
89
30
|
|
|
90
31
|
# Executes the tool with the given arguments.
|
|
91
32
|
#
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Riffer::Toolable provides the shared class-level DSL for anything that can
|
|
5
|
+
# present as a tool to an LLM — tools today, and subagents/workflows in the
|
|
6
|
+
# future.
|
|
7
|
+
#
|
|
8
|
+
# Extend this module to make a class discoverable as a tool by LLM providers.
|
|
9
|
+
# Provides identifier, description, params, timeout, and JSON schema
|
|
10
|
+
# generation.
|
|
11
|
+
#
|
|
12
|
+
# Instance-level execution concerns (+call+, +call_with_validation+, etc.)
|
|
13
|
+
# are NOT part of Toolable — those belong on Riffer::Tool.
|
|
14
|
+
#
|
|
15
|
+
# class MyTool
|
|
16
|
+
# extend Riffer::Toolable
|
|
17
|
+
#
|
|
18
|
+
# description "Does something useful"
|
|
19
|
+
#
|
|
20
|
+
# params do
|
|
21
|
+
# required :input, String
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
module Riffer::Toolable
|
|
26
|
+
DEFAULT_TIMEOUT = 10 #: Integer
|
|
27
|
+
|
|
28
|
+
# Tracks all classes that extend Toolable.
|
|
29
|
+
#
|
|
30
|
+
#--
|
|
31
|
+
#: (Module) -> void
|
|
32
|
+
def self.extended(base)
|
|
33
|
+
base.extend Riffer::Helpers::ClassNameConverter
|
|
34
|
+
@extenders ||= []
|
|
35
|
+
@extenders << base
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Returns all classes that have extended Toolable.
|
|
39
|
+
#
|
|
40
|
+
#--
|
|
41
|
+
#: () -> Array[Module]
|
|
42
|
+
def self.all
|
|
43
|
+
@extenders || []
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Gets or sets the tool description.
|
|
47
|
+
#
|
|
48
|
+
#--
|
|
49
|
+
#: (?String?) -> String?
|
|
50
|
+
def description(value = nil)
|
|
51
|
+
return @description if value.nil?
|
|
52
|
+
@description = value.to_s
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Gets or sets the tool identifier/name.
|
|
56
|
+
#
|
|
57
|
+
#--
|
|
58
|
+
#: (?String?) -> String
|
|
59
|
+
def identifier(value = nil)
|
|
60
|
+
return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
|
|
61
|
+
@identifier = value.to_s
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Alias for identifier — used by providers.
|
|
65
|
+
#
|
|
66
|
+
#--
|
|
67
|
+
#: (?String?) -> String
|
|
68
|
+
def name(value = nil)
|
|
69
|
+
return identifier(value) unless value.nil?
|
|
70
|
+
identifier
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Gets or sets the tool timeout in seconds.
|
|
74
|
+
#
|
|
75
|
+
#--
|
|
76
|
+
#: (?(Integer | Float)?) -> (Integer | Float)
|
|
77
|
+
def timeout(value = nil)
|
|
78
|
+
return @timeout || DEFAULT_TIMEOUT if value.nil?
|
|
79
|
+
@timeout = value.to_f
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Defines parameters using the Params DSL.
|
|
83
|
+
#
|
|
84
|
+
#--
|
|
85
|
+
#: () ?{ () -> void } -> Riffer::Params?
|
|
86
|
+
def params(&block)
|
|
87
|
+
return @params_builder if block.nil?
|
|
88
|
+
@params_builder = Riffer::Params.new
|
|
89
|
+
@params_builder.instance_eval(&block)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the JSON Schema for the tool's parameters.
|
|
93
|
+
#
|
|
94
|
+
#--
|
|
95
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
96
|
+
def parameters_schema(strict: false)
|
|
97
|
+
@params_builder&.to_json_schema(strict: strict) || empty_schema
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns the kind of toolable entity.
|
|
101
|
+
#
|
|
102
|
+
# Defaults to +:tool+. Extensible to +:agent+, +:workflow+, etc.
|
|
103
|
+
#
|
|
104
|
+
#--
|
|
105
|
+
#: (?Symbol?) -> Symbol
|
|
106
|
+
def kind(value = nil)
|
|
107
|
+
return @kind || :tool if value.nil?
|
|
108
|
+
@kind = value.to_sym
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns a provider-agnostic tool schema hash.
|
|
112
|
+
#
|
|
113
|
+
#--
|
|
114
|
+
#: (?strict: bool) -> Hash[Symbol, untyped]
|
|
115
|
+
def to_tool_schema(strict: false)
|
|
116
|
+
{
|
|
117
|
+
name: name,
|
|
118
|
+
description: description,
|
|
119
|
+
parameters_schema: parameters_schema(strict: strict)
|
|
120
|
+
}
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Validates that the minimum required metadata is present for LLM tool use.
|
|
124
|
+
#
|
|
125
|
+
# Raises Riffer::ArgumentError if validation fails.
|
|
126
|
+
#
|
|
127
|
+
#--
|
|
128
|
+
#: () -> true
|
|
129
|
+
def validate_as_tool!
|
|
130
|
+
raise Riffer::ArgumentError, "#{self} must define a description" if description.nil? || description.to_s.strip.empty?
|
|
131
|
+
raise Riffer::ArgumentError, "#{self} must have an identifier" if identifier.nil? || identifier.to_s.strip.empty?
|
|
132
|
+
true
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
private
|
|
136
|
+
|
|
137
|
+
def empty_schema # :nodoc:
|
|
138
|
+
{type: "object", properties: {}, required: [], additionalProperties: false}
|
|
139
|
+
end
|
|
140
|
+
end
|
data/lib/riffer/version.rb
CHANGED
|
@@ -247,6 +247,10 @@ class Riffer::Agent
|
|
|
247
247
|
# : ((String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base]), ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
|
|
248
248
|
def initialize_messages: (String | Array[Hash[Symbol, untyped] | Riffer::Messages::Base], ?files: Array[Hash[Symbol, untyped] | Riffer::FilePart]?) -> void
|
|
249
249
|
|
|
250
|
+
# --
|
|
251
|
+
# : (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
|
|
252
|
+
def validate_seed_ids!: (Array[Hash[Symbol, untyped] | Riffer::Messages::Base]) -> void
|
|
253
|
+
|
|
250
254
|
# --
|
|
251
255
|
# : (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
|
|
252
256
|
def build_instruction_message: (?Hash[Symbol, untyped]?) -> Riffer::Messages::System?
|
|
@@ -38,6 +38,17 @@ class Riffer::Config
|
|
|
38
38
|
| ({ ?api_key: untyped, ?endpoint: untyped }) -> instance
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
+
class Gemini < Struct[untyped]
|
|
42
|
+
attr_accessor api_key(): untyped
|
|
43
|
+
|
|
44
|
+
attr_accessor open_timeout(): untyped
|
|
45
|
+
|
|
46
|
+
attr_accessor read_timeout(): untyped
|
|
47
|
+
|
|
48
|
+
def self.new: (?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped) -> instance
|
|
49
|
+
| ({ ?api_key: untyped, ?open_timeout: untyped, ?read_timeout: untyped }) -> instance
|
|
50
|
+
end
|
|
51
|
+
|
|
41
52
|
class OpenAI < Struct[untyped]
|
|
42
53
|
attr_accessor api_key(): untyped
|
|
43
54
|
|
|
@@ -52,6 +63,8 @@ class Riffer::Config
|
|
|
52
63
|
| ({ ?judge_model: untyped }) -> instance
|
|
53
64
|
end
|
|
54
65
|
|
|
66
|
+
VALID_MESSAGE_ID_STRATEGIES: untyped
|
|
67
|
+
|
|
55
68
|
# Amazon Bedrock configuration (Struct with +api_token+ and +region+).
|
|
56
69
|
attr_reader amazon_bedrock: Riffer::Config::AmazonBedrock
|
|
57
70
|
|
|
@@ -61,6 +74,9 @@ class Riffer::Config
|
|
|
61
74
|
# Azure OpenAI configuration (Struct with +api_key+ and +endpoint+).
|
|
62
75
|
attr_reader azure_openai: Riffer::Config::AzureOpenAI
|
|
63
76
|
|
|
77
|
+
# Google Gemini configuration (Struct with +api_key+, +open_timeout+, and +read_timeout+).
|
|
78
|
+
attr_reader gemini: Riffer::Config::Gemini
|
|
79
|
+
|
|
64
80
|
# OpenAI configuration (Struct with +api_key+).
|
|
65
81
|
attr_reader openai: Riffer::Config::OpenAI
|
|
66
82
|
|
|
@@ -82,6 +98,23 @@ class Riffer::Config
|
|
|
82
98
|
# : ((singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc)) -> void
|
|
83
99
|
def tool_runtime=: (singleton(Riffer::ToolRuntime) | Riffer::ToolRuntime | Proc) -> void
|
|
84
100
|
|
|
101
|
+
# Strategy for auto-generating message ids. One of +:none+ (default, no id),
|
|
102
|
+
# +:uuid+ (UUIDv4), or +:uuidv7+ (time-ordered UUIDv7).
|
|
103
|
+
#
|
|
104
|
+
# When set to anything other than +:none+, each +Riffer::Messages::Base+
|
|
105
|
+
# instance gets an +id+ populated at construction time, and seeded messages
|
|
106
|
+
# passed to +Riffer::Agent#generate+ must carry their own +:id+.
|
|
107
|
+
attr_reader message_id_strategy: Symbol
|
|
108
|
+
|
|
109
|
+
# Sets the message id strategy.
|
|
110
|
+
#
|
|
111
|
+
# Raises +Riffer::ArgumentError+ if the value is not one of
|
|
112
|
+
# +:none+, +:uuid+, or +:uuidv7+.
|
|
113
|
+
#
|
|
114
|
+
# --
|
|
115
|
+
# : (Symbol) -> void
|
|
116
|
+
def message_id_strategy=: (Symbol) -> void
|
|
117
|
+
|
|
85
118
|
# --
|
|
86
119
|
# : () -> void
|
|
87
120
|
def initialize: () -> void
|
|
@@ -30,8 +30,8 @@ class Riffer::Messages::Assistant < Riffer::Messages::Base
|
|
|
30
30
|
attr_reader structured_output: Hash[Symbol, untyped]?
|
|
31
31
|
|
|
32
32
|
# --
|
|
33
|
-
# : (String, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
34
|
-
def initialize: (String, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
33
|
+
# : (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
34
|
+
def initialize: (String, ?id: String?, ?tool_calls: Array[Riffer::Messages::Assistant::ToolCall], ?token_usage: Riffer::TokenUsage?, ?structured_output: Hash[Symbol, untyped]?) -> void
|
|
35
35
|
|
|
36
36
|
# --
|
|
37
37
|
# : () -> Symbol
|
|
@@ -7,9 +7,12 @@ class Riffer::Messages::Base
|
|
|
7
7
|
# The message content.
|
|
8
8
|
attr_reader content: String
|
|
9
9
|
|
|
10
|
+
# The message id, or nil when +Riffer.config.message_id_strategy+ is +:none+.
|
|
11
|
+
attr_reader id: String?
|
|
12
|
+
|
|
10
13
|
# --
|
|
11
|
-
# : (String) -> void
|
|
12
|
-
def initialize: (String) -> void
|
|
14
|
+
# : (String, ?id: String?) -> void
|
|
15
|
+
def initialize: (String, ?id: String?) -> void
|
|
13
16
|
|
|
14
17
|
# Converts the message to a hash.
|
|
15
18
|
#
|
|
@@ -24,4 +27,9 @@ class Riffer::Messages::Base
|
|
|
24
27
|
# --
|
|
25
28
|
# : () -> Symbol
|
|
26
29
|
def role: () -> Symbol
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# : () -> String?
|
|
34
|
+
def generate_id: () -> String?
|
|
27
35
|
end
|
|
@@ -24,8 +24,8 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
|
|
|
24
24
|
attr_reader error_type: Symbol?
|
|
25
25
|
|
|
26
26
|
# --
|
|
27
|
-
# : (String, tool_call_id: String, name: String, ?error: String?, ?error_type: Symbol?) -> void
|
|
28
|
-
def initialize: (String, tool_call_id: String, name: String, ?error: String?, ?error_type: Symbol?) -> void
|
|
27
|
+
# : (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
|
|
28
|
+
def initialize: (String, tool_call_id: String, name: String, ?id: String?, ?error: String?, ?error_type: Symbol?) -> void
|
|
29
29
|
|
|
30
30
|
# Returns true if the tool execution resulted in an error.
|
|
31
31
|
#
|
|
@@ -15,8 +15,8 @@ class Riffer::Messages::User < Riffer::Messages::Base
|
|
|
15
15
|
# Initializes a user message.
|
|
16
16
|
#
|
|
17
17
|
# --
|
|
18
|
-
# : (String, ?files: Array[Riffer::FilePart]) -> void
|
|
19
|
-
def initialize: (String, ?files: Array[Riffer::FilePart]) -> void
|
|
18
|
+
# : (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
|
|
19
|
+
def initialize: (String, ?id: String?, ?files: Array[Riffer::FilePart]) -> void
|
|
20
20
|
|
|
21
21
|
# --
|
|
22
22
|
# : () -> Symbol
|