ruby_llm 1.6.4 → 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 (76) 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 +111 -327
  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 +54 -13
  37. data/lib/ruby_llm/attachment.rb +20 -0
  38. data/lib/ruby_llm/chat.rb +5 -5
  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/bedrock/chat.rb +2 -2
  48. data/lib/ruby_llm/providers/bedrock/models.rb +19 -1
  49. data/lib/ruby_llm/providers/gemini/chat.rb +1 -1
  50. data/lib/ruby_llm/providers/gemini/media.rb +1 -1
  51. data/lib/ruby_llm/providers/gpustack/chat.rb +11 -0
  52. data/lib/ruby_llm/providers/gpustack/media.rb +45 -0
  53. data/lib/ruby_llm/providers/gpustack/models.rb +44 -8
  54. data/lib/ruby_llm/providers/gpustack.rb +1 -0
  55. data/lib/ruby_llm/providers/ollama/media.rb +2 -6
  56. data/lib/ruby_llm/providers/ollama/models.rb +36 -0
  57. data/lib/ruby_llm/providers/ollama.rb +1 -0
  58. data/lib/ruby_llm/providers/openai/chat.rb +1 -1
  59. data/lib/ruby_llm/providers/openai/media.rb +4 -4
  60. data/lib/ruby_llm/providers/openai/tools.rb +11 -6
  61. data/lib/ruby_llm/providers/openai.rb +2 -2
  62. data/lib/ruby_llm/providers/vertexai/chat.rb +14 -0
  63. data/lib/ruby_llm/providers/vertexai/embeddings.rb +32 -0
  64. data/lib/ruby_llm/providers/vertexai/models.rb +130 -0
  65. data/lib/ruby_llm/providers/vertexai/streaming.rb +14 -0
  66. data/lib/ruby_llm/providers/vertexai.rb +55 -0
  67. data/lib/ruby_llm/railtie.rb +20 -3
  68. data/lib/ruby_llm/streaming.rb +1 -1
  69. data/lib/ruby_llm/utils.rb +5 -9
  70. data/lib/ruby_llm/version.rb +1 -1
  71. data/lib/ruby_llm.rb +4 -3
  72. data/lib/tasks/models.rake +39 -28
  73. data/lib/tasks/ruby_llm.rake +15 -0
  74. data/lib/tasks/vcr.rake +2 -2
  75. metadata +36 -2
  76. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
@@ -12,14 +12,13 @@ module RubyLLM
12
12
 
13
13
  source_root File.expand_path('install/templates', __dir__)
14
14
 
15
- class_option :chat_model_name, type: :string, default: 'Chat',
16
- desc: 'Name of the Chat model class'
17
- class_option :message_model_name, type: :string, default: 'Message',
18
- desc: 'Name of the Message model class'
19
- class_option :tool_call_model_name, type: :string, default: 'ToolCall',
20
- desc: 'Name of the ToolCall model class'
15
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
21
16
 
22
- desc 'Creates model files for Chat, Message, and ToolCall, and creates migrations for RubyLLM Rails integration'
17
+ class_option :skip_active_storage, type: :boolean, default: false,
18
+ desc: 'Skip ActiveStorage installation and attachment setup'
19
+
20
+ desc 'Creates models and migrations for RubyLLM Rails integration\n' \
21
+ 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
23
22
 
24
23
  def self.next_migration_number(dirname)
25
24
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -35,14 +34,48 @@ module RubyLLM
35
34
  false
36
35
  end
37
36
 
37
+ def parse_model_mappings
38
+ @model_names = {
39
+ chat: 'Chat',
40
+ message: 'Message',
41
+ tool_call: 'ToolCall',
42
+ model: 'Model'
43
+ }
44
+
45
+ model_mappings.each do |mapping|
46
+ if mapping.include?(':')
47
+ key, value = mapping.split(':', 2)
48
+ @model_names[key.to_sym] = value.classify
49
+ end
50
+ end
51
+
52
+ @model_names
53
+ end
54
+
55
+ %i[chat message tool_call model].each do |type|
56
+ define_method("#{type}_model_name") do
57
+ @model_names ||= parse_model_mappings
58
+ @model_names[type]
59
+ end
60
+ end
61
+
38
62
  def acts_as_chat_declaration
39
63
  acts_as_chat_params = []
