riffer 0.6.0 → 0.7.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.
@@ -1,12 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Riffer::Messages::Tool < Riffer::Messages::Base
4
- attr_reader :tool_call_id, :name
4
+ attr_reader :tool_call_id, :name, :error, :error_type
5
5
 
6
- def initialize(content, tool_call_id:, name:)
6
+ def initialize(content, tool_call_id:, name:, error: nil, error_type: nil)
7
7
  super(content)
8
8
  @tool_call_id = tool_call_id
9
9
  @name = name
10
+ @error = error
11
+ @error_type = error_type
12
+ end
13
+
14
+ def error?
15
+ !@error.nil?
10
16
  end
11
17
 
12
18
  def role
@@ -14,6 +20,11 @@ class Riffer::Messages::Tool < Riffer::Messages::Base
14
20
  end
15
21
 
16
22
  def to_h
17
- {role: role, content: content, tool_call_id: tool_call_id, name: name}
23
+ hash = {role: role, content: content, tool_call_id: tool_call_id, name: name}
24
+ if error?
25
+ hash[:error] = error
26
+ hash[:error_type] = error_type
27
+ end
28
+ hash
18
29
  end
19
30
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "json"
4
+
3
5
  class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
4
6
  # Initializes the Amazon Bedrock provider.
5
7
  #
@@ -27,37 +29,87 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
27
29
 
28
30
  private
29
31
 
30
- def perform_generate_text(messages, model:, reasoning: nil)
32
+ def perform_generate_text(messages, model:, **options)
31
33
  partitioned_messages = partition_messages(messages)
34
+ tools = options[:tools]
32
35
 
33
36
  params = {
34
37
  model_id: model,
35
38
  system: partitioned_messages[:system],
36
- messages: partitioned_messages[:conversation]
39
+ messages: partitioned_messages[:conversation],
40
+ **options.except(:tools)
37
41
  }
38
42
 
43
+ if tools && !tools.empty?
44
+ params[:tool_config] = {
45
+ tools: tools.map { |t| convert_tool_to_bedrock_format(t) }
46
+ }
47
+ end
48
+
39
49
  response = @client.converse(**params)
40
50
  extract_assistant_message(response)
41
51
  end
42
52
 
43
- def perform_stream_text(messages, model:, reasoning: nil)
53
+ def perform_stream_text(messages, model:, **options)
44
54
  Enumerator.new do |yielder|
45
55
  partitioned_messages = partition_messages(messages)
56
+ tools = options[:tools]
46
57
 
47
58
  params = {
48
59
  model_id: model,
49
60
  system: partitioned_messages[:system],
50
- messages: partitioned_messages[:conversation]
61
+ messages: partitioned_messages[:conversation],
62
+ **options.except(:tools)
51
63
  }
52
64
 
65
+ if tools && !tools.empty?
66
+ params[:tool_config] = {
67
+ tools: tools.map { |t| convert_tool_to_bedrock_format(t) }
68
+ }
69
+ end
70
+
53
71
  accumulated_text = ""
72
+ current_tool_use = nil
54
73
 
55
74
  @client.converse_stream(**params) do |stream|
75
+ stream.on_content_block_start_event do |event|
76
+ if event.start&.tool_use
77
+ tool_use = event.start.tool_use
78
+ current_tool_use = {
79
+ id: tool_use.tool_use_id,
80
+ name: tool_use.name,
81
+ arguments: ""
82
+ }
83
+ end
84
+ end
85
+
56
86
  stream.on_content_block_delta_event do |event|
57
87
  if event.delta&.text
58
88
  delta_text = event.delta.text
59
89
  accumulated_text += delta_text
60
90
  yielder << Riffer::StreamEvents::TextDelta.new(delta_text)
91
+ elsif event.delta&.tool_use
92
+ input_delta = event.delta.tool_use.input
93
+ if current_tool_use && input_delta
94
+ current_tool_use[:arguments] += input_delta
95
+ yielder << Riffer::StreamEvents::ToolCallDelta.new(
96
+ item_id: current_tool_use[:id],
97
+ name: current_tool_use[:name],
98
+ arguments_delta: input_delta
99
+ )
100
+ end
101
+ end
102
+ end
103
+
104
+ stream.on_content_block_stop_event do |_event|
105
+ if current_tool_use
106
+ yielder << Riffer::StreamEvents::ToolCallDone.new(
107
+ item_id: current_tool_use[:id],
108
+ call_id: current_tool_use[:id],
109
+ name: current_tool_use[:name],
110
+ arguments: current_tool_use[:arguments]
111
+ )
112
+ current_tool_use = nil
61
113
  end
