ruby_llm 2.1.0 → 2.2.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 +162 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- 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_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- 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_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +522 -277
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1598 -332
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +67 -28
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +134 -109
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +23 -2
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +2 -9
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +219 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
require 'active_support/concern'
|
|
4
4
|
require 'ruby_llm/active_record/attachment_helpers'
|
|
5
|
+
require 'ruby_llm/active_record/claim_heartbeat'
|
|
6
|
+
require 'ruby_llm/active_record/run_claims'
|
|
7
|
+
require 'ruby_llm/active_record/chat_approvals'
|
|
8
|
+
require 'ruby_llm/active_record/chat_questions'
|
|
9
|
+
require 'ruby_llm/active_record/chat_cancellation'
|
|
10
|
+
require 'ruby_llm/active_record/chat_placeholders'
|
|
11
|
+
require 'ruby_llm/active_record/chat_reply_error'
|
|
12
|
+
require 'ruby_llm/active_record/system_instructions'
|
|
13
|
+
require 'ruby_llm/active_record/chat_queue'
|
|
14
|
+
require 'ruby_llm/active_record/chat_tool_call_state'
|
|
15
|
+
require 'ruby_llm/active_record/chat_run'
|
|
16
|
+
require 'ruby_llm/active_record/chat_agent_hooks'
|
|
5
17
|
|
|
6
18
|
module RubyLLM
|
|
7
19
|
module ActiveRecord
|
|
@@ -13,22 +25,57 @@ module RubyLLM
|
|
|
13
25
|
# acts_as_chat
|
|
14
26
|
# end
|
|
15
27
|
#
|
|
16
|
-
# chat = Chat.create!(model: 'gpt-
|
|
28
|
+
# chat = Chat.create!(model: 'gpt-6-luna')
|
|
17
29
|
# chat.ask "What is the capital of France?"
|
|
18
30
|
# chat.messages.count # => 2
|
|
19
31
|
#
|
|
20
32
|
module ChatMethods
|
|
21
33
|
extend ActiveSupport::Concern
|
|
22
|
-
include
|
|
34
|
+
include RubyLLM::Support::DeprecatedEnumerable
|
|
23
35
|
include AttachmentHelpers
|
|
36
|
+
include RunClaims
|
|
37
|
+
include ChatApprovals
|
|
38
|
+
include ChatQuestions
|
|
39
|
+
include ChatCancellation
|
|
40
|
+
include ChatPlaceholders
|
|
41
|
+
include ChatReplyError
|
|
42
|
+
include SystemInstructions
|
|
43
|
+
prepend ChatAgentHooks
|
|
24
44
|
|
|
25
45
|
CANCELLATION_POLL_INTERVAL = 1.0 # :nodoc:
|
|
26
46
|
COMPLETION_ERRORS = [ # :nodoc:
|
|
27
|
-
|
|
47
|
+
CancelledError, Error, ConfigurationError, Faraday::Error, Timeout::Error, Errno::ETIMEDOUT
|
|
28
48
|
].freeze
|
|
29
49
|
|
|
50
|
+
##
|
|
51
|
+
# :singleton-method: interrupted
|
|
52
|
+
#
|
|
53
|
+
# Returns the chats whose run died holding its claim: a crash, or a
|
|
54
|
+
# deploy that stopped the process, left the claim behind, and its
|
|
55
|
+
# lease has expired. A run that ends any other way releases its
|
|
56
|
+
# claim, whether it completes, pauses on a question or a task, is
|
|
57
|
+
# cancelled, or fails, so none of those chats are returned, nor a
|
|
58
|
+
# chat whose job hasn't started yet, nor one someone stopped. No job
|
|
59
|
+
# backend runs such a reply again by itself, so a recurring job
|
|
60
|
+
# completes them:
|
|
61
|
+
#
|
|
62
|
+
# Chat.interrupted.find_each { |chat| ChatResponseJob.perform_later(chat.id) }
|
|
63
|
+
#
|
|
64
|
+
# Chains with other scopes. Returns none when the chats table has no
|
|
65
|
+
# claim columns, since then no process records that it runs a chat.
|
|
66
|
+
|
|
30
67
|
included do
|
|
68
|
+
scope :interrupted, -> { RunClaims.interrupted(self) }
|
|
31
69
|
before_save :resolve_model
|
|
70
|
+
after_commit :remember_model_registry_stored, if: -> { @model_registry_loaded_into }
|
|
71
|
+
after_rollback -> { @model_registry_loaded_into = nil }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Returns the record's attributes as a Hash, accepting the same options
|
|
75
|
+
# as any Active Record model, rather than the message list Enumerable
|
|
76
|
+
# would serialize.
|
|
77
|
+
def as_json(options = nil)
|
|
78
|
+
ActiveModel::Serializers::JSON.instance_method(:as_json).bind_call(self, options)
|
|
32
79
|
end
|
|
33
80
|
|
|
34
81
|
# When +true+, skips the model registry lookup so unregistered model ids
|
|
@@ -38,24 +85,27 @@ module RubyLLM
|
|
|
38
85
|
# Overrides the wire protocol the provider would pick for the model, such
|
|
39
86
|
# as +:responses+ or +:chat_completions+ for OpenAI, or +nil+ for the
|
|
40
87
|
# provider default. Not persisted; set it again after reloading the record.
|
|
41
|
-
|
|
88
|
+
attr_reader :protocol
|
|
42
89
|
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
|
|
46
|
-
|
|
90
|
+
# Sets the protocol override, switching the underlying chat to it when
|
|
91
|
+
# that is already built.
|
|
92
|
+
def protocol=(value)
|
|
93
|
+
@protocol = value
|
|
94
|
+
@chat_model_stale = true if @chat
|
|
95
|
+
end
|
|
47
96
|
|
|
48
|
-
#
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
if persisted?
|
|
53
|
-
update_column(:cancelled, true)
|
|
54
|
-
else
|
|
55
|
-
self[:cancelled] = true
|
|
56
|
-
end
|
|
97
|
+
# An optional RubyLLM::Context supplying per-chat configuration. Not
|
|
98
|
+
# persisted; set it again after reloading the record. Assigning one
|
|
99
|
+
# rebinds the underlying chat, like #with_context.
|
|
100
|
+
attr_reader :context
|
|
57
101
|
|
|
58
|
-
|
|
102
|
+
# Requests cancellation of the current in-flight chat operation, or of
|
|
103
|
+
# the run that will answer a message still waiting for one. The request
|
|
104
|
+
# is persisted so a background job can observe it from another process.
|
|
105
|
+
# Does nothing when no run holds the chat and nothing waits to run.
|
|
106
|
+
# Returns +self+.
|
|
107
|
+
def cancel
|
|
108
|
+
request_cancellation
|
|
59
109
|
self
|
|
60
110
|
end
|
|
61
111
|
|
|
@@ -65,56 +115,84 @@ module RubyLLM
|
|
|
65
115
|
@chat&.cancelled? || self[:cancelled]
|
|
66
116
|
end
|
|
67
117
|
|
|
68
|
-
# Records approval for +tool_call+ (
|
|
69
|
-
# an id) on the persisted tool call,
|
|
70
|
-
# from any process.
|
|
118
|
+
# Records approval for +tool_call+ (an approval RubyLLM::Question, a
|
|
119
|
+
# ToolCall, a tool call record, or an id) on the persisted tool call,
|
|
120
|
+
# so the next #complete executes it from any process. Approving a call
|
|
121
|
+
# that is running or answered changes nothing. With
|
|
122
|
+
# <tt>remember: true</tt>, later calls to the same tool run without
|
|
123
|
+
# asking for the rest of the chat, as RubyLLM::Chat#approve describes,
|
|
124
|
+
# in every process. Returns +self+.
|
|
71
125
|
#
|
|
72
126
|
# chat.approve(params[:tool_call_id])
|
|
127
|
+
# chat.approve(params[:tool_call_id], remember: true)
|
|
73
128
|
# CompleteJob.perform_later(chat.id)
|
|
74
129
|
#
|
|
75
|
-
def approve(tool_call)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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')
|
|
130
|
+
def approve(tool_call, remember: false)
|
|
131
|
+
record = tool_call_record(RubyLLM::Question.decided_call(tool_call, 'answer'))
|
|
132
|
+
to_llm.approve(record.to_llm, remember:) if record.result_id.nil?
|
|
133
|
+
record_tool_call_decision(record, remember ? 'remembered' : 'approved')
|
|
134
|
+
self
|
|
85
135
|
end
|
|
86
136
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
# RubyLLM::Chat#awaiting_approval?.
|
|
90
|
-
def awaiting_approval?
|
|
91
|
-
to_llm.awaiting_approval?
|
|
137
|
+
def claiming_delivery(&) # :nodoc:
|
|
138
|
+
exclusively(-> { false }) { transaction(requires_new: true, &) }
|
|
92
139
|
end
|
|
93
140
|
|
|
94
|
-
# Returns
|
|
95
|
-
#
|
|
96
|
-
#
|
|
97
|
-
#
|
|
141
|
+
# Returns whether a process is running this chat's conversation loop:
|
|
142
|
+
# calling the model or running tools from #complete, #step, #generate,
|
|
143
|
+
# #run_tools, or #compact. Reads the database, so it sees runs in
|
|
144
|
+
# other processes. While another process runs the chat, those methods
|
|
145
|
+
# return without doing anything and #waiting? is +true+. A process
|
|
146
|
+
# that dies stops counting as running within a minute.
|
|
98
147
|
#
|
|
99
|
-
#
|
|
148
|
+
# retry_job wait: 1.minute if chat.running?
|
|
100
149
|
#
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
150
|
+
def running?
|
|
151
|
+
return false unless run_claims?
|
|
152
|
+
|
|
153
|
+
claimed_until = self.class.uncached { chat_row.pick(:claimed_until) }
|
|
154
|
+
claimed_until&.future? || false
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Returns whether the conversation can't continue until something
|
|
158
|
+
# outside it happens, as RubyLLM::Chat#waiting? describes, or until
|
|
159
|
+
# the process that is #running? it finishes. Approvals, answers, and
|
|
160
|
+
# tasks persist on their tool calls, so any process sees what the
|
|
161
|
+
# chat waits on.
|
|
162
|
+
def waiting?
|
|
163
|
+
to_llm.waiting? || running_elsewhere?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def messages_association # :nodoc:
|
|
167
|
+
association(messages_association_name).reader
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def find_tool_call(tool_call_id) # :nodoc:
|
|
171
|
+
RubyLLM::ActiveRecord::ToolCall.find_by(
|
|
172
|
+
tool_call_id: tool_call_id,
|
|
173
|
+
message_type: messages_association.klass.polymorphic_name,
|
|
106
174
|
message_id: messages_association.select(:id)
|
|
107
175
|
)
|
|
108
176
|
end
|
|
109
177
|
|
|
110
|
-
def
|
|
111
|
-
|
|
178
|
+
def persist_usage_entry(entry) # :nodoc:
|
|
179
|
+
usage_records_by_entry[entry] = Usage.transaction(requires_new: true) do
|
|
180
|
+
ruby_llm_usages.create!(Usage.attributes_for(entry))
|
|
181
|
+
end
|
|
182
|
+
rescue StandardError => e
|
|
183
|
+
RubyLLM.logger.warn("RubyLLM could not record #{entry.operation} usage: #{e.class}: #{e.message}")
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def refresh_messages # :nodoc:
|
|
187
|
+
messages_association.reset
|
|
188
|
+
reload_usages
|
|
189
|
+
sync_messages if @chat
|
|
112
190
|
end
|
|
113
191
|
|
|
114
192
|
# Sets the chat's model from an id, a RubyLLM::Model value, or the
|
|
115
193
|
# associated internal model record.
|
|
116
194
|
#
|
|
117
|
-
# chat.model = 'gpt-
|
|
195
|
+
# chat.model = 'gpt-6-luna'
|
|
118
196
|
#
|
|
119
197
|
def model=(value)
|
|
120
198
|
if value.is_a?(RubyLLM::ActiveRecord::Model)
|
|
@@ -125,47 +203,59 @@ module RubyLLM
|
|
|
125
203
|
@pending_model_id = value.respond_to?(:id) ? value.id : value
|
|
126
204
|
@pending_provider = value.provider if value.respond_to?(:provider)
|
|
127
205
|
end
|
|
206
|
+
@chat_model_stale = true if @chat
|
|
128
207
|
end
|
|
129
208
|
|
|
130
209
|
# Stores +value+ as the model id, resolved to a model record before save.
|
|
131
210
|
def model_id=(value)
|
|
132
|
-
|
|
211
|
+
self.model = value
|
|
133
212
|
end
|
|
134
213
|
|
|
135
|
-
# Returns the model id
|
|
214
|
+
# Returns the model id assigned since the last save, or that of the
|
|
215
|
+
# associated model record, or +nil+.
|
|
136
216
|
#
|
|
137
|
-
# chat.model_id # => "gpt-
|
|
217
|
+
# chat.model_id # => "gpt-6-luna"
|
|
138
218
|
#
|
|
139
219
|
def model_id
|
|
140
|
-
model&.model_id
|
|
220
|
+
@pending_model_id || model&.model_id
|
|
141
221
|
end
|
|
142
222
|
|
|
143
223
|
# Stores +value+ as the provider used when resolving the model id
|
|
144
224
|
# before save.
|
|
145
225
|
def provider=(value)
|
|
146
226
|
@pending_provider = value
|
|
227
|
+
@chat_model_stale = true if @chat
|
|
147
228
|
end
|
|
148
229
|
|
|
149
|
-
# Returns the provider
|
|
230
|
+
# Returns the provider assigned since the last save, or that of the
|
|
231
|
+
# associated model record, or +nil+. A model id assigned without a
|
|
232
|
+
# provider has none until save resolves it.
|
|
150
233
|
def provider
|
|
151
|
-
model&.provider
|
|
234
|
+
@pending_provider || (model&.provider unless @pending_model_id)
|
|
152
235
|
end
|
|
153
236
|
|
|
154
237
|
# Returns the underlying RubyLLM::Chat for this record, building it on
|
|
155
238
|
# first call and memoizing it. The chat is loaded with the persisted
|
|
156
239
|
# messages and wired to persist new ones. Subsequent calls return the
|
|
157
|
-
# same chat without touching the database
|
|
158
|
-
# message history from the
|
|
240
|
+
# same chat without touching the database, switched to any model
|
|
241
|
+
# assigned since; use #reload to refresh its message history from the
|
|
242
|
+
# record.
|
|
159
243
|
def to_llm
|
|
160
|
-
@chat ||= build_llm_chat
|
|
244
|
+
@chat ||= build_llm_chat
|
|
245
|
+
@chat.with_model(model_id, provider: provider&.to_sym, protocol:, assume_model_exists:) if @chat_model_stale
|
|
246
|
+
@chat_model_stale = false
|
|
247
|
+
@chat
|
|
161
248
|
end
|
|
162
249
|
|
|
163
250
|
# 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
|
|
251
|
+
# underlying chat's model and persisted message history to match. Runtime-only
|
|
252
|
+
# configuration such as tools, temperature, and callbacks is preserved;
|
|
253
|
+
# a model or provider assigned since the last save is discarded.
|
|
166
254
|
# Returns +self+.
|
|
167
255
|
def reload(...)
|
|
168
256
|
super
|
|
257
|
+
@pending_model_id = @pending_provider = nil
|
|
258
|
+
@chat_model_stale = true if @chat && chat_model_moved?
|
|
169
259
|
sync_messages if @chat
|
|
170
260
|
self
|
|
171
261
|
end
|
|
@@ -175,10 +265,11 @@ module RubyLLM
|
|
|
175
265
|
# configuration. The Context itself is runtime-only and is not
|
|
176
266
|
# persisted.
|
|
177
267
|
def with_context(value)
|
|
178
|
-
|
|
268
|
+
@context = value
|
|
179
269
|
@chat&.with_context(value)
|
|
180
270
|
self
|
|
181
271
|
end
|
|
272
|
+
alias context= with_context
|
|
182
273
|
|
|
183
274
|
# Sets the system instructions, persisting them as a message with the
|
|
184
275
|
# +:system+ role. Replaces any persisted system messages unless
|
|
@@ -213,13 +304,13 @@ module RubyLLM
|
|
|
213
304
|
# RubyLLM::Chat. Each behaves exactly as documented on RubyLLM::Chat,
|
|
214
305
|
# then returns the record so calls chain.
|
|
215
306
|
CHAINABLE_CHAT_DELEGATES = %i[
|
|
216
|
-
with_tools with_mcp with_tool_options with_provider_tools with_fallbacks with_temperature
|
|
307
|
+
with_tools with_mcp with_tool_options with_provider_tools with_fallbacks with_temperature
|
|
217
308
|
with_max_output_tokens with_thinking with_citations with_caching
|
|
218
|
-
with_end_user with_compaction
|
|
309
|
+
with_end_user with_owner with_compaction with_context_budget with_code_execution with_skills
|
|
219
310
|
with_provider_options with_headers with_schema
|
|
220
311
|
convert_unsupported_attachments
|
|
221
|
-
before_request before_message after_message before_tool_call after_tool_result
|
|
222
|
-
before_fallback after_fallback
|
|
312
|
+
before_request before_message after_message before_tool_call around_tool_call after_tool_result
|
|
313
|
+
after_tool_progress before_fallback after_fallback around_join after_file_write
|
|
223
314
|
].freeze
|
|
224
315
|
|
|
225
316
|
##
|
|
@@ -234,19 +325,6 @@ module RubyLLM
|
|
|
234
325
|
#
|
|
235
326
|
# Applies Chat#with_mcp and returns this record.
|
|
236
327
|
|
|
237
|
-
##
|
|
238
|
-
# :method: answer
|
|
239
|
-
# :call-seq: answer(request, **values)
|
|
240
|
-
#
|
|
241
|
-
# Applies Chat#answer and returns this record. The answer persists
|
|
242
|
-
# on the tool call, so any process can resume the call.
|
|
243
|
-
|
|
244
|
-
##
|
|
245
|
-
# :method: decline
|
|
246
|
-
# :call-seq: decline(request)
|
|
247
|
-
#
|
|
248
|
-
# Applies Chat#decline and returns this record.
|
|
249
|
-
|
|
250
328
|
##
|
|
251
329
|
# :method: with_tool_options
|
|
252
330
|
# :call-seq: with_tool_options(**options)
|
|
@@ -261,7 +339,7 @@ module RubyLLM
|
|
|
261
339
|
|
|
262
340
|
##
|
|
263
341
|
# :method: with_fallbacks
|
|
264
|
-
# :call-seq: with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS)
|
|
342
|
+
# :call-seq: with_fallbacks(*models, on: Fallback::DEFAULT_ERRORS, drop_unsupported: false)
|
|
265
343
|
#
|
|
266
344
|
# Applies Chat#with_fallbacks and returns this record.
|
|
267
345
|
|
|
@@ -301,12 +379,48 @@ module RubyLLM
|
|
|
301
379
|
#
|
|
302
380
|
# Applies Chat#with_end_user and returns this record.
|
|
303
381
|
|
|
382
|
+
##
|
|
383
|
+
# :method: with_owner
|
|
384
|
+
# :call-seq: with_owner(owner)
|
|
385
|
+
#
|
|
386
|
+
# Applies Chat#with_owner and returns this record. For this record
|
|
387
|
+
# instance, the owner wins over the association +acts_as_chat owner:+
|
|
388
|
+
# names, both for usage and for the files of the Active Storage
|
|
389
|
+
# store. It is not persisted, so a record loaded later works for that
|
|
390
|
+
# association again. Read it with <tt>to_llm.owner</tt>: the record
|
|
391
|
+
# defines no +owner+, so an association of yours by that name keeps
|
|
392
|
+
# working.
|
|
393
|
+
|
|
394
|
+
##
|
|
395
|
+
# :method: with_context_budget
|
|
396
|
+
# :call-seq: with_context_budget(budget = {}, per_result: nil, per_request: nil, window: nil)
|
|
397
|
+
#
|
|
398
|
+
# Applies Chat#with_context_budget and returns this record.
|
|
399
|
+
|
|
304
400
|
##
|
|
305
401
|
# :method: with_compaction
|
|
306
402
|
# :call-seq: with_compaction(options = {})
|
|
307
403
|
#
|
|
308
404
|
# Applies Chat#with_compaction and returns this record.
|
|
309
405
|
|
|
406
|
+
##
|
|
407
|
+
# :method: with_code_execution
|
|
408
|
+
# :call-seq: with_code_execution(enabled = true, **options)
|
|
409
|
+
#
|
|
410
|
+
# Applies Chat#with_code_execution and returns this record.
|
|
411
|
+
|
|
412
|
+
##
|
|
413
|
+
# :method: with_skills
|
|
414
|
+
# :call-seq: with_skills(*skills)
|
|
415
|
+
#
|
|
416
|
+
# Applies Chat#with_skills and returns this record.
|
|
417
|
+
|
|
418
|
+
##
|
|
419
|
+
# :method: after_file_write
|
|
420
|
+
# :call-seq: after_file_write(&block)
|
|
421
|
+
#
|
|
422
|
+
# Applies Chat#after_file_write and returns this record.
|
|
423
|
+
|
|
310
424
|
##
|
|
311
425
|
# :method: with_provider_options
|
|
312
426
|
# :call-seq: with_provider_options(provider_options)
|
|
@@ -361,6 +475,12 @@ module RubyLLM
|
|
|
361
475
|
#
|
|
362
476
|
# Applies Chat#after_tool_result and returns this record.
|
|
363
477
|
|
|
478
|
+
##
|
|
479
|
+
# :method: around_tool_call
|
|
480
|
+
# :call-seq: around_tool_call(&block)
|
|
481
|
+
#
|
|
482
|
+
# Applies Chat#around_tool_call and returns this record.
|
|
483
|
+
|
|
364
484
|
##
|
|
365
485
|
# :method: after_tool_progress
|
|
366
486
|
# :call-seq: after_tool_progress(&block)
|
|
@@ -379,6 +499,25 @@ module RubyLLM
|
|
|
379
499
|
#
|
|
380
500
|
# Applies Chat#after_fallback and returns this record.
|
|
381
501
|
|
|
502
|
+
##
|
|
503
|
+
# :method: around_join
|
|
504
|
+
# :call-seq: around_join(&block)
|
|
505
|
+
#
|
|
506
|
+
# Applies Chat#around_join and returns this record. The block
|
|
507
|
+
# receives the QueuedMessage record and +join+, in the process that
|
|
508
|
+
# runs the chat, inside the transaction that creates the message
|
|
509
|
+
# record and deletes the queued one. <tt>join.call</tt> takes
|
|
510
|
+
# attributes for the new message record and returns it, so the
|
|
511
|
+
# record is saved with your own columns set:
|
|
512
|
+
#
|
|
513
|
+
# chat.around_join do |queued, join|
|
|
514
|
+
# join.call(steered: queued.steer?)
|
|
515
|
+
# end
|
|
516
|
+
#
|
|
517
|
+
# When the block raises, the transaction rolls back and the message
|
|
518
|
+
# stays queued. When it never calls +join+, the queued record is
|
|
519
|
+
# deleted without joining.
|
|
520
|
+
|
|
382
521
|
##
|
|
383
522
|
# :method: caching
|
|
384
523
|
# :call-seq: caching
|
|
@@ -391,12 +530,36 @@ module RubyLLM
|
|
|
391
530
|
#
|
|
392
531
|
# Delegates to Chat#citations. See that method for arguments and return values.
|
|
393
532
|
|
|
533
|
+
##
|
|
534
|
+
# :method: code_execution
|
|
535
|
+
# :call-seq: code_execution
|
|
536
|
+
#
|
|
537
|
+
# Delegates to Chat#code_execution. See that method for arguments and return values.
|
|
538
|
+
|
|
539
|
+
##
|
|
540
|
+
# :method: skills
|
|
541
|
+
# :call-seq: skills
|
|
542
|
+
#
|
|
543
|
+
# Delegates to Chat#skills. See that method for arguments and return values.
|
|
544
|
+
|
|
545
|
+
##
|
|
546
|
+
# :method: file_store
|
|
547
|
+
# :call-seq: file_store
|
|
548
|
+
#
|
|
549
|
+
# Delegates to Chat#file_store. See that method for arguments and return values.
|
|
550
|
+
|
|
394
551
|
##
|
|
395
552
|
# :method: compaction
|
|
396
553
|
# :call-seq: compaction
|
|
397
554
|
#
|
|
398
555
|
# Delegates to Chat#compaction. See that method for arguments and return values.
|
|
399
556
|
|
|
557
|
+
##
|
|
558
|
+
# :method: context_budget
|
|
559
|
+
# :call-seq: context_budget
|
|
560
|
+
#
|
|
561
|
+
# Delegates to Chat#context_budget. See that method for arguments and return values.
|
|
562
|
+
|
|
400
563
|
##
|
|
401
564
|
# :method: concurrency
|
|
402
565
|
# :call-seq: concurrency
|
|
@@ -481,32 +644,6 @@ module RubyLLM
|
|
|
481
644
|
#
|
|
482
645
|
# Delegates to Chat#mcp. See that method for arguments and return values.
|
|
483
646
|
|
|
484
|
-
##
|
|
485
|
-
# :method: waiting?
|
|
486
|
-
# :call-seq: waiting?
|
|
487
|
-
#
|
|
488
|
-
# Delegates to Chat#waiting?. Approvals, answers, and tasks persist on
|
|
489
|
-
# their tool calls, so any process sees what the chat waits on.
|
|
490
|
-
|
|
491
|
-
##
|
|
492
|
-
# :method: awaiting_input?
|
|
493
|
-
# :call-seq: awaiting_input?
|
|
494
|
-
#
|
|
495
|
-
# Delegates to Chat#awaiting_input?. See that method for arguments and return values.
|
|
496
|
-
|
|
497
|
-
##
|
|
498
|
-
# :method: pending_inputs
|
|
499
|
-
# :call-seq: pending_inputs
|
|
500
|
-
#
|
|
501
|
-
# Delegates to Chat#pending_inputs. Requests persist on their tool
|
|
502
|
-
# calls, so they survive restarts.
|
|
503
|
-
|
|
504
|
-
##
|
|
505
|
-
# :method: awaiting_tasks?
|
|
506
|
-
# :call-seq: awaiting_tasks?
|
|
507
|
-
#
|
|
508
|
-
# Delegates to Chat#awaiting_tasks?. See that method for arguments and return values.
|
|
509
|
-
|
|
510
647
|
##
|
|
511
648
|
# :method: pending_tasks
|
|
512
649
|
# :call-seq: pending_tasks
|
|
@@ -526,9 +663,11 @@ module RubyLLM
|
|
|
526
663
|
# The public methods behave as documented on RubyLLM::Chat.
|
|
527
664
|
|
|
528
665
|
PASSTHROUGH_CHAT_DELEGATES = %i[
|
|
529
|
-
caching citations compaction concurrency end_user fallbacks headers
|
|
530
|
-
schema provider_tools temperature thinking tool_options tools deferred_tools
|
|
531
|
-
|
|
666
|
+
caching citations code_execution compaction context_budget concurrency end_user fallbacks headers
|
|
667
|
+
max_output_tokens provider_options schema provider_tools temperature thinking tool_options tools deferred_tools
|
|
668
|
+
mcp skills file_store
|
|
669
|
+
pending_tasks add_completion count_tokens
|
|
670
|
+
render
|
|
532
671
|
].freeze
|
|
533
672
|
|
|
534
673
|
##
|
|
@@ -538,12 +677,6 @@ module RubyLLM
|
|
|
538
677
|
# Returns the number of input tokens the next request would carry,
|
|
539
678
|
# counted by the provider over the persisted conversation.
|
|
540
679
|
|
|
541
|
-
##
|
|
542
|
-
# :method: each
|
|
543
|
-
# :call-seq: each(&block)
|
|
544
|
-
#
|
|
545
|
-
# Yields each message in the conversation. Returns an Enumerator without a block.
|
|
546
|
-
|
|
547
680
|
##
|
|
548
681
|
# :method: render
|
|
549
682
|
# :call-seq: render
|
|
@@ -573,7 +706,7 @@ module RubyLLM
|
|
|
573
706
|
self.assume_model_exists = assume_model_exists
|
|
574
707
|
resolve_model
|
|
575
708
|
save!
|
|
576
|
-
to_llm
|
|
709
|
+
to_llm
|
|
577
710
|
self
|
|
578
711
|
end
|
|
579
712
|
|
|
@@ -585,38 +718,52 @@ module RubyLLM
|
|
|
585
718
|
# chat.add_message(role: :user, content: long_context)
|
|
586
719
|
#
|
|
587
720
|
def add_message(message_or_attributes)
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
llm_message = RubyLLM::Message.new(llm_message) unless llm_message.is_a?(RubyLLM::Message)
|
|
721
|
+
persist_message(message_or_attributes)
|
|
722
|
+
end
|
|
591
723
|
|
|
592
|
-
|
|
724
|
+
# Persists +message_or_attributes+ like #add_message, assigning
|
|
725
|
+
# +record_attributes+ to the new record. Returns the message record.
|
|
726
|
+
def persist_message(message_or_attributes, record_attributes = {}) # :nodoc:
|
|
727
|
+
llm_message = to_llm.prepare_message(message_or_attributes) do |id|
|
|
728
|
+
RubyLLM::ActiveRecord::ToolCall.exists?(tool_call_id: id)
|
|
729
|
+
end
|
|
730
|
+
|
|
731
|
+
message_record = create_message_record(llm_message, record_attributes)
|
|
732
|
+
@last_message_id = message_record.id
|
|
593
733
|
|
|
594
|
-
|
|
734
|
+
tool_call = find_tool_call(llm_message.tool_call_id) if llm_message.tool_call_id
|
|
735
|
+
if tool_call && tool_call.result_id.nil?
|
|
736
|
+
tool_call.mcp_result = llm_message.mcp_result&.dump if tool_call.has_attribute?(:mcp_result)
|
|
595
737
|
tool_call.update!(result: message_record)
|
|
596
738
|
end
|
|
597
739
|
|
|
598
740
|
persist_content(message_record, llm_message.attachments) if llm_message.attachments.any?
|
|
599
741
|
persist_tool_calls(llm_message.tool_calls, message_record:) if llm_message.tool_calls.present?
|
|
600
742
|
|
|
601
|
-
@chat
|
|
743
|
+
@chat.add_message(llm_message)
|
|
602
744
|
|
|
603
|
-
message_record
|
|
745
|
+
bind_message_chat(message_record)
|
|
604
746
|
end
|
|
605
747
|
|
|
606
|
-
#
|
|
607
|
-
#
|
|
608
|
-
#
|
|
748
|
+
# Deprecated: mark the message itself, or pass +cache_until_here:+ to
|
|
749
|
+
# #with_instructions. Marks the latest persisted message as a prompt
|
|
750
|
+
# cache boundary, or the latest in-memory message when none is
|
|
751
|
+
# persisted yet. Warns through RubyLLM.deprecator. RubyLLM 3.0 removes
|
|
752
|
+
# it. Returns +self+.
|
|
609
753
|
#
|
|
610
|
-
# chat.with_instructions('Reusable analysis prompt')
|
|
611
|
-
# chat.
|
|
754
|
+
# chat.with_instructions('Reusable analysis prompt', cache_until_here: { ttl: "1h" })
|
|
755
|
+
# chat.add_message(role: :user, content: long_context).cache_until_here
|
|
612
756
|
#
|
|
613
757
|
# Raises ArgumentError if the chat has no messages.
|
|
614
758
|
def cache_until_here(ttl: nil)
|
|
759
|
+
RubyLLM.deprecator.deprecate("#{self.class}#cache_until_here",
|
|
760
|
+
replacement: 'Message#cache_until_here or with_instructions(cache_until_here:)',
|
|
761
|
+
removal: '3.0')
|
|
615
762
|
message_record = messages_association.order(:id).last
|
|
616
763
|
if message_record
|
|
617
|
-
message_record.cache_until_here(ttl:)
|
|
764
|
+
bind_message_chat(message_record).cache_until_here(ttl:)
|
|
618
765
|
elsif @chat&.messages&.any?
|
|
619
|
-
@chat.cache_until_here(ttl:)
|
|
766
|
+
@chat.messages.last.cache_until_here(ttl:)
|
|
620
767
|
else
|
|
621
768
|
raise ArgumentError, 'No messages to cache'
|
|
622
769
|
end
|
|
@@ -643,9 +790,16 @@ module RubyLLM
|
|
|
643
790
|
RubyLLM::Cost.aggregate(records.map(&:cost), complete: records.all?(&:cost_available?))
|
|
644
791
|
end
|
|
645
792
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
793
|
+
def ruby_llm_usage_entries # :nodoc:
|
|
794
|
+
records = ruby_llm_usages.to_a
|
|
795
|
+
::ActiveRecord::Associations::Preloader.new(records:, associations: [:message, *Usage.owner_preload]).call
|
|
796
|
+
records.map(&:to_entry)
|
|
797
|
+
end
|
|
798
|
+
|
|
799
|
+
# Persists +message+ as a user message, after every queued message
|
|
800
|
+
# (see #queue), then runs the conversation loop and returns the latest
|
|
801
|
+
# assistant RubyLLM::Message. The loop pauses while a call waits on a
|
|
802
|
+
# person (see #pending_questions). Yields streaming chunks to a block.
|
|
649
803
|
#
|
|
650
804
|
# chat.ask "What is the capital of France?"
|
|
651
805
|
# chat.ask "What's in this file?", with: "diagram.png"
|
|
@@ -657,14 +811,39 @@ module RubyLLM
|
|
|
657
811
|
|
|
658
812
|
alias say ask
|
|
659
813
|
|
|
814
|
+
# Deprecated: iterate <tt>to_llm.messages</tt> for the same objects, or
|
|
815
|
+
# the message association for your records. Yields each RubyLLM::Message
|
|
816
|
+
# in the conversation, or returns an Enumerator without a block. The
|
|
817
|
+
# Enumerable methods built on it, such as +map+ and +count+, are
|
|
818
|
+
# deprecated with it. Warns through RubyLLM.deprecator. RubyLLM 3.0
|
|
819
|
+
# removes them.
|
|
820
|
+
def each(&)
|
|
821
|
+
RubyLLM.deprecator.deprecate("Enumerable on #{self.class}", replacement: "#{self.class}#to_llm.messages",
|
|
822
|
+
removal: '3.0')
|
|
823
|
+
to_llm.messages.each(&)
|
|
824
|
+
end
|
|
825
|
+
|
|
660
826
|
# Persists +message+ as a user message without calling the model, so
|
|
661
|
-
# #complete can run later.
|
|
827
|
+
# #complete can run later, usually in a job. Every queued message (see
|
|
828
|
+
# #queue) is persisted first, in order. Returns +self+.
|
|
662
829
|
#
|
|
663
|
-
# chat.ask_later
|
|
664
|
-
# chat.
|
|
830
|
+
# chat.ask_later params[:content]
|
|
831
|
+
# ChatResponseJob.perform_later(chat.id)
|
|
665
832
|
#
|
|
833
|
+
# While a run holds the chat, in this process or another, the message
|
|
834
|
+
# is queued instead, as #queue does, so the transcript that run writes
|
|
835
|
+
# stays whole. The run takes it at its next step, or the next
|
|
836
|
+
# #complete does. An MCP::Prompt, which the queue can't hold, is
|
|
837
|
+
# persisted either way.
|
|
838
|
+
#
|
|
839
|
+
# Raises PendingToolCallsError while the last response has tool calls
|
|
840
|
+
# waiting for their results and no run holds the chat.
|
|
666
841
|
def ask_later(message = nil, with: nil)
|
|
842
|
+
return tap { queue(message, with:) } if running? && !message.is_a?(RubyLLM::MCP::Prompt)
|
|
843
|
+
|
|
667
844
|
to_llm.raise_if_pending_tool_calls!
|
|
845
|
+
to_llm.raise_if_prompt_attachments!(message, with)
|
|
846
|
+
to_llm.take_queued_messages
|
|
668
847
|
if message.is_a?(RubyLLM::MCP::Prompt)
|
|
669
848
|
message.messages.each { |prompt_message| add_message(prompt_message) }
|
|
670
849
|
else
|
|
@@ -675,41 +854,39 @@ module RubyLLM
|
|
|
675
854
|
|
|
676
855
|
# Makes a single generation attempt, persists the response, and returns it as a
|
|
677
856
|
# RubyLLM::Message. Tool calls in the response are not executed. See
|
|
678
|
-
# RubyLLM::Chat#generate.
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
raise
|
|
857
|
+
# RubyLLM::Chat#generate. Returns +nil+ without calling the model
|
|
858
|
+
# while another process is #running? the chat. A block receives the
|
|
859
|
+
# chunks as it does in #complete.
|
|
860
|
+
def generate(&block)
|
|
861
|
+
exclusively(-> {}) { completing(block) { |on_chunk| to_llm.generate(&on_chunk) } }
|
|
684
862
|
end
|
|
685
863
|
|
|
686
864
|
# Compacts the model context and persists its assistant Message without
|
|
687
|
-
# deleting earlier messages. See RubyLLM::Chat#compact.
|
|
865
|
+
# deleting earlier messages. See RubyLLM::Chat#compact. Returns +nil+
|
|
866
|
+
# without calling the model while another process is #running? the
|
|
867
|
+
# chat.
|
|
688
868
|
def compact
|
|
689
|
-
to_llm.compact
|
|
690
|
-
rescue *COMPLETION_ERRORS => e
|
|
691
|
-
cleanup_after_failure(e)
|
|
692
|
-
raise
|
|
869
|
+
exclusively(-> {}) { completing { to_llm.compact } }
|
|
693
870
|
end
|
|
694
871
|
|
|
695
872
|
# Executes the pending tool calls and persists their results without
|
|
696
|
-
# calling the model. See RubyLLM::Chat#run_tools.
|
|
873
|
+
# calling the model. See RubyLLM::Chat#run_tools. Does nothing while
|
|
874
|
+
# another process is #running? the chat. Returns +self+.
|
|
697
875
|
def run_tools
|
|
698
|
-
to_llm.run_tools
|
|
876
|
+
exclusively(-> {}) { to_llm.run_tools }
|
|
699
877
|
self
|
|
700
878
|
end
|
|
701
879
|
|
|
702
880
|
# Advances the conversation by one move: runs the pending tool calls if
|
|
703
881
|
# there are any, otherwise generates a response. Returns +nil+ once the
|
|
704
|
-
# chat is complete or #waiting
|
|
882
|
+
# chat is complete or #waiting?, including while another process is
|
|
883
|
+
# #running? it. See RubyLLM::Chat#step. A block receives the chunks as
|
|
884
|
+
# it does in #complete.
|
|
705
885
|
#
|
|
706
886
|
# chat.step until chat.complete? || chat.waiting?
|
|
707
887
|
#
|
|
708
|
-
def step(
|
|
709
|
-
to_llm.step(
|
|
710
|
-
rescue *COMPLETION_ERRORS => e
|
|
711
|
-
cleanup_after_failure(e)
|
|
712
|
-
raise
|
|
888
|
+
def step(&block)
|
|
889
|
+
exclusively(-> {}) { completing(block) { |on_chunk| to_llm.step(&on_chunk) } }
|
|
713
890
|
end
|
|
714
891
|
|
|
715
892
|
# Returns whether the conversation has no pending work, neither a
|
|
@@ -720,31 +897,159 @@ module RubyLLM
|
|
|
720
897
|
end
|
|
721
898
|
|
|
722
899
|
# Runs the completion loop on the underlying chat, persisting each
|
|
723
|
-
# message, and returns the latest RubyLLM::Message. Pauses
|
|
724
|
-
#
|
|
725
|
-
# message and any orphaned tool results,
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
900
|
+
# message, and returns the latest RubyLLM::Message. Pauses while a call
|
|
901
|
+
# waits on a person (see #pending_questions). When the API call fails,
|
|
902
|
+
# destroys the empty assistant message and any orphaned tool results,
|
|
903
|
+
# then re-raises the error. When the block raises, destroys the empty
|
|
904
|
+
# assistant message and re-raises. Tool calls paused on a task or a
|
|
905
|
+
# question stay, so the next #complete picks them up.
|
|
906
|
+
#
|
|
907
|
+
# Only one process runs a chat at a time. While another process is
|
|
908
|
+
# #running? it, returns the latest message without calling the model
|
|
909
|
+
# or running tools, and #waiting? is +true+. Work recorded while this
|
|
910
|
+
# process ran, such as an approval, continues before it returns. A job
|
|
911
|
+
# that finds the chat running tries again later, which also covers a
|
|
912
|
+
# process that dies, since its claim expires within a minute:
|
|
913
|
+
#
|
|
914
|
+
# def perform(chat_id)
|
|
915
|
+
# chat = ChatAgent.find(chat_id)
|
|
916
|
+
# chat.complete(&:broadcast)
|
|
917
|
+
# retry_job wait: 1.minute if chat.running?
|
|
918
|
+
# end
|
|
919
|
+
#
|
|
920
|
+
# Each chunk passed to the block also answers +message+, the record
|
|
921
|
+
# it streams into, and +broadcast+ (see ChunkMethods). When the
|
|
922
|
+
# messages broadcast with Turbo, the chat's page refreshes once the
|
|
923
|
+
# run ends.
|
|
924
|
+
#
|
|
925
|
+
# Queued messages join as RubyLLM::Chat#complete describes, including
|
|
926
|
+
# those another process queues while this one runs. A message that
|
|
927
|
+
# steers, queued as the run ends, starts another loop before
|
|
928
|
+
# #complete returns. When #cancel stops the run while messages wait
|
|
929
|
+
# in the queue, the run goes on with them before #complete returns,
|
|
930
|
+
# as the next #complete would. It raises CancelledError when none
|
|
931
|
+
# wait, or when the stop came before the run began.
|
|
932
|
+
#
|
|
933
|
+
# When the chats table has a +reply_error+ column, a failed reply
|
|
934
|
+
# saves a short message for the person there before the error is
|
|
935
|
+
# raised, and the next run clears it, so the chat's page can show it
|
|
936
|
+
# whenever it loads. The message comes from I18n:
|
|
937
|
+
# +ruby_llm.reply_error.too_long+ for ContextLengthExceededError,
|
|
938
|
+
# +ruby_llm.reply_error.failed+ for any other error, with English
|
|
939
|
+
# defaults.
|
|
940
|
+
def complete(&block)
|
|
941
|
+
operation = :complete
|
|
942
|
+
loop do
|
|
943
|
+
busy = stopped_before_run = false
|
|
944
|
+
response = exclusively(-> { busy = true }) do
|
|
945
|
+
stopped_before_run = self[:cancelled]
|
|
946
|
+
clear_reply_error
|
|
947
|
+
completing(block) { |on_chunk| to_llm.public_send(operation, &on_chunk) }
|
|
948
|
+
end
|
|
949
|
+
return latest_message if busy
|
|
950
|
+
return response unless work_arrived_during_run?
|
|
951
|
+
|
|
952
|
+
operation = :continue_turn
|
|
953
|
+
rescue CancelledError
|
|
954
|
+
raise if stopped_before_run || ChatQueue.new(self).none?
|
|
955
|
+
|
|
956
|
+
operation = :complete
|
|
957
|
+
end
|
|
958
|
+
rescue StandardError => e
|
|
959
|
+
record_reply_error(e)
|
|
730
960
|
raise
|
|
961
|
+
ensure
|
|
962
|
+
broadcast_refresh_to(self, request_id: nil) if broadcasts_messages?
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
# Queues +message+ in the chat's +ruby_llm_queued_messages+ table, as
|
|
966
|
+
# RubyLLM::Chat#queue describes, and returns the QueuedMessage record.
|
|
967
|
+
# Attachments given with +with:+ are stored with Active Storage and
|
|
968
|
+
# move to the message it becomes. +sender+ is any record, such as the
|
|
969
|
+
# user who asked. It is stored, and when your message model has a
|
|
970
|
+
# +sender+ association that can hold it, the message record is saved
|
|
971
|
+
# with it.
|
|
972
|
+
#
|
|
973
|
+
# Any process can queue a message. The process that runs the chat
|
|
974
|
+
# takes it at its next step; when none is running, run #complete,
|
|
975
|
+
# usually in a job. #ask_later queues for you while a run holds the
|
|
976
|
+
# chat, so reach for #queue when a message should wait for the turn
|
|
977
|
+
# to end (<tt>steer: false</tt>) or keep its +sender+.
|
|
978
|
+
#
|
|
979
|
+
# chat.queue params[:content], sender: Current.user
|
|
980
|
+
# ChatResponseJob.perform_later(chat.id)
|
|
981
|
+
#
|
|
982
|
+
def queue(message = nil, with: nil, steer: true, sender: nil)
|
|
983
|
+
to_llm.queue(message, with:, steer:, sender:)
|
|
984
|
+
end
|
|
985
|
+
|
|
986
|
+
# Returns the QueuedMessage records waiting in the queue, oldest
|
|
987
|
+
# first, ready to render.
|
|
988
|
+
def queued_messages = ruby_llm_queued_messages.scope.skip_query_cache!
|
|
989
|
+
|
|
990
|
+
# Takes +queued+, a QueuedMessage record or its id, out of the queue
|
|
991
|
+
# and deletes its attachments. Returns +true+, or +false+ once the
|
|
992
|
+
# loop has taken it into the conversation or it was withdrawn already.
|
|
993
|
+
# QueuedMessage#withdraw does the same from the record. See
|
|
994
|
+
# RubyLLM::Chat#withdraw.
|
|
995
|
+
def withdraw(queued)
|
|
996
|
+
to_llm.withdraw(queued)
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
# Persists every queued message as a user message record, without
|
|
1000
|
+
# calling the model, and returns the records, oldest first. Each joins
|
|
1001
|
+
# through #around_join and runs #after_message, as
|
|
1002
|
+
# RubyLLM::Chat#join_queued_messages describes.
|
|
1003
|
+
#
|
|
1004
|
+
# chat.join_queued_messages.map(&:content) # => ["Use the EU file"]
|
|
1005
|
+
#
|
|
1006
|
+
def join_queued_messages
|
|
1007
|
+
to_llm.join_queued_messages
|
|
731
1008
|
end
|
|
732
1009
|
|
|
733
1010
|
private
|
|
734
1011
|
|
|
735
|
-
def
|
|
736
|
-
|
|
737
|
-
|
|
1012
|
+
def create_message_record(llm_message, record_attributes)
|
|
1013
|
+
transaction do
|
|
1014
|
+
forget_stale_cancellation if llm_message.role == :user
|
|
1015
|
+
messages_association.create!(message_attributes(llm_message).merge(record_attributes))
|
|
738
1016
|
end
|
|
739
|
-
|
|
740
|
-
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
def broadcasts_messages?
|
|
1020
|
+
message_class = messages_association.klass
|
|
1021
|
+
message_class.include?(MessageBroadcasting) && message_class.broadcasting?
|
|
1022
|
+
end
|
|
1023
|
+
|
|
1024
|
+
def completing(block = nil)
|
|
1025
|
+
yield ChunkMethods.streaming(block, on_error: method(:cleanup_after_block_failure)) { @message }
|
|
1026
|
+
rescue *COMPLETION_ERRORS => e
|
|
1027
|
+
cleanup_after_failure(e)
|
|
1028
|
+
raise
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
def chat_model_moved?
|
|
1032
|
+
[@chat.model.id, @chat.provider.slug] != [model_id, provider.to_s]
|
|
1033
|
+
end
|
|
1034
|
+
|
|
1035
|
+
def latest_message
|
|
1036
|
+
to_llm.messages.reverse.find { |message| message.role != :system } || to_llm.messages.last
|
|
1037
|
+
end
|
|
1038
|
+
|
|
1039
|
+
def bind_message_chat(message)
|
|
1040
|
+
message.association(message.chat_association_name).target = self
|
|
1041
|
+
message
|
|
741
1042
|
end
|
|
742
1043
|
|
|
743
1044
|
def link_usage_entries(message)
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1045
|
+
Usage.transaction(requires_new: true) do
|
|
1046
|
+
message.ruby_llm_usage_entries.each do |entry|
|
|
1047
|
+
record = usage_records_by_entry[entry]
|
|
1048
|
+
record&.update!(message: @message)
|
|
1049
|
+
end
|
|
747
1050
|
end
|
|
1051
|
+
rescue StandardError => e
|
|
1052
|
+
RubyLLM.logger.warn("RubyLLM could not link usage to message #{@message.id}: #{e.class}: #{e.message}")
|
|
748
1053
|
end
|
|
749
1054
|
|
|
750
1055
|
def usage_records_by_entry
|
|
@@ -754,7 +1059,7 @@ module RubyLLM
|
|
|
754
1059
|
def resolve_model
|
|
755
1060
|
config = context&.config || RubyLLM.config
|
|
756
1061
|
@pending_model_id ||= config.default_model unless model
|
|
757
|
-
return unless @pending_model_id
|
|
1062
|
+
return unless @pending_model_id || (@pending_provider && model)
|
|
758
1063
|
|
|
759
1064
|
model_info = resolve_model_info
|
|
760
1065
|
|
|
@@ -771,25 +1076,34 @@ module RubyLLM
|
|
|
771
1076
|
begin
|
|
772
1077
|
find_registered_model
|
|
773
1078
|
rescue RubyLLM::ModelNotFoundError
|
|
774
|
-
RubyLLM::Model.default(
|
|
1079
|
+
RubyLLM::Model.default(model_id, @pending_provider)
|
|
775
1080
|
end
|
|
776
1081
|
end
|
|
777
1082
|
|
|
778
1083
|
def find_registered_model
|
|
779
|
-
RubyLLM.models.
|
|
1084
|
+
RubyLLM.models.find_configured(model_id, provider: @pending_provider, config: context&.config)
|
|
780
1085
|
end
|
|
781
1086
|
|
|
782
1087
|
# An empty store would otherwise hold only this chat's model on the next
|
|
783
1088
|
# boot, since the registry reads whatever the store returns.
|
|
784
1089
|
def load_model_registry_into_store
|
|
785
1090
|
RubyLLM::ActiveRecord::Model.save_to_database(RubyLLM.models)
|
|
1091
|
+
@model_registry_loaded_into = RubyLLM::ActiveRecord::Model.connection_db_config
|
|
786
1092
|
rescue ::ActiveRecord::RecordInvalid, ::ActiveRecord::RecordNotUnique
|
|
787
1093
|
nil
|
|
788
1094
|
end
|
|
789
1095
|
|
|
1096
|
+
def model_store_empty?
|
|
1097
|
+
!RubyLLM::ActiveRecord::Model.registry_stored? && RubyLLM::ActiveRecord::Model.none?
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
def remember_model_registry_stored
|
|
1101
|
+
RubyLLM::ActiveRecord::Model.registry_stored!(@model_registry_loaded_into)
|
|
1102
|
+
end
|
|
1103
|
+
|
|
790
1104
|
def find_or_create_model(model_info)
|
|
791
1105
|
attributes = { model_id: model_info.id, provider: model_info.provider }
|
|
792
|
-
load_model_registry_into_store if
|
|
1106
|
+
load_model_registry_into_store if model_store_empty?
|
|
793
1107
|
|
|
794
1108
|
RubyLLM::ActiveRecord::Model.find_or_create_by!(attributes) do |record|
|
|
795
1109
|
record.name = model_info.name || model_info.id
|
|
@@ -812,6 +1126,11 @@ module RubyLLM
|
|
|
812
1126
|
reason = error.is_a?(RubyLLM::CancelledError) ? 'chat cancelled' : 'API call failed'
|
|
813
1127
|
cleanup_failed_messages(reason:) if blank_placeholder?
|
|
814
1128
|
cleanup_orphaned_tool_results
|
|
1129
|
+
resync_messages if @chat
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
def cleanup_after_block_failure
|
|
1133
|
+
cleanup_failed_messages(reason: 'streaming block failed') if blank_placeholder?
|
|
815
1134
|
end
|
|
816
1135
|
|
|
817
1136
|
def cleanup_failed_messages(reason:)
|
|
@@ -826,13 +1145,13 @@ module RubyLLM
|
|
|
826
1145
|
return unless last&.tool_call? || last&.tool_result?
|
|
827
1146
|
|
|
828
1147
|
if last.tool_call?
|
|
829
|
-
last.destroy
|
|
830
|
-
|
|
1148
|
+
last.destroy unless last.ruby_llm_tool_calls.any?(&:paused?)
|
|
1149
|
+
else
|
|
831
1150
|
parent = last.ruby_llm_parent_tool_call
|
|
832
1151
|
tool_call_message = parent.message
|
|
833
1152
|
calls = tool_call_message.ruby_llm_tool_calls
|
|
834
1153
|
|
|
835
|
-
if calls.any? { |call| call.result.nil? }
|
|
1154
|
+
if calls.any? { |call| call.result.nil? } && calls.none?(&:paused?)
|
|
836
1155
|
calls.filter_map(&:result).each(&:destroy)
|
|
837
1156
|
tool_call_message.destroy
|
|
838
1157
|
end
|
|
@@ -844,7 +1163,7 @@ module RubyLLM
|
|
|
844
1163
|
messages = assoc.to_a
|
|
845
1164
|
return messages unless assoc.respond_to?(:klass)
|
|
846
1165
|
|
|
847
|
-
associations =
|
|
1166
|
+
associations = [:ruby_llm_tool_calls, :ruby_llm_parent_tool_call, { ruby_llm_usages: Usage.owner_preload }]
|
|
848
1167
|
associations << { attachments_attachments: :blob } if attachment_association?(assoc.klass)
|
|
849
1168
|
rich_text_reflection = assoc.klass.reflect_on_association(:rich_text_content)
|
|
850
1169
|
associations << :rich_text_content if rich_text_reflection
|
|
@@ -866,50 +1185,23 @@ module RubyLLM
|
|
|
866
1185
|
assume_model_exists: assume_model_exists || false
|
|
867
1186
|
)
|
|
868
1187
|
sync_messages(chat)
|
|
869
|
-
chat.
|
|
870
|
-
chat.
|
|
871
|
-
chat.
|
|
872
|
-
chat.input_recorder = proc { |tool_call, input| persist_tool_call_input(tool_call, input) }
|
|
1188
|
+
chat.run = ChatRun.new(self)
|
|
1189
|
+
chat.tool_call_state = ChatToolCallState.new(self)
|
|
1190
|
+
chat.message_queue = ChatQueue.new(self)
|
|
873
1191
|
install_persistence_callbacks(chat)
|
|
874
1192
|
end
|
|
875
1193
|
|
|
876
|
-
def record_tool_call_decision(tool_call, decision)
|
|
877
|
-
id = tool_call.respond_to?(:tool_call_id) ? tool_call.tool_call_id : tool_call
|
|
878
|
-
id = id.id if id.respond_to?(:id) && !id.is_a?(String)
|
|
879
|
-
record = find_tool_call(id)
|
|
880
|
-
raise ArgumentError, "Unknown tool call: #{id.inspect}" unless record
|
|
881
|
-
|
|
882
|
-
record.update!(approval: decision)
|
|
883
|
-
self
|
|
884
|
-
end
|
|
885
|
-
|
|
886
|
-
def persisted_tool_call_input(tool_call)
|
|
887
|
-
record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
|
|
888
|
-
record.mcp_state if record&.has_attribute?(:mcp_state)
|
|
889
|
-
end
|
|
890
|
-
|
|
891
|
-
def persist_tool_call_input(tool_call, input)
|
|
892
|
-
record = find_tool_call(tool_call.id)
|
|
893
|
-
record.update!(mcp_state: input) if record&.has_attribute?(:mcp_state)
|
|
894
|
-
end
|
|
895
|
-
|
|
896
|
-
def persisted_tool_call_approval(tool_call)
|
|
897
|
-
record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
|
|
898
|
-
return unless record&.has_attribute?(:approval)
|
|
899
|
-
|
|
900
|
-
case record.approval
|
|
901
|
-
when 'approved' then true
|
|
902
|
-
when 'denied' then false
|
|
903
|
-
end
|
|
904
|
-
end
|
|
905
|
-
|
|
906
1194
|
def sync_messages(chat = @chat)
|
|
1195
|
+
snapshot_run_claim
|
|
907
1196
|
message_records = eager_load_messages
|
|
1197
|
+
@last_message_id = message_records.last&.id
|
|
908
1198
|
chat.messages = message_records
|
|
909
1199
|
linked_entry_pairs = message_records.zip(chat.messages).flat_map do |record, message|
|
|
910
1200
|
record.ruby_llm_usage_records.zip(message.ruby_llm_usage_entries)
|
|
911
1201
|
end
|
|
912
1202
|
linked_entries = linked_entry_pairs.to_h { |record, entry| [record.id, entry] }
|
|
1203
|
+
::ActiveRecord::Associations::Preloader.new(records: ruby_llm_usages.to_a,
|
|
1204
|
+
associations: Usage.owner_preload).call
|
|
913
1205
|
chat.usage_entries = ruby_llm_usages.map do |record|
|
|
914
1206
|
linked_entries.fetch(record.id) { record.to_entry }
|
|
915
1207
|
end
|
|
@@ -919,8 +1211,7 @@ module RubyLLM
|
|
|
919
1211
|
|
|
920
1212
|
def install_persistence_callbacks(chat)
|
|
921
1213
|
chat.before_message { persist_new_message }
|
|
922
|
-
chat.
|
|
923
|
-
chat.after_message { |msg| persist_message_completion(msg) }
|
|
1214
|
+
chat.after_message { |msg| persist_message_completion(msg) unless msg&.role == :user }
|
|
924
1215
|
chat
|
|
925
1216
|
end
|
|
926
1217
|
|
|
@@ -944,9 +1235,7 @@ module RubyLLM
|
|
|
944
1235
|
# Rewriting the same instructions every turn would move the system row
|
|
945
1236
|
# behind the user messages and rebroadcast it each time.
|
|
946
1237
|
def update_persisted_system_instruction(record, instructions, cache_until_here:)
|
|
947
|
-
|
|
948
|
-
boundary.delete(:cache_until_here) unless record.has_attribute?(:cache_until_here)
|
|
949
|
-
attributes = { content: instructions, **boundary }
|
|
1238
|
+
attributes = system_instruction_attributes(record, instructions, cache_until_here)
|
|
950
1239
|
changed = attributes.any? { |column, value| record[column] != value }
|
|
951
1240
|
record.update!(attributes) if changed
|
|
952
1241
|
messages_association.reset
|
|
@@ -998,35 +1287,23 @@ module RubyLLM
|
|
|
998
1287
|
end
|
|
999
1288
|
end
|
|
1000
1289
|
|
|
1001
|
-
def persist_new_message
|
|
1002
|
-
@message.destroy if blank_placeholder?
|
|
1003
|
-
|
|
1004
|
-
attrs = { role: :assistant, content: '' }
|
|
1005
|
-
@message = messages_association.create!(attrs)
|
|
1006
|
-
end
|
|
1007
|
-
|
|
1008
|
-
def blank_placeholder?
|
|
1009
|
-
return false unless @message&.persisted? && @message.content.blank?
|
|
1010
|
-
return false if @message.respond_to?(:attachments) && @message.attachments.attached?
|
|
1011
|
-
|
|
1012
|
-
!@message.ruby_llm_tool_calls.exists?
|
|
1013
|
-
end
|
|
1014
|
-
|
|
1015
1290
|
def persist_message_completion(message)
|
|
1016
1291
|
return unless message
|
|
1017
1292
|
|
|
1018
1293
|
tool_call = find_tool_call(message.tool_call_id) if message.tool_call_id
|
|
1019
1294
|
attrs = message_attributes(message)
|
|
1020
1295
|
|
|
1021
|
-
|
|
1296
|
+
answered = fenced_by_run_claim do
|
|
1022
1297
|
@message.assign_attributes(attrs)
|
|
1023
1298
|
@message.save!
|
|
1024
|
-
tool_call
|
|
1299
|
+
raise ::ActiveRecord::Rollback if tool_call && !tool_result_recorded?(tool_call, message)
|
|
1025
1300
|
|
|
1026
1301
|
persist_content(@message, message.attachments) if message.attachments.any?
|
|
1027
1302
|
persist_tool_calls(message.tool_calls) if message.tool_calls.present?
|
|
1028
1303
|
link_usage_entries(message)
|
|
1304
|
+
true
|
|
1029
1305
|
end
|
|
1306
|
+
discard_superseded_message unless answered
|
|
1030
1307
|
end
|
|
1031
1308
|
|
|
1032
1309
|
def message_attributes(message)
|
|
@@ -1037,16 +1314,18 @@ module RubyLLM
|
|
|
1037
1314
|
assign_supported_attribute(attrs, :server_tool_calls, message.server_tool_calls.map(&:to_h).presence)
|
|
1038
1315
|
assign_supported_attribute(attrs, :raw_content, message.raw_content)
|
|
1039
1316
|
assign_supported_attribute(attrs, :raw_reasoning, message.raw_reasoning)
|
|
1317
|
+
assign_supported_attribute(attrs, :phase, message.phase)
|
|
1318
|
+
assign_supported_attribute(attrs, :continuation, message.continuation)
|
|
1040
1319
|
assign_supported_attribute(attrs, :finish_reason, message.finish_reason)
|
|
1041
1320
|
assign_supported_attribute(attrs, :cache_until_here, message.cache_until_here?)
|
|
1042
1321
|
assign_supported_attribute(attrs, :cache_ttl, message.cache_ttl)
|
|
1322
|
+
assign_fallback_attributes(attrs, message) if message.stood_in?
|
|
1043
1323
|
attrs
|
|
1044
1324
|
end
|
|
1045
1325
|
|
|
1046
|
-
def
|
|
1047
|
-
attributes
|
|
1048
|
-
attributes
|
|
1049
|
-
attributes
|
|
1326
|
+
def assign_fallback_attributes(attributes, message)
|
|
1327
|
+
assign_supported_attribute(attributes, :model_id, message.model_id)
|
|
1328
|
+
assign_supported_attribute(attributes, :provider, message.provider)
|
|
1050
1329
|
end
|
|
1051
1330
|
|
|
1052
1331
|
def assign_supported_attribute(attributes, name, value)
|
|
@@ -1054,53 +1333,19 @@ module RubyLLM
|
|
|
1054
1333
|
end
|
|
1055
1334
|
|
|
1056
1335
|
def persist_tool_calls(tool_calls, message_record: @message)
|
|
1057
|
-
tool_calls.
|
|
1336
|
+
tool_calls.each do |tool_call|
|
|
1058
1337
|
attributes = tool_call.to_h
|
|
1059
1338
|
attributes[:tool_call_id] = attributes.delete(:id)
|
|
1339
|
+
malformed = attributes.delete(:malformed_arguments)
|
|
1340
|
+
attributes[:arguments] = without_nul(malformed) if malformed
|
|
1341
|
+
attributes[:approval] = 'pending' if to_llm.approval_pending?(tool_call)
|
|
1060
1342
|
message_record.ruby_llm_tool_calls.create!(**attributes)
|
|
1061
1343
|
end
|
|
1062
1344
|
end
|
|
1063
1345
|
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
message_type: self.class.message_class.constantize.polymorphic_name,
|
|
1068
|
-
message_id: messages_association.select(:id)
|
|
1069
|
-
)
|
|
1070
|
-
end
|
|
1071
|
-
|
|
1072
|
-
def consume_persisted_cancellation_request
|
|
1073
|
-
if self[:cancelled]
|
|
1074
|
-
clear_cancellation_request
|
|
1075
|
-
return :cancelled
|
|
1076
|
-
end
|
|
1077
|
-
return unless persisted?
|
|
1078
|
-
return unless cancellation_poll_due?
|
|
1079
|
-
return unless persisted_cancellation_request?
|
|
1080
|
-
|
|
1081
|
-
clear_cancellation_request
|
|
1082
|
-
:cancelled
|
|
1083
|
-
end
|
|
1084
|
-
|
|
1085
|
-
# Jobs and requests run with the query cache on, which would replay the
|
|
1086
|
-
# first poll's answer for the rest of the run.
|
|
1087
|
-
def persisted_cancellation_request?
|
|
1088
|
-
self.class.uncached do
|
|
1089
|
-
self.class.unscoped.where(self.class.primary_key => id).pick(:cancelled)
|
|
1090
|
-
end
|
|
1091
|
-
end
|
|
1092
|
-
|
|
1093
|
-
def clear_cancellation_request
|
|
1094
|
-
self[:cancelled] = false
|
|
1095
|
-
self.class.unscoped.where(self.class.primary_key => id).update_all(cancelled: false) if persisted?
|
|
1096
|
-
end
|
|
1097
|
-
|
|
1098
|
-
def cancellation_poll_due?
|
|
1099
|
-
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
1100
|
-
return false if @last_cancellation_poll_at && now - @last_cancellation_poll_at < CANCELLATION_POLL_INTERVAL
|
|
1101
|
-
|
|
1102
|
-
@last_cancellation_poll_at = now
|
|
1103
|
-
true
|
|
1346
|
+
# PostgreSQL jsonb cannot store U+0000.
|
|
1347
|
+
def without_nul(text)
|
|
1348
|
+
text.gsub("\u0000", '\u0000')
|
|
1104
1349
|
end
|
|
1105
1350
|
end
|
|
1106
1351
|
end
|