ruby_llm 1.7.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5a7c8f498c7fee7618711944119c65a33f8a2236a87cbd55d51b5e5a5d25000f
4
- data.tar.gz: 2bb4675e6bcad95099330783697c87c7d6eade98c159a6b00247c7e179f7bd9a
3
+ metadata.gz: ceecb8e7cd289f58ac1f76648306f61b54482a9b6f845a2f08b3303ee746a634
4
+ data.tar.gz: 3c075c03ccd1d7b841b99b58a1a4cda9289370b8dea039070cd78ede2d18a41c
5
5
  SHA512:
6
- metadata.gz: 06e41bd54bcf1c7c094750b67c494decf50e9db53a250d4732455742d688aa08c72669ba64f4cc771fa3a1ad5197f646ce58308be1f3fd7db84b21282180135b
7
- data.tar.gz: '08c739bbdb44380720e817704d9f980b49b6a7bafd80d890a3207531afe746d9039e2e3e0d6adf82e496b8ce16d68fa0ee8359ccf63cecf08de778e07fe6b632'
6
+ metadata.gz: 16ac70cc9787c0d845b5ad4c62171cb225752f160506864b07fdb0b2c1b27adc147e9799a1f13e9d7a5169595d247e5b6bc0c1083dc523647aa13e750b1d6f9d
7
+ data.tar.gz: 80543faec7119044c345f6751059e05e4cf76457ad3b288da38ea7c4e1338570fcbca1c8ad8e9dad84fb7ac5983998fd1ceba481b2e244d3542f59481a4d0e7f
data/README.md CHANGED
@@ -9,7 +9,7 @@
9
9
 
