ruby_llm 1.6.4 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) 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 +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/generator_helpers.rb +129 -0
  20. data/lib/generators/ruby_llm/install/install_generator.rb +104 -0
  21. data/lib/generators/ruby_llm/install/templates/chat_model.rb.tt +2 -2
  22. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -4
  23. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +8 -7
  24. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +40 -0
  25. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +6 -5
  26. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +10 -4
  27. data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +4 -3
  28. data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +3 -0
  29. data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +2 -2
  30. data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +145 -0
  31. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +121 -0
  32. data/lib/ruby_llm/active_record/acts_as.rb +111 -327
  33. data/lib/ruby_llm/active_record/acts_as_legacy.rb +398 -0
  34. data/lib/ruby_llm/active_record/chat_methods.rb +336 -0
  35. data/lib/ruby_llm/active_record/message_methods.rb +72 -0
  36. data/lib/ruby_llm/active_record/model_methods.rb +84 -0
  37. data/lib/ruby_llm/aliases.json +54 -13
  38. data/lib/ruby_llm/attachment.rb +20 -0
  39. data/lib/ruby_llm/chat.rb +5 -5
  40. data/lib/ruby_llm/configuration.rb +9 -0
  41. data/lib/ruby_llm/connection.rb +4 -4
  42. data/lib/ruby_llm/model/info.rb +12 -0
  43. data/lib/ruby_llm/models.json +3579 -2029
  44. data/lib/ruby_llm/models.rb +51 -22
  45. data/lib/ruby_llm/provider.rb +3 -3
  46. data/lib/ruby_llm/providers/anthropic/chat.rb +2 -2
  47. data/lib/ruby_llm/providers/anthropic/media.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 +1 -1
  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 +39 -28
  74. data/lib/tasks/ruby_llm.rake +15 -0
  75. data/lib/tasks/vcr.rake +2 -2
  76. metadata +38 -3
  77. data/lib/generators/ruby_llm/install/templates/INSTALL_INFO.md.tt +0 -108
  78. data/lib/generators/ruby_llm/install_generator.rb +0 -121
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ # Generator for RubyLLM Rails models and migrations
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+ include RubyLLM::GeneratorHelpers
12
+
13
+ namespace 'ruby_llm:install'
14
+
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
18
+
19
+ class_option :skip_active_storage, type: :boolean, default: false,
20
+ desc: 'Skip ActiveStorage installation and attachment setup'
21
+
22
+ desc 'Creates models and migrations for RubyLLM Rails integration\n' \
23
+ 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
24
+
25
+ def self.next_migration_number(dirname)
26
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
27
+ end
28
+
29
+ def create_migration_files
30
+ # Create migrations with timestamps to ensure proper order
31
+ # First create chats table
32
+ migration_template 'create_chats_migration.rb.tt',
33
+ "db/migrate/create_#{chat_table_name}.rb"
34
+
35
+ # Then create messages table (must come before tool_calls due to foreign key)
36
+ sleep 1 # Ensure different timestamp
37
+ migration_template 'create_messages_migration.rb.tt',
38
+ "db/migrate/create_#{message_table_name}.rb"
39
+
40
+ # Then create tool_calls table (references messages)
41
+ sleep 1 # Ensure different timestamp
42
+ migration_template 'create_tool_calls_migration.rb.tt',
43
+ "db/migrate/create_#{tool_call_table_name}.rb"
44
+
45
+ # Create models table
46
+ sleep 1 # Ensure different timestamp
47
+ migration_template 'create_models_migration.rb.tt',
48
+ "db/migrate/create_#{model_table_name}.rb"
49
+ end
50
+
51
+ def create_model_files
52
+ create_namespace_modules
53
+
54
+ template 'chat_model.rb.tt', "app/models/#{chat_model_name.underscore}.rb"
55
+ template 'message_model.rb.tt', "app/models/#{message_model_name.underscore}.rb"
56
+ template 'tool_call_model.rb.tt', "app/models/#{tool_call_model_name.underscore}.rb"
57
+
58
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
59
+ end
60
+
61
+ def create_initializer
62
+ template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
63
+ end
64
+
65
+ def install_active_storage
66
+ return if options[:skip_active_storage]
67
+
68
+ say ' Installing ActiveStorage for file attachments...', :cyan
69
+ rails_command 'active_storage:install'
70
+ end
71
+
72
+ def show_install_info
73
+ say "\n ✅ RubyLLM installed!", :green
74
+
75
+ say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
76
+
77
+ say "\n Next steps:", :yellow
78
+ say ' 1. Run: rails db:migrate'
79
+ say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
80
+
81
+ say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
82
+
83
+ say "\n 🚀 Model registry is database-backed!", :cyan
84
+ say ' Models automatically load from the database'
85
+ say ' Pass model names as strings - RubyLLM handles the rest!'
86
+ say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
87
+
88
+ if options[:skip_active_storage]
89
+ say "\n 📎 Note: ActiveStorage was skipped", :yellow
90
+ say ' File attachments won\'t work without ActiveStorage.'
91
+ say ' To enable later:'
92
+ say ' 1. Run: rails active_storage:install && rails db:migrate'
93
+ say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
94
+ end
95
+
96
+ say "\n 📚 Documentation: https://rubyllm.com", :cyan
97
+
98
+ say "\n ❤️ Love RubyLLM?", :magenta
99
+ say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
100
+ say ' • 🐦 Follow for updates: https://x.com/paolino'
101
+ say "\n"
102
+ end
103
+ end
104
+ 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
- end
3
+ end
@@ -1,8 +1,8 @@
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.string :model_id
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
8
- end
8
+ end
@@ -1,15 +1,16 @@
1
- # Migration for creating messages table with references to chats and tool_calls
2
- class Create<%= options[:message_model_name].pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
3
2
  def change