40
- if options[:message_model_name] != 'Message'
41
- acts_as_chat_params << "message_class: \"#{options[:message_model_name]}\""
64
+ messages_assoc = message_model_name.tableize.to_sym
65
+ model_assoc = model_model_name.underscore.to_sym
66
+
67
+ if messages_assoc != :messages
68
+ acts_as_chat_params << "messages: :#{messages_assoc}"
69
+ if message_model_name != messages_assoc.to_s.classify
70
+ acts_as_chat_params << "message_class: '#{message_model_name}'"
71
+ end
42
72
  end
43
- if options[:tool_call_model_name] != 'ToolCall'
44
- acts_as_chat_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
73
+
74
+ if model_assoc != :model
75
+ acts_as_chat_params << "model: :#{model_assoc}"
76
+ acts_as_chat_params << "model_class: '#{model_model_name}'" if model_model_name != model_assoc.to_s.classify
45
77
  end
78
+
46
79
  if acts_as_chat_params.any?
47
80
  "acts_as_chat #{acts_as_chat_params.join(', ')}"
48
81
  else
@@ -51,23 +84,40 @@ module RubyLLM
51
84
  end
52
85
 
53
86
  def acts_as_message_declaration
54
- acts_as_message_params = []
55
- acts_as_message_params << "chat_class: \"#{options[:chat_model_name]}\"" if options[:chat_model_name] != 'Chat'
56
- if options[:tool_call_model_name] != 'ToolCall'
57
- acts_as_message_params << "tool_call_class: \"#{options[:tool_call_model_name]}\""
58
- end
59
- if acts_as_message_params.any?
60
- "acts_as_message #{acts_as_message_params.join(', ')}"
61
- else
62
- 'acts_as_message'
63
- end
87
+ params = []
88
+
89
+ add_message_association_params(params, :chat, chat_model_name)
90
+ add_message_association_params(params, :tool_calls, tool_call_model_name, tableize: true)
91
+ add_message_association_params(params, :model, model_model_name)
92
+
93
+ params.any? ? "acts_as_message #{params.join(', ')}" : 'acts_as_message'
94
+ end
95
+
96
+ private
97
+
98
+ def add_message_association_params(params, default_assoc, model_name, tableize: false)
99
+ assoc = tableize ? model_name.tableize.to_sym : model_name.underscore.to_sym
100
+
101
+ return if assoc == default_assoc
102
+
103
+ params << "#{default_assoc}: :#{assoc}"
104
+ expected_class = assoc.to_s.classify
105
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != expected_class
64
106
  end
65
107
 
108
+ public
109
+
66
110
  def acts_as_tool_call_declaration
67
111
  acts_as_tool_call_params = []
68
- if options[:message_model_name] != 'Message'
69
- acts_as_tool_call_params << "message_class: \"#{options[:message_model_name]}\""
112
+ message_assoc = message_model_name.underscore.to_sym
113
+
114
+ if message_assoc != :message
115
+ acts_as_tool_call_params << "message: :#{message_assoc}"
116
+ if message_model_name != message_assoc.to_s.classify
117
+ acts_as_tool_call_params << "message_class: '#{message_model_name}'"
118
+ end
70
119
  end
120
+
71
121
  if acts_as_tool_call_params.any?
72
122
  "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
73
123
  else
@@ -75,46 +125,92 @@ module RubyLLM
75
125
  end
76
126
  end
77
127
 
128
+ def acts_as_model_declaration
129
+ acts_as_model_params = []
130
+ chats_assoc = chat_model_name.tableize.to_sym
131
+
132
+ if chats_assoc != :chats
133
+ acts_as_model_params << "chats: :#{chats_assoc}"
134
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
135
+ end
136
+
137
+ if acts_as_model_params.any?
138
+ "acts_as_model #{acts_as_model_params.join(', ')}"
139
+ else
140
+ 'acts_as_model'
141
+ end
142
+ end
143
+
78
144
  def create_migration_files
79
145
  # Create migrations with timestamps to ensure proper order
80
146
  # First create chats table
81
147
  migration_template 'create_chats_migration.rb.tt',
82
- "db/migrate/create_#{options[:chat_model_name].tableize}.rb"
148
+ "db/migrate/create_#{chat_model_name.tableize}.rb"
83
149
 
84
150
  # Then create messages table (must come before tool_calls due to foreign key)
85
151
  sleep 1 # Ensure different timestamp
86
152
  migration_template 'create_messages_migration.rb.tt',