62
114
  end
63
115
 
@@ -79,9 +131,17 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
79
131
  when Riffer::Messages::User
80
132
  conversation_messages << {role: "user", content: [{text: message.content}]}
81
133
  when Riffer::Messages::Assistant
82
- conversation_messages << {role: "assistant", content: [{text: message.content}]}
134
+ conversation_messages << convert_assistant_to_bedrock_format(message)
83
135
  when Riffer::Messages::Tool
84
- raise NotImplementedError, "Tool messages are not supported by Amazon Bedrock provider yet"
136
+ conversation_messages << {
137
+ role: "user",
138
+ content: [{
139
+ tool_result: {
140
+ tool_use_id: message.tool_call_id,
141
+ content: [{text: message.content}]
142
+ }
143
+ }]
144
+ }
85
145
  end
86
146
  end
87
147
 
@@ -91,6 +151,28 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
91
151
  }
92
152
  end
93
153
 
154
+ def convert_assistant_to_bedrock_format(message)
155
+ content = []
156
+ content << {text: message.content} if message.content && !message.content.empty?
157
+
158
+ message.tool_calls.each do |tc|
159
+ content << {
160
+ tool_use: {
161
+ tool_use_id: tc[:id] || tc[:call_id],
162
+ name: tc[:name],
163
+ input: parse_tool_arguments(tc[:arguments])
164
+ }
165
+ }
166
+ end
167
+
168
+ {role: "assistant", content: content}
169
+ end
170
+
171
+ def parse_tool_arguments(arguments)
172
+ return {} if arguments.nil? || arguments.empty?
173
+ arguments.is_a?(String) ? JSON.parse(arguments) : arguments
174
+ end
175
+
94
176
  def extract_assistant_message(response)
95
177
  output = response.output
96
178
  raise Riffer::Error, "No output returned from Bedrock API" if output.nil? || output.message.nil?
@@ -98,9 +180,38 @@ class Riffer::Providers::AmazonBedrock < Riffer::Providers::Base
98
180
  content_blocks = output.message.content
99
181
  raise Riffer::Error, "No content returned from Bedrock API" if content_blocks.nil? || content_blocks.empty?
100
182
 
101
- text_block = content_blocks.find { |block| block.respond_to?(:text) && block.text }
102
- raise Riffer::Error, "No text content returned from Bedrock API" if text_block.nil?
183
+ text_content = ""
184
+ tool_calls = []
185
+
186
+ content_blocks.each do |block|
187
+ if block.respond_to?(:text) && block.text
188
+ text_content = block.text
189
+ elsif block.respond_to?(:tool_use) && block.tool_use
190
+ tool_calls << {
191
+ id: block.tool_use.tool_use_id,
192
+ call_id: block.tool_use.tool_use_id,
193
+ name: block.tool_use.name,
194
+ arguments: block.tool_use.input.to_json
195
+ }
196
+ end
197
+ end
198
+
199
+ if text_content.empty? && tool_calls.empty?
200
+ raise Riffer::Error, "No content returned from Bedrock API"
201
+ end
202
+
203
+ Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls)
204
+ end
103
205
 
104
- Riffer::Messages::Assistant.new(text_block.text)
206
+ def convert_tool_to_bedrock_format(tool)
207
+ {
208
+ tool_spec: {
209
+ name: tool.name,
210
+ description: tool.description,
211
+ input_schema: {
212
+ json: tool.parameters_schema
213
+ }
214
+ }
215
+ }
105
216
  end
106
217
  end
@@ -10,13 +10,13 @@ class Riffer::Providers::Base
10
10
  # @param system [String, nil] an optional system message
11
11
  # @param messages [Array<Hash, Riffer::Messages::Base>, nil] optional messages array
12
12
  # @param model [String, nil] optional model string to override the configured model
13
- # @param reasoning [String, nil] optional reasoning level or instructions
13
+ # @param options [Hash] additional options passed to the model invocation
14
14
  # @return [Riffer::Messages::Assistant] the generated assistant message