4
- create_table :<%= options[:message_model_name].tableize %> do |t|
5
- t.references :<%= options[:chat_model_name].tableize.singularize %>, null: false, foreign_key: true
6
- t.string :role
3
+ create_table :<%= message_table_name %> do |t|
4
+ t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: true
5
+ t.string :role, null: false
7
6
  t.text :content
8
- t.string :model_id
7
+ t.references :<%= model_table_name.singularize %>, foreign_key: true
9
8
  t.integer :input_tokens
10
9
  t.integer :output_tokens
11
- t.references :<%= options[:tool_call_model_name].tableize.singularize %>
10
+ t.references :<%= tool_call_table_name.singularize %>, foreign_key: true
12
11
  t.timestamps
13
12
  end
13
+
14
+ add_index :<%= message_table_name %>, :role
14
15
  end
15
16
  end
@@ -0,0 +1,40 @@
1
+ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ create_table :<%= model_table_name %> do |t|
4
+ t.string :model_id, null: false
5
+ t.string :name, null: false
6
+ t.string :provider, null: false
7
+ t.string :family
8
+ t.datetime :model_created_at
9
+ t.integer :context_window
10
+ t.integer :max_output_tokens
11
+ t.date :knowledge_cutoff
12
+ <% if postgresql? %>
13
+ t.jsonb :modalities, default: {}
14
+ t.jsonb :capabilities, default: []
15
+ t.jsonb :pricing, default: {}
16
+ t.jsonb :metadata, default: {}
17
+ <% else %>
18
+ t.json :modalities, default: {}
19
+ t.json :capabilities, default: []
20
+ t.json :pricing, default: {}
21
+ t.json :metadata, default: {}
22
+ <% end %>
23
+ t.timestamps
24
+
25
+ t.index [:provider, :model_id], unique: true
26
+ t.index :provider
27
+ t.index :family
28
+ <% if postgresql? %>
29
+ t.index :capabilities, using: :gin
30
+ t.index :modalities, using: :gin
31
+ <% end %>
32
+ end
33
+
34
+ # Load models from JSON
35
+ say_with_time "Loading models from models.json" do
36
+ RubyLLM.models.load_from_json!
37
+ <%= model_model_name %>.save_to_database
38
+ end
39
+ end
40
+ end
@@ -1,14 +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
12
+ add_index :<%= tool_call_table_name %>, :tool_call_id, unique: true
13
+ add_index :<%= tool_call_table_name %>, :name
13
14
  end
