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