15
- def generate_text(prompt: nil, system: nil, messages: nil, model: nil, reasoning: nil)
15
+ def generate_text(prompt: nil, system: nil, messages: nil, model: nil, **options)
16
16
  validate_input!(prompt: prompt, system: system, messages: messages)
17
17
  normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
18
18
  validate_normalized_messages!(normalized_messages)
19
- perform_generate_text(normalized_messages, model: model, reasoning: reasoning)
19
+ perform_generate_text(normalized_messages, model: model, **options)
20
20
  end
21
21
 
22
22
  # Streams text from the provider.
@@ -25,22 +25,22 @@ class Riffer::Providers::Base
25
25
  # @param system [String, nil] an optional system message
26
26
  # @param messages [Array<Hash, Riffer::Messages::Base>, nil] optional messages array
27
27
  # @param model [String, nil] optional model string to override the configured model
28
- # @param reasoning [String, nil] optional reasoning level or instructions
28
+ # @param options [Hash] additional options passed to the model invocation
29
29
  # @return [Enumerator] an enumerator yielding stream events or chunks (provider-specific)
30
- def stream_text(prompt: nil, system: nil, messages: nil, model: nil, reasoning: nil)
30
+ def stream_text(prompt: nil, system: nil, messages: nil, model: nil, **options)
31
31
  validate_input!(prompt: prompt, system: system, messages: messages)
32
32
  normalized_messages = normalize_messages(prompt: prompt, system: system, messages: messages)
33
33
  validate_normalized_messages!(normalized_messages)
34
- perform_stream_text(normalized_messages, model: model, reasoning: reasoning)
34
+ perform_stream_text(normalized_messages, model: model, **options)
35
35
  end
36
36
 
37
37
  private
38
38
 
39
- def perform_generate_text(messages, model: nil, reasoning: nil)
39
+ def perform_generate_text(messages, model: nil, **options)
40
40
  raise NotImplementedError, "Subclasses must implement #perform_generate_text"
41
41
  end
42
42
 
43
- def perform_stream_text(messages, model: nil, reasoning: nil)
43
+ def perform_stream_text(messages, model: nil, **options)
44
44
  raise NotImplementedError, "Subclasses must implement #perform_stream_text"
45
45
  end
46
46
 
@@ -1,87 +1,124 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "openai"
4
-
5
3
  class Riffer::Providers::OpenAI < Riffer::Providers::Base
6
4
  # Initializes the OpenAI provider.
7
5
  # @param options [Hash] optional client options. Use `:api_key` to override `Riffer.config.openai.api_key`.
8
- # @raise [Riffer::ArgumentError] if an API key is not provided either via `:api_key` or `Riffer.config.openai.api_key`.
9
6
  def initialize(**options)
10
7
  depends_on "openai"
11
8
 
12
9
  api_key = options.fetch(:api_key, Riffer.config.openai.api_key)
13
- raise Riffer::ArgumentError, "OpenAI API key is required. Set it via Riffer.configure or pass :api_key option" if api_key.nil? || api_key.empty?
14
-
15
10
  @client = ::OpenAI::Client.new(api_key: api_key, **options.except(:api_key))
16
11
  end
17
12
 
18
13
  private
19
14
 
20
- def perform_generate_text(messages, model:, reasoning: nil)
21
- params = build_request_params(messages, model, reasoning)
15
+ def perform_generate_text(messages, model:, **options)
16
+ params = build_request_params(messages, model, options)
22
17
  response = @client.responses.create(params)
23
18
 
24
- output = response.output.find { |o| o.type == :message }
25
-
26
- if output.nil?
27
- raise Riffer::Error, "No output returned from OpenAI API"
28
- end
29
-
30
- content = output.content.find { |c| c.type == :output_text }
31
-
32
- if content.nil?
33
- raise Riffer::Error, "No content returned from OpenAI API"
34
- end
35
-
36
- if content.type == :refusal
37
- raise Riffer::Error, "Request was refused: #{content.refusal}"
38
- end
39
-
40
- if content.type != :output_text
41
- raise Riffer::Error, "Unexpected content type: #{content.type}"
42
- end
43
-
44
- Riffer::Messages::Assistant.new(content.text)
19
+ extract_assistant_message(response.output)
45
20
  end
46
21
 
47
- def perform_stream_text(messages, model:, reasoning: nil)
22
+ def perform_stream_text(messages, model:, **options)
48
23
  Enumerator.new do |yielder|
