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
|
@@ -68,6 +68,12 @@ module RubyLLM
|
|
|
68
68
|
entries + tools_with_options.map { |name, options| { name: name.to_sym, options: options.to_h } }
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
# Normalizes what an Agent's provider_tools block returns: a Hash of
|
|
72
|
+
# aliases and their options, like the macro's keywords, or a list.
|
|
73
|
+
def entries(value)
|
|
74
|
+
value.is_a?(Hash) ? normalize([], value) : Array(value).compact
|
|
75
|
+
end
|
|
76
|
+
|
|
71
77
|
def normalize_positional(tool)
|
|
72
78
|
case tool
|
|
73
79
|
when Symbol, String then { name: tool.to_sym, options: {} }
|
|
@@ -85,6 +91,23 @@ module RubyLLM
|
|
|
85
91
|
hash.key?(:raw) ? hash.size == 1 : hash.size == 2 && hash.key?(:name) && hash.key?(:options)
|
|
86
92
|
end
|
|
87
93
|
|
|
94
|
+
# Adds normalized entries to those a chat already holds. An alias
|
|
95
|
+
# keeps one entry, in its place, with the latest options. A raw tool
|
|
96
|
+
# and an MCP server, which may be enabled several times, are entered
|
|
97
|
+
# once per identical definition.
|
|
98
|
+
def merge(entries, additions)
|
|
99
|
+
additions.each_with_object(entries.dup) do |entry, merged|
|
|
100
|
+
index = merged.index { |existing| same_tool?(existing, entry) }
|
|
101
|
+
index ? merged[index] = entry : merged << entry
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def same_tool?(existing, entry)
|
|
106
|
+
return existing == entry if entry.key?(:raw) || entry[:name] == :mcp
|
|
107
|
+
|
|
108
|
+
existing[:name] == entry[:name]
|
|
109
|
+
end
|
|
110
|
+
|
|
88
111
|
# Resolves normalized entries against a protocol's alias table.
|
|
89
112
|
def resolve(entries, aliases:, owner:)
|
|
90
113
|
entries.each_with_object(Resolution.new) do |entry, resolution|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Tools
|
|
5
|
+
# The +tool_search+ tool a chat with deferred tools gives the model when
|
|
6
|
+
# its provider has no tool search of its own. The model searches the
|
|
7
|
+
# deferred tools by what it needs to do, or names them, and the chat
|
|
8
|
+
# sends the definitions of the tools it found with every request after
|
|
9
|
+
# that. See Chat#deferred_tools.
|
|
10
|
+
class Search < Tool # :nodoc:
|
|
11
|
+
DEFAULT_LIMIT = 5
|
|
12
|
+
MAX_LIMIT = 20
|
|
13
|
+
SELECT = 'select:'
|
|
14
|
+
SUMMARY_LENGTH = 200
|
|
15
|
+
|
|
16
|
+
inline_limit false
|
|
17
|
+
|
|
18
|
+
def self.tool_name
|
|
19
|
+
'tool_search'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.loaded_names(content)
|
|
23
|
+
names = JSON.parse(content.to_s)['loaded'] if content.to_s.start_with?('{')
|
|
24
|
+
names.is_a?(Array) ? names.map(&:to_s) : []
|
|
25
|
+
rescue JSON::ParserError
|
|
26
|
+
[]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(tools)
|
|
30
|
+
super()
|
|
31
|
+
@tools = tools
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def description
|
|
35
|
+
'Loads tools you can call in this conversation but whose definitions are not shown yet. Search by what ' \
|
|
36
|
+
'you need to do, in a few words, and the tools that match become callable from your next step on, for ' \
|
|
37
|
+
"the rest of the conversation. Pass #{SELECT}name,other_name to load tools by name. " \
|
|
38
|
+
"Tools you can load: #{@tools.call.keys.join(', ')}."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parameters_schema
|
|
42
|
+
{
|
|
43
|
+
'type' => 'object',
|
|
44
|
+
'properties' => {
|
|
45
|
+
'query' => { 'type' => 'string',
|
|
46
|
+
'description' => "A few words about the task, or #{SELECT} and tool names, comma-separated" },
|
|
47
|
+
'limit' => { 'type' => 'integer',
|
|
48
|
+
'description' => "How many matching tools to load, #{MAX_LIMIT} at most. " \
|
|
49
|
+
"#{SELECT} loads every tool it names",
|
|
50
|
+
'default' => DEFAULT_LIMIT, 'minimum' => 1, 'maximum' => MAX_LIMIT }
|
|
51
|
+
},
|
|
52
|
+
'required' => %w[query],
|
|
53
|
+
'additionalProperties' => false
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def execute(query:, limit: DEFAULT_LIMIT)
|
|
58
|
+
tools = @tools.call
|
|
59
|
+
query = query.to_s
|
|
60
|
+
found = matches(tools, query, limit.to_i.clamp(1, MAX_LIMIT))
|
|
61
|
+
result = found.empty? ? nothing_found(tools, query) : loaded(found)
|
|
62
|
+
missing = query.start_with?(SELECT) ? requested(query) - result[:loaded] : []
|
|
63
|
+
missing.empty? ? result : result.merge(not_loaded: missing)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def nothing_found(tools, query)
|
|
69
|
+
{ loaded: [], error: "No tools match #{query}. Tools you can load: #{tools.keys.join(', ')}" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def loaded(found)
|
|
73
|
+
{ loaded: found.map(&:first), tools: found.map { |name, tool| { name:, description: summary(tool) } } }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def matches(tools, query, limit)
|
|
77
|
+
return selected(tools, requested(query)) if query.start_with?(SELECT)
|
|
78
|
+
|
|
79
|
+
terms = words(query)
|
|
80
|
+
ranked = tools.each_with_index.filter_map do |(name, tool), index|
|
|
81
|
+
score = terms.sum { |term| score(name.to_s, tool, term) }
|
|
82
|
+
[-score, index, name] if score.positive?
|
|
83
|
+
end
|
|
84
|
+
ranked.sort.first(limit).map { |*, name| [name.to_s, tools[name]] }
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def requested(query)
|
|
88
|
+
query.delete_prefix(SELECT).split(',').map(&:strip).reject(&:empty?).uniq
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def selected(tools, names)
|
|
92
|
+
names.filter_map { |name| [name, tools[name.to_sym]] if tools.key?(name.to_sym) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def score(name, tool, term)
|
|
96
|
+
return 4 if words(name).include?(term)
|
|
97
|
+
return 2 if name.downcase.include?(term)
|
|
98
|
+
return 1 if words("#{tool.description} #{parameter_names(tool)}").any? { |word| word.start_with?(term) }
|
|
99
|
+
|
|
100
|
+
0
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def words(text)
|
|
104
|
+
text.to_s.downcase.scan(/[[:alnum:]]+/)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def parameter_names(tool)
|
|
108
|
+
properties = tool.parameters_schema.to_h.transform_keys(&:to_s)['properties']
|
|
109
|
+
properties.is_a?(Hash) ? properties.keys.join(' ') : ''
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def summary(tool)
|
|
113
|
+
text = tool.description.to_s.strip
|
|
114
|
+
text.length > SUMMARY_LENGTH ? "#{text[0, SUMMARY_LENGTH - 3].rstrip}..." : text
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Transcription
|
|
5
|
+
# A timed piece of a transcript: an entry of Transcription#segments or
|
|
6
|
+
# Transcription#words, or the TranscriptionChunk#segment a stream
|
|
7
|
+
# completed. Readers cover what providers share; #[] and #to_h reach
|
|
8
|
+
# the rest under the provider's own field names.
|
|
9
|
+
#
|
|
10
|
+
# transcription.segments.each do |segment|
|
|
11
|
+
# puts "#{segment.speaker} at #{segment.start}s: #{segment.text}"
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
class Segment
|
|
15
|
+
include Support::Inspectable
|
|
16
|
+
|
|
17
|
+
def self.wrap(segments) # :nodoc:
|
|
18
|
+
segments&.map { |segment| new(segment) }
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(attributes) # :nodoc:
|
|
22
|
+
@attributes = attributes.to_h.transform_keys(&:to_s)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# The spoken text, or +nil+ when the provider reports none.
|
|
26
|
+
def text = self['text'] || self['word'] || self['transcript']
|
|
27
|
+
|
|
28
|
+
# The speaker label, or +nil+ on models that do not separate speakers.
|
|
29
|
+
def speaker = self['speaker'] || self['speaker_id']
|
|
30
|
+
|
|
31
|
+
# When the text starts, in seconds from the start of the audio.
|
|
32
|
+
def start = self['start']
|
|
33
|
+
|
|
34
|
+
# When the text ends, in seconds from the start of the audio.
|
|
35
|
+
def end = self['end']
|
|
36
|
+
|
|
37
|
+
# Returns the provider's field named +key+, a String or Symbol.
|
|
38
|
+
def [](key)
|
|
39
|
+
@attributes[key.to_s]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the provider's field named +key+ the way Hash#fetch does.
|
|
43
|
+
def fetch(key, ...)
|
|
44
|
+
@attributes.fetch(key.to_s, ...)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns +true+ when the provider sent a field named +key+.
|
|
48
|
+
def key?(key) = @attributes.key?(key.to_s)
|
|
49
|
+
alias has_key? key?
|
|
50
|
+
|
|
51
|
+
# Returns the provider's fields as a Hash with String keys.
|
|
52
|
+
def to_h = @attributes.dup
|
|
53
|
+
alias to_hash to_h
|
|
54
|
+
|
|
55
|
+
# Returns +true+ for a Segment or Hash holding the same fields.
|
|
56
|
+
def ==(other)
|
|
57
|
+
other.respond_to?(:to_h) && to_h == other.to_h.transform_keys(&:to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def inspect_attributes
|
|
63
|
+
{ text:, speaker:, start:, end: self.end }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -40,7 +40,8 @@ module RubyLLM
|
|
|
40
40
|
|
|
41
41
|
parse_format(body) if name == 'fmt '
|
|
42
42
|
@data = body.b if name == 'data'
|
|
43
|
-
offset += 8 + length
|
|
43
|
+
offset += 8 + length
|
|
44
|
+
offset += length % 2 if offset < content.bytesize
|
|
44
45
|
end
|
|
45
46
|
raise ArgumentError, 'WAV file contains a truncated chunk or padding' unless offset == content.bytesize
|
|
46
47
|
end
|
|
@@ -12,6 +12,7 @@ module RubyLLM
|
|
|
12
12
|
class Transcription
|
|
13
13
|
include Support::Inspectable
|
|
14
14
|
include Accounting::Usage::Result
|
|
15
|
+
include Accounting::Usage::ModelInfo
|
|
15
16
|
|
|
16
17
|
# The transcribed text.
|
|
17
18
|
attr_reader :text
|
|
@@ -26,13 +27,14 @@ module RubyLLM
|
|
|
26
27
|
# report it.
|
|
27
28
|
attr_reader :duration
|
|
28
29
|
|
|
29
|
-
# The timed segments of the transcript as an array of
|
|
30
|
-
# when the provider does not return segments. Diarization
|
|
31
|
-
# speaker label to each segment.
|
|
30
|
+
# The timed segments of the transcript as an array of Segment objects,
|
|
31
|
+
# or +nil+ when the provider does not return segments. Diarization
|
|
32
|
+
# models add a speaker label to each segment.
|
|
32
33
|
attr_reader :segments
|
|
33
34
|
|
|
34
|
-
# Word timing and speaker labels as an array of
|
|
35
|
-
# the provider does not return them. Request timing with
|
|
35
|
+
# Word timing and speaker labels as an array of Segment objects, or
|
|
36
|
+
# +nil+ when the provider does not return them. Request timing with
|
|
37
|
+
# +timestamps:+.
|
|
36
38
|
attr_reader :words
|
|
37
39
|
|
|
38
40
|
def initialize(text:, model:, **attributes) # :nodoc:
|
|
@@ -40,10 +42,11 @@ module RubyLLM
|
|
|
40
42
|
@model = model
|
|
41
43
|
@language = attributes[:language]
|
|
42
44
|
@duration = attributes[:duration]
|
|
43
|
-
@segments = attributes[:segments]
|
|
44
|
-
@words = attributes[:words]
|
|
45
|
+
@segments = Segment.wrap(attributes[:segments])
|
|
46
|
+
@words = Segment.wrap(attributes[:words])
|
|
45
47
|
@input_tokens = attributes[:input_tokens]
|
|
46
48
|
@output_tokens = attributes[:output_tokens]
|
|
49
|
+
@audio_seconds = attributes[:audio_seconds]
|
|
47
50
|
@reported_cost = attributes[:reported_cost]
|
|
48
51
|
end
|
|
49
52
|
|
|
@@ -51,7 +54,8 @@ module RubyLLM
|
|
|
51
54
|
def tokens
|
|
52
55
|
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
53
56
|
|
|
54
|
-
Tokens.new(input: @input_tokens, output: @output_tokens,
|
|
57
|
+
Tokens.new(input: @input_tokens, output: @output_tokens, audio_seconds: @audio_seconds,
|
|
58
|
+
reported_cost: @reported_cost)
|
|
55
59
|
end
|
|
56
60
|
|
|
57
61
|
# Returns the transcription cost across every provider attempt.
|
|
@@ -61,16 +65,8 @@ module RubyLLM
|
|
|
61
65
|
Cost.new(tokens:, model: model_info, category: :audio_tokens)
|
|
62
66
|
end
|
|
63
67
|
|
|
64
|
-
attr_writer :model_info # :nodoc:
|
|
65
|
-
|
|
66
|
-
def model_info # :nodoc:
|
|
67
|
-
@model_info ||= RubyLLM.models.find(model)
|
|
68
|
-
rescue ModelNotFoundError
|
|
69
|
-
nil
|
|
70
|
-
end
|
|
71
|
-
|
|
72
68
|
# Transcribes +audio_file+ and returns a Transcription. The file may be
|
|
73
|
-
# a path, URL,
|
|
69
|
+
# a path, URL, IO object, or Attachment. Uses
|
|
74
70
|
# <tt>config.default_transcription_model</tt> unless +model:+ is given.
|
|
75
71
|
# Pass +provider:+ and <tt>assume_model_exists: true</tt> to use a model
|
|
76
72
|
# that is not in the registry.
|
|
@@ -85,21 +81,23 @@ module RubyLLM
|
|
|
85
81
|
# )
|
|
86
82
|
#
|
|
87
83
|
# +language:+ hints at the spoken language using the provider's accepted
|
|
88
|
-
# ISO 639-1 or BCP-47 language code.
|
|
84
|
+
# ISO 639-1 or BCP-47 language code. Some providers also take an Array
|
|
85
|
+
# of candidate languages and report the one spoken as #language.
|
|
89
86
|
# +prompt:+ gives the model vocabulary or formatting guidance, and
|
|
90
87
|
# +temperature:+ adjusts sampling. +format:+ selects the transcript
|
|
91
88
|
# format in the provider's own vocabulary: OpenAI takes values such as
|
|
92
89
|
# <tt>"text"</tt>, <tt>"verbose_json"</tt>, or <tt>"diarized_json"</tt>,
|
|
93
90
|
# while Gemini takes a MIME type such as <tt>"text/plain"</tt>.
|
|
94
91
|
# +speaker_names:+ and +speaker_references:+ label the speakers on
|
|
95
|
-
# models that support diarization; references may be paths, URLs,
|
|
96
|
-
# objects. Option support depends on the selected model
|
|
92
|
+
# models that support diarization; references may be paths, URLs, IO
|
|
93
|
+
# objects, or Attachments. Option support depends on the selected model
|
|
94
|
+
# and provider.
|
|
97
95
|
# +timestamps:+ requests +:word+ timestamps. Some providers also accept
|
|
98
96
|
# +:segment+ or +:character+; unsupported granularities raise ArgumentError.
|
|
99
97
|
# +provider_options:+ takes options in the provider's request vocabulary
|
|
100
98
|
# and merges them into the rendered request as-is.
|
|
101
99
|
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
102
|
-
# over RubyLLM.
|
|
100
|
+
# over RubyLLM.with_owner.
|
|
103
101
|
#
|
|
104
102
|
# Given a block, the transcript streams: each TranscriptionChunk is
|
|
105
103
|
# yielded as it arrives and the completed Transcription is still
|
|
@@ -130,34 +128,12 @@ module RubyLLM
|
|
|
130
128
|
metadata: nil,
|
|
131
129
|
owner: nil,
|
|
132
130
|
&block)
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
provider: provider_instance.slug,
|
|
140
|
-
provider_class: provider_instance.name,
|
|
141
|
-
model: model.id,
|
|
142
|
-
model_info: model,
|
|
143
|
-
language: language,
|
|
144
|
-
provider_options: provider_options,
|
|
145
|
-
metadata: metadata,
|
|
146
|
-
tokens: empty_tokens,
|
|
147
|
-
cost: Cost.new(tokens: empty_tokens, model:, category: :audio_tokens)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
RubyLLM.instrument('transcription.ruby_llm', payload, config: config) do |event|
|
|
151
|
-
result = Accounting::Usage.owned_by(owner) do
|
|
152
|
-
provider_instance.transcribe(audio_file, model:, language:, format:, timestamps:, speaker_names:,
|
|
153
|
-
speaker_references:, provider_options:, prompt:,
|
|
154
|
-
temperature:, &block)
|
|
155
|
-
end
|
|
156
|
-
event[:result] = result
|
|
157
|
-
event[:response_model] = result.model
|
|
158
|
-
event[:tokens] = result.tokens
|
|
159
|
-
event[:cost] = result.cost
|
|
160
|
-
result
|
|
131
|
+
Accounting::Operation.new(
|
|
132
|
+
'transcription', model, default: :default_transcription_model, category: :audio_tokens, provider:,
|
|
133
|
+
assume_model_exists:, context:, owner:
|
|
134
|
+
).run(streaming: !block.nil?, language:, temperature:, provider_options:, metadata:) do |provider_instance, model|
|
|
135
|
+
provider_instance.transcribe(audio_file, model:, language:, format:, timestamps:, speaker_names:,
|
|
136
|
+
speaker_references:, provider_options:, prompt:, temperature:, &block)
|
|
161
137
|
end
|
|
162
138
|
end
|
|
163
139
|
|
|
@@ -13,19 +13,19 @@ module RubyLLM
|
|
|
13
13
|
include Support::Inspectable
|
|
14
14
|
|
|
15
15
|
# Text deltas, arriving as the model transcribes.
|
|
16
|
-
DELTA =
|
|
16
|
+
DELTA = :delta
|
|
17
17
|
|
|
18
18
|
# A tentative transcript that may change before its segment completes.
|
|
19
|
-
PARTIAL =
|
|
19
|
+
PARTIAL = :partial
|
|
20
20
|
|
|
21
21
|
# A completed segment, on models that return timed or diarized segments.
|
|
22
|
-
SEGMENT =
|
|
22
|
+
SEGMENT = :segment
|
|
23
23
|
|
|
24
24
|
# The final event, carrying the complete transcript.
|
|
25
|
-
DONE =
|
|
25
|
+
DONE = :done
|
|
26
26
|
|
|
27
|
-
# The
|
|
28
|
-
#
|
|
27
|
+
# The event type: +:delta+, +:partial+, +:segment+, or +:done+, or +nil+
|
|
28
|
+
# for a provider event RubyLLM does not normalize.
|
|
29
29
|
attr_reader :type
|
|
30
30
|
|
|
31
31
|
# The text added by this event, or +nil+ for events that add no text.
|
|
@@ -34,8 +34,8 @@ module RubyLLM
|
|
|
34
34
|
# The transcript for a partial event or the complete final transcript.
|
|
35
35
|
attr_reader :text
|
|
36
36
|
|
|
37
|
-
# The
|
|
38
|
-
# models label each segment with a speaker.
|
|
37
|
+
# The Transcription::Segment this event completed, or +nil+.
|
|
38
|
+
# Diarization models label each segment with a speaker.
|
|
39
39
|
attr_reader :segment
|
|
40
40
|
|
|
41
41
|
# The parsed provider event, for fields RubyLLM does not normalize.
|
|
@@ -45,7 +45,7 @@ module RubyLLM
|
|
|
45
45
|
@type = type
|
|
46
46
|
@delta = delta
|
|
47
47
|
@text = text
|
|
48
|
-
@segment = segment
|
|
48
|
+
@segment = Transcription::Segment.new(segment) if segment
|
|
49
49
|
@raw = raw
|
|
50
50
|
end
|
|
51
51
|
|