14
- end
15
+ end
@@ -1,6 +1,12 @@
1
1
  RubyLLM.configure do |config|
2
- config.openai_api_key = ENV["OPENAI_API_KEY"]
3
- config.anthropic_api_key = ENV["ANTHROPIC_API_KEY"]
4
-
2
+ config.openai_api_key = Rails.application.credentials.dig(:openai_api_key)
5
3
  # config.default_model = "gpt-4.1-nano"
6
- end
4
+
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 %>"
11
+ <% end -%>
12
+ end
@@ -1,3 +1,4 @@
1
- class <%= options[:message_model_name] %> < ApplicationRecord
2
- <%= acts_as_message_declaration %>
3
- end
1
+ class <%= message_model_name %> < ApplicationRecord
2
+ <%= acts_as_message_declaration %><% unless options[:skip_active_storage] %>
3
+ has_many_attached :attachments<% end %>
4
+ end
@@ -0,0 +1,3 @@
1
+ class <%= model_model_name %> < ApplicationRecord
2
+ <%= acts_as_model_declaration %>
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
- end
3
+ end
@@ -0,0 +1,145 @@
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
+ <% if @model_table_already_existed %>
7
+ # Load models from models.json if Model table already existed
8
+ say_with_time "Loading models from models.json" do
9
+ RubyLLM.models.load_from_json!
10
+ model_class.save_to_database
11
+ "Loaded #{model_class.count} models"
12
+ end
13
+ <% end %>
14
+
15
+ # Then check for any models in existing data that aren't in models.json
16
+ say_with_time "Checking for additional models in existing data" do
17
+ collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
18
+ collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
19
+ model_class.count
20
+ end
21
+
22
+ # Migrate foreign keys
23
+ migrate_foreign_key(:<%= chat_table_name %>, chat_class, model_class, :<%= model_table_name.singularize %>)
24
+ migrate_foreign_key(:<%= message_table_name %>, message_class, model_class, :<%= model_table_name.singularize %>)
25
+ end
26
+
27
+ def down
28
+ # Remove foreign key references
29
+ if column_exists?(:<%= message_table_name %>, :<%= model_table_name.singularize %>_id)
30
+ remove_reference :<%= message_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
31
+ end
32
+
33
+ if column_exists?(:<%= chat_table_name %>, :<%= model_table_name.singularize %>_id)
34
+ remove_reference :<%= chat_table_name %>, :<%= model_table_name.singularize %>, foreign_key: true
35
+ end
36
+
37
+ # Restore original model_id string columns
38
+ if column_exists?(:<%= message_table_name %>, :model_id_string)
39
+ rename_column :<%= message_table_name %>, :model_id_string, :model_id
40
+ end
41
+
42
+ if column_exists?(:<%= chat_table_name %>, :model_id_string)
43
+ rename_column :<%= chat_table_name %>, :model_id_string, :model_id
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def collect_and_create_models(record_class, table_name, model_class)
50
+ return unless column_exists?(table_name, :model_id)
51
+
52
+ has_provider = column_exists?(table_name, :provider)
53
+
54
+ # Collect unique model/provider combinations using read_attribute to bypass overrides
55
+ models_set = Set.new
56
+
57
+ record_class.find_each do |record|
58
+ model_id = record.read_attribute(:model_id)
59
+ next if model_id.blank?
60
+
61
+ provider = has_provider ? record.read_attribute(:provider) : nil
62
+ models_set.add([ model_id, provider ])
63
+ end
64
+
65
+ models_set.each do |model_id, provider|
66
+ find_or_create_model(model_id, provider, model_class)
67
+ end
68
+ end
69
+
70
+ def find_or_create_model(model_id, provider, model_class)
71
+ return if model_id.blank?
72
+
73
+ begin
74
+ model_info, _provider = RubyLLM.models.resolve(model_id, provider: provider)
75
+
76
+ model_class.find_or_create_by!(
77
+ model_id: model_info.id,
78
+ provider: model_info.provider
79
+ ) do |m|
80
+ m.name = model_info.name || model_info.id
81
+ m.family = model_info.family
82
+ m.model_created_at = model_info.created_at
83
+ m.context_window = model_info.context_window
84
+ m.max_output_tokens = model_info.max_output_tokens
85
+ m.knowledge_cutoff = model_info.knowledge_cutoff
86
+ m.modalities = model_info.modalities.to_h
87
+ m.capabilities = model_info.capabilities
88
+ m.pricing = model_info.pricing.to_h
89
+ m.metadata = model_info.metadata
90
+ end
91
+ rescue => e
92
+ # Skip models that can't be resolved - they'll need manual fixing
93
+ Rails.logger.warn "Skipping unresolvable model: #{model_id} - will need manual update"
94
+ nil
95
+ end
96
+ end
97
+
98
+
99
+ def migrate_foreign_key(table_name, record_class, model_class, foreign_key_name)
100
+ return unless column_exists?(table_name, :model_id)
101
+
102
+ # Check if we need to rename the string column to avoid collision
103
+ if column_exists?(table_name, :model_id) && !foreign_key_exists?(table_name, :models)
104
+ # Temporarily rename the string column
105
+ rename_column table_name, :model_id, :model_id_string
106
+ end
107
+
108
+ # Add the foreign key reference
109
+ unless column_exists?(table_name, "#{foreign_key_name}_id")
110
+ add_reference table_name, foreign_key_name, foreign_key: true
111
+ end
112
+
113
+ say_with_time "Migrating #{table_name} model references" do
114
+ record_class.reset_column_information
115
+ has_provider = column_exists?(table_name, :provider)
116
+
117
+ # Determine which column to read from (renamed or original)
118
+ model_id_column = column_exists?(table_name, :model_id_string) ? :model_id_string : :model_id
119
+
120
+ record_class.find_each do |record|
121
+ model_id = record.read_attribute(model_id_column)
122
+ next if model_id.blank?
123
+
124
+ provider = has_provider ? record.read_attribute(:provider) : nil
125
+
126
+ model = if has_provider && provider.present?
127
+ model_class.find_by(model_id: model_id, provider: provider)
128
+ else
129
+ find_model_for_record(model_id, model_class)
130
+ end
131
+
132
+ record.update_column("#{foreign_key_name}_id", model.id) if model
133
+ end
134
+ end
135
+ end
136
+
137
+ def find_model_for_record(model_id, model_class)
138
+ begin
139
+ model_info, _provider = RubyLLM.models.resolve(model_id)
140
+ model_class.find_by(model_id: model_info.id, provider: model_info.provider)
141
+ rescue => e
142
+ model_class.find_by(model_id: model_id)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+ require_relative '../generator_helpers'
6
+
7
+ module RubyLLM
8
+ class UpgradeToV17Generator < Rails::Generators::Base # rubocop:disable Style/Documentation
9
+ include Rails::Generators::Migration
10
+ include RubyLLM::GeneratorHelpers
11
+
12
+ namespace 'ruby_llm:upgrade_to_v1_7'
13
+ source_root File.expand_path('templates', __dir__)
14
+
15
+ # Override source_paths to include install templates
16
+ def self.source_paths
17
+ [
18
+ File.expand_path('templates', __dir__),
19
+ File.expand_path('../install/templates', __dir__)
20
+ ]
21
+ end
22
+
23
+ argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
24
+
25
+ desc 'Upgrades existing RubyLLM apps to v1.7 with new Rails-like API\n' \
26
+ 'Usage: rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
27
+
28
+ def self.next_migration_number(dirname)
29
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
30
+ end
31
+
32
+ def create_migration_file
33
+ @model_table_already_existed = table_exists?(table_name_for(model_model_name))
34
+
35
+ # First check if models table exists, if not create it
36
+ unless @model_table_already_existed
37
+ migration_template 'create_models_migration.rb.tt',
38
+ "db/migrate/create_#{table_name_for(model_model_name)}.rb",
39
+ migration_version: migration_version,
40
+ model_model_name: model_model_name
41
+
42
+ sleep 1 # Ensure different timestamp
43
+ end
44
+
45
+ migration_template 'migration.rb.tt',
46
+ 'db/migrate/migrate_to_ruby_llm_model_references.rb',
47
+ migration_version: migration_version,
48
+ chat_model_name: chat_model_name,
49
+ message_model_name: message_model_name,
50
+ tool_call_model_name: tool_call_model_name,
51
+ model_model_name: model_model_name,
52
+ model_table_already_existed: @model_table_already_existed
53
+ end
54
+
55
+ def create_model_file
56
+ create_namespace_modules
57
+
58
+ template 'model_model.rb.tt', "app/models/#{model_model_name.underscore}.rb"
59
+ end
60
+
61
+ def update_existing_models
62
+ update_model_acts_as(chat_model_name, 'acts_as_chat', acts_as_chat_declaration)
63
+ update_model_acts_as(message_model_name, 'acts_as_message', acts_as_message_declaration)
64
+ update_model_acts_as(tool_call_model_name, 'acts_as_tool_call', acts_as_tool_call_declaration)
65
+ end
66
+
67
+ def update_initializer
68
+ initializer_path = 'config/initializers/ruby_llm.rb'
69
+
70
+ unless File.exist?(initializer_path)
71
+ say_status :warning, 'No initializer found. Creating one...', :yellow
72
+ template 'initializer.rb.tt', initializer_path
73
+ return
74
+ end
75
+
76
+ initializer_content = File.read(initializer_path)
77
+
78
+ return if initializer_content.include?('config.use_new_acts_as')
79
+
80
+ inject_into_file initializer_path, before: /^end/ do
81
+ lines = ["\n # Enable the new Rails-like API", ' config.use_new_acts_as = true']
82
+ lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
83
+ lines << "\n"
84
+ lines.join("\n")
85
+ end
86
+ end
87
+
88
+ def show_next_steps
89
+ say_status :success, 'Upgrade prepared!', :green
90
+ say <<~INSTRUCTIONS
91
+
92
+ Next steps:
93
+ 1. Review the generated migrations
94
+ 2. Run: rails db:migrate
95
+ 3. Update your code to use the new API: #{chat_model_name}.create! now has the same signature as RubyLLM.chat
96
+
97
+ ⚠️ If you get "undefined method 'acts_as_model'" during migration:
98
+ Add this to config/application.rb BEFORE your Application class:
99
+
100
+ RubyLLM.configure do |config|
101
+ config.use_new_acts_as = true
102
+ end
103
+
104
+ 📚 See the full migration guide: https://rubyllm.com/upgrading-to-1-7/
105
+
106
+ INSTRUCTIONS
107
+ end
108
+
109
+ private
110
+
111
+ def update_model_acts_as(model_name, old_acts_as, new_acts_as)
112
+ model_path = "app/models/#{model_name.underscore}.rb"
113
+ return unless File.exist?(Rails.root.join(model_path))
114
+
115
+ content = File.read(Rails.root.join(model_path))
116
+ return unless content.match?(/^\s*#{old_acts_as}/)
117
+
118
+ gsub_file model_path, /^\s*#{old_acts_as}.*$/, " #{new_acts_as}"
119
+ end
120
+ end
121
+ end