49
- params = build_request_params(messages, model, reasoning)
24
+ params = build_request_params(messages, model, options)
50
25
  stream = @client.responses.stream(params)
51
26
 
52
27
  process_stream_events(stream, yielder)
53
28
  end
54
29
  end
55
30
 
56
- def build_request_params(messages, model, reasoning)
57
- {
58
- input: convert_message_to_openai_format(messages),
31
+ def build_request_params(messages, model, options)
32
+ reasoning = options[:reasoning]
33
+ tools = options[:tools]
34
+
35
+ params = {
36
+ input: convert_messages_to_openai_format(messages),
59
37
  model: model,
60
38
  reasoning: reasoning && {
61
39
  effort: reasoning,
62
40
  summary: "auto"
63
- }
41
+ },
42
+ **options.except(:reasoning, :tools)
64
43
  }
44
+
45
+ if tools && !tools.empty?
46
+ params[:tools] = tools.map { |t| convert_tool_to_openai_format(t) }
47
+ end
48
+
49
+ params.compact
65
50
  end
66
51
 
67
- def convert_message_to_openai_format(messages)
68
- messages.map do |message|
52
+ def convert_messages_to_openai_format(messages)
53
+ messages.flat_map do |message|
69
54
  case message
70
55
  when Riffer::Messages::System
71
56
  {role: "developer", content: message.content}
72
57
  when Riffer::Messages::User
73
58
  {role: "user", content: message.content}
74
59
  when Riffer::Messages::Assistant
75
- {role: "assistant", content: message.content}
60
+ convert_assistant_to_openai_format(message)
76
61
  when Riffer::Messages::Tool
77
- raise NotImplementedError, "Tool messages are not supported by OpenAI provider yet"
62
+ {
63
+ type: "function_call_output",
64
+ call_id: message.tool_call_id,
65
+ output: message.content
66
+ }
78
67
  end
79
68
  end
80
69
  end
81
70
 
71
+ def convert_assistant_to_openai_format(message)
72
+ if message.tool_calls.empty?
73
+ {role: "assistant", content: message.content}
74
+ else
75
+ items = []
76
+ items << {type: "message", role: "assistant", content: message.content} if message.content && !message.content.empty?
77
+ message.tool_calls.each do |tc|
78
+ items << {
79
+ type: "function_call",
80
+ id: tc[:id],
81
+ call_id: tc[:call_id] || tc[:id],
82
+ name: tc[:name],
83
+ arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
84
+ }
85
+ end
86
+ items
87
+ end
88
+ end
89
+
90
+ def extract_assistant_message(output_items)
91
+ text_content = ""
92
+ tool_calls = []
93
+
94
+ output_items.each do |item|
95
+ case item.type
96
+ when :message
97
+ text_block = item.content&.find { |c| c.type == :output_text }
98
+ text_content = text_block&.text || "" if text_block
99
+ when :function_call
100
+ tool_calls << {
101
+ id: item.id,
102
+ call_id: item.call_id,
103
+ name: item.name,
104
+ arguments: item.arguments
105
+ }
106
+ end
107
+ end
108
+
109
+ if text_content.empty? && tool_calls.empty?
110
+ raise Riffer::Error, "No output returned from OpenAI API"
111
+ end
112
+
113
+ Riffer::Messages::Assistant.new(text_content, tool_calls: tool_calls)
114
+ end
115
+
82
116
  def process_stream_events(stream, yielder)
117
+ tool_info = {}
118
+
83
119
  stream.each do |raw_event|
84
- event = convert_event(raw_event)
120
+ track_tool_info(raw_event, tool_info)
121
+ event = convert_event(raw_event, tool_info)
85
122
 
86
123
  next unless event
87
124
 
@@ -89,7 +126,17 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
89
126
  end
90
127
  end
91
128
 
92
- def convert_event(event)
129
+ def track_tool_info(event, tool_info)
130
+ return unless event.type == :"response.output_item.added"
131
+ return unless event.item&.type == :function_call
132
+
133
+ tool_info[event.item.id] = {
134
+ name: event.item.name,
135
+ call_id: event.item.call_id
136
+ }
137
+ end
138
+
139
+ def convert_event(event, tool_info = {})
93
140
  case event.type
94
141
  when :"response.output_text.delta"
95
142
  Riffer::StreamEvents::TextDelta.new(event.delta)
