ruby_llm 1.12.0 → 1.14.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 (141) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -5
  3. data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
  4. data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
  5. data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
  6. data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
  7. data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
  8. data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
  9. data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
  10. data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
  11. data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +2 -2
  12. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
  13. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
  14. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
  15. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
  16. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
  17. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
  18. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
  19. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
  20. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
  21. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
  22. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
  23. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
  24. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
  25. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
  26. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
  27. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
  28. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
  29. data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
  30. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
  31. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
  32. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
  33. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
  34. data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
  35. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
  36. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
  37. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
  38. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
  39. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
  40. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
  41. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
  42. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
  43. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
  44. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
  45. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
  46. data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
  47. data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
  48. data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
  49. data/lib/generators/ruby_llm/generator_helpers.rb +37 -17
  50. data/lib/generators/ruby_llm/install/install_generator.rb +22 -18
  51. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
  52. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -1
  53. data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +4 -10
  54. data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +2 -2
  55. data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -2
  56. data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
  57. data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
  58. data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
  59. data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
  60. data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
  61. data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
  62. data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
  63. data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
  64. data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
  65. data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +2 -4
  66. data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
  67. data/lib/ruby_llm/active_record/acts_as.rb +10 -4
  68. data/lib/ruby_llm/active_record/acts_as_legacy.rb +87 -20
  69. data/lib/ruby_llm/active_record/chat_methods.rb +80 -22
  70. data/lib/ruby_llm/active_record/message_methods.rb +17 -0
  71. data/lib/ruby_llm/active_record/model_methods.rb +1 -1
  72. data/lib/ruby_llm/active_record/payload_helpers.rb +26 -0
  73. data/lib/ruby_llm/active_record/tool_call_methods.rb +15 -0
  74. data/lib/ruby_llm/agent.rb +50 -8
  75. data/lib/ruby_llm/aliases.json +60 -21
  76. data/lib/ruby_llm/attachment.rb +4 -1
  77. data/lib/ruby_llm/chat.rb +113 -12
  78. data/lib/ruby_llm/configuration.rb +65 -66
  79. data/lib/ruby_llm/connection.rb +11 -7
  80. data/lib/ruby_llm/content.rb +6 -2
  81. data/lib/ruby_llm/error.rb +37 -1
  82. data/lib/ruby_llm/message.rb +5 -3
  83. data/lib/ruby_llm/model/info.rb +15 -13
  84. data/lib/ruby_llm/models.json +12279 -13517
  85. data/lib/ruby_llm/models.rb +16 -6
  86. data/lib/ruby_llm/provider.rb +10 -1
  87. data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -119
  88. data/lib/ruby_llm/providers/anthropic/chat.rb +22 -5
  89. data/lib/ruby_llm/providers/anthropic/models.rb +3 -9
  90. data/lib/ruby_llm/providers/anthropic/tools.rb +20 -0
  91. data/lib/ruby_llm/providers/anthropic.rb +5 -1
  92. data/lib/ruby_llm/providers/azure/chat.rb +1 -1
  93. data/lib/ruby_llm/providers/azure/embeddings.rb +1 -1
  94. data/lib/ruby_llm/providers/azure/models.rb +1 -1
  95. data/lib/ruby_llm/providers/azure.rb +92 -0
  96. data/lib/ruby_llm/providers/bedrock/chat.rb +50 -5
  97. data/lib/ruby_llm/providers/bedrock/models.rb +17 -1
  98. data/lib/ruby_llm/providers/bedrock/streaming.rb +8 -4
  99. data/lib/ruby_llm/providers/bedrock.rb +9 -1
  100. data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -114
  101. data/lib/ruby_llm/providers/deepseek.rb +5 -1
  102. data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -207
  103. data/lib/ruby_llm/providers/gemini/chat.rb +20 -4
  104. data/lib/ruby_llm/providers/gemini/images.rb +1 -1
  105. data/lib/ruby_llm/providers/gemini/models.rb +2 -4
  106. data/lib/ruby_llm/providers/gemini/streaming.rb +2 -1
  107. data/lib/ruby_llm/providers/gemini/tools.rb +19 -0
  108. data/lib/ruby_llm/providers/gemini.rb +4 -0
  109. data/lib/ruby_llm/providers/gpustack/capabilities.rb +20 -0
  110. data/lib/ruby_llm/providers/gpustack.rb +8 -0
  111. data/lib/ruby_llm/providers/mistral/capabilities.rb +8 -0
  112. data/lib/ruby_llm/providers/mistral/chat.rb +2 -1
  113. data/lib/ruby_llm/providers/mistral.rb +4 -0
  114. data/lib/ruby_llm/providers/ollama/capabilities.rb +20 -0
  115. data/lib/ruby_llm/providers/ollama.rb +11 -1
  116. data/lib/ruby_llm/providers/openai/capabilities.rb +95 -195
  117. data/lib/ruby_llm/providers/openai/chat.rb +15 -5
  118. data/lib/ruby_llm/providers/openai/media.rb +4 -1
  119. data/lib/ruby_llm/providers/openai/models.rb +2 -4
  120. data/lib/ruby_llm/providers/openai/temperature.rb +2 -2
  121. data/lib/ruby_llm/providers/openai/tools.rb +27 -2
  122. data/lib/ruby_llm/providers/openai.rb +10 -0
  123. data/lib/ruby_llm/providers/openrouter/chat.rb +19 -5
  124. data/lib/ruby_llm/providers/openrouter/images.rb +69 -0
  125. data/lib/ruby_llm/providers/openrouter.rb +35 -1
  126. data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
  127. data/lib/ruby_llm/providers/perplexity/models.rb +12 -14
  128. data/lib/ruby_llm/providers/perplexity.rb +4 -0
  129. data/lib/ruby_llm/providers/vertexai/models.rb +1 -1
  130. data/lib/ruby_llm/providers/vertexai.rb +18 -6
  131. data/lib/ruby_llm/providers/xai.rb +4 -0
  132. data/lib/ruby_llm/stream_accumulator.rb +10 -5
  133. data/lib/ruby_llm/streaming.rb +7 -7
  134. data/lib/ruby_llm/tool.rb +48 -3
  135. data/lib/ruby_llm/version.rb +1 -1
  136. data/lib/tasks/models.rake +33 -7
  137. data/lib/tasks/release.rake +1 -1
  138. data/lib/tasks/ruby_llm.rake +9 -1
  139. data/lib/tasks/vcr.rake +1 -1
  140. metadata +56 -15
  141. data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
