ruby_llm 2.1.0 → 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 +162 -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 +522 -277
- 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 +1598 -332
- 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 +67 -28
- 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 +134 -109
- 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 +23 -2
- 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 +2 -9
- 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 +8 -3
- 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 +219 -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,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Skill
|
|
5
|
+
# The +load_skill+ tool a chat with skills gives the model. It returns a
|
|
6
|
+
# skill's instructions, or one of the files the instructions mention,
|
|
7
|
+
# and never leaves the skill's folder. Every request also lists the
|
|
8
|
+
# skills, through #listing, so the model knows what it can load.
|
|
9
|
+
class Loader < Tool # :nodoc:
|
|
10
|
+
MAX_FILE_SIZE = 256 * 1024
|
|
11
|
+
MOUNT = 'skills'
|
|
12
|
+
|
|
13
|
+
def self.tool_name
|
|
14
|
+
'load_skill'
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.mount_path(skill)
|
|
18
|
+
"#{MOUNT}/#{skill.name}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.resource_name(skill, file)
|
|
22
|
+
file = file.to_s
|
|
23
|
+
return if file.strip.empty? || file == FILE
|
|
24
|
+
|
|
25
|
+
file.delete_prefix("#{MOUNT}/#{skill}/")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(skills:, code_execution:)
|
|
29
|
+
super()
|
|
30
|
+
@skills = skills
|
|
31
|
+
@code_execution = code_execution
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def description
|
|
35
|
+
"Loads a skill's instructions, or a file inside a skill's folder that its instructions mention. " \
|
|
36
|
+
'Call it with only skill to get the instructions before following a skill, ' \
|
|
37
|
+
'and with file, a path relative to the skill folder, to read one of its files.'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def parameters_schema
|
|
41
|
+
{
|
|
42
|
+
'type' => 'object',
|
|
43
|
+
'properties' => {
|
|
44
|
+
'skill' => { 'type' => 'string', 'description' => 'The name of the skill' },
|
|
45
|
+
'file' => { 'type' => 'string',
|
|
46
|
+
'description' => 'A file path relative to the skill folder. Leave it out for the instructions' }
|
|
47
|
+
},
|
|
48
|
+
'required' => %w[skill],
|
|
49
|
+
'additionalProperties' => false
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def skill?(name)
|
|
54
|
+
@skills.call.any? { |skill| skill.name == name }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def listing
|
|
58
|
+
[
|
|
59
|
+
'You have skills: folders of instructions, scripts, and files for specific tasks. ' \
|
|
60
|
+
'When a task matches a skill, call load_skill with its name and follow the instructions it ' \
|
|
61
|
+
'returns. Load the files the instructions mention with load_skill as well, one at a time.',
|
|
62
|
+
scripts_note,
|
|
63
|
+
"<skills>\n#{@skills.call.map { |skill| "- #{skill.name}: #{skill.description}" }.join("\n")}\n</skills>"
|
|
64
|
+
].join("\n\n")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def execute(skill:, file: nil)
|
|
68
|
+
found = @skills.call.find { |candidate| candidate.name == skill.to_s }
|
|
69
|
+
return { error: "No skill named #{skill}. Skills: #{@skills.call.map(&:name).join(', ')}" } unless found
|
|
70
|
+
|
|
71
|
+
resource = self.class.resource_name(found.name, file)
|
|
72
|
+
resource ? read(found, resource) : instructions(found)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def instructions(skill)
|
|
78
|
+
folder = if @code_execution.call
|
|
79
|
+
mount = self.class.mount_path(skill)
|
|
80
|
+
"Its folder is at #{mount}/ in run_code's working directory, read-only. Paths in these " \
|
|
81
|
+
"instructions are relative to it, so prefix them with #{mount}/. Run its scripts with " \
|
|
82
|
+
'run_code from the working directory, and write output files to the working directory.'
|
|
83
|
+
else
|
|
84
|
+
'Paths in these instructions are relative to the skill folder. This conversation cannot run ' \
|
|
85
|
+
"code, so the skill's scripts cannot run: follow the rest of the instructions, and say so " \
|
|
86
|
+
'when a step needs a script.'
|
|
87
|
+
end
|
|
88
|
+
"# Skill: #{skill.name}\n\n#{folder}\n\n#{skill.instructions.strip}"
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def read(skill, file)
|
|
92
|
+
path = skill.resolve_file(file)
|
|
93
|
+
return { error: "#{file} is a folder holding: #{folder_entries(path)}" } if File.directory?(path)
|
|
94
|
+
return { error: "#{file} is larger than #{MAX_FILE_SIZE} bytes. #{use_from_code(skill, file)}" } if
|
|
95
|
+
File.size(path) > MAX_FILE_SIZE
|
|
96
|
+
|
|
97
|
+
content = File.binread(path).force_encoding(Encoding::UTF_8)
|
|
98
|
+
return { error: "#{file} is not a text file. #{use_from_code(skill, file)}" } unless text?(content)
|
|
99
|
+
|
|
100
|
+
content
|
|
101
|
+
rescue ArgumentError => e
|
|
102
|
+
{ error: e.message }
|
|
103
|
+
rescue SystemCallError
|
|
104
|
+
{ error: "The #{skill.name} skill has no file #{file}" }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def folder_entries(path)
|
|
108
|
+
Dir.children(path).sort.map { |entry| File.directory?(File.join(path, entry)) ? "#{entry}/" : entry }.join(', ')
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def text?(content)
|
|
112
|
+
content.valid_encoding? && !content.include?("\0")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def use_from_code(skill, file)
|
|
116
|
+
return 'This conversation cannot run code to use it.' unless @code_execution.call
|
|
117
|
+
|
|
118
|
+
"Use it from run_code at #{self.class.mount_path(skill)}/#{file}."
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def scripts_note
|
|
122
|
+
if @code_execution.call
|
|
123
|
+
'Each skill folder is mounted read-only in every run_code working directory at skills/<name>/, ' \
|
|
124
|
+
'so run_code can run its scripts, such as python skills/<name>/scripts/example.py.'
|
|
125
|
+
else
|
|
126
|
+
'This conversation cannot run code, so skill scripts cannot run. Follow the rest of their instructions.'
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'pathname'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
# A Skill is a folder of instructions, scripts, and reference files in the
|
|
8
|
+
# open {Agent Skills}[https://agentskills.io/specification] format. Its
|
|
9
|
+
# +SKILL.md+ starts with YAML front matter naming and describing the
|
|
10
|
+
# skill, followed by the instructions in Markdown.
|
|
11
|
+
#
|
|
12
|
+
# Chat#with_skills gives skills to a chat. The model sees each skill's
|
|
13
|
+
# name and description, and loads the instructions and files of the ones
|
|
14
|
+
# a task needs.
|
|
15
|
+
#
|
|
16
|
+
# skill = RubyLLM::Skill.find(:invoices)
|
|
17
|
+
# skill.name # => "invoices"
|
|
18
|
+
# skill.description # => "Creates invoices as PDFs. Use when ..."
|
|
19
|
+
# skill.path # => #<Pathname:/app/app/skills/invoices>
|
|
20
|
+
class Skill
|
|
21
|
+
include Support::Inspectable
|
|
22
|
+
|
|
23
|
+
# The file every skill folder holds.
|
|
24
|
+
FILE = 'SKILL.md'
|
|
25
|
+
|
|
26
|
+
# Characters a name may have at most.
|
|
27
|
+
MAX_NAME = 64
|
|
28
|
+
|
|
29
|
+
# Characters a description may have at most.
|
|
30
|
+
MAX_DESCRIPTION = 1024
|
|
31
|
+
|
|
32
|
+
# Characters the compatibility field may have at most.
|
|
33
|
+
MAX_COMPATIBILITY = 500
|
|
34
|
+
|
|
35
|
+
FRONT_MATTER = /\A---[ \t]*\r?\n(.*?)^---[ \t]*(?:\r?\n|\z)/m
|
|
36
|
+
NAME_FORMAT = /\A[[:alnum:]]+(?:-[[:alnum:]]+)*\z/
|
|
37
|
+
private_constant :FRONT_MATTER, :NAME_FORMAT
|
|
38
|
+
|
|
39
|
+
# The skill's name, matching its folder name.
|
|
40
|
+
attr_reader :name
|
|
41
|
+
|
|
42
|
+
# What the skill does and when to use it.
|
|
43
|
+
attr_reader :description
|
|
44
|
+
|
|
45
|
+
# The license the front matter names, or +nil+.
|
|
46
|
+
attr_reader :license
|
|
47
|
+
|
|
48
|
+
# The environment requirements the front matter states, or +nil+.
|
|
49
|
+
attr_reader :compatibility
|
|
50
|
+
|
|
51
|
+
# The tools the front matter pre-approves, as an Array of Strings.
|
|
52
|
+
# RubyLLM reads the field but does not act on it.
|
|
53
|
+
attr_reader :allowed_tools
|
|
54
|
+
|
|
55
|
+
# The front matter's +metadata+, a Hash of Strings.
|
|
56
|
+
attr_reader :metadata
|
|
57
|
+
|
|
58
|
+
# The skill folder, as an absolute Pathname.
|
|
59
|
+
attr_reader :path
|
|
60
|
+
|
|
61
|
+
# The Markdown instructions that follow the front matter.
|
|
62
|
+
attr_reader :instructions
|
|
63
|
+
|
|
64
|
+
class << self
|
|
65
|
+
# Returns the ordered skill directories. Append engine directories
|
|
66
|
+
# with +<<+:
|
|
67
|
+
#
|
|
68
|
+
# initializer "my_engine.skills", before: "ruby_llm.skills" do
|
|
69
|
+
# RubyLLM::Skill.roots << MyEngine::Engine.root.join("app/skills")
|
|
70
|
+
# end
|
|
71
|
+
#
|
|
72
|
+
def roots
|
|
73
|
+
@roots ||= Prompt::Roots.new { root }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def root # :nodoc:
|
|
77
|
+
if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
|
|
78
|
+
Rails.root.join('app/skills')
|
|
79
|
+
else
|
|
80
|
+
Pathname.new(Dir.pwd).join('app/skills')
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Returns every skill directly under the skill roots. A skill in an
|
|
85
|
+
# earlier root hides one with the same folder name in a later root.
|
|
86
|
+
# Raises InvalidSkillError when a skill's +SKILL.md+ is invalid.
|
|
87
|
+
def all
|
|
88
|
+
folders = roots.flat_map { |root| skill_folders(Pathname.new(root)) }
|
|
89
|
+
folders.uniq(&:basename).map { |folder| new(folder) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns the skill in the folder called +name+ under the first skill
|
|
93
|
+
# root that has one. Underscores in a Symbol stand for hyphens, so
|
|
94
|
+
# <tt>:pdf_processing</tt> finds +pdf-processing+. Raises
|
|
95
|
+
# SkillNotFoundError when no root has it.
|
|
96
|
+
#
|
|
97
|
+
# RubyLLM::Skill.find(:invoices)
|
|
98
|
+
#
|
|
99
|
+
def find(name)
|
|
100
|
+
folder = name.is_a?(Symbol) ? name.to_s.tr('_', '-') : name.to_s
|
|
101
|
+
found = (roots.map { |root| Pathname.new(root).join(folder) } if folder.match?(NAME_FORMAT))
|
|
102
|
+
&.find { |path| path.join(FILE).file? }
|
|
103
|
+
raise SkillNotFoundError, "No skill named #{folder} in #{roots.to_a.join(', ')}" unless found
|
|
104
|
+
|
|
105
|
+
new(found)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns +true+, +false+, or the skill names and paths in
|
|
109
|
+
# +selection+, the arguments of Chat#with_skills. Raises
|
|
110
|
+
# ArgumentError for +nil+, and for +true+ or +false+ with names.
|
|
111
|
+
def selection(selection) # :nodoc:
|
|
112
|
+
raise ArgumentError, 'skills cannot be nil; pass false to remove them' if selection.include?(nil)
|
|
113
|
+
return true if selection.empty? || selection == [true]
|
|
114
|
+
return false if selection == [false]
|
|
115
|
+
raise ArgumentError, 'skills true and false take no skill names' if selection.intersect?([true, false])
|
|
116
|
+
|
|
117
|
+
selection.flatten.freeze
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Returns the skills +selection+ names: Symbols are folders under the
|
|
121
|
+
# skill roots, Strings and Pathnames are skill folders, and +true+ is
|
|
122
|
+
# every skill under the roots.
|
|
123
|
+
def resolve(selection) # :nodoc:
|
|
124
|
+
case selection
|
|
125
|
+
when true then all
|
|
126
|
+
when false then []
|
|
127
|
+
else selection.map { |entry| skill(entry) }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Returns +current+ with +added+, refusing two skills with one name.
|
|
132
|
+
def merge(current, added) # :nodoc:
|
|
133
|
+
added.each_with_object(current.dup) do |skill, skills|
|
|
134
|
+
existing = skills.find { |other| other.name == skill.name }
|
|
135
|
+
next skills << skill unless existing
|
|
136
|
+
next if existing.path == skill.path
|
|
137
|
+
|
|
138
|
+
raise ArgumentError, "Two skills are named #{skill.name}: #{existing.path} and #{skill.path}"
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def skill(entry)
|
|
145
|
+
case entry
|
|
146
|
+
when Skill then entry
|
|
147
|
+
when Symbol then find(entry)
|
|
148
|
+
when String, Pathname then new(entry)
|
|
149
|
+
else raise ArgumentError, "skills are Symbols, paths, or RubyLLM::Skill objects, got #{entry.inspect}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def skill_folders(root)
|
|
154
|
+
return [] unless root.directory?
|
|
155
|
+
|
|
156
|
+
root.children.select { |child| child.join(FILE).file? }.sort
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Reads the skill in the folder at +path+. Raises SkillNotFoundError
|
|
161
|
+
# when the folder has no +SKILL.md+, and InvalidSkillError when its
|
|
162
|
+
# front matter breaks the Agent Skills specification. Front matter keys
|
|
163
|
+
# the specification does not define are ignored.
|
|
164
|
+
#
|
|
165
|
+
# RubyLLM::Skill.new("vendor/skills/pdf")
|
|
166
|
+
#
|
|
167
|
+
def initialize(path)
|
|
168
|
+
@path = Pathname.new(File.realpath(path))
|
|
169
|
+
file = @path.join(FILE)
|
|
170
|
+
raise SkillNotFoundError, "#{file} not found" unless file.file?
|
|
171
|
+
|
|
172
|
+
content = file.read(encoding: 'UTF-8')
|
|
173
|
+
invalid('must be valid UTF-8') unless content.valid_encoding?
|
|
174
|
+
|
|
175
|
+
front_matter, @instructions = parse(content)
|
|
176
|
+
assign(front_matter)
|
|
177
|
+
rescue Errno::ENOENT, Errno::ENOTDIR
|
|
178
|
+
raise SkillNotFoundError, "No skill folder at #{path}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Returns the absolute path of the file at +relative+ inside the skill
|
|
182
|
+
# folder. Raises ArgumentError when +relative+ leaves the folder, even
|
|
183
|
+
# through a symbolic link, and Errno::ENOENT when nothing is there.
|
|
184
|
+
def resolve_file(relative) # :nodoc:
|
|
185
|
+
parts = relative.to_s.tr('\\', '/').split('/').reject { |part| part.empty? || part == '.' }
|
|
186
|
+
if relative.to_s.start_with?('/', '\\') || relative.to_s.match?(/\A[A-Za-z]:/) || parts.include?('..')
|
|
187
|
+
raise ArgumentError, "#{relative} is not a path inside the #{name} skill"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
resolved = File.realpath(path.join(*parts))
|
|
191
|
+
inside = resolved == path.to_s || resolved.start_with?(File.join(path.to_s, ''))
|
|
192
|
+
raise ArgumentError, "#{relative} is not a path inside the #{name} skill" unless inside
|
|
193
|
+
|
|
194
|
+
resolved
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def inspect_attributes # :nodoc:
|
|
198
|
+
{ name:, description:, path: path.to_s }
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
private
|
|
202
|
+
|
|
203
|
+
def parse(content)
|
|
204
|
+
content = content.delete_prefix("\u{FEFF}")
|
|
205
|
+
match = FRONT_MATTER.match(content)
|
|
206
|
+
invalid('must start with YAML front matter between --- lines') unless match
|
|
207
|
+
|
|
208
|
+
front_matter = YAML.safe_load(match[1], permitted_classes: [Date, Time])
|
|
209
|
+
invalid('front matter must be a YAML mapping') unless front_matter.is_a?(Hash)
|
|
210
|
+
[front_matter.transform_keys(&:to_s), match.post_match]
|
|
211
|
+
rescue Psych::Exception => e
|
|
212
|
+
invalid("front matter is not valid YAML: #{e.message}")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def assign(front_matter)
|
|
216
|
+
@name = validated_name(front_matter['name'])
|
|
217
|
+
@description = required_text(front_matter, 'description', MAX_DESCRIPTION)
|
|
218
|
+
@license = optional_text(front_matter, 'license')
|
|
219
|
+
@compatibility = optional_text(front_matter, 'compatibility', MAX_COMPATIBILITY)
|
|
220
|
+
@allowed_tools = optional_text(front_matter, 'allowed-tools').to_s.split.freeze
|
|
221
|
+
@metadata = validated_metadata(front_matter['metadata'])
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def validated_name(value)
|
|
225
|
+
name = required_text({ 'name' => value }, 'name', MAX_NAME).unicode_normalize(:nfkc)
|
|
226
|
+
unless name == name.downcase && name.match?(NAME_FORMAT)
|
|
227
|
+
invalid("name #{name.inspect} must be lowercase letters, digits, and single hyphens between them")
|
|
228
|
+
end
|
|
229
|
+
folder = path.basename.to_s.dup.force_encoding(Encoding::UTF_8).scrub.unicode_normalize(:nfkc)
|
|
230
|
+
invalid("name #{name.inspect} must match its folder name, #{folder}") unless name == folder
|
|
231
|
+
|
|
232
|
+
name
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def required_text(front_matter, key, limit)
|
|
236
|
+
value = front_matter[key]
|
|
237
|
+
invalid("needs a #{key}") if value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
238
|
+
|
|
239
|
+
optional_text(front_matter, key, limit)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def optional_text(front_matter, key, limit = nil)
|
|
243
|
+
value = front_matter[key]
|
|
244
|
+
return if value.nil?
|
|
245
|
+
|
|
246
|
+
invalid("#{key} must be a string") unless value.is_a?(String)
|
|
247
|
+
invalid("#{key} must not be empty") if value.strip.empty?
|
|
248
|
+
invalid("#{key} must be at most #{limit} characters") if limit && value.length > limit
|
|
249
|
+
value.strip
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def validated_metadata(value)
|
|
253
|
+
return {}.freeze if value.nil?
|
|
254
|
+
|
|
255
|
+
invalid('metadata must be a mapping of strings') unless value.is_a?(Hash)
|
|
256
|
+
value.to_h { |key, entry| [key.to_s, entry.to_s] }.freeze
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def invalid(reason)
|
|
260
|
+
raise InvalidSkillError, "#{path.join(FILE)} #{reason}"
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
data/lib/ruby_llm/speech.rb
CHANGED
|
@@ -9,7 +9,9 @@ module RubyLLM
|
|
|
9
9
|
#
|
|
10
10
|
class Speech
|
|
11
11
|
include Support::Inspectable
|
|
12
|
+
include Files::Saving
|
|
12
13
|
include Accounting::Usage::Result
|
|
14
|
+
include Accounting::Usage::ModelInfo
|
|
13
15
|
|
|
14
16
|
# Maps audio format names to their MIME types.
|
|
15
17
|
MIME_TYPES = {
|
|
@@ -38,7 +40,7 @@ module RubyLLM
|
|
|
38
40
|
attr_reader :mime_type
|
|
39
41
|
|
|
40
42
|
def initialize(data:, model:, voice: nil, format: 'mp3', mime_type: nil, # :nodoc:
|
|
41
|
-
input_tokens: nil, output_tokens: nil)
|
|
43
|
+
input_tokens: nil, output_tokens: nil, characters: nil)
|
|
42
44
|
@data = data
|
|
43
45
|
@model = model
|
|
44
46
|
@voice = voice
|
|
@@ -46,6 +48,7 @@ module RubyLLM
|
|
|
46
48
|
@mime_type = mime_type || MIME_TYPES.fetch(@format, "audio/#{@format}")
|
|
47
49
|
@input_tokens = input_tokens
|
|
48
50
|
@output_tokens = output_tokens
|
|
51
|
+
@characters = characters
|
|
49
52
|
end
|
|
50
53
|
|
|
51
54
|
# Generates speech for +input+ and returns a Speech holding the audio.
|
|
@@ -55,7 +58,7 @@ module RubyLLM
|
|
|
55
58
|
# provider's request vocabulary, such as +instructions:+ and +speed:+
|
|
56
59
|
# for OpenAI, and merges them into the request as-is.
|
|
57
60
|
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
58
|
-
# over RubyLLM.
|
|
61
|
+
# over RubyLLM.with_owner.
|
|
59
62
|
#
|
|
60
63
|
# speech = RubyLLM.speak "Hello, welcome to RubyLLM!"
|
|
61
64
|
# speech.save "welcome.mp3"
|
|
@@ -86,42 +89,28 @@ module RubyLLM
|
|
|
86
89
|
metadata: nil,
|
|
87
90
|
owner: nil,
|
|
88
91
|
&block)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
model
|
|
99
|
-
model_info: model,
|
|
100
|
-
input: input,
|
|
101
|
-
voice: voice,
|
|
102
|
-
format: format,
|
|
103
|
-
provider_options: provider_options,
|
|
104
|
-
metadata: metadata,
|
|
105
|
-
streaming: !block.nil?,
|
|
106
|
-
tokens: empty_tokens,
|
|
107
|
-
cost: Cost.new(tokens: empty_tokens, model:, category: :audio_tokens)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
RubyLLM.instrument('speech.ruby_llm', payload, config: config) do |event|
|
|
111
|
-
result = Accounting::Usage.owned_by(owner) do
|
|
112
|
-
provider_instance.speak(input, model:, voice:, format:, provider_options:, &block)
|
|
113
|
-
end
|
|
114
|
-
event[:result] = result
|
|
115
|
-
event[:response_model] = result.model
|
|
116
|
-
event[:voice] = result.voice
|
|
117
|
-
event[:format] = result.format
|
|
118
|
-
event[:audio_bytes] = result.to_blob.bytesize
|
|
119
|
-
event[:tokens] = result.tokens
|
|
120
|
-
event[:cost] = result.cost
|
|
121
|
-
result
|
|
92
|
+
Accounting::Operation.new(
|
|
93
|
+
'speech', model, default: :default_speech_model, category: :audio_tokens, provider:, assume_model_exists:,
|
|
94
|
+
context:, owner:,
|
|
95
|
+
details: lambda { |event, result|
|
|
96
|
+
event[:voice] = result.voice
|
|
97
|
+
event[:format] = result.format
|
|
98
|
+
event[:audio_bytes] = result.to_blob.bytesize
|
|
99
|
+
}
|
|
100
|
+
).run(input:, voice:, format:, provider_options:, metadata:, streaming: !block.nil?) do |provider_instance, model|
|
|
101
|
+
provider_instance.speak(input, model:, voice:, format:, provider_options:, &block)
|
|
122
102
|
end
|
|
123
103
|
end
|
|
124
104
|
|
|
105
|
+
##
|
|
106
|
+
# :method: save
|
|
107
|
+
# :call-seq:
|
|
108
|
+
# save(path)
|
|
109
|
+
#
|
|
110
|
+
# Writes the audio to +path+ in binary mode and returns +path+.
|
|
111
|
+
#
|
|
112
|
+
# speech.save "welcome.mp3"
|
|
113
|
+
|
|
125
114
|
# Returns the raw audio bytes. Alias for #data, mirroring Image#to_blob.
|
|
126
115
|
def to_blob
|
|
127
116
|
data
|
|
@@ -132,7 +121,7 @@ module RubyLLM
|
|
|
132
121
|
def tokens
|
|
133
122
|
return ruby_llm_usage_tokens unless ruby_llm_usage_entries.empty?
|
|
134
123
|
|
|
135
|
-
Tokens.new(input: @input_tokens, output: @output_tokens)
|
|
124
|
+
Tokens.new(input: @input_tokens, output: @output_tokens, characters: @characters)
|
|
136
125
|
end
|
|
137
126
|
|
|
138
127
|
# Returns the speech cost across every provider attempt.
|
|
@@ -142,23 +131,6 @@ module RubyLLM
|
|
|
142
131
|
Cost.new(tokens:, model: model_info, category: :audio_tokens)
|
|
143
132
|
end
|
|
144
133
|
|
|
145
|
-
attr_writer :model_info # :nodoc:
|
|
146
|
-
|
|
147
|
-
def model_info # :nodoc:
|
|
148
|
-
@model_info ||= RubyLLM.models.find(model)
|
|
149
|
-
rescue ModelNotFoundError
|
|
150
|
-
nil
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
# Writes the audio to +path+ in binary mode and returns +path+.
|
|
154
|
-
#
|
|
155
|
-
# speech.save "welcome.mp3"
|
|
156
|
-
#
|
|
157
|
-
def save(path)
|
|
158
|
-
File.binwrite(File.expand_path(path), to_blob)
|
|
159
|
-
path
|
|
160
|
-
end
|
|
161
|
-
|
|
162
134
|
def inspect_attributes # :nodoc:
|
|
163
135
|
{ model: model, voice: voice, format: format, data: data && "#{data.bytesize} bytes" }
|
|
164
136
|
end
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A file in a FileStore: its path, size, content type, and a digest of
|
|
5
|
+
# its content. FileStore#list and FileStore#find return them, and
|
|
6
|
+
# Chat#after_file_write callbacks receive each one the conversation
|
|
7
|
+
# writes.
|
|
8
|
+
#
|
|
9
|
+
# chat.file_store.list
|
|
10
|
+
# # => [#<RubyLLM::StoredFile path: "report.pdf", byte_size: 2201344, content_type: "application/pdf">]
|
|
11
|
+
#
|
|
12
|
+
class StoredFile
|
|
13
|
+
include Support::Inspectable
|
|
14
|
+
|
|
15
|
+
# The path of the file inside its store, relative and with forward
|
|
16
|
+
# slashes, such as <tt>"reports/q3.pdf"</tt>.
|
|
17
|
+
attr_reader :path
|
|
18
|
+
|
|
19
|
+
# The size of the file in bytes.
|
|
20
|
+
attr_reader :byte_size
|
|
21
|
+
|
|
22
|
+
# The MIME type of the file, such as <tt>"application/pdf"</tt>.
|
|
23
|
+
attr_reader :content_type
|
|
24
|
+
|
|
25
|
+
# A digest of the file's content that changes whenever the content
|
|
26
|
+
# does, or +nil+ when the store does not know one without reading the
|
|
27
|
+
# file. Stores in the box use SHA-256 in hex.
|
|
28
|
+
attr_reader :digest
|
|
29
|
+
|
|
30
|
+
# When the file was last written, or +nil+ when the store does not say.
|
|
31
|
+
attr_reader :updated_at
|
|
32
|
+
|
|
33
|
+
# Who wrote the file, for stores that keep one, or +nil+. RubyLLM
|
|
34
|
+
# stores it and hands it back without interpreting it.
|
|
35
|
+
attr_reader :owner
|
|
36
|
+
|
|
37
|
+
def initialize(path:, byte_size:, content_type:, digest: nil, updated_at: nil, owner: nil) # :nodoc:
|
|
38
|
+
@path = path
|
|
39
|
+
@byte_size = byte_size
|
|
40
|
+
@content_type = content_type
|
|
41
|
+
@digest = digest
|
|
42
|
+
@updated_at = updated_at
|
|
43
|
+
@owner = owner
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Returns the last part of #path.
|
|
47
|
+
#
|
|
48
|
+
# file.path # => "reports/q3.pdf"
|
|
49
|
+
# file.filename # => "q3.pdf"
|
|
50
|
+
#
|
|
51
|
+
def filename
|
|
52
|
+
File.basename(path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns whether the content is text, like source code, CSV, or
|
|
56
|
+
# Markdown, judged by its content type.
|
|
57
|
+
def text?
|
|
58
|
+
Files::MimeType.text?(content_type)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the file's size for people and models to read, such as
|
|
62
|
+
# <tt>"2.1 MB"</tt>.
|
|
63
|
+
def human_size
|
|
64
|
+
units = %w[bytes KB MB GB TB]
|
|
65
|
+
size = byte_size.to_f
|
|
66
|
+
exponent = size.zero? ? 0 : [(Math.log(size) / Math.log(1024)).floor, units.size - 1].min
|
|
67
|
+
return "#{byte_size} #{byte_size == 1 ? 'byte' : 'bytes'}" if exponent.zero?
|
|
68
|
+
|
|
69
|
+
"#{(size / (1024**exponent)).round(1)} #{units[exponent]}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns whether +other+ is a StoredFile with the same path, size,
|
|
73
|
+
# content type, and digest.
|
|
74
|
+
def ==(other)
|
|
75
|
+
other.is_a?(StoredFile) && other.to_h.except(:updated_at, :owner) == to_h.except(:updated_at, :owner)
|
|
76
|
+
end
|
|
77
|
+
alias eql? ==
|
|
78
|
+
|
|
79
|
+
def hash # :nodoc:
|
|
80
|
+
to_h.except(:updated_at, :owner).hash
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Returns the attributes as a Hash.
|
|
84
|
+
def to_h
|
|
85
|
+
{ path:, byte_size:, content_type:, digest:, updated_at:, owner: }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
def inspect_attributes
|
|
91
|
+
{ path:, byte_size:, content_type: }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -13,15 +13,15 @@ module RubyLLM
|
|
|
13
13
|
module_function
|
|
14
14
|
|
|
15
15
|
def watch(checkpoint)
|
|
16
|
-
previous =
|
|
17
|
-
|
|
16
|
+
previous = Fiber[KEY]
|
|
17
|
+
Fiber[KEY] = checkpoint
|
|
18
18
|
yield
|
|
19
19
|
ensure
|
|
20
|
-
|
|
20
|
+
Fiber[KEY] = previous
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def check
|
|
24
|
-
|
|
24
|
+
Fiber[KEY]&.call
|
|
25
25
|
end
|
|
26
26
|
|
|
27
27
|
def pause(seconds)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support
|
|
5
|
+
# Keeps chats, agents, and <tt>acts_as_chat</tt> records Enumerable over
|
|
6
|
+
# their messages through RubyLLM 2.x. Their +each+ is deprecated, and
|
|
7
|
+
# the Enumerable methods built on it with it: iterate +messages+
|
|
8
|
+
# instead. RubyLLM 3.0 removes them.
|
|
9
|
+
module DeprecatedEnumerable # :nodoc:
|
|
10
|
+
include Enumerable
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
module Support
|
|
5
|
+
module DeprecatedJobPredicates # :nodoc:
|
|
6
|
+
def done?
|
|
7
|
+
warn_about_predicate(:done?, :complete?)
|
|
8
|
+
complete?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def completed?
|
|
12
|
+
warn_about_predicate(:completed?, :succeeded?)
|
|
13
|
+
succeeded?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def warn_about_predicate(name, replacement)
|
|
19
|
+
RubyLLM.deprecator.deprecate("#{self.class}##{name}", replacement: "#{self.class}##{replacement}",
|
|
20
|
+
removal: '3.0')
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -17,7 +17,7 @@ module RubyLLM
|
|
|
17
17
|
attributes = inspect_attributes
|
|
18
18
|
.reject { |_, value| value.nil? || (value.respond_to?(:empty?) && value.empty?) }
|
|
19
19
|
.map { |name, value| "#{name}: #{format_for_inspect(value)}" }
|
|
20
|
-
class_name = self.class.ancestors.find(&:name).name
|
|
20
|
+
class_name = self.class.ancestors.grep(Class).find(&:name).name
|
|
21
21
|
return "#<#{class_name}>" if attributes.empty?
|
|
22
22
|
|
|
23
23
|
"#<#{class_name} #{attributes.join(', ')}>"
|