@@ -99,6 +146,31 @@ class Riffer::Providers::OpenAI < Riffer::Providers::Base
99
146
  Riffer::StreamEvents::ReasoningDelta.new(event.delta)
100
147
  when :"response.reasoning_summary_text.done"
101
148
  Riffer::StreamEvents::ReasoningDone.new(event.text)
149
+ when :"response.function_call_arguments.delta"
150
+ tracked = tool_info[event.item_id] || {}
151
+ Riffer::StreamEvents::ToolCallDelta.new(
152
+ item_id: event.item_id,
153
+ name: tracked[:name],
154
+ arguments_delta: event.delta
155
+ )
156
+ when :"response.function_call_arguments.done"
157
+ tracked = tool_info[event.item_id] || {}
158
+ Riffer::StreamEvents::ToolCallDone.new(
159
+ item_id: event.item_id,
160
+ call_id: tracked[:call_id] || event.item_id,
161
+ name: tracked[:name],
162
+ arguments: event.arguments
163
+ )
102
164
  end
103
165
  end
166
+
167
+ def convert_tool_to_openai_format(tool)
168
+ {
169
+ type: "function",
170
+ name: tool.name,
171
+ description: tool.description,
172
+ parameters: tool.parameters_schema,
173
+ strict: true
174
+ }
175
+ end
104
176
  end
@@ -7,21 +7,51 @@ class Riffer::Providers::Test < Riffer::Providers::Base
7
7
  @responses = options[:responses] || []
8
8
  @current_index = 0
9
9
  @calls = []
10
- @stubbed_response = nil
11
- @stubbed_reasoning = nil
10
+ @stubbed_responses = []
12
11
  end
13
12
 
14
- def stub_response(content, tool_calls: [], reasoning: nil)
15
- @stubbed_response = {role: "assistant", content: content, tool_calls: tool_calls}
16
- @stubbed_reasoning = reasoning
13
+ # Stubs the next response from the provider
14
+ # Can be called multiple times to queue responses
15
+ # @param content [String] the response content
16
+ # @param tool_calls [Array<Hash>] optional tool calls to include
17
+ # @example
18
+ # provider.stub_response("Hello")
19
+ # provider.stub_response("", tool_calls: [{name: "my_tool", arguments: '{"key":"value"}'}])
20
+ # provider.stub_response("Final response") # Queued for after tool execution
21
+ def stub_response(content, tool_calls: [])
22
+ formatted_tool_calls = tool_calls.map.with_index do |tc, idx|
23
+ {
24
+ id: tc[:id] || "test_id_#{idx}",
25
+ call_id: tc[:call_id] || tc[:id] || "test_call_#{idx}",
26
+ name: tc[:name],
27
+ arguments: tc[:arguments].is_a?(String) ? tc[:arguments] : tc[:arguments].to_json
28
+ }
29
+ end
30
+ @stubbed_responses << {role: "assistant", content: content, tool_calls: formatted_tool_calls}
31
+ end
32
+
33
+ # Clears all stubbed responses
34
+ def clear_stubs
35
+ @stubbed_responses = []
17
36
  end
18
37
 
19
38
  private
20
39
 
21
- def perform_generate_text(messages, model: nil, reasoning: nil)
22
- @calls << {messages: messages.map(&:to_h), model: model, reasoning: reasoning}
23
- response = @stubbed_response || @responses[@current_index] || {role: "assistant", content: "Test response"}
24
- @current_index += 1
40
+ def next_response
41
+ if @stubbed_responses.any?
42
+ @stubbed_responses.shift
43
+ elsif @responses[@current_index]
44
+ response = @responses[@current_index]
45
+ @current_index += 1
46
+ response
47
+ else
48
+ {role: "assistant", content: "Test response"}
49
+ end
50
+ end
51
+
52
+ def perform_generate_text(messages, model: nil, **options)
53
+ @calls << {messages: messages.map(&:to_h), model: model, **options}
54
+ response = next_response
25
55
 
26
56
  if response.is_a?(Hash)
27
57
  Riffer::Messages::Assistant.new(response[:content], tool_calls: response[:tool_calls] || [])
@@ -30,26 +60,32 @@ class Riffer::Providers::Test < Riffer::Providers::Base
30
60
  end
31
61
  end
32
62
 