87
- "db/migrate/create_#{options[:message_model_name].tableize}.rb"
153
+ "db/migrate/create_#{message_model_name.tableize}.rb"
88
154
 
89
- # Finally create tool_calls table (references messages)
155
+ # Then create tool_calls table (references messages)
90
156
  sleep 1 # Ensure different timestamp
91
157
  migration_template 'create_tool_calls_migration.rb.tt',
92
- "db/migrate/create_#{options[:tool_call_model_name].tableize}.rb"
158
+ "db/migrate/create_#{tool_call_model_name.tableize}.rb"
159
+
160
+ # Create models table
161
+ sleep 1 # Ensure different timestamp
162
+ migration_template 'create_models_migration.rb.tt',
163
+ "db/migrate/create_#{model_model_name.tableize}.rb"
93
164
  end
94
165
 
95
166
  def create_model_files
96
- template 'chat_model.rb.tt', "app/models/#{options[:chat_model_name].underscore}.rb"
97
- template 'message_model.rb.tt', "app/models/#{options[:message_model_name].underscore}.rb"
98
- template 'tool_call_model.rb.tt', "app/models/#{options[:tool_call_model_name].underscore}.rb"
167
+ template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
168
+ template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
169
+ template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
170
+
171
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
99
172
  end
100
173
 
101
174
  def create_initializer
102
175
  template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
103
176
  end
104
177
 
178
+ def install_active_storage
179
+ return if options[:skip_active_storage]
180
+
181
+ say ' Installing ActiveStorage for file attachments...', :cyan
182
+ rails_command 'active_storage:install'
183
+ end
184
+
105
185
  def show_install_info
106
186
  say "\n ✅ RubyLLM installed!", :green
107
187
 
188
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
189
+
108
190
  say "\n Next steps:", :yellow
109
191
  say ' 1. Run: rails db:migrate'
110
192
  say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
111
- say " 3. Start chatting: #{options[:chat_model_name]}.create!(model_id: 'gpt-4.1-nano').ask('Hello!')"
112
193
 
113
- say "\n 📚 Full docs: https://rubyllm.com", :cyan
194
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
195
+
196
+ say "\n 🚀 Model registry is database-backed!", :cyan
197
+ say ' Models automatically load from the database'
198
+ say ' Pass model names as strings - RubyLLM handles the rest!'
199
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
200
+
201
+ if options[:skip_active_storage]
202
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
203
+ say ' File attachments won\'t work without ActiveStorage.'
204
+ say ' To enable later:'
205
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
206
+ say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
207
+ end
208
+
209
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
114
210
 
115
211
  say "\n ❤️ Love RubyLLM?", :magenta
116
212
  say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
117
- say ' • 💖 Sponsor: https://github.com/sponsors/crmne'
213
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
118
214
  say "\n"
119
215
  end
120
216
  end
