dify_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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  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 +1 -1
  21. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +3 -3
  22. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +6 -6
  23. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -3
  24. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +5 -5
  25. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +6 -3
  26. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -1
  27. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +1 -1
  28. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +1 -1
  29. data/lib/generators/ruby_llm/{migrate_model_fields → upgrade_to_v1_7}/templates/migration.rb.tt +16 -21
  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 +88 -58
  32. data/lib/ruby_llm/active_record/chat_methods.rb +51 -30
  33. data/lib/ruby_llm/active_record/message_methods.rb +2 -2
  34. data/lib/ruby_llm/aliases.json +0 -8
  35. data/lib/ruby_llm/configuration.rb +5 -0
  36. data/lib/ruby_llm/models.json +1724 -1497
  37. data/lib/ruby_llm/railtie.rb +4 -12
  38. data/lib/ruby_llm/version.rb +1 -1
  39. data/lib/ruby_llm.rb +2 -1
  40. metadata +23 -6
  41. data/lib/generators/ruby_llm/install/templates/create_chats_legacy_migration.rb.tt +0 -8
  42. data/lib/generators/ruby_llm/install/templates/create_messages_legacy_migration.rb.tt +0 -16
  43. data/lib/generators/ruby_llm/install_generator.rb +0 -184
  44. data/lib/generators/ruby_llm/migrate_model_fields_generator.rb +0 -84