33
- def perform_stream_text(messages, model: nil, reasoning: nil)
34
- @calls << {messages: messages.map(&:to_h), model: model, reasoning: reasoning}
35
- response = @stubbed_response || @responses[@current_index] || {role: "assistant", content: "Test response"}
36
- @current_index += 1
63
+ def perform_stream_text(messages, model: nil, **options)
64
+ @calls << {messages: messages.map(&:to_h), model: model, **options}
65
+ response = next_response
37
66
  Enumerator.new do |yielder|
38
- if @stubbed_reasoning
39
- reasoning_parts = @stubbed_reasoning.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
67
+ full_content = response[:content] || ""
68
+ tool_calls = response[:tool_calls] || []
40
69
 
41
- reasoning_parts.each do |part|
42
- yielder << Riffer::StreamEvents::ReasoningDelta.new(part + " ")
70
+ unless full_content.empty?
71
+ content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
72
+ content_parts.each do |part|
73
+ yielder << Riffer::StreamEvents::TextDelta.new(part + " ")
43
74
  end
44
-
45
- yielder << Riffer::StreamEvents::ReasoningDone.new(@stubbed_reasoning)
46
75
  end
47
76
 
48
- full_content = response[:content]
49
- content_parts = full_content.split(". ").map { |part| part + (part.end_with?(".") ? "" : ".") }
50
-
51
- content_parts.each do |part|
52
- yielder << Riffer::StreamEvents::TextDelta.new(part + " ")
77
+ tool_calls.each do |tc|
78
+ yielder << Riffer::StreamEvents::ToolCallDelta.new(
79
+ item_id: tc[:id],
80
+ name: tc[:name],
81
+ arguments_delta: tc[:arguments]
82
+ )
83
+ yielder << Riffer::StreamEvents::ToolCallDone.new(
84
+ item_id: tc[:id],
85
+ call_id: tc[:call_id],
86
+ name: tc[:name],
87
+ arguments: tc[:arguments]
88
+ )
53
89
  end
54
90
 
55
91
  yielder << Riffer::StreamEvents::TextDone.new(full_content)
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Riffer::StreamEvents::ToolCallDelta represents an incremental tool call chunk during streaming.
4
+ #
5
+ # Emitted when the LLM is building a tool call, containing partial argument data.
6
+ #
7
+ # @api public
8
+ class Riffer::StreamEvents::ToolCallDelta < Riffer::StreamEvents::Base
9
+ attr_reader :item_id, :name, :arguments_delta
10
+
11
+ # Creates a new tool call delta event
12
+ # @param item_id [String] the tool call item identifier
13
+ # @param name [String, nil] the tool name (may only be present in first delta)
14
+ # @param arguments_delta [String] the incremental arguments JSON fragment
15
+ # @param role [String] the message role (defaults to "assistant")
16
+ def initialize(item_id:, arguments_delta:, name: nil, role: "assistant")
17
+ super(role: role)
18
+ @item_id = item_id
19
+ @name = name
20
+ @arguments_delta = arguments_delta
21
+ end
22
+
23
+ # Converts the event to a hash
24
+ # @return [Hash] the event data
25
+ def to_h
26
+ {role: @role, item_id: @item_id, name: @name, arguments_delta: @arguments_delta}.compact
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Riffer::StreamEvents::ToolCallDone represents a completed tool call during streaming.
4
+ #
5
+ # Emitted when the LLM has finished building a tool call with complete arguments.
6
+ #
7
+ # @api public
8
+ class Riffer::StreamEvents::ToolCallDone < Riffer::StreamEvents::Base
9
+ attr_reader :item_id, :call_id, :name, :arguments
10
+
11
+ # Creates a new tool call done event
12
+ # @param item_id [String] the tool call item identifier
13
+ # @param call_id [String] the call identifier for response matching
14
+ # @param name [String] the tool name
15
+ # @param arguments [String] the complete arguments JSON string
16
+ # @param role [String] the message role (defaults to "assistant")
17
+ def initialize(item_id:, call_id:, name:, arguments:, role: "assistant")
18
+ super(role: role)
19
+ @item_id = item_id
20
+ @call_id = call_id
21
+ @name = name
22
+ @arguments = arguments
23
+ end
24
+
25
+ # Converts the event to a hash
26
+ # @return [Hash] the event data
27
+ def to_h
28
+ {role: @role, item_id: @item_id, call_id: @call_id, name: @name, arguments: @arguments}
29
+ end
30
+ end