ruby_llm 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +160 -120
- data/app/assets/javascripts/ruby_llm/chat_message_controller.js +163 -0
- data/app/assets/javascripts/ruby_llm/dompurify.js +9 -0
- data/app/assets/javascripts/ruby_llm/marked.js +71 -0
- data/config/importmap.rb +5 -0
- data/lib/generators/ruby_llm/agent/agent_generator.rb +11 -3
- data/lib/generators/ruby_llm/agent/templates/agent.rb.tt +6 -3
- data/lib/generators/ruby_llm/chat_ui/chat_ui_generator.rb +201 -55
- data/lib/generators/ruby_llm/chat_ui/templates/agents/chat_agent.rb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/answers_controller.rb.tt +31 -0
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/chats_controller.rb.tt +16 -5
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/messages_controller.rb.tt +27 -9
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/models_controller.rb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/controllers/queued_messages_controller.rb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/javascript/chat_message_controller.js +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/chat_response_job.rb.tt +5 -8
- data/lib/generators/ruby_llm/chat_ui/templates/jobs/resume_interrupted_chats_job.rb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/migrations/add_reply_error_migration.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/chats/show.html.erb.tt +32 -3
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_assistant.html.erb.tt +17 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_form.html.erb.tt +28 -18
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_queued_message.html.erb.tt +18 -0
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/_user.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/messages/tool_results/_default.html.erb.tt +4 -2
- data/lib/generators/ruby_llm/chat_ui/templates/tailwind/views/questions/_question.html.erb.tt +101 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/agents/chat_agent_test.rb.tt +28 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/application_system_test_case.rb.tt +5 -0
- data/lib/generators/ruby_llm/chat_ui/templates/test/system/chats_test.rb.tt +240 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/answers/create.turbo_stream.erb.tt +6 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_files.html.erb.tt +15 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_form.html.erb.tt +5 -7
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_model_select.html.erb.tt +7 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/_questions.html.erb.tt +13 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/chats/show.html.erb.tt +29 -3
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_assistant.html.erb.tt +16 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_attachments.html.erb.tt +14 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_error.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_form.html.erb.tt +26 -16
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_queued_message.html.erb.tt +8 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_system.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_tool_calls.html.erb.tt +4 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/_user.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/create.turbo_stream.erb.tt +1 -0
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_calls/_default.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/chat_ui/templates/views/messages/tool_results/_default.html.erb.tt +3 -2
- data/lib/generators/ruby_llm/chat_ui/templates/views/questions/_question.html.erb.tt +78 -0
- data/lib/generators/ruby_llm/evaluation/evaluation_generator.rb +43 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.rb.tt +9 -0
- data/lib/generators/ruby_llm/evaluation/templates/evaluation.yml.tt +4 -0
- data/lib/generators/ruby_llm/generator_helpers.rb +116 -3
- data/lib/generators/ruby_llm/install/install_generator.rb +5 -47
- data/lib/generators/ruby_llm/install/templates/create_chats_migration.rb.tt +4 -0
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +5 -3
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +41 -1
- data/lib/generators/ruby_llm/install/templates/initializer.rb.tt +2 -1
- data/lib/generators/ruby_llm/install/templates/message_model.rb.tt +1 -2
- data/lib/generators/ruby_llm/judge/judge_generator.rb +35 -0
- data/lib/generators/ruby_llm/judge/templates/judge.rb.tt +2 -0
- data/lib/generators/ruby_llm/mcp/mcp_generator.rb +58 -0
- data/lib/generators/ruby_llm/mcp/templates/mcp.rb.tt +7 -0
- data/lib/generators/ruby_llm/provider/scaffold.rb +5 -12
- data/lib/generators/ruby_llm/provider/templates/core/provider.rb.erb +6 -0
- data/lib/generators/ruby_llm/provider/templates/gem/chat_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/chat_streaming_spec.rb.erb +0 -1
- data/lib/generators/ruby_llm/provider/templates/gem/moderation_spec.rb.erb +1 -1
- data/lib/generators/ruby_llm/sandbox/sandbox_generator.rb +68 -0
- data/lib/generators/ruby_llm/sandbox/templates/sandbox.rb.tt +3 -0
- data/lib/generators/ruby_llm/skill/skill_generator.rb +48 -0
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tailwind/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool.rb.tt +0 -3
- data/lib/generators/ruby_llm/tool/templates/tool_call.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/templates/tool_result.html.erb.tt +1 -1
- data/lib/generators/ruby_llm/tool/tool_generator.rb +1 -5
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_2.rb.tt +164 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +41 -3
- data/lib/ruby_llm/accounting/operation.rb +73 -0
- data/lib/ruby_llm/accounting/usage.rb +74 -24
- data/lib/ruby_llm/active_record/acts_as.rb +78 -11
- data/lib/ruby_llm/active_record/attachment_helpers.rb +10 -4
- data/lib/ruby_llm/active_record/batch.rb +67 -9
- data/lib/ruby_llm/active_record/chat_agent_hooks.rb +17 -0
- data/lib/ruby_llm/active_record/chat_approvals.rb +71 -0
- data/lib/ruby_llm/active_record/chat_cancellation.rb +106 -0
- data/lib/ruby_llm/active_record/chat_column_guard.rb +51 -0
- data/lib/ruby_llm/active_record/chat_file.rb +37 -0
- data/lib/ruby_llm/active_record/chat_files.rb +30 -0
- data/lib/ruby_llm/active_record/chat_helper.rb +73 -0
- data/lib/ruby_llm/active_record/chat_methods.rb +515 -274
- data/lib/ruby_llm/active_record/chat_placeholders.rb +67 -0
- data/lib/ruby_llm/active_record/chat_questions.rb +177 -0
- data/lib/ruby_llm/active_record/chat_queue.rb +120 -0
- data/lib/ruby_llm/active_record/chat_reply_error.rb +35 -0
- data/lib/ruby_llm/active_record/chat_run.rb +51 -0
- data/lib/ruby_llm/active_record/chat_tool_call_state.rb +77 -0
- data/lib/ruby_llm/active_record/chunk_methods.rb +49 -0
- data/lib/ruby_llm/active_record/claim_heartbeat.rb +149 -0
- data/lib/ruby_llm/active_record/file_store.rb +269 -0
- data/lib/ruby_llm/active_record/mcp_credential.rb +24 -9
- data/lib/ruby_llm/active_record/message_broadcasting.rb +85 -0
- data/lib/ruby_llm/active_record/message_methods.rb +83 -11
- data/lib/ruby_llm/active_record/model.rb +37 -9
- data/lib/ruby_llm/active_record/provider_file.rb +21 -4
- data/lib/ruby_llm/active_record/queued_message.rb +50 -0
- data/lib/ruby_llm/active_record/record.rb +3 -1
- data/lib/ruby_llm/active_record/run_claims.rb +608 -0
- data/lib/ruby_llm/active_record/solid_queue_shutdown.rb +33 -0
- data/lib/ruby_llm/active_record/system_instructions.rb +30 -0
- data/lib/ruby_llm/active_record/tool_call.rb +55 -4
- data/lib/ruby_llm/active_record/usage.rb +79 -9
- data/lib/ruby_llm/agent/callbacks.rb +98 -0
- data/lib/ruby_llm/agent/context_budget.rb +26 -0
- data/lib/ruby_llm/agent/join_callbacks.rb +28 -0
- data/lib/ruby_llm/agent/model_options.rb +56 -0
- data/lib/ruby_llm/agent/ownership.rb +22 -0
- data/lib/ruby_llm/agent/tool_conversion.rb +19 -0
- data/lib/ruby_llm/agent.rb +620 -236
- data/lib/ruby_llm/agent_tool.rb +91 -0
- data/lib/ruby_llm/aliases.json +84 -91
- data/lib/ruby_llm/attachment.rb +101 -20
- data/lib/ruby_llm/batch.rb +132 -46
- data/lib/ruby_llm/cached_content.rb +4 -4
- data/lib/ruby_llm/chat/agent_run.rb +22 -0
- data/lib/ruby_llm/chat/context_budget.rb +119 -0
- data/lib/ruby_llm/chat/file_references.rb +259 -0
- data/lib/ruby_llm/chat/message_queue.rb +56 -0
- data/lib/ruby_llm/chat/ownership.rb +18 -0
- data/lib/ruby_llm/chat/rescuable.rb +48 -0
- data/lib/ruby_llm/chat/run.rb +32 -0
- data/lib/ruby_llm/chat/tool_call_ids.rb +46 -0
- data/lib/ruby_llm/chat/tool_call_state.rb +115 -0
- data/lib/ruby_llm/chat/tool_concurrency.rb +90 -49
- data/lib/ruby_llm/chat/tool_search.rb +36 -1
- data/lib/ruby_llm/chat/tool_sequences.rb +84 -0
- data/lib/ruby_llm/chat.rb +1593 -331
- data/lib/ruby_llm/chunk.rb +27 -1
- data/lib/ruby_llm/citation.rb +2 -1
- data/lib/ruby_llm/code_execution/language.rb +28 -0
- data/lib/ruby_llm/code_execution/node_installation.rb +56 -0
- data/lib/ruby_llm/code_execution/packages.rb +241 -0
- data/lib/ruby_llm/code_execution/ruby_installation.rb +170 -0
- data/lib/ruby_llm/code_execution/workspace.rb +314 -0
- data/lib/ruby_llm/code_execution.rb +781 -0
- data/lib/ruby_llm/configuration.rb +151 -4
- data/lib/ruby_llm/content_filter.rb +68 -0
- data/lib/ruby_llm/cost.rb +68 -41
- data/lib/ruby_llm/downloaded_file.rb +10 -9
- data/lib/ruby_llm/embedding.rb +30 -45
- data/lib/ruby_llm/embedding_request.rb +1 -1
- data/lib/ruby_llm/error.rb +167 -14
- data/lib/ruby_llm/evaluation/dataset.rb +5 -4
- data/lib/ruby_llm/evaluation/evidence.rb +5 -3
- data/lib/ruby_llm/evaluation/report.rb +7 -3
- data/lib/ruby_llm/evaluation/result.rb +1 -1
- data/lib/ruby_llm/evaluation.rb +2 -1
- data/lib/ruby_llm/fallback.rb +32 -4
- data/lib/ruby_llm/file_store/converters/ocr.rb +28 -0
- data/lib/ruby_llm/file_store/converters/office/markup.rb +148 -0
- data/lib/ruby_llm/file_store/converters/office/outline.rb +193 -0
- data/lib/ruby_llm/file_store/converters/office/prose.rb +161 -0
- data/lib/ruby_llm/file_store/converters/office/workbook.rb +187 -0
- data/lib/ruby_llm/file_store/converters/office/zip.rb +149 -0
- data/lib/ruby_llm/file_store/converters/office.rb +106 -0
- data/lib/ruby_llm/file_store/converters/text.rb +27 -0
- data/lib/ruby_llm/file_store/converters.rb +84 -0
- data/lib/ruby_llm/file_store/directory.rb +190 -0
- data/lib/ruby_llm/file_store/memory.rb +75 -0
- data/lib/ruby_llm/file_store/tools/base.rb +99 -0
- data/lib/ruby_llm/file_store/tools/delete.rb +48 -0
- data/lib/ruby_llm/file_store/tools/edit.rb +155 -0
- data/lib/ruby_llm/file_store/tools/list.rb +57 -0
- data/lib/ruby_llm/file_store/tools/read.rb +120 -0
- data/lib/ruby_llm/file_store/tools/search.rb +208 -0
- data/lib/ruby_llm/file_store/tools/write.rb +40 -0
- data/lib/ruby_llm/file_store/tools.rb +39 -0
- data/lib/ruby_llm/file_store.rb +338 -0
- data/lib/ruby_llm/files/saving.rb +14 -0
- data/lib/ruby_llm/image.rb +67 -45
- data/lib/ruby_llm/input_required_error.rb +28 -0
- data/lib/ruby_llm/judge/builder.rb +6 -1
- data/lib/ruby_llm/judge.rb +1 -1
- data/lib/ruby_llm/judgment.rb +6 -24
- data/lib/ruby_llm/mcp/client.rb +50 -24
- data/lib/ruby_llm/mcp/collection.rb +39 -2
- data/lib/ruby_llm/mcp/content.rb +7 -2
- data/lib/ruby_llm/mcp/error.rb +8 -2
- data/lib/ruby_llm/mcp/http.rb +117 -37
- data/lib/ruby_llm/mcp/input_request.rb +3 -112
- data/lib/ruby_llm/mcp/input_required_error.rb +3 -21
- data/lib/ruby_llm/mcp/insufficient_scope_error.rb +27 -0
- data/lib/ruby_llm/mcp/listener.rb +7 -1
- data/lib/ruby_llm/mcp/oauth.rb +102 -39
- data/lib/ruby_llm/mcp/outages.rb +172 -0
- data/lib/ruby_llm/mcp/resource.rb +9 -6
- data/lib/ruby_llm/mcp/resource_template.rb +3 -2
- data/lib/ruby_llm/mcp/session_expired.rb +10 -2
- data/lib/ruby_llm/mcp/stdio.rb +66 -18
- data/lib/ruby_llm/mcp/task.rb +13 -40
- data/lib/ruby_llm/mcp/tool.rb +34 -11
- data/lib/ruby_llm/mcp.rb +123 -59
- data/lib/ruby_llm/message.rb +217 -23
- data/lib/ruby_llm/model/modalities.rb +1 -1
- data/lib/ruby_llm/model/pricing.rb +26 -4
- data/lib/ruby_llm/model/pricing_category.rb +38 -15
- data/lib/ruby_llm/model/pricing_tier.rb +3 -2
- data/lib/ruby_llm/model.rb +24 -6
- data/lib/ruby_llm/models/lookup.rb +37 -4
- data/lib/ruby_llm/models/registry.rb +4 -19
- data/lib/ruby_llm/models.json +36186 -27832
- data/lib/ruby_llm/models.rb +44 -39
- data/lib/ruby_llm/moderation.rb +10 -30
- data/lib/ruby_llm/ocr.rb +5 -22
- data/lib/ruby_llm/open_telemetry/attributes.rb +68 -19
- data/lib/ruby_llm/open_telemetry/content.rb +105 -0
- data/lib/ruby_llm/open_telemetry.rb +18 -4
- data/lib/ruby_llm/prompt/context.rb +2 -2
- data/lib/ruby_llm/prompt.rb +7 -5
- data/lib/ruby_llm/protocol/binary_streaming.rb +2 -8
- data/lib/ruby_llm/protocol/cache_lifetime.rb +22 -0
- data/lib/ruby_llm/protocol/native_content.rb +69 -0
- data/lib/ruby_llm/protocol/replayed_arguments.rb +28 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +73 -33
- data/lib/ruby_llm/protocol/streaming.rb +105 -22
- data/lib/ruby_llm/protocol/tool_namespaces.rb +100 -0
- data/lib/ruby_llm/protocol/unsupported_operations.rb +42 -0
- data/lib/ruby_llm/protocol.rb +127 -106
- data/lib/ruby_llm/protocols/anthropic/batches.rb +41 -4
- data/lib/ruby_llm/protocols/anthropic/block_binding.rb +30 -0
- data/lib/ruby_llm/protocols/anthropic/chat.rb +135 -60
- data/lib/ruby_llm/protocols/anthropic/files.rb +9 -0
- data/lib/ruby_llm/protocols/anthropic/media.rb +14 -14
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -10
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +43 -22
- data/lib/ruby_llm/protocols/anthropic/tools.rb +36 -24
- data/lib/ruby_llm/protocols/anthropic.rb +61 -6
- data/lib/ruby_llm/protocols/azure/speech/transcription.rb +125 -0
- data/lib/ruby_llm/protocols/azure/speech.rb +14 -0
- data/lib/ruby_llm/protocols/bedrock/batches.rb +26 -8
- data/lib/ruby_llm/protocols/bedrock/event_stream.rb +205 -0
- data/lib/ruby_llm/protocols/bedrock/files.rb +15 -6
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +1 -1
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +2 -0
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +158 -58
- data/lib/ruby_llm/protocols/chat_completions/content_filters.rb +45 -0
- data/lib/ruby_llm/protocols/chat_completions/embedding_batches.rb +15 -15
- data/lib/ruby_llm/protocols/chat_completions/embeddings.rb +26 -3
- data/lib/ruby_llm/protocols/chat_completions/images.rb +7 -9
- data/lib/ruby_llm/protocols/chat_completions/media.rb +20 -20
- data/lib/ruby_llm/protocols/chat_completions/moderation.rb +8 -5
- data/lib/ruby_llm/protocols/chat_completions/speech.rb +8 -2
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +38 -17
- data/lib/ruby_llm/protocols/chat_completions/think_tags.rb +148 -0
- data/lib/ruby_llm/protocols/chat_completions/tools.rb +15 -28
- data/lib/ruby_llm/protocols/chat_completions/transcription.rb +17 -2
- data/lib/ruby_llm/protocols/chat_completions.rb +4 -0
- data/lib/ruby_llm/protocols/cohere/batches.rb +15 -8
- data/lib/ruby_llm/protocols/cohere/chat.rb +47 -25
- data/lib/ruby_llm/protocols/cohere/datasets.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -5
- data/lib/ruby_llm/protocols/cohere/media.rb +7 -7
- data/lib/ruby_llm/protocols/cohere/rerank.rb +2 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +26 -11
- data/lib/ruby_llm/protocols/cohere/tools.rb +17 -28
- data/lib/ruby_llm/protocols/cohere/transcription.rb +5 -4
- data/lib/ruby_llm/protocols/converse/batches.rb +2 -24
- data/lib/ruby_llm/protocols/converse/chat.rb +210 -75
- data/lib/ruby_llm/protocols/converse/media.rb +24 -22
- data/lib/ruby_llm/protocols/converse/reasoning.rb +23 -9
- data/lib/ruby_llm/protocols/converse/streaming.rb +68 -220
- data/lib/ruby_llm/protocols/converse.rb +17 -1
- data/lib/ruby_llm/protocols/deepgram/speech.rb +1 -2
- data/lib/ruby_llm/protocols/deepgram/streaming_transcription.rb +1 -3
- data/lib/ruby_llm/protocols/deepgram/transcription.rb +28 -19
- data/lib/ruby_llm/protocols/deepgram.rb +3 -2
- data/lib/ruby_llm/protocols/deepseek/files.rb +1 -1
- data/lib/ruby_llm/protocols/elevenlabs/assets.rb +6 -4
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +4 -0
- data/lib/ruby_llm/protocols/elevenlabs/speech.rb +3 -3
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +5 -2
- data/lib/ruby_llm/protocols/elevenlabs/transcription.rb +6 -3
- data/lib/ruby_llm/protocols/files.rb +27 -12
- data/lib/ruby_llm/protocols/gemini/caches.rb +4 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +118 -51
- data/lib/ruby_llm/protocols/gemini/embedding_batches.rb +9 -5
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +3 -3
- data/lib/ruby_llm/protocols/gemini/file_transcription.rb +4 -0
- data/lib/ruby_llm/protocols/gemini/files.rb +5 -4
- data/lib/ruby_llm/protocols/gemini/images.rb +11 -9
- data/lib/ruby_llm/protocols/gemini/live_transcription.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/long_decode.rb +104 -0
- data/lib/ruby_llm/protocols/gemini/media.rb +31 -28
- data/lib/ruby_llm/protocols/gemini/retry_info.rb +29 -0
- data/lib/ruby_llm/protocols/gemini/schemas.rb +126 -0
- data/lib/ruby_llm/protocols/gemini/speech.rb +9 -3
- data/lib/ruby_llm/protocols/gemini/streaming.rb +37 -13
- data/lib/ruby_llm/protocols/gemini/tools.rb +18 -10
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -9
- data/lib/ruby_llm/protocols/gemini/videos.rb +14 -3
- data/lib/ruby_llm/protocols/gemini.rb +16 -0
- data/lib/ruby_llm/protocols/gpustack/responses.rb +3 -3
- data/lib/ruby_llm/protocols/gpustack/tokenization.rb +14 -0
- data/lib/ruby_llm/protocols/interactions/chat.rb +29 -14
- data/lib/ruby_llm/protocols/interactions/content.rb +19 -14
- data/lib/ruby_llm/protocols/interactions/streaming.rb +1 -1
- data/lib/ruby_llm/protocols/interactions/tools.rb +10 -13
- data/lib/ruby_llm/protocols/interactions/videos.rb +99 -0
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +28 -10
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +1 -1
- data/lib/ruby_llm/protocols/invoke_model/nova_embeddings.rb +45 -11
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +5 -2
- data/lib/ruby_llm/protocols/invoke_model/titan_multimodal_embeddings.rb +11 -3
- data/lib/ruby_llm/protocols/invoke_model.rb +21 -2
- data/lib/ruby_llm/protocols/meta/transcription/stream.rb +108 -0
- data/lib/ruby_llm/protocols/meta/transcription.rb +76 -0
- data/lib/ruby_llm/protocols/minimax/files.rb +58 -0
- data/lib/ruby_llm/protocols/minimax/images.rb +54 -0
- data/lib/ruby_llm/protocols/minimax/speech.rb +53 -0
- data/lib/ruby_llm/protocols/minimax/transcription.rb +82 -0
- data/lib/ruby_llm/protocols/minimax/videos.rb +93 -0
- data/lib/ruby_llm/protocols/minimax.rb +21 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +44 -21
- data/lib/ruby_llm/protocols/mistral/files.rb +6 -4
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +15 -4
- data/lib/ruby_llm/protocols/moonshot/files.rb +48 -0
- data/lib/ruby_llm/protocols/openai/batches.rb +3 -2
- data/lib/ruby_llm/protocols/openai/decisions.rb +6 -4
- data/lib/ruby_llm/protocols/openai/files.rb +13 -6
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -17
- data/lib/ruby_llm/protocols/openrouter/responses.rb +6 -10
- data/lib/ruby_llm/protocols/perplexity/agent.rb +19 -35
- data/lib/ruby_llm/protocols/perplexity/batches.rb +37 -0
- data/lib/ruby_llm/protocols/perplexity/files.rb +19 -9
- data/lib/ruby_llm/protocols/responses/approvals.rb +12 -11
- data/lib/ruby_llm/protocols/responses/chat.rb +165 -75
- data/lib/ruby_llm/protocols/responses/citation_markers.rb +167 -0
- data/lib/ruby_llm/protocols/responses/compaction.rb +2 -2
- data/lib/ruby_llm/protocols/responses/media.rb +17 -9
- data/lib/ruby_llm/protocols/responses/streaming.rb +71 -12
- data/lib/ruby_llm/protocols/responses/token_counting.rb +1 -1
- data/lib/ruby_llm/protocols/responses/tools.rb +48 -1
- data/lib/ruby_llm/protocols/responses.rb +21 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +9 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +4 -3
- data/lib/ruby_llm/protocols/vertexai/text_to_speech.rb +116 -0
- data/lib/ruby_llm/protocols/xai/files.rb +13 -6
- data/lib/ruby_llm/protocols/zai/files.rb +37 -0
- data/lib/ruby_llm/provider.rb +128 -31
- data/lib/ruby_llm/providers/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/azure/chat.rb +17 -3
- data/lib/ruby_llm/providers/azure/chat_completions.rb +1 -0
- data/lib/ruby_llm/providers/azure/decisions.rb +17 -0
- data/lib/ruby_llm/providers/azure/embeddings.rb +1 -2
- data/lib/ruby_llm/providers/azure/media.rb +6 -6
- data/lib/ruby_llm/providers/azure/mistral.rb +17 -0
- data/lib/ruby_llm/providers/azure/models.rb +16 -2
- data/lib/ruby_llm/providers/azure/moderation.rb +16 -0
- data/lib/ruby_llm/providers/azure/responses.rb +9 -0
- data/lib/ruby_llm/providers/azure/videos.rb +2 -2
- data/lib/ruby_llm/providers/azure.rb +43 -1
- data/lib/ruby_llm/providers/bedrock/anthropic/batches.rb +32 -0
- data/lib/ruby_llm/providers/bedrock/anthropic.rb +134 -0
- data/lib/ruby_llm/providers/bedrock/auth.rb +12 -3
- data/lib/ruby_llm/providers/bedrock/connection.rb +19 -0
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/bedrock/mantle/chat_completions.rb +2 -2
- data/lib/ruby_llm/providers/bedrock/mantle/responses.rb +12 -2
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle.rb +7 -0
- data/lib/ruby_llm/providers/bedrock/models.rb +1 -7
- data/lib/ruby_llm/providers/bedrock.rb +51 -8
- data/lib/ruby_llm/providers/cohere.rb +5 -1
- data/lib/ruby_llm/providers/deepgram/chat.rb +25 -0
- data/lib/ruby_llm/providers/deepgram.rb +21 -2
- data/lib/ruby_llm/providers/deepseek/chat.rb +7 -16
- data/lib/ruby_llm/providers/deepseek/responses.rb +25 -8
- data/lib/ruby_llm/providers/deepseek.rb +5 -1
- data/lib/ruby_llm/providers/elevenlabs.rb +2 -1
- data/lib/ruby_llm/providers/gemini.rb +13 -1
- data/lib/ruby_llm/providers/gpustack/chat.rb +23 -3
- data/lib/ruby_llm/providers/gpustack/embeddings.rb +2 -2
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -8
- data/lib/ruby_llm/providers/gpustack/transcription.rb +14 -6
- data/lib/ruby_llm/providers/gpustack.rb +1 -1
- data/lib/ruby_llm/providers/hetzner/chat.rb +23 -6
- data/lib/ruby_llm/providers/hetzner.rb +5 -1
- data/lib/ruby_llm/providers/meta/responses.rb +81 -0
- data/lib/ruby_llm/providers/meta.rb +37 -0
- data/lib/ruby_llm/providers/minimax/anthropic.rb +95 -0
- data/lib/ruby_llm/providers/minimax/chat_completions.rb +48 -0
- data/lib/ruby_llm/providers/minimax/responses.rb +52 -0
- data/lib/ruby_llm/providers/minimax.rb +104 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +34 -4
- data/lib/ruby_llm/providers/mistral/chat_completions/batches.rb +5 -14
- data/lib/ruby_llm/providers/mistral/embeddings.rb +1 -12
- data/lib/ruby_llm/providers/mistral/input_tokens.rb +35 -0
- data/lib/ruby_llm/providers/mistral/media.rb +8 -8
- data/lib/ruby_llm/providers/mistral/transcription.rb +8 -6
- data/lib/ruby_llm/providers/mistral.rb +13 -1
- data/lib/ruby_llm/providers/moonshot/capabilities.rb +16 -0
- data/lib/ruby_llm/providers/moonshot/chat.rb +100 -0
- data/lib/ruby_llm/providers/moonshot/documents.rb +79 -0
- data/lib/ruby_llm/providers/moonshot/responses.rb +55 -0
- data/lib/ruby_llm/providers/moonshot.rb +54 -0
- data/lib/ruby_llm/providers/ollama/chat.rb +5 -6
- data/lib/ruby_llm/providers/ollama/embeddings.rb +41 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -6
- data/lib/ruby_llm/providers/ollama/speech.rb +24 -0
- data/lib/ruby_llm/providers/ollama.rb +13 -1
- data/lib/ruby_llm/providers/ollama_cloud.rb +22 -1
- data/lib/ruby_llm/providers/openai/responses.rb +8 -0
- data/lib/ruby_llm/providers/openai.rb +7 -3
- data/lib/ruby_llm/providers/openrouter/chat.rb +15 -9
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +12 -8
- data/lib/ruby_llm/providers/openrouter/images.rb +16 -10
- data/lib/ruby_llm/providers/openrouter/judgments.rb +47 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -5
- data/lib/ruby_llm/providers/openrouter/models.rb +19 -7
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -4
- data/lib/ruby_llm/providers/openrouter/videos.rb +1 -1
- data/lib/ruby_llm/providers/openrouter.rb +32 -5
- data/lib/ruby_llm/providers/perplexity/chat.rb +3 -3
- data/lib/ruby_llm/providers/perplexity/embeddings.rb +1 -1
- data/lib/ruby_llm/providers/perplexity/media.rb +6 -6
- data/lib/ruby_llm/providers/perplexity.rb +13 -2
- data/lib/ruby_llm/providers/typesafe.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/anthropic.rb +5 -1
- data/lib/ruby_llm/providers/vertexai/credentials.rb +27 -3
- data/lib/ruby_llm/providers/vertexai/embed_content.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/embeddings.rb +2 -3
- data/lib/ruby_llm/providers/vertexai/gemini.rb +1 -1
- data/lib/ruby_llm/providers/vertexai/mistral.rb +5 -0
- data/lib/ruby_llm/providers/vertexai/models.rb +24 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/videos.rb +1 -1
- data/lib/ruby_llm/providers/vertexai.rb +56 -9
- data/lib/ruby_llm/providers/xai/chat.rb +3 -3
- data/lib/ruby_llm/providers/xai/chat_completions/batches.rb +8 -3
- data/lib/ruby_llm/providers/xai/images.rb +9 -4
- data/lib/ruby_llm/providers/xai/models.rb +2 -2
- data/lib/ruby_llm/providers/xai/responses.rb +10 -0
- data/lib/ruby_llm/providers/xai/speech.rb +2 -2
- data/lib/ruby_llm/providers/xai/transcription.rb +14 -9
- data/lib/ruby_llm/providers/xai/videos.rb +1 -1
- data/lib/ruby_llm/providers/xai.rb +9 -1
- data/lib/ruby_llm/providers/zai/chat.rb +117 -0
- data/lib/ruby_llm/providers/zai/ocr.rb +65 -0
- data/lib/ruby_llm/providers/zai/token_counting.rb +25 -0
- data/lib/ruby_llm/providers/zai/videos.rb +67 -0
- data/lib/ruby_llm/providers/zai.rb +66 -0
- data/lib/ruby_llm/question.rb +390 -0
- data/lib/ruby_llm/queued_message.rb +74 -0
- data/lib/ruby_llm/railtie.rb +43 -0
- data/lib/ruby_llm/request_shape/part.rb +2 -2
- data/lib/ruby_llm/request_shape/problem.rb +2 -2
- data/lib/ruby_llm/request_shape/tool_round.rb +2 -2
- data/lib/ruby_llm/request_shape/turn.rb +2 -2
- data/lib/ruby_llm/request_shape.rb +1 -1
- data/lib/ruby_llm/rerank.rb +10 -37
- data/lib/ruby_llm/research_job.rb +26 -13
- data/lib/ruby_llm/sandbox/landlock.rb +78 -0
- data/lib/ruby_llm/sandbox/mxc.rb +137 -0
- data/lib/ruby_llm/sandbox.rb +141 -0
- data/lib/ruby_llm/search_results.rb +9 -4
- data/lib/ruby_llm/server_tool_call.rb +1 -1
- data/lib/ruby_llm/skill/loader.rb +131 -0
- data/lib/ruby_llm/skill.rb +263 -0
- data/lib/ruby_llm/speech.rb +25 -53
- data/lib/ruby_llm/stored_file.rb +94 -0
- data/lib/ruby_llm/support/cancellation.rb +4 -4
- data/lib/ruby_llm/support/deprecated_enumerable.rb +13 -0
- data/lib/ruby_llm/support/deprecated_job_predicates.rb +24 -0
- data/lib/ruby_llm/support/inspectable.rb +1 -1
- data/lib/ruby_llm/support/instrumentation.rb +6 -5
- data/lib/ruby_llm/support/rescuable.rb +84 -0
- data/lib/ruby_llm/support/utils.rb +26 -3
- data/lib/ruby_llm/task.rb +126 -0
- data/lib/ruby_llm/test_helper.rb +129 -0
- data/lib/ruby_llm/thinking.rb +3 -3
- data/lib/ruby_llm/tokens.rb +52 -12
- data/lib/ruby_llm/tool.rb +339 -5
- data/lib/ruby_llm/tool_call/collection.rb +132 -0
- data/lib/ruby_llm/tool_call.rb +68 -6
- data/lib/ruby_llm/tools/provider_tools.rb +23 -0
- data/lib/ruby_llm/tools/search.rb +118 -0
- data/lib/ruby_llm/transcription/segment.rb +67 -0
- data/lib/ruby_llm/transcription/wav_audio.rb +2 -1
- data/lib/ruby_llm/transcription.rb +25 -49
- data/lib/ruby_llm/transcription_chunk.rb +9 -9
- data/lib/ruby_llm/transport/connection.rb +146 -33
- data/lib/ruby_llm/transport/error_middleware.rb +94 -49
- data/lib/ruby_llm/transport/event_stream_parser.rb +18 -4
- data/lib/ruby_llm/transport/json_response.rb +9 -3
- data/lib/ruby_llm/transport/log_formatter.rb +20 -0
- data/lib/ruby_llm/transport/release_request.rb +35 -0
- data/lib/ruby_llm/transport/websocket_connection.rb +26 -30
- data/lib/ruby_llm/uploaded_file.rb +15 -6
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +34 -10
- data/lib/ruby_llm/video_job.rb +55 -42
- data/lib/ruby_llm/web_source.rb +38 -0
- data/lib/ruby_llm/workflow.rb +2 -2
- data/lib/ruby_llm.rb +63 -25
- data/lib/tasks/ruby_llm/evaluations.rb +1 -1
- data/skills/rubyllm/SKILL.md +128 -54
- data/skills/rubyllm/references/cost-and-tracing.md +8 -0
- data/skills/rubyllm/references/files-code-and-context.md +6 -0
- data/skills/rubyllm/references/operations.md +8 -0
- data/skills/rubyllm/references/rails-chats.md +22 -0
- data/skills/rubyllm/references/tools-and-agents.md +11 -0
- metadata +213 -14
- data/lib/generators/ruby_llm/chat_ui/templates/helpers/messages_helper.rb.tt +0 -25
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +0 -78
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
require 'monitor'
|
|
5
|
+
require 'stringio'
|
|
6
|
+
|
|
7
|
+
module RubyLLM
|
|
8
|
+
# A FileStore keeps a chat's files: what people attach, what tools
|
|
9
|
+
# return, long tool results, and what code writes. Chat#with_file_store gives
|
|
10
|
+
# a chat one, and the model works with it through tools that list, read,
|
|
11
|
+
# search, write, and edit its files, so a file reaches the model only
|
|
12
|
+
# when the model asks for it.
|
|
13
|
+
#
|
|
14
|
+
# chat = RubyLLM.chat.with_file_store
|
|
15
|
+
# chat.ask "Which clauses changed?", with: ["contract-v1.pdf", "contract-v2.pdf"]
|
|
16
|
+
# chat.file_store.list.map(&:path) # => ["contract-v1.pdf", "contract-v2.pdf"]
|
|
17
|
+
#
|
|
18
|
+
# RubyLLM ships FileStore::Memory, the default, and FileStore::Directory,
|
|
19
|
+
# which keeps the files in a folder. The Rails integration keeps a
|
|
20
|
+
# persisted chat's files in Active Storage.
|
|
21
|
+
#
|
|
22
|
+
# chat.with_file_store("tmp/work")
|
|
23
|
+
#
|
|
24
|
+
# Write your own store by subclassing FileStore and implementing #list,
|
|
25
|
+
# #[], #read, #write, and #delete. RubyLLM only ever passes them paths
|
|
26
|
+
# FileStore.path accepts: relative, with forward slashes, and without
|
|
27
|
+
# empty, <tt>.</tt>, or <tt>..</tt> parts.
|
|
28
|
+
#
|
|
29
|
+
# class BucketStore < RubyLLM::FileStore
|
|
30
|
+
# def initialize(bucket, prefix)
|
|
31
|
+
# super()
|
|
32
|
+
# @bucket = bucket
|
|
33
|
+
# @prefix = prefix
|
|
34
|
+
# end
|
|
35
|
+
#
|
|
36
|
+
# def list(prefix = nil) = ...
|
|
37
|
+
# def [](path) = ...
|
|
38
|
+
# def read(path) = ...
|
|
39
|
+
# def write(path, content, content_type: nil) = ...
|
|
40
|
+
# def delete(path) = ...
|
|
41
|
+
# end
|
|
42
|
+
#
|
|
43
|
+
# The model must read a file before its tools change it, and a file that
|
|
44
|
+
# changed since it read it must be read again. The store remembers what
|
|
45
|
+
# the model read, and #with_lock keeps two changes to the files from
|
|
46
|
+
# interleaving. Override #with_lock in a store that several processes
|
|
47
|
+
# share.
|
|
48
|
+
class FileStore
|
|
49
|
+
include Enumerable
|
|
50
|
+
|
|
51
|
+
# Raised by #delete for a path that is a folder.
|
|
52
|
+
class FolderError < ArgumentError; end # :nodoc:
|
|
53
|
+
|
|
54
|
+
# Converted texts each store keeps in memory, by default.
|
|
55
|
+
CACHED_TEXTS = 100
|
|
56
|
+
|
|
57
|
+
UNSAFE_CHARACTERS = /[\x00-\x1f\x7f\\:*?"<>|]/
|
|
58
|
+
DIGEST_CHUNK = 1024 * 1024
|
|
59
|
+
private_constant :UNSAFE_CHARACTERS, :DIGEST_CHUNK
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
# Returns +name+ as a path inside a store: relative, with forward
|
|
63
|
+
# slashes, and with a leading <tt>./</tt> or <tt>/</tt> and repeated
|
|
64
|
+
# slashes removed.
|
|
65
|
+
#
|
|
66
|
+
# RubyLLM::FileStore.path("./reports//q3.pdf") # => "reports/q3.pdf"
|
|
67
|
+
#
|
|
68
|
+
# Raises ArgumentError for an empty path or one with <tt>.</tt> or
|
|
69
|
+
# <tt>..</tt> parts.
|
|
70
|
+
def path(name)
|
|
71
|
+
parts = name.to_s.tr('\\', '/').split('/').reject(&:empty?)
|
|
72
|
+
parts.shift while parts.first == '.'
|
|
73
|
+
if parts.empty? || parts.intersect?(%w[. ..]) || parts.any? { |part| part.match?(/[\x00-\x1f]/) }
|
|
74
|
+
raise ArgumentError, "#{name.inspect} is not a path inside the chat's files"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
parts.join('/')
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns whether +path+ is inside the folder +prefix+, or is it.
|
|
81
|
+
# Every path is inside a +nil+ or empty prefix.
|
|
82
|
+
def inside?(path, prefix) # :nodoc:
|
|
83
|
+
return true if prefix.to_s.empty?
|
|
84
|
+
|
|
85
|
+
folder = path(prefix)
|
|
86
|
+
path == folder || path.start_with?("#{folder}/")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns whether +path+ matches the glob +pattern+. A pattern
|
|
90
|
+
# without a slash matches the file name in any folder; one with a
|
|
91
|
+
# slash matches the whole path, or the path inside the folder
|
|
92
|
+
# +inside+. <tt>**</tt> matches any number of folders, a trailing
|
|
93
|
+
# <tt>/**</tt> everything under a folder, and <tt>{a,b}</tt> either.
|
|
94
|
+
def match?(path, pattern, inside: nil) # :nodoc:
|
|
95
|
+
pattern = pattern.to_s.delete_prefix('./').delete_prefix('/')
|
|
96
|
+
flags = File::FNM_PATHNAME | File::FNM_EXTGLOB
|
|
97
|
+
return File.fnmatch?(pattern, File.basename(path), flags) unless pattern.include?('/')
|
|
98
|
+
|
|
99
|
+
pattern = "#{pattern}/*" if pattern.end_with?('/**')
|
|
100
|
+
candidates = [path]
|
|
101
|
+
candidates << path.delete_prefix("#{inside}/") unless inside.to_s.empty?
|
|
102
|
+
candidates.any? { |candidate| File.fnmatch?(pattern, candidate, flags) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns a safe file name for an attachment called +name+.
|
|
106
|
+
def filename(name) # :nodoc:
|
|
107
|
+
base = File.basename(name.to_s.tr('\\', '/')).gsub(UNSAFE_CHARACTERS, '_').strip.sub(/\A\.+/, '')
|
|
108
|
+
base.empty? ? 'file' : base
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns the SHA-256 of +content+, a String or an IO read to its
|
|
112
|
+
# end in chunks, in hex.
|
|
113
|
+
def digest(content) # :nodoc:
|
|
114
|
+
return Digest::SHA256.hexdigest(content) unless content.respond_to?(:read)
|
|
115
|
+
|
|
116
|
+
digest = Digest::SHA256.new
|
|
117
|
+
buffer = String.new(capacity: DIGEST_CHUNK)
|
|
118
|
+
digest << buffer while content.read(DIGEST_CHUNK, buffer)
|
|
119
|
+
digest.hexdigest
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Returns the MIME type of +content+, a String or an IO that can
|
|
123
|
+
# rewind, written to +path+. An IO is sniffed from its start and
|
|
124
|
+
# left there.
|
|
125
|
+
def content_type_for(path, content) # :nodoc:
|
|
126
|
+
type = Files::MimeType.for(name: File.basename(path))
|
|
127
|
+
return type unless type == 'application/octet-stream'
|
|
128
|
+
|
|
129
|
+
Files::MimeType.for(content.respond_to?(:read) ? content : StringIO.new(content))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns the bytes of +content+, a String or an IO.
|
|
133
|
+
def bytes(content) # :nodoc:
|
|
134
|
+
content = content.read if content.respond_to?(:read)
|
|
135
|
+
content.to_s.b
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def initialize # :nodoc:
|
|
140
|
+
@texts = {}
|
|
141
|
+
@texts_lock = Mutex.new
|
|
142
|
+
@reads = {}
|
|
143
|
+
@reads_lock = Mutex.new
|
|
144
|
+
@store_lock = Monitor.new
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns the files inside the folder +prefix+, or every file, as
|
|
148
|
+
# StoredFile objects sorted by path.
|
|
149
|
+
#
|
|
150
|
+
# chat.file_store.list("reports")
|
|
151
|
+
def list(prefix = nil)
|
|
152
|
+
raise NotImplementedError, "#{self.class} must implement list"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Returns the StoredFile at +path+, or +nil+ when there is none.
|
|
156
|
+
#
|
|
157
|
+
# chat.file_store["notes/plan.md"] # => #<RubyLLM::StoredFile path: "notes/plan.md", ...>
|
|
158
|
+
def [](path)
|
|
159
|
+
raise NotImplementedError, "#{self.class} must implement []"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Returns the StoredFile at +path+. Raises FileNotFoundError when there
|
|
163
|
+
# is none.
|
|
164
|
+
#
|
|
165
|
+
# chat.file_store.find("notes/plan.md")
|
|
166
|
+
def find(path)
|
|
167
|
+
self[path] || raise(FileNotFoundError, "No file at #{FileStore.path(path)}")
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Returns the content of the file at +path+ as a binary String.
|
|
171
|
+
# Raises FileNotFoundError when there is none.
|
|
172
|
+
def read(path)
|
|
173
|
+
raise NotImplementedError, "#{self.class} must implement read"
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Writes +content+, a String or an IO, to +path+, replacing any file
|
|
177
|
+
# there, and returns its StoredFile. The content type is detected from
|
|
178
|
+
# the path and the content unless given.
|
|
179
|
+
#
|
|
180
|
+
# chat.file_store.write("notes/plan.md", "# Plan")
|
|
181
|
+
def write(path, content, content_type: nil)
|
|
182
|
+
raise NotImplementedError, "#{self.class} must implement write"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Deletes the file at +path+ and returns its StoredFile, or +nil+ when
|
|
186
|
+
# there was none. Raises ArgumentError when +path+ is a folder.
|
|
187
|
+
#
|
|
188
|
+
# chat.file_store.delete("notes/plan.md")
|
|
189
|
+
def delete(path)
|
|
190
|
+
raise NotImplementedError, "#{self.class} must implement delete"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Deletes everything inside the folder +path+ and returns the deleted
|
|
194
|
+
# StoredFile objects, sorted by path. Raises ArgumentError when +path+
|
|
195
|
+
# is a file.
|
|
196
|
+
#
|
|
197
|
+
# chat.file_store.delete_folder("reports") # => [#<RubyLLM::StoredFile path: "reports/q3.pdf", ...>]
|
|
198
|
+
def delete_folder(path)
|
|
199
|
+
path = FileStore.path(path)
|
|
200
|
+
refuse_file(path) if self[path]
|
|
201
|
+
|
|
202
|
+
list(path).filter_map { |file| delete(file.path) }
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Returns the folder on disk that holds the files, or +nil+. A store
|
|
206
|
+
# with one has code execution run in that folder, reading and writing
|
|
207
|
+
# the files in place; a store without one has each run read the
|
|
208
|
+
# attachments in +attachments/+ and copy in the files it names.
|
|
209
|
+
# FileStore::Directory returns its root.
|
|
210
|
+
def local_root # :nodoc:
|
|
211
|
+
nil
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Yields each StoredFile, as #list returns them.
|
|
215
|
+
def each(&)
|
|
216
|
+
list.each(&)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Returns whether there is a file at +path+.
|
|
220
|
+
def exist?(path)
|
|
221
|
+
!self[path].nil?
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Returns the digest of the content the model last read at +path+, or
|
|
225
|
+
# +nil+ when it has not read it. The file tools compare it with the
|
|
226
|
+
# file's digest before they change the file.
|
|
227
|
+
def read_digest(path) # :nodoc:
|
|
228
|
+
@reads_lock.synchronize { @reads[FileStore.path(path)] }
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Remembers that the model read +digest+, a digest of the content, at
|
|
232
|
+
# +path+, in memory.
|
|
233
|
+
def record_read(path, digest) # :nodoc:
|
|
234
|
+
@reads_lock.synchronize { @reads[FileStore.path(path)] = digest }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Runs the block with the store locked and returns its value, so a tool
|
|
238
|
+
# that reads a file and writes it back sees no other change in
|
|
239
|
+
# between. It is reentrant. Stores lock within this process unless
|
|
240
|
+
# they override it.
|
|
241
|
+
def with_lock(&)
|
|
242
|
+
@store_lock.synchronize(&)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Returns the text of the file at +path+: its content when it is text,
|
|
246
|
+
# or what a converter makes of it, such as the text OCR reads from a
|
|
247
|
+
# PDF. Returns +nil+ when no converter can read the file.
|
|
248
|
+
# Conversions are kept, so each version of a file is converted once.
|
|
249
|
+
# Raises FileNotFoundError when there is no file at +path+.
|
|
250
|
+
#
|
|
251
|
+
# chat.file_store.text("contract.pdf") # => "## Page 1\n\nThis agreement..."
|
|
252
|
+
#
|
|
253
|
+
# See Configuration#file_converters for adding converters.
|
|
254
|
+
def text(path, config: nil)
|
|
255
|
+
file = find(path)
|
|
256
|
+
cached = cached_text(file)
|
|
257
|
+
return cached if cached
|
|
258
|
+
|
|
259
|
+
Converters.convert(attachment(file.path, file:), config: config || RubyLLM.config).tap do |text|
|
|
260
|
+
cache_text(file, text) if text
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Returns the file at +path+ as an Attachment, to send it to a model.
|
|
265
|
+
# Raises FileNotFoundError when there is none.
|
|
266
|
+
def attachment(path, file: nil)
|
|
267
|
+
file ||= find(path)
|
|
268
|
+
Attachment.new(StringIO.new(read(file.path)), filename: file.filename)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Returns the converted text kept for +file+, or +nil+, from memory.
|
|
272
|
+
def cached_text(file) # :nodoc:
|
|
273
|
+
@texts_lock.synchronize { @texts[text_key(file)] }
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Keeps +text+ as the conversion of +file+, in memory.
|
|
277
|
+
def cache_text(file, text) # :nodoc:
|
|
278
|
+
@texts_lock.synchronize do
|
|
279
|
+
@texts.delete(@texts.keys.first) if @texts.size >= CACHED_TEXTS
|
|
280
|
+
@texts[text_key(file)] = text
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# Writes +attachment+ under its file name and returns its StoredFile,
|
|
285
|
+
# yielding it when it is new. A file already there with the same
|
|
286
|
+
# content is reused; one with other content keeps its place, and the
|
|
287
|
+
# attachment gets a numbered name beside it, such as
|
|
288
|
+
# <tt>report-2.pdf</tt>.
|
|
289
|
+
def store_attachment(attachment) # :nodoc:
|
|
290
|
+
content = attachment.content.to_s.b
|
|
291
|
+
digest = self.class.digest(content)
|
|
292
|
+
candidates(self.class.filename(attachment.filename)).each do |path|
|
|
293
|
+
existing = self[path]
|
|
294
|
+
return existing if existing && same_content?(existing, content, digest)
|
|
295
|
+
next if existing || folder?(path)
|
|
296
|
+
|
|
297
|
+
file = write_attachment(path, attachment, content)
|
|
298
|
+
yield file if block_given?
|
|
299
|
+
return file
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
private
|
|
304
|
+
|
|
305
|
+
def folder?(_path)
|
|
306
|
+
false
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def refuse_folder(path)
|
|
310
|
+
raise FolderError, "#{path} is a folder: delete it with delete_folder"
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def refuse_file(path)
|
|
314
|
+
raise ArgumentError, "#{path} is a file: delete it with delete"
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def write_attachment(path, attachment, content)
|
|
318
|
+
write(path, content, content_type: attachment.mime_type)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def candidates(name)
|
|
322
|
+
extension = File.extname(name)
|
|
323
|
+
base = name.delete_suffix(extension)
|
|
324
|
+
Enumerator.new do |paths|
|
|
325
|
+
paths << name
|
|
326
|
+
(2..).each { |number| paths << "#{base}-#{number}#{extension}" }
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def same_content?(file, content, digest)
|
|
331
|
+
file.digest ? file.digest == digest : read(file.path).b == content
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def text_key(file)
|
|
335
|
+
file.digest || [file.path, file.byte_size, file.updated_at]
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Files
|
|
5
|
+
# The #save every complete downloadable result shares. Includers supply
|
|
6
|
+
# #to_blob and document #save in their own terms.
|
|
7
|
+
module Saving # :nodoc:
|
|
8
|
+
def save(path)
|
|
9
|
+
File.binwrite(File.expand_path(path), to_blob)
|
|
10
|
+
path
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -11,7 +11,9 @@ module RubyLLM
|
|
|
11
11
|
#
|
|
12
12
|
class Image
|
|
13
13
|
include Support::Inspectable
|
|
14
|
+
include Files::Saving
|
|
14
15
|
include Accounting::Usage::Result
|
|
16
|
+
include Accounting::Usage::ModelInfo
|
|
15
17
|
|
|
16
18
|
# The URL of the hosted image, for providers that return one, or +nil+.
|
|
17
19
|
attr_reader :url
|
|
@@ -44,7 +46,7 @@ module RubyLLM
|
|
|
44
46
|
# +context:+ supplies a Context whose configuration replaces the
|
|
45
47
|
# global one. +metadata:+ is included in the instrumentation payload.
|
|
46
48
|
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
47
|
-
# over RubyLLM.
|
|
49
|
+
# over RubyLLM.with_owner.
|
|
48
50
|
#
|
|
49
51
|
# image = RubyLLM.paint("A small watercolor robot", model: "gpt-image-2")
|
|
50
52
|
#
|
|
@@ -57,8 +59,19 @@ module RubyLLM
|
|
|
57
59
|
# with: "logo.png"
|
|
58
60
|
# )
|
|
59
61
|
#
|
|
60
|
-
# Providers that cannot generate several images in one request
|
|
61
|
-
# +count:+
|
|
62
|
+
# Providers that cannot generate several images in one request raise
|
|
63
|
+
# ArgumentError when +count:+ is above 1.
|
|
64
|
+
#
|
|
65
|
+
# +fallbacks:+ lists models to try, in order, when the request fails
|
|
66
|
+
# with one of the errors Chat#with_fallbacks moves on from by default,
|
|
67
|
+
# such as a rate limit or an overloaded provider. Each is a model id or
|
|
68
|
+
# a Model, and gets the same request. A fallback whose model can't be
|
|
69
|
+
# found is skipped. Each attempt has its own +image.ruby_llm+ event
|
|
70
|
+
# and usage entry.
|
|
71
|
+
#
|
|
72
|
+
# RubyLLM.paint("A small watercolor robot", model: "gpt-image-2",
|
|
73
|
+
# fallbacks: ["gemini-3.1-flash-image"])
|
|
74
|
+
#
|
|
62
75
|
def self.paint(prompt,
|
|
63
76
|
model: nil,
|
|
64
77
|
provider: nil,
|
|
@@ -70,39 +83,54 @@ module RubyLLM
|
|
|
70
83
|
mask: nil,
|
|
71
84
|
provider_options: {},
|
|
72
85
|
metadata: nil,
|
|
73
|
-
owner: nil
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
size: size,
|
|
86
|
-
count: count,
|
|
87
|
-
provider_options: provider_options,
|
|
88
|
-
metadata: metadata,
|
|
89
|
-
tokens: empty_tokens,
|
|
90
|
-
cost: Cost.new(tokens: empty_tokens, model:, category: :images)
|
|
91
|
-
}
|
|
86
|
+
owner: nil,
|
|
87
|
+
fallbacks: [])
|
|
88
|
+
queue = Array(fallbacks).flatten.compact.map { |fallback| Fallback.build(fallback) }
|
|
89
|
+
operation = painting(model, provider:, assume_model_exists:, context:, owner:)
|
|
90
|
+
begin
|
|
91
|
+
with_fallback_ready(queue) do
|
|
92
|
+
operation.run(prompt:, size:, count:, provider_options:, metadata:) do |provider_instance, image_model|
|
|
93
|
+
provider_instance.paint(prompt, model: image_model, size:, count:, with:, mask:, provider_options:)
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
raise unless Fallback.error?(e, Fallback::DEFAULT_ERRORS)
|
|
92
98
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
provider_instance.paint(prompt, model:, size:, count:, with:, mask:, provider_options:)
|
|
99
|
+
_, operation = Fallback.next_usable(queue) do |fallback|
|
|
100
|
+
painting(fallback.id, provider: fallback.provider, context:, owner:, resolved: fallback.model)
|
|
96
101
|
end
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
event[:tokens] = Tokens.aggregate(images.map(&:tokens))
|
|
101
|
-
event[:cost] = Cost.aggregate(images.map(&:cost))
|
|
102
|
-
result
|
|
102
|
+
raise e unless operation
|
|
103
|
+
|
|
104
|
+
retry
|
|
103
105
|
end
|
|
104
106
|
end
|
|
105
107
|
|
|
108
|
+
# While a fallback is left, a timeout, a failed connection, or a long
|
|
109
|
+
# retry wait moves on to it instead of retrying the same model, as
|
|
110
|
+
# Chat#with_fallbacks does. Every error the transport retries is one
|
|
111
|
+
# paint falls back on, so the fallback takes each of them.
|
|
112
|
+
def self.with_fallback_ready(queue, &)
|
|
113
|
+
return yield if queue.empty?
|
|
114
|
+
|
|
115
|
+
Transport::Connection.with_fallback_ready(->(_error) { true }, &)
|
|
116
|
+
end
|
|
117
|
+
private_class_method :with_fallback_ready
|
|
118
|
+
|
|
119
|
+
def self.painting(model, provider:, context:, owner:, assume_model_exists: false, resolved: nil) # :nodoc:
|
|
120
|
+
Accounting::Operation.new(
|
|
121
|
+
'image', model, default: :default_image_model, category: :images, provider:, assume_model_exists:, context:,
|
|
122
|
+
owner:, resolved:,
|
|
123
|
+
report: lambda { |event, result|
|
|
124
|
+
images = Support::Utils.to_safe_array(result)
|
|
125
|
+
event[:result] = result
|
|
126
|
+
event[:response_model] = images.first&.model
|
|
127
|
+
event[:tokens] = Tokens.aggregate(images.map(&:tokens))
|
|
128
|
+
event[:cost] = Cost.aggregate(images.map(&:cost))
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
private_class_method :painting
|
|
133
|
+
|
|
106
134
|
# :stopdoc:
|
|
107
135
|
|
|
108
136
|
# Set by the protocol that generated the image, so a Context's
|
|
@@ -137,20 +165,19 @@ module RubyLLM
|
|
|
137
165
|
if base64?
|
|
138
166
|
Base64.decode64 @data
|
|
139
167
|
else
|
|
140
|
-
|
|
141
|
-
response.body
|
|
168
|
+
Transport::Connection.download(@url, config)
|
|
142
169
|
end
|
|
143
170
|
end
|
|
144
171
|
|
|
172
|
+
##
|
|
173
|
+
# :method: save
|
|
174
|
+
# :call-seq:
|
|
175
|
+
# save(path)
|
|
176
|
+
#
|
|
145
177
|
# Writes the binary image to +path+, expanding it first. Returns
|
|
146
178
|
# +path+ as given.
|
|
147
179
|
#
|
|
148
180
|
# image.save("steampunk_owl.png")
|
|
149
|
-
#
|
|
150
|
-
def save(path)
|
|
151
|
-
File.binwrite(File.expand_path(path), to_blob)
|
|
152
|
-
path
|
|
153
|
-
end
|
|
154
181
|
|
|
155
182
|
# Returns a Tokens with usage across every provider attempt.
|
|
156
183
|
# Its fields are +nil+ when none were reported.
|
|
@@ -164,6 +191,7 @@ module RubyLLM
|
|
|
164
191
|
@tokens ||= Tokens.new(
|
|
165
192
|
input: raw_usage['input_tokens'],
|
|
166
193
|
output: raw_usage['output_tokens'],
|
|
194
|
+
cache_read: raw_usage['cache_read_tokens'],
|
|
167
195
|
reported_cost: raw_usage['cost']
|
|
168
196
|
)
|
|
169
197
|
end
|
|
@@ -179,17 +207,11 @@ module RubyLLM
|
|
|
179
207
|
Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
|
|
180
208
|
end
|
|
181
209
|
|
|
182
|
-
attr_writer :model_info # :nodoc:
|
|
183
|
-
|
|
184
210
|
# Returns the image's Model from its provider's registry, falling back
|
|
185
211
|
# to the requested model when the response ID is unknown. Images built
|
|
186
212
|
# without a request look up #model, or return +nil+ if unknown.
|
|
187
213
|
def model_info
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
@model_info ||= RubyLLM.models.find(model)
|
|
191
|
-
rescue ModelNotFoundError
|
|
192
|
-
nil
|
|
214
|
+
super if model
|
|
193
215
|
end
|
|
194
216
|
|
|
195
217
|
private
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Raised when a tool asks a person something and nothing can pause the
|
|
5
|
+
# call for the answer: Tool#ask outside a chat, or an MCP server's
|
|
6
|
+
# question that no MCP.before_input_request callback answered when you
|
|
7
|
+
# call the tool yourself (MCP::InputRequiredError). Its message includes
|
|
8
|
+
# what was asked, and #requests has the Question objects. In a chat, the
|
|
9
|
+
# tool call pauses instead, and Chat#pending_questions returns them.
|
|
10
|
+
class InputRequiredError < Error
|
|
11
|
+
def initialize(source, input) # :nodoc:
|
|
12
|
+
@input = input
|
|
13
|
+
asks = requests.map { |question| [question.message, question.url].compact.join(' ') }
|
|
14
|
+
super("#{source} needs input from the user: #{asks.join('; ')}")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The unanswered Question objects.
|
|
18
|
+
def requests
|
|
19
|
+
@input['requests'].reject(&:answered?)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns everything needed to answer the questions later and resume
|
|
23
|
+
# the call, as a Hash that serializes to JSON.
|
|
24
|
+
def to_h # :nodoc:
|
|
25
|
+
@input.merge('requests' => @input['requests'].map(&:to_h)).compact
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module RubyLLM
|
|
4
4
|
class Judge
|
|
5
|
-
class Builder # :nodoc:
|
|
5
|
+
class Builder < BasicObject # :nodoc:
|
|
6
6
|
def self.resolve(value, scope:)
|
|
7
7
|
return value unless value.is_a?(::Proc)
|
|
8
8
|
|
|
@@ -43,6 +43,11 @@ module RubyLLM
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
+
def proc(&block)
|
|
47
|
+
block
|
|
48
|
+
end
|
|
49
|
+
alias lambda proc
|
|
50
|
+
|
|
46
51
|
def runtime_input?(name, values, block)
|
|
47
52
|
values.empty? && !block && @scope.respond_to?(name)
|
|
48
53
|
end
|
data/lib/ruby_llm/judge.rb
CHANGED
|
@@ -136,7 +136,7 @@ module RubyLLM
|
|
|
136
136
|
# Judges the supplied input and returns a Judgment. Accepts model and
|
|
137
137
|
# provider overrides, images as +with:+, an isolated +context:+,
|
|
138
138
|
# +provider_options:+, instrumentation +metadata:+, and a usage +owner:+,
|
|
139
|
-
# as RubyLLM.
|
|
139
|
+
# as RubyLLM.with_owner describes. Additional
|
|
140
140
|
# +questions:+ are a Hash keyed by question name, with +type:+,
|
|
141
141
|
# +instructions:+, and +criteria:+ (probability), +options:+ (choice), or
|
|
142
142
|
# +levels:+ (score). The block supplies input only. Input may be omitted
|
data/lib/ruby_llm/judgment.rb
CHANGED
|
@@ -76,30 +76,12 @@ module RubyLLM
|
|
|
76
76
|
validate_input!(input, attachments)
|
|
77
77
|
raise ArgumentError, 'A judgment requires a model' unless model || config.default_judgment_model
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
model: model.id,
|
|
86
|
-
model_info: model,
|
|
87
|
-
question_count: questions.size,
|
|
88
|
-
attachment_count: attachments.size,
|
|
89
|
-
provider_options:,
|
|
90
|
-
metadata:,
|
|
91
|
-
tokens: empty_tokens,
|
|
92
|
-
cost: Cost.new(tokens: empty_tokens, model:)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
RubyLLM.instrument('judgment.ruby_llm', payload, config:) do |event|
|
|
96
|
-
result = Accounting::Usage.owned_by(owner) do
|
|
97
|
-
provider_instance.judge(input, questions:, model:, with: attachments, provider_options:)
|
|
98
|
-
end
|
|
99
|
-
event[:result] = result
|
|
100
|
-
event[:tokens] = result.tokens
|
|
101
|
-
event[:cost] = result.cost
|
|
102
|
-
result
|
|
79
|
+
Accounting::Operation.new(
|
|
80
|
+
'judgment', model, operation: :judge, default: :default_judgment_model, provider:, assume_model_exists:,
|
|
81
|
+
context:, owner:
|
|
82
|
+
).run(question_count: questions.size, attachment_count: attachments.size, provider_options:,
|
|
83
|
+
metadata:) do |provider_instance, model|
|
|
84
|
+
provider_instance.judge(input, questions:, model:, with: attachments, provider_options:)
|
|
103
85
|
end
|
|
104
86
|
end
|
|
105
87
|
|