ruby_llm 2.1.1 → 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 +160 -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 +515 -274
- 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 +1593 -331
- 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 +50 -24
- 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 +127 -106
- 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 +15 -15
- 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 +1 -7
- 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 +5 -14
- 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 +213 -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
|
@@ -7,7 +7,8 @@ module RubyLLM
|
|
|
7
7
|
# Rails integration brings the RubyLLM API to Active Record models, with
|
|
8
8
|
# Active Storage attachments and a persisted message lifecycle for Hotwire
|
|
9
9
|
# streaming and background jobs. Applications own chats and messages;
|
|
10
|
-
# RubyLLM owns the supporting
|
|
10
|
+
# RubyLLM owns the supporting records (see Record), which applications
|
|
11
|
+
# read and query.
|
|
11
12
|
module ActiveRecord
|
|
12
13
|
# Adds the RubyLLM conversation API to application-owned records.
|
|
13
14
|
#
|
|
@@ -17,10 +18,9 @@ module RubyLLM
|
|
|
17
18
|
#
|
|
18
19
|
# class Message < ApplicationRecord
|
|
19
20
|
# acts_as_message
|
|
20
|
-
# has_many_attached :attachments
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
|
-
# chat = Chat.create!(model: "gpt-
|
|
23
|
+
# chat = Chat.create!(model: "gpt-6-luna")
|
|
24
24
|
# response = chat.ask "Summarize this report.", with: report.document
|
|
25
25
|
# response.content
|
|
26
26
|
# chat.cost.total
|
|
@@ -40,7 +40,7 @@ module RubyLLM
|
|
|
40
40
|
##
|
|
41
41
|
# :singleton-method: acts_as_chat
|
|
42
42
|
# :call-seq:
|
|
43
|
-
# acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil)
|
|
43
|
+
# acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, owner: nil)
|
|
44
44
|
#
|
|
45
45
|
# Turns an application record into a persisted RubyLLM chat.
|
|
46
46
|
#
|
|
@@ -50,19 +50,34 @@ module RubyLLM
|
|
|
50
50
|
#
|
|
51
51
|
# Adds the RubyLLM::Chat API (see ChatMethods), a +has_many+
|
|
52
52
|
# association to the messages, a +belongs_to :model+ association to
|
|
53
|
-
# the registry row,
|
|
54
|
-
# the attempts the chat paid for
|
|
53
|
+
# the registry row, a +has_many :ruby_llm_usages+ association over
|
|
54
|
+
# the attempts the chat paid for, a
|
|
55
|
+
# +has_many :ruby_llm_queued_messages+ association over the messages
|
|
56
|
+
# waiting in its queue (see ChatMethods#queue), and a
|
|
57
|
+
# +has_many :ruby_llm_chat_files+ association over its files (see
|
|
58
|
+
# ChatFiles#with_file_store).
|
|
55
59
|
#
|
|
56
60
|
# Options:
|
|
57
61
|
#
|
|
58
62
|
# [+messages:+] Name of the messages association. Defaults to +:messages+.
|
|
59
63
|
# [+message_class:+] Class of the messages. Inferred from the association name.
|
|
60
64
|
# [+messages_foreign_key:+] Foreign key on the messages table. Inferred by Rails.
|
|
65
|
+
# [+owner:+] Name of the association whose record the chat works for,
|
|
66
|
+
# such as +:user+. Every attempt the chat makes is
|
|
67
|
+
# attributed to it in the usage ledger, and the Active
|
|
68
|
+
# Storage file store keeps the chat's files for it (see
|
|
69
|
+
# ChatFiles#with_file_store). Defaults to +nil+, which
|
|
70
|
+
# leaves usage to RubyLLM.with_owner.
|
|
71
|
+
#
|
|
72
|
+
# class Chat < ApplicationRecord
|
|
73
|
+
# belongs_to :user
|
|
74
|
+
# acts_as_chat owner: :user
|
|
75
|
+
# end
|
|
61
76
|
|
|
62
77
|
##
|
|
63
78
|
# :singleton-method: acts_as_message
|
|
64
79
|
# :call-seq:
|
|
65
|
-
# acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false)
|
|
80
|
+
# acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, broadcasts: nil)
|
|
66
81
|
#
|
|
67
82
|
# Turns an application record into a persisted RubyLLM message.
|
|
68
83
|
#
|
|
@@ -71,8 +86,15 @@ module RubyLLM
|
|
|
71
86
|
# end
|
|
72
87
|
#
|
|
73
88
|
# Adds the RubyLLM::Message readers (see MessageMethods), a
|
|
74
|
-
# +belongs_to+ association to the chat,
|
|
75
|
-
#
|
|
89
|
+
# +belongs_to+ association to the chat, the associations behind
|
|
90
|
+
# +tool_calls+, +tokens+, and +cost+ (+has_many :ruby_llm_tool_calls+
|
|
91
|
+
# over the ToolCall rows it requested, +has_one :ruby_llm_parent_tool_call+
|
|
92
|
+
# for the call a tool message answers, and +has_many :ruby_llm_usages+
|
|
93
|
+
# over its Usage rows), and
|
|
94
|
+
# <tt>has_many_attached :attachments</tt>, so Active Storage is
|
|
95
|
+
# required. When turbo-rails and Action Cable are loaded, it also
|
|
96
|
+
# broadcasts the chat's messages with Turbo when
|
|
97
|
+
# Configuration#broadcast_messages is on (see MessageBroadcasting).
|
|
76
98
|
#
|
|
77
99
|
# Options:
|
|
78
100
|
#
|
|
@@ -80,14 +102,21 @@ module RubyLLM
|
|
|
80
102
|
# [+chat_class:+] Class of the chat. Inferred from the association name.
|
|
81
103
|
# [+chat_foreign_key:+] Foreign key on this table. Inferred by Rails.
|
|
82
104
|
# [+touch_chat:+] Touch the chat when a message is saved. Defaults to +false+.
|
|
105
|
+
# [+broadcasts:+] Broadcast messages with Turbo. Defaults to +nil+,
|
|
106
|
+
# which follows Configuration#broadcast_messages.
|
|
107
|
+
# Overrides it with +true+ or +false+.
|
|
83
108
|
|
|
84
109
|
class_methods do # rubocop:disable Metrics/BlockLength
|
|
85
|
-
def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil)
|
|
110
|
+
def acts_as_chat(messages: :messages, message_class: nil, messages_foreign_key: nil, owner: nil)
|
|
86
111
|
include RubyLLM::ActiveRecord::ChatMethods
|
|
112
|
+
include RubyLLM::ActiveRecord::ChatFiles
|
|
113
|
+
singleton_class.prepend RubyLLM::ActiveRecord::ChatColumnGuard
|
|
87
114
|
|
|
88
115
|
class_attribute :messages_association_name, :message_class
|
|
116
|
+
class_attribute :owner_association_name, instance_accessor: false
|
|
89
117
|
self.messages_association_name = messages
|
|
90
118
|
self.message_class = (message_class || messages.to_s.classify).to_s
|
|
119
|
+
self.owner_association_name = owner&.to_sym
|
|
91
120
|
|
|
92
121
|
has_many messages,
|
|
93
122
|
-> { order(:created_at, :id) },
|
|
@@ -105,10 +134,27 @@ module RubyLLM
|
|
|
105
134
|
as: :chat,
|
|
106
135
|
class_name: 'RubyLLM::ActiveRecord::Usage',
|
|
107
136
|
inverse_of: :chat,
|
|
137
|
+
dependent: :nullify
|
|
138
|
+
|
|
139
|
+
has_many :ruby_llm_queued_messages,
|
|
140
|
+
-> { order(:created_at, :id) },
|
|
141
|
+
as: :chat,
|
|
142
|
+
class_name: 'RubyLLM::ActiveRecord::QueuedMessage',
|
|
143
|
+
inverse_of: :chat,
|
|
108
144
|
dependent: :destroy
|
|
145
|
+
|
|
146
|
+
has_many :ruby_llm_chat_files,
|
|
147
|
+
-> { order(:path, :id) },
|
|
148
|
+
as: :chat,
|
|
149
|
+
class_name: 'RubyLLM::ActiveRecord::ChatFile',
|
|
150
|
+
inverse_of: :chat,
|
|
151
|
+
dependent: :destroy
|
|
152
|
+
|
|
153
|
+
RubyLLM::ActiveRecord::QueuedMessage.declare_attachments
|
|
154
|
+
RubyLLM::ActiveRecord::ChatFile.declare_attachments
|
|
109
155
|
end
|
|
110
156
|
|
|
111
|
-
def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false)
|
|
157
|
+
def acts_as_message(chat: :chat, chat_class: nil, chat_foreign_key: nil, touch_chat: false, broadcasts: nil)
|
|
112
158
|
include RubyLLM::ActiveRecord::MessageMethods
|
|
113
159
|
|
|
114
160
|
class_attribute :chat_association_name, :chat_class
|
|
@@ -137,6 +183,27 @@ module RubyLLM
|
|
|
137
183
|
class_name: 'RubyLLM::ActiveRecord::Usage',
|
|
138
184
|
inverse_of: :message,
|
|
139
185
|
dependent: :nullify
|
|
186
|
+
|
|
187
|
+
ruby_llm_attachments
|
|
188
|
+
ruby_llm_broadcasting(broadcasts)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def ruby_llm_attachments
|
|
194
|
+
if respond_to?(:has_many_attached)
|
|
195
|
+
has_many_attached :attachments unless reflect_on_attachment(:attachments)
|
|
196
|
+
else
|
|
197
|
+
RubyLLM.logger.warn "#{name} has no attachments because Active Storage is not loaded. " \
|
|
198
|
+
'Load it with require "active_storage/engine" in config/application.rb, ' \
|
|
199
|
+
'then run bin/rails active_storage:install and bin/rails db:migrate.'
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def ruby_llm_broadcasting(broadcasts)
|
|
204
|
+
class_attribute :ruby_llm_broadcasts, instance_writer: false
|
|
205
|
+
self.ruby_llm_broadcasts = broadcasts
|
|
206
|
+
include RubyLLM::ActiveRecord::MessageBroadcasting if broadcasts || MessageBroadcasting.available?
|
|
140
207
|
end
|
|
141
208
|
end
|
|
142
209
|
end
|
|
@@ -8,7 +8,11 @@ module RubyLLM
|
|
|
8
8
|
private
|
|
9
9
|
|
|
10
10
|
def persist_content(message_record, attachments)
|
|
11
|
-
|
|
11
|
+
unless message_record.respond_to?(:attachments)
|
|
12
|
+
raise RubyLLM::ConfigurationError,
|
|
13
|
+
"#{message_record.class.name} cannot store attachments without Active Storage. " \
|
|
14
|
+
'Load it with require "active_storage/engine", then run bin/rails active_storage:install.'
|
|
15
|
+
end
|
|
12
16
|
|
|
13
17
|
attachables = prepare_for_active_storage(attachments)
|
|
14
18
|
return if attachables.empty?
|
|
@@ -61,7 +65,7 @@ module RubyLLM
|
|
|
61
65
|
io: StringIO.new(attachment.content),
|
|
62
66
|
filename: attachment.filename,
|
|
63
67
|
content_type: attachment.mime_type,
|
|
64
|
-
metadata: { resolution: attachment.resolution&.to_s }.compact
|
|
68
|
+
metadata: { resolution: attachment.resolution&.to_s, processing: attachment.processing&.to_s }.compact
|
|
65
69
|
}
|
|
66
70
|
end
|
|
67
71
|
rescue StandardError => e
|
|
@@ -233,7 +237,8 @@ module RubyLLM
|
|
|
233
237
|
stored = if pending_upload_attachable?(attachable)
|
|
234
238
|
pending_upload_attachment(attachable)
|
|
235
239
|
else
|
|
236
|
-
RubyLLM::Attachment.new(attachment, resolution: attachment.metadata['resolution']&.to_sym
|
|
240
|
+
RubyLLM::Attachment.new(attachment, resolution: attachment.metadata['resolution']&.to_sym,
|
|
241
|
+
processing: attachment.metadata['processing']&.to_sym)
|
|
237
242
|
end
|
|
238
243
|
stored.provider_file_store = ProviderFile.store { attachment.blob&.key }
|
|
239
244
|
stored
|
|
@@ -264,7 +269,8 @@ module RubyLLM
|
|
|
264
269
|
if attachable.is_a?(Hash)
|
|
265
270
|
RubyLLM::Attachment.new(attachment_hash_io(attachable),
|
|
266
271
|
filename: attachment_hash_filename(attachable),
|
|
267
|
-
resolution: attachable.dig(:metadata, :resolution)&.to_sym
|
|
272
|
+
resolution: attachable.dig(:metadata, :resolution)&.to_sym,
|
|
273
|
+
processing: attachable.dig(:metadata, :processing)&.to_sym)
|
|
268
274
|
else
|
|
269
275
|
RubyLLM::Attachment.new(attachable)
|
|
270
276
|
end
|
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
module ActiveRecord
|
|
5
|
-
#
|
|
6
|
-
|
|
5
|
+
# A provider-side batch of persisted chats, in RubyLLM's
|
|
6
|
+
# +ruby_llm_batches+ table. RubyLLM.batch writes a row when it submits
|
|
7
|
+
# persisted chats of one class, and RubyLLM::Batch.find reads it back
|
|
8
|
+
# in any process.
|
|
9
|
+
#
|
|
10
|
+
# RubyLLM::ActiveRecord::Batch.where(completed: false).pluck(:provider_batch_id)
|
|
11
|
+
#
|
|
12
|
+
# Its columns:
|
|
13
|
+
#
|
|
14
|
+
# [+provider_batch_id+] The provider's id for the batch.
|
|
15
|
+
# [+provider+] The provider's slug.
|
|
16
|
+
# [+status+] <tt>"pending"</tt>, <tt>"succeeded"</tt>, <tt>"failed"</tt>, or <tt>"cancelled"</tt>.
|
|
17
|
+
# [+raw_status+] The status as the provider last reported it.
|
|
18
|
+
# [+completed+] Whether the provider has finished the batch.
|
|
19
|
+
# [+request_counts+] The provider's request counts, as a Hash.
|
|
20
|
+
# [+reported_cost+] The cost the provider reported, as a Hash.
|
|
21
|
+
# [+chat_type+, +chat_ids+] The class and ids of the submitted chats.
|
|
22
|
+
#
|
|
23
|
+
# RubyLLM writes these rows. Read and query them, and add scopes and
|
|
24
|
+
# columns of your own, but do not create or update them.
|
|
25
|
+
# Use RubyLLM::Batch to refresh a batch and deliver its results.
|
|
26
|
+
class Batch < Record
|
|
7
27
|
self.table_name = 'ruby_llm_batches'
|
|
8
28
|
|
|
9
|
-
STATUSES = %w[pending succeeded failed cancelled].freeze
|
|
29
|
+
STATUSES = %w[pending succeeded failed cancelled].freeze # :nodoc:
|
|
10
30
|
|
|
11
31
|
validates :provider_batch_id, :provider, :status, presence: true
|
|
12
32
|
validates :status, inclusion: { in: STATUSES }
|
|
13
33
|
|
|
14
34
|
class << self
|
|
15
|
-
def persist(batch, chats)
|
|
35
|
+
def persist(batch, chats) # :nodoc:
|
|
16
36
|
records = Array(chats)
|
|
17
37
|
return unless table_exists?
|
|
18
38
|
return unless persistable_chats?(records)
|
|
@@ -31,11 +51,11 @@ module RubyLLM
|
|
|
31
51
|
)
|
|
32
52
|
end
|
|
33
53
|
|
|
34
|
-
def fetch(id, provider: nil, context: nil)
|
|
54
|
+
def fetch(id, provider: nil, context: nil) # :nodoc:
|
|
35
55
|
find_record(id, provider:)&.to_llm(context:, store: self)
|
|
36
56
|
end
|
|
37
57
|
|
|
38
|
-
def sync(batch)
|
|
58
|
+
def sync(batch) # :nodoc:
|
|
39
59
|
find_record(batch.id, provider: batch.provider)&.sync_from(batch)
|
|
40
60
|
end
|
|
41
61
|
|
|
@@ -58,6 +78,8 @@ module RubyLLM
|
|
|
58
78
|
end
|
|
59
79
|
end
|
|
60
80
|
|
|
81
|
+
# Returns the submitted chat records in submission order, with +nil+
|
|
82
|
+
# for a chat that no longer exists.
|
|
61
83
|
def chats
|
|
62
84
|
klass = chat_type.constantize
|
|
63
85
|
by_id = klass.where(klass.primary_key => Array(chat_ids))
|
|
@@ -65,12 +87,15 @@ module RubyLLM
|
|
|
65
87
|
Array(chat_ids).map { |id| by_id[id] }
|
|
66
88
|
end
|
|
67
89
|
|
|
68
|
-
def to_llm(context: nil, store: nil)
|
|
90
|
+
def to_llm(context: nil, store: nil) # :nodoc:
|
|
69
91
|
config = context&.config || RubyLLM.config
|
|
70
92
|
provider_instance = RubyLLM::Provider.resolve!(provider).new(config)
|
|
93
|
+
records = chats
|
|
71
94
|
RubyLLM::Batch.new(
|
|
72
95
|
provider: provider_instance,
|
|
73
|
-
chats:
|
|
96
|
+
chats: records.map { |chat| chat&.to_llm },
|
|
97
|
+
delivered: records.each_index.select { |index| answered?(records[index]) },
|
|
98
|
+
delivery: ->(index, failed: false, &deliver) { deliver_once(records[index], failed:, &deliver) },
|
|
74
99
|
id: provider_batch_id,
|
|
75
100
|
raw_status: raw_status,
|
|
76
101
|
completed: completed,
|
|
@@ -81,7 +106,7 @@ module RubyLLM
|
|
|
81
106
|
)
|
|
82
107
|
end
|
|
83
108
|
|
|
84
|
-
def sync_from(batch)
|
|
109
|
+
def sync_from(batch) # :nodoc:
|
|
85
110
|
attributes = {
|
|
86
111
|
status: batch.status,
|
|
87
112
|
raw_status: batch.raw_status,
|
|
@@ -92,6 +117,39 @@ module RubyLLM
|
|
|
92
117
|
attributes[:reported_cost] = batch.reported_cost.to_h if batch.reported_cost
|
|
93
118
|
update!(attributes)
|
|
94
119
|
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# Two pollers can load a batch before either delivers, so the check
|
|
124
|
+
# is repeated once this one holds the chat.
|
|
125
|
+
def deliver_once(chat, failed: false)
|
|
126
|
+
return yield.then { true } unless chat
|
|
127
|
+
|
|
128
|
+
chat.claiming_delivery do
|
|
129
|
+
yield unless failed ? billed_failure?(chat) : answered?(chat)
|
|
130
|
+
true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A failed slot adds no message, so its usage row is what marks it.
|
|
135
|
+
def billed_failure?(chat)
|
|
136
|
+
usages = chat.ruby_llm_usages
|
|
137
|
+
usages.klass.uncached do
|
|
138
|
+
usages.where(status: 'failed').where(usages.klass.arel_table[:created_at].gt(created_at)).exists?
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The chat was awaiting the model when the batch was submitted, so an
|
|
143
|
+
# assistant message newer than the batch answers that turn, whatever
|
|
144
|
+
# the chat went on to stage after it.
|
|
145
|
+
def answered?(chat)
|
|
146
|
+
return false unless chat
|
|
147
|
+
|
|
148
|
+
messages = chat.messages_association
|
|
149
|
+
messages.klass.uncached do
|
|
150
|
+
messages.where(role: :assistant).where(messages.klass.arel_table[:created_at].gt(created_at)).exists?
|
|
151
|
+
end
|
|
152
|
+
end
|
|
95
153
|
end
|
|
96
154
|
end
|
|
97
155
|
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# What the Agent that configured a persisted chat adds to its
|
|
6
|
+
# operations: its rescue_from handlers, and one +agent.ruby_llm+ event
|
|
7
|
+
# around each run, shared with the chat the record wraps.
|
|
8
|
+
module ChatAgentHooks # :nodoc:
|
|
9
|
+
include RubyLLM::Chat::AgentRun
|
|
10
|
+
include RubyLLM::Chat::Rescuable
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def agent_run_chat = @chat
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Keeps approval decisions on tool call records, read outside the query
|
|
6
|
+
# cache, so every process decides tool calls the same way. A call
|
|
7
|
+
# approved with <tt>remember: true</tt> is recorded as +remembered+,
|
|
8
|
+
# and later calls to the same tool in the chat run without asking.
|
|
9
|
+
# A call saved while it requires approval is recorded as +pending+, so
|
|
10
|
+
# pending approvals are read from the rows without building the chat's
|
|
11
|
+
# tools.
|
|
12
|
+
module ChatApprovals # :nodoc:
|
|
13
|
+
def persisted_tool_call_approval(tool_call) # :nodoc:
|
|
14
|
+
record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(tool_call.id) }
|
|
15
|
+
return unless record&.has_attribute?(:approval)
|
|
16
|
+
|
|
17
|
+
case record.approval
|
|
18
|
+
when 'approved', 'remembered' then true
|
|
19
|
+
when 'denied' then false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def persisted_remembered_tool?(call) # :nodoc:
|
|
24
|
+
return false unless persisted? && row_supports_approval?
|
|
25
|
+
|
|
26
|
+
RubyLLM::ActiveRecord::ToolCall.uncached do
|
|
27
|
+
identity = tool_identity.to_h { |column| [column, call.public_send(column)] }
|
|
28
|
+
chat_tool_calls.where(**identity, approval: 'remembered').exists?
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def persisted_pending_approvals
|
|
35
|
+
pending = chat_tool_calls.where(approval: 'pending', result_id: nil)
|
|
36
|
+
remembered = RubyLLM::ActiveRecord::ToolCall.uncached do
|
|
37
|
+
chat_tool_calls.where(approval: 'remembered').distinct.pluck(*tool_identity)
|
|
38
|
+
end
|
|
39
|
+
remembered.reduce(pending) do |calls, identity|
|
|
40
|
+
calls.where.not(id: pending.where(tool_identity.zip(Array(identity)).to_h).select(:id))
|
|
41
|
+
end.order(:created_at, :id).skip_query_cache!
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def tool_identity
|
|
45
|
+
RubyLLM::ActiveRecord::ToolCall.column_names.include?('namespace') ? %i[namespace name] : %i[name]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def chat_tool_calls
|
|
49
|
+
RubyLLM::ActiveRecord::ToolCall.where(
|
|
50
|
+
message_type: messages_association.klass.polymorphic_name,
|
|
51
|
+
message_id: messages_association.select(:id)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def record_tool_call_decision(tool_call, decision)
|
|
56
|
+
record = tool_call_record(tool_call)
|
|
57
|
+
decide_persisted_tool_call(record, decision)
|
|
58
|
+
record
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def tool_call_record(tool_call)
|
|
62
|
+
id = tool_call.respond_to?(:tool_call_id) ? tool_call.tool_call_id : tool_call
|
|
63
|
+
id = id.id if id.respond_to?(:id) && !id.is_a?(String)
|
|
64
|
+
record = RubyLLM::ActiveRecord::ToolCall.uncached { find_tool_call(id) }
|
|
65
|
+
raise ArgumentError, "Unknown tool call: #{id.inspect}" unless record
|
|
66
|
+
|
|
67
|
+
record
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Reads a cancellation request another process persisted on the chat
|
|
6
|
+
# record, at most once per poll interval, and clears it once observed.
|
|
7
|
+
# A request is for the run holding the chat, or for the run about to
|
|
8
|
+
# answer what waits. New work recorded while no run holds the chat, and
|
|
9
|
+
# a run that finds nothing waiting, clear a request that is left over.
|
|
10
|
+
module ChatCancellation # :nodoc:
|
|
11
|
+
def poll_run_cancellation # :nodoc:
|
|
12
|
+
renew_run_claim
|
|
13
|
+
consume_persisted_cancellation_request
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def forget_stale_cancellation # :nodoc:
|
|
17
|
+
return if @run_claim_token || !run_claims? || !has_attribute?(:cancelled)
|
|
18
|
+
|
|
19
|
+
requested = unclaimed(chat_row.where(cancelled: true), Time.current)
|
|
20
|
+
remember_cancellation(false) if requested.update_all(cancelled: false) == 1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def request_cancellation
|
|
26
|
+
return request_run_cancellation if run_claims?
|
|
27
|
+
|
|
28
|
+
persisted? ? update_column(:cancelled, true) : self[:cancelled] = true
|
|
29
|
+
@chat&.cancel
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The conditional update can't land after the run's release, which
|
|
33
|
+
# clears the request, so it can't outlive the run it was meant for.
|
|
34
|
+
def request_run_cancellation
|
|
35
|
+
claimed_until = chat_row.arel_table[:claimed_until]
|
|
36
|
+
if chat_row.where(claimed_until.gt(Time.current)).update_all(cancelled: true) == 1
|
|
37
|
+
remember_cancellation(true)
|
|
38
|
+
@chat&.cancel if @run_claim_token
|
|
39
|
+
elsif !nothing_to_stop?
|
|
40
|
+
update_column(:cancelled, true)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def nothing_to_stop?
|
|
45
|
+
return false unless ChatQueue.new(self).none?
|
|
46
|
+
|
|
47
|
+
messages_association.klass.uncached do
|
|
48
|
+
latest = messages_association.where.not(role: :system).reorder(created_at: :desc, id: :desc).first
|
|
49
|
+
latest.nil? || finished_reply?(latest)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def finished_reply?(message)
|
|
54
|
+
message.role.to_s == 'assistant' && message.content.present? &&
|
|
55
|
+
!unfinished_record?(message) && !message.tool_call?
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def settle_cancellation_request
|
|
59
|
+
return unless @run_claim_token && has_attribute?(:cancelled)
|
|
60
|
+
|
|
61
|
+
remember_cancellation(self.class.uncached { chat_row.pick(:cancelled) })
|
|
62
|
+
clear_cancellation_request if self[:cancelled] && to_llm.complete? && ChatQueue.new(self).none?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def remember_cancellation(requested)
|
|
66
|
+
self[:cancelled] = requested
|
|
67
|
+
clear_attribute_changes([:cancelled])
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def consume_persisted_cancellation_request
|
|
71
|
+
if self[:cancelled]
|
|
72
|
+
clear_cancellation_request
|
|
73
|
+
return :cancelled
|
|
74
|
+
end
|
|
75
|
+
return unless persisted?
|
|
76
|
+
return unless cancellation_poll_due?
|
|
77
|
+
return unless persisted_cancellation_request?
|
|
78
|
+
|
|
79
|
+
clear_cancellation_request
|
|
80
|
+
:cancelled
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Jobs and requests run with the query cache on, which would replay the
|
|
84
|
+
# first poll's answer for the rest of the run.
|
|
85
|
+
def persisted_cancellation_request?
|
|
86
|
+
self.class.uncached do
|
|
87
|
+
self.class.unscoped.where(self.class.primary_key => id).pick(:cancelled)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def clear_cancellation_request
|
|
92
|
+
self[:cancelled] = false
|
|
93
|
+
self.class.unscoped.where(self.class.primary_key => id).update_all(cancelled: false) if persisted?
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def cancellation_poll_due?
|
|
97
|
+
now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
98
|
+
interval = ChatMethods::CANCELLATION_POLL_INTERVAL
|
|
99
|
+
return false if @last_cancellation_poll_at && now - @last_cancellation_poll_at < interval
|
|
100
|
+
|
|
101
|
+
@last_cancellation_poll_at = now
|
|
102
|
+
true
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# Refuses a chat table with a column, or a chat model with an
|
|
6
|
+
# association, that a RubyLLM reader on the record would hide, such as
|
|
7
|
+
# a +temperature+ column or a +tools+ association: reading it would
|
|
8
|
+
# return RubyLLM's setting instead of the stored value. The check runs
|
|
9
|
+
# when the class loads its columns, so it needs no database at boot. A
|
|
10
|
+
# reader hides a column or an association when it comes from
|
|
11
|
+
# ChatMethods or ChatFiles, or a module they bring along, such as
|
|
12
|
+
# Enumerable.
|
|
13
|
+
module ChatColumnGuard # :nodoc:
|
|
14
|
+
OWNERS = (ChatMethods.ancestors | ChatFiles.ancestors).freeze
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def load_schema!
|
|
19
|
+
super
|
|
20
|
+
hidden = columns_hash.keys.select { |column| hidden_by_ruby_llm?(column) }
|
|
21
|
+
raise ArgumentError, hidden_columns_message(hidden) if hidden.any?
|
|
22
|
+
|
|
23
|
+
associations = reflect_on_all_associations.select do |association|
|
|
24
|
+
association.name.to_s != messages_association_name.to_s && hidden_by_ruby_llm?(association.name)
|
|
25
|
+
end
|
|
26
|
+
raise ArgumentError, hidden_associations_message(associations) if associations.any?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def hidden_by_ruby_llm?(name)
|
|
30
|
+
method_defined?(name) && OWNERS.include?(instance_method(name).owner)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def hidden_columns_message(hidden)
|
|
34
|
+
columns = hidden.map { |column| "#{table_name}.#{column}" }.join(', ')
|
|
35
|
+
readers = hidden.map { |column| "#{name}##{column}" }.join(', ')
|
|
36
|
+
them = hidden.one? ? 'it' : 'them'
|
|
37
|
+
"#{columns} cannot be read on #{name}: acts_as_chat defines #{readers}, which would hide #{them}. " \
|
|
38
|
+
"Rename the column in a migration, such as rename_column :#{table_name}, :#{hidden.first}, " \
|
|
39
|
+
":#{hidden.first}_setting."
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def hidden_associations_message(associations)
|
|
43
|
+
names = associations.map { |association| "#{association.macro} :#{association.name}" }.join(', ')
|
|
44
|
+
readers = associations.map { |association| "#{name}##{association.name}" }.join(', ')
|
|
45
|
+
them = associations.one? ? 'it' : 'them'
|
|
46
|
+
"#{names} cannot be read on #{name}: acts_as_chat defines #{readers}, which would hide #{them}. " \
|
|
47
|
+
"Rename #{them}, with class_name: and foreign_key: options that keep the same table and columns."
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# A file in a persisted chat's FileStore, in RubyLLM's
|
|
6
|
+
# +ruby_llm_chat_files+ table, with its content in Active Storage. It
|
|
7
|
+
# keeps the text a converter made of the file, so each version is
|
|
8
|
+
# converted once.
|
|
9
|
+
#
|
|
10
|
+
# chat.ruby_llm_chat_files.pluck(:path) # => ["contract.pdf", "results/search-call_1.json"]
|
|
11
|
+
#
|
|
12
|
+
# Rows with an +owner+ are seen only through a store for that owner
|
|
13
|
+
# (see ChatFiles#with_file_store); rows without one are seen by every
|
|
14
|
+
# store of the chat.
|
|
15
|
+
class ChatFile < Record
|
|
16
|
+
self.table_name = 'ruby_llm_chat_files'
|
|
17
|
+
|
|
18
|
+
belongs_to :chat, polymorphic: true
|
|
19
|
+
belongs_to :owner, polymorphic: true, optional: true
|
|
20
|
+
|
|
21
|
+
# Declares the content attachment when Active Storage is loaded.
|
|
22
|
+
# Called by +acts_as_chat+, since Active Storage loads after
|
|
23
|
+
# RubyLLM's records.
|
|
24
|
+
def self.declare_attachments # :nodoc:
|
|
25
|
+
return unless respond_to?(:has_one_attached) && !reflect_on_attachment(:file)
|
|
26
|
+
|
|
27
|
+
has_one_attached :file
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the file as a RubyLLM::StoredFile.
|
|
31
|
+
def to_stored_file
|
|
32
|
+
RubyLLM::StoredFile.new(path:, byte_size:, content_type:, digest:, updated_at:,
|
|
33
|
+
owner: (owner if owner_id))
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module ActiveRecord
|
|
5
|
+
# The file store of a persisted chat, added by +acts_as_chat+ with
|
|
6
|
+
# ChatMethods: the chat's files live in +ruby_llm_chat_files+, with
|
|
7
|
+
# their content in Active Storage.
|
|
8
|
+
module ChatFiles
|
|
9
|
+
# Applies Chat#with_file_store and returns this record. Without a
|
|
10
|
+
# +store+, the chat keeps its files in Active Storage, in
|
|
11
|
+
# +ruby_llm_chat_files+ (see ActiveRecord::FileStore), for the
|
|
12
|
+
# chat's owner, the record <tt>acts_as_chat owner:</tt> names: that
|
|
13
|
+
# store writes files for the owner and sees only their files and the
|
|
14
|
+
# files without an owner. Pass +nil+ to remove the store.
|
|
15
|
+
#
|
|
16
|
+
# chat.with_file_store
|
|
17
|
+
# chat.with_file_store("tmp/chats/42")
|
|
18
|
+
# chat.with_file_store(:memory)
|
|
19
|
+
# chat.with_file_store(nil)
|
|
20
|
+
#
|
|
21
|
+
# :call-seq:
|
|
22
|
+
# with_file_store(store = active_storage_store, inline_limit: 10_000, tools: %i[read write edit])
|
|
23
|
+
def with_file_store(store = RubyLLM::Chat::FileReferences::DEFAULT_STORE, **)
|
|
24
|
+
store = FileStore.new(self) if store.equal?(RubyLLM::Chat::FileReferences::DEFAULT_STORE)
|
|
25
|
+
to_llm.with_file_store(store, **)
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|