@@ -0,0 +1,227 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module RubyLLM
7
+ # Generator for RubyLLM Rails models and migrations
8
+ class InstallGenerator < Rails::Generators::Base
9
+ include Rails::Generators::Migration
10
+
11
+ namespace 'ruby_llm:install'
12
+
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
16
+
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] ...'
22
+
23
+ def self.next_migration_number(dirname)
24
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
25
+ end
26
+
27
+ def migration_version
28
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
29
+ end
30
+
31
+ def postgresql?
32
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
33
+ rescue StandardError
34
+ false
35
+ end
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
+
61
+ define_method("#{type}_table_name") do
62
+ table_name_for(send("#{type}_model_name"))
63
+ end
64
+ end
65
+
66
+ def acts_as_chat_declaration
67
+ acts_as_chat_params = []
68
+ messages_assoc = message_model_name.tableize.to_sym
69
+ model_assoc = model_model_name.underscore.to_sym
70
+
71
+ if messages_assoc != :messages
72
+ acts_as_chat_params << "messages: :#{messages_assoc}"
73
+ if message_model_name != messages_assoc.to_s.classify
74
+ acts_as_chat_params << "message_class: '#{message_model_name}'"
75
+ end
76
+ end
77
+
78
+ if model_assoc != :model
79
+ acts_as_chat_params << "model: :#{model_assoc}"
80
+ acts_as_chat_params << "model_class: '#{model_model_name}'" if model_model_name != model_assoc.to_s.classify
81
+ end
82
+
83
+ if acts_as_chat_params.any?
84
+ "acts_as_chat #{acts_as_chat_params.join(', ')}"
85
+ else
86
+ 'acts_as_chat'
87
+ end
88
+ end
89
+
90
+ def acts_as_message_declaration
91
+ params = []
92
+
93
+ add_message_association_params(params, :chat, chat_model_name)
94
+ add_message_association_params(params, :tool_calls, tool_call_model_name, tableize: true)
95
+ add_message_association_params(params, :model, model_model_name)
96
+
97
+ params.any? ? "acts_as_message #{params.join(', ')}" : 'acts_as_message'
98
+ end
99
+
100
+ private
101
+
102
+ def add_message_association_params(params, default_assoc, model_name, tableize: false)
103
+ assoc = tableize ? model_name.tableize.to_sym : model_name.underscore.to_sym
104
+
105
+ return if assoc == default_assoc
106
+
107
+ params << "#{default_assoc}: :#{assoc}"
108
+ expected_class = assoc.to_s.classify
109
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != expected_class
110
+ end
111
+
112
+ public
113
+
114
+ def acts_as_tool_call_declaration
115
+ acts_as_tool_call_params = []
116
+ message_assoc = message_model_name.underscore.to_sym
117
+
118
+ if message_assoc != :message
119
+ acts_as_tool_call_params << "message: :#{message_assoc}"
120
+ if message_model_name != message_assoc.to_s.classify
121
+ acts_as_tool_call_params << "message_class: '#{message_model_name}'"
122
+ end
123
+ end
124
+
125
+ if acts_as_tool_call_params.any?
126
+ "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
127
+ else
128
+ 'acts_as_tool_call'
129
+ end
130
+ end
131
+
132
+ def acts_as_model_declaration
133
+ acts_as_model_params = []
134
+ chats_assoc = chat_model_name.tableize.to_sym
135
+
136
+ if chats_assoc != :chats
137
+ acts_as_model_params << "chats: :#{chats_assoc}"
138
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
139
+ end
140
+
141
+ if acts_as_model_params.any?
142
+ "acts_as_model #{acts_as_model_params.join(', ')}"
143
+ else
144
+ 'acts_as_model'
145
+ end
146
+ end
147
+
148
+ def create_migration_files
149
+ # Create migrations with timestamps to ensure proper order
150
+ # First create chats table
151
+ migration_template 'create_chats_migration.rb.tt',
152
+ "db/migrate/create_#{chat_table_name}.rb"
153
+
154
+ # Then create messages table (must come before tool_calls due to foreign key)
155
+ sleep 1 # Ensure different timestamp
156
+ migration_template 'create_messages_migration.rb.tt',
157
+ "db/migrate/create_#{message_table_name}.rb"
158
+
159
+ # Then create tool_calls table (references messages)
160
+ sleep 1 # Ensure different timestamp
161
+ migration_template 'create_tool_calls_migration.rb.tt',
162
+ "db/migrate/create_#{tool_call_table_name}.rb"
163
+
164
+ # Create models table
165
+ sleep 1 # Ensure different timestamp
166
+ migration_template 'create_models_migration.rb.tt',
167
+ "db/migrate/create_#{model_table_name}.rb"
168
+ end
169
+
170
+ def create_model_files
171
+ template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
172
+ template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
173
+ template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
174
+
175
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
176
+ end
177
+
178
+ def create_initializer
179
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
180
+ end
181
+
182
+ def install_active_storage
183
+ return if options[:skip_active_storage]
184
+
185
+ say ' Installing ActiveStorage for file attachments...', :cyan
186
+ rails_command 'active_storage:install'
187
+ end
188
+
189
+ def table_name_for(model_name)
190
+ # Convert namespaced model names to proper table names
191
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
192
+ model_name.underscore.pluralize.tr('/', '_')
193
+ end
194
+
195
+ def show_install_info
196
+ say "\n ✅ RubyLLM installed!", :green
197
+
198
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
199
+
200
+ say "\n Next steps:", :yellow
201
+ say ' 1. Run: rails db:migrate'
202
+ say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
203
+
204
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
205
+
206
+ say "\n 🚀 Model registry is database-backed!", :cyan
207
+ say ' Models automatically load from the database'
208
+ say ' Pass model names as strings - RubyLLM handles the rest!'
209
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
210
+
211
+ if options[:skip_active_storage]
212
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
213
+ say ' File attachments won\'t work without ActiveStorage.'
214
+ say ' To enable later:'
215
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
216
+ say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
217
+ end
218
+
219
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
220
+
221
+ say "\n ❤️ Love RubyLLM?", :magenta
222
+ say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
223
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
224
+ say "\n"
225
+ end
226
+ end
227
+ end
@@ -1,3 +1,3 @@
1
- class <%= options[:chat_model_name] %> < ApplicationRecord
1
+ class <%= chat_model_name %> < ApplicationRecord
2
2
  <%= acts_as_chat_declaration %>
3
3
  end
