ruby_llm_swarm 1.9.1 → 1.9.2
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/lib/ruby_llm/active_record/acts_as.rb +130 -128
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +301 -299
- data/lib/ruby_llm/active_record/chat_methods.rb +278 -276
- data/lib/ruby_llm/active_record/message_methods.rb +60 -58
- data/lib/ruby_llm/active_record/model_methods.rb +68 -66
- data/lib/ruby_llm/railtie.rb +4 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +1 -1
- metadata +1 -1
|
@@ -1,383 +1,385 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
delegate :add_message, to: :to_llm
|
|
23
|
-
end
|
|
3
|
+
if defined?(ActiveRecord::Base)
|
|
4
|
+
module RubyLLM
|
|
5
|
+
module ActiveRecord
|
|
6
|
+
# Adds chat and message persistence capabilities to ActiveRecord models.
|
|
7
|
+
module ActsAsLegacy
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
|
11
|
+
def acts_as_chat(message_class: 'Message', tool_call_class: 'ToolCall')
|
|
12
|
+
include ChatLegacyMethods
|
|
13
|
+
|
|
14
|
+
@message_class = message_class.to_s
|
|
15
|
+
@tool_call_class = tool_call_class.to_s
|
|
16
|
+
|
|
17
|
+
has_many :messages,
|
|
18
|
+
-> { order(created_at: :asc) },
|
|
19
|
+
class_name: @message_class,
|
|
20
|
+
inverse_of: :chat,
|
|
21
|
+
dependent: :destroy
|
|
24
22
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
tool_call_class: 'ToolCall',
|
|
28
|
-
tool_call_foreign_key: nil,
|
|
29
|
-
touch_chat: false)
|
|
30
|
-
include MessageLegacyMethods
|
|
23
|
+
delegate :add_message, to: :to_llm
|
|
24
|
+
end
|
|
31
25
|
|
|
32
|
-
|
|
33
|
-
|
|
26
|
+
def acts_as_message(chat_class: 'Chat',
|
|
27
|
+
chat_foreign_key: nil,
|
|
28
|
+
tool_call_class: 'ToolCall',
|
|
29
|
+
tool_call_foreign_key: nil,
|
|
30
|
+
touch_chat: false)
|
|
31
|
+
include MessageLegacyMethods
|
|
34
32
|
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
@chat_class = chat_class.to_s
|
|
34
|
+
@chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
|
|
37
35
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
foreign_key: @chat_foreign_key,
|
|
41
|
-
inverse_of: :messages,
|
|
42
|
-
touch: touch_chat
|
|
36
|
+
@tool_call_class = tool_call_class.to_s
|
|
37
|
+
@tool_call_foreign_key = tool_call_foreign_key || ActiveSupport::Inflector.foreign_key(@tool_call_class)
|
|
43
38
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
belongs_to :chat,
|
|
40
|
+
class_name: @chat_class,
|
|
41
|
+
foreign_key: @chat_foreign_key,
|
|
42
|
+
inverse_of: :messages,
|
|
43
|
+
touch: touch_chat
|
|
47
44
|
|
|
48
|
-
|
|
45
|
+
has_many :tool_calls,
|
|
49
46
|
class_name: @tool_call_class,
|
|
50
|
-
|
|
51
|
-
optional: true,
|
|
52
|
-
inverse_of: :result
|
|
53
|
-
|
|
54
|
-
has_many :tool_results,
|
|
55
|
-
through: :tool_calls,
|
|
56
|
-
source: :result,
|
|
57
|
-
class_name: @message_class
|
|
47
|
+
dependent: :destroy
|
|
58
48
|
|
|
59
|
-
|
|
60
|
-
|
|
49
|
+
belongs_to :parent_tool_call,
|
|
50
|
+
class_name: @tool_call_class,
|
|
51
|
+
foreign_key: @tool_call_foreign_key,
|
|
52
|
+
optional: true,
|
|
53
|
+
inverse_of: :result
|
|
61
54
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
has_many :tool_results,
|
|
56
|
+
through: :tool_calls,
|
|
57
|
+
source: :result,
|
|
58
|
+
class_name: @message_class
|
|
66
59
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
foreign_key: @message_foreign_key,
|
|
70
|
-
inverse_of: :tool_calls
|
|
60
|
+
delegate :tool_call?, :tool_result?, to: :to_llm
|
|
61
|
+
end
|
|
71
62
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
|
64
|
+
@message_class = message_class.to_s
|
|
65
|
+
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
|
66
|
+
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
|
67
|
+
|
|
68
|
+
belongs_to :message,
|
|
69
|
+
class_name: @message_class,
|
|
70
|
+
foreign_key: @message_foreign_key,
|
|
71
|
+
inverse_of: :tool_calls
|
|
72
|
+
|
|
73
|
+
has_one :result,
|
|
74
|
+
class_name: @message_class,
|
|
75
|
+
foreign_key: @result_foreign_key,
|
|
76
|
+
inverse_of: :parent_tool_call,
|
|
77
|
+
dependent: :nullify
|
|
78
|
+
end
|
|
77
79
|
end
|
|
78
80
|
end
|
|
79
|
-
end
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
82
|
+
# Methods mixed into chat models.
|
|
83
|
+
module ChatLegacyMethods
|
|
84
|
+
extend ActiveSupport::Concern
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
class_methods do
|
|
87
|
+
attr_reader :tool_call_class
|
|
88
|
+
end
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
def to_llm(context: nil)
|
|
91
|
+
# model_id is a string that RubyLLM can resolve
|
|
92
|
+
@chat ||= if context
|
|
93
|
+
context.chat(model: model_id)
|
|
94
|
+
else
|
|
95
|
+
RubyLLM.chat(model: model_id)
|
|
96
|
+
end
|
|
97
|
+
@chat.reset_messages!
|
|
98
|
+
|
|
99
|
+
messages.each do |msg|
|
|
100
|
+
@chat.add_message(msg.to_llm)
|
|
101
|
+
end
|
|
97
102
|
|
|
98
|
-
|
|
99
|
-
@chat.add_message(msg.to_llm)
|
|
103
|
+
setup_persistence_callbacks
|
|
100
104
|
end
|
|
101
105
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
106
|
+
def with_instructions(instructions, replace: false)
|
|
107
|
+
transaction do
|
|
108
|
+
messages.where(role: :system).destroy_all if replace
|
|
109
|
+
messages.create!(role: :system, content: instructions)
|
|
110
|
+
end
|
|
111
|
+
to_llm.with_instructions(instructions)
|
|
112
|
+
self
|
|
109
113
|
end
|
|
110
|
-
to_llm.with_instructions(instructions)
|
|
111
|
-
self
|
|
112
|
-
end
|
|
113
114
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
def with_tool(...)
|
|
116
|
+
to_llm.with_tool(...)
|
|
117
|
+
self
|
|
118
|
+
end
|
|
118
119
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
def with_tools(...)
|
|
121
|
+
to_llm.with_tools(...)
|
|
122
|
+
self
|
|
123
|
+
end
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
def with_model(...)
|
|
126
|
+
update(model_id: to_llm.with_model(...).model.id)
|
|
127
|
+
self
|
|
128
|
+
end
|
|
128
129
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
130
|
+
def with_temperature(...)
|
|
131
|
+
to_llm.with_temperature(...)
|
|
132
|
+
self
|
|
133
|
+
end
|
|
133
134
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
135
|
+
def with_context(context)
|
|
136
|
+
to_llm(context: context)
|
|
137
|
+
self
|
|
138
|
+
end
|
|
138
139
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
140
|
+
def with_params(...)
|
|
141
|
+
to_llm.with_params(...)
|
|
142
|
+
self
|
|
143
|
+
end
|
|
143
144
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
def with_headers(...)
|
|
146
|
+
to_llm.with_headers(...)
|
|
147
|
+
self
|
|
148
|
+
end
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
def with_schema(...)
|
|
151
|
+
to_llm.with_schema(...)
|
|
152
|
+
self
|
|
153
|
+
end
|
|
153
154
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
def on_new_message(&)
|
|
156
|
+
to_llm.on_new_message(&)
|
|
157
|
+
self
|
|
158
|
+
end
|
|
158
159
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
def on_end_message(&)
|
|
161
|
+
to_llm.on_end_message(&)
|
|
162
|
+
self
|
|
163
|
+
end
|
|
163
164
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
165
|
+
def on_tool_call(...)
|
|
166
|
+
to_llm.on_tool_call(...)
|
|
167
|
+
self
|
|
168
|
+
end
|
|
168
169
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
170
|
+
def on_tool_result(...)
|
|
171
|
+
to_llm.on_tool_result(...)
|
|
172
|
+
self
|
|
173
|
+
end
|
|
173
174
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
175
|
+
def create_user_message(content, with: nil)
|
|
176
|
+
message_record = messages.create!(role: :user, content: content)
|
|
177
|
+
persist_content(message_record, with) if with.present?
|
|
178
|
+
message_record
|
|
179
|
+
end
|
|
179
180
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
def ask(message, with: nil, &)
|
|
182
|
+
create_user_message(message, with:)
|
|
183
|
+
complete(&)
|
|
184
|
+
end
|
|
184
185
|
|
|
185
|
-
|
|
186
|
+
alias say ask
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
188
|
+
def complete(...)
|
|
189
|
+
to_llm.complete(...)
|
|
190
|
+
rescue RubyLLM::Error => e
|
|
191
|
+
cleanup_failed_messages if @message&.persisted? && @message.content.blank?
|
|
192
|
+
cleanup_orphaned_tool_results
|
|
193
|
+
raise e
|
|
194
|
+
end
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
private
|
|
196
197
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
198
|
+
def cleanup_failed_messages
|
|
199
|
+
RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
|
|
200
|
+
@message.destroy
|
|
201
|
+
end
|
|
201
202
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
203
|
+
def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
|
|
204
|
+
messages.reload
|
|
205
|
+
last = messages.order(:id).last
|
|
205
206
|
|
|
206
|
-
|
|
207
|
+
return unless last&.tool_call? || last&.tool_result?
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
209
|
+
if last.tool_call?
|
|
210
|
+
last.destroy
|
|
211
|
+
elsif last.tool_result?
|
|
212
|
+
tool_call_message = last.parent_tool_call.message
|
|
213
|
+
expected_results = tool_call_message.tool_calls.pluck(:id)
|
|
214
|
+
actual_results = tool_call_message.tool_results.pluck(:tool_call_id)
|
|
214
215
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
if expected_results.sort != actual_results.sort
|
|
217
|
+
tool_call_message.tool_results.each(&:destroy)
|
|
218
|
+
tool_call_message.destroy
|
|
219
|
+
end
|
|
218
220
|
end
|
|
219
221
|
end
|
|
220
|
-
end
|
|
221
222
|
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
def setup_persistence_callbacks
|
|
224
|
+
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
@chat.on_new_message { persist_new_message }
|
|
227
|
+
@chat.on_end_message { |msg| persist_message_completion(msg) }
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
|
230
|
+
@chat
|
|
231
|
+
end
|
|
231
232
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
233
|
+
def persist_new_message
|
|
234
|
+
@message = messages.create!(role: :assistant, content: '')
|
|
235
|
+
end
|
|
235
236
|
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
def persist_message_completion(message) # rubocop:disable Metrics/PerceivedComplexity
|
|
238
|
+
return unless message
|
|
238
239
|
|
|
239
|
-
|
|
240
|
+
tool_call_id = find_tool_call_id(message.tool_call_id) if message.tool_call_id
|
|
240
241
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
242
|
+
transaction do
|
|
243
|
+
content = message.content
|
|
244
|
+
attachments_to_persist = nil
|
|
244
245
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
246
|
+
if content.is_a?(RubyLLM::Content)
|
|
247
|
+
attachments_to_persist = content.attachments if content.attachments.any?
|
|
248
|
+
content = content.text
|
|
249
|
+
elsif content.is_a?(Hash) || content.is_a?(Array)
|
|
250
|
+
content = content.to_json
|
|
251
|
+
end
|
|
251
252
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
253
|
+
@message.update!(
|
|
254
|
+
role: message.role,
|
|
255
|
+
content: content,
|
|
256
|
+
model_id: message.model_id,
|
|
257
|
+
input_tokens: message.input_tokens,
|
|
258
|
+
output_tokens: message.output_tokens
|
|
259
|
+
)
|
|
260
|
+
@message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
|
|
261
|
+
@message.save!
|
|
261
262
|
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
persist_content(@message, attachments_to_persist) if attachments_to_persist
|
|
264
|
+
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
|
265
|
+
end
|
|
264
266
|
end
|
|
265
|
-
end
|
|
266
267
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
268
|
+
def persist_tool_calls(tool_calls)
|
|
269
|
+
tool_calls.each_value do |tool_call|
|
|
270
|
+
attributes = tool_call.to_h
|
|
271
|
+
attributes[:tool_call_id] = attributes.delete(:id)
|
|
272
|
+
@message.tool_calls.create!(**attributes)
|
|
273
|
+
end
|
|
272
274
|
end
|
|
273
|
-
end
|
|
274
275
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
276
|
+
def find_tool_call_id(tool_call_id)
|
|
277
|
+
self.class.tool_call_class.constantize.find_by(tool_call_id: tool_call_id)&.id
|
|
278
|
+
end
|
|
278
279
|
|
|
279
|
-
|
|
280
|
-
|
|
280
|
+
def persist_content(message_record, attachments)
|
|
281
|
+
return unless message_record.respond_to?(:attachments)
|
|
281
282
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
283
|
+
attachables = prepare_for_active_storage(attachments)
|
|
284
|
+
message_record.attachments.attach(attachables) if attachables.any?
|
|
285
|
+
end
|
|
285
286
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
287
|
+
def prepare_for_active_storage(attachments)
|
|
288
|
+
Utils.to_safe_array(attachments).filter_map do |attachment|
|
|
289
|
+
case attachment
|
|
290
|
+
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
|
291
|
+
attachment
|
|
292
|
+
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
293
|
+
attachment.blobs
|
|
294
|
+
when Hash
|
|
295
|
+
attachment.values.map { |v| prepare_for_active_storage(v) }
|
|
296
|
+
else
|
|
297
|
+
convert_to_active_storage_format(attachment)
|
|
298
|
+
end
|
|
299
|
+
end.flatten.compact
|
|
300
|
+
end
|
|
300
301
|
|
|
301
|
-
|
|
302
|
-
|
|
302
|
+
def convert_to_active_storage_format(source)
|
|
303
|
+
return if source.blank?
|
|
303
304
|
|
|
304
|
-
|
|
305
|
+
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
305
306
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
307
|
+
{
|
|
308
|
+
io: StringIO.new(attachment.content),
|
|
309
|
+
filename: attachment.filename,
|
|
310
|
+
content_type: attachment.mime_type
|
|
311
|
+
}
|
|
312
|
+
rescue StandardError => e
|
|
313
|
+
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
314
|
+
nil
|
|
315
|
+
end
|
|
314
316
|
end
|
|
315
|
-
end
|
|
316
317
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
318
|
+
# Methods mixed into message models.
|
|
319
|
+
module MessageLegacyMethods
|
|
320
|
+
extend ActiveSupport::Concern
|
|
320
321
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
def to_llm
|
|
326
|
-
RubyLLM::Message.new(
|
|
327
|
-
role: role.to_sym,
|
|
328
|
-
content: extract_content,
|
|
329
|
-
tool_calls: extract_tool_calls,
|
|
330
|
-
tool_call_id: extract_tool_call_id,
|
|
331
|
-
input_tokens: input_tokens,
|
|
332
|
-
output_tokens: output_tokens,
|
|
333
|
-
model_id: model_id
|
|
334
|
-
)
|
|
335
|
-
end
|
|
322
|
+
class_methods do
|
|
323
|
+
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
|
324
|
+
end
|
|
336
325
|
|
|
337
|
-
|
|
326
|
+
def to_llm
|
|
327
|
+
RubyLLM::Message.new(
|
|
328
|
+
role: role.to_sym,
|
|
329
|
+
content: extract_content,
|
|
330
|
+
tool_calls: extract_tool_calls,
|
|
331
|
+
tool_call_id: extract_tool_call_id,
|
|
332
|
+
input_tokens: input_tokens,
|
|
333
|
+
output_tokens: output_tokens,
|
|
334
|
+
model_id: model_id
|
|
335
|
+
)
|
|
336
|
+
end
|
|
338
337
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
338
|
+
private
|
|
339
|
+
|
|
340
|
+
def extract_tool_calls
|
|
341
|
+
tool_calls.to_h do |tool_call|
|
|
342
|
+
[
|
|
343
|
+
tool_call.tool_call_id,
|
|
344
|
+
RubyLLM::ToolCall.new(
|
|
345
|
+
id: tool_call.tool_call_id,
|
|
346
|
+
name: tool_call.name,
|
|
347
|
+
arguments: tool_call.arguments
|
|
348
|
+
)
|
|
349
|
+
]
|
|
350
|
+
end
|
|
349
351
|
end
|
|
350
|
-
end
|
|
351
352
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
353
|
+
def extract_tool_call_id
|
|
354
|
+
parent_tool_call&.tool_call_id
|
|
355
|
+
end
|
|
355
356
|
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
def extract_content
|
|
358
|
+
return content unless respond_to?(:attachments) && attachments.attached?
|
|
358
359
|
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
RubyLLM::Content.new(content).tap do |content_obj|
|
|
361
|
+
@_tempfiles = []
|
|
361
362
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
attachments.each do |attachment|
|
|
364
|
+
tempfile = download_attachment(attachment)
|
|
365
|
+
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
|
366
|
+
end
|
|
365
367
|
end
|
|
366
368
|
end
|
|
367
|
-
end
|
|
368
369
|
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
370
|
+
def download_attachment(attachment)
|
|
371
|
+
ext = File.extname(attachment.filename.to_s)
|
|
372
|
+
basename = File.basename(attachment.filename.to_s, ext)
|
|
373
|
+
tempfile = Tempfile.new([basename, ext])
|
|
374
|
+
tempfile.binmode
|
|
374
375
|
|
|
375
|
-
|
|
376
|
+
attachment.download { |chunk| tempfile.write(chunk) }
|
|
376
377
|
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
378
|
+
tempfile.flush
|
|
379
|
+
tempfile.rewind
|
|
380
|
+
@_tempfiles << tempfile
|
|
381
|
+
tempfile
|
|
382
|
+
end
|
|
381
383
|
end
|
|
382
384
|
end
|
|
383
385
|
end
|