@@ -0,0 +1,137 @@
1
+ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ model_class = <%= model_model_name %>
4
+ chat_class = <%= chat_model_name %>
5
+ message_class = <%= message_model_name %>
6
+
7
+ # Then check for any models in existing data that aren't in models.json
8
+ say_with_time "Checking for additional models in existing data" do
9
+ collect_and_create_models(chat_class, :<%= chat_model_name.tableize %>, model_class)
10
+ collect_and_create_models(message_class, :<%= message_model_name.tableize %>, model_class)
11
+ model_class.count
12
+ end
13
+
14
+ # Migrate foreign keys
15
+ migrate_foreign_key(:<%= chat_model_name.tableize %>, chat_class, model_class, :<%= model_model_name.underscore %>)
16
+ migrate_foreign_key(:<%= message_model_name.tableize %>, message_class, model_class, :<%= model_model_name.underscore %>)
17
+ end
18
+
19
+ def down
20
+ # Remove foreign key references
21
+ if column_exists?(:<%= message_model_name.tableize %>, :<%= model_model_name.underscore %>_id)
22
+ remove_reference :<%= message_model_name.tableize %>, :<%= model_model_name.underscore %>, foreign_key: true
23
+ end
24
+
25
+ if column_exists?(:<%= chat_model_name.tableize %>, :<%= model_model_name.underscore %>_id)
26
+ remove_reference :<%= chat_model_name.tableize %>, :<%= model_model_name.underscore %>, foreign_key: true
27
+ end
28
+
29
+ # Restore original model_id string columns
30
+ if column_exists?(:<%= message_model_name.tableize %>, :model_id_string)
31
+ rename_column :<%= message_model_name.tableize %>, :model_id_string, :model_id
32
+ end
33
+
34
+ if column_exists?(:<%= chat_model_name.tableize %>, :model_id_string)
35
+ rename_column :<%= chat_model_name.tableize %>, :model_id_string, :model_id
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def collect_and_create_models(record_class, table_name, model_class)
42
+ return unless column_exists?(table_name, :model_id)
43
+
44
+ has_provider = column_exists?(table_name, :provider)
45
+
46
+ # Collect unique model/provider combinations using read_attribute to bypass overrides
47
+ models_set = Set.new
48
+
49
+ record_class.find_each do |record|
50
+ model_id = record.read_attribute(:model_id)
51
+ next if model_id.blank?
52
+
53
+ provider = has_provider ? record.read_attribute(:provider) : nil
54
+ models_set.add([ model_id, provider ])
55
+ end
56
+
57
+ models_set.each do |model_id, provider|
58
+ find_or_create_model(model_id, provider, model_class)
59
+ end
60
+ end
61
+
62
+ def find_or_create_model(model_id, provider, model_class)
63
+ return if model_id.blank?
64
+
65
+ begin
66
+ model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
67
+
68
+ model_class.find_or_create_by!(
69
+ model_id: model_info.id,
70
+ provider: model_info.provider
71
+ ) do |m|
72
+ m.name = model_info.name || model_info.id
73
+ m.family = model_info.family
74
+ m.model_created_at = model_info.created_at
75
+ m.context_window = model_info.context_window
76
+ m.max_output_tokens = model_info.max_output_tokens
77
+ m.knowledge_cutoff = model_info.knowledge_cutoff
78
+ m.modalities = model_info.modalities.to_h
79
+ m.capabilities = model_info.capabilities
80
+ m.pricing = model_info.pricing.to_h
81
+ m.metadata = model_info.metadata
82
+ end
83
+ rescue => e
84
+ # Skip models that can't be resolved - they'll need manual fixing
85
+ Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
86
+ nil
87
+ end
88
+ end
89
+
90
+
91
+ def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
92
+ return unless column_exists?(table_name, :model_id)
93
+
94
+ # Check if we need to rename the string column to avoid collision
95
+ if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
96
+ # Temporarily rename the string column
97
+ rename_column table_name, :model_id, :model_id_string
98
+ end
99
+
100
+ # Add the foreign key reference
101
+ unless column_exists?(table_name, "#{foreign_key_name}_id")
102
+ add_reference table_name, foreign_key_name, foreign_key: true
103
+ end
104
+
105
+ say_with_time "Migrating #{table_name} model references" do
106
+ record_class.reset_column_information
107
+ has_provider = column_exists?(table_name, :provider)
108
+
109
+ # Determine which column to read from (renamed or original)
110
+ model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
111
+
112
+ record_class.find_each do |record|
113
+ model_id = record.read_attribute(model_id_column)
114
+ next if model_id.blank?
115
+
116
+ provider = has_provider ? record.read_attribute(:provider) : nil
117
+
118
+ model = if has_provider && provider.present?
119
+ model_class.find_by(model_id: model_id, provider: provider)
120
+ else
121
+ find_model_for_record(model_id, model_class)
122
+ end
123
+
124
+ record.update_column("#{foreign_key_name}_id", model.id) if model
125
+ end
126
+ end
127
+ end
128
+
129
+ def find_model_for_record(model_id, model_class)
130
+ begin
131
+ model_info, _provider = RubyLLM.models.resolve(model_id)
132
+ model_class.find_by(model_id: model_info.id, provider: model_info.provider)
133
+ rescue => e
134
+ model_class.find_by(model_id: model_id)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module RubyLLM
7
+ class UpgradeToV17Generator < Rails::Generators::Base # rubocop:disable Style/Documentation
8
+ include Rails::Generators::Migration
9
+
10
+ namespace 'ruby_llm:upgrade_to_v1_7'
11
+ source_root File.expand_path('upgrade_to_v1_7/templates', __dir__)
12
+
13
+ # Override source_paths to include install templates
14
+ def self.source_paths
15
+ [
16
+ File.expand_path('upgrade_to_v1_7/templates', __dir__),
17
+ File.expand_path('install/templates', __dir__)
18
+ ]
19
+ end
20
+
21
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
22
+
23
+ desc 'Upgrades existing RubyLLM apps to v1.7 with new Rails-like API\n' \
24
+ 'Usage: rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
25
+
26
+ def self.next_migration_number(dirname)
27
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
28
+ end
29
+
30
+ def parse_model_mappings
31
+ @model_names = {
32
+ chat: 'Chat',
33
+ message: 'Message',
34
+ tool_call: 'ToolCall',
35
+ model: 'Model'
36
+ }
37
+
38
+ model_mappings.each do |mapping|
39
+ if mapping.include?(':')
40
+ key, value = mapping.split(':', 2)
41
+ @model_names[key.to_sym] = value.classify
42
+ end
43
+ end
44
+
45
+ @model_names
46
+ end
47
+
48
+ %i[chat message tool_call model].each do |type|
49
+ define_method("#{type}_model_name") do
50
+ @model_names ||= parse_model_mappings
51
+ @model_names[type]
52
+ end
53
+ end
54
+
55
+ def create_migration_file
56
+ # First check if models table exists, if not create it
57
+ unless table_exists?(model_model_name.tableize)
58
+ migration_template 'create_models_migration.rb.tt',
59
+ "db/migrate/create_#{model_model_name.tableize}.rb",
60
+ migration_version: migration_version,
61
+ model_model_name: model_model_name
62
+
63
+ sleep 1 # Ensure different timestamp
64
+ end
65
+
66
+ migration_template 'migration.rb.tt',
67
+ 'db/migrate/migrate_to_ruby_llm_model_references.rb',
68
+ migration_version: migration_version,
69
+ chat_model_name: chat_model_name,
70
+ message_model_name: message_model_name,
71
+ tool_call_model_name: tool_call_model_name,
72
+ model_model_name: model_model_name
73
+ end
74
+
75
+ def create_model_file
76
+ # Check if Model file already exists
77
+ model_path = "app/models/#{model_model_name.underscore}.rb"
78
+
79
+ if File.exist?(Rails.root.join(model_path))
80
+ say_status :skip, model_path, :yellow
81
+ else
82
+ create_file model_path do
83
+ <<~RUBY
84
+ class #{model_model_name} < ApplicationRecord
85
+ #{acts_as_model_declaration}
86
+ end
87
+ RUBY
88
+ end
89
+ end
90
+ end
91
+
92
+ def acts_as_model_declaration
93
+ acts_as_model_params = []
94
+ chats_assoc = chat_model_name.tableize.to_sym
95
+
96
+ if chats_assoc != :chats
97
+ acts_as_model_params << "chats: :#{chats_assoc}"
98
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
99
+ end
100
+
101
+ if acts_as_model_params.any?
102
+ "acts_as_model #{acts_as_model_params.join(', ')}"
103
+ else
104
+ 'acts_as_model'
105
+ end
106
+ end
107
+
108
+ def update_initializer
109
+ initializer_content = File.read('config/initializers/ruby_llm.rb')
110
+
111
+ unless initializer_content.include?('config.use_new_acts_as')
112
+ inject_into_file 'config/initializers/ruby_llm.rb', before: /^end/ do
113
+ lines = ["\n # Enable the new Rails-like API", ' config.use_new_acts_as = true']
114
+ lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
115
+ lines << "\n"
116
+ lines.join("\n")
117
+ end
118
+ end
119
+ rescue Errno::ENOENT
120
+ say_status :error, 'config/initializers/ruby_llm.rb not found', :red
121
+ end
122
+
123
+ def show_next_steps
124
+ say_status :success, 'Migration created!', :green
125
+ say <<~INSTRUCTIONS
126
+
127
+ Next steps:
128
+ 1. Review the migration: db/migrate/*_migrate_to_ruby_llm_model_references.rb
129
+ 2. Run: rails db:migrate
130
+ 3. Update config/initializers/ruby_llm.rb as shown above
131
+ 4. Test your application thoroughly
132
+
133
+ The migration will:
134
+ - Create the Models table if it doesn't exist
135
+ - Load all models from models.json
136
+ - Migrate your existing data to use foreign keys
137
+ - Preserve all existing data (string columns renamed to model_id_string)
138
+
139
+ INSTRUCTIONS
140
+ end
141
+
142
+ private
143
+
144
+ def migration_version
145
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
146
+ end
147
+
148
+ def table_exists?(table_name)
149
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
150
+ rescue StandardError
151
+ false
152
+ end
153
+
154
+ def postgresql?
155
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
156
+ rescue StandardError
157
+ false
158
+ end
159
+ end
160
+ end