Skip to content

Commit 53fca94

Browse files
committed
Refactored ActiveStorage integration
1 parent c99ff3a commit 53fca94

File tree

1 file changed

+33
-136
lines changed

1 file changed

+33
-136
lines changed

lib/ruby_llm/active_record/acts_as.rb

Lines changed: 33 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -143,26 +143,9 @@ def on_end_message(...)
143143
self
144144
end
145145

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?
166149
message_record
167150
end
168151

@@ -221,82 +204,13 @@ def persist_tool_calls(tool_calls)
221204
end
222205
end
223206

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)
259209

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?
266212

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)
300214
end
301215

302216
def extract_filename(file)
@@ -342,57 +256,40 @@ def extract_tool_call_id
342256
parent_tool_call&.tool_call_id
343257
end
344258

345-
def extract_content # rubocop:disable Metrics/PerceivedComplexity
259+
def extract_content
346260
return content unless respond_to?(:attachments) && attachments.attached?
347261

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)
382270
end
383271
end
384-
385-
content_obj
386272
end
387273

388274
private
389275

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)
395284
end
285+
286+
tempfile.flush
287+
tempfile.rewind
288+
289+
# Keep reference to prevent GC
290+
@_tempfiles << tempfile
291+
292+
tempfile
396293
end
397294
end
398295
end

0 commit comments

Comments
 (0)