@@ -53,9 +53,9 @@ module RubyLLM
53
53
  params = []
54
54
 
55
55
  add_association_params(params, :messages, message_table_name, message_model_name,
56
- owner_table: chat_table_name, plural: true)
56
+ owner_table: chat_table_name, owner_model_name: chat_model_name, plural: true)
57
57
  add_association_params(params, :model, model_table_name, model_model_name,
58
- owner_table: chat_table_name)
58
+ owner_table: chat_table_name, owner_model_name: chat_model_name)
59
59
 
60
60
  "acts_as_chat#{" #{params.join(', ')}" if params.any?}"
61
61
  end
@@ -64,11 +64,11 @@ module RubyLLM
64
64
  params = []
65
65
 
66
66
  add_association_params(params, :chat, chat_table_name, chat_model_name,
67
- owner_table: message_table_name)
67
+ owner_table: message_table_name, owner_model_name: message_model_name)
68
68
  add_association_params(params, :tool_calls, tool_call_table_name, tool_call_model_name,
69
- owner_table: message_table_name, plural: true)
69
+ owner_table: message_table_name, owner_model_name: message_model_name, plural: true)
70
70
  add_association_params(params, :model, model_table_name, model_model_name,
71
- owner_table: message_table_name)
71
+ owner_table: message_table_name, owner_model_name: message_model_name)
72
72
 
