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
data/lib/ruby_llm/rerank.rb
CHANGED
|
@@ -13,6 +13,7 @@ module RubyLLM
|
|
|
13
13
|
class Rerank
|
|
14
14
|
include Support::Inspectable
|
|
15
15
|
include Accounting::Usage::Result
|
|
16
|
+
include Accounting::Usage::ModelInfo
|
|
16
17
|
|
|
17
18
|
# One reranked document: its +index+ in the input array, the
|
|
18
19
|
# +document+ text, and the provider's relevance +score+.
|
|
@@ -27,11 +28,12 @@ module RubyLLM
|
|
|
27
28
|
# The raw provider response body.
|
|
28
29
|
attr_reader :raw
|
|
29
30
|
|
|
30
|
-
def initialize(results:, model:, raw: nil, input_tokens: nil, reported_cost: nil) # :nodoc:
|
|
31
|
+
def initialize(results:, model:, raw: nil, input_tokens: nil, search_units: nil, reported_cost: nil) # :nodoc:
|
|
31
32
|
@results = results
|
|
32
33
|
@model = model
|
|
33
34
|
@raw = raw
|
|
34
35
|
@input_tokens = input_tokens
|
|
36
|
+
@search_units = search_units
|
|
35
37
|
@reported_cost = reported_cost
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -39,7 +41,7 @@ module RubyLLM
|
|
|
39
41
|
def tokens
|
|
40
42
|
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
41
43
|
|
|
42
|
-
Tokens.new(input: @input_tokens, reported_cost: @reported_cost)
|
|
44
|
+
Tokens.new(input: @input_tokens, search_units: @search_units, reported_cost: @reported_cost)
|
|
43
45
|
end
|
|
44
46
|
|
|
45
47
|
# Returns the rerank cost across every provider attempt.
|
|
@@ -49,14 +51,6 @@ module RubyLLM
|
|
|
49
51
|
Cost.new(tokens:, model: model_info, category: :embeddings)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
|
-
attr_writer :model_info # :nodoc:
|
|
53
|
-
|
|
54
|
-
def model_info # :nodoc:
|
|
55
|
-
@model_info ||= RubyLLM.models.find(model)
|
|
56
|
-
rescue ModelNotFoundError
|
|
57
|
-
nil
|
|
58
|
-
end
|
|
59
|
-
|
|
60
54
|
def inspect_attributes # :nodoc:
|
|
61
55
|
{ model: model, results: results.length }
|
|
62
56
|
end
|
|
@@ -67,7 +61,7 @@ module RubyLLM
|
|
|
67
61
|
# limits how many results come back. +provider_options:+ merges
|
|
68
62
|
# options into the request in the provider's vocabulary.
|
|
69
63
|
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
70
|
-
# over RubyLLM.
|
|
64
|
+
# over RubyLLM.with_owner.
|
|
71
65
|
#
|
|
72
66
|
# RubyLLM.rerank "what is ruby", docs, model: "voyageai/rerank-2.5-lite", provider: :openrouter
|
|
73
67
|
#
|
|
@@ -80,32 +74,11 @@ module RubyLLM
|
|
|
80
74
|
provider_options: {},
|
|
81
75
|
metadata: nil,
|
|
82
76
|
owner: nil)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
provider: provider_instance.slug,
|
|
89
|
-
provider_class: provider_instance.name,
|
|
90
|
-
model: model.id,
|
|
91
|
-
model_info: model,
|
|
92
|
-
query: query,
|
|
93
|
-
document_count: documents.length,
|
|
94
|
-
top_n: top_n,
|
|
95
|
-
provider_options: provider_options,
|
|
96
|
-
metadata: metadata,
|
|
97
|
-
tokens: empty_tokens,
|
|
98
|
-
cost: Cost.new(tokens: empty_tokens, model:, category: :embeddings)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
RubyLLM.instrument('rerank.ruby_llm', payload, config: config) do |event|
|
|
102
|
-
result = Accounting::Usage.owned_by(owner) do
|
|
103
|
-
provider_instance.rerank(query, documents, model:, top_n:, provider_options:)
|
|
104
|
-
end
|
|
105
|
-
event[:result] = result
|
|
106
|
-
event[:tokens] = result.tokens
|
|
107
|
-
event[:cost] = result.cost
|
|
108
|
-
result
|
|
77
|
+
Accounting::Operation.new(
|
|
78
|
+
'rerank', model, category: :embeddings, provider:, assume_model_exists:, context:, owner:
|
|
79
|
+
).run(query:, document_count: documents.length, top_n:, provider_options:,
|
|
80
|
+
metadata:) do |provider_instance, model|
|
|
81
|
+
provider_instance.rerank(query, documents, model:, top_n:, provider_options:)
|
|
109
82
|
end
|
|
110
83
|
end
|
|
111
84
|
end
|
|
@@ -11,6 +11,7 @@ module RubyLLM
|
|
|
11
11
|
#
|
|
12
12
|
class ResearchJob
|
|
13
13
|
include Support::Inspectable
|
|
14
|
+
include Support::DeprecatedJobPredicates
|
|
14
15
|
|
|
15
16
|
class DeadlineExpired < StandardError; end # :nodoc:
|
|
16
17
|
private_constant :DeadlineExpired
|
|
@@ -70,7 +71,7 @@ module RubyLLM
|
|
|
70
71
|
# +provider_tools:+ accepts an array of aliases or a Hash of aliases
|
|
71
72
|
# and their options, as on Chat#with_provider_tools. The job records
|
|
72
73
|
# its usage, attributed to +owner:+ or the owner of
|
|
73
|
-
# RubyLLM.
|
|
74
|
+
# RubyLLM.with_owner, when it finishes.
|
|
74
75
|
def self.research_later(prompt, provider:, agent:, with: nil, provider_tools: nil,
|
|
75
76
|
context: nil, provider_options: {}, metadata: nil, owner: nil)
|
|
76
77
|
config = context&.config || RubyLLM.config
|
|
@@ -128,6 +129,7 @@ module RubyLLM
|
|
|
128
129
|
@agent = agent
|
|
129
130
|
@protocol = protocol
|
|
130
131
|
@usage_owner = Accounting::Usage.owner
|
|
132
|
+
@usage_workflow = Accounting::Usage.workflow
|
|
131
133
|
apply_state(state)
|
|
132
134
|
end
|
|
133
135
|
|
|
@@ -135,10 +137,10 @@ module RubyLLM
|
|
|
135
137
|
def pending? = status == :pending
|
|
136
138
|
|
|
137
139
|
# Returns whether the task reached any terminal state.
|
|
138
|
-
def
|
|
140
|
+
def complete? = !pending?
|
|
139
141
|
|
|
140
142
|
# Returns whether the task finished with a complete report.
|
|
141
|
-
def
|
|
143
|
+
def succeeded? = status == :completed
|
|
142
144
|
|
|
143
145
|
# Returns whether the provider stopped before completing the report.
|
|
144
146
|
def incomplete? = status == :incomplete
|
|
@@ -153,7 +155,7 @@ module RubyLLM
|
|
|
153
155
|
# task finishes. +timeout:+ limits this request in seconds.
|
|
154
156
|
def refresh(timeout: nil)
|
|
155
157
|
self.class.validate_polling_options(timeout, 1) unless timeout.nil?
|
|
156
|
-
unless
|
|
158
|
+
unless complete?
|
|
157
159
|
state = request_with_timeout(timeout) { @protocol.refresh_research_job(self, timeout:) }
|
|
158
160
|
apply_state(state)
|
|
159
161
|
end
|
|
@@ -166,7 +168,7 @@ module RubyLLM
|
|
|
166
168
|
def wait(timeout: 600, interval: 5)
|
|
167
169
|
self.class.validate_polling_options(timeout, interval)
|
|
168
170
|
deadline = monotonic_time + timeout
|
|
169
|
-
until
|
|
171
|
+
until complete?
|
|
170
172
|
remaining = deadline - monotonic_time
|
|
171
173
|
raise TimeoutError.new("Research timed out (job #{id})", job: self) unless remaining.positive?
|
|
172
174
|
|
|
@@ -182,7 +184,7 @@ module RubyLLM
|
|
|
182
184
|
# can confirm cancellation; this does not delete stored task data.
|
|
183
185
|
def cancel(timeout: 5)
|
|
184
186
|
self.class.validate_polling_options(timeout, 1)
|
|
185
|
-
unless
|
|
187
|
+
unless complete?
|
|
186
188
|
state = request_with_timeout(timeout) { @protocol.cancel_research_job(self, timeout:) }
|
|
187
189
|
apply_state(state)
|
|
188
190
|
end
|
|
@@ -212,19 +214,29 @@ module RubyLLM
|
|
|
212
214
|
def request_with_timeout(timeout, &)
|
|
213
215
|
Timeout.timeout(timeout, DeadlineExpired, &)
|
|
214
216
|
rescue DeadlineExpired, Faraday::TimeoutError => e
|
|
215
|
-
raise
|
|
217
|
+
raise request_timed_out, cause: e
|
|
216
218
|
rescue Error
|
|
217
219
|
raise
|
|
220
|
+
rescue ConnectionError => e
|
|
221
|
+
raise e.timeout? ? request_timed_out : request_failed(e), cause: e
|
|
218
222
|
rescue RubyLLM::Error, Faraday::Error => e
|
|
219
|
-
|
|
223
|
+
raise request_failed(e), cause: e
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def request_timed_out
|
|
227
|
+
TimeoutError.new("Research request timed out (job #{id})", job: self)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def request_failed(error)
|
|
231
|
+
response = error.response if error.respond_to?(:response)
|
|
220
232
|
@raw = response.body if response.respond_to?(:body)
|
|
221
|
-
|
|
233
|
+
Error.new("Research request failed: #{error.message} (job #{id})", job: self, response:)
|
|
222
234
|
end
|
|
223
235
|
|
|
224
|
-
#
|
|
225
|
-
#
|
|
236
|
+
# Each object records its job's usage once: when it first sees the job
|
|
237
|
+
# finished, at construction or on a later refresh.
|
|
226
238
|
def apply_state(state)
|
|
227
|
-
observed_finish = pending? && state.fetch(:status) != :pending
|
|
239
|
+
observed_finish = (@status.nil? || pending?) && state.fetch(:status) != :pending
|
|
228
240
|
@status = state.fetch(:status)
|
|
229
241
|
@raw = state[:raw]
|
|
230
242
|
@error = state[:error]
|
|
@@ -236,7 +248,8 @@ module RubyLLM
|
|
|
236
248
|
def record_usage
|
|
237
249
|
status = { completed: :succeeded, incomplete: :succeeded, cancelled: :cancelled }.fetch(@status, :failed)
|
|
238
250
|
entry = Accounting::Usage::Entry.new(operation: :research, provider:, model: agent, status:, tokens:, cost:,
|
|
239
|
-
owner: @usage_owner
|
|
251
|
+
owner: @usage_owner,
|
|
252
|
+
workflow: @usage_workflow)
|
|
240
253
|
Accounting::Usage.report(entry, config: @protocol.config)
|
|
241
254
|
end
|
|
242
255
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sandbox
|
|
5
|
+
# Rewrites a policy for Landlock, which grants whole directory trees
|
|
6
|
+
# and cannot take part of one away. Each readable or writable path that
|
|
7
|
+
# holds a denied one is replaced by the entries around it, down to the
|
|
8
|
+
# denied path, so the policy hides the same paths without denying
|
|
9
|
+
# anything inside a granted tree. Entries added later to a directory
|
|
10
|
+
# granted entry by entry stay hidden. A writable path cannot be split
|
|
11
|
+
# that way, since creating files needs the directory itself, so a
|
|
12
|
+
# denied path inside one raises ConfigurationError. Paths compare by
|
|
13
|
+
# where they really lead, since that is what Landlock grants.
|
|
14
|
+
module Landlock # :nodoc:
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
def policy(sandbox)
|
|
18
|
+
denied, readable, writable = [sandbox.denied, sandbox.readable, sandbox.writable].map do |paths|
|
|
19
|
+
paths.map { |path| real(path) }
|
|
20
|
+
end
|
|
21
|
+
refuse_writable(writable, denied)
|
|
22
|
+
sandbox.dup
|
|
23
|
+
.with_readable(nil).with_readable(*around(readable, denied))
|
|
24
|
+
.with_writable(nil).with_writable(*around(writable, denied))
|
|
25
|
+
.with_denied(nil)
|
|
26
|
+
.with_denied(*denied.reject { |path| (readable + writable).any? { |root| inside?(path, root) } })
|
|
27
|
+
.with_backend(:landlock)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def refuse_writable(writable, denied)
|
|
31
|
+
writable.each do |path|
|
|
32
|
+
hidden = denied.find { |candidate| candidate != path && inside?(candidate, path) }
|
|
33
|
+
next unless hidden
|
|
34
|
+
|
|
35
|
+
raise ConfigurationError, "Landlock cannot hide #{hidden} inside the writable #{path}. " \
|
|
36
|
+
'Leave it out of the writable path, or run on Bubblewrap'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def around(paths, denied)
|
|
41
|
+
paths.flat_map { |path| entries(path, denied) }.uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def entries(path, denied)
|
|
45
|
+
return [] if hidden?(path, denied)
|
|
46
|
+
|
|
47
|
+
return [path] if denied.none? { |hidden| inside?(hidden, path) }
|
|
48
|
+
return [] unless File.directory?(path)
|
|
49
|
+
|
|
50
|
+
Dir.children(path).sort.flat_map { |child| entries(File.join(path, child), denied) }
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def hidden?(path, denied)
|
|
54
|
+
denied.any? { |hidden| inside?(path, hidden) } || reaches?(path, denied)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def reaches?(path, denied)
|
|
58
|
+
return false unless File.symlink?(path)
|
|
59
|
+
|
|
60
|
+
target = File.realpath(path)
|
|
61
|
+
denied.any? { |hidden| inside?(target, hidden) || inside?(hidden, target) }
|
|
62
|
+
rescue SystemCallError
|
|
63
|
+
true
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def real(path)
|
|
67
|
+
path = File.expand_path(path)
|
|
68
|
+
File.realpath(path)
|
|
69
|
+
rescue SystemCallError
|
|
70
|
+
File.join(real(File.dirname(path)), File.basename(path))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inside?(path, root)
|
|
74
|
+
path == root || path.start_with?(File.join(root, ''))
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Sandbox
|
|
5
|
+
# Runs commands with an MXC::Sandbox class or instance from the mxc gem:
|
|
6
|
+
# Bubblewrap on Linux, unless Configuration#sandbox_backend or the
|
|
7
|
+
# sandbox asks for Landlock, Seatbelt on macOS, and process containers
|
|
8
|
+
# on Windows. Each run starts from a fresh copy of the sandbox's policy
|
|
9
|
+
# and adds only what RubyLLM passes for that run.
|
|
10
|
+
class MXC # :nodoc:
|
|
11
|
+
DOCS = 'https://rubyllm.com/sandboxes/#running-on-landlock'
|
|
12
|
+
NAMESPACES = /namespace/i
|
|
13
|
+
BUBBLEWRAP_REMEDIATION =
|
|
14
|
+
'Allow unprivileged user namespaces for Bubblewrap: on Ubuntu 23.10 and later, including 24.04, ' \
|
|
15
|
+
'give bwrap an AppArmor profile that allows userns, or, host-wide and weaker, run ' \
|
|
16
|
+
'sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0; on kernels with ' \
|
|
17
|
+
'kernel.unprivileged_userns_clone, set it to 1. In Docker, run with a seccomp profile that allows ' \
|
|
18
|
+
'namespaces: https://crmne.github.io/mxc/running-in-docker/. ' \
|
|
19
|
+
'See https://rubyllm.com/sandboxes/#running-on-linux'
|
|
20
|
+
LANDLOCK_OPT_IN = 'Or set config.sandbox_backend = :landlock to run on Landlock, which isolates less ' \
|
|
21
|
+
"and has no network access: #{DOCS}".freeze
|
|
22
|
+
MINIMUM_VERSION = '0.4.1'
|
|
23
|
+
private_constant :MINIMUM_VERSION
|
|
24
|
+
|
|
25
|
+
attr_reader :policy, :libraries
|
|
26
|
+
|
|
27
|
+
# Raises ConfigurationError when the installed mxc is older than
|
|
28
|
+
# MINIMUM_VERSION.
|
|
29
|
+
def initialize(policy, backend: :bubblewrap, libraries: [])
|
|
30
|
+
require_minimum_version
|
|
31
|
+
@policy = policy
|
|
32
|
+
@backend = backend
|
|
33
|
+
@libraries = libraries
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def supported?
|
|
37
|
+
sandbox.supported?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def reason
|
|
41
|
+
current = sandbox
|
|
42
|
+
availability = current.availability
|
|
43
|
+
return if availability.supported?
|
|
44
|
+
|
|
45
|
+
[availability.reason, *remediation(current.backend, availability)].compact.join(' ')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def backend = sandbox.backend
|
|
49
|
+
def readable = sandbox.readable
|
|
50
|
+
def writable = sandbox.writable
|
|
51
|
+
def network = sandbox.network
|
|
52
|
+
def directory = sandbox.directory
|
|
53
|
+
def timeout = sandbox.timeout
|
|
54
|
+
def limits = sandbox.respond_to?(:limits) ? sandbox.limits : {}
|
|
55
|
+
|
|
56
|
+
# Returns a runner for a sandbox with none of this one's access, on
|
|
57
|
+
# the same backend, for RubyLLM's own package installs.
|
|
58
|
+
def isolated
|
|
59
|
+
self.class.new(::MXC::Sandbox.new(backend: sandbox.backend), backend: @backend, libraries: @libraries)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a runner whose commands may load the libraries +binaries+
|
|
63
|
+
# link, such as an interpreter's extensions. mxc before 0.4 cannot
|
|
64
|
+
# grant them, and runs without.
|
|
65
|
+
def with_libraries_of(*binaries)
|
|
66
|
+
self.class.new(@policy, backend: @backend, libraries: [*@libraries, *binaries])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def run(*command, timeout:, readable: [], writable: [], denied: [], directory: nil, env: {}, network: nil, &)
|
|
70
|
+
run = sandbox.with_readable(*readable).with_writable(*writable).with_denied(*denied).with_timeout(timeout)
|
|
71
|
+
run.with_directory(directory) if run.directory.nil? && directory
|
|
72
|
+
run.with_env(env.reject { |name, _| run.env.key?(name) })
|
|
73
|
+
apply_network(run, network)
|
|
74
|
+
run.with_libraries_of(*@libraries) if run.respond_to?(:with_libraries_of)
|
|
75
|
+
run = Landlock.policy(run) if run.backend == :landlock
|
|
76
|
+
block_given? ? run_by_line(run, command, &) : run.run(*command)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def inspect
|
|
80
|
+
"#<#{self.class.name} #{sandbox.inspect}>"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def require_minimum_version
|
|
86
|
+
return unless defined?(::MXC::VERSION)
|
|
87
|
+
|
|
88
|
+
installed = ::MXC::VERSION
|
|
89
|
+
return if Gem::Version.new(installed) >= Gem::Version.new(MINIMUM_VERSION)
|
|
90
|
+
|
|
91
|
+
raise ConfigurationError,
|
|
92
|
+
"RubyLLM's sandbox needs mxc #{MINIMUM_VERSION} or later; you have #{installed}. Run bundle update mxc."
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def run_by_line(run, command)
|
|
96
|
+
pending = Hash.new { |buffers, stream| buffers[stream] = String.new(encoding: Encoding::BINARY) }
|
|
97
|
+
result = run.run(*command) do |chunk|
|
|
98
|
+
buffer = pending[chunk.stream]
|
|
99
|
+
scanned = buffer.bytesize
|
|
100
|
+
buffer << chunk.content.b
|
|
101
|
+
while (newline = buffer.index("\n", scanned))
|
|
102
|
+
yield text(buffer.slice!(0..newline))
|
|
103
|
+
scanned = 0
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
pending.each_value { |rest| yield text(rest) unless rest.empty? }
|
|
107
|
+
result
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def text(bytes)
|
|
111
|
+
bytes.force_encoding(Encoding::UTF_8).scrub
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def sandbox
|
|
115
|
+
sandbox = @policy.is_a?(Class) ? @policy.new : @policy.dup
|
|
116
|
+
sandbox.backend == :auto && linux? ? sandbox.with_backend(@backend) : sandbox
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def remediation(backend, availability)
|
|
120
|
+
return availability.remediation unless backend == :bubblewrap
|
|
121
|
+
|
|
122
|
+
namespaces = [availability.reason, availability.remediation].join(' ').match?(NAMESPACES)
|
|
123
|
+
[namespaces ? BUBBLEWRAP_REMEDIATION : availability.remediation, LANDLOCK_OPT_IN]
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def linux?
|
|
127
|
+
RUBY_PLATFORM.include?('linux')
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def apply_network(run, network)
|
|
131
|
+
return if network.nil?
|
|
132
|
+
|
|
133
|
+
network.is_a?(Hash) ? run.with_network(**network) : run.with_network(network)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A sandbox runs commands RubyLLM did not write, such as the code a model
|
|
5
|
+
# sends to Chat#with_code_execution, and holds the whole policy for them:
|
|
6
|
+
# what they may read and write, which hosts they reach, where they start,
|
|
7
|
+
# and how long they run. Define one as an MXC::Sandbox class from the mxc
|
|
8
|
+
# gem:
|
|
9
|
+
#
|
|
10
|
+
# class ReadOnlySandbox < MXC::Sandbox
|
|
11
|
+
# timeout 120
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# class WorkspaceSandbox < MXC::Sandbox
|
|
15
|
+
# directory Rails.root.join("storage/workspace")
|
|
16
|
+
# writable Rails.root.join("storage/workspace")
|
|
17
|
+
# timeout 300
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# RubyLLM.configure do |config|
|
|
21
|
+
# config.sandbox = "ReadOnlySandbox"
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# A String names the class and is looked up whenever a chat needs it,
|
|
25
|
+
# so in Rails the classes live in +app/sandboxes+, autoload, and reload
|
|
26
|
+
# like the rest of the application.
|
|
27
|
+
#
|
|
28
|
+
# A sandbox may be an MXC::Sandbox class or instance, or any other class
|
|
29
|
+
# or object whose instances answer two methods:
|
|
30
|
+
#
|
|
31
|
+
# class RemoteSandbox
|
|
32
|
+
# def supported?
|
|
33
|
+
# true
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def run(*command, timeout:, readable: [], writable: [], denied: [], directory: nil, env: {}, network: nil)
|
|
37
|
+
# # Runs +command+ and returns an object answering stdout, stderr,
|
|
38
|
+
# # exit_code, and timed_out?
|
|
39
|
+
# end
|
|
40
|
+
# end
|
|
41
|
+
#
|
|
42
|
+
# +run+ takes the command like Kernel#system: one String for the shell,
|
|
43
|
+
# or a program and its arguments. The keywords are what RubyLLM adds to
|
|
44
|
+
# the sandbox's own policy for one run: +readable+ and +writable+ paths
|
|
45
|
+
# holding the run's files, +denied+ paths it must not see, even inside
|
|
46
|
+
# readable ones, the +directory+ to start in when the sandbox has none of
|
|
47
|
+
# its own, and +env+, the environment the run's languages and packages
|
|
48
|
+
# need. +timeout+ is the sandbox's own, or 120 seconds when it answers
|
|
49
|
+
# +timeout+ with +nil+ or not at all. +network+ is +nil+ for the
|
|
50
|
+
# sandbox's own access; RubyLLM passes <tt>{ allow: [...] }</tt> only to
|
|
51
|
+
# install declared packages from their registries. With a block, +run+
|
|
52
|
+
# may yield each line of output as it arrives, from standard output and
|
|
53
|
+
# standard error, like IO#each_line, and code execution reports the
|
|
54
|
+
# lines as progress; a sandbox may ignore the block. The result may also
|
|
55
|
+
# answer +blocked+, the hosts the network policy refused. A sandbox may
|
|
56
|
+
# answer +reason+, which RubyLLM adds to the error it raises when
|
|
57
|
+
# #supported? is false, and +with_libraries_of+, taking paths and
|
|
58
|
+
# returning a sandbox whose runs may load the libraries those files
|
|
59
|
+
# link, which RubyLLM calls with Ruby and its extensions. It may also
|
|
60
|
+
# answer +readable+, +writable+, +network+, +directory+, and +timeout+,
|
|
61
|
+
# which describe it to the model and to the person approving an
|
|
62
|
+
# escalation.
|
|
63
|
+
module Sandbox
|
|
64
|
+
# Seconds a run may take when the sandbox sets no timeout.
|
|
65
|
+
DEFAULT_TIMEOUT = 120
|
|
66
|
+
|
|
67
|
+
# The values Configuration#sandbox_backend accepts.
|
|
68
|
+
BACKENDS = %i[bubblewrap landlock].freeze
|
|
69
|
+
|
|
70
|
+
class << self
|
|
71
|
+
# Returns a runner for the sandbox +value+ describes, or +nil+ for
|
|
72
|
+
# none. A String names the sandbox class, looked up on each call.
|
|
73
|
+
# An MXC::Sandbox runs on +backend+ on Linux unless it names its own.
|
|
74
|
+
#
|
|
75
|
+
# Raises ConfigurationError for a value that is not a sandbox, or for
|
|
76
|
+
# a backend other than those in BACKENDS.
|
|
77
|
+
def resolve(value, backend: RubyLLM.config.sandbox_backend) # :nodoc:
|
|
78
|
+
return if value.nil?
|
|
79
|
+
|
|
80
|
+
value = constant(value) if value.is_a?(String)
|
|
81
|
+
return MXC.new(value, backend: backend_name(backend)) if mxc?(value)
|
|
82
|
+
|
|
83
|
+
instance = value.is_a?(Class) ? value.new : value
|
|
84
|
+
return instance if instance.respond_to?(:run) && instance.respond_to?(:supported?)
|
|
85
|
+
|
|
86
|
+
raise ConfigurationError,
|
|
87
|
+
"A sandbox is an MXC::Sandbox class or an object answering supported? and run, got #{value.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns the name the model uses for +value+: its class name without
|
|
91
|
+
# a +Sandbox+ suffix, in snake case, so WorkspaceSandbox is
|
|
92
|
+
# +workspace+.
|
|
93
|
+
#
|
|
94
|
+
# Raises ArgumentError when the class has no name.
|
|
95
|
+
def name_for(value) # :nodoc:
|
|
96
|
+
base = class_name(value).to_s.split('::').last.to_s.delete_suffix('Sandbox')
|
|
97
|
+
if base.empty?
|
|
98
|
+
raise ArgumentError, "Name the sandbox #{value.inspect} with a Hash: escalate_to: { name => sandbox }"
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
base.gsub(/([A-Z\d])([A-Z][a-z])/, '\1_\2').gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Raises ConfigurationError unless +sandbox+ can run on this machine.
|
|
105
|
+
def verify(sandbox) # :nodoc:
|
|
106
|
+
return if sandbox.nil? || sandbox.supported?
|
|
107
|
+
|
|
108
|
+
reason = sandbox.reason if sandbox.respond_to?(:reason)
|
|
109
|
+
raise ConfigurationError, ['The configured sandbox cannot run on this machine.', reason].compact.join(' ')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns the class a String names. Raises ConfigurationError when
|
|
113
|
+
# nothing by that name is defined.
|
|
114
|
+
def constant(name) # :nodoc:
|
|
115
|
+
Object.const_get(name)
|
|
116
|
+
rescue NameError
|
|
117
|
+
raise ConfigurationError, "The sandbox #{name} is not defined"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def backend_name(backend)
|
|
123
|
+
name = backend.respond_to?(:to_sym) ? backend.to_sym : backend
|
|
124
|
+
return name if BACKENDS.include?(name)
|
|
125
|
+
|
|
126
|
+
raise ConfigurationError,
|
|
127
|
+
"config.sandbox_backend is #{BACKENDS.map(&:inspect).join(' or ')}, got #{backend.inspect}"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def class_name(value)
|
|
131
|
+
return value if value.is_a?(String)
|
|
132
|
+
|
|
133
|
+
(value.is_a?(Class) ? value : value.class).name
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mxc?(value)
|
|
137
|
+
defined?(::MXC::Sandbox) && (value.is_a?(::MXC::Sandbox) || (value.is_a?(Class) && value <= ::MXC::Sandbox))
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
@@ -13,12 +13,16 @@ module RubyLLM
|
|
|
13
13
|
# )
|
|
14
14
|
# end
|
|
15
15
|
#
|
|
16
|
-
# Cited passages come back on the response as Message#citations.
|
|
16
|
+
# Cited passages come back on the response as Message#citations. Give a
|
|
17
|
+
# result a +:source_id+ String, such as a Global ID, to identify it to
|
|
18
|
+
# your app; its citations return it as Citation#source_id.
|
|
19
|
+
#
|
|
20
|
+
# RubyLLM::SearchResults.new(title: doc.name, source_id: doc.to_global_id.to_s, text: doc.body)
|
|
17
21
|
class SearchResults
|
|
18
22
|
KEY = 'search_results' # :nodoc:
|
|
19
23
|
|
|
20
24
|
# The normalized results, as an array of hashes with +:title+, +:text+,
|
|
21
|
-
# and optional +:url+ keys.
|
|
25
|
+
# and optional +:url+ and +:source_id+ keys.
|
|
22
26
|
attr_reader :results
|
|
23
27
|
|
|
24
28
|
# Returns a new SearchResults built from one or more result hashes, or
|
|
@@ -27,7 +31,8 @@ module RubyLLM
|
|
|
27
31
|
# RubyLLM::SearchResults.new(title: 'Q4 Report', url: report_url, text: report_text)
|
|
28
32
|
# RubyLLM::SearchResults.new({ title: 'A', text: '...' }, { title: 'B', text: '...' })
|
|
29
33
|
#
|
|
30
|
-
# Each result is reduced to its +:title+, +:url+,
|
|
34
|
+
# Each result is reduced to its +:title+, +:url+, +:source_id+, and
|
|
35
|
+
# +:text+ entries.
|
|
31
36
|
# Raises ArgumentError if no results are given or a result is missing
|
|
32
37
|
# +:title+ or +:text+.
|
|
33
38
|
def initialize(*results, **result)
|
|
@@ -62,7 +67,7 @@ module RubyLLM
|
|
|
62
67
|
entry = Support::Utils.deep_symbolize_keys(entry.to_h)
|
|
63
68
|
raise ArgumentError, 'Search results require :title and :text' unless entry[:title] && entry[:text]
|
|
64
69
|
|
|
65
|
-
entry.slice(:title, :url, :text)
|
|
70
|
+
entry.slice(:title, :url, :source_id, :text)
|
|
66
71
|
end
|
|
67
72
|
end
|
|
68
73
|
end
|
|
@@ -46,7 +46,7 @@ module RubyLLM
|
|
|
46
46
|
attr_reader :search_suggestions
|
|
47
47
|
|
|
48
48
|
def self.from_h(data) # :nodoc:
|
|
49
|
-
data =
|
|
49
|
+
data = data.transform_keys(&:to_sym)
|
|
50
50
|
new(type: data[:type], name: data[:name], id: data[:id], input: data[:input],
|
|
51
51
|
result: data[:result], raw: data[:raw])
|
|
52
52
|
end
|