ruby_llm 1.16.0 → 2.0.0.rc2
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/.rdoc_options +25 -0
- data/README.md +85 -32
- data/exe/ruby_llm +8 -0
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +0 -1
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +3 -43
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +11 -3
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +4 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/models/show.html.erb.tt +5 -5
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_chat.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/index.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +1 -1
- 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.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +6 -4
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +4 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/_model.html.erb.tt +5 -6
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/index.html.erb.tt +2 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/models/show.html.erb.tt +3 -3
- data/lib/generators/ruby_llm/generator_helpers.rb +106 -62
- data/lib/generators/ruby_llm/install/install_generator.rb +3 -11
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +2 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +14 -8
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +117 -0
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +0 -8
- data/lib/generators/ruby_llm/provider/cli.rb +175 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +323 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +37 -0
- data/lib/generators/ruby_llm/provider/templates/core/provider_spec.rb.erb +34 -0
- data/lib/generators/ruby_llm/provider/templates/gem/archspec.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/console.erb +15 -0
- data/lib/generators/ruby_llm/provider/templates/gem/bin/setup.erb +5 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_schema_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +35 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_tools_spec.rb.erb +30 -0
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +32 -0
- data/lib/generators/ruby_llm/provider/templates/gem/embedding_spec.rb.erb +49 -0
- data/lib/generators/ruby_llm/provider/templates/gem/env.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/fixtures_gitkeep.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/flayignore.erb +1 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +28 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitignore.erb +7 -0
- data/lib/generators/ruby_llm/provider/templates/gem/gitleaks.yml.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/image_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/license.erb +21 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models.rb.erb +19 -0
- data/lib/generators/ruby_llm/provider/templates/gem/models_spec.rb.erb +17 -0
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +22 -0
- data/lib/generators/ruby_llm/provider/templates/gem/overcommit.yml.erb +31 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider.rb.erb +52 -0
- data/lib/generators/ruby_llm/provider/templates/gem/provider_spec.rb.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rakefile.erb +38 -0
- data/lib/generators/ruby_llm/provider/templates/gem/readme.md.erb +50 -0
- data/lib/generators/ruby_llm/provider/templates/gem/release.yml.erb +36 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rerank_spec.rb.erb +23 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rspec.erb +2 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +29 -0
- data/lib/generators/ruby_llm/provider/templates/gem/rubyllm_configuration.rb.erb +14 -0
- data/lib/generators/ruby_llm/provider/templates/gem/spec_helper.rb.erb +26 -0
- data/lib/generators/ruby_llm/provider/templates/gem/speech_spec.rb.erb +25 -0
- data/lib/generators/ruby_llm/provider/templates/gem/vcr_configuration.rb.erb +16 -0
- data/lib/generators/ruby_llm/provider/templates/gem/video_spec.rb.erb +27 -0
- data/lib/generators/ruby_llm/schema/schema_generator.rb +5 -1
- data/lib/generators/ruby_llm/schema/templates/schema.rb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +21 -0
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +3 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +7 -12
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +5 -2
- data/lib/generators/ruby_llm/tool/tool_generator.rb +25 -59
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +461 -0
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +101 -0
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +215 -0
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +660 -0
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +222 -0
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +13 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +167 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +344 -0
- data/lib/ruby_llm/accounting/usage.rb +245 -0
- data/lib/ruby_llm/active_record/acts_as.rb +94 -111
- data/lib/ruby_llm/active_record/attachment_helpers.rb +186 -0
- data/lib/ruby_llm/active_record/batch.rb +97 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +828 -305
- data/lib/ruby_llm/active_record/message_methods.rb +113 -136
- data/lib/ruby_llm/active_record/model.rb +135 -0
- data/lib/ruby_llm/active_record/payload_helpers.rb +1 -2
- data/lib/ruby_llm/active_record/tool_call.rb +33 -0
- data/lib/ruby_llm/active_record/usage.rb +61 -0
- data/lib/ruby_llm/agent.rb +1065 -151
- data/lib/ruby_llm/aliases.json +268 -100
- data/lib/ruby_llm/attachment.rb +187 -48
- data/lib/ruby_llm/batch.rb +432 -0
- data/lib/ruby_llm/cached_content.rb +112 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +111 -0
- data/lib/ruby_llm/chat.rb +1127 -198
- data/lib/ruby_llm/chunk.rb +10 -0
- data/lib/ruby_llm/citation.rb +105 -0
- data/lib/ruby_llm/configuration.rb +261 -24
- data/lib/ruby_llm/context.rb +128 -6
- data/lib/ruby_llm/cost.rb +217 -80
- data/lib/ruby_llm/downloaded_file.rb +33 -0
- data/lib/ruby_llm/embedding.rb +121 -17
- data/lib/ruby_llm/embedding_request.rb +53 -0
- data/lib/ruby_llm/error.rb +159 -23
- data/lib/ruby_llm/fallback.rb +133 -0
- data/lib/ruby_llm/files/mime_type.rb +97 -0
- data/lib/ruby_llm/image.rb +153 -32
- data/lib/ruby_llm/message.rb +233 -54
- data/lib/ruby_llm/model/modalities.rb +17 -4
- data/lib/ruby_llm/model/pricing.rb +24 -5
- data/lib/ruby_llm/model/pricing_category.rb +103 -14
- data/lib/ruby_llm/model/pricing_tier.rb +55 -15
- data/lib/ruby_llm/model.rb +244 -2
- data/lib/ruby_llm/models/aliases.rb +41 -0
- data/lib/ruby_llm/models/registry.rb +165 -0
- data/lib/ruby_llm/models/schema.rb +99 -0
- data/lib/ruby_llm/models.json +73261 -33733
- data/lib/ruby_llm/models.rb +477 -215
- data/lib/ruby_llm/moderation.rb +139 -26
- data/lib/ruby_llm/ocr.rb +112 -0
- data/lib/ruby_llm/prompt.rb +79 -0
- data/lib/ruby_llm/protocol/binary_streaming.rb +65 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +214 -0
- data/lib/ruby_llm/protocol/streaming.rb +230 -0
- data/lib/ruby_llm/protocol.rb +662 -0
- data/lib/ruby_llm/protocols/anthropic/batches.rb +73 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +548 -0
- data/lib/ruby_llm/protocols/anthropic/embeddings.rb +14 -0
- data/lib/ruby_llm/protocols/anthropic/files.rb +38 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +141 -0
- data/lib/ruby_llm/protocols/anthropic/models.rb +129 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +167 -0
- data/lib/ruby_llm/{providers → protocols}/anthropic/tools.rb +24 -41
- data/lib/ruby_llm/protocols/anthropic.rb +101 -0
- data/lib/ruby_llm/protocols/azure/files.rb +16 -0
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +109 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +129 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +110 -0
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +122 -0
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +95 -0
- data/lib/ruby_llm/protocols/chat_completions/batches.rb +32 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +493 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +35 -0
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +60 -0
- data/lib/ruby_llm/protocols/chat_completions/images.rb +127 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/media.rb +30 -17
- data/lib/ruby_llm/protocols/chat_completions/models.rb +39 -0
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +52 -0
- data/lib/ruby_llm/protocols/chat_completions/rerank.rb +56 -0
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +40 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +69 -0
- data/lib/ruby_llm/{providers/openai → protocols/chat_completions}/tools.rb +15 -17
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +150 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +21 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +75 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +98 -0
- data/lib/ruby_llm/protocols/cohere/chat.rb +227 -0
- data/lib/ruby_llm/protocols/cohere/datasets.rb +102 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +68 -0
- data/lib/ruby_llm/protocols/cohere/media.rb +77 -0
- data/lib/ruby_llm/protocols/cohere/models.rb +92 -0
- data/lib/ruby_llm/protocols/cohere/ocr.rb +63 -0
- data/lib/ruby_llm/protocols/cohere/rerank.rb +52 -0
- data/lib/ruby_llm/protocols/cohere/streaming.rb +105 -0
- data/lib/ruby_llm/protocols/cohere/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +132 -0
- data/lib/ruby_llm/protocols/cohere/transcription.rb +41 -0
- data/lib/ruby_llm/protocols/cohere.rb +21 -0
- data/lib/ruby_llm/protocols/converse/batches.rb +55 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +695 -0
- data/lib/ruby_llm/protocols/converse/media.rb +178 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +432 -0
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +56 -0
- data/lib/ruby_llm/protocols/converse.rb +54 -0
- data/lib/ruby_llm/protocols/deepgram/models.rb +74 -0
- data/lib/ruby_llm/protocols/deepgram/speech.rb +93 -0
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +89 -0
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +96 -0
- data/lib/ruby_llm/protocols/deepgram.rb +19 -0
- data/lib/ruby_llm/protocols/deepseek/files.rb +31 -0
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +36 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +74 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/media.rb +42 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +93 -0
- data/lib/ruby_llm/protocols/elevenlabs/flows.rb +14 -0
- data/lib/ruby_llm/protocols/elevenlabs/models.rb +64 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +67 -0
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +127 -0
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +61 -0
- data/lib/ruby_llm/protocols/elevenlabs.rb +15 -0
- data/lib/ruby_llm/protocols/files.rb +119 -0
- data/lib/ruby_llm/protocols/gemini/batches.rb +162 -0
- data/lib/ruby_llm/protocols/gemini/caches.rb +59 -0
- data/lib/ruby_llm/protocols/gemini/chat.rb +453 -0
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +86 -0
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +70 -0
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +32 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +115 -0
- data/lib/ruby_llm/protocols/gemini/images.rb +183 -0
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +140 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/media.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/models.rb +71 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +56 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +96 -0
- data/lib/ruby_llm/protocols/gemini/tools.rb +157 -0
- data/lib/ruby_llm/{providers → protocols}/gemini/transcription.rb +22 -22
- data/lib/ruby_llm/protocols/gemini/videos.rb +103 -0
- data/lib/ruby_llm/protocols/gemini.rb +35 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +111 -0
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +22 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +96 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +145 -0
- data/lib/ruby_llm/protocols/interactions/content.rb +90 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +91 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +51 -0
- data/lib/ruby_llm/protocols/interactions/transcription.rb +58 -0
- data/lib/ruby_llm/protocols/interactions.rb +29 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +51 -0
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +111 -0
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +50 -0
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +103 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +33 -0
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +44 -0
- data/lib/ruby_llm/protocols/invoke_model.rb +57 -0
- data/lib/ruby_llm/protocols/mistral/content.rb +49 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +160 -0
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +43 -0
- data/lib/ruby_llm/protocols/mistral/conversations/streaming.rb +83 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +30 -0
- data/lib/ruby_llm/protocols/mistral/files.rb +36 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +160 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +126 -0
- data/lib/ruby_llm/protocols/openai/files.rb +42 -0
- data/lib/ruby_llm/protocols/openrouter/batches.rb +147 -0
- data/lib/ruby_llm/protocols/openrouter/files.rb +24 -0
- data/lib/ruby_llm/protocols/openrouter/responses.rb +53 -0
- data/lib/ruby_llm/protocols/openrouter/transcription.rb +51 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +48 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +59 -0
- data/lib/ruby_llm/protocols/responses/approvals.rb +32 -0
- data/lib/ruby_llm/protocols/responses/batches.rb +32 -0
- data/lib/ruby_llm/protocols/responses/chat.rb +476 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +29 -0
- data/lib/ruby_llm/protocols/responses/media.rb +61 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +117 -0
- data/lib/ruby_llm/protocols/responses/token_counting.rb +26 -0
- data/lib/ruby_llm/protocols/responses/tools.rb +39 -0
- data/lib/ruby_llm/protocols/responses.rb +35 -0
- data/lib/ruby_llm/protocols/vertexai/batch_prediction.rb +155 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +85 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/results.rb +74 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction.rb +56 -0
- data/lib/ruby_llm/protocols/vertexai/files.rb +101 -0
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +69 -0
- data/lib/ruby_llm/protocols/vertexai/research.rb +193 -0
- data/lib/ruby_llm/protocols/xai/files.rb +30 -0
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +120 -0
- data/lib/ruby_llm/protocols/xai/tokenization.rb +23 -0
- data/lib/ruby_llm/provider.rb +560 -128
- data/lib/ruby_llm/providers/anthropic/capabilities.rb +5 -7
- data/lib/ruby_llm/providers/anthropic.rb +4 -6
- data/lib/ruby_llm/providers/azure/audio.rb +18 -0
- data/lib/ruby_llm/providers/azure/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/azure/chat.rb +2 -9
- data/lib/ruby_llm/providers/azure/chat_completions/batches.rb +29 -0
- data/lib/ruby_llm/providers/azure/chat_completions.rb +80 -0
- data/lib/ruby_llm/providers/azure/cohere.rb +33 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +3 -2
- data/lib/ruby_llm/providers/azure/images.rb +22 -0
- data/lib/ruby_llm/providers/azure/media.rb +5 -14
- data/lib/ruby_llm/providers/azure/models.rb +35 -0
- data/lib/ruby_llm/providers/azure/responses.rb +26 -0
- data/lib/ruby_llm/providers/azure/videos.rb +64 -0
- data/lib/ruby_llm/providers/azure.rb +77 -78
- data/lib/ruby_llm/providers/bedrock/auth.rb +60 -41
- data/lib/ruby_llm/providers/bedrock/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +39 -0
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +23 -0
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +24 -0
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +96 -0
- data/lib/ruby_llm/providers/bedrock/mantle.rb +57 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +193 -41
- data/lib/ruby_llm/providers/bedrock.rb +216 -45
- data/lib/ruby_llm/providers/cohere.rb +31 -0
- data/lib/ruby_llm/providers/deepgram.rb +37 -0
- data/lib/ruby_llm/providers/deepseek/capabilities.rb +4 -51
- data/lib/ruby_llm/providers/deepseek/chat.rb +49 -2
- data/lib/ruby_llm/providers/deepseek/responses.rb +68 -0
- data/lib/ruby_llm/providers/deepseek.rb +9 -2
- data/lib/ruby_llm/providers/elevenlabs.rb +35 -0
- data/lib/ruby_llm/providers/gemini/capabilities.rb +8 -107
- data/lib/ruby_llm/providers/gemini.rb +15 -8
- data/lib/ruby_llm/providers/gpustack/chat.rb +2 -16
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +28 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +17 -16
- data/lib/ruby_llm/providers/gpustack/models.rb +72 -62
- data/lib/ruby_llm/providers/gpustack/speech.rb +15 -0
- data/lib/ruby_llm/providers/gpustack/transcription.rb +29 -0
- data/lib/ruby_llm/providers/gpustack.rb +35 -11
- data/lib/ruby_llm/providers/mistral/capabilities.rb +7 -155
- data/lib/ruby_llm/providers/mistral/chat.rb +37 -61
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +120 -0
- data/lib/ruby_llm/providers/mistral/chat_completions.rb +21 -0
- data/lib/ruby_llm/providers/mistral/conversations.rb +12 -0
- data/lib/ruby_llm/providers/mistral/embeddings.rb +6 -4
- data/lib/ruby_llm/providers/mistral/media.rb +6 -18
- data/lib/ruby_llm/providers/mistral/models.rb +57 -23
- data/lib/ruby_llm/providers/mistral/ocr.rb +47 -0
- data/lib/ruby_llm/providers/mistral/speech.rb +51 -0
- data/lib/ruby_llm/providers/mistral/transcription.rb +62 -0
- data/lib/ruby_llm/providers/mistral.rb +16 -4
- data/lib/ruby_llm/providers/ollama/chat.rb +7 -13
- data/lib/ruby_llm/providers/ollama/media.rb +6 -15
- data/lib/ruby_llm/providers/ollama/models.rb +50 -9
- data/lib/ruby_llm/providers/ollama.rb +9 -8
- data/lib/ruby_llm/providers/ollama_cloud/models.rb +14 -0
- data/lib/ruby_llm/providers/ollama_cloud.rb +40 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +54 -259
- data/lib/ruby_llm/providers/openai/models.rb +23 -23
- data/lib/ruby_llm/providers/openai/responses.rb +13 -0
- data/lib/ruby_llm/providers/openai.rb +92 -11
- data/lib/ruby_llm/providers/openrouter/chat.rb +130 -108
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +51 -0
- data/lib/ruby_llm/providers/openrouter/images.rb +44 -43
- data/lib/ruby_llm/providers/openrouter/media.rb +34 -0
- data/lib/ruby_llm/providers/openrouter/models.rb +50 -11
- data/lib/ruby_llm/providers/openrouter/speech.rb +32 -0
- data/lib/ruby_llm/providers/openrouter/streaming.rb +31 -38
- data/lib/ruby_llm/providers/openrouter/videos.rb +81 -0
- data/lib/ruby_llm/providers/openrouter.rb +78 -20
- data/lib/ruby_llm/providers/perplexity/chat.rb +2 -9
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +32 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -21
- data/lib/ruby_llm/providers/perplexity/models.rb +80 -13
- data/lib/ruby_llm/providers/perplexity.rb +28 -20
- data/lib/ruby_llm/providers/vertexai/anthropic/batches.rb +52 -0
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +34 -0
- data/lib/ruby_llm/providers/vertexai/capabilities.rb +19 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions/batches.rb +54 -0
- data/lib/ruby_llm/providers/vertexai/chat_completions.rb +15 -0
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +22 -7
- data/lib/ruby_llm/providers/vertexai/gemini/batches.rb +42 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +69 -0
- data/lib/ruby_llm/providers/vertexai/live_transcription.rb +24 -0
- data/lib/ruby_llm/providers/vertexai/mistral.rb +28 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +145 -43
- data/lib/ruby_llm/providers/vertexai/transcription.rb +49 -4
- data/lib/ruby_llm/providers/vertexai/videos.rb +61 -0
- data/lib/ruby_llm/providers/vertexai.rb +160 -17
- data/lib/ruby_llm/providers/xai/capabilities.rb +18 -0
- data/lib/ruby_llm/providers/xai/chat.rb +3 -2
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +108 -0
- data/lib/ruby_llm/providers/xai/chat_completions.rb +19 -0
- data/lib/ruby_llm/providers/xai/images.rb +91 -0
- data/lib/ruby_llm/providers/xai/models.rb +32 -36
- data/lib/ruby_llm/providers/xai/reported_cost.rb +18 -0
- data/lib/ruby_llm/providers/xai/responses.rb +52 -0
- data/lib/ruby_llm/providers/xai/speech.rb +45 -0
- data/lib/ruby_llm/providers/xai/transcription.rb +48 -0
- data/lib/ruby_llm/providers/xai/videos.rb +87 -0
- data/lib/ruby_llm/providers/xai.rb +15 -5
- data/lib/ruby_llm/railtie.rb +7 -16
- data/lib/ruby_llm/rerank.rb +105 -0
- data/lib/ruby_llm/research_job.rb +241 -0
- data/lib/ruby_llm/search_results.rb +68 -0
- data/lib/ruby_llm/server_tool_call.rb +73 -0
- data/lib/ruby_llm/speech.rb +159 -0
- data/lib/ruby_llm/speech_chunk.rb +33 -0
- data/lib/ruby_llm/support/deprecator.rb +22 -0
- data/lib/ruby_llm/support/inspectable.rb +49 -0
- data/lib/ruby_llm/support/instrumentation.rb +41 -0
- data/lib/ruby_llm/support/utils.rb +147 -0
- data/lib/ruby_llm/thinking.rb +127 -20
- data/lib/ruby_llm/tokenization.rb +59 -0
- data/lib/ruby_llm/tokens.rb +103 -33
- data/lib/ruby_llm/tool.rb +266 -91
- data/lib/ruby_llm/tool_call.rb +36 -3
- data/lib/ruby_llm/tools/server_tools.rb +109 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +62 -0
- data/lib/ruby_llm/transcription.rb +138 -13
- data/lib/ruby_llm/transcription_chunk.rb +68 -0
- data/lib/ruby_llm/transport/connection.rb +193 -0
- data/lib/ruby_llm/transport/error_middleware.rb +131 -0
- data/lib/ruby_llm/transport/usage_middleware.rb +28 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +220 -0
- data/lib/ruby_llm/uploaded_file.rb +144 -0
- data/lib/ruby_llm/version.rb +2 -1
- data/lib/ruby_llm/video.rb +136 -0
- data/lib/ruby_llm/video_job.rb +150 -0
- data/lib/ruby_llm/workflow.rb +91 -0
- data/lib/ruby_llm.rb +380 -6
- data/lib/tasks/ruby_llm.rake +21 -16
- data/skills/rubyllm/SKILL.md +81 -0
- data/skills/rubyllm/agents/openai.yaml +4 -0
- metadata +339 -97
- data/lib/generators/ruby_llm/install/templates/add_references_to_chats_tool_calls_and_messages_migration.rb.tt +0 -9
- data/lib/generators/ruby_llm/install/templates/create_models_migration.rb.tt +0 -39
- data/lib/generators/ruby_llm/install/templates/create_tool_calls_migration.rb.tt +0 -21
- data/lib/generators/ruby_llm/install/templates/model_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/install/templates/tool_call_model.rb.tt +0 -3
- data/lib/generators/ruby_llm/upgrade_to_v1_10/templates/add_v1_10_message_columns.rb.tt +0 -19
- data/lib/generators/ruby_llm/upgrade_to_v1_10/upgrade_to_v1_10_generator.rb +0 -50
- data/lib/generators/ruby_llm/upgrade_to_v1_14/templates/add_v1_14_tool_call_columns.rb.tt +0 -7
- data/lib/generators/ruby_llm/upgrade_to_v1_14/upgrade_to_v1_14_generator.rb +0 -49
- data/lib/generators/ruby_llm/upgrade_to_v1_7/templates/migration.rb.tt +0 -145
- data/lib/generators/ruby_llm/upgrade_to_v1_7/upgrade_to_v1_7_generator.rb +0 -122
- data/lib/generators/ruby_llm/upgrade_to_v1_9/templates/add_v1_9_message_columns.rb.tt +0 -15
- data/lib/generators/ruby_llm/upgrade_to_v1_9/upgrade_to_v1_9_generator.rb +0 -49
- data/lib/ruby_llm/active_record/acts_as_legacy.rb +0 -597
- data/lib/ruby_llm/active_record/model_methods.rb +0 -82
- data/lib/ruby_llm/active_record/tool_call_methods.rb +0 -18
- data/lib/ruby_llm/aliases.rb +0 -41
- data/lib/ruby_llm/connection.rb +0 -159
- data/lib/ruby_llm/content.rb +0 -91
- data/lib/ruby_llm/deprecator.rb +0 -24
- data/lib/ruby_llm/error_middleware.rb +0 -81
- data/lib/ruby_llm/instrumentation.rb +0 -36
- data/lib/ruby_llm/mime_type.rb +0 -96
- data/lib/ruby_llm/model/info.rb +0 -164
- data/lib/ruby_llm/model_registry.rb +0 -39
- data/lib/ruby_llm/models_schema.json +0 -171
- data/lib/ruby_llm/providers/anthropic/chat.rb +0 -291
- data/lib/ruby_llm/providers/anthropic/content.rb +0 -44
- data/lib/ruby_llm/providers/anthropic/embeddings.rb +0 -20
- data/lib/ruby_llm/providers/anthropic/media.rb +0 -92
- data/lib/ruby_llm/providers/anthropic/models.rb +0 -59
- data/lib/ruby_llm/providers/anthropic/streaming.rb +0 -71
- data/lib/ruby_llm/providers/bedrock/chat.rb +0 -405
- data/lib/ruby_llm/providers/bedrock/media.rb +0 -108
- data/lib/ruby_llm/providers/bedrock/streaming.rb +0 -328
- data/lib/ruby_llm/providers/gemini/chat.rb +0 -542
- data/lib/ruby_llm/providers/gemini/embeddings.rb +0 -37
- data/lib/ruby_llm/providers/gemini/images.rb +0 -47
- data/lib/ruby_llm/providers/gemini/models.rb +0 -38
- data/lib/ruby_llm/providers/gemini/streaming.rb +0 -98
- data/lib/ruby_llm/providers/gemini/tools.rb +0 -234
- data/lib/ruby_llm/providers/gpustack/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/ollama/capabilities.rb +0 -20
- data/lib/ruby_llm/providers/openai/chat.rb +0 -236
- data/lib/ruby_llm/providers/openai/embeddings.rb +0 -33
- data/lib/ruby_llm/providers/openai/images.rb +0 -90
- data/lib/ruby_llm/providers/openai/moderation.rb +0 -34
- data/lib/ruby_llm/providers/openai/streaming.rb +0 -55
- data/lib/ruby_llm/providers/openai/temperature.rb +0 -28
- data/lib/ruby_llm/providers/openai/transcription.rb +0 -71
- data/lib/ruby_llm/providers/perplexity/capabilities.rb +0 -72
- data/lib/ruby_llm/providers/vertexai/chat.rb +0 -14
- data/lib/ruby_llm/providers/vertexai/streaming.rb +0 -14
- data/lib/ruby_llm/stream_accumulator.rb +0 -218
- data/lib/ruby_llm/streaming.rb +0 -179
- data/lib/ruby_llm/tool_concurrency.rb +0 -105
- data/lib/ruby_llm/utils.rb +0 -130
- data/lib/tasks/models.rake +0 -593
- data/lib/tasks/release.rake +0 -94
- data/lib/tasks/vcr.rake +0 -124
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A hosted, single-turn research task. RubyLLM.research_later returns a
|
|
5
|
+
# job immediately; #wait polls it and #message returns its report.
|
|
6
|
+
# The provider's agent identity is separate from an inference model.
|
|
7
|
+
#
|
|
8
|
+
# job = RubyLLM.research_later(question, provider: provider, agent: agent_id)
|
|
9
|
+
# job.wait
|
|
10
|
+
# puts job.message.content
|
|
11
|
+
#
|
|
12
|
+
class ResearchJob
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
|
|
15
|
+
class DeadlineExpired < StandardError; end # :nodoc:
|
|
16
|
+
private_constant :DeadlineExpired
|
|
17
|
+
|
|
18
|
+
# A research failure that retains the job for inspection or recovery.
|
|
19
|
+
class Error < RubyLLM::Error
|
|
20
|
+
# The research job, including its remote ID and last known state.
|
|
21
|
+
attr_reader :job
|
|
22
|
+
|
|
23
|
+
# Creates an error retaining +job+ and the optional HTTP response.
|
|
24
|
+
def initialize(message, job:, response: nil)
|
|
25
|
+
@job = job
|
|
26
|
+
super(message, response:)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Raised when polling exceeds its deadline. The independent job may
|
|
31
|
+
# still be running; Error#job retains its ID.
|
|
32
|
+
class TimeoutError < Error; end
|
|
33
|
+
|
|
34
|
+
# An interrupted blocking research call retaining its remote job.
|
|
35
|
+
class InterruptedError < Interrupt
|
|
36
|
+
# The job whose cancellation was attempted before interrupting.
|
|
37
|
+
attr_reader :job
|
|
38
|
+
|
|
39
|
+
# Creates an interruption retaining +job+ for recovery.
|
|
40
|
+
def initialize(message, job:)
|
|
41
|
+
@job = job
|
|
42
|
+
super(message)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# The provider-assigned job ID.
|
|
47
|
+
attr_reader :id
|
|
48
|
+
|
|
49
|
+
# The provider slug.
|
|
50
|
+
attr_reader :provider
|
|
51
|
+
|
|
52
|
+
# The hosted agent's identity, separate from a model ID.
|
|
53
|
+
attr_reader :agent
|
|
54
|
+
|
|
55
|
+
# The normalized state: +:pending+, +:completed+, +:incomplete+,
|
|
56
|
+
# +:failed+, or +:cancelled+.
|
|
57
|
+
attr_reader :status
|
|
58
|
+
|
|
59
|
+
# The provider's failure explanation, if any.
|
|
60
|
+
attr_reader :error
|
|
61
|
+
|
|
62
|
+
# The original provider response from submission or the latest poll.
|
|
63
|
+
attr_reader :raw
|
|
64
|
+
|
|
65
|
+
# The error from an unsuccessful automatic cancellation attempt, if any.
|
|
66
|
+
attr_reader :cancellation_error
|
|
67
|
+
|
|
68
|
+
# Submits one research task without waiting. +provider:+ and +agent:+
|
|
69
|
+
# are required; +with:+ attaches documents or images where supported.
|
|
70
|
+
# +server_tools:+ accepts an array of aliases or a Hash of aliases
|
|
71
|
+
# and their options, as on Chat#with_server_tools.
|
|
72
|
+
def self.research_later(prompt, provider:, agent:, with: nil, server_tools: nil,
|
|
73
|
+
context: nil, provider_options: {}, metadata: nil)
|
|
74
|
+
config = context&.config || RubyLLM.config
|
|
75
|
+
instance = Provider.resolve!(provider).new(config)
|
|
76
|
+
payload = { provider: instance.slug, agent:, prompt:, metadata: }
|
|
77
|
+
RubyLLM.instrument('research_job.ruby_llm', payload, config:) do |event|
|
|
78
|
+
job = instance.research_later(prompt, agent:, with:, server_tools:, provider_options:)
|
|
79
|
+
event[:job_id] = job.id
|
|
80
|
+
event[:status] = job.status
|
|
81
|
+
job
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Runs a research task and returns its Message. On a timeout or
|
|
86
|
+
# interrupt, attempts to cancel the remote task before raising an
|
|
87
|
+
# error retaining the job. +timeout:+ and +interval:+ are seconds.
|
|
88
|
+
def self.research(prompt, timeout: 600, interval: 5, **options)
|
|
89
|
+
validate_polling_options(timeout, interval)
|
|
90
|
+
job = research_later(prompt, **options)
|
|
91
|
+
job.wait(timeout:, interval:).message
|
|
92
|
+
rescue Interrupt => e
|
|
93
|
+
raise unless job
|
|
94
|
+
|
|
95
|
+
job.send(:attempt_cancellation)
|
|
96
|
+
raise InterruptedError.new("Research interrupted (job #{job.id})", job:), cause: e
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
job ||= e.job if e.is_a?(Error)
|
|
99
|
+
raise unless job
|
|
100
|
+
|
|
101
|
+
job.send(:attempt_cancellation)
|
|
102
|
+
raise if e.is_a?(Error)
|
|
103
|
+
|
|
104
|
+
response = e.response if e.respond_to?(:response)
|
|
105
|
+
raise Error.new("Research failed: #{e.message} (job #{job.id})", job:, response:), cause: e
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Retrieves an existing task by ID without submitting another one.
|
|
109
|
+
# Use the same provider configuration that created the task.
|
|
110
|
+
def self.find(id, provider:, context: nil)
|
|
111
|
+
config = context&.config || RubyLLM.config
|
|
112
|
+
Provider.resolve!(provider).new(config).find_research_job(id)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.validate_polling_options(timeout, interval) # :nodoc:
|
|
116
|
+
return if [timeout, interval].all? { |value| value.is_a?(Numeric) && value.positive? && value.finite? }
|
|
117
|
+
|
|
118
|
+
raise ArgumentError, 'Research timeout and interval must be positive finite numbers'
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def initialize(id:, provider:, agent:, protocol:, **state) # :nodoc:
|
|
122
|
+
@id = id
|
|
123
|
+
@provider = provider.to_sym
|
|
124
|
+
@agent = agent
|
|
125
|
+
@protocol = protocol
|
|
126
|
+
apply_state(state)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Returns whether the task is waiting or running.
|
|
130
|
+
def pending? = status == :pending
|
|
131
|
+
|
|
132
|
+
# Returns whether the task reached any terminal state.
|
|
133
|
+
def done? = !pending?
|
|
134
|
+
|
|
135
|
+
# Returns whether the task finished with a complete report.
|
|
136
|
+
def completed? = status == :completed
|
|
137
|
+
|
|
138
|
+
# Returns whether the provider stopped before completing the report.
|
|
139
|
+
def incomplete? = status == :incomplete
|
|
140
|
+
|
|
141
|
+
# Returns whether the provider reported failure.
|
|
142
|
+
def failed? = status == :failed
|
|
143
|
+
|
|
144
|
+
# Returns whether the provider confirmed cancellation.
|
|
145
|
+
def cancelled? = status == :cancelled
|
|
146
|
+
|
|
147
|
+
# Fetches the latest state and returns self. Does nothing after the
|
|
148
|
+
# task finishes. +timeout:+ limits this request in seconds.
|
|
149
|
+
def refresh(timeout: nil)
|
|
150
|
+
self.class.validate_polling_options(timeout, 1) unless timeout.nil?
|
|
151
|
+
unless done?
|
|
152
|
+
state = request_with_timeout(timeout) { @protocol.refresh_research_job(self, timeout:) }
|
|
153
|
+
apply_state(state)
|
|
154
|
+
end
|
|
155
|
+
self
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# Polls until a terminal state and returns self. A timeout leaves the
|
|
159
|
+
# independent task running and raises TimeoutError with this job.
|
|
160
|
+
# Incomplete reports remain available through #message.
|
|
161
|
+
def wait(timeout: 600, interval: 5)
|
|
162
|
+
self.class.validate_polling_options(timeout, interval)
|
|
163
|
+
deadline = monotonic_time + timeout
|
|
164
|
+
until done?
|
|
165
|
+
remaining = deadline - monotonic_time
|
|
166
|
+
raise TimeoutError.new("Research timed out (job #{id})", job: self) unless remaining.positive?
|
|
167
|
+
|
|
168
|
+
refresh(timeout: remaining)
|
|
169
|
+
sleep [interval, deadline - monotonic_time].min if pending? && monotonic_time < deadline
|
|
170
|
+
end
|
|
171
|
+
raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?
|
|
172
|
+
|
|
173
|
+
self
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Requests cancellation and returns self. Only the provider's response
|
|
177
|
+
# can confirm cancellation; this does not delete stored task data.
|
|
178
|
+
def cancel(timeout: 5)
|
|
179
|
+
self.class.validate_polling_options(timeout, 1)
|
|
180
|
+
unless done?
|
|
181
|
+
state = request_with_timeout(timeout) { @protocol.cancel_research_job(self, timeout:) }
|
|
182
|
+
apply_state(state)
|
|
183
|
+
end
|
|
184
|
+
self
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Returns the report, or +nil+ while pending. An incomplete report has
|
|
188
|
+
# Message#finish_reason +:max_tokens+. Raises Error for failed or
|
|
189
|
+
# cancelled tasks.
|
|
190
|
+
def message
|
|
191
|
+
raise Error.new("Research #{status}: #{error} (job #{id})", job: self) if failed? || cancelled?
|
|
192
|
+
|
|
193
|
+
@message
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Returns the provider-reported task usage. Unreported fields are nil.
|
|
197
|
+
attr_reader :tokens
|
|
198
|
+
|
|
199
|
+
# Returns the reported cost, or unknown cost when the provider supplies
|
|
200
|
+
# no price. Agent IDs are never used to look up model token prices.
|
|
201
|
+
def cost
|
|
202
|
+
Cost.from_h({ total: tokens.reported_cost }.compact, tokens:)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
private
|
|
206
|
+
|
|
207
|
+
def request_with_timeout(timeout, &)
|
|
208
|
+
Timeout.timeout(timeout, DeadlineExpired, &)
|
|
209
|
+
rescue DeadlineExpired, Faraday::TimeoutError => e
|
|
210
|
+
raise TimeoutError.new("Research request timed out (job #{id})", job: self), cause: e
|
|
211
|
+
rescue Error
|
|
212
|
+
raise
|
|
213
|
+
rescue RubyLLM::Error, Faraday::Error => e
|
|
214
|
+
response = e.response if e.respond_to?(:response)
|
|
215
|
+
@raw = response.body if response.respond_to?(:body)
|
|
216
|
+
raise Error.new("Research request failed: #{e.message} (job #{id})", job: self, response:), cause: e
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def apply_state(state)
|
|
220
|
+
@status = state.fetch(:status)
|
|
221
|
+
@raw = state[:raw]
|
|
222
|
+
@error = state[:error]
|
|
223
|
+
@message = state[:message]
|
|
224
|
+
@tokens = state[:tokens] || @message&.tokens || Tokens.new
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def attempt_cancellation
|
|
228
|
+
cancel(timeout: 5)
|
|
229
|
+
rescue StandardError => e
|
|
230
|
+
@cancellation_error = e
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def monotonic_time
|
|
234
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def inspect_attributes # :nodoc:
|
|
238
|
+
{ id:, provider:, agent:, status: }
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A SearchResults wraps documents a Tool returns so the model can cite
|
|
5
|
+
# each source. RubyLLM renders them in the selected provider's citation
|
|
6
|
+
# format; cited passages appear on Message#citations.
|
|
7
|
+
#
|
|
8
|
+
# def execute(query:)
|
|
9
|
+
# docs = MyVectorStore.search(query)
|
|
10
|
+
#
|
|
11
|
+
# RubyLLM::SearchResults.new(
|
|
12
|
+
# *docs.map { |doc| { title: doc.name, url: doc.link, text: doc.body } }
|
|
13
|
+
# )
|
|
14
|
+
# end
|
|
15
|
+
#
|
|
16
|
+
# Cited passages come back on the response as Message#citations.
|
|
17
|
+
class SearchResults
|
|
18
|
+
KEY = 'search_results' # :nodoc:
|
|
19
|
+
|
|
20
|
+
# The normalized results, as an array of hashes with +:title+, +:text+,
|
|
21
|
+
# and optional +:url+ keys.
|
|
22
|
+
attr_reader :results
|
|
23
|
+
|
|
24
|
+
# Returns a new SearchResults built from one or more result hashes, or
|
|
25
|
+
# from a single result given as keywords.
|
|
26
|
+
#
|
|
27
|
+
# RubyLLM::SearchResults.new(title: 'Q4 Report', url: report_url, text: report_text)
|
|
28
|
+
# RubyLLM::SearchResults.new({ title: 'A', text: '...' }, { title: 'B', text: '...' })
|
|
29
|
+
#
|
|
30
|
+
# Each result is reduced to its +:title+, +:url+, and +:text+ entries.
|
|
31
|
+
# Raises ArgumentError if no results are given or a result is missing
|
|
32
|
+
# +:title+ or +:text+.
|
|
33
|
+
def initialize(*results, **result)
|
|
34
|
+
results << result if result.any?
|
|
35
|
+
@results = results.map { |entry| normalize(entry) }
|
|
36
|
+
raise ArgumentError, 'SearchResults requires at least one result' if @results.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.from_content(content) # :nodoc:
|
|
40
|
+
return unless content.is_a?(String) && content.lstrip.start_with?('{')
|
|
41
|
+
|
|
42
|
+
parsed = JSON.parse(content)
|
|
43
|
+
entries = parsed[KEY]
|
|
44
|
+
return unless entries.is_a?(Array) && entries.any? && entries.all?(Hash)
|
|
45
|
+
|
|
46
|
+
new(*entries)
|
|
47
|
+
rescue JSON::ParserError, ArgumentError
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_h # :nodoc:
|
|
52
|
+
{ KEY => results }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def to_json(*args) # :nodoc:
|
|
56
|
+
JSON.generate(to_h, *args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def normalize(entry)
|
|
62
|
+
entry = Support::Utils.deep_symbolize_keys(entry.to_h)
|
|
63
|
+
raise ArgumentError, 'Search results require :title and :text' unless entry[:title] && entry[:text]
|
|
64
|
+
|
|
65
|
+
entry.slice(:title, :url, :text)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A ServerToolCall records one provider-executed tool step in an assistant
|
|
5
|
+
# response: a web search the model ran, a code execution, or the results
|
|
6
|
+
# block a provider returned for one. They appear on
|
|
7
|
+
# Message#server_tool_calls when a chat enables tools with
|
|
8
|
+
# Chat#with_server_tools.
|
|
9
|
+
#
|
|
10
|
+
# response = chat.with_server_tools(:web_search).ask "What changed in Ruby 3.5?"
|
|
11
|
+
# response.server_tool_calls.map(&:type) # => ["server_tool_use", "web_search_tool_result"]
|
|
12
|
+
#
|
|
13
|
+
# RubyLLM does not model each tool's result schema. #raw always holds the
|
|
14
|
+
# provider's block exactly as received, and is what RubyLLM replays to the
|
|
15
|
+
# provider in subsequent turns when the wire format requires it.
|
|
16
|
+
class ServerToolCall
|
|
17
|
+
include Support::Inspectable
|
|
18
|
+
|
|
19
|
+
# The provider's block or item type, such as <tt>"server_tool_use"</tt>,
|
|
20
|
+
# <tt>"web_search_tool_result"</tt>, or <tt>"web_search_call"</tt>.
|
|
21
|
+
attr_reader :type
|
|
22
|
+
|
|
23
|
+
# The tool name when the provider reports one, such as
|
|
24
|
+
# <tt>"web_search"</tt>, or +nil+.
|
|
25
|
+
attr_reader :name
|
|
26
|
+
|
|
27
|
+
# The provider's identifier for the call, or +nil+.
|
|
28
|
+
attr_reader :id
|
|
29
|
+
|
|
30
|
+
# The input the model gave the tool (a query, code to run), in the
|
|
31
|
+
# provider's shape, or +nil+.
|
|
32
|
+
attr_reader :input
|
|
33
|
+
|
|
34
|
+
# The tool's output in the provider's shape, or +nil+ for blocks that
|
|
35
|
+
# only record the invocation.
|
|
36
|
+
attr_reader :result
|
|
37
|
+
|
|
38
|
+
# The complete provider block as received, used verbatim when the
|
|
39
|
+
# conversation is sent back to the provider.
|
|
40
|
+
attr_reader :raw
|
|
41
|
+
|
|
42
|
+
def self.from_h(data) # :nodoc:
|
|
43
|
+
data = Support::Utils.deep_symbolize_keys(data)
|
|
44
|
+
new(type: data[:type], name: data[:name], id: data[:id], input: data[:input],
|
|
45
|
+
result: data[:result], raw: data[:raw])
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def initialize(type:, raw:, name: nil, id: nil, input: nil, result: nil) # :nodoc:
|
|
49
|
+
@type = type
|
|
50
|
+
@name = name
|
|
51
|
+
@id = id
|
|
52
|
+
@input = input
|
|
53
|
+
@result = result
|
|
54
|
+
@raw = raw
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the call's attributes as a Hash, omitting +nil+ values.
|
|
58
|
+
def to_h
|
|
59
|
+
{
|
|
60
|
+
type: type,
|
|
61
|
+
name: name,
|
|
62
|
+
id: id,
|
|
63
|
+
input: input,
|
|
64
|
+
result: result,
|
|
65
|
+
raw: raw
|
|
66
|
+
}.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def inspect_attributes # :nodoc:
|
|
70
|
+
{ type: type, name: name, id: id, input: input }
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A Speech is audio generated from text. RubyLLM.speak returns one. It
|
|
5
|
+
# holds the raw audio bytes along with the model, voice, and format used.
|
|
6
|
+
#
|
|
7
|
+
# speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
|
|
8
|
+
# speech.save "welcome.mp3"
|
|
9
|
+
#
|
|
10
|
+
class Speech
|
|
11
|
+
include Support::Inspectable
|
|
12
|
+
include Accounting::Usage::Result
|
|
13
|
+
|
|
14
|
+
# Maps audio format names to their MIME types.
|
|
15
|
+
MIME_TYPES = {
|
|
16
|
+
'aac' => 'audio/aac',
|
|
17
|
+
'flac' => 'audio/flac',
|
|
18
|
+
'mp3' => 'audio/mpeg',
|
|
19
|
+
'opus' => 'audio/opus',
|
|
20
|
+
'pcm' => 'audio/pcm',
|
|
21
|
+
'wav' => 'audio/wav'
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# The raw audio bytes returned by the provider.
|
|
25
|
+
attr_reader :data
|
|
26
|
+
|
|
27
|
+
# The id of the model that generated the audio.
|
|
28
|
+
attr_reader :model
|
|
29
|
+
|
|
30
|
+
# The voice used for synthesis. When no +voice:+ was given, this is the
|
|
31
|
+
# provider default.
|
|
32
|
+
attr_reader :voice
|
|
33
|
+
|
|
34
|
+
# The audio format name, such as <tt>"mp3"</tt> or <tt>"pcm"</tt>.
|
|
35
|
+
attr_reader :format
|
|
36
|
+
|
|
37
|
+
# The MIME type of the audio, such as <tt>"audio/mpeg"</tt>.
|
|
38
|
+
attr_reader :mime_type
|
|
39
|
+
|
|
40
|
+
def initialize(data:, model:, voice: nil, format: 'mp3', mime_type: nil, # :nodoc:
|
|
41
|
+
input_tokens: nil, output_tokens: nil)
|
|
42
|
+
@data = data
|
|
43
|
+
@model = model
|
|
44
|
+
@voice = voice
|
|
45
|
+
@format = (format || 'mp3').to_s
|
|
46
|
+
@mime_type = mime_type || MIME_TYPES.fetch(@format, "audio/#{@format}")
|
|
47
|
+
@input_tokens = input_tokens
|
|
48
|
+
@output_tokens = output_tokens
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Generates speech for +input+ and returns a Speech holding the audio.
|
|
52
|
+
# Uses <tt>config.default_speech_model</tt> unless +model:+ is given.
|
|
53
|
+
# Pass +provider:+ and <tt>assume_model_exists: true</tt> to use a model
|
|
54
|
+
# that is not in the registry. +provider_options:+ takes options in the
|
|
55
|
+
# provider's request vocabulary, such as +instructions:+ and +speed:+
|
|
56
|
+
# for OpenAI, and merges them into the request as-is.
|
|
57
|
+
#
|
|
58
|
+
# speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
|
|
59
|
+
# speech.save "welcome.mp3"
|
|
60
|
+
#
|
|
61
|
+
# RubyLLM.speak "Welcome back.", voice: "nova"
|
|
62
|
+
# RubyLLM.speak "Save this as a WAV file.", format: "wav"
|
|
63
|
+
# RubyLLM.speak "Say cheerfully: Have a wonderful day!",
|
|
64
|
+
# model: "gemini-3.1-flash-tts-preview", provider: :gemini
|
|
65
|
+
#
|
|
66
|
+
# Given a block, yields SpeechChunk objects as audio arrives and still
|
|
67
|
+
# returns the complete Speech. Chunks contain consecutive bytes of the
|
|
68
|
+
# recording and are not separate audio files.
|
|
69
|
+
#
|
|
70
|
+
# File.open("welcome.mp3", "wb") do |file|
|
|
71
|
+
# RubyLLM.speak("Welcome back.") { |chunk| file.write(chunk.data) }
|
|
72
|
+
# end
|
|
73
|
+
#
|
|
74
|
+
# Raises RubyLLM::Error when the selected protocol cannot stream speech,
|
|
75
|
+
# or RubyLLM::ModelNotFoundError if +model:+ is not in the registry.
|
|
76
|
+
def self.speak(input,
|
|
77
|
+
model: nil,
|
|
78
|
+
provider: nil,
|
|
79
|
+
assume_model_exists: false,
|
|
80
|
+
voice: nil,
|
|
81
|
+
format: nil,
|
|
82
|
+
context: nil,
|
|
83
|
+
provider_options: {},
|
|
84
|
+
metadata: nil,
|
|
85
|
+
&block)
|
|
86
|
+
config = context&.config || RubyLLM.config
|
|
87
|
+
model ||= config.default_speech_model
|
|
88
|
+
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
89
|
+
config: config)
|
|
90
|
+
empty_tokens = Tokens.new
|
|
91
|
+
|
|
92
|
+
payload = {
|
|
93
|
+
provider: provider_instance.slug,
|
|
94
|
+
provider_class: provider_instance.class.display_name,
|
|
95
|
+
model: model.id,
|
|
96
|
+
model_info: model,
|
|
97
|
+
input: input,
|
|
98
|
+
voice: voice,
|
|
99
|
+
format: format,
|
|
100
|
+
provider_options: provider_options,
|
|
101
|
+
metadata: metadata,
|
|
102
|
+
streaming: !block.nil?,
|
|
103
|
+
tokens: empty_tokens,
|
|
104
|
+
cost: Cost.new(tokens: empty_tokens, model:, category: :audio_tokens)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
RubyLLM.instrument('speech.ruby_llm', payload, config: config) do |event|
|
|
108
|
+
result = provider_instance.speak(input, model:, voice:, format:, provider_options:, &block)
|
|
109
|
+
event[:result] = result
|
|
110
|
+
event[:response_model] = result.model
|
|
111
|
+
event[:voice] = result.voice
|
|
112
|
+
event[:format] = result.format
|
|
113
|
+
event[:audio_bytes] = result.to_blob.bytesize
|
|
114
|
+
event[:tokens] = result.tokens
|
|
115
|
+
event[:cost] = result.cost
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the raw audio bytes. Alias for #data, mirroring Image#to_blob.
|
|
121
|
+
def to_blob
|
|
122
|
+
data
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Returns provider-reported usage across every attempt. Its fields are
|
|
126
|
+
# +nil+ when the provider did not report any.
|
|
127
|
+
def tokens
|
|
128
|
+
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
129
|
+
|
|
130
|
+
Tokens.new(input: @input_tokens, output: @output_tokens)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns the speech cost across every provider attempt.
|
|
134
|
+
def cost
|
|
135
|
+
return ruby_llm_usage_cost unless ruby_llm_usage_entries.empty?
|
|
136
|
+
|
|
137
|
+
Cost.new(tokens:, model: model_info, category: :audio_tokens)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def model_info # :nodoc:
|
|
141
|
+
@model_info ||= RubyLLM.models.find(model)
|
|
142
|
+
rescue ModelNotFoundError
|
|
143
|
+
nil
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Writes the audio to +path+ in binary mode and returns +path+.
|
|
147
|
+
#
|
|
148
|
+
# speech.save "welcome.mp3"
|
|
149
|
+
#
|
|
150
|
+
def save(path)
|
|
151
|
+
File.binwrite(File.expand_path(path), to_blob)
|
|
152
|
+
path
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def inspect_attributes # :nodoc:
|
|
156
|
+
{ model: model, voice: voice, format: format, data: data && "#{data.bytesize} bytes" }
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A piece of generated audio, yielded by RubyLLM.speak as it arrives.
|
|
5
|
+
# Chunks are consecutive bytes of one recording, not separate audio files.
|
|
6
|
+
class SpeechChunk
|
|
7
|
+
include Support::Inspectable
|
|
8
|
+
|
|
9
|
+
# The raw audio bytes in this chunk.
|
|
10
|
+
attr_reader :data
|
|
11
|
+
|
|
12
|
+
# The audio format name, such as <tt>"mp3"</tt> or <tt>"pcm"</tt>.
|
|
13
|
+
attr_reader :format
|
|
14
|
+
|
|
15
|
+
# The MIME type of the audio, such as <tt>"audio/mpeg"</tt>.
|
|
16
|
+
attr_reader :mime_type
|
|
17
|
+
|
|
18
|
+
def initialize(data:, format:, mime_type: nil) # :nodoc:
|
|
19
|
+
@data = data.b
|
|
20
|
+
@format = format.to_s
|
|
21
|
+
@mime_type = mime_type || Speech::MIME_TYPES.fetch(@format, "audio/#{@format}")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns the raw audio bytes. Alias for #data.
|
|
25
|
+
def to_blob
|
|
26
|
+
data
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def inspect_attributes # :nodoc:
|
|
30
|
+
{ format: format, data: "#{data.bytesize} bytes" }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support # :nodoc:
|
|
5
|
+
class Deprecator # :nodoc:
|
|
6
|
+
def warn(message)
|
|
7
|
+
case RubyLLM.config.deprecation_behavior
|
|
8
|
+
when :silence
|
|
9
|
+
nil
|
|
10
|
+
when :raise
|
|
11
|
+
raise DeprecationError, message
|
|
12
|
+
else
|
|
13
|
+
RubyLLM.logger.warn(message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deprecate(name, replacement:, removal:)
|
|
18
|
+
warn("#{name} is deprecated and will be removed in RubyLLM #{removal}. Use #{replacement} instead.")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support # :nodoc:
|
|
5
|
+
# Concise console output, the way Active Record prints records. Including
|
|
6
|
+
# classes declare what matters in +inspect_attributes+; #inspect shows
|
|
7
|
+
# those on one line with long strings truncated, and pretty printing
|
|
8
|
+
# follows #inspect so IRB stays readable. Inspection is a summary, not
|
|
9
|
+
# the data: everything remains reachable through the readers
|
|
10
|
+
# (+message.raw+, +embedding.vectors+, +chat.messages+).
|
|
11
|
+
module Inspectable
|
|
12
|
+
TRUNCATE_AT = 64 # :nodoc:
|
|
13
|
+
|
|
14
|
+
# Returns a one-line summary of the object built from its
|
|
15
|
+
# +inspect_attributes+, omitting empty ones and truncating long values.
|
|
16
|
+
def inspect
|
|
17
|
+
attributes = inspect_attributes
|
|
18
|
+
.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
19
|
+
.map { |name, value| "#{name}: #{format_for_inspect(value)}" }
|
|
20
|
+
return "#<#{self.class.name}>" if attributes.empty?
|
|
21
|
+
|
|
22
|
+
"#<#{self.class.name} #{attributes.join(', ')}>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def pretty_print(printer) # :nodoc:
|
|
26
|
+
printer.text(inspect)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the stock Ruby dump with every instance variable, for when
|
|
30
|
+
# the one-line summary is not enough.
|
|
31
|
+
#
|
|
32
|
+
# message.full_inspect
|
|
33
|
+
#
|
|
34
|
+
def full_inspect
|
|
35
|
+
Object.instance_method(:inspect).bind_call(self)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def format_for_inspect(value)
|
|
41
|
+
if value.is_a?(String) && value.length > TRUNCATE_AT
|
|
42
|
+
"#{value[0, TRUNCATE_AT]}...".inspect
|
|
43
|
+
else
|
|
44
|
+
value.inspect
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support # :nodoc:
|
|
5
|
+
module Instrumentation # :nodoc:
|
|
6
|
+
WORKFLOW_CONTEXT_KEY = :ruby_llm_workflow_context
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def instrument(name, payload = nil, config: nil, **attributes) # rubocop:disable Metrics/PerceivedComplexity
|
|
11
|
+
payload ||= {}
|
|
12
|
+
payload = payload.merge(attributes) unless attributes.empty?
|
|
13
|
+
workflow_context = current_workflow
|
|
14
|
+
payload = payload.merge(workflow_context) if workflow_context
|
|
15
|
+
instrumenter = (config || RubyLLM.config).instrumenter
|
|
16
|
+
|
|
17
|
+
if instrumenter.respond_to?(:instrument)
|
|
18
|
+
if block_given?
|
|
19
|
+
instrumenter.instrument(name, payload) { yield(payload) }
|
|
20
|
+
else
|
|
21
|
+
instrumenter.instrument(name, payload)
|
|
22
|
+
end
|
|
23
|
+
elsif block_given?
|
|
24
|
+
yield(payload)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def current_workflow
|
|
29
|
+
Thread.current[WORKFLOW_CONTEXT_KEY]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def with_workflow(context)
|
|
33
|
+
previous_context = current_workflow
|
|
34
|
+
Thread.current[WORKFLOW_CONTEXT_KEY] = context
|
|
35
|
+
yield
|
|
36
|
+
ensure
|
|
37
|
+
Thread.current[WORKFLOW_CONTEXT_KEY] = previous_context
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|