ruby_llm 2.0.0.rc3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/.rdoc_options +2 -2
  3. data/README.md +34 -23
  4. data/lib/generators/ruby_llm/generator_helpers.rb +4 -1
  5. data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +1 -1
  6. data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -1
  7. data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +5 -5
  8. data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +9 -0
  9. data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +5 -1
  10. data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +6 -2
  11. data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +8 -2
  12. data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +2 -2
  13. data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +1 -1
  14. data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +1 -1
  15. data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +11 -0
  16. data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +2 -1
  17. data/lib/ruby_llm/active_record/attachment_helpers.rb +81 -3
  18. data/lib/ruby_llm/active_record/chat_methods.rb +11 -8
  19. data/lib/ruby_llm/agent.rb +17 -17
  20. data/lib/ruby_llm/batch.rb +2 -2
  21. data/lib/ruby_llm/chat.rb +17 -17
  22. data/lib/ruby_llm/embedding.rb +1 -1
  23. data/lib/ruby_llm/error.rb +1 -1
  24. data/lib/ruby_llm/image.rb +1 -1
  25. data/lib/ruby_llm/message.rb +15 -1
  26. data/lib/ruby_llm/models.json +1654 -1791
  27. data/lib/ruby_llm/moderation.rb +1 -1
  28. data/lib/ruby_llm/ocr.rb +1 -1
  29. data/lib/ruby_llm/protocol.rb +36 -13
  30. data/lib/ruby_llm/protocols/anthropic/chat.rb +1 -6
  31. data/lib/ruby_llm/protocols/chat_completions/chat.rb +2 -11
  32. data/lib/ruby_llm/protocols/converse/chat.rb +0 -1
  33. data/lib/ruby_llm/protocols/responses/chat.rb +0 -10
  34. data/lib/ruby_llm/protocols/vertexai/research.rb +8 -8
  35. data/lib/ruby_llm/provider.rb +4 -4
  36. data/lib/ruby_llm/providers/openrouter/chat.rb +5 -5
  37. data/lib/ruby_llm/rerank.rb +1 -1
  38. data/lib/ruby_llm/research_job.rb +4 -4
  39. data/lib/ruby_llm/server_tool_call.rb +2 -2
  40. data/lib/ruby_llm/speech.rb +1 -1
  41. data/lib/ruby_llm/tools/{server_tools.rb → provider_tools.rb} +7 -7
  42. data/lib/ruby_llm/transcription.rb +1 -1
  43. data/lib/ruby_llm/version.rb +1 -1
  44. data/lib/ruby_llm/video_job.rb +1 -1
  45. data/lib/ruby_llm.rb +2 -2
  46. metadata +7 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf1e2bc13df637c93268a6ee20e52c704c47f942be416c4d6728882ae2cc35bf
4
- data.tar.gz: b63c7e4eef810843ad1bcb91a8ad55413f90764ba7e984d310c8a6b4bec3bec0
3
+ metadata.gz: 42dd9dfdb910f8e5df4eb6114ed59586a13e4a51797173d3f3bdb13b6d59c28b
4
+ data.tar.gz: fa4e0aa374c915ab5e97146bcacb438fc2c3fca0c029ed51c1aea6c934769da8
5
5
  SHA512:
6
- metadata.gz: 030126750af8dea0303a81093d415e361fdfb7335620d8e84da3fd024acba2e5323bb7fcbd47530a6a3d5cbc24ba3c2f771db431aabd8011502bd19b9b68aa71
7
- data.tar.gz: c783cfc4c121db83f4e05867893b7b822ffeddb53010eb22a5053d37eca7a539ee26dc6181b669ac5b2fac856464a18cc8929303127c1e5ae1f1fe28f9c1f373
6
+ metadata.gz: 3e5489b6fdd071de5c4281c9cc8eff9d345dd10876cb6773144873d2e58c3ec400109d208706d4c04c12d93328f5864f839907521fd751accc29aa9a5a753602
7
+ data.tar.gz: 19c33af936f0626262ad3e469c955b98aee8599b15fe088ba745e31584f91ebbf166cabc981bef4bef526f6e6c96800aebe07dc133ca732313ca58598aa6273f
data/.rdoc_options CHANGED
@@ -17,8 +17,8 @@ exclude:
17
17
  - \.json$
18
18
  footer_content:
19
19
  Documentation:
20
- Guides: https://rubyllm.com/next/
21
- Upgrading: https://rubyllm.com/next/upgrading/
20
+ Guides: https://rubyllm.com/
21
+ Upgrading: https://rubyllm.com/upgrading/
22
22
  Resources:
