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,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
# The OpenAI Chat Completions API — the lingua franca of LLM APIs.
|
|
6
|
+
class ChatCompletions < Protocol
|
|
7
|
+
include ChatCompletions::Chat
|
|
8
|
+
include ChatCompletions::Embeddings
|
|
9
|
+
include ChatCompletions::Models
|
|
10
|
+
include ChatCompletions::Moderation
|
|
11
|
+
include ChatCompletions::Streaming
|
|
12
|
+
include ChatCompletions::Tools
|
|
13
|
+
include ChatCompletions::Images
|
|
14
|
+
include ChatCompletions::Media
|
|
15
|
+
include ChatCompletions::Speech
|
|
16
|
+
include ChatCompletions::Transcription
|
|
17
|
+
|
|
18
|
+
public :render_transcription_options
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
module BatchRequests # :nodoc: all
|
|
7
|
+
CHAT_FIELDS = %w[
|
|
8
|
+
messages tools temperature p frequency_penalty presence_penalty reasoning thinking_budget
|
|
9
|
+
return_prompt logprobs max_tokens max_input_tokens k seed
|
|
10
|
+
].freeze
|
|
11
|
+
EMBEDDING_FIELDS = %w[texts images input_type inputs max_tokens output_dimension embedding_types
|
|
12
|
+
truncate].freeze
|
|
13
|
+
private_constant :CHAT_FIELDS, :EMBEDDING_FIELDS
|
|
14
|
+
|
|
15
|
+
def batch_dataset_type(requests)
|
|
16
|
+
types = requests.map do |request|
|
|
17
|
+
body = request.fetch(:payload)
|
|
18
|
+
body.key?(:messages) || body.key?('messages') ? 'batch-chat-v2-input' : 'batch-embed-v2-input'
|
|
19
|
+
end.uniq
|
|
20
|
+
raise ArgumentError, 'Cohere batches cannot mix chat and embeddings' unless types.one?
|
|
21
|
+
|
|
22
|
+
types.first
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_batch_request(request, type:)
|
|
26
|
+
body = JSON.parse(JSON.generate(batch_payload(request, except: :model)))
|
|
27
|
+
if type == 'batch-embed-v2-input' && body['output_dimension']
|
|
28
|
+
raise ArgumentError,
|
|
29
|
+
'Cohere batch datasets currently reject dimensions; omit dimensions to use the model default'
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
render_batch_chat(body) if type == 'batch-chat-v2-input'
|
|
33
|
+
allowed = type == 'batch-chat-v2-input' ? CHAT_FIELDS : EMBEDDING_FIELDS
|
|
34
|
+
unsupported = body.keys - allowed
|
|
35
|
+
unless unsupported.empty?
|
|
36
|
+
raise ArgumentError, "Cohere batches do not support these request options: #{unsupported.join(', ')}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
custom_id = request.fetch(:custom_id)
|
|
40
|
+
custom_id = "#{custom_id}:array" if request[:text].is_a?(Array)
|
|
41
|
+
{ custom_id:, body: }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_batch_chat(body)
|
|
45
|
+
if (thinking = body.delete('thinking'))
|
|
46
|
+
body['reasoning'] = thinking['type'] != 'disabled'
|
|
47
|
+
body['thinking_budget'] = thinking['token_budget'] if thinking['token_budget']
|
|
48
|
+
end
|
|
49
|
+
Array(body['messages']).each { |message| render_batch_message(message) }
|
|
50
|
+
Array(body['tools']).each do |tool|
|
|
51
|
+
function = tool.fetch('function')
|
|
52
|
+
parameters = function['parameters']
|
|
53
|
+
function['parameters'] = JSON.generate(parameters) unless parameters.is_a?(String)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def render_batch_message(message)
|
|
58
|
+
content = message['content']
|
|
59
|
+
content = [{ 'type' => 'text', 'text' => content }] if content.is_a?(String)
|
|
60
|
+
message['content'] = content&.map { |part| render_batch_content(part) }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def render_batch_content(part)
|
|
64
|
+
unless %w[text thinking image_url].include?(part['type'])
|
|
65
|
+
raise ArgumentError, "Cohere batches do not support #{part['type']} content"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
part = part.dup
|
|
69
|
+
part['image_url'] = part['image_url'].fetch('url') if part['image_url'].is_a?(Hash)
|
|
70
|
+
part
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
module Batches # :nodoc: all
|
|
7
|
+
include RubyLLM::Batch::Helpers
|
|
8
|
+
include Cohere::BatchRequests
|
|
9
|
+
|
|
10
|
+
TERMINAL_STATUSES = %w[BATCH_STATUS_COMPLETED BATCH_STATUS_FAILED BATCH_STATUS_CANCELED].freeze
|
|
11
|
+
Response = Struct.new(:body)
|
|
12
|
+
private_constant :TERMINAL_STATUSES, :Response
|
|
13
|
+
|
|
14
|
+
def create_batch(requests)
|
|
15
|
+
model = single_batch_model!(requests, 'Cohere')
|
|
16
|
+
type = batch_dataset_type(requests)
|
|
17
|
+
rows = requests.map { |request| render_batch_request(request, type:) }
|
|
18
|
+
file = datasets.upload(StringIO.new(rows.map { |row| "#{JSON.generate(row)}\n" }.join),
|
|
19
|
+
filename: 'ruby-llm-batch.jsonl', purpose: type)
|
|
20
|
+
datasets.wait_for_validation(file.id)
|
|
21
|
+
response = @connection.post('v2/batches', {
|
|
22
|
+
name: 'ruby-llm-batch', input_dataset_id: file.id, model: model
|
|
23
|
+
}, idempotent: false)
|
|
24
|
+
parse_batch_response(response.body.fetch('batch'))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def find_batch(id)
|
|
28
|
+
parse_batch_response(batch_data(id))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def cancel_batch(id)
|
|
32
|
+
@connection.post("v2/batches/#{id}/cancel", {})
|
|
33
|
+
find_batch(id)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def batch_results(id)
|
|
37
|
+
data = batch_data(id)
|
|
38
|
+
return [] if data['output_dataset_id'].to_s.empty?
|
|
39
|
+
|
|
40
|
+
file = datasets.wait_for_validation(data.fetch('output_dataset_id'))
|
|
41
|
+
model = RubyLLM.models.find(data.fetch('model'), provider: @provider.slug, config: @config)
|
|
42
|
+
parser = self.class.new(@provider, model)
|
|
43
|
+
results = datasets.records(file).map { |row| parse_batch_result(row, parser:, model: model.id) }
|
|
44
|
+
unless results.map(&:first).uniq.size == results.size
|
|
45
|
+
raise Error,
|
|
46
|
+
'Cohere returned duplicate batch request IDs'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
results
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def datasets
|
|
55
|
+
@datasets ||= Cohere::Datasets.new(@provider)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def batch_data(id)
|
|
59
|
+
@connection.get("v2/batches/#{id}").body.fetch('batch')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_batch_response(data)
|
|
63
|
+
{
|
|
64
|
+
id: data.fetch('id'), raw_status: data.fetch('status'),
|
|
65
|
+
completed: TERMINAL_STATUSES.include?(data['status']), request_count: data['num_records'],
|
|
66
|
+
request_counts: {
|
|
67
|
+
'total' => data['num_records'], 'succeeded' => data['num_successful_records'],
|
|
68
|
+
'failed' => data['num_failed_records']
|
|
69
|
+
}.compact
|
|
70
|
+
}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def parse_batch_status(raw_status, completed:)
|
|
74
|
+
return :pending unless completed
|
|
75
|
+
return :succeeded if raw_status == 'BATCH_STATUS_COMPLETED'
|
|
76
|
+
return :cancelled if raw_status == 'BATCH_STATUS_CANCELED'
|
|
77
|
+
|
|
78
|
+
:failed
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parse_batch_result(row, parser:, model:)
|
|
82
|
+
custom_id, shape = row.fetch('custom_id').split(':', 2)
|
|
83
|
+
index = batch_result_index(custom_id)
|
|
84
|
+
return [index, nil, batch_failure(custom_id, row['error'])] if !row['error'].to_s.empty? || !row['body']
|
|
85
|
+
|
|
86
|
+
body = row.fetch('body')
|
|
87
|
+
result = if body['embeddings']
|
|
88
|
+
parser.send(:parse_embedding_response, Response.new(body), model:,
|
|
89
|
+
text: shape == 'array' ? [] : nil)
|
|
90
|
+
else
|
|
91
|
+
parser.send(:parse_completion_body, body, raw: body)
|
|
92
|
+
end
|
|
93
|
+
[index, result]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Chat methods for the Cohere v2 API implementation
|
|
7
|
+
module Chat
|
|
8
|
+
FINISH_REASONS = {
|
|
9
|
+
'COMPLETE' => :stop, 'STOP_SEQUENCE' => :stop, 'MAX_TOKENS' => :max_tokens, 'TOOL_CALL' => :tool_calls
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def finish_reasons = FINISH_REASONS
|
|
15
|
+
|
|
16
|
+
def normalize_finish_reason(reason)
|
|
17
|
+
return nil if reason.nil?
|
|
18
|
+
|
|
19
|
+
finish_reasons.fetch(reason.to_s) { reason.to_s.to_sym }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def completion_url
|
|
23
|
+
'v2/chat'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
27
|
+
def render_payload(messages, tools:, temperature:, model:, stream: false, max_output_tokens: nil,
|
|
28
|
+
schema: nil, thinking: nil, citations: false, caching: nil, tool_prefs: nil)
|
|
29
|
+
warn_unsupported_citations(model) if citations && !model.supports?(:citations)
|
|
30
|
+
|
|
31
|
+
payload = {
|
|
32
|
+
model: model.id,
|
|
33
|
+
messages: format_messages(messages, citations: citations),
|
|
34
|
+
stream: stream
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
add_optional_fields(payload, messages, temperature:, max_output_tokens:, citations:, schema:)
|
|
38
|
+
add_tools(payload, tools, tool_prefs || {})
|
|
39
|
+
add_thinking(payload, thinking)
|
|
40
|
+
payload
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def add_optional_fields(payload, messages, temperature:, max_output_tokens:, citations:, schema:)
|
|
44
|
+
payload[:temperature] = temperature unless temperature.nil?
|
|
45
|
+
payload[:max_tokens] = max_output_tokens unless max_output_tokens.nil?
|
|
46
|
+
payload[:documents] = Media.format_documents(messages) if citations && Media.documents?(messages)
|
|
47
|
+
payload[:response_format] = build_response_format(schema) if schema
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def warn_unsupported_citations(model)
|
|
51
|
+
RubyLLM.logger.warn(
|
|
52
|
+
"#{model.id} does not support citations according to the model registry. " \
|
|
53
|
+
'with_citations may have no effect.'
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def add_tools(payload, tools, tool_prefs)
|
|
58
|
+
return if tools.empty?
|
|
59
|
+
|
|
60
|
+
payload[:tools] = tools.values.map { |tool| Tools.function_for(tool) }
|
|
61
|
+
tool_choice = Tools.build_tool_choice(tool_prefs[:choice])
|
|
62
|
+
payload[:tool_choice] = tool_choice if tool_choice
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Cohere takes a bare JSON Schema under json_schema, with no name or
|
|
66
|
+
# strict wrapper.
|
|
67
|
+
def build_response_format(schema)
|
|
68
|
+
normalized = RubyLLM::Support::Utils.deep_dup(schema[:schema])
|
|
69
|
+
normalized.delete(:strict)
|
|
70
|
+
normalized.delete('strict')
|
|
71
|
+
|
|
72
|
+
{ type: 'json_object', json_schema: normalized }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Reasoning is on by default for models that support it, so an
|
|
76
|
+
# explicit disable is as meaningful as an explicit enable.
|
|
77
|
+
def add_thinking(payload, thinking)
|
|
78
|
+
return unless thinking&.enabled?
|
|
79
|
+
return payload[:thinking] = { type: 'disabled' } if thinking.disabled?
|
|
80
|
+
|
|
81
|
+
payload[:thinking] = { type: 'enabled', token_budget: thinking.budget }.compact
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def format_messages(messages, citations: false)
|
|
85
|
+
messages.map { |msg| format_message(msg, citations: citations) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def format_message(msg, citations: false)
|
|
89
|
+
return Tools.format_tool_result(msg) if msg.tool_result?
|
|
90
|
+
return format_assistant_message(msg) if msg.role == :assistant
|
|
91
|
+
|
|
92
|
+
{
|
|
93
|
+
role: msg.role.to_s,
|
|
94
|
+
content: Media.format_content(msg.content, msg.attachments, citations: citations)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Cohere returns tool_plan alongside tool calls, and RubyLLM surfaces
|
|
99
|
+
# it as thinking, but the newer models reject it on the way back in.
|
|
100
|
+
# It is optional in a request, so the plan stays out of the history.
|
|
101
|
+
def format_assistant_message(msg)
|
|
102
|
+
message = { role: 'assistant' }
|
|
103
|
+
content = Media.format_content(msg.content, msg.attachments)
|
|
104
|
+
message[:content] = content unless content.empty?
|
|
105
|
+
message[:tool_calls] = Tools.format_tool_calls(msg.tool_calls) if msg.tool_call?
|
|
106
|
+
message
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_completion_body(data, raw:)
|
|
110
|
+
message_data = data['message'] || {}
|
|
111
|
+
blocks = Array(message_data['content'])
|
|
112
|
+
content, offsets = extract_text(blocks)
|
|
113
|
+
finish_reason = normalize_finish_reason(data['finish_reason'])
|
|
114
|
+
|
|
115
|
+
Message.new(
|
|
116
|
+
role: :assistant,
|
|
117
|
+
content: content,
|
|
118
|
+
citations: parse_citations(message_data['citations'], offsets),
|
|
119
|
+
thinking: Thinking.build(text: extract_thinking(blocks, message_data)),
|
|
120
|
+
tool_calls: Tools.parse_tool_calls(message_data['tool_calls'], response: raw, finish_reason:),
|
|
121
|
+
finish_reason: finish_reason,
|
|
122
|
+
model: model&.id,
|
|
123
|
+
raw: raw,
|
|
124
|
+
**usage_tokens(data['usage'] || {})
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Cohere reports what the model processed under tokens and what it
|
|
129
|
+
# charges for under billed_units; the two differ because Cohere does
|
|
130
|
+
# not bill its own preamble.
|
|
131
|
+
def usage_tokens(usage)
|
|
132
|
+
tokens = usage['tokens'] || {}
|
|
133
|
+
billed = usage['billed_units'] || {}
|
|
134
|
+
|
|
135
|
+
{
|
|
136
|
+
input_tokens: tokens['input_tokens'] || billed['input_tokens'],
|
|
137
|
+
output_tokens: tokens['output_tokens'] || billed['output_tokens'],
|
|
138
|
+
cache_read_tokens: usage['cached_tokens']
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Returns the joined text of the response along with the offset each
|
|
143
|
+
# content block starts at, so citation spans resolve against the
|
|
144
|
+
# content string RubyLLM exposes.
|
|
145
|
+
def extract_text(blocks)
|
|
146
|
+
text = +''
|
|
147
|
+
offsets = {}
|
|
148
|
+
|
|
149
|
+
blocks.each_with_index do |block, index|
|
|
150
|
+
next unless block['type'] == 'text'
|
|
151
|
+
|
|
152
|
+
offsets[index] = text.length
|
|
153
|
+
text << block['text'].to_s
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
[text, offsets]
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The tool plan is the model's reasoning for a tool-calling turn, and
|
|
160
|
+
# is the only reasoning Cohere returns when thinking blocks are absent.
|
|
161
|
+
def extract_thinking(blocks, message_data)
|
|
162
|
+
thoughts = blocks.select { |block| block['type'] == 'thinking' }
|
|
163
|
+
.map { |block| block['thinking'] }.join
|
|
164
|
+
thoughts.empty? ? message_data['tool_plan'] : thoughts
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def parse_citations(citations, offsets)
|
|
168
|
+
Array(citations).map { |citation| parse_citation(citation, offsets) }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Only citations of the response text carry offsets into #content.
|
|
172
|
+
# Citations of the thinking blocks or the tool plan point into text
|
|
173
|
+
# RubyLLM exposes elsewhere, so they keep their snippet without a span.
|
|
174
|
+
def parse_citation(data, offsets = {})
|
|
175
|
+
source = Array(data['sources']).first
|
|
176
|
+
document = source_document(source)
|
|
177
|
+
start_index, end_index = citation_span(data, offsets)
|
|
178
|
+
|
|
179
|
+
Citation.new(
|
|
180
|
+
url: citation_url(document),
|
|
181
|
+
title: document['title'] || document['id'],
|
|
182
|
+
cited_text: document['text'] || document['snippet'],
|
|
183
|
+
text: data['text'],
|
|
184
|
+
start_index: start_index,
|
|
185
|
+
end_index: end_index,
|
|
186
|
+
source_index: source_index(source)
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def citation_span(data, offsets)
|
|
191
|
+
return [nil, nil] unless text_citation?(data)
|
|
192
|
+
|
|
193
|
+
offset = offsets[data['content_index'] || 0]
|
|
194
|
+
return [nil, nil] unless offset
|
|
195
|
+
|
|
196
|
+
[data['start'] && (offset + data['start']), data['end'] && (offset + data['end'])]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def text_citation?(data)
|
|
200
|
+
type = data['type']
|
|
201
|
+
type.nil? || type == 'TEXT_CONTENT'
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def source_document(source)
|
|
205
|
+
return {} unless source
|
|
206
|
+
|
|
207
|
+
source['document'] || source['tool_output'] || {}
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def citation_url(document)
|
|
211
|
+
url = document['url']
|
|
212
|
+
url if url.is_a?(String) && url.match?(%r{\Ahttps?://}i)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Documents RubyLLM sends, and those Cohere numbers itself, are
|
|
216
|
+
# identified as doc:N where N is the document's position.
|
|
217
|
+
def source_index(source)
|
|
218
|
+
id = source && source['id']
|
|
219
|
+
return unless id.is_a?(String)
|
|
220
|
+
|
|
221
|
+
match = id.match(/\Adoc:(\d+)\z/)
|
|
222
|
+
match && match[1].to_i
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Cohere datasets for batch input and generated results.
|
|
7
|
+
class Datasets < Protocols::Files
|
|
8
|
+
def upload(file, filename: nil, purpose: nil, expires_in: nil, uri: nil, content_type: nil,
|
|
9
|
+
provider_options: {})
|
|
10
|
+
raise ArgumentError, 'Cohere datasets require purpose: with a dataset type' unless purpose
|
|
11
|
+
raise ArgumentError, 'Cohere datasets do not accept expires_in or uri' if expires_in || uri
|
|
12
|
+
|
|
13
|
+
attachment = file_attachment(file, filename:)
|
|
14
|
+
options = { name: attachment.filename, type: purpose, keep_original_file: true }
|
|
15
|
+
.merge(provider_options.transform_keys(&:to_sym))
|
|
16
|
+
response = @connection.post('v1/datasets', { data: file_part(attachment, content_type:) },
|
|
17
|
+
idempotent: false) do |request|
|
|
18
|
+
request.headers.delete('Content-Type')
|
|
19
|
+
request.params.update(options)
|
|
20
|
+
end
|
|
21
|
+
find(response.body.fetch('id'))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def download(file_id)
|
|
25
|
+
file = wait_for_validation(file_id)
|
|
26
|
+
parts = dataset_parts(file.metadata)
|
|
27
|
+
originals = parts.filter_map { |part| part['original_url'] }.uniq
|
|
28
|
+
if parts.any? && parts.all? { |part| part['original_url'] }
|
|
29
|
+
return originals.map do |url|
|
|
30
|
+
download_part(url)
|
|
31
|
+
end.join
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
records(file).map { |row| "#{JSON.generate(row)}\n" }.join
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def records(file)
|
|
38
|
+
load_avro
|
|
39
|
+
dataset_parts(file.metadata).flat_map do |part|
|
|
40
|
+
reader = nil
|
|
41
|
+
content = StringIO.new(download_part(part.fetch('url')))
|
|
42
|
+
reader = Avro::DataFile::Reader.new(content, Avro::IO::DatumReader.new)
|
|
43
|
+
reader.to_a
|
|
44
|
+
ensure
|
|
45
|
+
reader&.close
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def wait_for_validation(id)
|
|
50
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @config.request_timeout
|
|
51
|
+
loop do
|
|
52
|
+
file = find(id)
|
|
53
|
+
return file if file.status == 'validated'
|
|
54
|
+
if file.status == 'failed'
|
|
55
|
+
raise Error, "Cohere dataset #{id} failed validation: #{file.metadata['validation_error']}"
|
|
56
|
+
end
|
|
57
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
58
|
+
raise Error, "Cohere dataset validation timed out: #{id}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
sleep 1
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def files_url
|
|
68
|
+
'v1/datasets'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def parse_file_response(response)
|
|
72
|
+
data = response.fetch('dataset')
|
|
73
|
+
parts = dataset_parts(data)
|
|
74
|
+
original = parts.first&.fetch('original_url', nil)
|
|
75
|
+
filename = original ? File.basename(URI.parse(original).path) : "#{data.fetch('name')}.jsonl"
|
|
76
|
+
mime_type = if File.extname(filename) == '.jsonl'
|
|
77
|
+
'application/jsonl'
|
|
78
|
+
else
|
|
79
|
+
RubyLLM::Files::MimeType.for(name: filename)
|
|
80
|
+
end
|
|
81
|
+
uploaded_file(data, id: data.fetch('id'), filename:, mime_type:,
|
|
82
|
+
created_at: timestamp(data['created_at']), status: data['validation_status'],
|
|
83
|
+
purpose: data['dataset_type'], downloadable: !parts.empty?)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def dataset_parts(data)
|
|
87
|
+
Array(data['dataset_parts']).sort_by.with_index { |part, index| part.fetch('index', index) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def download_part(url)
|
|
91
|
+
Transport::Connection.basic(@config).get(url).body
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def load_avro
|
|
95
|
+
require 'avro'
|
|
96
|
+
rescue LoadError
|
|
97
|
+
raise LoadError, 'Add gem "avro" to your Gemfile to read Cohere batch results and processed datasets'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Embeddings methods for the Cohere v2 API integration
|
|
7
|
+
module Embeddings
|
|
8
|
+
DEFAULT_INPUT_TYPE = 'search_document'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def embedding_url(...)
|
|
13
|
+
'v2/embed'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# rubocop:disable-next Lint/UnusedMethodArgument
|
|
17
|
+
def render_embedding_payload(text, model:, dimensions:, task_type: nil, title: nil, with: [],
|
|
18
|
+
provider_options: {})
|
|
19
|
+
image_only = with.any? && separate_image_embeddings?(model)
|
|
20
|
+
payload = {
|
|
21
|
+
model: model,
|
|
22
|
+
input_type: task_type || (image_only ? 'image' : DEFAULT_INPUT_TYPE),
|
|
23
|
+
embedding_types: ['float'],
|
|
24
|
+
output_dimension: dimensions
|
|
25
|
+
}.compact
|
|
26
|
+
|
|
27
|
+
payload.merge!(image_only ? image_embedding_inputs(text, with) : embedding_inputs(text, with))
|
|
28
|
+
Support::Utils.deep_merge(payload, provider_options)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def supports_embedding_media?
|
|
32
|
+
true
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def separate_image_embeddings?(model) # :nodoc:
|
|
36
|
+
%w[embed-english-v3.0 embed-multilingual-v3.0].include?(model)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def image_embedding_inputs(text, attachments) # :nodoc:
|
|
40
|
+
raise ArgumentError, 'Cohere Embed v3 accepts text or an image, not both' unless text.nil? || text == ''
|
|
41
|
+
raise ArgumentError, 'Cohere Embed v3 accepts one image per request' unless attachments.one?
|
|
42
|
+
raise UnsupportedAttachmentError, attachments.first.mime_type unless attachments.first.image?
|
|
43
|
+
|
|
44
|
+
{ images: ["data:#{attachments.first.mime_type};base64,#{attachments.first.encoded}"] }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def parse_embedding_response(response, model:, text:)
|
|
48
|
+
data = response.body
|
|
49
|
+
vectors = data.dig('embeddings', 'float')
|
|
50
|
+
vectors = vectors.first if vectors&.length == 1 && !text.is_a?(Array)
|
|
51
|
+
billed = data.dig('meta', 'billed_units') || {}
|
|
52
|
+
|
|
53
|
+
Embedding.new(vectors:, model:, input_tokens: billed['input_tokens'])
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# embed-v4 takes mixed text and images as `inputs`; text-only requests
|
|
57
|
+
# keep using the simpler `texts` array every Embed model accepts.
|
|
58
|
+
def embedding_inputs(text, attachments)
|
|
59
|
+
return { texts: Support::Utils.to_safe_array(text).map(&:to_s) } if attachments.empty?
|
|
60
|
+
|
|
61
|
+
raise ArgumentError, 'embed one text at a time when embedding attachments' if text.is_a?(Array)
|
|
62
|
+
|
|
63
|
+
{ inputs: [{ content: Media.format_content(text, attachments) }] }
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Protocols
|
|
5
|
+
class Cohere
|
|
6
|
+
# Handles formatting of media content for the Cohere v2 API
|
|
7
|
+
module Media
|
|
8
|
+
# Cohere resolves a remote image by its extension and rejects a URL
|
|
9
|
+
# without one, so those are inlined from the bytes already fetched.
|
|
10
|
+
URL_IMAGE_EXTENSIONS = %w[png jpeg jpg gif webp].freeze
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Cohere user messages accept only text and image_url blocks. Text
|
|
15
|
+
# attachments become citable documents when citations are on, so they
|
|
16
|
+
# are pulled out of the message and left out of the content blocks.
|
|
17
|
+
def format_content(content, attachments = [], citations: false)
|
|
18
|
+
parts = []
|
|
19
|
+
parts << format_text(content) if content
|
|
20
|
+
|
|
21
|
+
attachments.each do |attachment|
|
|
22
|
+
case attachment.type
|
|
23
|
+
when :image
|
|
24
|
+
parts << format_image(attachment)
|
|
25
|
+
when :text
|
|
26
|
+
parts << format_text(attachment.for_llm) unless citations
|
|
27
|
+
else
|
|
28
|
+
raise UnsupportedAttachmentError, attachment.mime_type
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
parts
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_text(text)
|
|
36
|
+
{
|
|
37
|
+
type: 'text',
|
|
38
|
+
text: text
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def format_image(image)
|
|
43
|
+
{
|
|
44
|
+
type: 'image_url',
|
|
45
|
+
image_url: { url: image_source(image) }
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def image_source(image)
|
|
50
|
+
return image.source.to_s if image.url? && URL_IMAGE_EXTENSIONS.include?(image.extension)
|
|
51
|
+
|
|
52
|
+
"data:#{image.mime_type};base64,#{image.encoded}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Text attachments across the conversation become the request's
|
|
56
|
+
# top-level documents array, which is what Cohere cites against.
|
|
57
|
+
def format_documents(messages)
|
|
58
|
+
index = -1
|
|
59
|
+
|
|
60
|
+
messages.flat_map do |message|
|
|
61
|
+
message.attachments.select(&:text?).map do |attachment|
|
|
62
|
+
index += 1
|
|
63
|
+
{
|
|
64
|
+
id: "doc:#{index}",
|
|
65
|
+
data: { title: attachment.filename, text: attachment.content }.compact
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def documents?(messages)
|
|
72
|
+
messages.any? { |message| message.attachments.any?(&:text?) }
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|