ruby_llm 1.13.2 → 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 +16 -13
- data/lib/generators/ruby_llm/agent/agent_generator.rb +36 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -0
- data/lib/generators/ruby_llm/agent/templates/instructions.txt.erb.tt +0 -0
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +110 -41
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +14 -15
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -11
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +25 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/new.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_content.html.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +23 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +10 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +17 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +40 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +27 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +19 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/new.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +5 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_content.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +2 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +9 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +16 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +11 -12
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +27 -17
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -4
- data/lib/generators/ruby_llm/generator_helpers.rb +41 -17
- data/lib/generators/ruby_llm/install/install_generator.rb +21 -18
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +3 -4
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +1 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +3 -3
- data/lib/generators/ruby_llm/schema/schema_generator.rb +26 -0
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +2 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +9 -0
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/tool_generator.rb +96 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +1 -1
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +7 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +49 -0
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +5 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +1 -1
- data/lib/ruby_llm/active_record/acts_as.rb +6 -26
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +125 -29
- data/lib/ruby_llm/active_record/chat_methods.rb +50 -26
- data/lib/ruby_llm/active_record/message_methods.rb +104 -4
- data/lib/ruby_llm/active_record/model_methods.rb +8 -10
- data/lib/ruby_llm/active_record/payload_helpers.rb +29 -0
- data/lib/ruby_llm/active_record/tool_call_methods.rb +18 -0
- data/lib/ruby_llm/agent.rb +15 -2
- data/lib/ruby_llm/aliases.json +141 -89
- data/lib/ruby_llm/aliases.rb +3 -0
- data/lib/ruby_llm/attachment.rb +44 -40
- data/lib/ruby_llm/chat.rb +229 -59
- data/lib/ruby_llm/configuration.rb +69 -75
- data/lib/ruby_llm/connection.rb +37 -10
- 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 +16 -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 +67 -14
- 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 +21796 -20601
- data/lib/ruby_llm/models.rb +107 -34
- data/lib/ruby_llm/models_schema.json +3 -0
- data/lib/ruby_llm/provider.rb +25 -4
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +1 -133
- data/lib/ruby_llm/providers/anthropic/chat.rb +49 -15
- data/lib/ruby_llm/providers/anthropic/models.rb +4 -8
- 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/anthropic.rb +4 -0
- data/lib/ruby_llm/providers/azure/media.rb +1 -1
- data/lib/ruby_llm/providers/azure.rb +4 -0
- 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 +5 -1
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +28 -103
- data/lib/ruby_llm/providers/deepseek/chat.rb +9 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +45 -215
- 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/models.rb +2 -4
- 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/gemini.rb +4 -0
- 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/gpustack.rb +4 -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 +5 -1
- data/lib/ruby_llm/providers/ollama/chat.rb +8 -1
- data/lib/ruby_llm/providers/ollama.rb +4 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +157 -195
- 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/models.rb +2 -4
- 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/openai.rb +10 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +36 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +3 -3
- 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/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +34 -99
- 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/models.rb +12 -14
- data/lib/ruby_llm/providers/perplexity.rb +5 -1
- data/lib/ruby_llm/providers/vertexai.rb +8 -0
- 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 +5 -1
- 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 +51 -24
- data/lib/tasks/ruby_llm.rake +6 -5
- data/lib/tasks/vcr.rake +1 -1
- metadata +81 -21
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_message.html.erb.tt +0 -13
|
@@ -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.
|
|
@@ -58,6 +61,8 @@ module RubyLLM
|
|
|
58
61
|
end
|
|
59
62
|
|
|
60
63
|
def acts_as_tool_call(message_class: 'Message', message_foreign_key: nil, result_foreign_key: nil)
|
|
64
|
+
include RubyLLM::ActiveRecord::ToolCallMethods
|
|
65
|
+
|
|
61
66
|
@message_class = message_class.to_s
|
|
62
67
|
@message_foreign_key = message_foreign_key || ActiveSupport::Inflector.foreign_key(@message_class)
|
|
63
68
|
@result_foreign_key = result_foreign_key || ActiveSupport::Inflector.foreign_key(name)
|
|
@@ -158,27 +163,33 @@ module RubyLLM
|
|
|
158
163
|
self
|
|
159
164
|
end
|
|
160
165
|
|
|
161
|
-
def on_new_message(&
|
|
162
|
-
to_llm
|
|
166
|
+
def on_new_message(&)
|
|
167
|
+
to_llm.on_new_message(&)
|
|
168
|
+
self
|
|
169
|
+
end
|
|
163
170
|
|
|
164
|
-
|
|
171
|
+
def on_end_message(&)
|
|
172
|
+
to_llm.on_end_message(&)
|
|
173
|
+
self
|
|
174
|
+
end
|
|
165
175
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
block&.call
|
|
169
|
-
end
|
|
176
|
+
def before_message(...)
|
|
177
|
+
to_llm.before_message(...)
|
|
170
178
|
self
|
|
171
179
|
end
|
|
172
180
|
|
|
173
|
-
def
|
|
174
|
-
to_llm
|
|
181
|
+
def after_message(...)
|
|
182
|
+
to_llm.after_message(...)
|
|
183
|
+
self
|
|
184
|
+
end
|
|
175
185
|
|
|
176
|
-
|
|
186
|
+
def before_tool_call(...)
|
|
187
|
+
to_llm.before_tool_call(...)
|
|
188
|
+
self
|
|
189
|
+
end
|
|
177
190
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
block&.call(msg)
|
|
181
|
-
end
|
|
191
|
+
def after_tool_result(...)
|
|
192
|
+
to_llm.after_tool_result(...)
|
|
182
193
|
self
|
|
183
194
|
end
|
|
184
195
|
|
|
@@ -211,7 +222,7 @@ module RubyLLM
|
|
|
211
222
|
end
|
|
212
223
|
|
|
213
224
|
def create_user_message(content, with: nil)
|
|
214
|
-
RubyLLM.
|
|
225
|
+
RubyLLM.deprecator.warn(
|
|
215
226
|
'`create_user_message` is deprecated and will be removed in RubyLLM 2.0. ' \
|
|
216
227
|
'Use `add_message(role: :user, content: ...)` instead.'
|
|
217
228
|
)
|
|
@@ -317,8 +328,8 @@ module RubyLLM
|
|
|
317
328
|
def setup_persistence_callbacks
|
|
318
329
|
return @chat if @chat.instance_variable_get(:@_persistence_callbacks_setup)
|
|
319
330
|
|
|
320
|
-
@chat.
|
|
321
|
-
@chat.
|
|
331
|
+
@chat.before_message { persist_new_message }
|
|
332
|
+
@chat.after_message { |msg| persist_message_completion(msg) }
|
|
322
333
|
|
|
323
334
|
@chat.instance_variable_set(:@_persistence_callbacks_setup, true)
|
|
324
335
|
@chat
|
|
@@ -381,8 +392,8 @@ module RubyLLM
|
|
|
381
392
|
case attachment
|
|
382
393
|
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
|
383
394
|
attachment
|
|
384
|
-
when ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
385
|
-
attachment
|
|
395
|
+
when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
396
|
+
active_storage_blobs(attachment)
|
|
386
397
|
when Hash
|
|
387
398
|
attachment.values.map { |v| prepare_for_active_storage(v) }
|
|
388
399
|
else
|
|
@@ -396,16 +407,28 @@ module RubyLLM
|
|
|
396
407
|
|
|
397
408
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
398
409
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
|
404
419
|
rescue StandardError => e
|
|
405
420
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
406
421
|
nil
|
|
407
422
|
end
|
|
408
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
|
+
|
|
409
432
|
def build_content(message, attachments)
|
|
410
433
|
return message if content_like?(message)
|
|
411
434
|
|
|
@@ -471,18 +494,91 @@ module RubyLLM
|
|
|
471
494
|
end
|
|
472
495
|
|
|
473
496
|
def extract_content
|
|
474
|
-
|
|
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?
|
|
475
504
|
|
|
476
|
-
RubyLLM::Content.new(
|
|
505
|
+
RubyLLM::Content.new(text_content).tap do |content_obj|
|
|
477
506
|
@_tempfiles = []
|
|
478
507
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
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)
|
|
482
510
|
end
|
|
483
511
|
end
|
|
484
512
|
end
|
|
485
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
|
+
|
|
486
582
|
def download_attachment(attachment)
|
|
487
583
|
ext = File.extname(attachment.filename.to_s)
|
|
488
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
|
|
|
@@ -125,7 +127,7 @@ module RubyLLM
|
|
|
125
127
|
self.assume_model_exists = assume_exists
|
|
126
128
|
resolve_model_from_strings
|
|
127
129
|
save!
|
|
128
|
-
to_llm.with_model(
|
|
130
|
+
to_llm.with_model(model_association.model_id, provider: model_association.provider.to_sym, assume_exists:)
|
|
129
131
|
self
|
|
130
132
|
end
|
|
131
133
|
|
|
@@ -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
|
|
@@ -417,16 +429,28 @@ module RubyLLM
|
|
|
417
429
|
|
|
418
430
|
attachment = source.is_a?(RubyLLM::Attachment) ? source : RubyLLM::Attachment.new(source)
|
|
419
431
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
432
|
+
if attachment.active_storage?
|
|
433
|
+
active_storage_blobs(attachment.source)
|
|
434
|
+
else
|
|
435
|
+
{
|
|
436
|
+
io: StringIO.new(attachment.content),
|
|
437
|
+
filename: attachment.filename,
|
|
438
|
+
content_type: attachment.mime_type
|
|
439
|
+
}
|
|
440
|
+
end
|
|
425
441
|
rescue StandardError => e
|
|
426
442
|
RubyLLM.logger.warn "Failed to process attachment #{source}: #{e.message}"
|
|
427
443
|
nil
|
|
428
444
|
end
|
|
429
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
|
+
|
|
430
454
|
def build_content(message, attachments)
|
|
431
455
|
return message if content_like?(message)
|
|
432
456
|
|
|
@@ -1,10 +1,14 @@
|
|
|
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.
|
|
6
9
|
module MessageMethods
|
|
7
10
|
extend ActiveSupport::Concern
|
|
11
|
+
include PayloadHelpers
|
|
8
12
|
|
|
9
13
|
class_methods do
|
|
10
14
|
attr_reader :chat_class, :tool_call_class, :chat_foreign_key, :tool_call_foreign_key
|
|
@@ -39,6 +43,34 @@ module RubyLLM
|
|
|
39
43
|
)
|
|
40
44
|
end
|
|
41
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
|
+
|
|
58
|
+
def to_partial_path
|
|
59
|
+
partial_prefix = self.class.name.underscore.pluralize
|
|
60
|
+
role_partial = if to_llm.tool_call?
|
|
61
|
+
'tool_calls'
|
|
62
|
+
elsif role.to_s == 'tool'
|
|
63
|
+
'tool'
|
|
64
|
+
else
|
|
65
|
+
role.to_s.presence || 'assistant'
|
|
66
|
+
end
|
|
67
|
+
"#{partial_prefix}/#{role_partial}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def tool_error_message
|
|
71
|
+
payload_error_message(content)
|
|
72
|
+
end
|
|
73
|
+
|
|
42
74
|
private
|
|
43
75
|
|
|
44
76
|
def thinking_text_value
|
|
@@ -82,20 +114,88 @@ module RubyLLM
|
|
|
82
114
|
def extract_content
|
|
83
115
|
return RubyLLM::Content::Raw.new(content_raw) if has_attribute?(:content_raw) && content_raw.present?
|
|
84
116
|
|
|
85
|
-
content_value =
|
|
117
|
+
content_value = content
|
|
118
|
+
content_value = content_value.to_plain_text if content_value.respond_to?(:to_plain_text)
|
|
86
119
|
|
|
87
120
|
return content_value unless respond_to?(:attachments) && attachments.attached?
|
|
88
121
|
|
|
89
122
|
RubyLLM::Content.new(content_value).tap do |content_obj|
|
|
90
123
|
@_tempfiles = []
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
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)
|
|
95
127
|
end
|
|
96
128
|
end
|
|
97
129
|
end
|
|
98
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
|
+
|
|
99
199
|
def download_attachment(attachment)
|
|
100
200
|
ext = File.extname(attachment.filename.to_s)
|
|
101
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
|
-
:reasoning?, :citations?, :streaming?, :provider_class,
|
|
77
|
+
:reasoning?, :citations?, :streaming?, :provider_class, :label,
|
|
78
|
+
:cost_for,
|
|
81
79
|
to: :to_llm
|
|
82
80
|
end
|
|
83
81
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/object/blank'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
# Shared helpers for parsing serialized payloads on ActiveRecord-backed models.
|
|
9
|
+
module PayloadHelpers
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def payload_error_message(value)
|
|
13
|
+
payload = parse_payload(value)
|
|
14
|
+
return unless payload.is_a?(Hash)
|
|
15
|
+
|
|
16
|
+
payload['error'] || payload[:error]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def parse_payload(value)
|
|
20
|
+
return value if value.is_a?(Hash) || value.is_a?(Array)
|
|
21
|
+
return if value.blank?
|
|
22
|
+
|
|
23
|
+
JSON.parse(value)
|
|
24
|
+
rescue JSON::ParserError
|
|
25
|
+
nil
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/concern'
|
|
4
|
+
require 'ruby_llm/active_record/payload_helpers'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
module ActiveRecord
|
|
8
|
+
# Methods mixed into tool call models.
|
|
9
|
+
module ToolCallMethods
|
|
10
|
+
extend ActiveSupport::Concern
|
|
11
|
+
include PayloadHelpers
|
|
12
|
+
|
|
13
|
+
def tool_error_message
|
|
14
|
+
payload_error_message(arguments)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/ruby_llm/agent.rb
CHANGED
|
@@ -121,14 +121,17 @@ module RubyLLM
|
|
|
121
121
|
input_values, = partition_inputs(kwargs)
|
|
122
122
|
record = resolved_chat_model.find(id)
|
|
123
123
|
apply_configuration(record, input_values:, persist_instructions: false)
|
|
124
|
+
|
|
124
125
|
record
|
|
125
126
|
end
|
|
126
127
|
|
|
128
|
+
# Mutates persisted instructions on the configured chat record.
|
|
127
129
|
def sync_instructions!(chat_or_id, **kwargs)
|
|
128
130
|
raise ArgumentError, 'chat_model must be configured to use sync_instructions!' unless resolved_chat_model
|
|
129
131
|
|
|
130
132
|
input_values, = partition_inputs(kwargs)
|
|
131
133
|
record = chat_or_id.is_a?(resolved_chat_model) ? chat_or_id : resolved_chat_model.find(chat_or_id)
|
|
134
|
+
apply_assume_model_exists(record)
|
|
132
135
|
runtime = runtime_context(chat: record, inputs: input_values)
|
|
133
136
|
instructions_value = resolved_instructions_value(record, runtime, inputs: input_values)
|
|
134
137
|
return record if instructions_value.nil?
|
|
@@ -227,9 +230,18 @@ module RubyLLM
|
|
|
227
230
|
end
|
|
228
231
|
|
|
229
232
|
def llm_chat_for(chat_object)
|
|
233
|
+
apply_assume_model_exists(chat_object)
|
|
230
234
|
chat_object.respond_to?(:to_llm) ? chat_object.to_llm : chat_object
|
|
231
235
|
end
|
|
232
236
|
|
|
237
|
+
def apply_assume_model_exists(chat_object)
|
|
238
|
+
return unless chat_kwargs.key?(:assume_model_exists) &&
|
|
239
|
+
resolved_chat_model &&
|
|
240
|
+
chat_object.is_a?(resolved_chat_model)
|
|
241
|
+
|
|
242
|
+
chat_object.assume_model_exists = chat_kwargs[:assume_model_exists]
|
|
243
|
+
end
|
|
244
|
+
|
|
233
245
|
def evaluate(value, runtime)
|
|
234
246
|
value.is_a?(Proc) ? runtime.instance_exec(&value) : value
|
|
235
247
|
end
|
|
@@ -348,7 +360,8 @@ module RubyLLM
|
|
|
348
360
|
|
|
349
361
|
def_delegators :chat, :model, :messages, :tools, :params, :headers, :schema, :ask, :say, :with_tool, :with_tools,
|
|
350
362
|
:with_model, :with_temperature, :with_thinking, :with_context, :with_params, :with_headers,
|
|
351
|
-
:with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :
|
|
352
|
-
:
|
|
363
|
+
:with_schema, :on_new_message, :on_end_message, :on_tool_call, :on_tool_result, :before_message,
|
|
364
|
+
:after_message, :before_tool_call, :after_tool_result, :each, :complete, :add_message,
|
|
365
|
+
:reset_messages!, :cost
|
|
353
366
|
end
|
|
354
367
|
end
|