riffer 0.8.0 → 0.10.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: d44459cec1b14508aac77178786e7230640ad1d455ced72ebda4ff02166283e4
4
+ data.tar.gz: 58a86d78ac5025d17245596e5ab0680d740fc0345cc3d51f2e68a65c5f3b8641
5
5
  SHA512:
6
- metadata.gz: 6c32731c0ac374bcc90fddd3c1cf071e3e2a1a2e2ae197ffcf98aace5acc4402c38d24657b2d3c211dfbf8d951a48ab9e94087a4b3accb2e763eb6202fa07312
7
- data.tar.gz: da8b8ba45b0a5e08e4f2c7b212ddba5761fd2673d1c89fea695b1cb2d60e9ba1f7a01d9138f0eed33b406a7c7e116eb219e1f7a4919a7a4183cfe2dc97f59622
6
+ metadata.gz: 9f8816ae7deb524c786afd74096f55bad54102c32d246f706f7bf15ed1c47cb7cd4f2ccb30c05af833b80e4898049febf5df45c98b8085c7b15c0ed71f425c98
7
+ data.tar.gz: 4c2627096ae1181b34d710d744ff339a61ebde623c30c6ea1e000ddac82bf9b374c7eb0d23364f7821856b407466332cc26fedca6288e93b23f9785515d220e7
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.8.0"
2
+ ".": "0.10.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,26 @@ 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.10.0](https://github.com/janeapp/riffer/compare/riffer/v0.9.0...riffer/v0.10.0) (2026-01-30)
9
+
10
+
11
+ ### Features
12
+
13
+ * update class name conversion to support configurable namespace separators ([#96](https://github.com/janeapp/riffer/issues/96)) ([e7091e9](https://github.com/janeapp/riffer/commit/e7091e95210c2df27138e61e64032d52ecf174e1))
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * handle multiple tools correctly for bedrock ([#95](https://github.com/janeapp/riffer/issues/95)) ([50ae6f6](https://github.com/janeapp/riffer/commit/50ae6f6cd803d5e95b79cb6ceafca5b2d9b4a52c))
19
+ * update class name conversion to use double underscore format ([#93](https://github.com/janeapp/riffer/issues/93)) ([f6ffad7](https://github.com/janeapp/riffer/commit/f6ffad775a2d8254543dd7819dca93c15f514742))
20
+
21
+ ## [0.9.0](https://github.com/janeapp/riffer/compare/riffer/v0.8.0...riffer/v0.9.0) (2026-01-28)
22
+
23
+
24
+ ### Features
25
+
26
+ * 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))
27
+
8
28
  ## [0.8.0](https://github.com/janeapp/riffer/compare/riffer/v0.7.0...riffer/v0.8.0) (2026-01-26)
9
29
 
10
30
 
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
 
@@ -2,15 +2,18 @@
2
2
 
3
3
  # Helper module for converting class names.
4
4
  module Riffer::Helpers::ClassNameConverter
5
- # Converts a class name to snake_case path format.
5
+ DEFAULT_SEPARATOR = "/"
6
+
7
+ # Converts a class name to snake_case identifier format.
6
8
  #
7
9
  # class_name:: String - the class name (e.g., "Riffer::Agent")
10
+ # separator:: String - the separator to use for namespaces (default: "/")
8
11
  #
9
- # Returns String - the snake_case path (e.g., "riffer/agent").
10
- def class_name_to_path(class_name)
12
+ # Returns String - the snake_case identifier (e.g., "riffer/agent").
13
+ def class_name_to_path(class_name, separator: DEFAULT_SEPARATOR)
11
14
  class_name
12
15
  .to_s
13
- .gsub("::", "/")
16
+ .gsub("::", separator)
14
17
  .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
15
18
  .gsub(/([a-z\d])([A-Z])/, '\1_\2')
16
19
  .downcase
@@ -137,15 +137,7 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
137
137
  when Riffer::Messages::Assistant
138
138
  conversation_messages << convert_assistant_to_bedrock_format(message)
139
139
  when Riffer::Messages::Tool
140
- conversation_messages << {
141
- role: "user",
142
- content: [{
143
- tool_result: {
144
- tool_use_id: message.tool_call_id,
145
- content: [{text: message.content}]
146
- }
147
- }]
148
- }
140
+ append_tool_result(conversation_messages, message)
149
141
  end
150
142
  end
151
143
 
@@ -155,6 +147,22 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
155
147
  }
156
148
  end
157
149
 
150
+ def append_tool_result(conversation_messages, message)
151
+ tool_result = {
152
+ tool_result: {
153
+ tool_use_id: message.tool_call_id,
154
+ content: [{text: message.content}]
155
+ }
156
+ }
157
+
158
+ prev = conversation_messages.last
159
+ if prev && prev[:role] == "user" && prev[:content]&.first&.key?(:tool_result)
160
+ prev[:content] << tool_result
161
+ else
162
+ conversation_messages << {role: "user", content: [tool_result]}
163
+ end
164
+ end
165
+
158
166
  def convert_assistant_to_bedrock_format(message)
159
167
  content = []
160
168
  content << {text: message.content} if message.content && !message.content.empty?
data/lib/riffer/tool.rb CHANGED
@@ -25,6 +25,9 @@ require "timeout"
25
25
  class Riffer::Tool
26
26
  DEFAULT_TIMEOUT = 10
27
27
 
28
+ # Some providers do not allow "/" in tool names, so we use "__" as separator.
29
+ TOOL_SEPARATOR = "__"
30
+
28
31
  class << self
29
32
  include Riffer::Helpers::ClassNameConverter
30
33
 
@@ -44,7 +47,7 @@ class Riffer::Tool
44
47
  #
45
48
  # Returns String - the tool identifier (defaults to snake_case class name).
46
49
  def identifier(value = nil)
47
- return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self)) if value.nil?
50
+ return @identifier || class_name_to_path(Module.instance_method(:name).bind_call(self), separator: TOOL_SEPARATOR) if value.nil?
48
51
  @identifier = value.to_s
49
52
  end
50
53
 
@@ -98,22 +101,57 @@ class Riffer::Tool
98
101
  raise NotImplementedError, "#{self.class} must implement #call"
99
102
  end
100
103
 
104
+ # Creates a text response. Shorthand for Riffer::Tools::Response.text.
105
+ #
106
+ # result:: Object - the tool result (converted via to_s)
107
+ #
108
+ # Returns Riffer::Tools::Response.
109
+ def text(result)
110
+ Riffer::Tools::Response.text(result)
111
+ end
112
+
113
+ # Creates a JSON response. Shorthand for Riffer::Tools::Response.json.
114
+ #
115
+ # result:: Object - the tool result (converted via JSON.generate)
116
+ #
117
+ # Returns Riffer::Tools::Response.
118
+ def json(result)
119
+ Riffer::Tools::Response.json(result)
120
+ end
121
+
122
+ # Creates an error response. Shorthand for Riffer::Tools::Response.error.
123
+ #
124
+ # message:: String - the error message
125
+ # type:: Symbol - the error type (default: :execution_error)
126
+ #
127
+ # Returns Riffer::Tools::Response.
128
+ def error(message, type: :execution_error)
129
+ Riffer::Tools::Response.error(message, type: type)
130
+ end
131
+
101
132
  # Executes the tool with validation and timeout (used by Agent).
102
133
  #
103
134
  # context:: Object or nil - context passed from the agent
104
135
  # kwargs:: Hash - the tool arguments
105
136
  #
106
- # Returns Object - the tool result.
137
+ # Returns Riffer::Tools::Response - the tool response.
107
138
  #
108
139
  # Raises Riffer::ValidationError if validation fails.
109
140
  # Raises Riffer::TimeoutError if execution exceeds the configured timeout.
141
+ # Raises Riffer::Error if the tool does not return a Response object.
110
142
  def call_with_validation(context:, **kwargs)
111
143
  params_builder = self.class.params
112
144
  validated_args = params_builder ? params_builder.validate(kwargs) : kwargs
113
145
 
114
- Timeout.timeout(self.class.timeout) do
146
+ result = Timeout.timeout(self.class.timeout) do
115
147
  call(context: context, **validated_args)
116
148
  end
149
+
150
+ unless result.is_a?(Riffer::Tools::Response)
151
+ raise Riffer::Error, "#{self.class} must return a Riffer::Tools::Response from #call"
152
+ end
153
+
154
+ result
117
155
  rescue Timeout::Error
118
156
  raise Riffer::TimeoutError, "Tool execution timed out after #{self.class.timeout} seconds"
119
157
  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.10.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.10.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