10
10
  Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="https://chatwithwork.com/logotype-dark.svg"><img src="https://chatwithwork.com/logotype.svg" alt="Chat with Work" height="30" align="absmiddle"></picture>](https://chatwithwork.com) — *Claude Code for your documents*
11
11
 
12
- [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=7)](https://badge.fury.io/rb/ruby_llm)
12
+ [![Gem Version](https://badge.fury.io/rb/ruby_llm.svg?a=8)](https://badge.fury.io/rb/ruby_llm)
13
13
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
14
14
  [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm)](https://rubygems.org/gems/ruby_llm)
15
15
  [![codecov](https://codecov.io/gh/crmne/ruby_llm/branch/main/graph/badge.svg?a=2)](https://codecov.io/gh/crmne/ruby_llm)
@@ -38,35 +38,39 @@ module RubyLLM
38
38
  @model_names ||= parse_model_mappings
39
39
  @model_names[type]
40
40
  end
41
+
42
+ define_method("#{type}_table_name") do
43
+ table_name_for(send("#{type}_model_name"))
44
+ end
41
45
  end
42
46
 
43
47
  def create_views
44
48
  # Chat views
45
- template 'views/chats/index.html.erb', "app/views/#{chat_model_name.tableize}/index.html.erb"
46
- template 'views/chats/new.html.erb', "app/views/#{chat_model_name.tableize}/new.html.erb"
47
- template 'views/chats/show.html.erb', "app/views/#{chat_model_name.tableize}/show.html.erb"
49
+ template 'views/chats/index.html.erb', "app/views/#{chat_table_name}/index.html.erb"
50
+ template 'views/chats/new.html.erb', "app/views/#{chat_table_name}/new.html.erb"
51
+ template 'views/chats/show.html.erb', "app/views/#{chat_table_name}/show.html.erb"
48
52
  template 'views/chats/_chat.html.erb',
49
- "app/views/#{chat_model_name.tableize}/_#{chat_model_name.underscore}.html.erb"
50
- template 'views/chats/_form.html.erb', "app/views/#{chat_model_name.tableize}/_form.html.erb"
53
+ "app/views/#{chat_table_name}/_#{chat_model_name.underscore}.html.erb"
54
+ template 'views/chats/_form.html.erb', "app/views/#{chat_table_name}/_form.html.erb"
51
55
 
52
56
  # Message views
53
57
  template 'views/messages/_message.html.erb',
54
- "app/views/#{message_model_name.tableize}/_#{message_model_name.underscore}.html.erb"
55
- template 'views/messages/_form.html.erb', "app/views/#{message_model_name.tableize}/_form.html.erb"
58
+ "app/views/#{message_table_name}/_#{message_model_name.underscore}.html.erb"
59
+ template 'views/messages/_form.html.erb', "app/views/#{message_table_name}/_form.html.erb"
56
60
  template 'views/messages/create.turbo_stream.erb',
57
- "app/views/#{message_model_name.tableize}/create.turbo_stream.erb"
61
+ "app/views/#{message_table_name}/create.turbo_stream.erb"
58
62
 
59
63
  # Model views
60
- template 'views/models/index.html.erb', "app/views/#{model_model_name.tableize}/index.html.erb"
61
- template 'views/models/show.html.erb', "app/views/#{model_model_name.tableize}/show.html.erb"
64
+ template 'views/models/index.html.erb', "app/views/#{model_table_name}/index.html.erb"
65
+ template 'views/models/show.html.erb', "app/views/#{model_table_name}/show.html.erb"
62
66
  template 'views/models/_model.html.erb',
63
- "app/views/#{model_model_name.tableize}/_#{model_model_name.underscore}.html.erb"
67
+ "app/views/#{model_table_name}/_#{model_model_name.underscore}.html.erb"
64
68
  end
65
69
 
66
70
  def create_controllers
67
- template 'controllers/chats_controller.rb', "app/controllers/#{chat_model_name.tableize}_controller.rb"
68
- template 'controllers/messages_controller.rb', "app/controllers/#{message_model_name.tableize}_controller.rb"
69
- template 'controllers/models_controller.rb', "app/controllers/#{model_model_name.tableize}_controller.rb"
71
+ template 'controllers/chats_controller.rb', "app/controllers/#{chat_table_name}_controller.rb"
72
+ template 'controllers/messages_controller.rb', "app/controllers/#{message_table_name}_controller.rb"
73
+ template 'controllers/models_controller.rb', "app/controllers/#{model_table_name}_controller.rb"
70
74
  end
71
75
 
72
76
  def create_jobs
@@ -75,7 +79,7 @@ module RubyLLM
75
79
 
76
80
  def add_routes
77
81
  model_routes = <<~ROUTES.strip
78
- resources :#{model_model_name.tableize}, only: [:index, :show] do
82
+ resources :#{model_table_name}, only: [:index, :show] do
79
83
  collection do
80
84
  post :refresh
81
85
  end
@@ -83,8 +87,8 @@ module RubyLLM
83
87
  ROUTES
84
88
  route model_routes
85
89
  chat_routes = <<~ROUTES.strip
86
- resources :#{chat_model_name.tableize} do
87
- resources :#{message_model_name.tableize}, only: [:create]
90
+ resources :#{chat_table_name} do
91
+ resources :#{message_table_name}, only: [:create]
88
92
  end
89
93
  ROUTES
90
94
  route chat_routes
@@ -107,9 +111,17 @@ module RubyLLM
107
111
  return unless behavior == :invoke
108
112
 
109
113
  say "\n ✅ Chat UI installed!", :green
110
- say "\n Start your server and visit http://localhost:3000/#{chat_model_name.tableize}", :cyan
114
+ say "\n Start your server and visit http://localhost:3000/#{chat_table_name}", :cyan
111
115
  say "\n"
112
116
  end
117
+
118
+ private
119
+
120
+ def table_name_for(model_name)
121
+ # Convert namespaced model names to proper table names
122
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
123
+ model_name.underscore.pluralize.tr('/', '_')
124
+ end
113
125
  end
114
126
  end
115
127
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ # Shared helpers for RubyLLM generators
5
+ module GeneratorHelpers
6
+ def parse_model_mappings
7
+ @model_names = {
8
+ chat: 'Chat',
9
+ message: 'Message',
10
+ tool_call: 'ToolCall',
11
+ model: 'Model'
12
+ }
13
+
14
+ model_mappings.each do |mapping|
15
+ if mapping.include?(':')
16
+ key, value = mapping.split(':', 2)
17
+ @model_names[key.to_sym] = value.classify
18
+ end
19
+ end
20
+
21
+ @model_names
22
+ end
23
+
24
+ %i[chat message tool_call model].each do |type|
25
+ define_method("#{type}_model_name") do
26
+ @model_names ||= parse_model_mappings
27
+ @model_names[type]
28
+ end
29
+
30
+ define_method("#{type}_table_name") do
31
+ table_name_for(send("#{type}_model_name"))
32
+ end
33
+ end
34
+
35
+ def acts_as_chat_declaration
36
+ params = []
37
+
38
+ add_association_params(params, :messages, message_table_name, message_model_name, plural: true)
39
+ add_association_params(params, :model, model_table_name, model_model_name)
40
+
41
+ "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
42
+ end
43
+
44
+ def acts_as_message_declaration
45
+ params = []
46
+
47
+ add_association_params(params, :chat, chat_table_name, chat_model_name)
48
+ add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name, plural: true)
49
+ add_association_params(params, :model, model_table_name, model_model_name)
50
+
51
+ "acts_as_message#{" #{params.join(', ')}" if params.any?}"
52
+ end
53
+
54
+ def acts_as_model_declaration
55
+ params = []
56
+
57
+ add_association_params(params, :chats, chat_table_name, chat_model_name, plural: true)
58
+
59
+ "acts_as_model#{" #{params.join(', ')}" if params.any?}"
60
+ end
61
+
62
+ def acts_as_tool_call_declaration
63
+ params = []
64
+
65
+ add_association_params(params, :message, message_table_name, message_model_name)
66
+
67
+ "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
68
+ end
69
+
70
+ def create_namespace_modules
71
+ namespaces = []
72
+
73
+ [chat_model_name, message_model_name, tool_call_model_name, model_model_name].each do |model_name|
74
+ if model_name.include?('::')
75
+ namespace = model_name.split('::').first
76
+ namespaces << namespace unless namespaces.include?(namespace)
77
+ end
78
+ end
79
+
80
+ namespaces.each do |namespace|
81
+ module_path = "app/models/#{namespace.underscore}.rb"
82
+ next if File.exist?(Rails.root.join(module_path))
83
+
84
+ create_file module_path do
85
+ <<~RUBY
86
+ module #{namespace}
87
+ def self.table_name_prefix
88
+ "#{namespace.underscore}_"
89
+ end
90
+ end
91
+ RUBY
92
+ end
93
+ end
94
+ end
95
+
96
+ def migration_version
97
+ "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
98
+ end
99
+
100
+ def postgresql?
101
+ ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
102
+ rescue StandardError
103
+ false
104
+ end
105
+
106
+ def table_exists?(table_name)
107
+ ::ActiveRecord::Base.connection.table_exists?(table_name)
108
+ rescue StandardError
109
+ false
110
+ end
111
+
112
+ private
113
+
114
+ def add_association_params(params, default_assoc, table_name, model_name, plural: false)
115
+ assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
116
+
117
+ return if assoc == default_assoc
118
+
119
+ params << "#{default_assoc}: :#{assoc}"
120
+ params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
121
+ end
122
+
123
+ def table_name_for(model_name)
124
+ # Convert namespaced model names to proper table names
125
+ # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
126
+ model_name.underscore.pluralize.tr('/', '_')
127
+ end
128
+ end
129
+ end
@@ -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,7 +1,7 @@
1
- class Create<%= 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 :<%= chat_model_name.tableize %> do |t|
4
- t.references :<%= 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<%= 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 :<%= message_model_name.tableize %> do |t|
4
- t.references :<%= 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 :<%= 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 :<%= 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 :<%= 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<%= 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 :<%= 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,10 +34,7 @@ class Create<%= model_model_name.pluralize %> < ActiveRecord::Migration<%= migra
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 = '<%= model_model_name %>'.constantize
38
- model_class.save_to_database
39
-
40
- "Loaded #{model_class.count} models"
37
+ <%= model_model_name %>.save_to_database
41
38
  end
42
39
  end
43
40
  end
@@ -1,15 +1,15 @@
1
1
  <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
- class Create<%= 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 :<%= tool_call_model_name.tableize %> do |t|
5
- t.references :<%= 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 :<%= tool_call_model_name.tableize %>, :tool_call_id, unique: true
13
- add_index :<%= 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
@@ -3,36 +3,44 @@ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_ver
3
3
  model_class = <%= model_model_name %>
4
4
  chat_class = <%= chat_model_name %>
5
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 %>
6
14
 
7
15
  # Then check for any models in existing data that aren't in models.json
8
16
  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)
17
+ collect_and_create_models(chat_class, :<%= chat_table_name %>, model_class)
18
+ collect_and_create_models(message_class, :<%= message_table_name %>, model_class)
11
19
  model_class.count
12
20
  end
13
21
 
14
22
  # 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 %>)
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 %>)
17
25
  end
18
26
 
19
27
  def down
20
28
  # 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
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
23
31
  end
24
32
 
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
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
27
35
  end
28
36
 
29
37
  # 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
38
+ if column_exists?(:<%= message_table_name %>, :model_id_string)
39
+ rename_column :<%= message_table_name %>, :model_id_string, :model_id
32
40
  end
33
41
 
34
- if column_exists?(:<%= chat_model_name.tableize %>, :model_id_string)
35
- rename_column :<%= chat_model_name.tableize %>, :model_id_string, :model_id
42
+ if column_exists?(:<%= chat_table_name %>, :model_id_string)
43
+ rename_column :<%= chat_table_name %>, :model_id_string, :model_id
36
44
  end
37
45
  end
38
46
 
@@ -134,4 +142,4 @@ class MigrateToRubyLLMModelReferences < ActiveRecord::Migration<%= migration_ver
134
142
  model_class.find_by(model_id: model_id)
135
143
  end
136
144
  end
137
- 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyLLM
4
- VERSION = '1.7.0'
4
+ VERSION = '1.7.1'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 1.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carmine Paolino
@@ -152,6 +152,8 @@ files:
152
152
  - lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt
153
153
  - lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt
154
154
  - lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt
155
+ - lib/generators/ruby_llm/generator_helpers.rb
156
+ - lib/generators/ruby_llm/install/install_generator.rb
155
157
  - lib/generators/ruby_llm/install/templates/chat_model.rb.tt
156
158
  - lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt
157
159
  - lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt
@@ -161,9 +163,8 @@ files:
161
163
  - lib/generators/ruby_llm/install/templates/message_model.rb.tt
162
164
  - lib/generators/ruby_llm/install/templates/model_model.rb.tt
163
165
  - lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt
164
- - lib/generators/ruby_llm/install_generator.rb
165
166
  - lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt
166
- - lib/generators/ruby_llm/upgrade_to_v1_7_generator.rb
167
+ - lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb
167
168
  - lib/ruby_llm.rb
168
169
  - lib/ruby_llm/active_record/acts_as.rb
169
170
  - lib/ruby_llm/active_record/acts_as_legacy.rb
@@ -1,217 +0,0 @@
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('install/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
- end
61
-
62
- def acts_as_chat_declaration
63
- acts_as_chat_params = []
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
72
- end
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
77
- end
78
-
79
- if acts_as_chat_params.any?
80
- "acts_as_chat #{acts_as_chat_params.join(', ')}"
81
- else
82
- 'acts_as_chat'
83
- end
84
- end
85
-
86
- def acts_as_message_declaration
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
106
- end
107
-
108
- public
109
-
110
- def acts_as_tool_call_declaration
111
- acts_as_tool_call_params = []
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
119
- end
120
-
121
- if acts_as_tool_call_params.any?
122
- "acts_as_tool_call #{acts_as_tool_call_params.join(', ')}"
123
- else
124
- 'acts_as_tool_call'
125
- end
126
- end
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
-
144
- def create_migration_files
145
- # Create migrations with timestamps to ensure proper order
146
- # First create chats table
147
- migration_template 'create_chats_migration.rb.tt',
148
- "db/migrate/create_#{chat_model_name.tableize}.rb"
149
-
150
- # Then create messages table (must come before tool_calls due to foreign key)
151
- sleep 1 # Ensure different timestamp
152
- migration_template 'create_messages_migration.rb.tt',
153
- "db/migrate/create_#{message_model_name.tableize}.rb"
154
-
155
- # Then create tool_calls table (references messages)
156
- sleep 1 # Ensure different timestamp
157
- migration_template 'create_tool_calls_migration.rb.tt',
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"
164
- end
165
-
166
- def create_model_files
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"
172
- end
173
-
174
- def create_initializer
175
- template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
176
- end
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
-
185
- def show_install_info
186
- say "\n ✅ RubyLLM installed!", :green
187
-
188
- say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
189
-
190
- say "\n Next steps:", :yellow
191
- say ' 1. Run: rails db:migrate'
192
- say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
193
-
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
210
-
211
- say "\n ❤️ Love RubyLLM?", :magenta
212
- say ' • ⭐ Star on GitHub: https://github.com/crmne/ruby_llm'
213
- say ' • 🐦 Follow for updates: https://x.com/paolino'
214
- say "\n"
215
- end
216
- end
217
- end
@@ -1,160 +0,0 @@
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