riffer 0.8.0 → 0.9.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/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +7 -0
- data/docs/04_TOOLS.md +106 -15
- data/lib/riffer/agent.rb +11 -25
- data/lib/riffer/tool.rb +37 -2
- data/lib/riffer/tools/response.rb +90 -0
- data/lib/riffer/tools.rb +2 -1
- data/lib/riffer/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7aa794200e1e9eef7fff84170952900baa69535798e7bed5b67990a886edc587
|
|
4
|
+
data.tar.gz: 0cad248bb54b1814b184f4727900f22955cc62abf0b76789ef4099824d05d4c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3dceae48df77f99164eeabd11b9f29e95a980d7efbc2472ee5192512e46467dd6b17f4b2dbf3869eeb9b4a45cee18996fda7bbd6b88813e0563ee22484931ea9
|
|
7
|
+
data.tar.gz: 9ca7a462a2cdcefa0d5576b1004f5f7d70160c4ac977d4343e81c64eb61621f667f478a2bb9807ad1d348d79dde8238dbadb4f26d69cc52549538a8102ff78a0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.9.0](https://github.com/janeapp/riffer/compare/riffer/v0.8.0...riffer/v0.9.0) (2026-01-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* implement Riffer::Tools::Response for consistent tool result handling ([#91](https://github.com/janeapp/riffer/issues/91)) ([df44f1f](https://github.com/janeapp/riffer/commit/df44f1fe8ff0b5bea73a2df8d6c0b8359e6c47f3))
|
|
14
|
+
|
|
8
15
|
## [0.8.0](https://github.com/janeapp/riffer/compare/riffer/v0.7.0...riffer/v0.8.0) (2026-01-26)
|
|
9
16
|
|
|
10
17
|
|
data/docs/04_TOOLS.md
CHANGED
|
@@ -17,7 +17,7 @@ class WeatherTool < Riffer::Tool
|
|
|
17
17
|
|
|
18
18
|
def call(context:, city:, units: nil)
|
|
19
19
|
weather = WeatherAPI.fetch(city, units: units || "celsius")
|
|
20
|
-
"The weather in #{city} is #{weather.temperature} #{units}."
|
|
20
|
+
text("The weather in #{city} is #{weather.temperature} #{units}.")
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
```
|
|
@@ -110,12 +110,14 @@ Options:
|
|
|
110
110
|
|
|
111
111
|
## The call Method
|
|
112
112
|
|
|
113
|
-
Every tool must implement the `call` method
|
|
113
|
+
Every tool must implement the `call` method and return a `Riffer::Tools::Response`:
|
|
114
114
|
|
|
115
115
|
```ruby
|
|
116
116
|
def call(context:, **kwargs)
|
|
117
117
|
# context - The tool_context passed to agent.generate()
|
|
118
118
|
# kwargs - Validated parameters
|
|
119
|
+
#
|
|
120
|
+
# Must return a Riffer::Tools::Response
|
|
119
121
|
end
|
|
120
122
|
```
|
|
121
123
|
|
|
@@ -129,10 +131,12 @@ class UserOrdersTool < Riffer::Tool
|
|
|
129
131
|
|
|
130
132
|
def call(context:)
|
|
131
133
|
user_id = context&.dig(:user_id)
|
|
132
|
-
|
|
134
|
+
unless user_id
|
|
135
|
+
return error("No user ID provided")
|
|
136
|
+
end
|
|
133
137
|
|
|
134
138
|
orders = Order.where(user_id: user_id)
|
|
135
|
-
orders.map(&:to_s).join("\n")
|
|
139
|
+
text(orders.map(&:to_s).join("\n"))
|
|
136
140
|
end
|
|
137
141
|
end
|
|
138
142
|
|
|
@@ -140,22 +144,104 @@ end
|
|
|
140
144
|
agent.generate("Show my orders", tool_context: {user_id: 123})
|
|
141
145
|
```
|
|
142
146
|
|
|
143
|
-
|
|
147
|
+
## Response Objects
|
|
148
|
+
|
|
149
|
+
All tools must return a `Riffer::Tools::Response` object from their `call` method. Riffer::Tool provides shorthand methods for creating responses.
|
|
150
|
+
|
|
151
|
+
### Success Responses
|
|
144
152
|
|
|
145
|
-
|
|
153
|
+
Use `text` for string responses and `json` for structured data:
|
|
146
154
|
|
|
147
155
|
```ruby
|
|
148
156
|
def call(context:, query:)
|
|
149
157
|
results = Database.search(query)
|
|
150
158
|
|
|
151
159
|
if results.empty?
|
|
152
|
-
"No results found for '#{query}'"
|
|
160
|
+
text("No results found for '#{query}'")
|
|
153
161
|
else
|
|
154
|
-
results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n")
|
|
162
|
+
text(results.map { |r| "- #{r.title}: #{r.summary}" }.join("\n"))
|
|
155
163
|
end
|
|
156
164
|
end
|
|
157
165
|
```
|
|
158
166
|
|
|
167
|
+
#### text
|
|
168
|
+
|
|
169
|
+
Converts the result to a string via `to_s`:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
text("Hello, world!")
|
|
173
|
+
# => content: "Hello, world!"
|
|
174
|
+
|
|
175
|
+
text(42)
|
|
176
|
+
# => content: "42"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
#### json
|
|
180
|
+
|
|
181
|
+
Converts the result to JSON via `to_json`:
|
|
182
|
+
|
|
183
|
+
```ruby
|
|
184
|
+
json({name: "Alice", age: 30})
|
|
185
|
+
# => content: '{"name":"Alice","age":30}'
|
|
186
|
+
|
|
187
|
+
json([1, 2, 3])
|
|
188
|
+
# => content: '[1,2,3]'
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Error Responses
|
|
192
|
+
|
|
193
|
+
Use `error(message, type:)` for errors:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
def call(context:, user_id:)
|
|
197
|
+
user = User.find_by(id: user_id)
|
|
198
|
+
|
|
199
|
+
unless user
|
|
200
|
+
return error("User not found", type: :not_found)
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
text("User: #{user.name}")
|
|
204
|
+
end
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The error type is any symbol that describes the error category:
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
error("Invalid input", type: :validation_error)
|
|
211
|
+
error("Service unavailable", type: :service_error)
|
|
212
|
+
error("Rate limit exceeded", type: :rate_limit)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
If no type is specified, it defaults to `:execution_error`.
|
|
216
|
+
|
|
217
|
+
### Using Riffer::Tools::Response Directly
|
|
218
|
+
|
|
219
|
+
The shorthand methods delegate to `Riffer::Tools::Response`. You can also use the class directly if preferred:
|
|
220
|
+
|
|
221
|
+
```ruby
|
|
222
|
+
Riffer::Tools::Response.text("Hello")
|
|
223
|
+
Riffer::Tools::Response.json({data: [1, 2, 3]})
|
|
224
|
+
Riffer::Tools::Response.error("Failed", type: :custom_error)
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Response Methods
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
response = text("result")
|
|
231
|
+
response.content # => "result"
|
|
232
|
+
response.success? # => true
|
|
233
|
+
response.error? # => false
|
|
234
|
+
response.error_message # => nil
|
|
235
|
+
response.error_type # => nil
|
|
236
|
+
|
|
237
|
+
error_response = error("failed", type: :not_found)
|
|
238
|
+
error_response.content # => "failed"
|
|
239
|
+
error_response.success? # => false
|
|
240
|
+
error_response.error? # => true
|
|
241
|
+
error_response.error_message # => "failed"
|
|
242
|
+
error_response.error_type # => :not_found
|
|
243
|
+
```
|
|
244
|
+
|
|
159
245
|
## Timeout Configuration
|
|
160
246
|
|
|
161
247
|
Configure timeouts to prevent tools from running indefinitely. The default timeout is 10 seconds.
|
|
@@ -166,7 +252,8 @@ class SlowExternalApiTool < Riffer::Tool
|
|
|
166
252
|
timeout 30 # 30 seconds
|
|
167
253
|
|
|
168
254
|
def call(context:, query:)
|
|
169
|
-
ExternalAPI.search(query)
|
|
255
|
+
result = ExternalAPI.search(query)
|
|
256
|
+
text(result)
|
|
170
257
|
end
|
|
171
258
|
end
|
|
172
259
|
```
|
|
@@ -181,7 +268,7 @@ Arguments are automatically validated before `call` is invoked:
|
|
|
181
268
|
- Types must match the schema
|
|
182
269
|
- Enum values must be in the allowed list
|
|
183
270
|
|
|
184
|
-
Validation errors are captured and sent back to the LLM as tool results
|
|
271
|
+
Validation errors are captured and sent back to the LLM as tool results with error type `:validation_error`.
|
|
185
272
|
|
|
186
273
|
## JSON Schema Generation
|
|
187
274
|
|
|
@@ -237,15 +324,19 @@ end
|
|
|
237
324
|
|
|
238
325
|
## Error Handling
|
|
239
326
|
|
|
240
|
-
Errors
|
|
327
|
+
Errors can be returned explicitly using `error`:
|
|
241
328
|
|
|
242
329
|
```ruby
|
|
243
330
|
def call(context:, query:)
|
|
244
|
-
|
|
331
|
+
results = ExternalAPI.search(query)
|
|
332
|
+
json(results)
|
|
333
|
+
rescue RateLimitError => e
|
|
334
|
+
error("API rate limit exceeded, please try again later", type: :rate_limit)
|
|
245
335
|
rescue => e
|
|
246
|
-
|
|
247
|
-
# "Error executing tool: API rate limit exceeded"
|
|
336
|
+
error("Search failed: #{e.message}")
|
|
248
337
|
end
|
|
249
338
|
```
|
|
250
339
|
|
|
251
|
-
|
|
340
|
+
Unhandled exceptions are caught by Riffer and converted to error responses with type `:execution_error`. However, it's recommended to handle expected errors explicitly for better error messages.
|
|
341
|
+
|
|
342
|
+
The LLM receives the error message and can decide how to respond (retry, apologize, ask for different input, etc.).
|
data/lib/riffer/agent.rb
CHANGED
|
@@ -266,11 +266,11 @@ class Riffer::Agent
|
|
|
266
266
|
response.tool_calls.each do |tool_call|
|
|
267
267
|
result = execute_tool_call(tool_call)
|
|
268
268
|
add_message(Riffer::Messages::Tool.new(
|
|
269
|
-
result
|
|
269
|
+
result.content,
|
|
270
270
|
tool_call_id: tool_call[:id],
|
|
271
271
|
name: tool_call[:name],
|
|
272
|
-
error: result
|
|
273
|
-
error_type: result
|
|
272
|
+
error: result.error_message,
|
|
273
|
+
error_type: result.error_type
|
|
274
274
|
))
|
|
275
275
|
end
|
|
276
276
|
end
|
|
@@ -279,37 +279,23 @@ class Riffer::Agent
|
|
|
279
279
|
tool_class = find_tool_class(tool_call[:name])
|
|
280
280
|
|
|
281
281
|
if tool_class.nil?
|
|
282
|
-
return
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
282
|
+
return Riffer::Tools::Response.error(
|
|
283
|
+
"Unknown tool '#{tool_call[:name]}'",
|
|
284
|
+
type: :unknown_tool
|
|
285
|
+
)
|
|
287
286
|
end
|
|
288
287
|
|
|
289
288
|
tool_instance = tool_class.new
|
|
290
289
|
arguments = parse_tool_arguments(tool_call[:arguments])
|
|
291
290
|
|
|
292
291
|
begin
|
|
293
|
-
|
|
294
|
-
{content: result.to_s, error: nil, error_type: nil}
|
|
292
|
+
tool_instance.call_with_validation(context: @tool_context, **arguments)
|
|
295
293
|
rescue Riffer::TimeoutError => e
|
|
296
|
-
|
|
297
|
-
content: "Error: #{e.message}",
|
|
298
|
-
error: e.message,
|
|
299
|
-
error_type: :timeout_error
|
|
300
|
-
}
|
|
294
|
+
Riffer::Tools::Response.error(e.message, type: :timeout_error)
|
|
301
295
|
rescue Riffer::ValidationError => e
|
|
302
|
-
|
|
303
|
-
content: "Validation error: #{e.message}",
|
|
304
|
-
error: e.message,
|
|
305
|
-
error_type: :validation_error
|
|
306
|
-
}
|
|
296
|
+
Riffer::Tools::Response.error(e.message, type: :validation_error)
|
|
307
297
|
rescue => e
|
|
308
|
-
{
|
|
309
|
-
content: "Error executing tool: #{e.message}",
|
|
310
|
-
error: e.message,
|
|
311
|
-
error_type: :execution_error
|
|
312
|
-
}
|
|
298
|
+
Riffer::Tools::Response.error("Error executing tool: #{e.message}", type: :execution_error)
|
|
313
299
|
end
|
|
314
300
|
end
|
|
315
301
|
|
data/lib/riffer/tool.rb
CHANGED
|
@@ -98,22 +98,57 @@ class Riffer::Tool
|
|
|
98
98
|
raise NotImplementedError, "#{self.class} must implement #call"
|
|
99
99
|
end
|
|
100
100
|
|
|
101
|
+
# Creates a text response. Shorthand for Riffer::Tools::Response.text.
|
|
102
|
+
#
|
|
103
|
+
# result:: Object - the tool result (converted via to_s)
|
|
104
|
+
#
|
|
105
|
+
# Returns Riffer::Tools::Response.
|
|
106
|
+
def text(result)
|
|
107
|
+
Riffer::Tools::Response.text(result)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Creates a JSON response. Shorthand for Riffer::Tools::Response.json.
|
|
111
|
+
#
|
|
112
|
+
# result:: Object - the tool result (converted via JSON.generate)
|
|
113
|
+
#
|
|
114
|
+
# Returns Riffer::Tools::Response.
|
|
115
|
+
def json(result)
|
|
116
|
+
Riffer::Tools::Response.json(result)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Creates an error response. Shorthand for Riffer::Tools::Response.error.
|
|
120
|
+
#
|
|
121
|
+
# message:: String - the error message
|
|
122
|
+
# type:: Symbol - the error type (default: :execution_error)
|
|
123
|
+
#
|
|
124
|
+
# Returns Riffer::Tools::Response.
|
|
125
|
+
def error(message, type: :execution_error)
|
|
126
|
+
Riffer::Tools::Response.error(message, type: type)
|
|
127
|
+
end
|
|
128
|
+
|
|
101
129
|
# Executes the tool with validation and timeout (used by Agent).
|
|
102
130
|
#
|
|
103
131
|
# context:: Object or nil - context passed from the agent
|
|
104
132
|
# kwargs:: Hash - the tool arguments
|
|
105
133
|
#
|
|
106
|
-
# Returns
|
|
134
|
+
# Returns Riffer::Tools::Response - the tool response.
|
|
107
135
|
#
|
|
108
136
|
# Raises Riffer::ValidationError if validation fails.
|
|
109
137
|
# Raises Riffer::TimeoutError if execution exceeds the configured timeout.
|
|
138
|
+
# Raises Riffer::Error if the tool does not return a Response object.
|
|
110
139
|
def call_with_validation(context:, **kwargs)
|
|
111
140
|
params_builder = self.class.params
|
|
112
141
|
validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
|
|
113
142
|
|
|
114
|
-
Timeout.timeout(self.class.timeout) do
|
|
143
|
+
result = Timeout.timeout(self.class.timeout) do
|
|
115
144
|
call(context: context, **validated_args)
|
|
116
145
|
end
|
|
146
|
+
|
|
147
|
+
unless result.is_a?(Riffer::Tools::Response)
|
|
148
|
+
raise Riffer::Error, "#{self.class} must return a Riffer::Tools::Response from #call"
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
result
|
|
117
152
|
rescue Timeout::Error
|
|
118
153
|
raise Riffer::TimeoutError, "Tool execution timed out after #{self.class.timeout} seconds"
|
|
119
154
|
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
# Riffer::Tools::Response represents the result of a tool execution.
|
|
6
|
+
#
|
|
7
|
+
# All tools must return a Response object from their +call+ method.
|
|
8
|
+
# Use +Response.success+ for successful results and +Response.error+ for failures.
|
|
9
|
+
#
|
|
10
|
+
# class MyTool < Riffer::Tool
|
|
11
|
+
# def call(context:, **kwargs)
|
|
12
|
+
# result = perform_operation
|
|
13
|
+
# Riffer::Tools::Response.success(result)
|
|
14
|
+
# rescue MyError => e
|
|
15
|
+
# Riffer::Tools::Response.error(e.message)
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
class Riffer::Tools::Response
|
|
20
|
+
VALID_FORMATS = %i[text json].freeze
|
|
21
|
+
|
|
22
|
+
attr_reader :content, :error_message, :error_type
|
|
23
|
+
|
|
24
|
+
# Creates a success response.
|
|
25
|
+
#
|
|
26
|
+
# result:: Object - the tool result
|
|
27
|
+
# format:: Symbol - the format (:text or :json; default: :text)
|
|
28
|
+
#
|
|
29
|
+
# Returns Riffer::Tools::Response.
|
|
30
|
+
#
|
|
31
|
+
# Raises Riffer::ArgumentError if format is invalid.
|
|
32
|
+
def self.success(result, format: :text)
|
|
33
|
+
unless VALID_FORMATS.include?(format)
|
|
34
|
+
raise Riffer::ArgumentError, "Invalid format: #{format}. Must be one of: #{VALID_FORMATS.join(", ")}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
content = (format == :json) ? result.to_json : result.to_s
|
|
38
|
+
new(content: content, success: true)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Creates a success response with text format.
|
|
42
|
+
#
|
|
43
|
+
# result:: Object - the tool result (converted via to_s)
|
|
44
|
+
#
|
|
45
|
+
# Returns Riffer::Tools::Response.
|
|
46
|
+
def self.text(result)
|
|
47
|
+
success(result, format: :text)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Creates a success response with JSON format.
|
|
51
|
+
#
|
|
52
|
+
# result:: Object - the tool result (converted via to_json)
|
|
53
|
+
#
|
|
54
|
+
# Returns Riffer::Tools::Response.
|
|
55
|
+
def self.json(result)
|
|
56
|
+
success(result, format: :json)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Creates an error response.
|
|
60
|
+
#
|
|
61
|
+
# message:: String - the error message
|
|
62
|
+
# type:: Symbol - the error type (default: :execution_error)
|
|
63
|
+
#
|
|
64
|
+
# Returns Riffer::Tools::Response.
|
|
65
|
+
def self.error(message, type: :execution_error)
|
|
66
|
+
new(content: message, success: false, error_message: message, error_type: type)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Returns true if the response is successful.
|
|
70
|
+
def success? = @success
|
|
71
|
+
|
|
72
|
+
# Returns true if the response is an error.
|
|
73
|
+
def error? = !@success
|
|
74
|
+
|
|
75
|
+
# Returns a hash representation of the response.
|
|
76
|
+
#
|
|
77
|
+
# Returns Hash with :content, :error, and :error_type keys.
|
|
78
|
+
def to_h
|
|
79
|
+
{content: @content, error: @error_message, error_type: @error_type}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def initialize(content:, success:, error_message: nil, error_type: nil)
|
|
85
|
+
@content = content
|
|
86
|
+
@success = success
|
|
87
|
+
@error_message = error_message
|
|
88
|
+
@error_type = error_type
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/riffer/tools.rb
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
# Namespace for tool-related classes in the Riffer framework.
|
|
4
4
|
#
|
|
5
5
|
# Contains:
|
|
6
|
-
# - Riffer::Tools::Params - DSL for defining tool parameters
|
|
7
6
|
# - Riffer::Tools::Param - Individual parameter definition
|
|
7
|
+
# - Riffer::Tools::Params - DSL for defining tool parameters
|
|
8
|
+
# - Riffer::Tools::Response - Required return type for tool execution
|
|
8
9
|
module Riffer::Tools
|
|
9
10
|
end
|
data/lib/riffer/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: riffer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jake Bottrall
|
|
@@ -214,6 +214,7 @@ files:
|
|
|
214
214
|
- lib/riffer/tools.rb
|
|
215
215
|
- lib/riffer/tools/param.rb
|
|
216
216
|
- lib/riffer/tools/params.rb
|
|
217
|
+
- lib/riffer/tools/response.rb
|
|
217
218
|
- lib/riffer/version.rb
|
|
218
219
|
- sig/riffer.rbs
|
|
219
220
|
homepage: https://riffer.ai
|