73
73
  "acts_as_message#{" #{params.join(', ')}" if params.any?}"
74
74
  end
@@ -77,7 +77,7 @@ module RubyLLM
77
77
  params = []
78
78
 
79
79
  add_association_params(params, :chats, chat_table_name, chat_model_name,
80
- owner_table: model_table_name, plural: true)
80
+ owner_table: model_table_name, owner_model_name: model_model_name, plural: true)
81
81
 
82
82
  "acts_as_model#{" #{params.join(', ')}" if params.any?}"
83
83
  end
@@ -86,7 +86,7 @@ module RubyLLM
86
86
  params = []
87
87
 
88
88
  add_association_params(params, :message, message_table_name, message_model_name,
89
- owner_table: tool_call_table_name)
89
+ owner_table: tool_call_table_name, owner_model_name: tool_call_model_name)
90
90
 
91
91
  "acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
92
92
  end
@@ -121,6 +121,10 @@ module RubyLLM
121
121
  "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]"
122
122
  end
123
123
 
124
+ def create_migration_class_name(table_name)
125
+ "create_#{table_name}".camelize
126
+ end
127
+
124
128
  def postgresql?
125
129
  ::ActiveRecord::Base.connection.adapter_name.downcase.include?('postgresql')
126
130
  rescue StandardError
@@ -141,22 +145,38 @@ module RubyLLM
141
145
 
142
146
  private
143
147
 
144
- def add_association_params(params, default_assoc, table_name, model_name, owner_table:, plural: false) # rubocop:disable Metrics/ParameterLists
148
+ # rubocop:disable Metrics/ParameterLists
149
+ def add_association_params(params, default_assoc, table_name, model_name,
150
+ owner_table:, owner_model_name:, plural: false)
145
151
  assoc = plural ? table_name.to_sym : table_name.singularize.to_sym
146
-
147
- default_foreign_key = "#{default_assoc}_id"
148
- # has_many/has_one: foreign key is on the associated table pointing back to owner
149
- # belongs_to: foreign key is on the owner table pointing to associated table
150
- foreign_key = if plural || default_assoc.to_s.pluralize == default_assoc.to_s # has_many or has_one
151
- "#{owner_table.singularize}_id"
152
- else # belongs_to
153
- "#{table_name.singularize}_id"
154
- end
152
+ collection_association = collection_association?(default_assoc, plural)
153
+ foreign_key = inferred_foreign_key(table_name, owner_table, collection_association)
154
+ default_foreign_key = default_inferred_foreign_key(assoc, owner_model_name, collection_association)
155
155
 
156
156
  params << "#{default_assoc}: :#{assoc}" if assoc != default_assoc
157
157
  params << "#{default_assoc.to_s.singularize}_class: '#{model_name}'" if model_name != assoc.to_s.classify
158
158
  params << "#{default_assoc}_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
159
159
  end
160
+ # rubocop:enable Metrics/ParameterLists
161
+
162
+ def collection_association?(default_assoc, plural)
163
+ plural || default_assoc.to_s.pluralize == default_assoc.to_s
164
+ end
165
+
166
+ def inferred_foreign_key(table_name, owner_table, collection_association)
167
+ return "#{table_name.singularize}_id" unless collection_association
168
+
169
+ "#{owner_table.singularize}_id"
170
+ end
171
+
172
+ # Rails default inference:
173
+ # belongs_to :assoc -> assoc_id
174
+ # has_many/has_one -> owner demodulized model name + _id
175
+ def default_inferred_foreign_key(association_name, owner_model_name, collection_association)
176
+ return "#{association_name}_id" unless collection_association
177
+
178
+ "#{owner_model_name.demodulize.underscore}_id"
179
+ end
160
180
 
