ruby_llm 1.14.1 → 1.15.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -3
  3. data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
  4. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
  5. data/lib/ruby_llm/active_record/acts_as.rb +3 -0
  6. data/lib/ruby_llm/active_record/acts_as_legacy.rb +52 -25
  7. data/lib/ruby_llm/active_record/chat_methods.rb +39 -22
  8. data/lib/ruby_llm/active_record/message_methods.rb +17 -1
  9. data/lib/ruby_llm/active_record/model_methods.rb +7 -9
  10. data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
  11. data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
  12. data/lib/ruby_llm/agent.rb +3 -2
  13. data/lib/ruby_llm/aliases.json +34 -4
  14. data/lib/ruby_llm/attachment.rb +11 -27
  15. data/lib/ruby_llm/chat.rb +62 -21
  16. data/lib/ruby_llm/cost.rb +224 -0
  17. data/lib/ruby_llm/image.rb +37 -4
  18. data/lib/ruby_llm/message.rb +20 -0
  19. data/lib/ruby_llm/model/info.rb +17 -0
  20. data/lib/ruby_llm/model/pricing_category.rb +13 -2
  21. data/lib/ruby_llm/models.json +25168 -20374
  22. data/lib/ruby_llm/models.rb +2 -1
  23. data/lib/ruby_llm/models_schema.json +3 -0
  24. data/lib/ruby_llm/provider.rb +10 -3
  25. data/lib/ruby_llm/providers/anthropic/tools.rb +4 -1
  26. data/lib/ruby_llm/providers/bedrock/chat.rb +24 -13
  27. data/lib/ruby_llm/providers/bedrock/streaming.rb +4 -1
  28. data/lib/ruby_llm/providers/gemini/chat.rb +8 -1
  29. data/lib/ruby_llm/providers/gemini/images.rb +2 -2
  30. data/lib/ruby_llm/providers/gemini/streaming.rb +4 -1
  31. data/lib/ruby_llm/providers/gemini/tools.rb +3 -1
  32. data/lib/ruby_llm/providers/mistral/capabilities.rb +6 -1
  33. data/lib/ruby_llm/providers/mistral/chat.rb +55 -4
  34. data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
  35. data/lib/ruby_llm/providers/openai/chat.rb +45 -6
  36. data/lib/ruby_llm/providers/openai/images.rb +58 -6
  37. data/lib/ruby_llm/providers/openai/streaming.rb +5 -6
  38. data/lib/ruby_llm/providers/openrouter/chat.rb +30 -6
  39. data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
  40. data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
  41. data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
  42. data/lib/ruby_llm/railtie.rb +6 -0
  43. data/lib/ruby_llm/tokens.rb +8 -0
  44. data/lib/ruby_llm/tool.rb +24 -7
  45. data/lib/ruby_llm/version.rb +1 -1
  46. data/lib/ruby_llm.rb +2 -4
  47. data/lib/tasks/models.rake +13 -12
  48. metadata +19 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2e29291ded69046aa32b77f524a148e14ce1b1bfbcaddea1da99b5cee66f7b13
4
- data.tar.gz: '08a7aaa292f898f0e25f69658f19289799eb5e520feabf8a736bb3737d05fb9d'
3
+ metadata.gz: 3cfdd21451b6e3daac0463dfdb7655892967e5a4da6b00846f353809a9abe18a
4
+ data.tar.gz: e1505ddd57326601298bb8975c20049a195eb87babd62b08ac0e6340c9e653f6
5
5
  SHA512:
6
- metadata.gz: e9d4a4c8f284fc94669217c0979da4f6e6eccc95e0e47a8429b64c35c447ade3d994f278431c1593bc1fef804545e2bc0f5529775bc209b72dd728eeb4d99016
7
- data.tar.gz: c6ed2232f289a940ad3b9ee29279005528bf74a5921b911fda19e8e2cc6ecb939405e46db76a7e1e83f580da69ed368fa197355b05efada524a537419ac37381
6
+ metadata.gz: 7222e575d4f1e5cf620fdc37f4c255c7ededeee945d7b77fac4129c2947d6f78f7d5641b45ed39470180e5bf2dfd48b36e0a6c16579e4e4835564c48b6907920
7
+ data.tar.gz: ce3fb0f0e8b3665fafe56717cf8fcb10733008821633b024013d0f6e5d0bc90ef47f25b0109aa0e80ee3df4002e2f9b6a20cd98595031aa57235588cb4677052
data/README.md CHANGED
@@ -86,9 +86,7 @@ RubyLLM.moderate "Check if this text is safe"
86
86
  ```ruby
87
87
  # Let AI use your code
88
88
  class Weather < RubyLLM::Tool
89
- description "Get current weather"
90
- param :latitude
91
- param :longitude
89
+ desc "Get current weather"
92
90
 
93
91
  def execute(latitude:, longitude:)
94
92
  url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
@@ -87,6 +87,7 @@ module RubyLLM
87
87
 
88
88
  add_association_params(params, :message, message_table_name, message_model_name,
89
89
  owner_table: tool_call_table_name, owner_model_name: tool_call_model_name)
90
+ add_result_foreign_key_param(params)
90
91
 
91
92
  "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
93
  end
@@ -178,6 +179,13 @@ module RubyLLM
178
179
  "#{owner_model_name.demodulize.underscore}_id"
179
180
  end
180
181
 
182
+ def add_result_foreign_key_param(params)
183
+ foreign_key = "#{tool_call_table_name.singularize}_id"
184
+ default_foreign_key = "#{tool_call_model_name.demodulize.underscore}_id"
185
+
186
+ params << "result_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
187
+ end
188
+
181
189
  # Convert namespaced model names to proper table names
182
190
  # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
183
191
  def table_name_for(model_name)
@@ -1,5 +1,5 @@
1
1
  class <%= class_name %>Tool < RubyLLM::Tool
2
- description "TODO: describe what this tool does"
2
+ desc "TODO: describe what this tool does"
3
3
 
4
4
  def execute
5
5
  # TODO: return something to the LLM.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'active_support/inflector'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Adds chat and message persistence capabilities to ActiveRecord models.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'active_support/inflector'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Adds chat and message persistence capabilities to ActiveRecord models.
@@ -160,27 +163,33 @@ module RubyLLM
160
163
  self
161
164
  end
162
165
 
163
- def on_new_message(&block)
164
- to_llm
166
+ def on_new_message(&)
167
+ to_llm.on_new_message(&)
168
+ self
169
+ end
165
170
 
166
- existing_callback = @chat.instance_variable_get(:@on)[:new_message]
171
+ def on_end_message(&)
172
+ to_llm.on_end_message(&)
173
+ self
174
+ end
167
175
 
168
- @chat.on_new_message do
169
- existing_callback&.call
170
- block&.call
171
- end
176
+ def before_message(...)
177
+ to_llm.before_message(...)
172
178
  self
173
179
  end
174
180
 
175
- def on_end_message(&block)
176
- to_llm
181
+ def after_message(...)
182
+ to_llm.after_message(...)
183
+ self
184
+ end
177
185
 
178
- existing_callback = @chat.instance_variable_get(:@on)[:end_message]
186
+ def before_tool_call(...)
187
+ to_llm.before_tool_call(...)
188
+ self
189
+ end
179
190
 
180
- @chat.on_end_message do |msg|
181
- existing_callback&.call(msg)
182
- block&.call(msg)
183
- end
191
+ def after_tool_result(...)
192
+ to_llm.after_tool_result(...)
184
193
  self
185
194
  end
186
195
 
@@ -319,8 +328,8 @@ module RubyLLM
319
328
  def setup_persistence_callbacks
320
329
  return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
321
330
 
322
- @chat.on_new_message { persist_new_message }
323
- @chat.on_end_message { |msg| persist_message_completion(msg) }
331
+ @chat.before_message { persist_new_message }
332
+ @chat.after_message { |msg| persist_message_completion(msg) }
324
333
 
325
334
  @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
326
335
  @chat
@@ -383,8 +392,8 @@ module RubyLLM
383
392
  case attachment
384
393
  when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
385
394
  attachment
386
- when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
387
- attachment.blobs
395
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
396
+ active_storage_blobs(attachment)
388
397
  when Hash
389
398
  attachment.values.map { |v| prepare_for_active_storage(v) }
390
399
  else
@@ -398,16 +407,28 @@ module RubyLLM
398
407
 
399
408
  attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
400
409
 
401
- {
402
- io: StringIO.new(attachment.content),
403
- filename: attachment.filename,
404
- content_type: attachment.mime_type
405
- }
410
+ if attachment.active_storage?
411
+ active_storage_blobs(attachment.source)
412
+ else
413
+ {
414
+ io: StringIO.new(attachment.content),
415
+ filename: attachment.filename,
416
+ content_type: attachment.mime_type
417
+ }
418
+ end
406
419
  rescue StandardError => e
407
420
  RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
408
421
  nil
409
422
  end
410
423
 
424
+ def active_storage_blobs(attachment)
425
+ case attachment
426
+ when ActiveStorage::Blob then attachment
427
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One then attachment.blob
428
+ when ActiveStorage::Attached::Many then attachment.blobs
429
+ end
430
+ end
431
+
411
432
  def build_content(message, attachments)
412
433
  return message if content_like?(message)
413
434
 
@@ -473,9 +494,15 @@ module RubyLLM
473
494
  end
474
495
 
475
496
  def extract_content
476
- return content unless respond_to?(:attachments) && attachments.attached?
497
+ text_content = if content.respond_to?(:to_plain_text)
498
+ content.to_plain_text
499
+ else
500
+ content
501
+ end
502
+
503
+ return text_content unless respond_to?(:attachments) && attachments.attached?
477
504
 
478
- RubyLLM::Content.new(content).tap do |content_obj|
505
+ RubyLLM::Content.new(text_content).tap do |content_obj|
479
506
  @_tempfiles = []
480
507
 
481
508
  attachments.each do |attachment|
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+
3
5
  module RubyLLM
4
6
  module ActiveRecord
5
7
  # Methods mixed into chat models.
@@ -154,27 +156,33 @@ module RubyLLM
154
156
  self
155
157
  end
156
158
 
157
- def on_new_message(&block)
158
- to_llm
159
+ def on_new_message(&)
160
+ to_llm.on_new_message(&)
161
+ self
162
+ end
159
163
 
160
- existing_callback = @chat.instance_variable_get(:@on)[:new_message]
164
+ def on_end_message(&)
165
+ to_llm.on_end_message(&)
166
+ self
167
+ end
161
168
 
162
- @chat.on_new_message do
163
- existing_callback&.call
164
- block&.call
165
- end
169
+ def before_message(...)
170
+ to_llm.before_message(...)
166
171
  self
167
172
  end
168
173
 
169
- def on_end_message(&block)
170
- to_llm
174
+ def after_message(...)
175
+ to_llm.after_message(...)
176
+ self
177
+ end
171
178
 
172
- existing_callback = @chat.instance_variable_get(:@on)[:end_message]
179
+ def before_tool_call(...)
180
+ to_llm.before_tool_call(...)
181
+ self
182
+ end
173
183
 
174
- @chat.on_end_message do |msg|
175
- existing_callback&.call(msg)
176
- block&.call(msg)
177
- end
184
+ def after_tool_result(...)
185
+ to_llm.after_tool_result(...)
178
186
  self
179
187
  end
180
188
 
@@ -208,6 +216,10 @@ module RubyLLM
208
216
  message_record
209
217
  end
210
218
 
219
+ def cost
220
+ RubyLLM::Cost.aggregate(messages_association.map(&:cost))
221
+ end
222
+
211
223
  def create_user_message(content, with: nil)
212
224
  add_message(role: :user, content: build_content(content, with))
213
225
  end
@@ -258,8 +270,8 @@ module RubyLLM
258
270
  def setup_persistence_callbacks
259
271
  return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
260
272
 
261
- @chat.on_new_message { persist_new_message }
262
- @chat.on_end_message { |msg| persist_message_completion(msg) }
273
+ @chat.before_message { persist_new_message }
274
+ @chat.after_message { |msg| persist_message_completion(msg) }
263
275
 
264
276
  @chat.instance_variable_set(:@_persistence_callbacks_setup, true)
265
277
  @chat
@@ -402,8 +414,8 @@ module RubyLLM
402
414
  case attachment
403
415
  when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
404
416
  attachment
405
- when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
406
- attachment.blobs
417
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
418
+ active_storage_blobs(attachment)
407
419
  when Hash
408
420
  attachment.values.map { |v| prepare_for_active_storage(v) }
409
421
  else
@@ -418,10 +430,7 @@ module RubyLLM
418
430
  attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
419
431
 
420
432
  if attachment.active_storage?
421
- case attachment.source
422
- when ActiveStorage::Blob then attachment.source
423
- when ActiveStorage::Attached::One, ActiveStorage::Attached::Many then attachment.source.blobs
424
- end
433
+ active_storage_blobs(attachment.source)
425
434
  else
426
435
  {
427
436
  io: StringIO.new(attachment.content),
@@ -434,6 +443,14 @@ module RubyLLM
434
443
  nil
435
444
  end
436
445
 
446
+ def active_storage_blobs(attachment)
447
+ case attachment
448
+ when ActiveStorage::Blob then attachment
449
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One then attachment.blob
450
+ when ActiveStorage::Attached::Many then attachment.blobs
451
+ end
452
+ end
453
+
437
454
  def build_content(message, attachments)
438
455
  return message if content_like?(message)
439
456
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'ruby_llm/active_record/payload_helpers'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Methods mixed into message models.
@@ -40,6 +43,18 @@ module RubyLLM
40
43
  )
41
44
  end
42
45
 
46
+ def cost
47
+ RubyLLM::Cost.new(tokens:, model: model_association)
48
+ end
49
+
50
+ def cache_read_tokens
51
+ cached_value
52
+ end
53
+
54
+ def cache_write_tokens
55
+ cache_creation_value
56
+ end
57
+
43
58
  def to_partial_path
44
59
  partial_prefix = self.class.name.underscore.pluralize
45
60
  role_partial = if to_llm.tool_call?
@@ -99,7 +114,8 @@ module RubyLLM
99
114
  def extract_content
100
115
  return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
101
116
 
102
- content_value = self[:content]
117
+ content_value = content
118
+ content_value = content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
103
119
 
104
120
  return content_value unless respond_to?(:attachments) && attachments.attached?
105
121
 
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'active_support/core_ext/module/delegation'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Methods mixed into model registry models.
@@ -10,15 +13,7 @@ module RubyLLM
10
13
  def refresh!
11
14
  RubyLLM.models.refresh!
12
15
 
13
- transaction do
14
- RubyLLM.models.all.each do |model_info|
15
- model = find_or_initialize_by(
16
- model_id: model_info.id,
17
- provider: model_info.provider
18
- )
19
- model.update!(from_llm_attributes(model_info))
20
- end
21
- end
16
+ save_to_database
22
17
  end
23
18
 
24
19
  def save_to_database
@@ -76,8 +71,11 @@ module RubyLLM
76
71
 
77
72
  delegate :supports?, :supports_vision?, :supports_functions?, :type,
78
73
  :input_price_per_million, :output_price_per_million,
74
+ :cache_read_input_price_per_million, :cache_write_input_price_per_million,
75
+ :cached_input_price_per_million, :cache_creation_input_price_per_million,
79
76
  :function_calling?, :structured_output?, :batch?,
80
77
  :reasoning?, :citations?, :streaming?, :provider_class, :label,
78
+ :cost_for,
81
79
  to: :to_llm
82
80
  end
83
81
  end
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'json'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Shared helpers for parsing serialized payloads on ActiveRecord-backed models.
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'active_support/concern'
4
+ require 'ruby_llm/active_record/payload_helpers'
5
+
3
6
  module RubyLLM
4
7
  module ActiveRecord
5
8
  # Methods mixed into tool call models.
@@ -359,7 +359,8 @@ module RubyLLM
359
359
 
360
360
  def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
361
361
  :with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
362
- :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :each, :complete,
363
- :add_message, :reset_messages!
362
+ :with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :before_message,
363
+ :after_message, :before_tool_call, :after_tool_result, :each, :complete, :add_message,
364
+ :reset_messages!, :cost
364
365
  end
365
366
  end
@@ -9,7 +9,6 @@
9
9
  },
10
10
  "claude-3-5-sonnet": {
11
11
  "anthropic": "claude-3-5-sonnet-20241022",
12
- "openrouter": "anthropic/claude-3.5-sonnet",
13
12
  "bedrock": "anthropic.claude-3-5-sonnet-20241022-v2:0"
14
13
  },
15
14
  "claude-3-7-sonnet": {
@@ -17,9 +16,6 @@
17
16
  "openrouter": "anthropic/claude-3.7-sonnet",
18
17
  "bedrock": "anthropic.claude-3-7-sonnet-20250219-v1:0"
19
18
  },
20
- "claude-3-7-sonnet-latest": {
21
- "anthropic": "claude-3-7-sonnet-latest"
22
- },
23
19
  "claude-3-haiku": {
24
20
  "anthropic": "claude-3-haiku-20240307",
25
21
  "openrouter": "anthropic/claude-3-haiku",
@@ -64,6 +60,12 @@
64
60
  "bedrock": "anthropic.claude-opus-4-6-v1",
65
61
  "azure": "claude-opus-4-6"
66
62
  },
63
+ "claude-opus-4-7": {
64
+ "anthropic": "claude-opus-4-7",
65
+ "openrouter": "anthropic/claude-opus-4.7",
66
+ "bedrock": "anthropic.claude-opus-4-7",
67
+ "azure": "claude-opus-4-7"
68
+ },
67
69
  "claude-sonnet-4": {
68
70
  "anthropic": "claude-sonnet-4-20250514",
69
71
  "openrouter": "anthropic/claude-sonnet-4",
@@ -88,6 +90,14 @@
88
90
  "deepseek": "deepseek-chat",
89
91
  "openrouter": "deepseek/deepseek-chat"
90
92
  },
93
+ "deepseek-v4-flash": {
94
+ "deepseek": "deepseek-v4-flash",
95
+ "openrouter": "deepseek/deepseek-v4-flash"
96
+ },
97
+ "deepseek-v4-pro": {
98
+ "deepseek": "deepseek-v4-pro",
99
+ "openrouter": "deepseek/deepseek-v4-pro"
100
+ },
91
101
  "gemini-1.5-flash": {
92
102
  "gemini": "gemini-1.5-flash",
93
103
  "vertexai": "gemini-1.5-flash"
@@ -207,6 +217,10 @@
207
217
  "gemini": "gemini-embedding-001",
208
218
  "vertexai": "gemini-embedding-001"
209
219
  },
220
+ "gemini-embedding-2": {
221
+ "gemini": "gemini-embedding-2",
222
+ "vertexai": "gemini-embedding-2"
223
+ },
210
224
  "gemini-flash": {
211
225
  "gemini": "gemini-flash-latest",
212
226
  "vertexai": "gemini-flash-latest"
@@ -231,6 +245,14 @@
231
245
  "gemini": "gemma-3n-e4b-it",
232
246
  "openrouter": "google/gemma-3n-e4b-it"
233
247
  },
248
+ "gemma-4-26b-a4b-it": {
249
+ "gemini": "gemma-4-26b-a4b-it",
250
+ "openrouter": "google/gemma-4-26b-a4b-it"
251
+ },
252
+ "gemma-4-31b-it": {
253
+ "gemini": "gemma-4-31b-it",
254
+ "openrouter": "google/gemma-4-31b-it"
255
+ },
234
256
  "gpt-3.5-turbo": {
235
257
  "openai": "gpt-3.5-turbo",
236
258
  "openrouter": "openai/gpt-3.5-turbo"
@@ -378,6 +400,14 @@
378
400
  "openai": "gpt-5.4-pro",
379
401
  "openrouter": "openai/gpt-5.4-pro"
380
402
  },
403
+ "gpt-5.5": {
404
+ "openai": "gpt-5.5",
405
+ "openrouter": "openai/gpt-5.5"
406
+ },
407
+ "gpt-5.5-pro": {
408
+ "openai": "gpt-5.5-pro",
409
+ "openrouter": "openai/gpt-5.5-pro"
410
+ },
381
411
  "gpt-audio": {
382
412
  "openai": "gpt-audio",
383
413
  "openrouter": "openai/gpt-audio"
@@ -32,6 +32,7 @@ module RubyLLM
32
32
  return false unless defined?(ActiveStorage)
33
33
 
34
34
  @source.is_a?(ActiveStorage::Blob) ||
35
+ @source.is_a?(ActiveStorage::Attachment) ||
35
36
  @source.is_a?(ActiveStorage::Attached::One) ||
36
37
  @source.is_a?(ActiveStorage::Attached::Many)
37
38
  end
@@ -148,16 +149,7 @@ module RubyLLM
148
149
  def load_content_from_active_storage
149
150
  return unless defined?(ActiveStorage)
150
151
 
151
- @content = case @source
152
- when ActiveStorage::Blob
153
- @source.download
154
- when ActiveStorage::Attached::One
155
- @source.blob&.download
156
- when ActiveStorage::Attached::Many
157
- # For multiple attachments, just take the first one
158
- # This maintains the single-attachment interface
159
- @source.blobs.first&.download
160
- end
152
+ @content = active_storage_blob&.download
161
153
  end
162
154
 
163
155
  def source_type_cast
@@ -192,31 +184,23 @@ module RubyLLM
192
184
  end
193
185
  end
194
186
 
195
- def extract_filename_from_active_storage # rubocop:disable Metrics/PerceivedComplexity
187
+ def extract_filename_from_active_storage
196
188
  return 'attachment' unless defined?(ActiveStorage)
197
189
 
198
- case @source
199
- when ActiveStorage::Blob
200
- @source.filename.to_s
201
- when ActiveStorage::Attached::One
202
- @source.blob&.filename&.to_s || 'attachment'
203
- when ActiveStorage::Attached::Many
204
- @source.blobs.first&.filename&.to_s || 'attachment'
205
- else
206
- 'attachment'
207
- end
190
+ active_storage_blob&.filename&.to_s || 'attachment'
208
191
  end
209
192
 
210
193
  def active_storage_content_type
211
194
  return unless defined?(ActiveStorage)
212
195
 
196
+ active_storage_blob&.content_type
197
+ end
198
+
199
+ def active_storage_blob
213
200
  case @source
214
- when ActiveStorage::Blob
215
- @source.content_type
216
- when ActiveStorage::Attached::One
217
- @source.blob&.content_type
218
- when ActiveStorage::Attached::Many
219
- @source.blobs.first&.content_type
201
+ when ActiveStorage::Blob then @source
202
+ when ActiveStorage::Attachment, ActiveStorage::Attached::One then @source.blob
203
+ when ActiveStorage::Attached::Many then @source.blobs.first
220
204
  end
221
205
  end
222
206
  end