23
23
  GitHub: https://github.com/crmne/ruby_llm
24
24
  Issues: https://github.com/crmne/ruby_llm/issues
data/README.md CHANGED
@@ -7,7 +7,9 @@
7
7
 
8
8
  <strong>Build AI features the Ruby way</strong>
9
9
 
10
- <p>The Ruby-native AI framework. Build with chats, tools, agents, images, audio, and video through one consistent API, in plain Ruby or Rails.</p>
10
+ <p>RubyLLM is the Ruby-native AI framework. Work with models, tools, and agents through one consistent API, in plain Ruby or Rails.</p>
11
+
12
+ [Website](https://rubyllm.com/) · [Getting Started](https://rubyllm.com/getting-started/) · [What's New in 2.0](https://rubyllm.com/whats-new-in-2-0/)
11
13
 
12
14
  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) - *Fully private work AI*
13
15
 
@@ -24,26 +26,19 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
24
26
 
25
27
  ---
26
28
 
27
- Work with OpenAI, xAI, Anthropic, Google, AWS, local models, and more. Seventeen providers are built in, and you can connect an OpenAI-compatible endpoint directly.
28
-
29
- ## Build a working Ruby AI chat in two minutes
30
-
31
- https://github.com/user-attachments/assets/65422091-9338-47da-a303-92b918bd1345
32
-
33
- ## Why RubyLLM?
29
+ ## 17 providers. One Ruby API.
34
30
 
35
- Use the same Ruby methods across providers. Add files to a conversation, give an agent tools, generate media, or build a search feature with embeddings and reranking. Read response text, generated files, and usage through Ruby objects.
31
+ Build with the models you want. Move between hosted and local providers without rewriting your application, or connect an OpenAI-compatible endpoint.
36
32
 
