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
|
@@ -35,8 +35,9 @@ module RubyLLM
|
|
|
35
35
|
@mutex.synchronize { @subscribers = (@subscribers - [subscriber]).freeze }
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
def capture_context
|
|
39
|
-
|
|
38
|
+
def capture_context(config: nil)
|
|
39
|
+
instrumenter = (config || RubyLLM.config).instrumenter
|
|
40
|
+
(@subscribers | [instrumenter]).filter_map do |subscriber|
|
|
40
41
|
[subscriber, subscriber.capture_context] if subscriber.respond_to?(:capture_context)
|
|
41
42
|
end
|
|
42
43
|
end
|
|
@@ -63,15 +64,15 @@ module RubyLLM
|
|
|
63
64
|
private_class_method :dispatch
|
|
64
65
|
|
|
65
66
|
def current_workflow
|
|
66
|
-
|
|
67
|
+
Fiber[WORKFLOW_CONTEXT_KEY]
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
def with_workflow(context)
|
|
70
71
|
previous_context = current_workflow
|
|
71
|
-
|
|
72
|
+
Fiber[WORKFLOW_CONTEXT_KEY] = context
|
|
72
73
|
yield
|
|
73
74
|
ensure
|
|
74
|
-
|
|
75
|
+
Fiber[WORKFLOW_CONTEXT_KEY] = previous_context
|
|
75
76
|
end
|
|
76
77
|
end
|
|
77
78
|
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support
|
|
5
|
+
# The +rescue_from+ macro Agent, Tool, and MCP share: handlers named
|
|
6
|
+
# with +with:+ or given as a block, searched in reverse declaration
|
|
7
|
+
# order from the class up through its ancestors when an exception is
|
|
8
|
+
# raised, and run on the instance whose work raised.
|
|
9
|
+
module Rescuable # :nodoc:
|
|
10
|
+
def self.included(base)
|
|
11
|
+
base.extend(ClassMethods)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# The class side of Rescuable.
|
|
15
|
+
module ClassMethods # :nodoc:
|
|
16
|
+
def rescue_from(*exception_classes, with: nil, &block)
|
|
17
|
+
raise ArgumentError, 'rescue_from needs a handler: pass with: or a block' unless with || block
|
|
18
|
+
raise ArgumentError, 'rescue_from takes with: or a block, not both' if with && block
|
|
19
|
+
|
|
20
|
+
exception_classes.flatten.each do |exception_class|
|
|
21
|
+
rescue_handlers << [rescue_handler_key(exception_class), with || block]
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def rescue_handlers
|
|
26
|
+
@rescue_handlers ||= []
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the handler for +exception+, searched from this class up
|
|
30
|
+
# through +within+, or through every ancestor without it.
|
|
31
|
+
def rescue_handler_for(exception, within: nil)
|
|
32
|
+
rescue_handlers.reverse_each do |class_name, handler|
|
|
33
|
+
exception_class = rescue_handler_class(class_name)
|
|
34
|
+
return handler if exception_class && exception.is_a?(exception_class)
|
|
35
|
+
end
|
|
36
|
+
return if equal?(within)
|
|
37
|
+
|
|
38
|
+
superclass.rescue_handler_for(exception, within:) if superclass.respond_to?(:rescue_handler_for)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def rescue_handlers?
|
|
42
|
+
rescue_handlers.any? || (superclass.respond_to?(:rescue_handlers?) && superclass.rescue_handlers?)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def rescue_handler_key(exception_class)
|
|
48
|
+
case exception_class
|
|
49
|
+
when ->(klass) { klass.is_a?(Module) && !(klass.is_a?(Class) && (klass <=> StandardError).nil?) }
|
|
50
|
+
exception_class.name || exception_class
|
|
51
|
+
when String then exception_class
|
|
52
|
+
else raise ArgumentError, "#{exception_class.inspect} is not a StandardError class or its name"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def rescue_handler_class(class_name)
|
|
57
|
+
return class_name if class_name.is_a?(Module)
|
|
58
|
+
|
|
59
|
+
Object.const_get(class_name)
|
|
60
|
+
rescue NameError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Runs the ::rescue_from handler matching +exception+ and returns its
|
|
66
|
+
# value, or re-raises when no handler matches.
|
|
67
|
+
def rescue_with_handler(exception)
|
|
68
|
+
handler = self.class.rescue_handler_for(exception)
|
|
69
|
+
raise exception unless handler
|
|
70
|
+
|
|
71
|
+
run_rescue_handler(handler, exception)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def run_rescue_handler(handler, exception)
|
|
77
|
+
return instance_exec(exception, &handler) unless handler.is_a?(Symbol)
|
|
78
|
+
|
|
79
|
+
handler_method = method(handler)
|
|
80
|
+
handler_method.arity.zero? ? handler_method.call : handler_method.call(exception)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -12,9 +12,24 @@ module RubyLLM
|
|
|
12
12
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
|
-
def
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
def cache_directory
|
|
16
|
+
home = Dir.home
|
|
17
|
+
host_os = RbConfig::CONFIG['host_os']
|
|
18
|
+
|
|
19
|
+
if host_os.match?(/darwin/i)
|
|
20
|
+
File.join(home, 'Library', 'Caches', 'RubyLLM')
|
|
21
|
+
elsif host_os.match?(/mswin|mingw|cygwin/i)
|
|
22
|
+
local_app_data = ENV.fetch('LOCALAPPDATA', nil)
|
|
23
|
+
local_app_data = File.join(home, 'AppData', 'Local') if local_app_data.to_s.empty?
|
|
24
|
+
File.join(local_app_data, 'RubyLLM', 'Cache')
|
|
25
|
+
else
|
|
26
|
+
xdg_cache = ENV.fetch('XDG_CACHE_HOME', nil)
|
|
27
|
+
xdg_cache = File.join(home, '.cache') if xdg_cache.to_s.empty?
|
|
28
|
+
File.join(xdg_cache, 'ruby_llm')
|
|
29
|
+
end
|
|
30
|
+
rescue ArgumentError
|
|
31
|
+
# Dir.home raises when the home directory cannot be determined.
|
|
32
|
+
nil
|
|
18
33
|
end
|
|
19
34
|
|
|
20
35
|
def strip_schema_metadata(schema)
|
|
@@ -28,6 +43,14 @@ module RubyLLM
|
|
|
28
43
|
name.gsub(UNDERSCORE_BOUNDARY, '_').downcase
|
|
29
44
|
end
|
|
30
45
|
|
|
46
|
+
# The path Rails autoloads a class from, which follows the application's
|
|
47
|
+
# inflections when Active Support is loaded.
|
|
48
|
+
def file_path(class_name)
|
|
49
|
+
return ActiveSupport::Inflector.underscore(class_name) if defined?(ActiveSupport::Inflector)
|
|
50
|
+
|
|
51
|
+
underscore(class_name.gsub('::', '/'))
|
|
52
|
+
end
|
|
53
|
+
|
|
31
54
|
def to_safe_array(item)
|
|
32
55
|
case item
|
|
33
56
|
when Array then item
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A tool call that finishes later. Return one from Tool#execute and a
|
|
5
|
+
# chat pauses the call instead of waiting for it, the way it pauses for
|
|
6
|
+
# an approval. Chat#pending_tasks returns the tasks it waits on, and each
|
|
7
|
+
# Chat#complete hands every one back to its tool's Tool#resume, which
|
|
8
|
+
# returns the call's result once it has one:
|
|
9
|
+
#
|
|
10
|
+
# class RequestSignature < RubyLLM::Tool
|
|
11
|
+
# description "Sends a contract for signature and reports when it is signed"
|
|
12
|
+
#
|
|
13
|
+
# def execute(contract_id:)
|
|
14
|
+
# envelope = Signatures.send_for_signature(Contract.find(contract_id))
|
|
15
|
+
# RubyLLM::Task.new(status: :input_required, data: { "envelope_id" => envelope.id })
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# def resume(task, **)
|
|
19
|
+
# envelope = Signatures.find(task.data["envelope_id"])
|
|
20
|
+
# envelope.signed? ? "Signed on #{envelope.signed_on}" : task
|
|
21
|
+
# end
|
|
22
|
+
# end
|
|
23
|
+
#
|
|
24
|
+
# A task is saved with its tool call as #to_h, so a Rails chat can pause
|
|
25
|
+
# in one process and resume in another. MCP::Task is the task of an MCP
|
|
26
|
+
# tool call that a server runs in the background.
|
|
27
|
+
class Task
|
|
28
|
+
include Support::Inspectable
|
|
29
|
+
include Support::DeprecatedJobPredicates
|
|
30
|
+
|
|
31
|
+
FINISHED = %i[completed failed cancelled].freeze
|
|
32
|
+
STATUSES = %i[working input_required].concat(FINISHED).freeze
|
|
33
|
+
private_constant :FINISHED, :STATUSES
|
|
34
|
+
|
|
35
|
+
# The task's state as a Symbol: +:working+, +:input_required+,
|
|
36
|
+
# +:completed+, +:failed+, or +:cancelled+.
|
|
37
|
+
attr_reader :status
|
|
38
|
+
|
|
39
|
+
# What the task is doing, such as how far it got, or +nil+.
|
|
40
|
+
attr_reader :status_message
|
|
41
|
+
|
|
42
|
+
# How many seconds to wait before checking on the task again, or +nil+.
|
|
43
|
+
attr_reader :poll_interval
|
|
44
|
+
|
|
45
|
+
# The tool's own state for the task, a Hash with String keys that
|
|
46
|
+
# serializes to JSON.
|
|
47
|
+
attr_reader :data
|
|
48
|
+
|
|
49
|
+
# The ToolCall paused on the task, when it came from a chat.
|
|
50
|
+
attr_reader :tool_call
|
|
51
|
+
|
|
52
|
+
# Returns the Task that +state+, the Hash #to_h returned, describes.
|
|
53
|
+
def self.load(state, tool_call: nil)
|
|
54
|
+
task = state.fetch('task')
|
|
55
|
+
new(status: task.fetch('status'), status_message: task['status_message'],
|
|
56
|
+
poll_interval: task['poll_interval'], data: task.fetch('data', {}), tool_call:)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Returns a task with +status+, +:working+ unless the call waits on a
|
|
60
|
+
# person, +:input_required+. +data+ is what Tool#resume needs to carry
|
|
61
|
+
# the call on, as a Hash that serializes to JSON. Raises ArgumentError
|
|
62
|
+
# for a status other than the five #status lists.
|
|
63
|
+
def initialize(status: :working, status_message: nil, poll_interval: nil, data: {}, tool_call: nil)
|
|
64
|
+
@status = status.to_sym
|
|
65
|
+
raise ArgumentError, "Unknown task status: #{status.inspect}" unless STATUSES.include?(@status)
|
|
66
|
+
|
|
67
|
+
@status_message = status_message
|
|
68
|
+
@poll_interval = poll_interval
|
|
69
|
+
@data = Support::Utils.deep_stringify_keys(Support::Utils.deep_dup(data))
|
|
70
|
+
@tool_call = tool_call
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns +true+ when the task waits on a person, so checking on it
|
|
74
|
+
# changes nothing until they act.
|
|
75
|
+
def input_required?
|
|
76
|
+
status == :input_required
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Returns +true+ once the task completed, failed, or was cancelled.
|
|
80
|
+
def complete?
|
|
81
|
+
FINISHED.include?(status)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns +true+ when the task finished with a result.
|
|
85
|
+
def succeeded?
|
|
86
|
+
status == :completed
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns +true+ when the task failed with an error.
|
|
90
|
+
def failed?
|
|
91
|
+
status == :failed
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns +true+ when the task was cancelled before it finished.
|
|
95
|
+
def cancelled?
|
|
96
|
+
status == :cancelled
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Checks on the task once and returns +self+. Subclasses that can check
|
|
100
|
+
# on their work override it; the base implementation does nothing.
|
|
101
|
+
def refresh
|
|
102
|
+
self
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Stops the task and returns +self+. A chat calls it when it is
|
|
106
|
+
# cancelled while the call waits. Subclasses that can stop their work
|
|
107
|
+
# override it; the base implementation does nothing.
|
|
108
|
+
def cancel
|
|
109
|
+
self
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Returns the task as a Hash that serializes to JSON, which ::load turns
|
|
113
|
+
# back into the task.
|
|
114
|
+
def to_h
|
|
115
|
+
task = { 'status' => status.to_s, 'status_message' => status_message,
|
|
116
|
+
'poll_interval' => poll_interval, 'data' => (data unless data.empty?) }
|
|
117
|
+
{ 'task' => task.compact }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def inspect_attributes
|
|
123
|
+
{ status:, status_message: }
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ruby_llm'
|
|
4
|
+
|
|
5
|
+
module RubyLLM
|
|
6
|
+
# Lets a test script what the model replies, with no provider and no API
|
|
7
|
+
# key. Replies go through the real conversation loop, so streaming, tools,
|
|
8
|
+
# persistence, cost, and #cancel all behave as they do in production.
|
|
9
|
+
# Require it in your test helper, and include it like ActiveJob::TestHelper:
|
|
10
|
+
#
|
|
11
|
+
# require "ruby_llm/test_helper"
|
|
12
|
+
#
|
|
13
|
+
# class SupportAgentTest < ActiveSupport::TestCase
|
|
14
|
+
# include RubyLLM::TestHelper
|
|
15
|
+
#
|
|
16
|
+
# test "answers" do
|
|
17
|
+
# stub_replies "Hello there", cost: 0.01
|
|
18
|
+
#
|
|
19
|
+
# reply = SupportAgent.new.ask("Hi")
|
|
20
|
+
#
|
|
21
|
+
# assert_equal "Hello there", reply.content
|
|
22
|
+
# assert_equal 0.01, reply.cost.total
|
|
23
|
+
# end
|
|
24
|
+
# end
|
|
25
|
+
#
|
|
26
|
+
# It also works in RSpec: <tt>config.include RubyLLM::TestHelper</tt>. A
|
|
27
|
+
# chat's request reaches the stub. Other operations, such as RubyLLM.embed,
|
|
28
|
+
# still call their provider.
|
|
29
|
+
module TestHelper
|
|
30
|
+
# The scripted replies, shared across threads because a system test's
|
|
31
|
+
# server and its jobs run in threads of their own.
|
|
32
|
+
module Script # :nodoc:
|
|
33
|
+
@lock = Mutex.new
|
|
34
|
+
@replies = nil
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
def stub(replies, options)
|
|
38
|
+
@lock.synchronize { @replies = [replies, options] }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def clear
|
|
42
|
+
@lock.synchronize { @replies = nil }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def active?
|
|
46
|
+
@lock.synchronize { !@replies.nil? }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The next reply. The last one repeats, so a test that doesn't count
|
|
50
|
+
# requests only names one.
|
|
51
|
+
def next_reply
|
|
52
|
+
@lock.synchronize do
|
|
53
|
+
replies, options = @replies
|
|
54
|
+
[replies.size > 1 ? replies.shift : replies.first, options]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Answers requests from the script, once #stub_replies has been called.
|
|
61
|
+
module StubbedProvider # :nodoc:
|
|
62
|
+
# Skips the API key check, which some providers define for themselves.
|
|
63
|
+
def initialize(config)
|
|
64
|
+
if Script.active?
|
|
65
|
+
@config = config
|
|
66
|
+
@connection = Transport::Connection.new(self, config)
|
|
67
|
+
else
|
|
68
|
+
super
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def complete(messages, usage_recorder: nil, progress_recorder: nil, **options, &)
|
|
73
|
+
return super unless Script.active?
|
|
74
|
+
|
|
75
|
+
reply, settings = Script.next_reply
|
|
76
|
+
raise reply if reply.is_a?(Exception)
|
|
77
|
+
|
|
78
|
+
tool_call = reply if reply.is_a?(ToolCall)
|
|
79
|
+
text = tool_call ? '' : reply.to_s
|
|
80
|
+
stream(text, settings, options[:model], &) if block_given?
|
|
81
|
+
Message.new(role: :assistant, content: text, model_id: options[:model].id,
|
|
82
|
+
tool_calls: tool_call && { tool_call.id => tool_call }, input_tokens: 1, output_tokens: 1,
|
|
83
|
+
reported_cost: settings[:cost])
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def stream(text, settings, model)
|
|
89
|
+
text.scan(/\S+\s*|\s+/).each do |word|
|
|
90
|
+
sleep settings[:interval] if settings[:interval]
|
|
91
|
+
yield Chunk.new(role: :assistant, content: word, model_id: model.id)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
Provider.prepend(StubbedProvider)
|
|
97
|
+
|
|
98
|
+
def self.included(base) # :nodoc:
|
|
99
|
+
base.after { Script.clear } if base.respond_to?(:after)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def after_teardown # :nodoc:
|
|
103
|
+
super
|
|
104
|
+
ensure
|
|
105
|
+
Script.clear
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Makes every chat answer with +replies+ until the test ends, in order,
|
|
109
|
+
# repeating the last, instead of calling a provider. A String streams
|
|
110
|
+
# word by word, #tool_call asks the conversation loop to run a tool, and
|
|
111
|
+
# an exception such as RubyLLM::ServerError fails the request.
|
|
112
|
+
# +cost:+ is what each reply reports, in dollars. +interval:+ is the
|
|
113
|
+
# seconds between words, so a test can press Stop while a reply streams.
|
|
114
|
+
#
|
|
115
|
+
# stub_replies "Hello there", cost: 0.01
|
|
116
|
+
# stub_replies tool_call(:weather, city: "Berlin"), "It is sunny."
|
|
117
|
+
# stub_replies "A long reply", interval: 0.1
|
|
118
|
+
#
|
|
119
|
+
def stub_replies(*replies, cost: nil, interval: nil)
|
|
120
|
+
Script.stub(replies, { cost:, interval: })
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns a reply that calls the tool +name+ with +arguments+, for
|
|
124
|
+
# #stub_replies.
|
|
125
|
+
def tool_call(name, **arguments)
|
|
126
|
+
ToolCall.new(id: "call_#{SecureRandom.hex(4)}", name: name.to_s, arguments:)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
data/lib/ruby_llm/thinking.rb
CHANGED
|
@@ -134,14 +134,14 @@ module RubyLLM
|
|
|
134
134
|
enabled == false || effort == :none
|
|
135
135
|
end
|
|
136
136
|
|
|
137
|
-
def resolve(model)
|
|
137
|
+
def resolve(model, strict: true)
|
|
138
138
|
return self if @intent.nil?
|
|
139
139
|
|
|
140
140
|
options = Controls.new(model).public_send(@intent)
|
|
141
141
|
return nil if options == :already_on
|
|
142
|
-
raise ArgumentError, resolution_error(model)
|
|
142
|
+
raise ArgumentError, resolution_error(model) if strict && !options
|
|
143
143
|
|
|
144
|
-
self.class.new(**options)
|
|
144
|
+
self.class.new(**(options || { enabled: @intent == :enable }))
|
|
145
145
|
end
|
|
146
146
|
|
|
147
147
|
private
|
data/lib/ruby_llm/tokens.rb
CHANGED
|
@@ -12,11 +12,23 @@ module RubyLLM
|
|
|
12
12
|
# response.tokens.cache_read # prompt cache reads
|
|
13
13
|
# response.tokens.cache_write # prompt cache writes
|
|
14
14
|
#
|
|
15
|
+
# Operations a provider bills in other units count those units here too,
|
|
16
|
+
# so Cost prices them the same way:
|
|
17
|
+
#
|
|
18
|
+
# RubyLLM.speak("Hello").tokens.characters # => 5
|
|
19
|
+
# RubyLLM.transcribe("call.mp3").tokens.audio_seconds # => 42.3
|
|
20
|
+
# RubyLLM.rerank(query, documents).tokens.search_units # => 1
|
|
21
|
+
#
|
|
15
22
|
class Tokens
|
|
16
23
|
include Support::Inspectable
|
|
17
24
|
|
|
18
25
|
READERS = %i[
|
|
19
|
-
input output cache_read cache_write thinking
|
|
26
|
+
input output cache_read cache_write thinking characters audio_seconds search_units cache_write_by_ttl
|
|
27
|
+
server_tool_use reported_cost
|
|
28
|
+
].freeze # :nodoc:
|
|
29
|
+
|
|
30
|
+
SUMMED = %i[
|
|
31
|
+
input output cache_read cache_write thinking characters audio_seconds search_units reported_cost
|
|
20
32
|
].freeze # :nodoc:
|
|
21
33
|
|
|
22
34
|
# The number of standard (non-cached) input tokens, or +nil+ if the
|
|
@@ -50,6 +62,20 @@ module RubyLLM
|
|
|
50
62
|
# does not report them.
|
|
51
63
|
attr_reader :thinking
|
|
52
64
|
|
|
65
|
+
# The number of input characters the provider bills, such as the text
|
|
66
|
+
# sent to a text-to-speech model, or +nil+ if the operation is not
|
|
67
|
+
# billed by character.
|
|
68
|
+
attr_reader :characters
|
|
69
|
+
|
|
70
|
+
# The seconds of audio the provider bills, such as the length of a
|
|
71
|
+
# transcribed recording, or +nil+ if the operation is not billed by
|
|
72
|
+
# duration. May be fractional.
|
|
73
|
+
attr_reader :audio_seconds
|
|
74
|
+
|
|
75
|
+
# The number of search units the provider bills, such as Cohere's
|
|
76
|
+
# rerank searches, or +nil+ if the operation is not billed by search.
|
|
77
|
+
attr_reader :search_units
|
|
78
|
+
|
|
53
79
|
# How many times each provider-executed tool ran, as a Hash of String
|
|
54
80
|
# keys and Integer counts, or +nil+ if the provider reported none.
|
|
55
81
|
# Web searches are <tt>"web_search_requests"</tt> on every provider
|
|
@@ -72,20 +98,26 @@ module RubyLLM
|
|
|
72
98
|
# even when every count is +nil+.
|
|
73
99
|
#
|
|
74
100
|
# tokens = RubyLLM::Tokens.new(input: 100, output: 20)
|
|
75
|
-
# RubyLLM.models.find("gpt-
|
|
101
|
+
# RubyLLM.models.find("gpt-6.1-sol").cost_for(tokens).total
|
|
76
102
|
#
|
|
103
|
+
# +characters:+, +audio_seconds:+, and +search_units:+ count usage
|
|
104
|
+
# billed in those units;
|
|
77
105
|
# +cache_write_by_ttl:+ splits the cache writes by lifetime, keyed as
|
|
78
106
|
# #cache_write_by_ttl describes;
|
|
79
107
|
# +server_tool_use:+ counts the provider-executed tools that ran, keyed
|
|
80
108
|
# as #server_tool_use describes;
|
|
81
109
|
# +reported_cost:+ holds the provider's total price in US dollars.
|
|
82
110
|
def initialize(input: nil, output: nil, cache_read: nil, cache_write: nil, thinking: nil,
|
|
111
|
+
characters: nil, audio_seconds: nil, search_units: nil,
|
|
83
112
|
cache_write_by_ttl: nil, server_tool_use: nil, reported_cost: nil)
|
|
84
113
|
@input = input
|
|
85
114
|
@output = output
|
|
86
115
|
@cache_read = cache_read
|
|
87
116
|
@cache_write = cache_write
|
|
88
117
|
@thinking = thinking
|
|
118
|
+
@characters = characters
|
|
119
|
+
@audio_seconds = audio_seconds
|
|
120
|
+
@search_units = search_units
|
|
89
121
|
@cache_write_by_ttl = positive_counts(cache_write_by_ttl)
|
|
90
122
|
@server_tool_use = positive_counts(server_tool_use)
|
|
91
123
|
@reported_cost = reported_cost
|
|
@@ -95,10 +127,9 @@ module RubyLLM
|
|
|
95
127
|
# no attempt reported it.
|
|
96
128
|
def self.aggregate(tokens)
|
|
97
129
|
tokens = Array(tokens).compact
|
|
98
|
-
return new if tokens.empty?
|
|
99
130
|
return tokens.first if tokens.one?
|
|
100
131
|
|
|
101
|
-
values =
|
|
132
|
+
values = SUMMED.to_h do |component|
|
|
102
133
|
reported = tokens.filter_map { |usage| usage.public_send(component) }
|
|
103
134
|
[component, reported.empty? ? nil : reported.sum]
|
|
104
135
|
end
|
|
@@ -114,20 +145,22 @@ module RubyLLM
|
|
|
114
145
|
end)
|
|
115
146
|
end
|
|
116
147
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
148
|
+
def without_reported_cost # :nodoc:
|
|
149
|
+
self.class.new(**(READERS - [:reported_cost]).to_h { |reader| [reader, public_send(reader)] })
|
|
150
|
+
end
|
|
120
151
|
|
|
121
|
-
|
|
122
|
-
|
|
152
|
+
def self.aggregate_counts(tokens, reader) # :nodoc:
|
|
153
|
+
tokens.filter_map(&reader).each_with_object({}) do |counters, total|
|
|
154
|
+
counters.each { |key, count| total[key] = total.fetch(key, 0) + (count.is_a?(Float) ? count : count.to_i) }
|
|
123
155
|
end
|
|
124
156
|
end
|
|
125
157
|
private_class_method :aggregate_counts
|
|
126
158
|
|
|
127
159
|
# Returns the counts as a hash with keys +:input_tokens+,
|
|
128
160
|
# +:output_tokens+, +:cache_read_tokens+, +:cache_write_tokens+, and
|
|
129
|
-
# +:thinking_tokens+, omitting +nil+ counts. Includes
|
|
130
|
-
# +:
|
|
161
|
+
# +:thinking_tokens+, omitting +nil+ counts. Includes +:characters+,
|
|
162
|
+
# +:audio_seconds+, +:search_units+, +:cache_write_tokens_by_ttl+, and
|
|
163
|
+
# +:server_tool_use+ when reported.
|
|
131
164
|
# The provider's reported cost is available separately
|
|
132
165
|
# through #reported_cost.
|
|
133
166
|
#
|
|
@@ -141,6 +174,9 @@ module RubyLLM
|
|
|
141
174
|
cache_read_tokens: cache_read,
|
|
142
175
|
cache_write_tokens: cache_write,
|
|
143
176
|
thinking_tokens: thinking,
|
|
177
|
+
characters: characters,
|
|
178
|
+
audio_seconds: audio_seconds,
|
|
179
|
+
search_units: search_units,
|
|
144
180
|
cache_write_tokens_by_ttl: cache_write_by_ttl,
|
|
145
181
|
server_tool_use: server_tool_use
|
|
146
182
|
}.compact
|
|
@@ -153,6 +189,9 @@ module RubyLLM
|
|
|
153
189
|
cache_read: cache_read,
|
|
154
190
|
cache_write: cache_write,
|
|
155
191
|
thinking: thinking,
|
|
192
|
+
characters: characters,
|
|
193
|
+
audio_seconds: audio_seconds,
|
|
194
|
+
search_units: search_units,
|
|
156
195
|
cache_write_by_ttl: cache_write_by_ttl,
|
|
157
196
|
server_tool_use: server_tool_use
|
|
158
197
|
}.compact
|
|
@@ -161,7 +200,8 @@ module RubyLLM
|
|
|
161
200
|
private
|
|
162
201
|
|
|
163
202
|
def positive_counts(counts)
|
|
164
|
-
used = counts.to_h.to_h { |key, count| [key.to_s, count.
|
|
203
|
+
used = counts.to_h.to_h { |key, count| [key.to_s, count.is_a?(Float) ? count : count.to_i] }
|
|
204
|
+
.select { |_, count| count.positive? }
|
|
165
205
|
used unless used.empty?
|
|
166
206
|
end
|
|
167
207
|
end
|