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