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
data/lib/ruby_llm/chunk.rb
CHANGED
|
@@ -5,12 +5,38 @@ module RubyLLM
|
|
|
5
5
|
# yielded to the block passed to Chat#ask or Chat#complete when streaming.
|
|
6
6
|
# A Chunk is a Message, so it responds to the same readers, but it holds
|
|
7
7
|
# only the data received in that part of the stream, such as a piece of
|
|
8
|
-
# content, partial tool calls, or the finish reason
|
|
8
|
+
# content, partial tool calls, or the finish reason.
|
|
9
9
|
#
|
|
10
10
|
# chat.ask "Write a haiku about Ruby" do |chunk|
|
|
11
11
|
# print chunk.content
|
|
12
12
|
# end
|
|
13
13
|
#
|
|
14
|
+
# Streamed chunks always have the +:assistant+ role, and most carry the
|
|
15
|
+
# model ID. #finish_reason, with predicates such as #max_tokens?,
|
|
16
|
+
# #content_filtered?, #tool_call_stop?, and #stopped?, arrives near the end
|
|
17
|
+
# of the stream, not necessarily on the last chunk. #tokens is often empty
|
|
18
|
+
# until then: +output+ counts the billable output tokens up to that chunk,
|
|
19
|
+
# including thinking tokens the provider bills as output, and providers
|
|
20
|
+
# differ in when they report it. Read the final reason and usage from the
|
|
21
|
+
# Message that Chat#ask returns.
|
|
22
|
+
#
|
|
23
|
+
# Pass +index:+ to build one with its position, such as in a test:
|
|
24
|
+
#
|
|
25
|
+
# RubyLLM::Chunk.new(role: :assistant, content: "Hel", index: 0)
|
|
26
|
+
#
|
|
14
27
|
class Chunk < Message
|
|
28
|
+
def initialize(options = {}) # :nodoc:
|
|
29
|
+
super
|
|
30
|
+
@index = options[:index]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns the position of this chunk within the response it belongs
|
|
34
|
+
# to, starting at 0. Each response in a Chat#complete loop, such as
|
|
35
|
+
# the one after a tool call, starts again at 0. Counts every chunk,
|
|
36
|
+
# including those without content.
|
|
37
|
+
#
|
|
38
|
+
# chat.ask("Count to three") { |chunk| puts "#{chunk.index}: #{chunk.content}" }
|
|
39
|
+
#
|
|
40
|
+
attr_accessor :index
|
|
15
41
|
end
|
|
16
42
|
end
|
data/lib/ruby_llm/citation.rb
CHANGED
|
@@ -41,7 +41,8 @@ module RubyLLM
|
|
|
41
41
|
# content, or +nil+.
|
|
42
42
|
attr_reader :end_index
|
|
43
43
|
|
|
44
|
-
# The provider's identifier for the cited source, or +nil+.
|
|
44
|
+
# The provider's identifier for the cited source, or +nil+. For a cited
|
|
45
|
+
# SearchResults result, this is its +:source_id+ when it has one.
|
|
45
46
|
attr_reader :source_id
|
|
46
47
|
|
|
47
48
|
# The 0-indexed position of the source document or search result,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class CodeExecution
|
|
5
|
+
Language = Data.define(:name, :label, :filename, :program, :check) do # :nodoc:
|
|
6
|
+
def self.[](name)
|
|
7
|
+
self::ALL.fetch(name)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def command(path)
|
|
11
|
+
[program, path]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def check_command
|
|
15
|
+
[program, *check]
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Language::ALL = {
|
|
20
|
+
ruby: Language.new(name: :ruby, label: 'Ruby', filename: 'main.rb', program: 'ruby', check: ['-e', 'exit']),
|
|
21
|
+
python: Language.new(name: :python, label: 'Python', filename: 'main.py', program: 'python3',
|
|
22
|
+
check: ['-c', 'pass']),
|
|
23
|
+
javascript: Language.new(name: :javascript, label: 'JavaScript', filename: 'main.js', program: 'node',
|
|
24
|
+
check: ['-e', '0']),
|
|
25
|
+
shell: Language.new(name: :shell, label: 'shell', filename: 'main.sh', program: 'sh', check: ['-c', 'exit 0'])
|
|
26
|
+
}.freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open3'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
class CodeExecution
|
|
7
|
+
# The Node.js the application's PATH finds, as code execution mounts
|
|
8
|
+
# it: the installation read-only, wherever a version manager put it.
|
|
9
|
+
# Asks +node+ itself for its executable, since a version manager's
|
|
10
|
+
# shim is a different program.
|
|
11
|
+
class NodeInstallation # :nodoc:
|
|
12
|
+
def self.current
|
|
13
|
+
@current ||= new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def initialize(node: 'node')
|
|
17
|
+
@node = node
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def prefix
|
|
21
|
+
executable && File.dirname(executable, 2)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def bin_dir
|
|
25
|
+
executable && File.dirname(executable)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def version
|
|
29
|
+
details&.last
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def readable
|
|
33
|
+
[prefix].compact
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def executable
|
|
39
|
+
details&.first
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def details
|
|
43
|
+
return @details if defined?(@details)
|
|
44
|
+
|
|
45
|
+
@details = begin
|
|
46
|
+
output, status = Open3.capture2(@node, '-p', 'process.execPath + "\\n" + process.version',
|
|
47
|
+
err: File::NULL)
|
|
48
|
+
lines = output.lines.map(&:chomp)
|
|
49
|
+
status.success? && lines.size == 2 ? [File.realpath(lines.first), lines.last] : nil
|
|
50
|
+
rescue SystemCallError
|
|
51
|
+
nil
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
|
|
8
|
+
module RubyLLM
|
|
9
|
+
class CodeExecution
|
|
10
|
+
# The Ruby and Node.js installations, gems, Python packages, and npm
|
|
11
|
+
# packages code runs with. Declared packages are installed once per
|
|
12
|
+
# set, in the sandbox with only their registries reachable, into a
|
|
13
|
+
# directory under Configuration#package_cache_dir that every later run
|
|
14
|
+
# mounts read-only. A prebuilt GEM_HOME, virtualenv, or node_modules is
|
|
15
|
+
# mounted as it is. npm packages install without running their scripts.
|
|
16
|
+
class Packages # :nodoc:
|
|
17
|
+
SYSTEM_PATH = %w[/usr/local/bin /usr/bin /bin].freeze
|
|
18
|
+
RESOLVER_CONFIG = '/etc/resolv.conf'
|
|
19
|
+
INSTALL_TIMEOUT = 600
|
|
20
|
+
MAX_FAILURE_OUTPUT = 2000
|
|
21
|
+
REGISTRIES = {
|
|
22
|
+
ruby: %w[rubygems.org *.rubygems.org],
|
|
23
|
+
python: %w[pypi.org files.pythonhosted.org],
|
|
24
|
+
javascript: %w[registry.npmjs.org]
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_reader :gems, :python, :npm
|
|
28
|
+
|
|
29
|
+
def initialize(sandbox:, cache_dir:, gems: [], python: [], npm: [], gem_home: nil, virtualenv: nil,
|
|
30
|
+
node_modules: nil, node: nil, installation: RubyInstallation.current)
|
|
31
|
+
@sandbox = sandbox
|
|
32
|
+
@installation = installation
|
|
33
|
+
@node = node
|
|
34
|
+
@npm = Array(npm).map(&:to_s)
|
|
35
|
+
@node_modules = node_modules && File.expand_path(node_modules)
|
|
36
|
+
@cache_dir = cache_dir && File.expand_path(cache_dir)
|
|
37
|
+
@gems = Array(gems).map(&:to_s)
|
|
38
|
+
@python = Array(python).map(&:to_s)
|
|
39
|
+
@gem_home = gem_home && File.expand_path(gem_home)
|
|
40
|
+
@virtualenv = virtualenv && File.expand_path(virtualenv)
|
|
41
|
+
@lock = Mutex.new
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def gem_names
|
|
45
|
+
return gems if gems.any? || @gem_home.nil?
|
|
46
|
+
|
|
47
|
+
Dir.glob(File.join(@gem_home, 'specifications', '*.gemspec'))
|
|
48
|
+
.map { |path| File.basename(path, '.gemspec').sub(/-\d[\w.]*(?:-[a-z][^.]*)?\z/, '') }.uniq.sort
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def python_names
|
|
52
|
+
return python if python.any? || @virtualenv.nil?
|
|
53
|
+
|
|
54
|
+
Dir.glob(File.join(@virtualenv, 'lib', 'python*', 'site-packages', '*.dist-info'))
|
|
55
|
+
.map { |path| File.basename(path, '.dist-info').sub(/-[^-]+\z/, '') }.uniq.sort
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def npm_names
|
|
59
|
+
return npm if npm.any? || @node_modules.nil?
|
|
60
|
+
|
|
61
|
+
manifest = File.join(File.dirname(@node_modules), 'package.json')
|
|
62
|
+
return [] unless File.file?(manifest)
|
|
63
|
+
|
|
64
|
+
JSON.parse(File.read(manifest)).fetch('dependencies', {}).keys.sort
|
|
65
|
+
rescue JSON::ParserError
|
|
66
|
+
[]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def readable
|
|
70
|
+
home = gem_home
|
|
71
|
+
[*runtimes, home, (ruby_gems if home), virtualenv, node_modules].compact
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def runtimes
|
|
75
|
+
[*@installation.readable, *@node&.readable]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns +runner+ able to load the libraries Ruby links, when it can
|
|
79
|
+
# grant them.
|
|
80
|
+
def with_libraries(runner)
|
|
81
|
+
runner.respond_to?(:with_libraries_of) ? runner.with_libraries_of(*@installation.libraries) : runner
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def denied
|
|
85
|
+
@installation.denied - [@gem_home]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def resolver_config(network)
|
|
89
|
+
return [] unless network && File.exist?(RESOLVER_CONFIG)
|
|
90
|
+
|
|
91
|
+
[RESOLVER_CONFIG, File.realpath(RESOLVER_CONFIG)].uniq
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def env(home, tmp = home)
|
|
95
|
+
base_env(home, tmp).merge(
|
|
96
|
+
'PATH' => path,
|
|
97
|
+
'GEM_HOME' => gem_home,
|
|
98
|
+
'GEM_PATH' => gem_home && [gem_home, ruby_gems].join(File::PATH_SEPARATOR),
|
|
99
|
+
'VIRTUAL_ENV' => virtualenv
|
|
100
|
+
).compact
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def gem_home
|
|
104
|
+
return @gem_home if gems.empty?
|
|
105
|
+
|
|
106
|
+
@lock.synchronize { @installed_gem_home ||= install(:ruby, gems) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def virtualenv
|
|
110
|
+
return @virtualenv if python.empty?
|
|
111
|
+
|
|
112
|
+
@lock.synchronize { @installed_virtualenv ||= install(:python, python) }
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def node_modules
|
|
116
|
+
return @node_modules if npm.empty?
|
|
117
|
+
|
|
118
|
+
@lock.synchronize { @installed_node_modules ||= File.join(install(:javascript, npm), 'node_modules') }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cache_dir
|
|
122
|
+
@cache_dir || raise(ConfigurationError, 'Set config.package_cache_dir to install code execution packages')
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def base_env(home, tmp = home)
|
|
126
|
+
{
|
|
127
|
+
'PATH' => [RbConfig::CONFIG['bindir'], @node&.bin_dir, *SYSTEM_PATH].compact.join(File::PATH_SEPARATOR),
|
|
128
|
+
'HOME' => home,
|
|
129
|
+
'XDG_CACHE_HOME' => File.join(home, '.cache'),
|
|
130
|
+
'TMPDIR' => tmp,
|
|
131
|
+
'LANG' => 'C.UTF-8'
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def ruby_gems
|
|
136
|
+
@installation.ruby_gems(@cache_dir)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def run(*command, home:, readable: [], writable: [], network: false, env: {})
|
|
140
|
+
sandbox = with_libraries(@sandbox.is_a?(Proc) ? @sandbox.call : @sandbox)
|
|
141
|
+
sandbox.run(*command, readable: [*runtimes, *resolver_config(network),
|
|
142
|
+
*readable], denied:,
|
|
143
|
+
writable: [*writable, home], network:,
|
|
144
|
+
env: base_env(home).merge(env), timeout: INSTALL_TIMEOUT)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def path
|
|
150
|
+
[virtualenv && File.join(virtualenv, 'bin'), gem_home && File.join(gem_home, 'bin'),
|
|
151
|
+
RbConfig::CONFIG['bindir'], @node&.bin_dir, *SYSTEM_PATH].compact.join(File::PATH_SEPARATOR)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def install(kind, packages)
|
|
155
|
+
directory = File.join(cache_dir, "#{kind}-#{cache_key(kind, packages)}")
|
|
156
|
+
return directory if File.exist?("#{directory}.ready")
|
|
157
|
+
|
|
158
|
+
FileUtils.mkdir_p(cache_dir)
|
|
159
|
+
File.open("#{directory}.lock", File::RDWR | File::CREAT, 0o644) do |lock|
|
|
160
|
+
lock.flock(File::LOCK_EX)
|
|
161
|
+
build(kind, directory, packages) unless File.exist?("#{directory}.ready")
|
|
162
|
+
end
|
|
163
|
+
directory
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def build(kind, directory, packages)
|
|
167
|
+
FileUtils.rm_rf(directory)
|
|
168
|
+
FileUtils.mkdir_p(directory)
|
|
169
|
+
Dir.mktmpdir('ruby_llm-install-') do |home|
|
|
170
|
+
case kind
|
|
171
|
+
when :ruby then install_gems(directory, packages, home)
|
|
172
|
+
when :python then install_python(directory, packages, home)
|
|
173
|
+
else install_npm(directory, packages, home)
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
File.write("#{directory}.ready", "#{packages.sort.join("\n")}\n")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def install_gems(directory, gems, home)
|
|
180
|
+
result = run('ruby', '-S', 'gem', 'install', '--no-document', '--install-dir', directory,
|
|
181
|
+
'--bindir', File.join(directory, 'bin'), *gems,
|
|
182
|
+
home:, readable: [ruby_gems], writable: [directory], network: { allow: REGISTRIES[:ruby] },
|
|
183
|
+
env: { 'GEM_HOME' => directory, 'GEM_PATH' => [directory, ruby_gems].join(File::PATH_SEPARATOR) })
|
|
184
|
+
verify(result, "gems #{gems.join(', ')}")
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def install_python(directory, packages, home)
|
|
188
|
+
description = "Python packages #{packages.join(', ')}"
|
|
189
|
+
verify(run('python3', '-m', 'venv', directory, home:, writable: [directory]), description)
|
|
190
|
+
result = run(File.join(directory, 'bin', 'python'), '-m', 'pip', 'install', '--no-input',
|
|
191
|
+
'--disable-pip-version-check', *packages,
|
|
192
|
+
home:, writable: [directory], network: { allow: REGISTRIES[:python] })
|
|
193
|
+
verify(result, description)
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def install_npm(directory, packages, home)
|
|
197
|
+
raise Error, 'Could not install npm packages: node is not on the PATH' unless @node&.bin_dir
|
|
198
|
+
|
|
199
|
+
result = run('npm', 'install', '--prefix', directory, '--ignore-scripts', '--no-audit', '--no-fund',
|
|
200
|
+
'--no-update-notifier', *packages,
|
|
201
|
+
home:, writable: [directory], network: { allow: REGISTRIES[:javascript] })
|
|
202
|
+
verify(result, "npm packages #{packages.join(', ')}")
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def verify(result, description)
|
|
206
|
+
return if result.exit_code&.zero? && !result.timed_out?
|
|
207
|
+
|
|
208
|
+
raise Error, "Could not install #{description}: #{failure(result)}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def failure(result)
|
|
212
|
+
return "timed out after #{INSTALL_TIMEOUT} seconds" if result.timed_out?
|
|
213
|
+
|
|
214
|
+
stderr = result.stderr.to_s.strip
|
|
215
|
+
blocked = result.respond_to?(:blocked) ? Array(result.blocked) : []
|
|
216
|
+
details = failure_details(stderr)
|
|
217
|
+
blocked.empty? ? details : "#{details} (blocked: #{blocked.join(', ')})"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def failure_details(stderr)
|
|
221
|
+
return stderr if stderr.length <= MAX_FAILURE_OUTPUT
|
|
222
|
+
|
|
223
|
+
half = MAX_FAILURE_OUTPUT / 2
|
|
224
|
+
"#{stderr[0, half]}\n[#{stderr.length - MAX_FAILURE_OUTPUT} characters omitted]\n#{stderr[-half..]}"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def cache_key(kind, packages)
|
|
228
|
+
Digest::SHA256.hexdigest(JSON.generate([kind, runtime_version(kind), packages.sort]))[0, 24]
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def runtime_version(kind)
|
|
232
|
+
return [RUBY_VERSION, RUBY_PLATFORM, RbConfig.ruby] if kind == :ruby
|
|
233
|
+
return [@node&.version, RUBY_PLATFORM] if kind == :javascript
|
|
234
|
+
|
|
235
|
+
Dir.mktmpdir('ruby_llm-install-') do |home|
|
|
236
|
+
run('python3', '-c', 'import sys; print(sys.version)', home:).stdout.to_s.strip
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
require 'rbconfig'
|
|
6
|
+
require 'tmpdir'
|
|
7
|
+
|
|
8
|
+
module RubyLLM
|
|
9
|
+
class CodeExecution
|
|
10
|
+
# The application's Ruby, as code execution mounts it: the installation
|
|
11
|
+
# read-only, with every gem in its gem directory hidden except Ruby's
|
|
12
|
+
# default and bundled gems, and every other gem directory the
|
|
13
|
+
# application uses hidden entirely, as are Bundler's git checkouts and
|
|
14
|
+
# RubyGems' cache and documents. RubyGems lists the plugins
|
|
15
|
+
# directory on every +gem+ command, so only the application's plugins
|
|
16
|
+
# are hidden, not the directory: macOS refuses to list a hidden one.
|
|
17
|
+
class RubyInstallation # :nodoc:
|
|
18
|
+
HIDDEN_DIRECTORIES = %w[bundler cache doc].freeze
|
|
19
|
+
BUILD_MARKER = 'gem.build_complete'
|
|
20
|
+
|
|
21
|
+
# Ruby's bundled gems that Gem::BUNDLED_GEMS::SINCE may leave out, by
|
|
22
|
+
# the Ruby version that first bundled them. SINCE lists only the gems
|
|
23
|
+
# that recently stopped being default gems, and an installed Ruby
|
|
24
|
+
# keeps no other list of them.
|
|
25
|
+
BUNDLED_GEMS = {
|
|
26
|
+
'3.2' => %w[minitest power_assert rake test-unit rexml rss net-ftp net-imap net-pop net-smtp matrix prime rbs
|
|
27
|
+
typeprof debug],
|
|
28
|
+
'3.4' => %w[repl_type_completor],
|
|
29
|
+
'4.0' => %w[readline]
|
|
30
|
+
}.freeze
|
|
31
|
+
|
|
32
|
+
def self.current
|
|
33
|
+
@current ||= new
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def initialize(prefix: RbConfig::CONFIG['prefix'], gem_dir: Gem.default_dir, gem_path: Gem.path)
|
|
37
|
+
@prefix = prefix
|
|
38
|
+
@gem_dir = gem_dir
|
|
39
|
+
@gem_path = gem_path
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def readable
|
|
43
|
+
@gem_dir.start_with?(File.join(@prefix, '')) ? [@prefix] : [@prefix, @gem_dir]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns Ruby, its shared library, and its extensions: the files
|
|
47
|
+
# whose own libraries, such as Homebrew's libyaml and OpenSSL on
|
|
48
|
+
# macOS, a sandbox lets Ruby load.
|
|
49
|
+
def libraries
|
|
50
|
+
@libraries ||= [RbConfig.ruby, *shared_library,
|
|
51
|
+
*Dir.glob(File.join(RbConfig::CONFIG['archdir'], '**', "*.#{RbConfig::CONFIG['DLEXT']}"))]
|
|
52
|
+
.select { |path| File.file?(path) }.freeze
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def denied
|
|
56
|
+
@denied ||= [
|
|
57
|
+
*application_gems.flat_map { |stub| gem_paths(stub) },
|
|
58
|
+
*HIDDEN_DIRECTORIES.map { |name| File.join(@gem_dir, name) },
|
|
59
|
+
*(@gem_path - [@gem_dir])
|
|
60
|
+
].select { |path| File.exist?(path) }.uniq.freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns a directory, built once under +cache_dir+, or under a
|
|
64
|
+
# private temporary directory without one, that holds only Ruby's
|
|
65
|
+
# bundled gems: their gemspecs, and links to their files. A GEM_PATH
|
|
66
|
+
# with it instead of the gem directory finds no hidden application
|
|
67
|
+
# gem, so <tt>gem install</tt> installs every dependency and RubyGems
|
|
68
|
+
# never activates a gem whose files the sandbox hides. Default gems
|
|
69
|
+
# load from any GEM_PATH.
|
|
70
|
+
def ruby_gems(cache_dir)
|
|
71
|
+
cache_dir ||= (@private_dir ||= Dir.mktmpdir('ruby_llm-ruby-gems-'))
|
|
72
|
+
directory = File.join(cache_dir, "bundled-gems-#{ruby_gems_key}")
|
|
73
|
+
return directory if File.exist?("#{directory}.ready")
|
|
74
|
+
|
|
75
|
+
FileUtils.mkdir_p(cache_dir)
|
|
76
|
+
File.open("#{directory}.lock", File::RDWR | File::CREAT, 0o644) do |lock|
|
|
77
|
+
lock.flock(File::LOCK_EX)
|
|
78
|
+
link_ruby_gems(directory) unless File.exist?("#{directory}.ready")
|
|
79
|
+
end
|
|
80
|
+
directory
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ruby_gem?(name)
|
|
84
|
+
default_gems.include?(name) || bundled_gems.include?(name)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def shared_library
|
|
90
|
+
return unless RbConfig::CONFIG['ENABLE_SHARED'] == 'yes'
|
|
91
|
+
|
|
92
|
+
File.join(RbConfig::CONFIG['libdir'], RbConfig::CONFIG['LIBRUBY_SO'])
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def application_gems
|
|
96
|
+
installed_stubs.reject { |stub| ruby_gem?(stub.name) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def installed_stubs
|
|
100
|
+
Dir.glob(File.join(@gem_dir, 'specifications', '*.gemspec')).map do |path|
|
|
101
|
+
Gem::StubSpecification.gemspec_stub(path, @gem_dir, File.join(@gem_dir, 'gems'))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def bundled_gem_stubs
|
|
106
|
+
installed_stubs.select { |stub| bundled_gems.include?(stub.name) && !default_gems.include?(stub.name) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def ruby_gems_key
|
|
110
|
+
Digest::SHA256.hexdigest([RbConfig.ruby, RUBY_VERSION, *bundled_gem_stubs.map(&:full_name)].join("\n"))[0, 24]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def link_ruby_gems(directory)
|
|
114
|
+
FileUtils.rm_rf(directory)
|
|
115
|
+
FileUtils.mkdir_p(File.join(directory, 'specifications'))
|
|
116
|
+
bundled_gem_stubs.each do |stub|
|
|
117
|
+
[File.join(@gem_dir, 'gems', stub.full_name), extensions_dir(stub)].each do |path|
|
|
118
|
+
next unless File.exist?(path)
|
|
119
|
+
|
|
120
|
+
link = File.join(directory, path.delete_prefix(File.join(@gem_dir, '')))
|
|
121
|
+
FileUtils.mkdir_p(File.dirname(link))
|
|
122
|
+
File.symlink(path, link)
|
|
123
|
+
end
|
|
124
|
+
FileUtils.cp(stub.loaded_from, File.join(directory, 'specifications'))
|
|
125
|
+
end
|
|
126
|
+
File.write("#{directory}.ready", '')
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def extensions_dir(stub)
|
|
130
|
+
File.join(@gem_dir, 'extensions', Gem::Platform.local.to_s, Gem.extension_api_version, stub.full_name)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def gem_paths(stub)
|
|
134
|
+
extensions = extensions_dir(stub)
|
|
135
|
+
built = Dir.exist?(extensions) ? Dir.children(extensions) - [BUILD_MARKER] : []
|
|
136
|
+
[File.join(@gem_dir, 'gems', stub.full_name), *built.map { |name| File.join(extensions, name) },
|
|
137
|
+
File.join(@gem_dir, 'plugins', "#{stub.name}_plugin.rb")]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def default_gems
|
|
141
|
+
@default_gems ||= Dir.glob(File.join(@gem_dir, 'specifications', 'default', '*.gemspec')).map do |path|
|
|
142
|
+
Gem::StubSpecification.default_gemspec_stub(path, @gem_dir, File.join(@gem_dir, 'gems')).name
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def bundled_gems
|
|
147
|
+
@bundled_gems ||= bundled_gems_file || (bundled_gems_table | bundled_gems_constant)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def bundled_gems_table
|
|
151
|
+
ruby = Gem::Version.new(RUBY_VERSION)
|
|
152
|
+
BUNDLED_GEMS.select { |version, _| ruby >= Gem::Version.new(version) }.values.flatten
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def bundled_gems_file
|
|
156
|
+
path = File.join(RbConfig::CONFIG['srcdir'].to_s, 'gems', 'bundled_gems')
|
|
157
|
+
return unless File.file?(path)
|
|
158
|
+
|
|
159
|
+
File.readlines(path).filter_map { |line| line.split.first unless line.start_with?('#') || line.strip.empty? }
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def bundled_gems_constant
|
|
163
|
+
require 'bundled_gems' unless defined?(Gem::BUNDLED_GEMS)
|
|
164
|
+
Gem::BUNDLED_GEMS::SINCE.keys
|
|
165
|
+
rescue LoadError, NameError
|
|
166
|
+
[]
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|