161
181
  # Convert namespaced model names to proper table names
162
182
  # e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
@@ -21,7 +21,7 @@ module RubyLLM
21
21
  desc: 'Skip ActiveStorage installation and attachment setup'
22
22
 
23
23
  desc 'Creates models and migrations for RubyLLM Rails integration\n' \
24
- 'Usage: rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
24
+ 'Usage: bin/rails g ruby_llm:install [chat:ChatName] [message:MessageName] ...'
25
25
 
26
26
  def self.next_migration_number(dirname)
27
27
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -30,20 +30,12 @@ module RubyLLM
30
30
  def create_migration_files
31
31
  migration_template 'create_chats_migration.rb.tt',
32
32
  "db/migrate/create_#{chat_table_name}.rb"
33
-
34
- sleep 1 # Ensure different timestamp
35
33
  migration_template 'create_messages_migration.rb.tt',
36
34
  "db/migrate/create_#{message_table_name}.rb"
37
-
38
- sleep 1 # Ensure different timestamp
39
35
  migration_template 'create_tool_calls_migration.rb.tt',
40
36
  "db/migrate/create_#{tool_call_table_name}.rb"
41
-
42
- sleep 1 # Ensure different timestamp
43
37
  migration_template 'create_models_migration.rb.tt',
44
38
  "db/migrate/create_#{model_table_name}.rb"
45
-
46
- sleep 1 # Ensure different timestamp
47
39
  migration_template 'add_references_to_chats_tool_calls_and_messages_migration.rb.tt',
48
40
  'db/migrate/add_references_to_' \
49
41
  "#{chat_table_name}_#{tool_call_table_name}_and_#{message_table_name}.rb"
@@ -63,6 +55,13 @@ module RubyLLM
63
55
  template 'initializer.rb.tt', 'config/initializers/ruby_llm.rb'
64
56
  end
65
57
 
58
+ def create_convention_directories
59
+ %w[agents tools schemas prompts].each do |name|
60
+ empty_directory "app/#{name}"
61
+ create_file "app/#{name}/.gitkeep" unless File.exist?(Rails.root.join("app/#{name}/.gitkeep"))
62
+ end
63
+ end
64
+
66
65
  def install_active_storage
67
66
  return if options[:skip_active_storage]
68
67
 
@@ -76,21 +75,18 @@ module RubyLLM
76
75
  say ' ✅ ActiveStorage configured for file attachments support', :green unless options[:skip_active_storage]
77
76
 
78
77
  say "\n Next steps:", :yellow
79
- say ' 1. Run: rails db:migrate'
80
- say ' 2. Set your API keys in config/initializers/ruby_llm.rb'
81
-
82
- say " 3. Start chatting: #{chat_model_name}.create!(model: 'gpt-4.1-nano').ask('Hello!')"
78
+ say ' 1. Run: bin/rails db:migrate'
79
+ say ' 2. Run: bin/rails ruby_llm:load_models'
80
+ say ' 3. Set your API keys in config/initializers/ruby_llm.rb'
83
81
 
84
- say "\n 🚀 Model registry is database-backed!", :cyan
85
- say ' Models automatically load from the database'
86
- say ' Pass model names as strings - RubyLLM handles the rest!'
87
- say " Specify provider when needed: Chat.create!(model: 'gemini-2.5-flash', provider: 'vertexai')"
82
+ say " 4. Start chatting: #{chat_model_name}.create!(model: 'gpt-5-nano').ask('Hello!')"
83
+ say " 5. Optional UI: #{chat_ui_generator_command}"
88
84
 
89
85
  if options[:skip_active_storage]
90
86
  say "\n 📎 Note: ActiveStorage was skipped", :yellow
91
87
  say ' File attachments won\'t work without ActiveStorage.'
92
88
  say ' To enable later:'