@@ -1,7 +1,7 @@
1
- class Create<%= options[:chat_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= chat_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= options[:chat_model_name].tableize %> do |t|
4
- t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
3
+ create_table :<%= chat_table_name %> do |t|
4
+ t.references :<%= model_table_name.singularize %>, foreign_key: true
5
5
  t.timestamps
6
6
  end
7
7
  end
@@ -1,16 +1,16 @@
1
- class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= options[:message_model_name].tableize %> do |t|
4
- t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
3
+ create_table :<%= message_table_name %> do |t|
4
+ t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: true
5
5
  t.string :role, null: false
6
6
  t.text :content
7
- t.references :<%= options[:model_model_name].tableize.singularize %>, foreign_key: true
7
+ t.references :<%= model_table_name.singularize %>, foreign_key: true
8
8
  t.integer :input_tokens
9
9
  t.integer :output_tokens
10
- t.references :<%= options[:tool_call_model_name].tableize.singularize %>, foreign_key: true
10
+ t.references :<%= tool_call_table_name.singularize %>, foreign_key: true
11
11
  t.timestamps
12
12
  end
13
13
 
14
- add_index :<%= options[:message_model_name].tableize %>, :role
14
+ add_index :<%= message_table_name %>, :role
15
15
  end
16
16
  end
@@ -1,6 +1,6 @@
1
- class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= options[:model_model_name].tableize %> do |t|
3
+ create_table :<%= model_table_name %> do |t|
4
4
  t.string :model_id, null: false
5
5
  t.string :name, null: false
6
6
  t.string :provider, null: false
@@ -34,7 +34,7 @@ class Create<%= options[:model_model_name].pluralize %> < ActiveRecord::Migratio
34
34
  # Load models from JSON
35
35
  say_with_time "Loading models from models.json" do
36
36
  RubyLLM.models.load_from_json!
37
- model_class = '<%= options[:model_model_name] %>'.constantize
37
+ model_class = '<%= model_model_name %>'.constantize
38
38
  model_class.save_to_database
39
39
 
40
40
  "Loaded #{model_class.count} models"
@@ -1,15 +1,15 @@
1
1
  <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
- class Create<%= options[:tool_call_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
- create_table :<%= options[:tool_call_model_name].tableize %> do |t|
5
- t.references :<%= options[:message_model_name].tableize.singularize %>, null: false, foreign_key: true
4
+ create_table :<%= tool_call_table_name %> do |t|
5
+ t.references :<%= message_table_name.singularize %>, null: false, foreign_key: true
6
6
  t.string :tool_call_id, null: false
7
7
  t.string :name, null: false
8
8
  t.<%= postgresql? ? 'jsonb' : 'json' %> :arguments, default: {}
9
9
  t.timestamps
10
10
  end
11
11
 
12
- add_index :<%= options[:tool_call_model_name].tableize %>, :tool_call_id, unique: true
13
- add_index :<%= options[:tool_call_model_name].tableize %>, :name
12
+ add_index :<%= tool_call_table_name %>, :tool_call_id, unique: true
13
+ add_index :<%= tool_call_table_name %>, :name
14
14
  end
15
15
  end
@@ -2,8 +2,11 @@ RubyLLM.configure do |config|
2
2
  config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
3
3
  # config.default_model = "gpt-4.1-nano"
4
4
 
5
- <% unless skip_model_registry? -%>
6
- # DB-backed model registry for rich model metadata
7
- config.model_registry_class = "<%= options[:model_model_name] %>"
5
+ # Use the new association-based acts_as API (recommended)
6
+ config.use_new_acts_as = true
7
+ <% if model_model_name != 'Model' -%>
8
+
9
+ # Custom model registry class name
10
+ config.model_registry_class = "<%= model_model_name %>"
8
11
  <% end -%>
9
12
  end
@@ -1,4 +1,4 @@
1
- class <%= options[:message_model_name] %> < ApplicationRecord
1
+ class <%= message_model_name %> < ApplicationRecord
2
2
  <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
3
  has_many_attached :attachments<% end %>
4
4
  end
@@ -1,3 +1,3 @@
1
- class <%= options[:model_model_name] %> < ApplicationRecord
1
+ class <%= model_model_name %> < ApplicationRecord
2
2
  <%= acts_as_model_declaration %>
3
3
  end
@@ -1,3 +1,3 @@
1
- class <%= options[:tool_call_model_name] %> < ApplicationRecord
1
+ class <%= tool_call_model_name %> < ApplicationRecord
2
2
  <%= acts_as_tool_call_declaration %>
3
3
  end
@@ -1,43 +1,38 @@
1
1
  class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_version %>
2
2
  def up
3
- # Ensure Models table exists
4
- unless table_exists?(:<%= options[:model_model_name].tableize %>)
5
- raise "Models table doesn't exist. Please run 'rails generate ruby_llm:install' with model registry first."
6
- end
7
-
8
- model_class = <%= options[:model_model_name] %>
9
- chat_class = <%= options[:chat_model_name] %>
10
- message_class = <%= options[:message_model_name] %>
3
+ model_class = <%= model_model_name %>
4
+ chat_class = <%= chat_model_name %>
5
+ message_class = <%= message_model_name %>
11
6
 
12
7
  # Then check for any models in existing data that aren't in models.json
13
8
  say_with_time "Checking for additional models in existing data" do
14
- collect_and_create_models(chat_class, :<%= options[:chat_model_name].tableize %>, model_class)
15
- collect_and_create_models(message_class, :<%= options[:message_model_name].tableize %>, model_class)
9
+ collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
10
+ collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
16
11
  model_class.count
17
12
  end
18
13
 
19
14
  # Migrate foreign keys
20
- migrate_foreign_key(:<%= options[:chat_model_name].tableize %>, chat_class, model_class, :<%= options[:model_model_name].underscore %>)
21
- migrate_foreign_key(:<%= options[:message_model_name].tableize %>, message_class, model_class, :<%= options[:model_model_name].underscore %>)
15
+ migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
16
+ migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
22
17
  end
23
18
 
24
19
  def down
25
20
  # Remove foreign key references
26
- if column_exists?(:<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
27
- remove_reference :<%= options[:message_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
21
+ if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
22
+ remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
28
23
  end
29
24
 
30
- if column_exists?(:<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>_id)
31
- remove_reference :<%= options[:chat_model_name].tableize %>, :<%= options[:model_model_name].underscore %>, foreign_key: true
25
+ if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
26
+ remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
32
27
  end
33
28
 
34
29
  # Restore original model_id string columns
35
- if column_exists?(:<%= options[:message_model_name].tableize %>, :model_id_string)
36
- rename_column :<%= options[:message_model_name].tableize %>, :model_id_string, :model_id
30
+ if column_exists?(:<%= message_table_name %>, :model_id_string)
31
+ rename_column :<%= message_table_name %>, :model_id_string, :model_id
37
32
  end
38
33
 
39
- if column_exists?(:<%= options[:chat_model_name].tableize %>, :model_id_string)
40
- rename_column :<%= options[:chat_model_name].tableize %>, :model_id_string, :model_id
34
+ if column_exists?(:<%= chat_table_name %>, :model_id_string)
35
+ rename_column :<%= chat_table_name %>, :model_id_string, :model_id
41
36
  end
42
37
  end
43
38
 
@@ -139,4 +134,4 @@ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_ver
139
134
  model_class.find_by(model_id: model_id)
140
135
  end
141
136
  end
142
- end
137
+ end
@@ -0,0 +1,170 @@
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('templates', __dir__)
12
+
13
+ # Override source_paths to include install templates
14
+ def self.source_paths
15
+ [
16
+ File.expand_path('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
+
54
+ define_method("#{type}_table_name") do
55
+ table_name_for(send("#{type}_model_name"))
56
+ end
57
+ end
58
+
59
+ def create_migration_file
60
+ # First check if models table exists, if not create it
61
+ unless table_exists?(table_name_for(model_model_name))
62
+ migration_template 'create_models_migration.rb.tt',
63
+ "db/migrate/create_#{table_name_for(model_model_name)}.rb",
64
+ migration_version: migration_version,
65
+ model_model_name: model_model_name
66
+
67
+ sleep 1 # Ensure different timestamp
68
+ end
69
+
70
+ migration_template 'migration.rb.tt',
71
+ 'db/migrate/migrate_to_ruby_llm_model_references.rb',
72
+ migration_version: migration_version,
73
+ chat_model_name: chat_model_name,
74
+ message_model_name: message_model_name,
75
+ tool_call_model_name: tool_call_model_name,
76
+ model_model_name: model_model_name
77
+ end
78
+
79
+ def create_model_file
80
+ # Check if Model file already exists
81
+ model_path = "app/models/#{model_model_name.underscore}.rb"
82
+
83
+ if File.exist?(Rails.root.join(model_path))
84
+ say_status :skip, model_path, :yellow
85
+ else
86
+ create_file model_path do
87
+ <<~RUBY
88
+ class #{model_model_name} < ApplicationRecord
89
+ #{acts_as_model_declaration}
90
+ end
91
+ RUBY
92
+ end
93
+ end
94
+ end
95
+
96
+ def acts_as_model_declaration
97
+ acts_as_model_params = []
98
+ chats_assoc = chat_model_name.tableize.to_sym
99
+
100
+ if chats_assoc != :chats
101
+ acts_as_model_params << "chats: :#{chats_assoc}"
102
+ acts_as_model_params << "chat_class: '#{chat_model_name}'" if chat_model_name != chats_assoc.to_s.classify
103
+ end
104
+
105
+ if acts_as_model_params.any?
106
+ "acts_as_model #{acts_as_model_params.join(', ')}"
107
+ else
108
+ 'acts_as_model'
109
+ end
110
+ end
111
+
112
+ def update_initializer
113
+ initializer_content = File.read('config/initializers/ruby_llm.rb')
114
+
115
+ unless initializer_content.include?('config.use_new_acts_as')
116
+ inject_into_file 'config/initializers/ruby_llm.rb', before: /^end/ do
117
+ lines = ["\n # Enable the new Rails-like API", ' config.use_new_acts_as = true']
118
+ lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
119
+ lines << "\n"
120
+ lines.join("\n")
121
+ end
122
+ end
123
+ rescue Errno::ENOENT
124
+ say_status :error, 'config/initializers/ruby_llm.rb not found', :red
125
+ end
126
+
127
+ def show_next_steps
128
+ say_status :success, 'Migration created!', :green
129
+ say <<~INSTRUCTIONS
130
+
131
+ Next steps:
132
+ 1. Review the migration: db/migrate/*_migrate_to_ruby_llm_model_references.rb
133
+ 2. Run: rails db:migrate
134
+ 3. Update config/initializers/ruby_llm.rb as shown above
135
+ 4. Test your application thoroughly
136
+
137
+ The migration will:
138
+ - Create the Models table if it doesn't exist
139
+ - Load all models from models.json
140
+ - Migrate your existing data to use foreign keys
141
+ - Preserve all existing data (string columns renamed to model_id_string)
142
+
143
+ INSTRUCTIONS
144
+ end
145
+
146
+ private
147
+
148
+ def migration_version
149
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
150
+ end
151
+
152
+ def table_name_for(model_name)
153
+ # Convert namespaced model names to proper table names
154
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
155
+ model_name.underscore.pluralize.tr('/', '_')
156
+ end
157
+
158
+ def table_exists?(table_name)
159
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
160
+ rescue StandardError
161
+ false
162
+ end
163
+
164
+ def postgresql?
165
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
166
+ rescue StandardError
167
+ false
168
+ end
169
+ end
170
+ end