ruby_llm 1.6.4 → 1.7.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.
- checksums.yaml +4 -4
- data/README.md +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +127 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +39 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +24 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +12 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +29 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +11 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +30 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +129 -0
- data/lib/generators/ruby_llm/install/install_generator.rb +104 -0
- data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +40 -0
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +121 -0
- data/lib/ruby_llm/active_record/acts_as.rb +111 -327
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
- data/lib/ruby_llm/active_record/message_methods.rb +72 -0
- data/lib/ruby_llm/active_record/model_methods.rb +84 -0
- data/lib/ruby_llm/aliases.json +54 -13
- data/lib/ruby_llm/attachment.rb +20 -0
- data/lib/ruby_llm/chat.rb +5 -5
- data/lib/ruby_llm/configuration.rb +9 -0
- data/lib/ruby_llm/connection.rb +4 -4
- data/lib/ruby_llm/model/info.rb +12 -0
- data/lib/ruby_llm/models.json +3579 -2029
- data/lib/ruby_llm/models.rb +51 -22
- data/lib/ruby_llm/provider.rb +3 -3
- data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
- data/lib/ruby_llm/providers/anthropic/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/chat.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
- data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
- data/lib/ruby_llm/providers/gemini/media.rb +1 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +11 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
- data/lib/ruby_llm/providers/gpustack/models.rb +44 -8
- data/lib/ruby_llm/providers/gpustack.rb +1 -0
- data/lib/ruby_llm/providers/ollama/media.rb +2 -6
- data/lib/ruby_llm/providers/ollama/models.rb +36 -0
- data/lib/ruby_llm/providers/ollama.rb +1 -0
- data/lib/ruby_llm/providers/openai/chat.rb +1 -1
- data/lib/ruby_llm/providers/openai/media.rb +4 -4
- data/lib/ruby_llm/providers/openai/tools.rb +11 -6
- data/lib/ruby_llm/providers/openai.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
- data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
- data/lib/ruby_llm/providers/vertexai.rb +55 -0
- data/lib/ruby_llm/railtie.rb +20 -3
- data/lib/ruby_llm/streaming.rb +1 -1
- data/lib/ruby_llm/utils.rb +5 -9
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +4 -3
- data/lib/tasks/models.rake +39 -28
- data/lib/tasks/ruby_llm.rake +15 -0
- data/lib/tasks/vcr.rake +2 -2
- metadata +38 -3
- data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
- data/lib/generators/ruby_llm/install_generator.rb +0 -121
@@ -6,378 +6,162 @@ module RubyLLM
|
|
6
6
|
module ActsAs
|
7
7
|
extend ActiveSupport::Concern
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
9
|
+
# When ActsAs is included, ensure models are loaded from database
|
10
|
+
def self.included(base)
|
11
|
+
super
|
12
|
+
# Monkey-patch Models to use database when ActsAs is active
|
13
|
+
RubyLLM::Models.class_eval do
|
14
|
+
def load_models
|
15
|
+
read_from_database
|
16
|
+
rescue StandardError => e
|
17
|
+
RubyLLM.logger.debug "Failed to load models from database: #{e.message}, falling back to JSON"
|
18
|
+
read_from_json
|
19
|
+
end
|
15
20
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
inverse_of: :chat,
|
20
|
-
dependent: :destroy
|
21
|
+
def load_from_database!
|
22
|
+
@models = read_from_database
|
23
|
+
end
|
21
24
|
|
22
|
-
|
25
|
+
def read_from_database
|
26
|
+
model_class = RubyLLM.config.model_registry_class
|
27
|
+
model_class = model_class.constantize if model_class.is_a?(String)
|
28
|
+
model_class.all.map(&:to_llm)
|
29
|
+
end
|
23
30
|
end
|
31
|
+
end
|
24
32
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
touch_chat: false)
|
30
|
-
include MessageMethods
|
33
|
+
class_methods do # rubocop:disable Metrics/BlockLength
|
34
|
+
def acts_as_chat(messages: :messages, message_class: nil,
|
35
|
+
model: :model, model_class: nil)
|
36
|
+
include RubyLLM::ActiveRecord::ChatMethods
|
31
37
|
|
32
|
-
|
33
|
-
@chat_foreign_key = chat_foreign_key || ActiveSupport::Inflector.foreign_key(@chat_class)
|
38
|
+
class_attribute :messages_association_name, :model_association_name, :message_class, :model_class
|
34
39
|
|
35
|
-
|
36
|
-
|
40
|
+
self.messages_association_name = messages
|
41
|
+
self.model_association_name = model
|
42
|
+
self.message_class = (message_class || messages.to_s.classify).to_s
|
43
|
+
self.model_class = (model_class || model.to_s.classify).to_s
|
37
44
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
inverse_of: :messages,
|
42
|
-
touch: touch_chat
|
43
|
-
|
44
|
-
has_many :tool_calls,
|
45
|
-
class_name: @tool_call_class,
|
45
|
+
has_many messages,
|
46
|
+
-> { order(created_at: :asc) },
|
47
|
+
class_name: self.message_class,
|
46
48
|
dependent: :destroy
|
47
49
|
|
48
|
-
belongs_to
|
49
|
-
class_name:
|
50
|
-
|
51
|
-
optional: true,
|
52
|
-
inverse_of: :result
|
53
|
-
|
54
|
-
delegate :tool_call?, :tool_result?, :tool_results, to: :to_llm
|
55
|
-
end
|
56
|
-
|
57
|
-
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
58
|
-
@message_class = message_class.to_s
|
59
|
-
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
60
|
-
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
61
|
-
|
62
|
-
belongs_to :message,
|
63
|
-
class_name: @message_class,
|
64
|
-
foreign_key: @message_foreign_key,
|
65
|
-
inverse_of: :tool_calls
|
66
|
-
|
67
|
-
has_one :result,
|
68
|
-
class_name: @message_class,
|
69
|
-
foreign_key: @result_foreign_key,
|
70
|
-
inverse_of: :parent_tool_call,
|
71
|
-
dependent: :nullify
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# Methods mixed into chat models.
|
77
|
-
module ChatMethods
|
78
|
-
extend ActiveSupport::Concern
|
79
|
-
|
80
|
-
class_methods do
|
81
|
-
attr_reader :tool_call_class
|
82
|
-
end
|
50
|
+
belongs_to model,
|
51
|
+
class_name: self.model_class,
|
52
|
+
optional: true
|
83
53
|
|
84
|
-
|
85
|
-
@chat ||= if context
|
86
|
-
context.chat(model: model_id)
|
87
|
-
else
|
88
|
-
RubyLLM.chat(model: model_id)
|
89
|
-
end
|
90
|
-
@chat.reset_messages!
|
54
|
+
delegate :add_message, to: :to_llm
|
91
55
|
|
92
|
-
|
93
|
-
|
94
|
-
|
56
|
+
define_method :messages_association do
|
57
|
+
send(messages_association_name)
|
58
|
+
end
|
95
59
|
|
96
|
-
|
97
|
-
|
60
|
+
define_method :model_association do
|
61
|
+
send(model_association_name)
|
62
|
+
end
|
98
63
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
messages.create!(role: :system, content: instructions)
|
64
|
+
define_method :'model_association=' do |value|
|
65
|
+
send("#{model_association_name}=", value)
|
66
|
+
end
|
103
67
|
end
|
104
|
-
to_llm.with_instructions(instructions)
|
105
|
-
self
|
106
|
-
end
|
107
|
-
|
108
|
-
def with_tool(...)
|
109
|
-
to_llm.with_tool(...)
|
110
|
-
self
|
111
|
-
end
|
112
|
-
|
113
|
-
def with_tools(...)
|
114
|
-
to_llm.with_tools(...)
|
115
|
-
self
|
116
|
-
end
|
117
|
-
|
118
|
-
def with_model(...)
|
119
|
-
update(model_id: to_llm.with_model(...).model.id)
|
120
|
-
self
|
121
|
-
end
|
122
68
|
|
123
|
-
|
124
|
-
|
125
|
-
self
|
126
|
-
end
|
127
|
-
|
128
|
-
def with_context(context)
|
129
|
-
to_llm(context: context)
|
130
|
-
self
|
131
|
-
end
|
69
|
+
def acts_as_model(chats: :chats, chat_class: nil)
|
70
|
+
include RubyLLM::ActiveRecord::ModelMethods
|
132
71
|
|
133
|
-
|
134
|
-
to_llm.with_params(...)
|
135
|
-
self
|
136
|
-
end
|
72
|
+
class_attribute :chats_association_name, :chat_class
|
137
73
|
|
138
|
-
|
139
|
-
|
140
|
-
self
|
141
|
-
end
|
142
|
-
|
143
|
-
def with_schema(...)
|
144
|
-
to_llm.with_schema(...)
|
145
|
-
self
|
146
|
-
end
|
74
|
+
self.chats_association_name = chats
|
75
|
+
self.chat_class = (chat_class || chats.to_s.classify).to_s
|
147
76
|
|
148
|
-
|
149
|
-
|
77
|
+
validates :model_id, presence: true, uniqueness: { scope: :provider }
|
78
|
+
validates :provider, presence: true
|
79
|
+
validates :name, presence: true
|
150
80
|
|
151
|
-
|
81
|
+
has_many chats, class_name: self.chat_class
|
152
82
|
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
157
|
-
self
|
158
|
-
end
|
159
|
-
|
160
|
-
def on_end_message(&block)
|
161
|
-
to_llm
|
162
|
-
|
163
|
-
existing_callback = @chat.instance_variable_get(:@on)[:end_message]
|
164
|
-
|
165
|
-
@chat.on_end_message do |msg|
|
166
|
-
existing_callback&.call(msg)
|
167
|
-
block&.call(msg)
|
83
|
+
define_method :chats_association do
|
84
|
+
send(chats_association_name)
|
85
|
+
end
|
168
86
|
end
|
169
|
-
self
|
170
|
-
end
|
171
|
-
|
172
|
-
def on_tool_call(...)
|
173
|
-
to_llm.on_tool_call(...)
|
174
|
-
self
|
175
|
-
end
|
176
|
-
|
177
|
-
def on_tool_result(...)
|
178
|
-
to_llm.on_tool_result(...)
|
179
|
-
self
|
180
|
-
end
|
181
|
-
|
182
|
-
def create_user_message(content, with: nil)
|
183
|
-
message_record = messages.create!(role: :user, content: content)
|
184
|
-
persist_content(message_record, with) if with.present?
|
185
|
-
message_record
|
186
|
-
end
|
187
87
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
alias say ask
|
194
|
-
|
195
|
-
def complete(...)
|
196
|
-
to_llm.complete(...)
|
197
|
-
rescue RubyLLM::Error => e
|
198
|
-
cleanup_failed_messages if @message&.persisted? && @message.content.blank?
|
199
|
-
cleanup_orphaned_tool_results
|
200
|
-
raise e
|
201
|
-
end
|
202
|
-
|
203
|
-
private
|
204
|
-
|
205
|
-
def cleanup_failed_messages
|
206
|
-
RubyLLM.logger.debug "RubyLLM: API call failed, destroying message: #{@message.id}"
|
207
|
-
@message.destroy
|
208
|
-
end
|
88
|
+
def acts_as_message(chat: :chat, chat_class: nil, touch_chat: false, # rubocop:disable Metrics/ParameterLists
|
89
|
+
tool_calls: :tool_calls, tool_call_class: nil,
|
90
|
+
model: :model, model_class: nil)
|
91
|
+
include RubyLLM::ActiveRecord::MessageMethods
|
209
92
|
|
210
|
-
|
211
|
-
|
212
|
-
messages.reload
|
213
|
-
last = messages.order(:id).last
|
93
|
+
class_attribute :chat_association_name, :tool_calls_association_name, :model_association_name,
|
94
|
+
:chat_class, :tool_call_class, :model_class
|
214
95
|
|
215
|
-
|
96
|
+
self.chat_association_name = chat
|
97
|
+
self.tool_calls_association_name = tool_calls
|
98
|
+
self.model_association_name = model
|
99
|
+
self.chat_class = (chat_class || chat.to_s.classify).to_s
|
100
|
+
self.tool_call_class = (tool_call_class || tool_calls.to_s.classify).to_s
|
101
|
+
self.model_class = (model_class || model.to_s.classify).to_s
|
216
102
|
|
217
|
-
|
218
|
-
|
219
|
-
|
103
|
+
belongs_to chat,
|
104
|
+
class_name: self.chat_class,
|
105
|
+
touch: touch_chat
|
220
106
|
|
221
|
-
|
222
|
-
|
107
|
+
has_many tool_calls,
|
108
|
+
class_name: self.tool_call_class,
|
109
|
+
dependent: :destroy
|
223
110
|
|
224
|
-
|
225
|
-
|
111
|
+
belongs_to :parent_tool_call,
|
112
|
+
class_name: self.tool_call_class,
|
113
|
+
foreign_key: ActiveSupport::Inflector.foreign_key(tool_calls.to_s.singularize),
|
114
|
+
optional: true
|
226
115
|
|
227
|
-
|
228
|
-
|
229
|
-
|
116
|
+
has_many :tool_results,
|
117
|
+
through: tool_calls,
|
118
|
+
source: :result,
|
119
|
+
class_name: name
|
230
120
|
|
231
|
-
|
232
|
-
|
233
|
-
|
121
|
+
belongs_to model,
|
122
|
+
class_name: self.model_class,
|
123
|
+
optional: true
|
234
124
|
|
235
|
-
|
236
|
-
return unless message
|
125
|
+
delegate :tool_call?, :tool_result?, to: :to_llm
|
237
126
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
content = message.content
|
242
|
-
attachments_to_persist = nil
|
127
|
+
define_method :chat_association do
|
128
|
+
send(chat_association_name)
|
129
|
+
end
|
243
130
|
|
244
|
-
|
245
|
-
|
246
|
-
content = content.text
|
247
|
-
elsif content.is_a?(Hash) || content.is_a?(Array)
|
248
|
-
content = content.to_json
|
131
|
+
define_method :tool_calls_association do
|
132
|
+
send(tool_calls_association_name)
|
249
133
|
end
|
250
134
|
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
model_id: message.model_id,
|
255
|
-
input_tokens: message.input_tokens,
|
256
|
-
output_tokens: message.output_tokens
|
257
|
-
)
|
258
|
-
@message.write_attribute(@message.class.tool_call_foreign_key, tool_call_id) if tool_call_id
|
259
|
-
@message.save!
|
260
|
-
|
261
|
-
persist_content(@message, attachments_to_persist) if attachments_to_persist
|
262
|
-
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
135
|
+
define_method :model_association do
|
136
|
+
send(model_association_name)
|
137
|
+
end
|
263
138
|
end
|
264
|
-
end
|
265
139
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
attributes[:tool_call_id] = attributes.delete(:id)
|
270
|
-
@message.tool_calls.create!(**attributes)
|
271
|
-
end
|
272
|
-
end
|
140
|
+
def acts_as_tool_call(message: :message, message_class: nil,
|
141
|
+
result: :result, result_class: nil)
|
142
|
+
class_attribute :message_association_name, :result_association_name, :message_class, :result_class
|
273
143
|
|
274
|
-
|
275
|
-
|
276
|
-
|
144
|
+
self.message_association_name = message
|
145
|
+
self.result_association_name = result
|
146
|
+
self.message_class = (message_class || message.to_s.classify).to_s
|
147
|
+
self.result_class = (result_class || self.message_class).to_s
|
277
148
|
|
278
|
-
|
279
|
-
|
149
|
+
belongs_to message,
|
150
|
+
class_name: self.message_class
|
280
151
|
|
281
|
-
|
282
|
-
|
283
|
-
|
152
|
+
has_one result,
|
153
|
+
class_name: self.result_class,
|
154
|
+
dependent: :nullify
|
284
155
|
|
285
|
-
|
286
|
-
|
287
|
-
case attachment
|
288
|
-
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
289
|
-
attachment
|
290
|
-
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
291
|
-
attachment.blobs
|
292
|
-
when Hash
|
293
|
-
attachment.values.map { |v| prepare_for_active_storage(v) }
|
294
|
-
else
|
295
|
-
convert_to_active_storage_format(attachment)
|
156
|
+
define_method :message_association do
|
157
|
+
send(message_association_name)
|
296
158
|
end
|
297
|
-
end.flatten.compact
|
298
|
-
end
|
299
|
-
|
300
|
-
def convert_to_active_storage_format(source)
|
301
|
-
return if source.blank?
|
302
|
-
|
303
|
-
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
304
|
-
|
305
|
-
{
|
306
|
-
io: StringIO.new(attachment.content),
|
307
|
-
filename: attachment.filename,
|
308
|
-
content_type: attachment.mime_type
|
309
|
-
}
|
310
|
-
rescue StandardError => e
|
311
|
-
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
312
|
-
nil
|
313
|
-
end
|
314
|
-
end
|
315
|
-
|
316
|
-
# Methods mixed into message models.
|
317
|
-
module MessageMethods
|
318
|
-
extend ActiveSupport::Concern
|
319
|
-
|
320
|
-
class_methods do
|
321
|
-
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
322
|
-
end
|
323
159
|
|
324
|
-
|
325
|
-
|
326
|
-
role: role.to_sym,
|
327
|
-
content: extract_content,
|
328
|
-
tool_calls: extract_tool_calls,
|
329
|
-
tool_call_id: extract_tool_call_id,
|
330
|
-
input_tokens: input_tokens,
|
331
|
-
output_tokens: output_tokens,
|
332
|
-
model_id: model_id
|
333
|
-
)
|
334
|
-
end
|
335
|
-
|
336
|
-
private
|
337
|
-
|
338
|
-
def extract_tool_calls
|
339
|
-
tool_calls.to_h do |tool_call|
|
340
|
-
[
|
341
|
-
tool_call.tool_call_id,
|
342
|
-
RubyLLM::ToolCall.new(
|
343
|
-
id: tool_call.tool_call_id,
|
344
|
-
name: tool_call.name,
|
345
|
-
arguments: tool_call.arguments
|
346
|
-
)
|
347
|
-
]
|
348
|
-
end
|
349
|
-
end
|
350
|
-
|
351
|
-
def extract_tool_call_id
|
352
|
-
parent_tool_call&.tool_call_id
|
353
|
-
end
|
354
|
-
|
355
|
-
def extract_content
|
356
|
-
return content unless respond_to?(:attachments) && attachments.attached?
|
357
|
-
|
358
|
-
RubyLLM::Content.new(content).tap do |content_obj|
|
359
|
-
@_tempfiles = []
|
360
|
-
|
361
|
-
attachments.each do |attachment|
|
362
|
-
tempfile = download_attachment(attachment)
|
363
|
-
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
160
|
+
define_method :result_association do
|
161
|
+
send(result_association_name)
|
364
162
|
end
|
365
163
|
end
|
366
164
|
end
|
367
|
-
|
368
|
-
def download_attachment(attachment)
|
369
|
-
ext = File.extname(attachment.filename.to_s)
|
370
|
-
basename = File.basename(attachment.filename.to_s, ext)
|
371
|
-
tempfile = Tempfile.new([basename, ext])
|
372
|
-
tempfile.binmode
|
373
|
-
|
374
|
-
attachment.download { |chunk| tempfile.write(chunk) }
|
375
|
-
|
376
|
-
tempfile.flush
|
377
|
-
tempfile.rewind
|
378
|
-
@_tempfiles << tempfile
|
379
|
-
tempfile
|
380
|
-
end
|
381
165
|
end
|
382
166
|
end
|
383
167
|
end
|