37
- In Rails, the API works on your own Chat and Message records, with Active Storage attachments, Hotwire streaming, and background jobs. RubyLLM maintains the supporting model registry, tool calls, usage ledger, and batches. A handful of small dependencies keeps it easy to bring into an existing application.
33
+ [Browse models and pricing](https://rubyllm.com/available-models/) · [Connect a provider](https://rubyllm.com/configuration-providers/)
38
34
 
39
- ## Show me the code
35
+ ## Start with one line. Add files, tools, and agents
40
36
 
41
- These examples use **2.0.0.rc3**. Follow [Getting Started](https://rubyllm.com/next/getting-started/) to install it and configure the providers you want to try.
37
+ These examples use **2.0.0**. Follow [Getting Started](https://rubyllm.com/getting-started/) to install it and configure the providers you want to try. For 1.x, use the [1.x docs](https://rubyllm.com/v1/).
42
38
 
43
39
  ```ruby
44
- # Just ask questions
45
- chat = RubyLLM.chat
46
- chat.ask "What's the best way to learn Ruby?"
40
+ # Just ask
41
+ RubyLLM.chat.ask "What's the best way to learn Ruby?"
47
42
  ```
48
43
 
49
44
  ```ruby
@@ -155,7 +150,9 @@ response = chat.with_schema(ProductSchema).ask "Analyze this product", with: "pr
155
150
  response.parsed
156
151
  ```
157
152
 
158
- ## Features
153
+ ## A complete AI framework for Ruby
154
+
155
+ Agents, workflows, RAG, images, audio, and video. Built in, with usage tracking and Rails integration to bring them into your app.
159
156
 
160
157
  * **Chat:** Conversational AI with `RubyLLM.chat`
161
158
  * **Vision:** Analyze images and videos
@@ -170,7 +167,7 @@ response.parsed
170
167
  * **Tools:** Let AI call your Ruby methods
171
168
  * **Tool approval:** Park a run until a human approves with `requires_approval`
172
169
  * **The agentic loop:** Drive it yourself with `ask_later`, `step`, and `complete?`
173
- * **Server tools:** Web search, code execution, and MCP connectors with `with_server_tools`
170
+ * **Provider tools:** Web search, code execution, and MCP connectors with `with_provider_tools`
174
171
  * **Agents:** Reusable assistants with `RubyLLM::Agent`
175
172
  * **Prompt templates:** ERB prompts in `app/prompts`, rendered with `RubyLLM.render_prompt`
176
173
  * **Workflows:** Correlate multi-agent runs in your telemetry with `RubyLLM.workflow`
@@ -192,10 +189,10 @@ response.parsed
192
189
 
193
190
  ## Installation
194
191
 
195
- Install the 2.0 release candidate:
192
+ Install RubyLLM 2.0:
196
193
 
197
194
  ```bash
198
- bundle add ruby_llm --version 2.0.0.rc3
195
+ bundle add ruby_llm --version 2.0.0
199
196
  ```
200
197
 
201
198
  Configure a provider in your script, or in `config/initializers/ruby_llm.rb` in Rails:
@@ -208,9 +205,13 @@ RubyLLM.configure do |config|
208
205
  end
209
206
  ```
210
207
 
211
- Configure the other providers used by the examples as needed: Gemini for files, xAI for video, Mistral for OCR, and Cohere for reranking. [Getting Started](https://rubyllm.com/next/getting-started/) shows each setup beside its example. If your app uses 1.16, follow the [upgrade guide](https://rubyllm.com/next/upgrading/) before deploying 2.0.
208
+ Configure the other providers used by the examples as needed: Gemini for files, xAI for video, Mistral for OCR, and Cohere for reranking. [Getting Started](https://rubyllm.com/getting-started/) shows each setup beside its example. If your app uses 1.16, follow the [upgrade guide](https://rubyllm.com/upgrading/) before deploying 2.0.
212
209
 
213
- ## Rails
210
+ ## Feels at home in Rails
211
+
212
+ Save conversations with Active Record and stream replies with Hotwire. The generators give you a working chat UI. Watch the two-minute demo.
213
+
214
+ https://github.com/user-attachments/assets/65422091-9338-47da-a303-92b918bd1345
214
215
 
215
216
  ```bash
216
217
  # Install Rails Integration
@@ -231,11 +232,21 @@ chat = Chat.create! model: "gpt-5.6-luna"
231
232
  chat.ask "What's in this file?", with: "report.pdf"
232
233
  ```
233
234
 
234
- Visit `http://localhost:3000/chats` for a ready-to-use chat interface!
235
+ Start your Rails server and visit `http://localhost:3000/chats` to try the chat interface. See the [Rails guide](https://rubyllm.com/rails/) for persistence, streaming, and background jobs.
236
+
237
+ ## AI coding assistants
238
+
239
+ Give your coding assistant the RubyLLM API and documentation that match your application. From your application directory, install the skill packaged with your gem:
240
+
241
+ ```bash
242
+ npx skills add "$(bundle show ruby_llm)" --skill rubyllm
243
+ ```
244
+
245
+ Choose your coding assistant and installation scope when prompted. See [AI Coding Assistants](https://rubyllm.com/ai-coding-assistants/) for setup and updates.
235
246
 
236
247
  ## Documentation
237
248
 
238
- [Guides](https://rubyllm.com/next/getting-started/) · [API reference](https://rubyllm.com/next/api/) · [Models](https://rubyllm.com/available-models/) · [Upgrading](https://rubyllm.com/next/upgrading/)
249
+ [Guides](https://rubyllm.com/getting-started/) · [API reference](https://rubyllm.com/api/) · [Models](https://rubyllm.com/available-models/) · [Upgrading](https://rubyllm.com/upgrading/) · [1.x docs](https://rubyllm.com/v1/)
239
250
 
240
251
  ## Contributing
241
252
 
@@ -138,7 +138,10 @@ module RubyLLM
138
138
  end
139
139
 
140
140
  def reference_type
141
- Rails.application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint
141
+ application = Rails.application if Rails.respond_to?(:application)
142
+ return :bigint unless application
143
+
144
+ application.config.generators.options.dig(:active_record, :primary_key_type) || :bigint
142
145
  end
143
146
 
144
147
  def create_migration_class_name(table_name)
@@ -1,6 +1,6 @@
1
1
  class <%= create_migration_class_name(chat_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= chat_table_name %> do |t|
3
+ create_table :<%= chat_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :ruby_llm_model, null: false, foreign_key: { to_table: :ruby_llm_models }, type: :<%= reference_type %>
5
5
  t.boolean :cancelled, null: false, default: false
6
6
  t.timestamps
@@ -1,6 +1,6 @@
1
1
  class <%= create_migration_class_name(message_table_name) %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :<%= message_table_name %> do |t|
3
+ create_table :<%= message_table_name %>, id: :<%= reference_type %> do |t|
4
4
  t.references :<%= chat_table_name.singularize %>, null: false, foreign_key: { to_table: :<%= chat_table_name %> }, type: :<%= reference_type %>
5
5
  t.string :role, null: false
6
6
  t.text :content
@@ -1,6 +1,6 @@
1
- class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
1
+ class <%= create_migration_class_name('ruby_llm_records') %> < ActiveRecord::Migration<%= migration_version %>
2
2
  def change
3
- create_table :ruby_llm_models do |t|
3
+ create_table :ruby_llm_models, id: :<%= reference_type %> do |t|
4
4
  t.string :model_id, null: false
5
5
  t.string :name, null: false
6
6
  t.string :provider, null: false
@@ -37,7 +37,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
37
37
  <% end -%>
38
38
  end
39
39
 
40
- create_table :ruby_llm_tool_calls do |t|
40
+ create_table :ruby_llm_tool_calls, id: :<%= reference_type %> do |t|
41
41
  t.references :message, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
42
42
  t.references :result, polymorphic: true, type: :<%= reference_type %>, index: false
43
43
  t.string :tool_call_id, null: false
@@ -60,7 +60,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
60
60
  t.index :name
61
61
  end
62
62
 
63
- create_table :ruby_llm_usages do |t|
63
+ create_table :ruby_llm_usages, id: :<%= reference_type %> do |t|
64
64
  t.references :chat, polymorphic: true, null: false, type: :<%= reference_type %>, index: false
65
65
  t.references :message, polymorphic: true, type: :<%= reference_type %>, index: false
66
66
  t.string :operation, null: false
@@ -87,7 +87,7 @@ class CreateRubyLlmRecords < ActiveRecord::Migration<%= migration_version %>
87
87
  t.check_constraint "status IN (<%= usage_statuses_sql %>)"
88
88
  end
89
89
 
90
- create_table :ruby_llm_batches do |t|
90
+ create_table :ruby_llm_batches, id: :<%= reference_type %> do |t|
91
91
  t.string :provider_batch_id, null: false
92
92
  t.string :provider, null: false
93
93
  t.string :status, null: false
@@ -63,6 +63,15 @@ module RubyLLM
63
63
  end
64
64
  end
65
65
 
66
+ def discard_incomplete_tool_calls
67
+ source = records(configuration.fetch('tool_call_table'))
68
+ result_key = configuration.fetch('tool_call_foreign_key')
69
+ results = messages.where.not(result_key => nil).select(result_key)
70
+ owners = legacy_messages.select(messages.primary_key)
71
+ source.where(configuration.fetch('message_foreign_key') => owners)
72
+ .where.not(source.primary_key => results).delete_all
73
+ end
74
+
66
75
  def catch_up(passes: nil)
67
76
  sync_models
68
77
  pass = 0
@@ -47,7 +47,7 @@ module RubyLLM
47
47
  end
48
48
  end
49
49
 
50
- def finish
50
+ def finish(discard_incomplete_tool_calls: false)
51
51
  with_migration_lock do
52
52
  state = states.first!
53
53
  return if state.active_version == 2 && state.status == 'active' && data.finished?
@@ -55,6 +55,10 @@ module RubyLLM
55
55
  raise 'Run the copy backfill before finish' unless data.completed?
56
56
 
57
57
  pause(state, from: 1, statuses: %w[preparing active finishing])
58
+ if discard_incomplete_tool_calls
59
+ discarded = data.discard_incomplete_tool_calls
60
+ ::ActiveRecord::Migration.say "Discarded #{discarded} incomplete legacy tool calls"
61
+ end
58
62
  data.catch_up
59
63
  data.verify
60
64
  verify_completed_work
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'generators/ruby_llm/upgrade/legacy_content_sql'
4
3
  <% if copy_mode? -%>
5
-
6
4
  require 'generators/ruby_llm/upgrade/upgrade_migration'
5
+ <% else -%>
6
+ require 'generators/ruby_llm/upgrade/legacy_content_sql'
7
7
  <% end -%>
8
8
 
9
9
  class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migration_version %>
10
10
  disable_ddl_transaction!
11
11
 
12
+ <% unless copy_mode? -%>
12
13
  BATCH_SIZE = 10_000
14
+ <% end -%>
13
15
  PROGRESS_TABLE = :ruby_llm_v2_backfills
14
16
 
15
17
  def up
@@ -46,6 +48,7 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
46
48
  raise 'The RubyLLM 2.0 upgrade is already finished. Do not run the backfill again.'
47
49
  end
48
50
 
51
+ <% unless copy_mode? -%>
49
52
  def backfill_required_defaults
50
53
  {
51
54
  <%= chat_table_name %>: {cancelled: false},
@@ -453,6 +456,7 @@ class <%= backfill_migration_class_name %> < ActiveRecord::Migration<%= migratio
453
456
  yield
454
457
  end
455
458
 
459
+ <% end -%>
456
460
  def progress_records = migration_record(PROGRESS_TABLE)
457
461
 
458
462
  def migration_record(table)
@@ -13,7 +13,7 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
13
13
  def up
14
14
  <% if copy_mode? -%>
15
15
  RubyLLM::Generators::UpgradeMigration.for.verify_settings(<%= copy_upgrade_settings.inspect %>)
16
- RubyLLM::Generators::UpgradeMigration.for.finish do
16
+ RubyLLM::Generators::UpgradeMigration.for.finish<% if discard_incomplete_tool_calls? %>(discard_incomplete_tool_calls: true)<% end %> do
17
17
  verify_completed_backfills
18
18
  enforce_required_defaults
19
19
  relax_legacy_message_constraints
@@ -113,6 +113,7 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
113
113
  raise "RubyLLM 2.0 backfills are incomplete: #{missing.join(', ')}"
114
114
  end
115
115
 
116
+ <% unless copy_mode? -%>
116
117
  def verify_message_content
117
118
  return unless column_exists?(:<%= message_table_name %>, :content_raw)
118
119
 
@@ -220,17 +221,22 @@ class <%= finish_migration_class_name %> < ActiveRecord::Migration<%= migration_
220
221
  <% end -%>
221
222
  end
222
223
 
224
+ <% end -%>
223
225
  def with_upgrade_safety(&)
224
226
  return safety_assured(&) if respond_to?(:safety_assured, true)
225
227
 
226
228
  yield
227
229
  end
228
230
 
229
- def message_value(column) = "legacy_messages.#{quote_column(column)}"
230
231
  def column_definition(table, column) = connection.columns(table).find { |candidate| candidate.name == column.to_s }
232
+ <% unless copy_mode? -%>
233
+ def message_value(column) = "legacy_messages.#{quote_column(column)}"
231
234
  def quoted_primary_key(table) = quote_column(connection.primary_key(table))
232
235
  def quote_table(table) = connection.quote_table_name(table)
236
+ <% end -%>
237
+ <% if postgresql? || !copy_mode? -%>
233
238
  def quote_column(column) = connection.quote_column_name(column)
239
+ <% end -%>
234
240
 
235
241
  def migration_record(table)
236
242
  Class.new(ActiveRecord::Base) do
@@ -382,7 +382,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
382
382
 
383
383
  def create_usages
384
384
  unless table_exists?(:ruby_llm_usages)
385
- create_table :ruby_llm_usages do |table|
385
+ create_table :ruby_llm_usages, id: :<%= reference_type %> do |table|
386
386
  table.references :chat, polymorphic: true, null: false,
387
387
  type: reference_type_for(:<%= chat_table_name %>), index: false
388
388
  table.references :message, polymorphic: true,
@@ -417,7 +417,7 @@ class <%= prepare_migration_class_name %> < ActiveRecord::Migration<%= migration
417
417
 
418
418
  def create_batches
419
419
  unless table_exists?(:ruby_llm_batches)
420
- create_table :ruby_llm_batches do |table|
420
+ create_table :ruby_llm_batches, id: :<%= reference_type %> do |table|
421
421
  table.string :provider_batch_id, null: false
422
422
  table.string :provider, null: false
423
423
  table.string :status, null: false
@@ -15,7 +15,7 @@ module RubyLLMUpgrade # :nodoc: all
15
15
  self.enumerate_columns_in_select_statements = true
16
16
  default_scope { RubyLLMUpgrade.visible(self) }
17
17
  after_initialize { @ruby_llm_upgrade_epoch = RubyLLMUpgrade.epoch }
18
- around_save :guard_ruby_llm_upgrade, prepend: true
18
+ around_save :guard_ruby_llm_upgrade
19
19
  around_destroy :guard_ruby_llm_upgrade, prepend: true
20
20
  before_save { RubyLLMUpgrade.prepare_model(self) if ruby_llm_upgrade_kind == :chat }
21
21
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record/base'
4
3
  require Rails.root.join('app/models/concerns/ruby_llm_upgrade')
5
4
 
6
5
  Rails.application.config.to_prepare do
6
+ require 'active_record/base'
7
7
  tool_call = if Gem::Version.new(RubyLLM::VERSION).segments.first >= 2
8
8
  RubyLLM::ActiveRecord::ToolCall
9
9
  else
@@ -26,6 +26,8 @@ module RubyLLM
26
26
  desc: 'Generate one upgrade phase; cleanup runs in a later deployment'
27
27
  class_option :mode, type: :string, enum: %w[rename copy], default: 'rename',
28
28
  desc: 'Copy prepares and backfills online, with protected 1.16 rollback until cleanup'
29
+ class_option :discard_incomplete_tool_calls, type: :boolean, default: false,
30
+ desc: 'Discard all incomplete legacy tool calls at copy finish'
29
31
 
30
32
  argument :model_mappings,
31
33
  type: :array,
@@ -39,6 +41,7 @@ module RubyLLM
39
41
  end
40
42
 
41
43
  def create_migration_files
44
+ validate_cleanup_options
42
45
  parse_model_mappings(allowed_types: MODEL_MAPPING_TYPES, defaults: MODEL_MAPPING_DEFAULTS)
43
46
  say_status :models, resolved_model_mappings
44
47
 
@@ -132,6 +135,14 @@ module RubyLLM
132
135
  private
133
136
 
134
137
  def copy_mode? = options[:mode] == 'copy'
138
+ def discard_incomplete_tool_calls? = options[:discard_incomplete_tool_calls]
139
+
140
+ def validate_cleanup_options
141
+ return unless discard_incomplete_tool_calls?
142
+ return if copy_mode? && options[:phase].in?([nil, 'finish'])
143
+
144
+ raise Thor::Error, 'Discard incomplete tool calls only with --mode copy and the finish phase'
145
+ end
135
146
 
136
147
  def copy_upgrade_settings
137
148
  {
@@ -248,7 +248,8 @@ module RubyLLM
248
248
  calls = records(:ruby_llm_tool_calls).where(message_type: configuration.fetch('message_class'), result_id: nil)
249
249
  return unless calls.exists?
250
250
 
251
- raise 'Finish pending tool calls and approvals before switching versions'
251
+ raise 'Finish pending tool calls and approvals before switching versions. ' \
252
+ 'For abandoned legacy calls, see https://rubyllm.com/upgrading/#incomplete-tool-calls'
252
253
  end
253
254
 
254
255
  def reconcile
@@ -58,18 +58,96 @@ module RubyLLM
58
58
  end
59
59
 
60
60
  def plain_text_content(content_value)
61
+ return action_text_plain_text(content_value) if action_text_content?(content_value)
61
62
  return content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
62
63
 
63
64
  content_value
64
65
  end
65
66
 
66
67
  def action_text_attachment_sources(content_value)
67
- return [] unless content_value.respond_to?(:body)
68
+ return [] unless action_text_content?(content_value)
68
69
 
70
+ action_text_attachables(content_value).flat_map do |attachable|
71
+ action_text_attachable_sources(attachable)
72
+ end.compact
73
+ end
74
+
75
+ def action_text_content?(content_value)
76
+ content_value.respond_to?(:body) && action_text_body?(content_value.body)
77
+ end
78
+
79
+ def preload_action_text_embeds(messages)
80
+ return unless defined?(ActionText::Attachment)
81
+
82
+ rich_texts = messages.filter_map do |message|
83
+ rich_text = message.rich_text_content
84
+ body = rich_text&.body
85
+ rich_text if action_text_body?(body) && action_text_embeds?(body)
86
+ end
87
+ return if rich_texts.empty?
88
+
89
+ ::ActiveRecord::Associations::Preloader.new(
90
+ records: rich_texts,
91
+ associations: { embeds_attachments: :blob }
92
+ ).call
93
+ end
94
+
95
+ def action_text_embeds?(body)
96
+ body.fragment.find_all(ActionText::Attachment.tag_name).any? { |node| action_text_blob_id(node) }
97
+ end
98
+
99
+ def action_text_body?(body)
100
+ defined?(ActionText::Attachment) && body.respond_to?(:fragment) && body.respond_to?(:attachables) &&
101
+ body.respond_to?(:sanitize_content_attachment)
102
+ end
103
+
104
+ def action_text_plain_text(content_value)
105
+ body = content_value.body
106
+ preloaded_blobs = action_text_preloaded_blobs(content_value).index_by { |blob| blob.id.to_s }
107
+ rendered = body.fragment.replace(ActionText::Attachment.tag_name) do |node|
108
+ if node.key?('content')
109
+ sanitized_content = body.sanitize_content_attachment(node.remove_attribute('content').to_s)
110
+ node['content'] = sanitized_content if sanitized_content.present?
111
+ end
112
+
113
+ attachable = action_text_attachable_for_node(node, preloaded_blobs)
114
+ ActionText::Attachment.from_node(node, attachable).to_plain_text
115
+ end
116
+
117
+ ActionText::Content.new(rendered, canonicalize: false).fragment.to_plain_text
118
+ end
119
+
120
+ def action_text_attachables(content_value)
69
121
  body = content_value.body
70
- return [] unless body.respond_to?(:attachables)
122
+ preloaded_blobs = action_text_preloaded_blobs(content_value).index_by { |blob| blob.id.to_s }
123
+ body.fragment.find_all(ActionText::Attachment.tag_name).map do |node|
124
+ action_text_attachable_for_node(node, preloaded_blobs)
125
+ end
126
+ end
71
127
 
72
- body.attachables.flat_map { |attachable| action_text_attachable_sources(attachable) }.compact
128
+ def action_text_attachable_for_node(node, preloaded_blobs)
129
+ preloaded_blobs[action_text_blob_id(node)] || ActionText::Attachable.from_node(node)
130
+ end
131
+
132
+ def action_text_blob_id(node)
133
+ sgid = node['sgid']
134
+ gid = SignedGlobalID.parse(sgid, for: ActionText::Attachable::LOCATOR_NAME) if sgid
135
+ return unless gid && gid.app == GlobalID.app && gid.model_name == ActiveStorage::Blob.name
136
+
137
+ gid.model_id.to_s
138
+ end
139
+
140
+ def action_text_preloaded_blobs(content_value)
141
+ return [] unless content_value.class.respond_to?(:reflect_on_association) &&
142
+ content_value.class.reflect_on_association(:embeds_attachments)
143
+
144
+ association = content_value.association(:embeds_attachments)
145
+ return [] unless association.loaded?
146
+
147
+ content_value.embeds_attachments.filter_map do |attachment|
148
+ blob_association = attachment.association(:blob)
149
+ blob_association.target if blob_association.loaded?
150
+ end
73
151
  end
74
152
 
75
153
  def action_text_attachable_sources(attachable)
@@ -212,7 +212,7 @@ module RubyLLM
212
212
  # RubyLLM::Chat. Each behaves exactly as documented on RubyLLM::Chat,
213
213
  # then returns the record so calls chain.
214
214
  CHAINABLE_CHAT_DELEGATES = %i[
215
- with_tools with_tool_options with_server_tools with_fallbacks with_temperature
215
+ with_tools with_tool_options with_provider_tools with_fallbacks with_temperature
216
216
  with_max_output_tokens with_thinking with_citations with_caching
217
217
  with_end_user with_compaction
218
218
  with_provider_options with_headers with_schema
@@ -233,10 +233,10 @@ module RubyLLM
233
233
  # Applies Chat#with_tool_options and returns this record.
234
234
 
235
235
  ##
236
- # :method: with_server_tools
237
- # :call-seq: with_server_tools(*tools, **tools_with_options)
236
+ # :method: with_provider_tools
237
+ # :call-seq: with_provider_tools(*tools, **tools_with_options)
238
238
  #
239
- # Applies Chat#with_server_tools and returns this record.
239
+ # Applies Chat#with_provider_tools and returns this record.
240
240
 
241
241
  ##
242
242
  # :method: with_fallbacks
@@ -407,10 +407,10 @@ module RubyLLM
407
407
  # Delegates to Chat#schema. See that method for arguments and return values.
408
408
 
409
409
  ##
410
- # :method: server_tools
411
- # :call-seq: server_tools
410
+ # :method: provider_tools
411
+ # :call-seq: provider_tools
412
412
  #
413
- # Delegates to Chat#server_tools. See that method for arguments and return values.
413
+ # Delegates to Chat#provider_tools. See that method for arguments and return values.
414
414
 
415
415
  ##
416
416
  # :method: temperature
@@ -449,7 +449,7 @@ module RubyLLM
449
449
 
450
450
  PASSTHROUGH_CHAT_DELEGATES = %i[
451
451
  caching citations compaction concurrency end_user fallbacks headers max_output_tokens provider_options
452
- schema server_tools temperature thinking tool_options tools
452
+ schema provider_tools temperature thinking tool_options tools
453
453
  add_completion count_tokens each render
454
454
  ].freeze
455
455
 
@@ -778,8 +778,11 @@ module RubyLLM
778
778
 
779
779
  associations = %i[ruby_llm_tool_calls ruby_llm_parent_tool_call ruby_llm_usages]
780
780
  associations << { attachments_attachments: :blob } if attachment_association?(assoc.klass)
781
+ rich_text_reflection = assoc.klass.reflect_on_association(:rich_text_content)
782
+ associations << :rich_text_content if rich_text_reflection
781
783
 
782
784
  ::ActiveRecord::Associations::Preloader.new(records: messages, associations: associations).call
785
+ preload_action_text_embeds(messages) if rich_text_reflection
783
786
  messages
784
787
  end
785
788
 
@@ -44,7 +44,7 @@ module RubyLLM
44
44
  DUPED_INHERITED_CONFIG = {
45
45
  :@chat_kwargs => {},
46
46
  :@tools => [],
47
- :@server_tools => [],
47
+ :@provider_tools => [],
48
48
  :@tool_options => {},
49
49
  :@caching => nil,
50
50
  :@compaction => nil,
@@ -66,7 +66,7 @@ module RubyLLM
66
66
 
67
67
  # Chat methods that return the wrapped chat so calls can be chained there.
68
68
  CHAINABLE_CHAT_DELEGATES = %i[
69
- with_instructions with_tools with_server_tools with_tool_options with_model
69
+ with_instructions with_tools with_provider_tools with_tool_options with_model
70
70
  with_temperature with_max_output_tokens with_thinking with_citations
71
71
  with_end_user with_compaction with_caching with_context with_provider_options
72
72
  with_headers with_schema with_fallbacks
@@ -77,7 +77,7 @@ module RubyLLM
77
77
 
78
78
  # Chat values and operations whose return values pass through unchanged.
79
79
  PASSTHROUGH_CHAT_DELEGATES = %i[
80
- model provider messages tools server_tools tool_options provider_options headers schema concurrency
80
+ model provider messages tools provider_tools tool_options provider_options headers schema concurrency
81
81
  caching citations compaction context end_user fallbacks thinking temperature max_output_tokens
82
82
  each complete? cancelled? awaiting_approval? pending_approvals
83
83
  add_message add_completion tokens cost render
@@ -150,17 +150,17 @@ module RubyLLM
150
150
  end
151
151
 
152
152
  # Enables provider-executed tools for chats this agent builds, applied
153
- # via Chat#with_server_tools. Accepts the same aliases, options, and
153
+ # via Chat#with_provider_tools. Accepts the same aliases, options, and
154
154
  # raw Hashes; a block defers evaluation until the chat is built.
155
155
  # Called with no arguments, returns the declared entries.
156
156
  #
157
- # server_tools :web_search
158
- # server_tools web_search: { allowed_domains: ["ruby-lang.org"] }
157
+ # provider_tools :web_search
158
+ # provider_tools web_search: { allowed_domains: ["ruby-lang.org"] }
159
159
  #
160
- def server_tools(*tools, **tools_with_options, &block)
161
- return @server_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?
160
+ def provider_tools(*tools, **tools_with_options, &block)
161
+ return @provider_tools || [] if tools.empty? && tools_with_options.empty? && !block_given?
162
162
 
163
- @server_tools = block_given? ? block : RubyLLM::Tools::ServerTools.normalize(tools, tools_with_options)
163
+ @provider_tools = block_given? ? block : RubyLLM::Tools::ProviderTools.normalize(tools, tools_with_options)
164
164
  end
165
165
 
166
166
  # Adds system instructions for chats this agent builds. Accepts a string,
@@ -667,8 +667,8 @@ module RubyLLM
667
667
  options = evaluate(tool_options, runtime)
668
668
  chat.with_tool_options(**options) if options && !options.empty?
669
669
 
670
- server_tools_to_apply = Array(evaluate(server_tools, runtime)).compact
671
- chat.with_server_tools(*server_tools_to_apply) if server_tools_to_apply.any?
670
+ provider_tools_to_apply = Array(evaluate(provider_tools, runtime)).compact
671
+ chat.with_provider_tools(*provider_tools_to_apply) if provider_tools_to_apply.any?
672
672
  end
673
673
 
674
674
  def apply_passthrough_options(chat)
@@ -873,10 +873,10 @@ module RubyLLM
873
873
  # Delegates to Chat#with_tools. See that method for arguments and return values.
874
874
 
875
875
  ##
876
- # :method: with_server_tools
877
- # :call-seq: with_server_tools(*tools, **tools_with_options)
876
+ # :method: with_provider_tools
877
+ # :call-seq: with_provider_tools(*tools, **tools_with_options)
878
878
  #
879
- # Delegates to Chat#with_server_tools. See that method for arguments and return values.
879
+ # Delegates to Chat#with_provider_tools. See that method for arguments and return values.
880
880
 
881
881
  ##
882
882
  # :method: with_tool_options
@@ -1053,10 +1053,10 @@ module RubyLLM
1053
1053
  # Delegates to Chat#tools. See that method for arguments and return values.
1054
1054
 
1055
1055
  ##
1056
- # :method: server_tools
1057
- # :call-seq: server_tools
1056
+ # :method: provider_tools
1057
+ # :call-seq: provider_tools
1058
1058
  #
1059
- # Delegates to Chat#server_tools. See that method for arguments and return values.
1059
+ # Delegates to Chat#provider_tools. See that method for arguments and return values.
1060
1060
 
1061
1061
  ##
1062
1062
  # :method: tool_options