Skip to content

Commit c1d2aec

Browse files
Refactor Attachment#initialize to extract source handling methods (#487)
## What this does <!-- Clear description of what this PR does and why --> ## Type of change - [ ] Bug fix - [x] New feature - [ ] Breaking change - [ ] Documentation - [ ] Performance improvement ## Scope check - [x] I read the [Contributing Guide](https://github.com/crmne/ruby_llm/blob/main/CONTRIBUTING.md) - [x] This aligns with RubyLLM's focus on **LLM communication** - [x] This isn't application-specific logic that belongs in user code - [x] This benefits most users, not just my specific use case ## Quality check - [x] I ran `overcommit --install` and all hooks pass - [x] I tested my changes thoroughly - [x] For provider changes: Re-recorded VCR cassettes with `bundle exec rake vcr:record[provider_name]` - [x] All tests pass: `bundle exec rspec` - [x] I updated documentation if needed - [x] I didn't modify auto-generated files manually (`models.json`, `aliases.json`) ## API changes - [x] Breaking change - [x] New public methods/classes - [x] Changed method signatures - [x] No API changes ## Related issues <!-- Link issues: "Fixes #123" or "Related to #123" --> [FEATURE] Support for handling ActionDispatch::Http::UploadedFile in RubyLLM (filename is always nil) #468 Co-authored-by: Carmine Paolino <[email protected]>
1 parent fa3057c commit c1d2aec

File tree

2 files changed

+133
-12
lines changed

2 files changed

+133
-12
lines changed

lib/ruby_llm/attachment.rb

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,8 @@ class Attachment
77

88
def initialize(source, filename: nil)
99
@source = source
10-
if url?
11-
@source = URI source
12-
@filename = filename || File.basename(@source.path).to_s
13-
elsif path?
14-
@source = Pathname.new source
15-
@filename = filename || @source.basename.to_s
16-
elsif active_storage?
17-
@filename = filename || extract_filename_from_active_storage
18-
else
19-
@filename = filename
20-
end
10+
@source = source_type_cast
11+
@filename = filename || source_filename
2112

2213
determine_mime_type
2314
end
@@ -166,6 +157,38 @@ def load_content_from_active_storage
166157
end
167158
end
168159

160+
def source_type_cast
161+
if url?
162+
URI(@source)
163+
elsif path?
164+
Pathname.new(@source)
165+
else
166+
@source
167+
end
168+
end
169+
170+
def source_filename
171+
if url?
172+
File.basename(@source.path).to_s
173+
elsif path?
174+
@source.basename.to_s
175+
elsif io_like?
176+
extract_filename_from_io
177+
elsif active_storage?
178+
extract_filename_from_active_storage
179+
end
180+
end
181+
182+
def extract_filename_from_io
183+
if defined?(ActionDispatch::Http::UploadedFile) && @source.is_a?(ActionDispatch::Http::UploadedFile)
184+
@source.original_filename.to_s
185+
elsif @source.respond_to?(:path)
186+
File.basename(@source.path).to_s
187+
else
188+
'attachment'
189+
end
190+
end
191+
169192
def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
170193
return 'attachment' unless defined?(ActiveStorage)
171194

spec/ruby_llm/chat_content_spec.rb

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'spec_helper'
4-
4+
require 'action_dispatch/http/upload'
55
RSpec.describe RubyLLM::Chat do # rubocop:disable RSpec/MultipleMemoizedHelpers
66
include_context 'with configured RubyLLM'
77

@@ -226,4 +226,102 @@
226226
expect(attachment.send(:url?)).to be true
227227
end
228228
end
229+
230+
describe 'IO attachment handling' do # rubocop:disable RSpec/MultipleMemoizedHelpers
231+
it 'handles StringIO objects' do
232+
require 'stringio'
233+
text_content = 'Hello, this is a test file'
234+
string_io = StringIO.new(text_content)
235+
236+
attachment = RubyLLM::Attachment.new(string_io)
237+
238+
expect(attachment.io_like?).to be true
239+
expect(attachment.content).to eq(text_content)
240+
expect(attachment.filename).to eq('attachment')
241+
expect(attachment.mime_type).to eq('application/octet-stream')
242+
end
243+
244+
it 'handles StringIO objects with filename' do
245+
require 'stringio'
246+
text_content = 'Hello, this is a test file'
247+
string_io = StringIO.new(text_content)
248+
249+
attachment = RubyLLM::Attachment.new(string_io, filename: 'test.txt')
250+
251+
expect(attachment.io_like?).to be true
252+
expect(attachment.content).to eq(text_content)
253+
expect(attachment.filename).to eq('test.txt')
254+
expect(attachment.mime_type).to eq('text/plain')
255+
end
256+
257+
it 'handles Tempfile objects' do
258+
tempfile = Tempfile.new(['test', '.txt'])
259+
tempfile.write('Tempfile content')
260+
tempfile.rewind
261+
262+
attachment = RubyLLM::Attachment.new(tempfile)
263+
264+
expect(attachment.io_like?).to be true
265+
expect(attachment.content).to eq('Tempfile content')
266+
expect(attachment.filename).to be_present
267+
expect(attachment.mime_type).to eq('text/plain')
268+
end
269+
270+
it 'handles File objects' do
271+
file = File.open(text_path, 'r')
272+
273+
attachment = RubyLLM::Attachment.new(file)
274+
275+
expect(attachment.io_like?).to be true
276+
expect(attachment.content).to be_present
277+
expect(attachment.filename).to eq('ruby.txt')
278+
expect(attachment.mime_type).to eq('text/plain')
279+
280+
file.close
281+
end
282+
283+
it 'handles ActionDispatch::Http::UploadedFile' do
284+
tempfile = Tempfile.new(['ruby', '.png'])
285+
tempfile.binmode
286+
File.open(image_path, 'rb') { |f| tempfile.write(f.read) }
287+
tempfile.rewind
288+
289+
uploaded_file = ActionDispatch::Http::UploadedFile.new(
290+
tempfile: tempfile,
291+
filename: 'ruby.png',
292+
type: 'image/png'
293+
)
294+
295+
attachment = RubyLLM::Attachment.new(uploaded_file)
296+
297+
expect(attachment.io_like?).to be true
298+
expect(attachment.content).to be_present
299+
expect(attachment.filename).to eq('ruby.png')
300+
expect(attachment.mime_type).to eq('image/png')
301+
expect(attachment.type).to eq(:image)
302+
end
303+
304+
it 'rewinds IO objects before reading' do
305+
require 'stringio'
306+
string_io = StringIO.new('Initial content')
307+
string_io.read # Move position to end
308+
309+
attachment = RubyLLM::Attachment.new(string_io, filename: 'test.txt')
310+
311+
expect(attachment.content).to eq('Initial content')
312+
end
313+
314+
it 'creates content with IO attachments' do
315+
require 'stringio'
316+
string_io = StringIO.new('Test content')
317+
content = RubyLLM::Content.new('Check this')
318+
content.add_attachment(string_io, filename: 'test.txt')
319+
320+
expect(content.attachments).not_to be_empty
321+
expect(content.attachments.first).to be_a(RubyLLM::Attachment)
322+
expect(content.attachments.first.io_like?).to be true
323+
expect(content.attachments.first.filename).to eq('test.txt')
324+
expect(content.attachments.first.mime_type).to eq('text/plain')
325+
end
326+
end
229327
end

0 commit comments

Comments
 (0)