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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ace7d52275897dc91ce42de1acc67db3de289c55953a56fa833a0f92f9c48c7c
4
- data.tar.gz: ddee9798fb18502bd0e0e14e61d899a3cb24b240a6e6f9ab95b00199f03f7b59
3
+ metadata.gz: 7aa794200e1e9eef7fff84170952900baa69535798e7bed5b67990a886edc587
4
+ data.tar.gz: 0cad248bb54b1814b184f4727900f22955cc62abf0b76789ef4099824d05d4c3
5
5
  SHA512:
6
- metadata.gz: 6c32731c0ac374bcc90fddd3c1cf071e3e2a1a2e2ae197ffcf98aace5acc4402c38d24657b2d3c211dfbf8d951a48ab9e94087a4b3accb2e763eb6202fa07312
7
- data.tar.gz: da8b8ba45b0a5e08e4f2c7b212ddba5761fd2673d1c89fea695b1cb2d60e9ba1f7a01d9138f0eed33b406a7c7e116eb219e1f7a4919a7a4183cfe2dc97f59622
6
+ metadata.gz: 3dceae48df77f99164eeabd11b9f29e95a980d7efbc2472ee5192512e46467dd6b17f4b2dbf3869eeb9b4a45cee18996fda7bbd6b88813e0563ee22484931ea9
7
+ data.tar.gz: 9ca7a462a2cdcefa0d5576b1004f5f7d70160c4ac977d4343e81c64eb61621f667f478a2bb9807ad1d348d79dde8238dbadb4f26d69cc52549538a8102ff78a0
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.0"
2
+ ".": "0.9.0"
3
3
  }
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
- return "No user ID provided" unless user_id
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
- ### Return Values
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
- Return a string that will be sent back to the LLM:
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 in tools are captured and reported back to the LLM:
327
+ Errors can be returned explicitly using `error`:
241
328
 
242
329
  ```ruby
243
330
  def call(context:, query:)
244
- raise "API rate limit exceeded"
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
- # Error is caught by Riffer and sent as tool result:
247
- # "Error executing tool: API rate limit exceeded"
336
+ error("Search failed: #{e.message}")
248
337
  end
249
338
  ```
250
339
 
251
- The LLM can then decide how to respond (retry, apologize, ask for different input, etc.).
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[:content],
269
+ result.content,
270
270
  tool_call_id: tool_call[:id],
271
271
  name: tool_call[:name],
272
- error: result[:error],
273
- error_type: result[:error_type]
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
- content: "Error: Unknown tool '#{tool_call[:name]}'",
284
- error: "Unknown tool '#{tool_call[:name]}'",
285
- error_type: :unknown_tool
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
- result = tool_instance.call_with_validation(context: @tool_context, **arguments)
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 Object - the tool result.
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riffer
4
- VERSION = "0.8.0"
4
+ VERSION = "0.9.0"
5
5
  end
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.8.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