ruby_llm 1.14.1 → 1.16.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.
- checksums.yaml +4 -4
- data/README.md +6 -7
- data/lib/generators/ruby_llm/generator_helpers.rb +8 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +3 -3
- data/lib/ruby_llm/active_record/acts_as.rb +4 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +123 -29
- data/lib/ruby_llm/active_record/chat_methods.rb +41 -24
- data/lib/ruby_llm/active_record/message_methods.rb +87 -4
- data/lib/ruby_llm/active_record/model_methods.rb +7 -9
- data/lib/ruby_llm/active_record/payload_helpers.rb +3 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +3 -0
- data/lib/ruby_llm/agent.rb +4 -2
- data/lib/ruby_llm/aliases.json +108 -75
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +41 -40
- data/lib/ruby_llm/chat.rb +229 -59
- data/lib/ruby_llm/configuration.rb +14 -1
- data/lib/ruby_llm/connection.rb +36 -7
- data/lib/ruby_llm/content.rb +15 -1
- data/lib/ruby_llm/cost.rb +224 -0
- data/lib/ruby_llm/deprecator.rb +24 -0
- data/lib/ruby_llm/embedding.rb +31 -1
- data/lib/ruby_llm/error.rb +11 -75
- data/lib/ruby_llm/error_middleware.rb +81 -0
- data/lib/ruby_llm/image.rb +39 -4
- data/lib/ruby_llm/instrumentation.rb +36 -0
- data/lib/ruby_llm/message.rb +20 -0
- data/lib/ruby_llm/mime_type.rb +25 -0
- data/lib/ruby_llm/model/info.rb +53 -2
- data/lib/ruby_llm/model/pricing.rb +19 -9
- data/lib/ruby_llm/model/pricing_category.rb +13 -2
- data/lib/ruby_llm/model/pricing_tier.rb +20 -9
- data/lib/ruby_llm/model_registry.rb +39 -0
- data/lib/ruby_llm/models.json +17817 -13942
- data/lib/ruby_llm/models.rb +97 -31
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +20 -4
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/streaming.rb +2 -0
- data/lib/ruby_llm/providers/anthropic/tools.rb +32 -3
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -0
- data/lib/ruby_llm/providers/bedrock/chat.rb +26 -13
- data/lib/ruby_llm/providers/bedrock/media.rb +21 -3
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/streaming.rb +10 -1
- data/lib/ruby_llm/providers/bedrock.rb +2 -2
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +43 -0
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/gemini/chat.rb +10 -4
- data/lib/ruby_llm/providers/gemini/images.rb +2 -2
- data/lib/ruby_llm/providers/gemini/media.rb +16 -9
- data/lib/ruby_llm/providers/gemini/streaming.rb +6 -1
- data/lib/ruby_llm/providers/gemini/tools.rb +5 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +8 -1
- data/lib/ruby_llm/providers/gpustack/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -2
- data/lib/ruby_llm/providers/mistral/chat.rb +56 -5
- data/lib/ruby_llm/providers/mistral/media.rb +55 -0
- data/lib/ruby_llm/providers/mistral/models.rb +2 -0
- data/lib/ruby_llm/providers/mistral.rb +2 -2
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/openai/capabilities.rb +82 -12
- data/lib/ruby_llm/providers/openai/chat.rb +61 -7
- data/lib/ruby_llm/providers/openai/images.rb +58 -6
- data/lib/ruby_llm/providers/openai/media.rb +40 -16
- data/lib/ruby_llm/providers/openai/streaming.rb +7 -6
- data/lib/ruby_llm/providers/openai/tools.rb +2 -0
- data/lib/ruby_llm/providers/openai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +2 -2
- data/lib/ruby_llm/providers/openrouter/models.rb +1 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +5 -6
- data/lib/ruby_llm/providers/perplexity/chat.rb +11 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +62 -0
- data/lib/ruby_llm/providers/perplexity.rb +2 -2
- data/lib/ruby_llm/providers/vertexai.rb +5 -1
- data/lib/ruby_llm/providers/xai/chat.rb +9 -0
- data/lib/ruby_llm/providers/xai/models.rb +15 -27
- data/lib/ruby_llm/providers/xai.rb +2 -2
- data/lib/ruby_llm/railtie.rb +11 -1
- data/lib/ruby_llm/stream_accumulator.rb +45 -30
- data/lib/ruby_llm/streaming.rb +4 -0
- data/lib/ruby_llm/tokens.rb +8 -0
- data/lib/ruby_llm/tool.rb +24 -7
- data/lib/ruby_llm/tool_concurrency.rb +105 -0
- data/lib/ruby_llm/transcription.rb +2 -1
- data/lib/ruby_llm/utils.rb +39 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm.rb +11 -6
- data/lib/tasks/models.rake +45 -16
- data/lib/tasks/release.rake +50 -23
- metadata +35 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ba1ac6556eb9321cc5c4840eb10c4f2253a35991298d3a4f9cf6dd85035ba069
|
|
4
|
+
data.tar.gz: f87c2f1e7c1898c31ae732db0bae3a96f8b9c9082d8f9abd9a728b2b9d295426
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f75855b2dc79679febdf4edb67130f5ee60d7445bd1a34d7f29c69f3cd363711b881ffe2b51eac8fa4f2a74b297e364e53263e12674333055c8daf702d528a3a
|
|
7
|
+
data.tar.gz: aceac99dd86c4e4db028411d1d58cd2602351f3ba0b3cb7fc2334d979459a97294f2617e10770d851b57f70aa26b49f9995dd239d1f52cff43814d1293419a75
|
data/README.md
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
<img src="/docs/assets/images/logotype.svg" alt="RubyLLM" height="120" width="250">
|
|
6
6
|
</picture>
|
|
7
7
|
|
|
8
|
-
<strong>
|
|
8
|
+
<strong>A single, beautiful Ruby framework for all major AI providers. Easily build chatbots, AI agents, RAG applications, content generators, and every AI workflow you can think of.
|
|
9
|
+
</strong>
|
|
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)
|
|
11
|
+
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*
|
|
11
12
|
|
|
12
13
|
[](https://badge.fury.io/rb/ruby_llm)
|
|
13
14
|
[](https://github.com/rubocop/rubocop)
|
|
@@ -24,7 +25,7 @@ Battle tested at [<picture><source media="(prefers-color-scheme: dark)" srcset="
|
|
|
24
25
|
|
|
25
26
|
Build chatbots, AI agents, RAG applications. Works with OpenAI, xAI, Anthropic, Google, AWS, local models, and any OpenAI-compatible API.
|
|
26
27
|
|
|
27
|
-
##
|
|
28
|
+
## Build a working Ruby AI chat in two minutes
|
|
28
29
|
|
|
29
30
|
https://github.com/user-attachments/assets/65422091-9338-47da-a303-92b918bd1345
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ https://github.com/user-attachments/assets/65422091-9338-47da-a303-92b918bd1345
|
|
|
32
33
|
|
|
33
34
|
Every AI provider ships their own bloated client. Different APIs. Different response formats. Different conventions. It's exhausting.
|
|
34
35
|
|
|
35
|
-
RubyLLM gives you one beautiful
|
|
36
|
+
RubyLLM gives you one beautiful framework for all of them. Same interface whether you're using GPT, Claude, or your local Ollama. Just three dependencies: Faraday, Zeitwerk, and Marcel. That's it.
|
|
36
37
|
|
|
37
38
|
## Show me the code
|
|
38
39
|
|
|
@@ -86,9 +87,7 @@ RubyLLM.moderate "Check if this text is safe"
|
|
|
86
87
|
```ruby
|
|
87
88
|
# Let AI use your code
|
|
88
89
|
class Weather < RubyLLM::Tool
|
|
89
|
-
|
|
90
|
-
param :latitude
|
|
91
|
-
param :longitude
|
|
90
|
+
desc "Get current weather"
|
|
92
91
|
|
|
93
92
|
def execute(latitude:, longitude:)
|
|
94
93
|
url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}¤t=temperature_2m,wind_speed_10m"
|
|
@@ -87,6 +87,7 @@ module RubyLLM
|
|
|
87
87
|
|
|
88
88
|
add_association_params(params, :message, message_table_name, message_model_name,
|
|
89
89
|
owner_table: tool_call_table_name, owner_model_name: tool_call_model_name)
|
|
90
|
+
add_result_foreign_key_param(params)
|
|
90
91
|
|
|
91
92
|
"acts_as_tool_call#{" #{params.join(', ')}" if params.any?}"
|
|
92
93
|
end
|
|
@@ -178,6 +179,13 @@ module RubyLLM
|
|
|
178
179
|
"#{owner_model_name.demodulize.underscore}_id"
|
|
179
180
|
end
|
|
180
181
|
|
|
182
|
+
def add_result_foreign_key_param(params)
|
|
183
|
+
foreign_key = "#{tool_call_table_name.singularize}_id"
|
|
184
|
+
default_foreign_key = "#{tool_call_model_name.demodulize.underscore}_id"
|
|
185
|
+
|
|
186
|
+
params << "result_foreign_key: :#{foreign_key}" if foreign_key != default_foreign_key
|
|
187
|
+
end
|
|
188
|
+
|
|
181
189
|
# Convert namespaced model names to proper table names
|
|
182
190
|
# e.g., "Assistant::Chat" -> "assistant_chats" (not "assistant/chats")
|
|
183
191
|
def table_name_for(model_name)
|
|
@@ -2,7 +2,7 @@ RubyLLM.configure do |config|
|
|
|
2
2
|
config.openai_api_key = ENV.fetch("OPENAI_API_KEY", Rails.application.credentials.dig(:openai_api_key))
|
|
3
3
|
# config.default_model = "gpt-5-nano"
|
|
4
4
|
|
|
5
|
-
# Use the
|
|
5
|
+
# Use the association-based acts_as API (recommended)
|
|
6
6
|
config.use_new_acts_as = true
|
|
7
7
|
<% if model_model_name != 'Model' -%>
|
|
8
8
|
|
|
@@ -6,7 +6,7 @@ require_relative '../generator_helpers'
|
|
|
6
6
|
|
|
7
7
|
module RubyLLM
|
|
8
8
|
module Generators
|
|
9
|
-
# Generator to upgrade existing RubyLLM apps to v1.7 with
|
|
9
|
+
# Generator to upgrade existing RubyLLM apps to v1.7 with association-based Rails API
|
|
10
10
|
class UpgradeToV17Generator < Rails::Generators::Base
|
|
11
11
|
include Rails::Generators::Migration
|
|
12
12
|
include RubyLLM::Generators::GeneratorHelpers
|
|
@@ -24,7 +24,7 @@ module RubyLLM
|
|
|
24
24
|
|
|
25
25
|
argument :model_mappings, type: :array, default: [], banner: 'chat:ChatName message:MessageName ...'
|
|
26
26
|
|
|
27
|
-
desc 'Upgrades existing RubyLLM apps to v1.7 with
|
|
27
|
+
desc 'Upgrades existing RubyLLM apps to v1.7 with association-based Rails API\n' \
|
|
28
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)
|
|
@@ -78,7 +78,7 @@ module RubyLLM
|
|
|
78
78
|
return if initializer_content.include?('config.use_new_acts_as')
|
|
79
79
|
|
|
80
80
|
inject_into_file initializer_path, before: /^end/ do
|
|
81
|
-
lines = ["\n # Enable the
|
|
81
|
+
lines = ["\n # Enable the association-based Rails API", ' config.use_new_acts_as = true']
|
|
82
82
|
lines << " config.model_registry_class = \"#{model_model_name}\"" if model_model_name != 'Model'
|
|
83
83
|
lines << "\n"
|
|
84
84
|
lines.join("\n")
|
|
@@ -1,39 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'active_support/inflector'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
module ActiveRecord
|
|
5
8
|
# Adds chat and message persistence capabilities to ActiveRecord models.
|
|
6
9
|
module ActsAs
|
|
7
10
|
extend ActiveSupport::Concern
|
|
8
11
|
|
|
9
|
-
# When ActsAs is included, ensure models are loaded from database
|
|
10
12
|
def self.included(base)
|
|
11
13
|
super
|
|
12
|
-
|
|
13
|
-
RubyLLM::Models.class_eval do
|
|
14
|
-
def self.load_models
|
|
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
|
|
20
|
-
rescue StandardError => e
|
|
21
|
-
RubyLLM.logger.debug { "Failed to load models from database: #{e.message}, falling back to JSON" }
|
|
22
|
-
read_from_json
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def self.read_from_database
|
|
26
|
-
model_class = RubyLLM.config.model_registry_class
|
|
27
|
-
model_class = model_class.constantize if model_class.is_a?(String)
|
|
28
|
-
return [] unless model_class.table_exists?
|
|
29
|
-
|
|
30
|
-
model_class.all.map(&:to_llm)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def load_from_database!
|
|
34
|
-
@models = self.class.read_from_database
|
|
35
|
-
end
|
|
36
|
-
end
|
|
14
|
+
RubyLLM.config.model_registry_source ||= RubyLLM::ModelRegistry::ActiveRecordSource.new
|
|
37
15
|
end
|
|
38
16
|
|
|
39
17
|
class_methods do # rubocop:disable Metrics/BlockLength
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'active_support/inflector'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
module ActiveRecord
|
|
5
8
|
# Adds chat and message persistence capabilities to ActiveRecord models.
|
|
@@ -160,27 +163,33 @@ module RubyLLM
|
|
|
160
163
|
self
|
|
161
164
|
end
|
|
162
165
|
|
|
163
|
-
def on_new_message(&
|
|
164
|
-
to_llm
|
|
166
|
+
def on_new_message(&)
|
|
167
|
+
to_llm.on_new_message(&)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
165
170
|
|
|
166
|
-
|
|
171
|
+
def on_end_message(&)
|
|
172
|
+
to_llm.on_end_message(&)
|
|
173
|
+
self
|
|
174
|
+
end
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
block&.call
|
|
171
|
-
end
|
|
176
|
+
def before_message(...)
|
|
177
|
+
to_llm.before_message(...)
|
|
172
178
|
self
|
|
173
179
|
end
|
|
174
180
|
|
|
175
|
-
def
|
|
176
|
-
to_llm
|
|
181
|
+
def after_message(...)
|
|
182
|
+
to_llm.after_message(...)
|
|
183
|
+
self
|
|
184
|
+
end
|
|
177
185
|
|
|
178
|
-
|
|
186
|
+
def before_tool_call(...)
|
|
187
|
+
to_llm.before_tool_call(...)
|
|
188
|
+
self
|
|
189
|
+
end
|
|
179
190
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
block&.call(msg)
|
|
183
|
-
end
|
|
191
|
+
def after_tool_result(...)
|
|
192
|
+
to_llm.after_tool_result(...)
|
|
184
193
|
self
|
|
185
194
|
end
|
|
186
195
|
|
|
@@ -213,7 +222,7 @@ module RubyLLM
|
|
|
213
222
|
end
|
|
214
223
|
|
|
215
224
|
def create_user_message(content, with: nil)
|
|
216
|
-
RubyLLM.
|
|
225
|
+
RubyLLM.deprecator.warn(
|
|
217
226
|
'`create_user_message` is deprecated and will be removed in RubyLLM 2.0. ' \
|
|
218
227
|
'Use `add_message(role: :user, content: ...)` instead.'
|
|
219
228
|
)
|
|
@@ -319,8 +328,8 @@ module RubyLLM
|
|
|
319
328
|
def setup_persistence_callbacks
|
|
320
329
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
321
330
|
|
|
322
|
-
@chat.
|
|
323
|
-
@chat.
|
|
331
|
+
@chat.before_message { persist_new_message }
|
|
332
|
+
@chat.after_message { |msg| persist_message_completion(msg) }
|
|
324
333
|
|
|
325
334
|
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
|
326
335
|
@chat
|
|
@@ -383,8 +392,8 @@ module RubyLLM
|
|
|
383
392
|
case attachment
|
|
384
393
|
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
|
385
394
|
attachment
|
|
386
|
-
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
387
|
-
attachment
|
|
395
|
+
when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
396
|
+
active_storage_blobs(attachment)
|
|
388
397
|
when Hash
|
|
389
398
|
attachment.values.map { |v| prepare_for_active_storage(v) }
|
|
390
399
|
else
|
|
@@ -398,16 +407,28 @@ module RubyLLM
|
|
|
398
407
|
|
|
399
408
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
400
409
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
410
|
+
if attachment.active_storage?
|
|
411
|
+
active_storage_blobs(attachment.source)
|
|
412
|
+
else
|
|
413
|
+
{
|
|
414
|
+
io: StringIO.new(attachment.content),
|
|
415
|
+
filename: attachment.filename,
|
|
416
|
+
content_type: attachment.mime_type
|
|
417
|
+
}
|
|
418
|
+
end
|
|
406
419
|
rescue StandardError => e
|
|
407
420
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
408
421
|
nil
|
|
409
422
|
end
|
|
410
423
|
|
|
424
|
+
def active_storage_blobs(attachment)
|
|
425
|
+
case attachment
|
|
426
|
+
when ActiveStorage::Blob then attachment
|
|
427
|
+
when ActiveStorage::Attachment, ActiveStorage::Attached::One then attachment.blob
|
|
428
|
+
when ActiveStorage::Attached::Many then attachment.blobs
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
411
432
|
def build_content(message, attachments)
|
|
412
433
|
return message if content_like?(message)
|
|
413
434
|
|
|
@@ -473,18 +494,91 @@ module RubyLLM
|
|
|
473
494
|
end
|
|
474
495
|
|
|
475
496
|
def extract_content
|
|
476
|
-
|
|
497
|
+
text_content = if content.respond_to?(:to_plain_text)
|
|
498
|
+
content.to_plain_text
|
|
499
|
+
else
|
|
500
|
+
content
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
return text_content unless respond_to?(:attachments) && attachments.attached?
|
|
477
504
|
|
|
478
|
-
RubyLLM::Content.new(
|
|
505
|
+
RubyLLM::Content.new(text_content).tap do |content_obj|
|
|
479
506
|
@_tempfiles = []
|
|
480
507
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
|
508
|
+
attachment_sources.each do |attachment, attachable|
|
|
509
|
+
add_attachment_to_content(content_obj, attachment, attachable)
|
|
484
510
|
end
|
|
485
511
|
end
|
|
486
512
|
end
|
|
487
513
|
|
|
514
|
+
def attachment_sources
|
|
515
|
+
change = pending_attachment_change
|
|
516
|
+
return attachments.map { |attachment| [attachment, nil] } unless pending_attachment_change?(change)
|
|
517
|
+
|
|
518
|
+
change.attachments.zip(change.attachables)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def pending_attachment_change
|
|
522
|
+
attachment_changes['attachments'] if respond_to?(:attachment_changes)
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
def pending_attachment_change?(change)
|
|
526
|
+
change.respond_to?(:attachments) && change.respond_to?(:attachables)
|
|
527
|
+
end
|
|
528
|
+
|
|
529
|
+
def add_attachment_to_content(content_obj, attachment, attachable)
|
|
530
|
+
if pending_upload_attachable?(attachable)
|
|
531
|
+
add_pending_upload_attachment(content_obj, attachable)
|
|
532
|
+
else
|
|
533
|
+
tempfile = download_attachment(attachment)
|
|
534
|
+
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def pending_upload_attachable?(attachable)
|
|
539
|
+
return false if attachable.nil? || attachable.is_a?(String)
|
|
540
|
+
return false if instance_of_class?(attachable, 'ActiveStorage::Blob')
|
|
541
|
+
|
|
542
|
+
uploaded_file?(attachable) || active_storage_upload_hash?(attachable) ||
|
|
543
|
+
attachable.is_a?(File) || pathname?(attachable)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def uploaded_file?(attachable)
|
|
547
|
+
instance_of_class?(attachable, 'ActionDispatch::Http::UploadedFile') ||
|
|
548
|
+
instance_of_class?(attachable, 'Rack::Test::UploadedFile')
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def active_storage_upload_hash?(attachable)
|
|
552
|
+
attachable.is_a?(Hash) && attachment_hash_io(attachable).present?
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def pathname?(attachable)
|
|
556
|
+
defined?(Pathname) && attachable.is_a?(Pathname)
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def add_pending_upload_attachment(content_obj, attachable)
|
|
560
|
+
if attachable.is_a?(Hash)
|
|
561
|
+
content_obj.add_attachment(attachment_hash_io(attachable), filename: attachment_hash_filename(attachable))
|
|
562
|
+
else
|
|
563
|
+
content_obj.add_attachment(attachable)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def attachment_hash_io(attachable)
|
|
568
|
+
attachable[:io] || attachable['io']
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def attachment_hash_filename(attachable)
|
|
572
|
+
filename = attachable[:filename] || attachable['filename']
|
|
573
|
+
filename&.to_s
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def instance_of_class?(object, class_name)
|
|
577
|
+
Object.const_get(class_name).then { |klass| object.is_a?(klass) }
|
|
578
|
+
rescue NameError
|
|
579
|
+
false
|
|
580
|
+
end
|
|
581
|
+
|
|
488
582
|
def download_attachment(attachment)
|
|
489
583
|
ext = File.extname(attachment.filename.to_s)
|
|
490
584
|
basename = File.basename(attachment.filename.to_s, ext)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
|
|
3
5
|
module RubyLLM
|
|
4
6
|
module ActiveRecord
|
|
5
7
|
# Methods mixed into chat models.
|
|
@@ -63,8 +65,8 @@ module RubyLLM
|
|
|
63
65
|
m.context_window = model_info.context_window
|
|
64
66
|
m.max_output_tokens = model_info.max_output_tokens
|
|
65
67
|
m.capabilities = model_info.capabilities || []
|
|
66
|
-
m.modalities = model_info.modalities
|
|
67
|
-
m.pricing = model_info.pricing
|
|
68
|
+
m.modalities = model_info.modalities.to_h
|
|
69
|
+
m.pricing = model_info.pricing.to_h
|
|
68
70
|
m.metadata = model_info.metadata || {}
|
|
69
71
|
end
|
|
70
72
|
|
|
@@ -154,27 +156,33 @@ module RubyLLM
|
|
|
154
156
|
self
|
|
155
157
|
end
|
|
156
158
|
|
|
157
|
-
def on_new_message(&
|
|
158
|
-
to_llm
|
|
159
|
+
def on_new_message(&)
|
|
160
|
+
to_llm.on_new_message(&)
|
|
161
|
+
self
|
|
162
|
+
end
|
|
159
163
|
|
|
160
|
-
|
|
164
|
+
def on_end_message(&)
|
|
165
|
+
to_llm.on_end_message(&)
|
|
166
|
+
self
|
|
167
|
+
end
|
|
161
168
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
block&.call
|
|
165
|
-
end
|
|
169
|
+
def before_message(...)
|
|
170
|
+
to_llm.before_message(...)
|
|
166
171
|
self
|
|
167
172
|
end
|
|
168
173
|
|
|
169
|
-
def
|
|
170
|
-
to_llm
|
|
174
|
+
def after_message(...)
|
|
175
|
+
to_llm.after_message(...)
|
|
176
|
+
self
|
|
177
|
+
end
|
|
171
178
|
|
|
172
|
-
|
|
179
|
+
def before_tool_call(...)
|
|
180
|
+
to_llm.before_tool_call(...)
|
|
181
|
+
self
|
|
182
|
+
end
|
|
173
183
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
block&.call(msg)
|
|
177
|
-
end
|
|
184
|
+
def after_tool_result(...)
|
|
185
|
+
to_llm.after_tool_result(...)
|
|
178
186
|
self
|
|
179
187
|
end
|
|
180
188
|
|
|
@@ -208,6 +216,10 @@ module RubyLLM
|
|
|
208
216
|
message_record
|
|
209
217
|
end
|
|
210
218
|
|
|
219
|
+
def cost
|
|
220
|
+
RubyLLM::Cost.aggregate(messages_association.map(&:cost))
|
|
221
|
+
end
|
|
222
|
+
|
|
211
223
|
def create_user_message(content, with: nil)
|
|
212
224
|
add_message(role: :user, content: build_content(content, with))
|
|
213
225
|
end
|
|
@@ -258,8 +270,8 @@ module RubyLLM
|
|
|
258
270
|
def setup_persistence_callbacks
|
|
259
271
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
260
272
|
|
|
261
|
-
@chat.
|
|
262
|
-
@chat.
|
|
273
|
+
@chat.before_message { persist_new_message }
|
|
274
|
+
@chat.after_message { |msg| persist_message_completion(msg) }
|
|
263
275
|
|
|
264
276
|
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
|
265
277
|
@chat
|
|
@@ -402,8 +414,8 @@ module RubyLLM
|
|
|
402
414
|
case attachment
|
|
403
415
|
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
|
404
416
|
attachment
|
|
405
|
-
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
406
|
-
attachment
|
|
417
|
+
when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
418
|
+
active_storage_blobs(attachment)
|
|
407
419
|
when Hash
|
|
408
420
|
attachment.values.map { |v| prepare_for_active_storage(v) }
|
|
409
421
|
else
|
|
@@ -418,10 +430,7 @@ module RubyLLM
|
|
|
418
430
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
419
431
|
|
|
420
432
|
if attachment.active_storage?
|
|
421
|
-
|
|
422
|
-
when ActiveStorage::Blob then attachment.source
|
|
423
|
-
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many then attachment.source.blobs
|
|
424
|
-
end
|
|
433
|
+
active_storage_blobs(attachment.source)
|
|
425
434
|
else
|
|
426
435
|
{
|
|
427
436
|
io: StringIO.new(attachment.content),
|
|
@@ -434,6 +443,14 @@ module RubyLLM
|
|
|
434
443
|
nil
|
|
435
444
|
end
|
|
436
445
|
|
|
446
|
+
def active_storage_blobs(attachment)
|
|
447
|
+
case attachment
|
|
448
|
+
when ActiveStorage::Blob then attachment
|
|
449
|
+
when ActiveStorage::Attachment, ActiveStorage::Attached::One then attachment.blob
|
|
450
|
+
when ActiveStorage::Attached::Many then attachment.blobs
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
437
454
|
def build_content(message, attachments)
|
|
438
455
|
return message if content_like?(message)
|
|
439
456
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'ruby_llm/active_record/payload_helpers'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
module ActiveRecord
|
|
5
8
|
# Methods mixed into message models.
|
|
@@ -40,6 +43,18 @@ module RubyLLM
|
|
|
40
43
|
)
|
|
41
44
|
end
|
|
42
45
|
|
|
46
|
+
def cost
|
|
47
|
+
RubyLLM::Cost.new(tokens:, model: model_association)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def cache_read_tokens
|
|
51
|
+
cached_value
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def cache_write_tokens
|
|
55
|
+
cache_creation_value
|
|
56
|
+
end
|
|
57
|
+
|
|
43
58
|
def to_partial_path
|
|
44
59
|
partial_prefix = self.class.name.underscore.pluralize
|
|
45
60
|
role_partial = if to_llm.tool_call?
|
|
@@ -99,20 +114,88 @@ module RubyLLM
|
|
|
99
114
|
def extract_content
|
|
100
115
|
return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
|
|
101
116
|
|
|
102
|
-
content_value =
|
|
117
|
+
content_value = content
|
|
118
|
+
content_value = content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
|
|
103
119
|
|
|
104
120
|
return content_value unless respond_to?(:attachments) && attachments.attached?
|
|
105
121
|
|
|
106
122
|
RubyLLM::Content.new(content_value).tap do |content_obj|
|
|
107
123
|
@_tempfiles = []
|
|
108
124
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
|
125
|
+
attachment_sources.each do |attachment, attachable|
|
|
126
|
+
add_attachment_to_content(content_obj, attachment, attachable)
|
|
112
127
|
end
|
|
113
128
|
end
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
def attachment_sources
|
|
132
|
+
change = pending_attachment_change
|
|
133
|
+
return attachments.map { |attachment| [attachment, nil] } unless pending_attachment_change?(change)
|
|
134
|
+
|
|
135
|
+
change.attachments.zip(change.attachables)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def pending_attachment_change
|
|
139
|
+
attachment_changes['attachments'] if respond_to?(:attachment_changes)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def pending_attachment_change?(change)
|
|
143
|
+
change.respond_to?(:attachments) && change.respond_to?(:attachables)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def add_attachment_to_content(content_obj, attachment, attachable)
|
|
147
|
+
if pending_upload_attachable?(attachable)
|
|
148
|
+
add_pending_upload_attachment(content_obj, attachable)
|
|
149
|
+
else
|
|
150
|
+
tempfile = download_attachment(attachment)
|
|
151
|
+
content_obj.add_attachment(tempfile, filename: attachment.filename.to_s)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def pending_upload_attachable?(attachable)
|
|
156
|
+
return false if attachable.nil? || attachable.is_a?(String)
|
|
157
|
+
return false if instance_of_class?(attachable, 'ActiveStorage::Blob')
|
|
158
|
+
|
|
159
|
+
uploaded_file?(attachable) || active_storage_upload_hash?(attachable) ||
|
|
160
|
+
attachable.is_a?(File) || pathname?(attachable)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def uploaded_file?(attachable)
|
|
164
|
+
instance_of_class?(attachable, 'ActionDispatch::Http::UploadedFile') ||
|
|
165
|
+
instance_of_class?(attachable, 'Rack::Test::UploadedFile')
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def active_storage_upload_hash?(attachable)
|
|
169
|
+
attachable.is_a?(Hash) && attachment_hash_io(attachable).present?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def pathname?(attachable)
|
|
173
|
+
defined?(Pathname) && attachable.is_a?(Pathname)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def add_pending_upload_attachment(content_obj, attachable)
|
|
177
|
+
if attachable.is_a?(Hash)
|
|
178
|
+
content_obj.add_attachment(attachment_hash_io(attachable), filename: attachment_hash_filename(attachable))
|
|
179
|
+
else
|
|
180
|
+
content_obj.add_attachment(attachable)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def attachment_hash_io(attachable)
|
|
185
|
+
attachable[:io] || attachable['io']
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def attachment_hash_filename(attachable)
|
|
189
|
+
filename = attachable[:filename] || attachable['filename']
|
|
190
|
+
filename&.to_s
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def instance_of_class?(object, class_name)
|
|
194
|
+
Object.const_get(class_name).then { |klass| object.is_a?(klass) }
|
|
195
|
+
rescue NameError
|
|
196
|
+
false
|
|
197
|
+
end
|
|
198
|
+
|
|
116
199
|
def download_attachment(attachment)
|
|
117
200
|
ext = File.extname(attachment.filename.to_s)
|
|
118
201
|
basename = File.basename(attachment.filename.to_s, ext)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'active_support/core_ext/module/delegation'
|
|
5
|
+
|
|
3
6
|
module RubyLLM
|
|
4
7
|
module ActiveRecord
|
|
5
8
|
# Methods mixed into model registry models.
|
|
@@ -10,15 +13,7 @@ module RubyLLM
|
|
|
10
13
|
def refresh!
|
|
11
14
|
RubyLLM.models.refresh!
|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
RubyLLM.models.all.each do |model_info|
|
|
15
|
-
model = find_or_initialize_by(
|
|
16
|
-
model_id: model_info.id,
|
|
17
|
-
provider: model_info.provider
|
|
18
|
-
)
|
|
19
|
-
model.update!(from_llm_attributes(model_info))
|
|
20
|
-
end
|
|
21
|
-
end
|
|
16
|
+
save_to_database
|
|
22
17
|
end
|
|
23
18
|
|
|
24
19
|
def save_to_database
|
|
@@ -76,8 +71,11 @@ module RubyLLM
|
|
|
76
71
|
|
|
77
72
|
delegate :supports?, :supports_vision?, :supports_functions?, :type,
|
|
78
73
|
:input_price_per_million, :output_price_per_million,
|
|
74
|
+
:cache_read_input_price_per_million, :cache_write_input_price_per_million,
|
|
75
|
+
:cached_input_price_per_million, :cache_creation_input_price_per_million,
|
|
79
76
|
:function_calling?, :structured_output?, :batch?,
|
|
80
77
|
:reasoning?, :citations?, :streaming?, :provider_class, :label,
|
|
78
|
+
:cost_for,
|
|
81
79
|
to: :to_llm
|
|
82
80
|
end
|
|
83
81
|
end
|