93
- say ' 1. Run: rails active_storage:install && rails db:migrate'
89
+ say ' 1. Run: bin/rails active_storage:install && bin/rails db:migrate'
94
90
  say " 2. Add to your #{message_model_name} model: has_many_attached :attachments"
95
91
  end
96
92
 
@@ -101,6 +97,14 @@ module RubyLLM
101
97
  say ' • 🐦 Follow for updates: https://x.com/paolino'
102
98
  say "\n"
103
99
  end
100
+
101
+ private
102
+
103
+ def chat_ui_generator_command
104
+ mappings = model_mappings.join(' ')
105
+ mappings = " #{mappings}" unless mappings.empty?
106
+ "bin/rails generate ruby_llm:chat_ui#{mappings}"
107
+ end
104
108
  end
105
109
  end
106
110
  end
@@ -1,4 +1,4 @@
1
- class Create<%= chat_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= create_migration_class_name(chat_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :<%= chat_table_name %> do |t|
4
4
  t.timestamps
@@ -1,4 +1,4 @@
1
- class Create<%= message_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :<%= message_table_name %> do |t|
4
4
  t.string :role, null: false
@@ -1,4 +1,4 @@
1
- class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= create_migration_class_name(model_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
3
  create_table :<%= model_table_name %> do |t|
4
4
  t.string :model_id, null: false
@@ -27,19 +27,13 @@ class Create<%= model_model_name.gsub('::', '').pluralize %> < ActiveRecord::Mig
27
27
  <% end %>
28
28
  t.timestamps
29
29
 
30
- t.index [:provider, :model_id], unique: true
30
+ t.index [ :provider, :model_id ], unique: true
31
31
  t.index :provider
32
32
  t.index :family
33
- <% if postgresql? %>
33
+ <% if postgresql? -%>
34
34
  t.index :capabilities, using: :gin
35
35
  t.index :modalities, using: :gin
36
- <% end %>
37
- end
38
-
39
- # Load models from JSON
40
- say_with_time "Loading models from models.json" do
41
- RubyLLM.models.load_from_json!
42
- <%= model_model_name %>.save_to_database
36
+ <% end -%>
43
37
  end
44
38
  end
45
39
  end
@@ -1,10 +1,10 @@
1
1
  <%#- # Migration for creating tool_calls table with database-specific JSON handling -%>
2
- class Create<%= tool_call_model_name.gsub('::', '').pluralize %> < ActiveRecord::Migration<%= migration_version %>
2
+ class <%= create_migration_class_name(tool_call_table_name) %> < ActiveRecord::Migration<%= migration_version %>
3
3
  def change
4
4
  create_table :<%= tool_call_table_name %> do |t|
5
5
  t.string :tool_call_id, null: false
6
6
  t.string :name, null: false
7
- t.string :thought_signature
7
+ t.text :thought_signature
8
8
  <% if postgresql? %>
9
9
  t.jsonb :arguments, default: {}
10
10
  <% elsif mysql? %>
@@ -1,6 +1,6 @@
1
1
  RubyLLM.configure do |config|
2
- config.openai_api_key = ENV['OPENAI_API_KEY'] || Rails.application.credentials.dig(:openai_api_key)
3
- # config.default_model = "gpt-4.1-nano"
2
+ config.openai_api_key = ENV.fetch("OPENAI_API_KEY", Rails.application.credentials.dig(:openai_api_key))
3
+ # config.default_model = "gpt-5-nano"
4
4
 
5
5
  # Use the new association-based acts_as API (recommended)
6
6
  config.use_new_acts_as = true
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for RubyLLM schema classes.
8
+ class SchemaGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:schema'
12
+
13
+ desc 'Creates a RubyLLM schema class'
14
+
15
+ def create_schema_file
16
+ template 'schema.rb.tt', File.join('app/schemas', class_path, "#{file_name}.rb")
17
+ end
18
+
19
+ private
20
+
21
+ def schema_class_name
22
+ class_name.end_with?('Schema') ? class_name : "#{class_name}Schema"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,2 @@
1
+ class <%= schema_class_name %> < RubyLLM::Schema
2
+ end
@@ -0,0 +1,9 @@
1
+ class <%= class_name %>Tool < RubyLLM::Tool
2
+ description "TODO: describe what this tool does"
3
+
4
+ def execute
5
+ # TODO: return something to the LLM.
6
+ # Anything you return here gets converted to a string.
7
+ "TODO: implement <%= class_name %>Tool"
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ <%% tool_call_error = tool_call.tool_error_message %>
2
+ <%% if tool_call_error.present? %>
3
+ <%%= render "messages/error", message: tool_calls, title: "Tool Call Error", error_message: tool_call_error %>
4
+ <%% else %>
5
+ <div id="message_<%%= tool_calls.id %>" class="message"
6
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
7
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Call</div>
8
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool_call.name %>(<%%= tool_call.arguments.map { |k, v| "#{k}: #{v.inspect}" }.join(", ") %>)</pre>
9
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
10
+ <%%= tool_calls.created_at&.strftime("%I:%M %p") %>
11
+ </div>
12
+ </div>
13
+ <%% end %>
@@ -0,0 +1,13 @@
1
+ <%% error_message = tool.tool_error_message %>
2
+ <%% if error_message.present? %>
3
+ <%%= render "messages/error", message: tool, title: "Tool Result Error", error_message: error_message %>
4
+ <%% else %>
5
+ <div id="message_<%%= tool.id %>" class="message"
6
+ style="margin-bottom: 20px; padding: 10px; border-left: 3px solid #6b7280; background: #f9fafb;">
7
+ <div style="font-weight: bold; margin-bottom: 5px;"><%= tool_display_name %> Result</div>
8
+ <pre style="white-space: pre-wrap; margin: 0;"><%%= tool.content.presence || "(no output)" %></pre>
9
+ <div style="font-size: 0.85em; color: #666; margin-top: 5px;">
10
+ <%%= tool.created_at&.strftime("%I:%M %p") %>
11
+ </div>
12
+ </div>
13
+ <%% end %>
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+
5
+ module RubyLLM
6
+ module Generators
7
+ # Generator for RubyLLM tool classes and related message partials.
8
+ class ToolGenerator < Rails::Generators::NamedBase
9
+ source_root File.expand_path('templates', __dir__)
10
+
11
+ namespace 'ruby_llm:tool'
12
+
13
+ check_class_collision suffix: 'Tool'
14
+
15
+ desc 'Creates a RubyLLM tool class and matching tool call/result view partials'
16
+
17
+ def create_tool_file
18
+ template 'tool.rb.tt', File.join('app/tools', class_path, "#{file_name}_tool.rb")
19
+ end
20
+
21
+ def create_tool_view_partials
22
+ empty_directory 'app/views/messages/tool_calls'
23
+ empty_directory 'app/views/messages/tool_results'
24
+
25
+ create_tool_call_partial
26
+ create_tool_result_partial
27
+ end
28
+
29
+ private
30
+
31
+ def create_tool_call_partial
32
+ destination_path = File.join('app/views/messages/tool_calls', "_#{tool_partial_name}.html.erb")
33
+ default_partial_path = File.join(destination_root, 'app/views/messages/tool_calls/_default.html.erb')
34
+
35
+ if File.exist?(default_partial_path)
36
+ default_markup = tool_named_call_markup(File.read(default_partial_path))
37
+ indented_markup = indent_non_empty_lines(default_markup, 2)
38
+ create_file destination_path, <<~ERB
39
+ <% tool_call_error = tool_call.tool_error_message %>
40
+ <% if tool_call_error.present? %>
41
+ <%= render "messages/error", message: tool_calls, title: "Tool Call Error", error_message: tool_call_error %>
42
+ <% else %>
43
+ #{indented_markup}<% end %>
44
+ ERB
45
+ else
46
+ template 'tool_call.html.erb.tt', destination_path
47
+ end
48
+
49
+ strip_trailing_whitespace(destination_path)
50
+ end
51
+
52
+ def create_tool_result_partial
53
+ destination_path = File.join('app/views/messages/tool_results', "_#{tool_partial_name}.html.erb")
54
+ default_partial_path = File.join(destination_root, 'app/views/messages/tool_results/_default.html.erb')
55
+
56
+ if File.exist?(default_partial_path)
57
+ create_file destination_path, tool_named_result_markup(File.read(default_partial_path))
58
+ else
59
+ template 'tool_result.html.erb.tt', destination_path
60
+ end
61
+
62
+ strip_trailing_whitespace(destination_path)
63
+ end
64
+
65
+ def tool_named_call_markup(markup)
66
+ markup.sub('Tool Call', "#{tool_display_name} Call")
67
+ end
68
+
69
+ def tool_named_result_markup(markup)
70
+ markup.sub(/\bTool\b(?!\s*Result)/, "#{tool_display_name} Result")
71
+ end
72
+
73
+ def tool_display_name
74
+ class_name.demodulize
75
+ end
76
+
77
+ def tool_partial_name
78
+ file_name.delete_suffix('_tool')
79
+ end
80
+
81
+ def indent_non_empty_lines(markup, spaces)
82
+ indentation = ' ' * spaces
83
+ markup.lines.map { |line| line.strip.empty? ? line : "#{indentation}#{line}" }.join
84
+ end
85
+
86
+ def strip_trailing_whitespace(path)
87
+ content = File.read(path)
88
+ stripped_content = content.lines.map(&:rstrip).join("\n")
89
+ stripped_content = "#{stripped_content}\n" unless stripped_content.end_with?("\n")
90
+ return if content == stripped_content
91
+
92
+ File.write(path, stripped_content)
93
+ end
94
+ end
95
+ end
96
+ end
@@ -38,7 +38,7 @@ module RubyLLM
38
38
 
39
39
  Next steps:
40
40
  1. Review the generated migration
41
- 2. Run: rails db:migrate
41
+ 2. Run: bin/rails db:migrate
42
42
  3. Restart your application server
43
43
 
44
44
  📚 See the v1.10.0 release notes for details on extended thinking support.
@@ -0,0 +1,7 @@
1
+ class AddRubyLlmV114Columns < ActiveRecord::Migration<%= migration_version %>
2
+ def change
3
+ if column_exists?(:<%= tool_call_table_name %>, :thought_signature, :string)
4
+ change_column :<%= tool_call_table_name %>, :thought_signature, :text
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,49 @@
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
+ module Generators
9
+ # Generator to fix tool call thought signature column type for MySQL safety.
10
+ class UpgradeToV114Generator < Rails::Generators::Base
11
+ include Rails::Generators::Migration
12
+ include RubyLLM::Generators::GeneratorHelpers
13
+
14
+ namespace 'ruby_llm:upgrade_to_v1_14'
15
+ source_root File.expand_path('templates', __dir__)
16
+
17
+ argument :model_mappings, type: :array, default: [], banner: 'tool_call:ToolCallName'
18
+
19
+ desc 'Updates tool call thought_signature column to text introduced in v1.14.0'
20
+
21
+ def self.next_migration_number(dirname)
22
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
23
+ end
24
+
25
+ def create_migration_file
26
+ parse_model_mappings
27
+
28
+ migration_template 'add_v1_14_tool_call_columns.rb.tt',
29
+ 'db/migrate/add_ruby_llm_v1_14_columns.rb',
30
+ migration_version: migration_version,
31
+ tool_call_table_name: tool_call_table_name
32
+ end
33
+
34
+ def show_next_steps
35
+ say_status :success, 'Upgrade prepared!', :green
36
+ say <<~INSTRUCTIONS
37
+
38
+ Next steps:
39
+ 1. Review the generated migration
40
+ 2. Run: bin/rails db:migrate
41
+ 3. Restart your application server
42
+
43
+ 📚 See the v1.14.0 release notes for details on thought signature persistence.
44
+
45
+ INSTRUCTIONS
46
+ end
47
+ end
48
+ end
49
+ end
@@ -25,7 +25,7 @@ module RubyLLM
25
25
  argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
26
26
 
27
27
  desc 'Upgrades existing RubyLLM apps to v1.7 with new Rails-like API\n' \
28
- 'Usage: rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
28
+ 'Usage: bin/rails g ruby_llm:upgrade_to_v1_7 [chat:ChatName] [message:MessageName] ...'
29
29
 
30
30
  def self.next_migration_number(dirname)
31
31
  ::ActiveRecord::Generators::Base.next_migration_number(dirname)
@@ -40,8 +40,6 @@ module RubyLLM
40
40
  "db/migrate/create_#{table_name_for(model_model_name)}.rb",
41
41
  migration_version: migration_version,
42
42
  model_model_name: model_model_name
43
-
44
- sleep 1 # Ensure different timestamp
45
43
  end
46
44
 
47
45
  migration_template 'migration.rb.tt',
@@ -93,7 +91,7 @@ module RubyLLM
93
91
 
94
92
  Next steps:
95
93
  1. Review the generated migrations
96
- 2. Run: rails db:migrate
94
+ 2. Run: bin/rails db:migrate
97
95
  3. Update your code to use the new API: #{chat_model_name}.create! now has the same signature as RubyLLM.chat
98
96
 
99
97
  ⚠️ If you get "undefined method 'acts_as_model'" during migration:
@@ -37,7 +37,7 @@ module RubyLLM
37
37
 
38
38
  Next steps:
39
39
  1. Review the generated migration
40
- 2. Run: rails db:migrate
40
+ 2. Run: bin/rails db:migrate
41
41
  3. Restart your application server
42
42
 
43
43
  📚 See the v1.9.0 release notes for details on cached token tracking and raw content support.
@@ -12,15 +12,21 @@ module RubyLLM
12
12
  # Monkey-patch Models to use database when ActsAs is active
13
13
  RubyLLM::Models.class_eval do
14
14
  def self.load_models
15
- read_from_database
15
+ database_models = read_from_database
16
+ return database_models if database_models.any?
17
+
18
+ RubyLLM.logger.debug { 'Model registry is empty in database, falling back to JSON registry' }
19
+ read_from_json
16
20
  rescue StandardError => e
17
- RubyLLM.logger.debug "Failed to load models from database: #{e.message}, falling back to JSON"
21
+ RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
18
22
  read_from_json
19
23
  end
20
24
 
21
25
  def self.read_from_database
22
26
  model_class = RubyLLM.config.model_registry_class
23
27
  model_class = model_class.constantize if model_class.is_a?(String)
28
+ return [] unless model_class.table_exists?
29
+
24
30
  model_class.all.map(&:to_llm)
25
31
  end
26
32
 
@@ -53,8 +59,6 @@ module RubyLLM
53
59
  foreign_key: model_foreign_key,
54
60
  optional: true
55
61
 
56
- delegate :add_message, to: :to_llm
57
-
58
62
  define_method :messages_association do
59
63
  send(messages_association_name)
60
64
  end
@@ -144,6 +148,8 @@ module RubyLLM
144
148
 
145
149
  def acts_as_tool_call(message: :message, message_class: nil, message_foreign_key: nil, # rubocop:disable Metrics/ParameterLists
146
150
  result: :result, result_class: nil, result_foreign_key: nil)
151
+ include RubyLLM::ActiveRecord::ToolCallMethods
152
+
147
153
  class_attribute :message_association_name, :result_association_name, :message_class, :result_class
148
154
 
149
155
  self.message_association_name = message