ruby_llm_swarm 1.9.1

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