@@ -143,26 +143,9 @@ def on_end_message(...)
143
143
self
144
144
end
145
145
146
- def create_user_message ( content , with : nil ) # rubocop:disable Metrics/PerceivedComplexity
147
- message_record = messages . create! (
148
- role : :user ,
149
- content : content
150
- )
151
-
152
- if with . present?
153
- files = Array ( with ) . reject do |f |
154
- f . nil? || ( f . respond_to? ( :empty? ) && f . empty? ) || ( f . respond_to? ( :blank? ) && f . blank? )
155
- end
156
-
157
- if files . any?
158
- if files . first . is_a? ( ActionDispatch ::Http ::UploadedFile )
159
- message_record . attachments . attach ( files )
160
- else
161
- attach_files ( message_record , process_attachments ( with ) )
162
- end
163
- end
164
- end
165
-
146
+ def create_user_message ( content , with : nil )
147
+ message_record = messages . create! ( role : :user , content : content )
148
+ persist_content ( message_record , with ) if with . present?
166
149
message_record
167
150
end
168
151
@@ -221,82 +204,13 @@ def persist_tool_calls(tool_calls)
221
204
end
222
205
end
223
206
224
- def process_attachments ( attachments ) # rubocop:disable Metrics/PerceivedComplexity
225
- return { } if attachments . nil?
226
-
227
- result = { }
228
- files = Array ( attachments )
229
-
230
- files . each do |file |
231
- content_type = if file . respond_to? ( :content_type )
232
- file . content_type
233
- elsif file . is_a? ( ActiveStorage ::Attachment )
234
- file . blob . content_type
235
- else
236
- RubyLLM ::MimeTypes . detect_from_path ( file . to_s )
237
- end
238
-
239
- if RubyLLM ::MimeTypes . image? ( content_type )
240
- result [ :image ] ||= [ ]
241
- result [ :image ] << file
242
- elsif RubyLLM ::MimeTypes . audio? ( content_type )
243
- result [ :audio ] ||= [ ]
244
- result [ :audio ] << file
245
- elsif RubyLLM ::MimeTypes . pdf? ( content_type )
246
- result [ :pdf ] ||= [ ]
247
- result [ :pdf ] << file
248
- else
249
- result [ :text ] ||= [ ]
250
- result [ :text ] << file
251
- end
252
- end
253
-
254
- result
255
- end
256
-
257
- def attach_files ( message , attachments_hash )
258
- return unless message . respond_to? ( :attachments )
207
+ def persist_content ( message_record , attachments )
208
+ return unless message_record . respond_to? ( :attachments )
259
209
260
- %i[ image audio pdf text ] . each do |type |
261
- Array ( attachments_hash [ type ] ) . each do |file_source |
262
- attach_file ( message , file_source )
263
- end
264
- end
265
- end
210
+ attachments = Utils . to_safe_array ( attachments ) . reject ( &:blank? )
211
+ return if attachments . empty?
266
212
267
- def attach_file ( message , file_source )
268
- if file_source . to_s . match? ( %r{^https?://} )
269
- # For URLs, create a special attachment that just stores the URL
270
- content_type = RubyLLM ::MimeTypes . detect_from_path ( file_source . to_s )
271
-
272
- # Create a minimal blob that just stores the URL
273
- blob = ActiveStorage ::Blob . create_and_upload! (
274
- io : StringIO . new ( 'URL Reference' ) ,
275
- filename : File . basename ( file_source ) ,
276
- content_type : content_type ,
277
- metadata : { original_url : file_source . to_s }
278
- )
279
- message . attachments . attach ( blob )
280
- elsif file_source . respond_to? ( :read )
281
- # Handle various file source types
282
- message . attachments . attach (
283
- io : file_source ,
284
- filename : extract_filename ( file_source ) ,
285
- content_type : RubyLLM ::MimeTypes . detect_from_path ( extract_filename ( file_source ) )
286
- ) # Already a file-like object
287
- elsif file_source . is_a? ( ::ActiveStorage ::Attachment )
288
- # Copy from existing ActiveStorage attachment
289
- message . attachments . attach ( file_source . blob )
290
- elsif file_source . is_a? ( ::ActiveStorage ::Blob )
291
- message . attachments . attach ( file_source )
292
- else
293
- # Local file path
294
- message . attachments . attach (
295
- io : File . open ( file_source ) ,
296
- filename : File . basename ( file_source ) ,
297
- content_type : RubyLLM ::MimeTypes . detect_from_path ( file_source )
298
- )
299
- end
213
+ message_record . attachments . attach ( attachments )
300
214
end
301
215
302
216
def extract_filename ( file )
@@ -342,57 +256,40 @@ def extract_tool_call_id
342
256
parent_tool_call &.tool_call_id
343
257
end
344
258
345
- def extract_content # rubocop:disable Metrics/PerceivedComplexity
259
+ def extract_content
346
260
return content unless respond_to? ( :attachments ) && attachments . attached?
347
261
348
- content_obj = RubyLLM ::Content . new ( content )
349
-
350
- # We need to keep tempfiles alive for the duration of the API call
351
- @_tempfiles = [ ]
352
-
353
- attachments . each do |attachment |
354
- attachment_data = if attachment . metadata &.key? ( 'original_url' )
355
- attachment . metadata [ 'original_url' ]
356
- elsif defined? ( ActiveJob ) && caller . any? { |c | c . include? ( 'active_job' ) }
357
- # We're in a background job - need to download the data
358
- temp_file = Tempfile . new ( [ File . basename ( attachment . filename . to_s , '.*' ) ,
359
- File . extname ( attachment . filename . to_s ) ] )
360
- temp_file . binmode
361
- temp_file . write ( attachment . download )
362
- temp_file . flush
363
- temp_file . rewind
364
-
365
- # Store the tempfile reference in the instance variable to prevent GC
366
- @_tempfiles << temp_file
367
-
368
- # Return the file object itself, not just the path
369
- temp_file
370
- else
371
- blob_path_for ( attachment )
372
- end
373
-
374
- if RubyLLM ::MimeTypes . image? ( attachment . content_type )
375
- content_obj . add_image ( attachment_data )
376
- elsif RubyLLM ::MimeTypes . audio? ( attachment . content_type )
377
- content_obj . add_audio ( attachment_data )
378
- elsif RubyLLM ::MimeTypes . pdf? ( attachment . content_type )
379
- content_obj . add_pdf ( attachment_data )
380
- else
381
- content_obj . add_text ( attachment_data )
262
+ RubyLLM ::Content . new ( content ) . tap do |content_obj |
263
+ # Prevent tempfiles from being garbage-collected during API calls
264
+ @_tempfiles = [ ]
265
+
266
+ attachments . each do |attachment |
267
+ # Always download the file to ensure it works across all storage backends
268
+ tempfile = download_attachment ( attachment )
269
+ content_obj . add_attachment ( tempfile )
382
270
end
383
271
end
384
-
385
- content_obj
386
272
end
387
273
388
274
private
389
275
390
- def blob_path_for ( attachment )
391
- if Rails . application . routes . url_helpers . respond_to? ( :rails_blob_path )
392
- Rails . application . routes . url_helpers . rails_blob_path ( attachment , only_path : true )
393
- else
394
- attachment . service_url
276
+ def download_attachment ( attachment )
277
+ ext = File . extname ( attachment . filename . to_s )
278
+ basename = File . basename ( attachment . filename . to_s , ext )
279
+ tempfile = Tempfile . new ( [ basename , ext ] )
280
+ tempfile . binmode
281
+
282
+ attachment . download do |chunk |
283
+ tempfile . write ( chunk )
395
284
end
285
+
286
+ tempfile . flush
287
+ tempfile . rewind
288
+
289
+ # Keep reference to prevent GC
290
+ @_tempfiles << tempfile
291
+
292
+ tempfile
396
293
end
397
294
end
398
295
end
0 commit comments