ruby_llm 1.16.0 → 2.0.0.rc2
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/.rdoc_options +25 -0
- data/README.md +85 -32
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -111
- data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
- data/lib/ruby_llm/active_record/message_methods.rb +113 -136
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -151
- data/lib/ruby_llm/aliases.json +268 -100
- data/lib/ruby_llm/attachment.rb +187 -48
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1127 -198
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +261 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +121 -17
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +159 -23
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +153 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +24 -5
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +55 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +73261 -33733
- data/lib/ruby_llm/models.rb +477 -215
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
- data/lib/ruby_llm/protocols/anthropic.rb +101 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +560 -128
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +5 -14
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
- data/lib/ruby_llm/providers/bedrock.rb +216 -45
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
- data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +6 -18
- data/lib/ruby_llm/providers/mistral/models.rb +57 -23
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +16 -4
- data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +28 -20
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +160 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +3 -2
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -36
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +15 -5
- data/lib/ruby_llm/railtie.rb +7 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +138 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +380 -6
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +339 -97
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -41
- data/lib/ruby_llm/connection.rb +0 -159
- data/lib/ruby_llm/content.rb +0 -91
- data/lib/ruby_llm/deprecator.rb +0 -24
- data/lib/ruby_llm/error_middleware.rb +0 -81
- data/lib/ruby_llm/instrumentation.rb +0 -36
- data/lib/ruby_llm/mime_type.rb +0 -96
- data/lib/ruby_llm/model/info.rb +0 -164
- data/lib/ruby_llm/model_registry.rb +0 -39
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -236
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -218
- data/lib/ruby_llm/streaming.rb +0 -179
- data/lib/ruby_llm/tool_concurrency.rb +0 -105
- data/lib/ruby_llm/utils.rb +0 -130
- data/lib/tasks/models.rake +0 -593
- data/lib/tasks/release.rake +0 -94
- data/lib/tasks/vcr.rake +0 -124
|
@@ -1,484 +1,1007 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'active_support/concern'
|
|
4
|
+
require 'ruby_llm/active_record/attachment_helpers'
|
|
4
5
|
|
|
5
6
|
module RubyLLM
|
|
6
7
|
module ActiveRecord
|
|
7
|
-
#
|
|
8
|
+
# ChatMethods provides the RubyLLM::Chat API on ActiveRecord models
|
|
9
|
+
# declared with <tt>acts_as_chat</tt>, persisting every message to the
|
|
10
|
+
# database. Configuration methods return +self+ so calls can be chained.
|
|
11
|
+
#
|
|
12
|
+
# class Chat < ApplicationRecord
|
|
13
|
+
# acts_as_chat
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# chat = Chat.create!(model: 'gpt-5.6-luna')
|
|
17
|
+
# chat.ask "What is the capital of France?"
|
|
18
|
+
# chat.messages.count # => 2
|
|
19
|
+
#
|
|
8
20
|
module ChatMethods
|
|
9
21
|
extend ActiveSupport::Concern
|
|
22
|
+
include Enumerable
|
|
23
|
+
include AttachmentHelpers
|
|
24
|
+
|
|
25
|
+
CANCELLATION_POLL_INTERVAL = 1.0 # :nodoc:
|
|
26
|
+
COMPLETION_ERRORS = [ # :nodoc:
|
|
27
|
+
RubyLLM::CancelledError, RubyLLM::Error, Faraday::Error, Timeout::Error, Errno::ETIMEDOUT
|
|
28
|
+
].freeze
|
|
10
29
|
|
|
11
30
|
included do
|
|
12
|
-
before_save :
|
|
31
|
+
before_save :resolve_model
|
|
13
32
|
end
|
|
14
33
|
|
|
15
|
-
|
|
34
|
+
# When +true+, skips the model registry lookup so unregistered model ids
|
|
35
|
+
# are accepted. Not persisted; set it again after reloading the record.
|
|
36
|
+
attr_accessor :assume_model_exists
|
|
16
37
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
# Overrides the wire protocol the provider would pick for the model, such
|
|
39
|
+
# as +:responses+ or +:chat_completions+ for OpenAI, or +nil+ for the
|
|
40
|
+
# provider default. Not persisted; set it again after reloading the record.
|
|
41
|
+
attr_accessor :protocol
|
|
42
|
+
|
|
43
|
+
# An optional RubyLLM::Context supplying per-chat configuration, used
|
|
44
|
+
# when building the underlying chat. Not persisted; set it again after
|
|
45
|
+
# reloading the record.
|
|
46
|
+
attr_accessor :context
|
|
47
|
+
|
|
48
|
+
# Requests cancellation of the current in-flight chat operation. The
|
|
49
|
+
# request is persisted so a background job can observe it from another
|
|
50
|
+
# process.
|
|
51
|
+
def cancel
|
|
52
|
+
if persisted?
|
|
53
|
+
update_column(:cancelled, true)
|
|
54
|
+
else
|
|
55
|
+
self[:cancelled] = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@chat&.cancel
|
|
59
|
+
self
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns whether this record or its memoized in-memory chat has a
|
|
63
|
+
# pending cancellation request.
|
|
64
|
+
def cancelled?
|
|
65
|
+
@chat&.cancelled? || self[:cancelled]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Records approval for +tool_call+ (a ToolCall, a tool call record, or
|
|
69
|
+
# an id) on the persisted tool call, so the next #complete executes it
|
|
70
|
+
# from any process. Returns +self+.
|
|
71
|
+
#
|
|
72
|
+
# chat.approve(params[:tool_call_id])
|
|
73
|
+
# CompleteJob.perform_later(chat.id)
|
|
74
|
+
#
|
|
75
|
+
def approve(tool_call)
|
|
76
|
+
record_tool_call_decision(tool_call, 'approved')
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Records denial for +tool_call+ (a ToolCall, a tool call record, or
|
|
80
|
+
# an id) on the persisted tool call. The next #complete appends a
|
|
81
|
+
# structured denial result instead of executing the tool. Returns
|
|
82
|
+
# +self+.
|
|
83
|
+
def deny(tool_call)
|
|
84
|
+
record_tool_call_decision(tool_call, 'denied')
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Returns whether the conversation is waiting on tool calls that
|
|
88
|
+
# require approval and have no recorded decision. See
|
|
89
|
+
# RubyLLM::Chat#awaiting_approval?.
|
|
90
|
+
def awaiting_approval?
|
|
91
|
+
to_llm.awaiting_approval?
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the persisted tool call records that require approval and
|
|
95
|
+
# have no recorded decision, ready to render as approval cards. Pass
|
|
96
|
+
# a record (or its tool_call_id) to #approve or #deny.
|
|
97
|
+
# A record's +remote?+ identifies a provider-executed call.
|
|
98
|
+
#
|
|
99
|
+
# chat.pending_approvals.each { |record| render record }
|
|
100
|
+
#
|
|
101
|
+
def pending_approvals
|
|
102
|
+
ids = to_llm.pending_approvals.map(&:id)
|
|
103
|
+
RubyLLM::ActiveRecord::ToolCall.where(
|
|
104
|
+
tool_call_id: ids,
|
|
105
|
+
message_type: self.class.message_class.constantize.polymorphic_name,
|
|
106
|
+
message_id: messages_association.select(:id)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
20
109
|
|
|
21
|
-
|
|
110
|
+
def messages_association # :nodoc:
|
|
111
|
+
send(messages_association_name)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Sets the chat's model from an id, a RubyLLM::Model value, or the
|
|
115
|
+
# associated internal model record.
|
|
116
|
+
#
|
|
117
|
+
# chat.model = 'gpt-5.6-luna'
|
|
118
|
+
#
|
|
119
|
+
def model=(value)
|
|
120
|
+
if value.is_a?(RubyLLM::ActiveRecord::Model)
|
|
121
|
+
@pending_model_id = nil
|
|
122
|
+
@pending_provider = nil
|
|
22
123
|
super
|
|
23
124
|
else
|
|
24
|
-
|
|
125
|
+
@pending_model_id = value.respond_to?(:id) ? value.id : value
|
|
126
|
+
@pending_provider = value.provider if value.respond_to?(:provider)
|
|
25
127
|
end
|
|
26
128
|
end
|
|
27
129
|
|
|
130
|
+
# Stores +value+ as the model id, resolved to a model record before save.
|
|
28
131
|
def model_id=(value)
|
|
29
|
-
@
|
|
132
|
+
@pending_model_id = value
|
|
30
133
|
end
|
|
31
134
|
|
|
135
|
+
# Returns the model id of the associated model record, or +nil+.
|
|
136
|
+
#
|
|
137
|
+
# chat.model_id # => "gpt-5.6-luna"
|
|
138
|
+
#
|
|
32
139
|
def model_id
|
|
33
|
-
|
|
140
|
+
model&.model_id || @pending_model_id
|
|
34
141
|
end
|
|
35
142
|
|
|
143
|
+
# Stores +value+ as the provider used when resolving the model id
|
|
144
|
+
# before save.
|
|
36
145
|
def provider=(value)
|
|
37
|
-
@
|
|
146
|
+
@pending_provider = value
|
|
38
147
|
end
|
|
39
148
|
|
|
149
|
+
# Returns the provider of the associated model record, or +nil+.
|
|
40
150
|
def provider
|
|
41
|
-
|
|
151
|
+
model&.provider || @pending_provider
|
|
42
152
|
end
|
|
43
153
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return unless @model_string
|
|
50
|
-
|
|
51
|
-
model_info, _provider = Models.resolve(
|
|
52
|
-
@model_string,
|
|
53
|
-
provider: @provider_string,
|
|
54
|
-
assume_exists: assume_model_exists || false,
|
|
55
|
-
config: config
|
|
56
|
-
)
|
|
57
|
-
|
|
58
|
-
model_class = self.class.model_class.constantize
|
|
59
|
-
model_record = model_class.find_or_create_by!(
|
|
60
|
-
model_id: model_info.id,
|
|
61
|
-
provider: model_info.provider
|
|
62
|
-
) do |m|
|
|
63
|
-
m.name = model_info.name || model_info.id
|
|
64
|
-
m.family = model_info.family
|
|
65
|
-
m.context_window = model_info.context_window
|
|
66
|
-
m.max_output_tokens = model_info.max_output_tokens
|
|
67
|
-
m.capabilities = model_info.capabilities || []
|
|
68
|
-
m.modalities = model_info.modalities.to_h
|
|
69
|
-
m.pricing = model_info.pricing.to_h
|
|
70
|
-
m.metadata = model_info.metadata || {}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
self.model_association = model_record
|
|
74
|
-
@model_string = nil
|
|
75
|
-
@provider_string = nil
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
public
|
|
79
|
-
|
|
154
|
+
# Returns the underlying RubyLLM::Chat for this record, building it on
|
|
155
|
+
# first call and memoizing it. The chat is loaded with the persisted
|
|
156
|
+
# messages and wired to persist new ones. Subsequent calls return the
|
|
157
|
+
# same chat without touching the database; use #reload to refresh its
|
|
158
|
+
# message history from the record.
|
|
80
159
|
def to_llm
|
|
81
|
-
|
|
82
|
-
@chat ||= (context || RubyLLM).chat(
|
|
83
|
-
model: model_record.model_id,
|
|
84
|
-
provider: model_record.provider.to_sym,
|
|
85
|
-
assume_model_exists: assume_model_exists || false
|
|
86
|
-
)
|
|
87
|
-
@chat.reset_messages!
|
|
88
|
-
|
|
89
|
-
ordered_messages = order_messages_for_llm(messages_association.to_a)
|
|
90
|
-
ordered_messages.each do |msg|
|
|
91
|
-
@chat.add_message(msg.to_llm)
|
|
92
|
-
end
|
|
93
|
-
reapply_runtime_instructions(@chat)
|
|
94
|
-
|
|
95
|
-
setup_persistence_callbacks
|
|
160
|
+
@chat ||= build_llm_chat # rubocop:disable Naming/MemoizedInstanceVariableName
|
|
96
161
|
end
|
|
97
162
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
163
|
+
# Reloads the record from the database, Rails-style, and refreshes the
|
|
164
|
+
# underlying chat's persisted message history to match. Runtime-only
|
|
165
|
+
# configuration such as tools, temperature, and callbacks is preserved.
|
|
166
|
+
# Returns +self+.
|
|
167
|
+
def reload(...)
|
|
168
|
+
super
|
|
169
|
+
sync_messages if @chat
|
|
170
|
+
self
|
|
171
|
+
end
|
|
101
172
|
|
|
102
|
-
|
|
173
|
+
# Rebinds the underlying chat to +value+ so subsequent requests use its
|
|
174
|
+
# configuration. Pass +nil+ to return to the global RubyLLM
|
|
175
|
+
# configuration. The Context itself is runtime-only and is not
|
|
176
|
+
# persisted.
|
|
177
|
+
def with_context(value)
|
|
178
|
+
self.context = value
|
|
179
|
+
@chat&.with_context(value)
|
|
103
180
|
self
|
|
104
181
|
end
|
|
105
182
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
183
|
+
# Sets the system instructions, persisting them as a message with the
|
|
184
|
+
# +:system+ role. Replaces any persisted system messages unless
|
|
185
|
+
# +append:+ is true. Pass <tt>persist: false</tt> to apply the
|
|
186
|
+
# instructions only to the in-memory chat for this record instance.
|
|
187
|
+
# With <tt>cache_until_here: true</tt> the instruction becomes an
|
|
188
|
+
# explicit prompt cache boundary. Returns +self+.
|
|
189
|
+
#
|
|
190
|
+
# chat.with_instructions "You are a Ruby expert."
|
|
191
|
+
# chat.with_instructions "Use short bullet points.", append: true
|
|
192
|
+
# chat.with_instructions current_context, persist: false
|
|
193
|
+
#
|
|
194
|
+
def with_instructions(instructions, append: false, persist: true, cache_until_here: false)
|
|
195
|
+
to_llm
|
|
196
|
+
|
|
197
|
+
if persist
|
|
198
|
+
if instructions.nil?
|
|
199
|
+
clear_persisted_system_instructions
|
|
200
|
+
else
|
|
201
|
+
persist_system_instruction(instructions, append:, cache_until_here:)
|
|
202
|
+
end
|
|
203
|
+
else
|
|
204
|
+
store_unpersisted_instruction(instructions, append:, cache_until_here:)
|
|
205
|
+
end
|
|
109
206
|
|
|
110
|
-
|
|
207
|
+
sync_messages
|
|
111
208
|
self
|
|
112
209
|
end
|
|
113
210
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
211
|
+
# Chat configuration and callback methods forwarded to the underlying
|
|
212
|
+
# RubyLLM::Chat. Each behaves exactly as documented on RubyLLM::Chat,
|
|
213
|
+
# then returns the record so calls chain.
|
|
214
|
+
CHAINABLE_CHAT_DELEGATES = %i[
|
|
215
|
+
with_tools with_tool_options with_server_tools with_fallbacks with_temperature
|
|
216
|
+
with_max_output_tokens with_thinking with_citations with_caching
|
|
217
|
+
with_end_user with_compaction
|
|
218
|
+
with_provider_options with_headers with_schema
|
|
219
|
+
before_request before_message after_message before_tool_call after_tool_result
|
|
220
|
+
before_fallback after_fallback
|
|
221
|
+
].freeze
|
|
222
|
+
|
|
223
|
+
##
|
|
224
|
+
# :method: with_tools
|
|
225
|
+
# :call-seq: with_tools(*tools)
|
|
226
|
+
#
|
|
227
|
+
# Applies Chat#with_tools and returns this record.
|
|
228
|
+
|
|
229
|
+
##
|
|
230
|
+
# :method: with_tool_options
|
|
231
|
+
# :call-seq: with_tool_options(**options)
|
|
232
|
+
#
|
|
233
|
+
# Applies Chat#with_tool_options and returns this record.
|
|
234
|
+
|
|
235
|
+
##
|
|
236
|
+
# :method: with_server_tools
|
|
237
|
+
# :call-seq: with_server_tools(*tools, **tools_with_options)
|
|
238
|
+
#
|
|
239
|
+
# Applies Chat#with_server_tools and returns this record.
|
|
240
|
+
|
|
241
|
+
##
|
|
242
|
+
# :method: with_fallbacks
|
|
243
|
+
# :call-seq: with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS)
|
|
244
|
+
#
|
|
245
|
+
# Applies Chat#with_fallbacks and returns this record.
|
|
246
|
+
|
|
247
|
+
##
|
|
248
|
+
# :method: with_temperature
|
|
249
|
+
# :call-seq: with_temperature(temperature)
|
|
250
|
+
#
|
|
251
|
+
# Applies Chat#with_temperature and returns this record.
|
|
252
|
+
|
|
253
|
+
##
|
|
254
|
+
# :method: with_max_output_tokens
|
|
255
|
+
# :call-seq: with_max_output_tokens(max_output_tokens)
|
|
256
|
+
#
|
|
257
|
+
# Applies Chat#with_max_output_tokens and returns this record.
|
|
258
|
+
|
|
259
|
+
##
|
|
260
|
+
# :method: with_thinking
|
|
261
|
+
# :call-seq: with_thinking(enabled = true, **options)
|
|
262
|
+
#
|
|
263
|
+
# Applies Chat#with_thinking and returns this record.
|
|
264
|
+
|
|
265
|
+
##
|
|
266
|
+
# :method: with_citations
|
|
267
|
+
# :call-seq: with_citations(enabled = true)
|
|
268
|
+
#
|
|
269
|
+
# Applies Chat#with_citations and returns this record.
|
|
270
|
+
|
|
271
|
+
##
|
|
272
|
+
# :method: with_caching
|
|
273
|
+
# :call-seq: with_caching(options = {})
|
|
274
|
+
#
|
|
275
|
+
# Applies Chat#with_caching and returns this record.
|
|
276
|
+
|
|
277
|
+
##
|
|
278
|
+
# :method: with_end_user
|
|
279
|
+
# :call-seq: with_end_user(end_user)
|
|
280
|
+
#
|
|
281
|
+
# Applies Chat#with_end_user and returns this record.
|
|
282
|
+
|
|
283
|
+
##
|
|
284
|
+
# :method: with_compaction
|
|
285
|
+
# :call-seq: with_compaction(options = {})
|
|
286
|
+
#
|
|
287
|
+
# Applies Chat#with_compaction and returns this record.
|
|
288
|
+
|
|
289
|
+
##
|
|
290
|
+
# :method: with_provider_options
|
|
291
|
+
# :call-seq: with_provider_options(provider_options)
|
|
292
|
+
#
|
|
293
|
+
# Applies Chat#with_provider_options and returns this record.
|
|
294
|
+
|
|
295
|
+
##
|
|
296
|
+
# :method: with_headers
|
|
297
|
+
# :call-seq: with_headers(headers)
|
|
298
|
+
#
|
|
299
|
+
# Applies Chat#with_headers and returns this record.
|
|
300
|
+
|
|
301
|
+
##
|
|
302
|
+
# :method: with_schema
|
|
303
|
+
# :call-seq: with_schema(schema)
|
|
304
|
+
#
|
|
305
|
+
# Applies Chat#with_schema and returns this record.
|
|
306
|
+
|
|
307
|
+
##
|
|
308
|
+
# :method: before_request
|
|
309
|
+
# :call-seq: before_request(&block)
|
|
310
|
+
#
|
|
311
|
+
# Applies Chat#before_request and returns this record.
|
|
312
|
+
|
|
313
|
+
##
|
|
314
|
+
# :method: before_message
|
|
315
|
+
# :call-seq: before_message(&block)
|
|
316
|
+
#
|
|
317
|
+
# Applies Chat#before_message and returns this record.
|
|
318
|
+
|
|
319
|
+
##
|
|
320
|
+
# :method: after_message
|
|
321
|
+
# :call-seq: after_message(&block)
|
|
322
|
+
#
|
|
323
|
+
# Applies Chat#after_message and returns this record.
|
|
324
|
+
|
|
325
|
+
##
|
|
326
|
+
# :method: before_tool_call
|
|
327
|
+
# :call-seq: before_tool_call(&block)
|
|
328
|
+
#
|
|
329
|
+
# Applies Chat#before_tool_call and returns this record.
|
|
330
|
+
|
|
331
|
+
##
|
|
332
|
+
# :method: after_tool_result
|
|
333
|
+
# :call-seq: after_tool_result(&block)
|
|
334
|
+
#
|
|
335
|
+
# Applies Chat#after_tool_result and returns this record.
|
|
336
|
+
|
|
337
|
+
##
|
|
338
|
+
# :method: before_fallback
|
|
339
|
+
# :call-seq: before_fallback(&block)
|
|
340
|
+
#
|
|
341
|
+
# Applies Chat#before_fallback and returns this record.
|
|
342
|
+
|
|
343
|
+
##
|
|
344
|
+
# :method: after_fallback
|
|
345
|
+
# :call-seq: after_fallback(&block)
|
|
346
|
+
#
|
|
347
|
+
# Applies Chat#after_fallback and returns this record.
|
|
348
|
+
|
|
349
|
+
##
|
|
350
|
+
# :method: caching
|
|
351
|
+
# :call-seq: caching
|
|
352
|
+
#
|
|
353
|
+
# Delegates to Chat#caching. See that method for arguments and return values.
|
|
354
|
+
|
|
355
|
+
##
|
|
356
|
+
# :method: citations
|
|
357
|
+
# :call-seq: citations
|
|
358
|
+
#
|
|
359
|
+
# Delegates to Chat#citations. See that method for arguments and return values.
|
|
360
|
+
|
|
361
|
+
##
|
|
362
|
+
# :method: compaction
|
|
363
|
+
# :call-seq: compaction
|
|
364
|
+
#
|
|
365
|
+
# Delegates to Chat#compaction. See that method for arguments and return values.
|
|
366
|
+
|
|
367
|
+
##
|
|
368
|
+
# :method: concurrency
|
|
369
|
+
# :call-seq: concurrency
|
|
370
|
+
#
|
|
371
|
+
# Delegates to Chat#concurrency. See that method for arguments and return values.
|
|
372
|
+
|
|
373
|
+
##
|
|
374
|
+
# :method: end_user
|
|
375
|
+
# :call-seq: end_user
|
|
376
|
+
#
|
|
377
|
+
# Delegates to Chat#end_user. See that method for arguments and return values.
|
|
378
|
+
|
|
379
|
+
##
|
|
380
|
+
# :method: fallbacks
|
|
381
|
+
# :call-seq: fallbacks
|
|
382
|
+
#
|
|
383
|
+
# Delegates to Chat#fallbacks. See that method for arguments and return values.
|
|
384
|
+
|
|
385
|
+
##
|
|
386
|
+
# :method: headers
|
|
387
|
+
# :call-seq: headers
|
|
388
|
+
#
|
|
389
|
+
# Delegates to Chat#headers. See that method for arguments and return values.
|
|
390
|
+
|
|
391
|
+
##
|
|
392
|
+
# :method: max_output_tokens
|
|
393
|
+
# :call-seq: max_output_tokens
|
|
394
|
+
#
|
|
395
|
+
# Delegates to Chat#max_output_tokens. See that method for arguments and return values.
|
|
396
|
+
|
|
397
|
+
##
|
|
398
|
+
# :method: provider_options
|
|
399
|
+
# :call-seq: provider_options
|
|
400
|
+
#
|
|
401
|
+
# Delegates to Chat#provider_options. See that method for arguments and return values.
|
|
402
|
+
|
|
403
|
+
##
|
|
404
|
+
# :method: schema
|
|
405
|
+
# :call-seq: schema
|
|
406
|
+
#
|
|
407
|
+
# Delegates to Chat#schema. See that method for arguments and return values.
|
|
408
|
+
|
|
409
|
+
##
|
|
410
|
+
# :method: server_tools
|
|
411
|
+
# :call-seq: server_tools
|
|
412
|
+
#
|
|
413
|
+
# Delegates to Chat#server_tools. See that method for arguments and return values.
|
|
414
|
+
|
|
415
|
+
##
|
|
416
|
+
# :method: temperature
|
|
417
|
+
# :call-seq: temperature
|
|
418
|
+
#
|
|
419
|
+
# Delegates to Chat#temperature. See that method for arguments and return values.
|
|
420
|
+
|
|
421
|
+
##
|
|
422
|
+
# :method: thinking
|
|
423
|
+
# :call-seq: thinking()
|
|
424
|
+
#
|
|
425
|
+
# Delegates to Chat#thinking. See that method for arguments and return values.
|
|
426
|
+
|
|
427
|
+
##
|
|
428
|
+
# :method: tool_options
|
|
429
|
+
# :call-seq: tool_options()
|
|
430
|
+
#
|
|
431
|
+
# Delegates to Chat#tool_options. See that method for arguments and return values.
|
|
432
|
+
|
|
433
|
+
##
|
|
434
|
+
# :method: tools
|
|
435
|
+
# :call-seq: tools
|
|
436
|
+
#
|
|
437
|
+
# Delegates to Chat#tools. See that method for arguments and return values.
|
|
438
|
+
|
|
439
|
+
CHAINABLE_CHAT_DELEGATES.each do |name|
|
|
440
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
441
|
+
to_llm.public_send(name, *args, **kwargs, &block)
|
|
442
|
+
self
|
|
443
|
+
end
|
|
117
444
|
end
|
|
118
445
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
446
|
+
# Chat values and operations whose return values pass through unchanged.
|
|
447
|
+
#
|
|
448
|
+
# The public methods behave as documented on RubyLLM::Chat.
|
|
449
|
+
|
|
450
|
+
PASSTHROUGH_CHAT_DELEGATES = %i[
|
|
451
|
+
caching citations compaction concurrency end_user fallbacks headers max_output_tokens provider_options
|
|
452
|
+
schema server_tools temperature thinking tool_options tools
|
|
453
|
+
add_completion count_tokens each render
|
|
454
|
+
].freeze
|
|
455
|
+
|
|
456
|
+
##
|
|
457
|
+
# :method: count_tokens
|
|
458
|
+
# :call-seq: count_tokens(message = nil)
|
|
459
|
+
#
|
|
460
|
+
# Returns the number of input tokens the next request would carry,
|
|
461
|
+
# counted by the provider over the persisted conversation.
|
|
462
|
+
|
|
463
|
+
##
|
|
464
|
+
# :method: each
|
|
465
|
+
# :call-seq: each(&block)
|
|
466
|
+
#
|
|
467
|
+
# Yields each message in the conversation. Returns an Enumerator without a block.
|
|
468
|
+
|
|
469
|
+
##
|
|
470
|
+
# :method: render
|
|
471
|
+
# :call-seq: render
|
|
472
|
+
#
|
|
473
|
+
# Returns the next request payload with #before_request hooks applied.
|
|
474
|
+
|
|
475
|
+
PASSTHROUGH_CHAT_DELEGATES.each do |name|
|
|
476
|
+
define_method(name) do |*args, **kwargs, &block|
|
|
477
|
+
to_llm.public_send(name, *args, **kwargs, &block)
|
|
478
|
+
end
|
|
122
479
|
end
|
|
123
480
|
|
|
124
|
-
|
|
481
|
+
private_constant :CHAINABLE_CHAT_DELEGATES, :PASSTHROUGH_CHAT_DELEGATES
|
|
482
|
+
|
|
483
|
+
# Switches the chat to +model_name+, resolving and saving the model
|
|
484
|
+
# record and updating the underlying chat. Falls back to the configured
|
|
485
|
+
# default model when +model_name+ is +nil+. Pass +protocol:+ to override
|
|
486
|
+
# the wire protocol the provider would pick for the model. Returns +self+.
|
|
487
|
+
#
|
|
488
|
+
# chat.with_model 'claude-sonnet-5'
|
|
489
|
+
#
|
|
490
|
+
def with_model(model_name, provider: nil, protocol: nil, assume_model_exists: false)
|
|
491
|
+
model_name ||= (context&.config || RubyLLM.config).default_model
|
|
125
492
|
self.model = model_name
|
|
126
493
|
self.provider = provider if provider
|
|
127
|
-
self.
|
|
128
|
-
|
|
494
|
+
self.protocol = protocol
|
|
495
|
+
self.assume_model_exists = assume_model_exists
|
|
496
|
+
resolve_model
|
|
129
497
|
save!
|
|
130
|
-
to_llm.with_model(
|
|
498
|
+
to_llm.with_model(model_id, provider: provider&.to_sym, protocol:, assume_model_exists:)
|
|
131
499
|
self
|
|
132
500
|
end
|
|
133
501
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
502
|
+
# Persists +message_or_attributes+ as a message record, including any
|
|
503
|
+
# attachments and tool calls. Accepts a RubyLLM::Message, an attributes
|
|
504
|
+
# Hash, or a record responding to +to_llm+.
|
|
505
|
+
# Returns the message record.
|
|
506
|
+
#
|
|
507
|
+
# chat.add_message(role: :user, content: long_context)
|
|
508
|
+
#
|
|
509
|
+
def add_message(message_or_attributes)
|
|
510
|
+
llm_message = message_or_attributes
|
|
511
|
+
llm_message = llm_message.to_llm if llm_message.respond_to?(:to_llm)
|
|
512
|
+
llm_message = RubyLLM::Message.new(llm_message) unless llm_message.is_a?(RubyLLM::Message)
|
|
138
513
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
514
|
+
message_record = messages_association.create!(message_attributes(llm_message))
|
|
515
|
+
|
|
516
|
+
if llm_message.tool_call_id && (tool_call = find_tool_call(llm_message.tool_call_id))
|
|
517
|
+
tool_call.update!(result: message_record)
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
persist_content(message_record, llm_message.attachments) if llm_message.attachments.any?
|
|
521
|
+
persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
|
|
522
|
+
|
|
523
|
+
@chat&.add_message(llm_message)
|
|
524
|
+
|
|
525
|
+
message_record
|
|
142
526
|
end
|
|
143
527
|
|
|
144
|
-
|
|
145
|
-
|
|
528
|
+
# Marks the latest persisted message as a prompt cache boundary, or the
|
|
529
|
+
# latest in-memory message when none is persisted yet. Returns +self+.
|
|
530
|
+
#
|
|
531
|
+
# chat.with_instructions('Reusable analysis prompt').cache_until_here
|
|
532
|
+
#
|
|
533
|
+
# Raises ArgumentError if the chat has no messages.
|
|
534
|
+
def cache_until_here
|
|
535
|
+
message_record = messages_association.order(:id).last
|
|
536
|
+
if message_record
|
|
537
|
+
message_record.cache_until_here
|
|
538
|
+
elsif @chat&.messages&.any?
|
|
539
|
+
@chat.cache_until_here
|
|
540
|
+
else
|
|
541
|
+
raise ArgumentError, 'No messages to cache'
|
|
542
|
+
end
|
|
543
|
+
|
|
146
544
|
self
|
|
147
545
|
end
|
|
148
546
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
547
|
+
# Returns token usage aggregated across every persisted usage entry,
|
|
548
|
+
# including retries and attempts that did not produce a message.
|
|
549
|
+
#
|
|
550
|
+
# chat.tokens.input
|
|
551
|
+
#
|
|
552
|
+
def tokens
|
|
553
|
+
RubyLLM::Tokens.aggregate(ruby_llm_usages.map(&:tokens))
|
|
152
554
|
end
|
|
153
555
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
556
|
+
# Returns a RubyLLM::Cost aggregating every persisted usage entry,
|
|
557
|
+
# including retries and attempts that did not produce a message.
|
|
558
|
+
#
|
|
559
|
+
# chat.cost.total
|
|
560
|
+
#
|
|
561
|
+
def cost
|
|
562
|
+
records = ruby_llm_usages.to_a
|
|
563
|
+
RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?))
|
|
157
564
|
end
|
|
158
565
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
566
|
+
# Persists +message+ as a user message, then runs the conversation loop
|
|
567
|
+
# and returns the latest assistant RubyLLM::Message. The loop pauses
|
|
568
|
+
# when #awaiting_approval? is true. Yields streaming chunks to a block.
|
|
569
|
+
#
|
|
570
|
+
# chat.ask "What is the capital of France?"
|
|
571
|
+
# chat.ask "What's in this file?", with: "diagram.png"
|
|
572
|
+
#
|
|
573
|
+
def ask(message = nil, with: nil, &)
|
|
574
|
+
ask_later(message, with: with)
|
|
575
|
+
complete(&)
|
|
162
576
|
end
|
|
163
577
|
|
|
164
|
-
|
|
165
|
-
|
|
578
|
+
alias say ask
|
|
579
|
+
|
|
580
|
+
# Persists +message+ as a user message without calling the model, so
|
|
581
|
+
# #complete can run later. Returns +self+.
|
|
582
|
+
#
|
|
583
|
+
# chat.ask_later "Summarize this document."
|
|
584
|
+
# chat.complete
|
|
585
|
+
#
|
|
586
|
+
def ask_later(message = nil, with: nil)
|
|
587
|
+
to_llm.raise_if_pending_tool_calls!
|
|
588
|
+
add_message(role: :user, content: message, attachments: with)
|
|
166
589
|
self
|
|
167
590
|
end
|
|
168
591
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
592
|
+
# Makes a single generation attempt, persists the response, and returns it as a
|
|
593
|
+
# RubyLLM::Message. Tool calls in the response are not executed. See
|
|
594
|
+
# RubyLLM::Chat#generate.
|
|
595
|
+
def generate(...)
|
|
596
|
+
to_llm.generate(...)
|
|
597
|
+
rescue *COMPLETION_ERRORS => e
|
|
598
|
+
cleanup_after_failure(e)
|
|
599
|
+
raise
|
|
172
600
|
end
|
|
173
601
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
602
|
+
# Compacts the model context and persists its assistant Message without
|
|
603
|
+
# deleting earlier messages. See RubyLLM::Chat#compact.
|
|
604
|
+
def compact
|
|
605
|
+
to_llm.compact
|
|
606
|
+
rescue *COMPLETION_ERRORS => e
|
|
607
|
+
cleanup_after_failure(e)
|
|
608
|
+
raise
|
|
177
609
|
end
|
|
178
610
|
|
|
179
|
-
|
|
180
|
-
|
|
611
|
+
# Executes the pending tool calls and persists their results without
|
|
612
|
+
# calling the model. See RubyLLM::Chat#run_tools. Returns +self+.
|
|
613
|
+
def run_tools
|
|
614
|
+
to_llm.run_tools
|
|
181
615
|
self
|
|
182
616
|
end
|
|
183
617
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
618
|
+
# Advances the conversation by one move: runs the pending tool calls if
|
|
619
|
+
# there are any, otherwise generates a response. Returns +nil+ once the
|
|
620
|
+
# chat is complete or waiting for approval. See RubyLLM::Chat#step.
|
|
621
|
+
#
|
|
622
|
+
# chat.step until chat.complete? || chat.awaiting_approval?
|
|
623
|
+
#
|
|
624
|
+
def step(...)
|
|
625
|
+
to_llm.step(...)
|
|
626
|
+
rescue *COMPLETION_ERRORS => e
|
|
627
|
+
cleanup_after_failure(e)
|
|
628
|
+
raise
|
|
187
629
|
end
|
|
188
630
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
631
|
+
# Returns whether the conversation has no pending work, neither a
|
|
632
|
+
# response to generate nor tool calls to run. See
|
|
633
|
+
# RubyLLM::Chat#complete?.
|
|
634
|
+
def complete?
|
|
635
|
+
to_llm.complete?
|
|
192
636
|
end
|
|
193
637
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
638
|
+
# Runs the completion loop on the underlying chat, persisting each
|
|
639
|
+
# message, and returns the latest RubyLLM::Message. Pauses when a tool
|
|
640
|
+
# requires approval. When the API call fails, destroys the empty assistant
|
|
641
|
+
# message and any orphaned tool results, then re-raises the error.
|
|
642
|
+
def complete(...)
|
|
643
|
+
to_llm.complete(...)
|
|
644
|
+
rescue *COMPLETION_ERRORS => e
|
|
645
|
+
cleanup_after_failure(e)
|
|
646
|
+
raise
|
|
197
647
|
end
|
|
198
648
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
649
|
+
private
|
|
650
|
+
|
|
651
|
+
def persist_usage_entry(entry)
|
|
652
|
+
tokens = entry.tokens
|
|
653
|
+
cost = entry.cost
|
|
654
|
+
attributes = {
|
|
655
|
+
operation: entry.operation,
|
|
656
|
+
provider: entry.provider,
|
|
657
|
+
model: entry.model,
|
|
658
|
+
status: entry.status,
|
|
659
|
+
input_tokens: tokens.input,
|
|
660
|
+
output_tokens: tokens.output,
|
|
661
|
+
cache_read_tokens: tokens.cache_read,
|
|
662
|
+
cache_write_tokens: tokens.cache_write,
|
|
663
|
+
thinking_tokens: tokens.thinking,
|
|
664
|
+
input_cost: cost.input,
|
|
665
|
+
output_cost: cost.output,
|
|
666
|
+
cache_read_cost: cost.cache_read,
|
|
667
|
+
cache_write_cost: cost.cache_write,
|
|
668
|
+
thinking_cost: cost.thinking,
|
|
669
|
+
total_cost: cost.total
|
|
670
|
+
}
|
|
671
|
+
record = ruby_llm_usages.create!(attributes)
|
|
672
|
+
usage_records_by_entry[entry] = record
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def link_usage_entries(message)
|
|
676
|
+
message.ruby_llm_usage_entries.each do |entry|
|
|
677
|
+
record = usage_records_by_entry[entry]
|
|
678
|
+
record&.update!(message: @message)
|
|
208
679
|
end
|
|
680
|
+
end
|
|
209
681
|
|
|
210
|
-
|
|
211
|
-
|
|
682
|
+
def usage_records_by_entry
|
|
683
|
+
@usage_records_by_entry ||= {}.compare_by_identity
|
|
684
|
+
end
|
|
212
685
|
|
|
213
|
-
|
|
214
|
-
|
|
686
|
+
def resolve_model
|
|
687
|
+
config = context&.config || RubyLLM.config
|
|
688
|
+
@pending_model_id ||= config.default_model unless model
|
|
689
|
+
return unless @pending_model_id
|
|
215
690
|
|
|
216
|
-
|
|
691
|
+
model_info = resolve_model_info
|
|
692
|
+
|
|
693
|
+
self.model = find_or_create_model(model_info)
|
|
694
|
+
@pending_model_id = nil
|
|
695
|
+
@pending_provider = nil
|
|
217
696
|
end
|
|
218
697
|
|
|
219
|
-
def
|
|
220
|
-
|
|
698
|
+
def resolve_model_info
|
|
699
|
+
return find_registered_model unless assume_model_exists
|
|
700
|
+
|
|
701
|
+
raise ArgumentError, 'Provider must be specified if assume_model_exists is true' unless @pending_provider
|
|
702
|
+
|
|
703
|
+
begin
|
|
704
|
+
find_registered_model
|
|
705
|
+
rescue RubyLLM::ModelNotFoundError
|
|
706
|
+
RubyLLM::Model.default(@pending_model_id, @pending_provider)
|
|
707
|
+
end
|
|
221
708
|
end
|
|
222
709
|
|
|
223
|
-
def
|
|
224
|
-
|
|
710
|
+
def find_registered_model
|
|
711
|
+
RubyLLM.models.find(@pending_model_id, provider: @pending_provider, config: context&.config)
|
|
225
712
|
end
|
|
226
713
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
714
|
+
# An empty store would otherwise hold only this chat's model on the next
|
|
715
|
+
# boot, since the registry reads whatever the store returns.
|
|
716
|
+
def load_model_registry_into_store
|
|
717
|
+
RubyLLM::ActiveRecord::Model.save_to_database(RubyLLM.models)
|
|
718
|
+
rescue ::ActiveRecord::RecordInvalid, ::ActiveRecord::RecordNotUnique
|
|
719
|
+
nil
|
|
230
720
|
end
|
|
231
721
|
|
|
232
|
-
|
|
722
|
+
def find_or_create_model(model_info)
|
|
723
|
+
attributes = { model_id: model_info.id, provider: model_info.provider }
|
|
724
|
+
load_model_registry_into_store if RubyLLM::ActiveRecord::Model.none?
|
|
725
|
+
|
|
726
|
+
RubyLLM::ActiveRecord::Model.find_or_create_by!(attributes) do |record|
|
|
727
|
+
record.name = model_info.name || model_info.id
|
|
728
|
+
record.family = model_info.family
|
|
729
|
+
record.model_created_at = model_info.created_at
|
|
730
|
+
record.context_window = model_info.context_window
|
|
731
|
+
record.max_output_tokens = model_info.max_output_tokens
|
|
732
|
+
record.knowledge_cutoff = model_info.knowledge_cutoff
|
|
733
|
+
record.capabilities = model_info.capabilities || []
|
|
734
|
+
record.modalities = model_info.modalities.to_h
|
|
735
|
+
record.pricing = model_info.pricing.to_h
|
|
736
|
+
record.metadata = model_info.metadata || {}
|
|
737
|
+
end
|
|
738
|
+
rescue ::ActiveRecord::RecordInvalid, ::ActiveRecord::RecordNotUnique
|
|
739
|
+
# Another process can insert the row between the lookup and the insert.
|
|
740
|
+
RubyLLM::ActiveRecord::Model.find_by(attributes) || raise
|
|
741
|
+
end
|
|
233
742
|
|
|
234
|
-
def
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
cleanup_failed_messages if @message&.persisted? && @message.content.blank?
|
|
743
|
+
def cleanup_after_failure(error)
|
|
744
|
+
reason = error.is_a?(RubyLLM::CancelledError) ? 'chat cancelled' : 'API call failed'
|
|
745
|
+
cleanup_failed_messages(reason:) if blank_placeholder?
|
|
238
746
|
cleanup_orphaned_tool_results
|
|
239
|
-
raise e
|
|
240
747
|
end
|
|
241
748
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
def cleanup_failed_messages
|
|
245
|
-
RubyLLM.logger.warn "RubyLLM: API call failed, destroying message: #{@message.id}"
|
|
749
|
+
def cleanup_failed_messages(reason:)
|
|
750
|
+
RubyLLM.logger.warn "RubyLLM: #{reason}, destroying message: #{@message.id}"
|
|
246
751
|
@message.destroy
|
|
247
752
|
end
|
|
248
753
|
|
|
249
754
|
def cleanup_orphaned_tool_results # rubocop:disable Metrics/PerceivedComplexity
|
|
250
755
|
messages_association.reload
|
|
251
|
-
last =
|
|
756
|
+
last = eager_load_messages.last
|
|
252
757
|
|
|
253
758
|
return unless last&.tool_call? || last&.tool_result?
|
|
254
759
|
|
|
255
760
|
if last.tool_call?
|
|
256
761
|
last.destroy
|
|
257
762
|
elsif last.tool_result?
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
actual_results = tool_call_message.tool_results.pluck(fk_column)
|
|
763
|
+
parent = last.ruby_llm_parent_tool_call
|
|
764
|
+
tool_call_message = parent.message
|
|
765
|
+
calls = tool_call_message.ruby_llm_tool_calls
|
|
262
766
|
|
|
263
|
-
if
|
|
264
|
-
|
|
767
|
+
if calls.any? { |call| call.result.nil? }
|
|
768
|
+
calls.filter_map(&:result).each(&:destroy)
|
|
265
769
|
tool_call_message.destroy
|
|
266
770
|
end
|
|
267
771
|
end
|
|
268
772
|
end
|
|
269
773
|
|
|
270
|
-
def
|
|
271
|
-
|
|
774
|
+
def eager_load_messages
|
|
775
|
+
assoc = messages_association
|
|
776
|
+
messages = assoc.to_a
|
|
777
|
+
return messages unless assoc.respond_to?(:klass)
|
|
272
778
|
|
|
273
|
-
|
|
274
|
-
|
|
779
|
+
associations = %i[ruby_llm_tool_calls ruby_llm_parent_tool_call ruby_llm_usages]
|
|
780
|
+
associations << { attachments_attachments: :blob } if attachment_association?(assoc.klass)
|
|
275
781
|
|
|
276
|
-
|
|
277
|
-
|
|
782
|
+
::ActiveRecord::Associations::Preloader.new(records: messages, associations: associations).call
|
|
783
|
+
messages
|
|
278
784
|
end
|
|
279
785
|
|
|
280
|
-
def
|
|
281
|
-
|
|
786
|
+
def attachment_association?(message_class)
|
|
787
|
+
message_class.reflect_on_association(:attachments_attachments).present?
|
|
788
|
+
end
|
|
282
789
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
790
|
+
def build_llm_chat
|
|
791
|
+
chat = (context || RubyLLM).chat(
|
|
792
|
+
model: model_id,
|
|
793
|
+
provider: provider&.to_sym,
|
|
794
|
+
protocol: protocol,
|
|
795
|
+
assume_model_exists: assume_model_exists || false
|
|
796
|
+
)
|
|
797
|
+
sync_messages(chat)
|
|
798
|
+
chat.cancellation_checker = proc { consume_persisted_cancellation_request }
|
|
799
|
+
chat.approval_checker = proc { |tool_call| persisted_tool_call_approval(tool_call) }
|
|
800
|
+
install_persistence_callbacks(chat)
|
|
801
|
+
end
|
|
802
|
+
|
|
803
|
+
def record_tool_call_decision(tool_call, decision)
|
|
804
|
+
id = tool_call.respond_to?(:tool_call_id) ? tool_call.tool_call_id : tool_call
|
|
805
|
+
id = id.id if id.respond_to?(:id) && !id.is_a?(String)
|
|
806
|
+
record = find_tool_call(id)
|
|
807
|
+
raise ArgumentError, "Unknown tool call: #{id.inspect}" unless record
|
|
808
|
+
|
|
809
|
+
record.update!(approval: decision)
|
|
810
|
+
self
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
def persisted_tool_call_approval(tool_call)
|
|
814
|
+
record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
|
|
815
|
+
return unless record&.has_attribute?(:approval)
|
|
816
|
+
|
|
817
|
+
case record.approval
|
|
818
|
+
when 'approved' then true
|
|
819
|
+
when 'denied' then false
|
|
286
820
|
end
|
|
821
|
+
end
|
|
287
822
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
823
|
+
def sync_messages(chat = @chat)
|
|
824
|
+
message_records = eager_load_messages
|
|
825
|
+
chat.messages = message_records
|
|
826
|
+
linked_entry_pairs = message_records.zip(chat.messages).flat_map do |record, message|
|
|
827
|
+
record.ruby_llm_usages.zip(message.ruby_llm_usage_entries)
|
|
828
|
+
end
|
|
829
|
+
linked_entries = linked_entry_pairs.to_h { |record, entry| [record.id, entry] }
|
|
830
|
+
chat.usage_entries = ruby_llm_usages.map do |record|
|
|
831
|
+
linked_entries.fetch(record.id) { record.to_entry }
|
|
832
|
+
end
|
|
833
|
+
reapply_runtime_instructions(chat)
|
|
834
|
+
chat
|
|
291
835
|
end
|
|
292
836
|
|
|
293
|
-
def
|
|
294
|
-
|
|
837
|
+
def install_persistence_callbacks(chat)
|
|
838
|
+
chat.before_message { persist_new_message }
|
|
839
|
+
chat.usage_recorder = method(:persist_usage_entry)
|
|
840
|
+
chat.after_message { |msg| persist_message_completion(msg) }
|
|
841
|
+
chat
|
|
842
|
+
end
|
|
295
843
|
|
|
296
|
-
|
|
844
|
+
def clear_persisted_system_instructions
|
|
845
|
+
association = messages_association
|
|
846
|
+
association.where(role: :system).destroy_all
|
|
847
|
+
association.reset
|
|
297
848
|
end
|
|
298
849
|
|
|
299
|
-
def
|
|
850
|
+
def replace_persisted_system_instructions(instructions, cache_until_here:)
|
|
851
|
+
existing = messages_association.where(role: :system).order(:id).to_a
|
|
852
|
+
if existing.one?
|
|
853
|
+
update_persisted_system_instruction(existing.first, instructions, cache_until_here:)
|
|
854
|
+
else
|
|
855
|
+
clear_persisted_system_instructions
|
|
856
|
+
messages_association.create!(role: :system, content: instructions, cache_until_here: cache_until_here)
|
|
857
|
+
end
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
# Rewriting the same instructions every turn would move the system row
|
|
861
|
+
# behind the user messages and rebroadcast it each time.
|
|
862
|
+
def update_persisted_system_instruction(record, instructions, cache_until_here:)
|
|
863
|
+
attributes = { content: instructions }
|
|
864
|
+
attributes[:cache_until_here] = cache_until_here if record.has_attribute?(:cache_until_here)
|
|
865
|
+
changed = attributes.any? { |column, value| record[column] != value }
|
|
866
|
+
record.update!(attributes) if changed
|
|
867
|
+
messages_association.reset
|
|
868
|
+
record
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def persist_system_instruction(instructions, append:, cache_until_here:)
|
|
300
872
|
transaction do
|
|
301
873
|
if append
|
|
302
|
-
messages_association.create!(
|
|
874
|
+
messages_association.create!(
|
|
875
|
+
role: :system,
|
|
876
|
+
content: instructions,
|
|
877
|
+
cache_until_here: cache_until_here
|
|
878
|
+
)
|
|
303
879
|
else
|
|
304
|
-
replace_persisted_system_instructions(instructions)
|
|
880
|
+
replace_persisted_system_instructions(instructions, cache_until_here:)
|
|
305
881
|
end
|
|
306
882
|
end
|
|
307
883
|
end
|
|
308
884
|
|
|
309
|
-
def
|
|
310
|
-
|
|
311
|
-
system_messages + non_system_messages
|
|
885
|
+
def unpersisted_instructions
|
|
886
|
+
@unpersisted_instructions ||= []
|
|
312
887
|
end
|
|
313
888
|
|
|
314
|
-
def
|
|
315
|
-
|
|
316
|
-
|
|
889
|
+
def store_unpersisted_instruction(instructions, append:, cache_until_here:)
|
|
890
|
+
if instructions.nil?
|
|
891
|
+
@unpersisted_instructions = []
|
|
892
|
+
return
|
|
893
|
+
end
|
|
317
894
|
|
|
318
|
-
|
|
895
|
+
entry = [instructions, append, cache_until_here]
|
|
319
896
|
if append
|
|
320
|
-
|
|
897
|
+
unpersisted_instructions << entry
|
|
321
898
|
else
|
|
322
|
-
@
|
|
899
|
+
@unpersisted_instructions = [entry]
|
|
323
900
|
end
|
|
324
901
|
end
|
|
325
902
|
|
|
326
903
|
def reapply_runtime_instructions(chat)
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
chat.with_instructions(first)
|
|
331
|
-
rest.each { |instruction| chat.with_instructions(instruction, append: true) }
|
|
904
|
+
unpersisted_instructions.each do |instructions, append, cache_until_here|
|
|
905
|
+
chat.with_instructions(instructions, append:, cache_until_here:)
|
|
906
|
+
end
|
|
332
907
|
end
|
|
333
908
|
|
|
334
909
|
def persist_new_message
|
|
335
|
-
@message
|
|
910
|
+
@message.destroy if blank_placeholder?
|
|
911
|
+
|
|
912
|
+
attrs = { role: :assistant, content: '' }
|
|
913
|
+
@message = messages_association.create!(attrs)
|
|
914
|
+
end
|
|
915
|
+
|
|
916
|
+
def blank_placeholder?
|
|
917
|
+
return false unless @message&.persisted? && @message.content.blank?
|
|
918
|
+
return false if @message.respond_to?(:attachments) && @message.attachments.attached?
|
|
919
|
+
|
|
920
|
+
!@message.ruby_llm_tool_calls.exists?
|
|
336
921
|
end
|
|
337
922
|
|
|
338
|
-
# rubocop:disable Metrics/PerceivedComplexity
|
|
339
923
|
def persist_message_completion(message)
|
|
340
924
|
return unless message
|
|
341
925
|
|
|
342
|
-
|
|
926
|
+
tool_call = find_tool_call(message.tool_call_id) if message.tool_call_id
|
|
927
|
+
attrs = message_attributes(message)
|
|
343
928
|
|
|
344
929
|
transaction do
|
|
345
|
-
content_text, attachments_to_persist, content_raw = prepare_content_for_storage(message.content)
|
|
346
|
-
|
|
347
|
-
attrs = {
|
|
348
|
-
role: message.role,
|
|
349
|
-
content: content_text,
|
|
350
|
-
input_tokens: message.input_tokens,
|
|
351
|
-
output_tokens: message.output_tokens
|
|
352
|
-
}
|
|
353
|
-
attrs[:cached_tokens] = message.cached_tokens if @message.has_attribute?(:cached_tokens)
|
|
354
|
-
if @message.has_attribute?(:cache_creation_tokens)
|
|
355
|
-
attrs[:cache_creation_tokens] = message.cache_creation_tokens
|
|
356
|
-
end
|
|
357
|
-
attrs[:thinking_text] = message.thinking&.text if @message.has_attribute?(:thinking_text)
|
|
358
|
-
attrs[:thinking_signature] = message.thinking&.signature if @message.has_attribute?(:thinking_signature)
|
|
359
|
-
attrs[:thinking_tokens] = message.thinking_tokens if @message.has_attribute?(:thinking_tokens)
|
|
360
|
-
|
|
361
|
-
# Add model association dynamically
|
|
362
|
-
attrs[self.class.model_association_name] = model_association
|
|
363
|
-
|
|
364
|
-
if tool_call_id
|
|
365
|
-
parent_tool_call_assoc = @message.class.reflect_on_association(:parent_tool_call)
|
|
366
|
-
attrs[parent_tool_call_assoc.foreign_key] = tool_call_id
|
|
367
|
-
end
|
|
368
|
-
|
|
369
930
|
@message.assign_attributes(attrs)
|
|
370
|
-
@message.content_raw = content_raw if @message.respond_to?(:content_raw=)
|
|
371
931
|
@message.save!
|
|
932
|
+
tool_call&.update!(result: @message)
|
|
372
933
|
|
|
373
|
-
persist_content(@message,
|
|
934
|
+
persist_content(@message, message.attachments) if message.attachments.any?
|
|
374
935
|
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
|
936
|
+
link_usage_entries(message)
|
|
375
937
|
end
|
|
376
938
|
end
|
|
377
|
-
# rubocop:enable Metrics/PerceivedComplexity
|
|
378
939
|
|
|
379
|
-
def
|
|
380
|
-
|
|
381
|
-
|
|
940
|
+
def message_attributes(message)
|
|
941
|
+
attrs = { role: message.role, content: message.content }
|
|
942
|
+
assign_supported_attribute(attrs, :thinking_text, message.thinking&.text)
|
|
943
|
+
assign_supported_attribute(attrs, :thinking_signature, message.thinking&.signature)
|
|
944
|
+
assign_supported_attribute(attrs, :citations, message.citations.map(&:to_h).presence)
|
|
945
|
+
assign_supported_attribute(attrs, :server_tool_calls, message.server_tool_calls.map(&:to_h).presence)
|
|
946
|
+
assign_supported_attribute(attrs, :raw_content, message.raw_content)
|
|
947
|
+
assign_supported_attribute(attrs, :raw_reasoning, message.raw_reasoning)
|
|
948
|
+
assign_supported_attribute(attrs, :finish_reason, message.finish_reason)
|
|
949
|
+
assign_supported_attribute(attrs, :cache_until_here, message.cache_until_here?)
|
|
950
|
+
attrs
|
|
951
|
+
end
|
|
382
952
|
|
|
953
|
+
def assign_supported_attribute(attributes, name, value)
|
|
954
|
+
attributes[name] = value if messages_association.klass.column_names.include?(name.to_s)
|
|
955
|
+
end
|
|
956
|
+
|
|
957
|
+
def persist_tool_calls(tool_calls, message_record: @message)
|
|
383
958
|
tool_calls.each_value do |tool_call|
|
|
384
959
|
attributes = tool_call.to_h
|
|
385
|
-
attributes.delete(:thought_signature) unless supports_thought_signature
|
|
386
960
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
387
|
-
message_record.
|
|
961
|
+
message_record.ruby_llm_tool_calls.create!(**attributes)
|
|
388
962
|
end
|
|
389
963
|
end
|
|
390
964
|
|
|
391
|
-
def
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
message_with_tool_call = messages.joins(tool_calls_assoc)
|
|
398
|
-
.find_by(tool_call_table_name => { tool_call_id: tool_call_id })
|
|
399
|
-
return nil unless message_with_tool_call
|
|
400
|
-
|
|
401
|
-
tool_call = message_with_tool_call.tool_calls_association.find_by(tool_call_id: tool_call_id)
|
|
402
|
-
tool_call&.id
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
def persist_content(message_record, attachments)
|
|
406
|
-
return unless message_record.respond_to?(:attachments)
|
|
407
|
-
|
|
408
|
-
attachables = prepare_for_active_storage(attachments)
|
|
409
|
-
message_record.attachments.attach(attachables) if attachables.any?
|
|
410
|
-
end
|
|
411
|
-
|
|
412
|
-
def prepare_for_active_storage(attachments)
|
|
413
|
-
Utils.to_safe_array(attachments).filter_map do |attachment|
|
|
414
|
-
case attachment
|
|
415
|
-
when ActionDispatch::Http::UploadedFile, ActiveStorage::Blob
|
|
416
|
-
attachment
|
|
417
|
-
when ActiveStorage::Attachment, ActiveStorage::Attached::One, ActiveStorage::Attached::Many
|
|
418
|
-
active_storage_blobs(attachment)
|
|
419
|
-
when Hash
|
|
420
|
-
attachment.values.map { |v| prepare_for_active_storage(v) }
|
|
421
|
-
else
|
|
422
|
-
convert_to_active_storage_format(attachment)
|
|
423
|
-
end
|
|
424
|
-
end.flatten.compact
|
|
965
|
+
def find_tool_call(tool_call_id)
|
|
966
|
+
RubyLLM::ActiveRecord::ToolCall.find_by(
|
|
967
|
+
tool_call_id: tool_call_id,
|
|
968
|
+
message_type: self.class.message_class.constantize.polymorphic_name,
|
|
969
|
+
message_id: messages_association.select(:id)
|
|
970
|
+
)
|
|
425
971
|
end
|
|
426
972
|
|
|
427
|
-
def
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
-
}
|
|
973
|
+
def consume_persisted_cancellation_request
|
|
974
|
+
if self[:cancelled]
|
|
975
|
+
clear_cancellation_request
|
|
976
|
+
return :cancelled
|
|
440
977
|
end
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
end
|
|
978
|
+
return unless persisted?
|
|
979
|
+
return unless cancellation_poll_due?
|
|
980
|
+
return unless persisted_cancellation_request?
|
|
445
981
|
|
|
446
|
-
|
|
447
|
-
|
|
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
|
|
982
|
+
clear_cancellation_request
|
|
983
|
+
:cancelled
|
|
452
984
|
end
|
|
453
985
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
986
|
+
# Jobs and requests run with the query cache on, which would replay the
|
|
987
|
+
# first poll's answer for the rest of the run.
|
|
988
|
+
def persisted_cancellation_request?
|
|
989
|
+
self.class.uncached do
|
|
990
|
+
self.class.unscoped.where(self.class.primary_key => id).pick(:cancelled)
|
|
991
|
+
end
|
|
458
992
|
end
|
|
459
993
|
|
|
460
|
-
def
|
|
461
|
-
|
|
994
|
+
def clear_cancellation_request
|
|
995
|
+
self[:cancelled] = false
|
|
996
|
+
self.class.unscoped.where(self.class.primary_key => id).update_all(cancelled: false) if persisted?
|
|
462
997
|
end
|
|
463
998
|
|
|
464
|
-
def
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
content_text = content
|
|
468
|
-
|
|
469
|
-
case content
|
|
470
|
-
when RubyLLM::Content::Raw
|
|
471
|
-
content_raw = content.value
|
|
472
|
-
content_text = nil
|
|
473
|
-
when RubyLLM::Content
|
|
474
|
-
attachments = content.attachments if content.attachments.any?
|
|
475
|
-
content_text = content.text
|
|
476
|
-
when Hash, Array
|
|
477
|
-
content_raw = content
|
|
478
|
-
content_text = nil
|
|
479
|
-
end
|
|
999
|
+
def cancellation_poll_due?
|
|
1000
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1001
|
+
return false if @last_cancellation_poll_at && now - @last_cancellation_poll_at < CANCELLATION_POLL_INTERVAL
|
|
480
1002
|
|
|
481
|
-
|
|
1003
|
+
@last_cancellation_poll_at = now
|
|
1004
|
+
true
|
|
482
1005
|
end
|
|
483
1006
|
end
|
|
484
1007
|
end
|