ruby_llm 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +160 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +515 -274
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1593 -331
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +50 -24
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +127 -106
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +15 -15
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -7
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +5 -14
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +213 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
data/lib/ruby_llm/tool.rb
CHANGED
|
@@ -38,8 +38,16 @@ module RubyLLM
|
|
|
38
38
|
# or ::parameters when arguments need explicit types, descriptions, or
|
|
39
39
|
# structure.
|
|
40
40
|
class Tool
|
|
41
|
+
include Support::Rescuable
|
|
42
|
+
|
|
41
43
|
KEYWORD_PARAMETER_KINDS = %i[keyreq key].freeze # :nodoc:
|
|
42
44
|
TOOL_CALL_KEYWORD = :tool_call # :nodoc:
|
|
45
|
+
ASKING = :ruby_llm_tool_asking # :nodoc:
|
|
46
|
+
HALT = :ruby_llm_tool_halt # :nodoc:
|
|
47
|
+
|
|
48
|
+
# A result that ends the turn, from #halt.
|
|
49
|
+
Halt = Data.define(:content) # :nodoc:
|
|
50
|
+
RESUMING = :ruby_llm_tool_resuming # :nodoc:
|
|
43
51
|
|
|
44
52
|
DUPED_INHERITED_CONFIG = {
|
|
45
53
|
:@declared_parameters => {},
|
|
@@ -47,15 +55,73 @@ module RubyLLM
|
|
|
47
55
|
}.freeze
|
|
48
56
|
COPIED_INHERITED_CONFIG = %i[
|
|
49
57
|
@description
|
|
58
|
+
@namespace
|
|
59
|
+
@namespace_description
|
|
50
60
|
@parameters_schema_definition
|
|
51
61
|
@requires_approval
|
|
52
62
|
@approval_resolver
|
|
53
63
|
@deferred
|
|
64
|
+
@exclusive
|
|
65
|
+
@max_run_time
|
|
66
|
+
@inline_limit
|
|
54
67
|
].freeze
|
|
55
68
|
private_constant :DUPED_INHERITED_CONFIG, :COPIED_INHERITED_CONFIG
|
|
56
69
|
|
|
70
|
+
##
|
|
71
|
+
# :singleton-method: rescue_from
|
|
72
|
+
# :call-seq: rescue_from(*exception_classes, with: nil, &block)
|
|
73
|
+
#
|
|
74
|
+
# Registers a handler for exceptions a call raises when a chat runs
|
|
75
|
+
# the tool, from #execute, #resume, or a #call the tool overrides. Name
|
|
76
|
+
# the handler with +with:+ or pass a block; either runs on the tool
|
|
77
|
+
# instance. The handler's return value becomes the call's result, as
|
|
78
|
+
# if #execute had returned it, so the model reads it and the chat
|
|
79
|
+
# goes on. Re-raise inside a handler to stop the chat with the
|
|
80
|
+
# exception, as an unhandled one does. Handlers only run for calls a
|
|
81
|
+
# chat makes. Calling #call or #execute yourself is supported and
|
|
82
|
+
# runs without handlers, so exceptions reach you.
|
|
83
|
+
#
|
|
84
|
+
# class ApplicationTool < RubyLLM::Tool
|
|
85
|
+
# rescue_from StandardError, with: :report_failure
|
|
86
|
+
#
|
|
87
|
+
# rescue_from Faraday::Error do |error|
|
|
88
|
+
# { error: "The service didn't answer (#{error.class.name.split("::").last}). Try again later." }
|
|
89
|
+
# end
|
|
90
|
+
#
|
|
91
|
+
# private
|
|
92
|
+
#
|
|
93
|
+
# def report_failure(error)
|
|
94
|
+
# Rails.error.report(error, context: { tool: name })
|
|
95
|
+
# { error: "This tool failed unexpectedly. Trying the same call again won't help." }
|
|
96
|
+
# end
|
|
97
|
+
# end
|
|
98
|
+
#
|
|
99
|
+
# Handlers are searched in reverse declaration order, a class's own
|
|
100
|
+
# before its parent's, so the last matching one wins. Subclasses use
|
|
101
|
+
# their parents' handlers, including ones declared after the subclass,
|
|
102
|
+
# so a handler on Tool itself also covers RubyLLM's own tools, such as
|
|
103
|
+
# CodeExecution. Declare handlers for a server's tools on its MCP
|
|
104
|
+
# class. A question from Tool#ask and a cancelled chat never reach a
|
|
105
|
+
# handler.
|
|
106
|
+
#
|
|
107
|
+
# Exception classes may be named as Strings, which defers constant
|
|
108
|
+
# lookup until an exception is raised. A class that can never match a
|
|
109
|
+
# StandardError, such as Interrupt, raises ArgumentError.
|
|
110
|
+
|
|
57
111
|
class << self
|
|
58
|
-
attr_reader :parameters_schema_definition, :approval_resolver # :nodoc:
|
|
112
|
+
attr_reader :parameters_schema_definition, :approval_resolver, :namespace_description # :nodoc:
|
|
113
|
+
|
|
114
|
+
# Opens the questions of the call a chat makes of +tool+, with
|
|
115
|
+
# +input+, the answers Chat saved for it, so the call and its
|
|
116
|
+
# ::rescue_from handler ask in turn and each reads its answers when
|
|
117
|
+
# the call runs again.
|
|
118
|
+
def asking(tool, input) # :nodoc:
|
|
119
|
+
previous = Fiber[ASKING]
|
|
120
|
+
Fiber[ASKING] = { 'input' => input, 'asked' => 0, 'tool' => tool }
|
|
121
|
+
yield
|
|
122
|
+
ensure
|
|
123
|
+
Fiber[ASKING] = previous
|
|
124
|
+
end
|
|
59
125
|
|
|
60
126
|
def inherited(subclass) # :nodoc:
|
|
61
127
|
super
|
|
@@ -102,6 +168,29 @@ module RubyLLM
|
|
|
102
168
|
@description = text
|
|
103
169
|
end
|
|
104
170
|
|
|
171
|
+
# Puts the tool in a namespace, or returns its namespace. Tools in
|
|
172
|
+
# different namespaces can share a name. +description:+ tells the
|
|
173
|
+
# model what the namespace's tools are for, on APIs that describe
|
|
174
|
+
# namespaces. Pass +nil+ to remove an inherited namespace.
|
|
175
|
+
#
|
|
176
|
+
# class CustomerLookup < RubyLLM::Tool
|
|
177
|
+
# namespace "crm", description: "Customer records and orders"
|
|
178
|
+
# end
|
|
179
|
+
#
|
|
180
|
+
# A native client toolset, such as Anthropic's computer toolset, calls
|
|
181
|
+
# the tools in its own namespace:
|
|
182
|
+
#
|
|
183
|
+
# class Screenshot < RubyLLM::Tool
|
|
184
|
+
# namespace "computer"
|
|
185
|
+
# end
|
|
186
|
+
def namespace(value = (get = true), description: nil)
|
|
187
|
+
return @namespace if get
|
|
188
|
+
|
|
189
|
+
@namespace = value&.to_s
|
|
190
|
+
@namespace_description = description
|
|
191
|
+
@namespace
|
|
192
|
+
end
|
|
193
|
+
|
|
105
194
|
# Declares a parameter for the tool. +options+ accepts +type:+
|
|
106
195
|
# (defaults to <tt>'string'</tt>), +description:+, and +required:+
|
|
107
196
|
# (defaults to +true+).
|
|
@@ -149,7 +238,7 @@ module RubyLLM
|
|
|
149
238
|
|
|
150
239
|
# Declares that this tool must be approved before it executes. The
|
|
151
240
|
# conversation loop pauses the tool call until a decision is recorded with
|
|
152
|
-
# Chat#approve or Chat#
|
|
241
|
+
# Chat#approve or Chat#decline, so Chat#complete returns cleanly and
|
|
153
242
|
# can be called again once the decision exists. In Rails the decision
|
|
154
243
|
# persists on the tool call record and survives process restarts.
|
|
155
244
|
#
|
|
@@ -182,6 +271,34 @@ module RubyLLM
|
|
|
182
271
|
@requires_approval || false
|
|
183
272
|
end
|
|
184
273
|
|
|
274
|
+
# Declares that this tool runs by itself. When a chat runs a round of
|
|
275
|
+
# tool calls concurrently (see Chat#with_tool_options), an exclusive
|
|
276
|
+
# call waits for the calls before it to finish, and the calls after
|
|
277
|
+
# it wait for it, while the other tools still run in parallel. Use it
|
|
278
|
+
# for tools that share state, such as a browser page. When an
|
|
279
|
+
# exclusive call waits for an approval or a Task, the calls after it
|
|
280
|
+
# stay pending until it finishes.
|
|
281
|
+
#
|
|
282
|
+
# class BrowserClick < RubyLLM::Tool
|
|
283
|
+
# exclusive
|
|
284
|
+
# end
|
|
285
|
+
#
|
|
286
|
+
def exclusive
|
|
287
|
+
@exclusive = true
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Returns whether this tool was declared with ::exclusive.
|
|
291
|
+
def exclusive?
|
|
292
|
+
@exclusive || false
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Seconds a run of this tool may take before a persisted chat treats
|
|
296
|
+
# it as crashed, overriding Configuration#max_run_time.
|
|
297
|
+
def max_run_time(seconds = nil) # :nodoc:
|
|
298
|
+
@max_run_time = seconds if seconds
|
|
299
|
+
@max_run_time
|
|
300
|
+
end
|
|
301
|
+
|
|
185
302
|
# :call-seq:
|
|
186
303
|
# provider_options(options) -> self
|
|
187
304
|
# provider_options -> hash
|
|
@@ -219,6 +336,32 @@ module RubyLLM
|
|
|
219
336
|
@deferred || false
|
|
220
337
|
end
|
|
221
338
|
|
|
339
|
+
# :call-seq:
|
|
340
|
+
# inline_limit(characters) -> characters
|
|
341
|
+
# inline_limit(false) -> false
|
|
342
|
+
# inline_limit -> characters, false, or nil
|
|
343
|
+
#
|
|
344
|
+
# Sets how many characters this tool's results may have and still go
|
|
345
|
+
# to the model whole. In a chat with a file store, a longer result is
|
|
346
|
+
# saved as a file, and the model gets its start and the file's path.
|
|
347
|
+
# It overrides the chat's own limit (see Chat#with_file_store).
|
|
348
|
+
# +false+ always sends results whole, for tools that already return
|
|
349
|
+
# what they find in pages. Without an argument,
|
|
350
|
+
# returns the declared value, or +nil+.
|
|
351
|
+
#
|
|
352
|
+
# class ExportOrders < RubyLLM::Tool
|
|
353
|
+
# inline_limit 50_000
|
|
354
|
+
# end
|
|
355
|
+
#
|
|
356
|
+
def inline_limit(characters = (get = true))
|
|
357
|
+
return @inline_limit if get
|
|
358
|
+
unless characters == false || (characters.is_a?(Integer) && characters.positive?)
|
|
359
|
+
raise ArgumentError, 'inline_limit takes a positive number of characters or false'
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
@inline_limit = characters
|
|
363
|
+
end
|
|
364
|
+
|
|
222
365
|
def split_result(result) # :nodoc:
|
|
223
366
|
case result
|
|
224
367
|
when Attachment then ['', [result]]
|
|
@@ -260,13 +403,29 @@ module RubyLLM
|
|
|
260
403
|
self.class.tool_name
|
|
261
404
|
end
|
|
262
405
|
|
|
406
|
+
# Returns the tool's namespace, or +nil+ when it has none. See
|
|
407
|
+
# ::namespace.
|
|
408
|
+
def namespace
|
|
409
|
+
self.class.namespace
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def namespace_description # :nodoc:
|
|
413
|
+
self.class.namespace_description
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
def qualified_name # :nodoc:
|
|
417
|
+
namespace ? "#{namespace}_#{name}" : name
|
|
418
|
+
end
|
|
419
|
+
|
|
263
420
|
# Returns the tool description declared on the class with ::description.
|
|
264
421
|
def description
|
|
265
422
|
self.class.description
|
|
266
423
|
end
|
|
267
424
|
|
|
268
|
-
# Returns whether
|
|
269
|
-
|
|
425
|
+
# Returns whether +tool_call+ must be approved before it runs: whether
|
|
426
|
+
# this tool was declared with ::requires_approval. A subclass may
|
|
427
|
+
# override it to decide from the call's arguments.
|
|
428
|
+
def requires_approval?(tool_call = nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
270
429
|
self.class.requires_approval?
|
|
271
430
|
end
|
|
272
431
|
|
|
@@ -274,6 +433,22 @@ module RubyLLM
|
|
|
274
433
|
self.class.approval_resolver
|
|
275
434
|
end
|
|
276
435
|
|
|
436
|
+
# Returns whether this tool runs by itself in a concurrent round of
|
|
437
|
+
# tool calls: whether its class was declared with ::exclusive.
|
|
438
|
+
def exclusive?
|
|
439
|
+
self.class.exclusive?
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Returns the limit declared on the class with ::inline_limit: a number
|
|
443
|
+
# of characters, +false+, or +nil+ to use the chat's.
|
|
444
|
+
def inline_limit
|
|
445
|
+
self.class.inline_limit
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def max_run_time # :nodoc:
|
|
449
|
+
self.class.max_run_time
|
|
450
|
+
end
|
|
451
|
+
|
|
277
452
|
def declared_parameters # :nodoc:
|
|
278
453
|
self.class.declared_parameters
|
|
279
454
|
end
|
|
@@ -283,9 +458,39 @@ module RubyLLM
|
|
|
283
458
|
self.class.provider_options
|
|
284
459
|
end
|
|
285
460
|
|
|
461
|
+
# Keeps this tool, and no other of its class, out of the model's
|
|
462
|
+
# context until tool search loads it, as ::defer does for the class.
|
|
463
|
+
# Returns +self+. Chat#with_tools and Chat#with_mcp take +defer:+ to
|
|
464
|
+
# decide for every tool they register instead.
|
|
465
|
+
#
|
|
466
|
+
# chat.with_tools(CurrentTime.new, *connector_tools.map(&:defer))
|
|
467
|
+
#
|
|
468
|
+
def defer
|
|
469
|
+
@deferred = true
|
|
470
|
+
self
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Returns whether this tool waits to be found by tool search: whether
|
|
474
|
+
# it was deferred with #defer, or its class with ::defer.
|
|
475
|
+
def deferred?
|
|
476
|
+
@deferred || self.class.deferred?
|
|
477
|
+
end
|
|
478
|
+
|
|
286
479
|
class Deferred < SimpleDelegator # :nodoc:
|
|
287
480
|
end
|
|
288
481
|
|
|
482
|
+
class Flattened < SimpleDelegator # :nodoc:
|
|
483
|
+
def self.wrap(tool)
|
|
484
|
+
return Deferred.new(new(tool.__getobj__)) if tool.is_a?(Deferred)
|
|
485
|
+
|
|
486
|
+
new(tool)
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def name = __getobj__.qualified_name
|
|
490
|
+
def namespace = nil
|
|
491
|
+
def qualified_name = name
|
|
492
|
+
end
|
|
493
|
+
|
|
289
494
|
# Returns the JSON Schema for the tool's arguments, whether declared
|
|
290
495
|
# explicitly or inferred from the +execute+ signature.
|
|
291
496
|
def parameters_schema
|
|
@@ -317,11 +522,61 @@ module RubyLLM
|
|
|
317
522
|
|
|
318
523
|
RubyLLM.logger.debug { "Tool #{name} called with: #{normalized_args.inspect}" }
|
|
319
524
|
normalized_args[TOOL_CALL_KEYWORD] = tool_call if execute_accepts_tool_call?
|
|
320
|
-
result = execute(**normalized_args)
|
|
525
|
+
result = asking { halting { execute(**normalized_args) } }
|
|
321
526
|
RubyLLM.logger.debug { "Tool #{name} returned: #{result.inspect}" }
|
|
322
527
|
result
|
|
323
528
|
end
|
|
324
529
|
|
|
530
|
+
# Asks the person who runs the chat +message+ while #execute runs, and
|
|
531
|
+
# returns their answer. The call pauses until they answer (see
|
|
532
|
+
# Chat#pending_questions), then #execute runs again from the top, and
|
|
533
|
+
# this time +ask+ returns the answer:
|
|
534
|
+
#
|
|
535
|
+
# def execute(destination:)
|
|
536
|
+
# seat = ask("Window or aisle?", choices: %w[window aisle]) or return "No seat chosen."
|
|
537
|
+
# Flights.book!(destination, seat:)
|
|
538
|
+
# end
|
|
539
|
+
#
|
|
540
|
+
# Code before +ask+ runs again too, so make it safe to repeat: ask
|
|
541
|
+
# first, then act. A call may ask several questions, one after
|
|
542
|
+
# another, and each run asks them in the same order.
|
|
543
|
+
#
|
|
544
|
+
# Without options, +ask+ takes free text and returns the String. With
|
|
545
|
+
# +choices:+ it returns the one the person picked. With a block, it
|
|
546
|
+
# asks for the fields the block declares, in the DSL of
|
|
547
|
+
# Tool.parameters, and returns their values by name. With +url:+, it
|
|
548
|
+
# asks the person to visit a page, such as to connect an account, and
|
|
549
|
+
# returns +true+ once they say they did.
|
|
550
|
+
#
|
|
551
|
+
# passenger = ask("Who is flying?") do
|
|
552
|
+
# string :name, description: "As on the passport"
|
|
553
|
+
# integer :age, required: false
|
|
554
|
+
# end
|
|
555
|
+
# passenger # => {name: "Ada Lovelace", age: 36}
|
|
556
|
+
#
|
|
557
|
+
# ask("Connect your calendar", url: authorization_url)
|
|
558
|
+
#
|
|
559
|
+
# Returns +nil+ when the person declines. Outside a chat, nothing can
|
|
560
|
+
# pause the call, so +ask+ raises InputRequiredError. Raises
|
|
561
|
+
# ArgumentError when given more than one of +choices:+, +url:+, and a
|
|
562
|
+
# block.
|
|
563
|
+
def ask(message, choices: nil, url: nil, &fields)
|
|
564
|
+
if [choices, url, fields].compact.size > 1
|
|
565
|
+
raise ArgumentError, 'ask takes choices:, url:, or a block of fields: one kind of answer at a time'
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
state = (Fiber[ASKING] ||= { 'input' => nil, 'asked' => 0 })
|
|
569
|
+
index = state['asked']
|
|
570
|
+
state['asked'] += 1
|
|
571
|
+
requests = Array(state['input']&.[]('requests'))
|
|
572
|
+
return Question.from_h(requests[index]).asked_answer(fields) if requests[index]&.[]('response')
|
|
573
|
+
|
|
574
|
+
schema = SchemaDefinition.new(block: fields).json_schema if fields
|
|
575
|
+
asked = requests.first(index).map { |data| Question.from_h(data) }
|
|
576
|
+
question = Question.asked(index, message, choices:, url:, schema:)
|
|
577
|
+
raise InputRequiredError.new(name, { 'requests' => [*asked, question] })
|
|
578
|
+
end
|
|
579
|
+
|
|
325
580
|
# Runs the tool with the arguments chosen by the model. Subclasses must
|
|
326
581
|
# implement this method; the base implementation raises
|
|
327
582
|
# NotImplementedError. The return value is sent back to the model.
|
|
@@ -341,6 +596,55 @@ module RubyLLM
|
|
|
341
596
|
raise NotImplementedError, 'Subclasses must implement #execute'
|
|
342
597
|
end
|
|
343
598
|
|
|
599
|
+
# Carries on a call that paused on +task+, the Task #execute returned,
|
|
600
|
+
# with the call's +arguments+. Each Chat#complete calls it once for
|
|
601
|
+
# every paused call of the round. Return the call's result to finish
|
|
602
|
+
# it, or a Task, +task+ itself or a new one, to keep it paused. Tools
|
|
603
|
+
# that return a Task must implement this method; the base
|
|
604
|
+
# implementation raises NotImplementedError.
|
|
605
|
+
#
|
|
606
|
+
# def resume(task, **)
|
|
607
|
+
# envelope = Signatures.find(task.data["envelope_id"])
|
|
608
|
+
# envelope.signed? ? "Signed on #{envelope.signed_on}" : task
|
|
609
|
+
# end
|
|
610
|
+
#
|
|
611
|
+
def resume(task, **arguments)
|
|
612
|
+
raise NotImplementedError, "#{self.class} returned a Task, so it must implement #resume"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
# Returns the Task that +tool_call+ paused on, from +state+, the Hash
|
|
616
|
+
# Task#to_h returned. Override it to return a Task subclass.
|
|
617
|
+
#
|
|
618
|
+
# def task(state, tool_call: nil)
|
|
619
|
+
# SignatureTask.load(state, tool_call:)
|
|
620
|
+
# end
|
|
621
|
+
#
|
|
622
|
+
def task(state, tool_call: nil)
|
|
623
|
+
Task.load(state, tool_call:)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def resume_call(state, arguments, tool_call:) # :nodoc:
|
|
627
|
+
return answered(state, arguments, tool_call:) unless state.key?('task')
|
|
628
|
+
|
|
629
|
+
halting { resume(task(state, tool_call:), **arguments.transform_keys(&:to_sym)) }
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Ends the turn with +result+ as this call's result: the model is not
|
|
633
|
+
# asked to respond once the round's other calls finish. It stops
|
|
634
|
+
# #execute where it is called, so call it, or return it, when the tool
|
|
635
|
+
# has said the last word, such as filing a ticket a triage turn exists
|
|
636
|
+
# for. The result reaches the conversation as any other, and the next
|
|
637
|
+
# Chat#ask starts a new turn. See Chat#complete?.
|
|
638
|
+
#
|
|
639
|
+
# def execute(category:)
|
|
640
|
+
# ticket.update!(category:)
|
|
641
|
+
# halt "Filed as #{category}"
|
|
642
|
+
# end
|
|
643
|
+
#
|
|
644
|
+
def halt(result = nil)
|
|
645
|
+
throw HALT, Halt.new(result)
|
|
646
|
+
end
|
|
647
|
+
|
|
344
648
|
# Reports what the tool is doing while #execute runs, so an application
|
|
345
649
|
# can show it before the result arrives. Chat#after_tool_progress
|
|
346
650
|
# callbacks receive it as a Progress. Pass +value:+ and +total:+ when
|
|
@@ -376,6 +680,36 @@ module RubyLLM
|
|
|
376
680
|
nil
|
|
377
681
|
end
|
|
378
682
|
|
|
683
|
+
# A call's questions are counted from its start; one resumed keeps the
|
|
684
|
+
# answers it was given (#answered), and a call it makes of another tool
|
|
685
|
+
# neither sees them nor moves its count.
|
|
686
|
+
# A fresh Hash each time: fibers inherit their parent's storage, and
|
|
687
|
+
# parallel calls mustn't share a count.
|
|
688
|
+
def halting(&) # :nodoc:
|
|
689
|
+
catch(HALT, &)
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
# The call a chat makes of a tool takes the questions Tool.asking
|
|
693
|
+
# opened for it, which its handler then shares.
|
|
694
|
+
def asking # :nodoc:
|
|
695
|
+
previous = Fiber[ASKING]
|
|
696
|
+
input = Fiber[RESUMING]
|
|
697
|
+
Fiber[RESUMING] = nil
|
|
698
|
+
Fiber[ASKING] = previous&.delete('tool') == self ? previous : { 'input' => input, 'asked' => 0 }
|
|
699
|
+
yield
|
|
700
|
+
ensure
|
|
701
|
+
Fiber[ASKING] = previous
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
# Runs a call again that paused on its questions (#ask), now answered or
|
|
705
|
+
# declined, with +input+, the state Chat saved for it.
|
|
706
|
+
def answered(input, arguments, tool_call:) # :nodoc:
|
|
707
|
+
Fiber[RESUMING] = input
|
|
708
|
+
call(**arguments, tool_call:)
|
|
709
|
+
ensure
|
|
710
|
+
Fiber[RESUMING] = nil
|
|
711
|
+
end
|
|
712
|
+
|
|
379
713
|
def execute_accepts_tool_call? # :nodoc:
|
|
380
714
|
method(:execute).parameters.any? do |kind, name|
|
|
381
715
|
name == TOOL_CALL_KEYWORD && KEYWORD_PARAMETER_KINDS.include?(kind)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class ToolCall
|
|
5
|
+
# The tool calls of a Message: an Enumerable of ToolCall objects in the
|
|
6
|
+
# order the provider returned them, also readable by call ID. Empty on
|
|
7
|
+
# a message without calls.
|
|
8
|
+
#
|
|
9
|
+
# response.tool_calls.first
|
|
10
|
+
# response.tool_calls.map(&:name) # => ["search_issues"]
|
|
11
|
+
# response.tool_calls["call_1"]
|
|
12
|
+
#
|
|
13
|
+
class Collection
|
|
14
|
+
include Enumerable
|
|
15
|
+
include Support::Inspectable
|
|
16
|
+
|
|
17
|
+
def initialize(calls = {}) # :nodoc:
|
|
18
|
+
@calls = calls.to_h
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Yields each ToolCall, in provider order.
|
|
22
|
+
#
|
|
23
|
+
# response.tool_calls.each { |call| puts call.name }
|
|
24
|
+
#
|
|
25
|
+
def each(&)
|
|
26
|
+
return enum_for(__method__) { size } unless block_given?
|
|
27
|
+
|
|
28
|
+
@calls.each_value(&)
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
alias each_value each
|
|
32
|
+
|
|
33
|
+
# Yields each key, in provider order: the call ID on a complete message,
|
|
34
|
+
# and on a streamed Chunk the stream index when the provider sends one.
|
|
35
|
+
def each_key(&)
|
|
36
|
+
return enum_for(__method__) { size } unless block_given?
|
|
37
|
+
|
|
38
|
+
@calls.each_key(&)
|
|
39
|
+
self
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the ToolCall with the ID +id+, or +nil+.
|
|
43
|
+
def [](id)
|
|
44
|
+
@calls[key_for(id)]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def []=(id, call) # :nodoc:
|
|
48
|
+
@calls[id] = call
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Returns the ToolCall with the ID +id+. Without a default or a block,
|
|
52
|
+
# raises KeyError when there is none.
|
|
53
|
+
def fetch(id, ...)
|
|
54
|
+
@calls.fetch(key_for(id), ...)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the number of tool calls.
|
|
58
|
+
def size
|
|
59
|
+
@calls.size
|
|
60
|
+
end
|
|
61
|
+
alias length size
|
|
62
|
+
|
|
63
|
+
# Returns +true+ when there are no tool calls.
|
|
64
|
+
def empty?
|
|
65
|
+
@calls.empty?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Returns the tool calls as an Array.
|
|
69
|
+
def values
|
|
70
|
+
@calls.values
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns the keys as an Array, as #each_key yields them.
|
|
74
|
+
def keys
|
|
75
|
+
@calls.keys
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Returns +true+ when there is a tool call with the ID +id+.
|
|
79
|
+
def key?(id)
|
|
80
|
+
@calls.key?(key_for(id))
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns a Hash of what the block returns for each ToolCall, keyed by
|
|
84
|
+
# call ID.
|
|
85
|
+
def transform_values(&)
|
|
86
|
+
@calls.transform_values(&)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns a Collection without the calls with the given IDs.
|
|
90
|
+
def except(*ids)
|
|
91
|
+
self.class.new(@calls.except(*ids.map { |id| key_for(id) }))
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Returns the tool calls as a new Hash of ToolCall objects keyed by call
|
|
95
|
+
# ID. With a block, returns the Hash of the pairs it makes from each ID
|
|
96
|
+
# and ToolCall.
|
|
97
|
+
def to_h(&block)
|
|
98
|
+
block ? @calls.to_h(&block) : @calls.dup
|
|
99
|
+
end
|
|
100
|
+
alias to_hash to_h
|
|
101
|
+
|
|
102
|
+
# Returns +true+ when +other+ is a Collection or Hash of the same calls
|
|
103
|
+
# under the same IDs.
|
|
104
|
+
def ==(other)
|
|
105
|
+
other.respond_to?(:to_hash) && @calls == other.to_hash
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns the JSON of #to_h.
|
|
109
|
+
def to_json(*)
|
|
110
|
+
@calls.to_json(*)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns the JSON-ready form of #to_h, where Active Support provides
|
|
114
|
+
# one, or #to_h.
|
|
115
|
+
def as_json(*)
|
|
116
|
+
@calls.respond_to?(:as_json) ? @calls.as_json(*) : to_h
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def key_for(id)
|
|
122
|
+
return id if id.nil? || @calls.key?(id)
|
|
123
|
+
|
|
124
|
+
@calls.find(-> { [id] }) { |_, call| call.id == id }.first
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def inspect_attributes
|
|
128
|
+
{ calls: map(&:name) }
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
data/lib/ruby_llm/tool_call.rb
CHANGED
|
@@ -4,7 +4,8 @@ module RubyLLM
|
|
|
4
4
|
# A ToolCall is a request to run a tool with specific arguments. Instances
|
|
5
5
|
# appear in Message#tool_calls. Local calls are yielded to tool callbacks
|
|
6
6
|
# such as Chat#before_tool_call. Remote calls awaiting approval return
|
|
7
|
-
# +true+ from #remote
|
|
7
|
+
# +true+ from #remote?, and their approvals appear among
|
|
8
|
+
# Chat#pending_questions.
|
|
8
9
|
#
|
|
9
10
|
# chat.before_tool_call do |tool_call|
|
|
10
11
|
# puts "Calling tool: #{tool_call.name}"
|
|
@@ -14,13 +15,31 @@ module RubyLLM
|
|
|
14
15
|
class ToolCall
|
|
15
16
|
include Support::Inspectable
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
def self.index(calls, by: :id) # :nodoc:
|
|
19
|
+
calls.each_with_object({}) do |call, indexed|
|
|
20
|
+
id = call.public_send(by) || call.provider_call_id
|
|
21
|
+
raise Error, "Duplicate tool call ID in one response: #{id.inspect}" if indexed.key?(id)
|
|
22
|
+
|
|
23
|
+
indexed[id] = call
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The RubyLLM identifier for this invocation. The tool result message
|
|
28
|
+
# answering this call carries the same id, including after a reload.
|
|
29
|
+
# This is independent of the ID in the provider's response.
|
|
19
30
|
attr_reader :id
|
|
20
31
|
|
|
21
32
|
# The name of the tool the model wants to invoke.
|
|
22
33
|
attr_reader :name
|
|
23
34
|
|
|
35
|
+
# The namespace of the tool the model wants to invoke, or +nil+ for a
|
|
36
|
+
# tool without one. See Tool.namespace.
|
|
37
|
+
attr_reader :namespace
|
|
38
|
+
|
|
39
|
+
def qualified_name # :nodoc:
|
|
40
|
+
namespace ? "#{namespace}_#{name}" : name
|
|
41
|
+
end
|
|
42
|
+
|
|
24
43
|
# The arguments the model supplied for the invocation, as a Hash. Keys
|
|
25
44
|
# are ordered the way PostgreSQL +jsonb+ and MySQL +json+ store them,
|
|
26
45
|
# shorter keys first, so a call reloaded from the database renders the
|
|
@@ -35,24 +54,67 @@ module RubyLLM
|
|
|
35
54
|
# RubyLLM replays it to the provider on later requests.
|
|
36
55
|
attr_accessor :thought_signature
|
|
37
56
|
|
|
38
|
-
def
|
|
57
|
+
def self.parse_arguments(text, response: nil, finish_reason: nil) # :nodoc:
|
|
58
|
+
return { arguments: text } if text.is_a?(Hash)
|
|
59
|
+
|
|
60
|
+
text = JSON.generate(text) unless text.nil? || text.is_a?(String)
|
|
61
|
+
return { arguments: {} } if text.nil? || text.empty?
|
|
62
|
+
|
|
63
|
+
{ arguments: Hash(JSON.parse(text)) }
|
|
64
|
+
rescue JSON::ParserError, TypeError => e
|
|
65
|
+
raise ToolCallTruncatedError.new(response:), cause: e if finish_reason == :max_tokens
|
|
66
|
+
|
|
67
|
+
{ malformed_arguments: text }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def initialize(name:, id: nil, provider_call_id: nil, arguments: {}, thought_signature: nil,
|
|
71
|
+
remote: false, namespace: nil, malformed_arguments: nil) # :nodoc:
|
|
39
72
|
@id = id
|
|
73
|
+
@provider_call_id = provider_call_id
|
|
40
74
|
@name = name
|
|
41
|
-
@
|
|
75
|
+
@namespace = namespace&.to_s
|
|
76
|
+
@malformed_arguments = malformed_arguments
|
|
77
|
+
@arguments = malformed_arguments ? {} : canonical_arguments(arguments)
|
|
42
78
|
@thought_signature = thought_signature
|
|
43
79
|
@remote = remote
|
|
44
80
|
end
|
|
45
81
|
|
|
82
|
+
# The provider's argument text when it was not a JSON object, or +nil+.
|
|
83
|
+
# A call with such text has empty #arguments and never runs.
|
|
84
|
+
attr_reader :malformed_arguments # :nodoc:
|
|
85
|
+
|
|
86
|
+
def malformed_arguments? = !@malformed_arguments.nil? # :nodoc:
|
|
87
|
+
|
|
88
|
+
def malformed_arguments_problem # :nodoc:
|
|
89
|
+
JSON.parse(@malformed_arguments)
|
|
90
|
+
'not a JSON object'
|
|
91
|
+
rescue JSON::ParserError => e
|
|
92
|
+
"not valid JSON (#{e.message})"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def provider_call_id = @provider_call_id || id # :nodoc:
|
|
96
|
+
|
|
97
|
+
def with_id(id) # :nodoc:
|
|
98
|
+
dup.tap do |call|
|
|
99
|
+
call.instance_variable_set(:@provider_call_id, provider_call_id)
|
|
100
|
+
call.instance_variable_set(:@id, id)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
46
104
|
def inspect_attributes # :nodoc:
|
|
47
|
-
{ id: id, name: name, arguments: arguments, remote: remote? || nil
|
|
105
|
+
{ id: id, name: name, namespace: namespace, arguments: arguments, remote: remote? || nil,
|
|
106
|
+
malformed_arguments: malformed_arguments }.compact
|
|
48
107
|
end
|
|
49
108
|
|
|
50
109
|
# Returns the call as a Hash, omitting +nil+ values.
|
|
51
110
|
def to_h
|
|
52
111
|
{
|
|
53
112
|
id: @id,
|
|
113
|
+
provider_call_id: @provider_call_id,
|
|
54
114
|
name: @name,
|
|
115
|
+
namespace: @namespace,
|
|
55
116
|
arguments: @arguments,
|
|
117
|
+
malformed_arguments: @malformed_arguments,
|
|
56
118
|
remote: remote? || nil,
|
|
57
119
|
thought_signature: @thought_signature
|
|
58
120
|
}.compact
|