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
data/lib/ruby_llm/error.rb
CHANGED
|
@@ -4,9 +4,8 @@ module RubyLLM
|
|
|
4
4
|
# Error is the base class for provider-operation errors raised by
|
|
5
5
|
# RubyLLM, including API, network, capability, and provider response-shape
|
|
6
6
|
# failures. When an HTTP response is available, it wraps that response and
|
|
7
|
-
# normalizes the message across providers.
|
|
8
|
-
#
|
|
9
|
-
# common HTTP status codes: BadRequestError (400), UnauthorizedError (401),
|
|
7
|
+
# normalizes the message across providers. Subclasses map common HTTP
|
|
8
|
+
# status codes: BadRequestError (400), UnauthorizedError (401),
|
|
10
9
|
# PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
|
|
11
10
|
# ServerError (500), ServiceUnavailableError (502 to 504), and
|
|
12
11
|
# OverloadedError (529).
|
|
@@ -20,6 +19,11 @@ module RubyLLM
|
|
|
20
19
|
# puts e.response&.status
|
|
21
20
|
# end
|
|
22
21
|
#
|
|
22
|
+
# Providers often quote the request back in their error, so with
|
|
23
|
+
# Configuration#redact_error_messages the message carries only the HTTP
|
|
24
|
+
# status, the provider, and the provider's error code. What the provider
|
|
25
|
+
# wrote stays on #provider_message and its raw body on #response_body.
|
|
26
|
+
#
|
|
23
27
|
# Local setup and programming errors, such as ConfigurationError and
|
|
24
28
|
# ModelNotFoundError, inherit from StandardError directly and are not
|
|
25
29
|
# caught by rescuing Error.
|
|
@@ -28,6 +32,20 @@ module RubyLLM
|
|
|
28
32
|
# available. Its +status+ and +body+ carry the provider's reply.
|
|
29
33
|
attr_reader :response
|
|
30
34
|
|
|
35
|
+
# The slug of the provider that answered, such as <tt>"openai"</tt>, or
|
|
36
|
+
# +nil+ when it is not known.
|
|
37
|
+
attr_reader :provider
|
|
38
|
+
|
|
39
|
+
# The provider's code for the error, such as
|
|
40
|
+
# <tt>"invalid_request_error"</tt> or <tt>"INVALID_ARGUMENT"</tt>, or
|
|
41
|
+
# +nil+ when it gave none that reads like a code.
|
|
42
|
+
attr_reader :error_code
|
|
43
|
+
|
|
44
|
+
# What the provider wrote about the error, or +nil+ when it wrote
|
|
45
|
+
# nothing RubyLLM could read. It can quote the request, so
|
|
46
|
+
# Configuration#redact_error_messages keeps it out of #message.
|
|
47
|
+
attr_reader :provider_message
|
|
48
|
+
|
|
31
49
|
# The Protocol that rendered the request the error answered, which
|
|
32
50
|
# reads its payload back for #request_shape.
|
|
33
51
|
attr_accessor :request_protocol # :nodoc:
|
|
@@ -39,11 +57,28 @@ module RubyLLM
|
|
|
39
57
|
nil
|
|
40
58
|
end
|
|
41
59
|
|
|
60
|
+
ERROR_CODE = /\A[A-Za-z][\w.:-]{0,63}\z/ # :nodoc:
|
|
61
|
+
|
|
42
62
|
# Creates an error with +message+. Pass +response:+ to attach the HTTP
|
|
43
|
-
# response
|
|
44
|
-
|
|
63
|
+
# response, +provider:+ to name the provider's slug, +error_code:+ for
|
|
64
|
+
# the provider's code, kept only when it reads like one, and
|
|
65
|
+
# +provider_message:+ for what the provider wrote, which follows
|
|
66
|
+
# +message+, or stands in for it when +message+ is +nil+. Without
|
|
67
|
+
# either, the response body supplies the message.
|
|
68
|
+
def initialize(message = nil, response: nil, provider: nil, provider_message: nil, error_code: nil, redact: nil)
|
|
45
69
|
@response = response
|
|
46
|
-
|
|
70
|
+
@provider = provider
|
|
71
|
+
@provider_message = provider_message
|
|
72
|
+
@error_code = error_code if error_code.is_a?(Integer) || error_code.to_s.match?(ERROR_CODE)
|
|
73
|
+
redact = RubyLLM.config.redact_error_messages if redact.nil?
|
|
74
|
+
super(full_message_for(message, redact:) || self.class.default_message)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns the body of #response, or +nil+ without one. It can quote the
|
|
78
|
+
# request, so Configuration#redact_error_messages keeps it out of
|
|
79
|
+
# #message.
|
|
80
|
+
def response_body
|
|
81
|
+
response&.body
|
|
47
82
|
end
|
|
48
83
|
|
|
49
84
|
# Returns a RequestShape describing the conversation request the error
|
|
@@ -57,12 +92,40 @@ module RubyLLM
|
|
|
57
92
|
# Rails.logger.error("#{e.message}\n#{e.request_shape}")
|
|
58
93
|
# Rails.error.report(e, context: { request_shape: e.request_shape&.to_h })
|
|
59
94
|
#
|
|
60
|
-
def request_shape
|
|
95
|
+
def request_shape # :nodoc:
|
|
61
96
|
return unless request_protocol
|
|
62
97
|
return @request_shape if defined?(@request_shape)
|
|
63
98
|
|
|
64
99
|
@request_shape = request_protocol.request_shape(request_payload)
|
|
65
100
|
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def full_message_for(message, redact:)
|
|
105
|
+
return message || (redacted_message if remote?) if redact
|
|
106
|
+
return "#{message}: #{provider_message}" if message && provider_message
|
|
107
|
+
|
|
108
|
+
message || provider_message || response_body_message
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def remote?
|
|
112
|
+
!response.nil? || !provider_message.nil?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def redacted_message
|
|
116
|
+
status = response.status if response.respond_to?(:status)
|
|
117
|
+
text = "#{provider || redacted_source} returned #{status ? "HTTP #{status}" : 'an error'}"
|
|
118
|
+
error_code ? "#{text} (#{error_code})" : text
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def redacted_source
|
|
122
|
+
'The provider'
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def response_body_message
|
|
126
|
+
body = response_body
|
|
127
|
+
body unless body.respond_to?(:empty?) && body.empty?
|
|
128
|
+
end
|
|
66
129
|
end
|
|
67
130
|
|
|
68
131
|
# Raised when a deprecated API is used and
|
|
@@ -75,12 +138,28 @@ module RubyLLM
|
|
|
75
138
|
class DeprecationError < StandardError; end
|
|
76
139
|
|
|
77
140
|
# Raised when required configuration, such as a provider API key, is
|
|
78
|
-
# missing
|
|
141
|
+
# missing, or when a setup can't do what a chat asks of it, such as a
|
|
142
|
+
# subagent added with +tools+ that stops to wait (see AgentTool).
|
|
79
143
|
class ConfigurationError < StandardError; end
|
|
80
144
|
|
|
81
145
|
# Raised when RubyLLM.render_prompt cannot find the named prompt file.
|
|
82
146
|
class PromptNotFoundError < StandardError; end
|
|
83
147
|
|
|
148
|
+
# Raised when a skill named by Chat#with_skills or Skill.find is missing.
|
|
149
|
+
class SkillNotFoundError < StandardError; end
|
|
150
|
+
|
|
151
|
+
# Raised when a skill's +SKILL.md+ breaks the Agent Skills specification:
|
|
152
|
+
# missing or malformed front matter, or a name or description it rejects.
|
|
153
|
+
class InvalidSkillError < StandardError; end
|
|
154
|
+
|
|
155
|
+
# Raised when a FileStore has no file at the path asked for.
|
|
156
|
+
class FileNotFoundError < StandardError; end
|
|
157
|
+
|
|
158
|
+
# Raised by a FileStore when a write would take it past its limit. Its
|
|
159
|
+
# message is what the model reads, so say what to do. The file tools and
|
|
160
|
+
# +run_code+ answer the model with it instead of failing the reply.
|
|
161
|
+
class FileStoreFullError < StandardError; end
|
|
162
|
+
|
|
84
163
|
# Raised when a message role outside +:system+, +:user+, +:assistant+, or
|
|
85
164
|
# +:tool+ is used.
|
|
86
165
|
class InvalidRoleError < StandardError; end
|
|
@@ -92,9 +171,20 @@ module RubyLLM
|
|
|
92
171
|
# Raised when a user message is staged while the last response still has
|
|
93
172
|
# unanswered tool calls. Providers reject such a transcript, so the chat
|
|
94
173
|
# refuses it up front: finish the round with #complete, recording
|
|
95
|
-
# #approve or #
|
|
174
|
+
# #approve or #decline decisions for calls that require approval.
|
|
96
175
|
class PendingToolCallsError < StandardError; end
|
|
97
176
|
|
|
177
|
+
# Raised by Chat#complete when the limit set with
|
|
178
|
+
# <tt>Chat#with_tool_options(rounds:)</tt> is reached. Every tool call of
|
|
179
|
+
# the rounds that ran has its result, so the conversation stays valid and
|
|
180
|
+
# another #complete continues with a fresh count. Not a RubyLLM::Error,
|
|
181
|
+
# which a record takes for a failed request and a job may retry.
|
|
182
|
+
class ToolRoundLimitError < StandardError; end
|
|
183
|
+
|
|
184
|
+
# Raised when a persisted tool call is declined after it started running.
|
|
185
|
+
# The call keeps running and the model receives its result.
|
|
186
|
+
class ToolCallRunningError < StandardError; end
|
|
187
|
+
|
|
98
188
|
# Raised when a requested model id is not in the model registry.
|
|
99
189
|
class ModelNotFoundError < StandardError; end
|
|
100
190
|
|
|
@@ -133,6 +223,24 @@ module RubyLLM
|
|
|
133
223
|
end
|
|
134
224
|
end
|
|
135
225
|
|
|
226
|
+
# Raised when the provider rejects a request because the model does not
|
|
227
|
+
# take one of its parameters, such as a temperature on a reasoning model.
|
|
228
|
+
# Chat#with_fallbacks can retry without it with <tt>drop_unsupported: true</tt>.
|
|
229
|
+
class UnsupportedParameterError < BadRequestError
|
|
230
|
+
# The parameter the provider rejected: a Symbol naming the RubyLLM
|
|
231
|
+
# setting, such as +:temperature+, +:thinking+, or +:max_output_tokens+,
|
|
232
|
+
# or the provider's own name as a String for anything else, such as a
|
|
233
|
+
# key of Chat#with_provider_options.
|
|
234
|
+
attr_reader :parameter
|
|
235
|
+
|
|
236
|
+
# Creates the error for +parameter+. Takes the other arguments of
|
|
237
|
+
# Error.new.
|
|
238
|
+
def initialize(message = nil, parameter: nil, **)
|
|
239
|
+
@parameter = parameter
|
|
240
|
+
super(message, **)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
136
244
|
# Raised for HTTP 403 responses when the API key lacks permission for the
|
|
137
245
|
# requested resource.
|
|
138
246
|
class ForbiddenError < Error
|
|
@@ -149,17 +257,39 @@ module RubyLLM
|
|
|
149
257
|
end
|
|
150
258
|
end
|
|
151
259
|
|
|
152
|
-
#
|
|
153
|
-
#
|
|
260
|
+
# The parent of ToolCallTruncatedError, the error RubyLLM raises for
|
|
261
|
+
# tool-call arguments it cannot read. Other arguments that are not a JSON
|
|
262
|
+
# object do not raise: the chat answers the call with an error result
|
|
263
|
+
# and the model calls the tool again.
|
|
154
264
|
class ToolCallParseError < Error
|
|
155
265
|
# Returns the normalized reason generation stopped, or +nil+.
|
|
156
266
|
attr_reader :finish_reason
|
|
157
267
|
|
|
158
268
|
def initialize(message = nil, response: nil, finish_reason: nil) # :nodoc:
|
|
159
269
|
@finish_reason = finish_reason
|
|
160
|
-
message
|
|
161
|
-
|
|
162
|
-
|
|
270
|
+
super(message || default_message, response: response)
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
private
|
|
274
|
+
|
|
275
|
+
def default_message
|
|
276
|
+
message = 'Provider returned malformed tool call arguments'
|
|
277
|
+
finish_reason ? "#{message} (finish_reason: #{finish_reason})" : message
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Raised when the response reached its output token limit while the model
|
|
282
|
+
# was writing a tool call, so the call's arguments are incomplete. Raise
|
|
283
|
+
# the limit with Chat#with_max_output_tokens, or ask for less at once.
|
|
284
|
+
class ToolCallTruncatedError < ToolCallParseError
|
|
285
|
+
def initialize(message = nil, response: nil) # :nodoc:
|
|
286
|
+
super(message, response:, finish_reason: :max_tokens)
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
private
|
|
290
|
+
|
|
291
|
+
def default_message
|
|
292
|
+
'The response reached its output token limit while writing a tool call (finish_reason: max_tokens)'
|
|
163
293
|
end
|
|
164
294
|
end
|
|
165
295
|
|
|
@@ -207,6 +337,29 @@ module RubyLLM
|
|
|
207
337
|
end
|
|
208
338
|
end
|
|
209
339
|
|
|
340
|
+
# Raised when a request never gets an answer from the provider or an
|
|
341
|
+
# MCP server: the connection is refused or drops, TLS fails, or the
|
|
342
|
+
# request times out. RubyLLM retries provider requests first, following
|
|
343
|
+
# Configuration#max_retries. The original Faraday error is its +cause+.
|
|
344
|
+
#
|
|
345
|
+
# rescue RubyLLM::ConnectionError => e
|
|
346
|
+
# retry_later if e.timeout?
|
|
347
|
+
class ConnectionError < Error
|
|
348
|
+
# Returns whether the request timed out, including while opening the
|
|
349
|
+
# connection or waiting for the next bytes of a stream, rather than
|
|
350
|
+
# failing to connect.
|
|
351
|
+
def timeout?
|
|
352
|
+
return true if cause.is_a?(Faraday::TimeoutError)
|
|
353
|
+
|
|
354
|
+
cause.respond_to?(:wrapped_exception) && TIMEOUTS.any? { |timeout| cause.wrapped_exception.is_a?(timeout) }
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Some adapters, such as async-http-faraday, report a read that waited
|
|
358
|
+
# too long as a failed connection wrapping IO::TimeoutError.
|
|
359
|
+
TIMEOUTS = [Timeout::Error, IO::TimeoutError].freeze # :nodoc:
|
|
360
|
+
private_constant :TIMEOUTS
|
|
361
|
+
end
|
|
362
|
+
|
|
210
363
|
# Raised for HTTP 401 responses when the API key is missing or invalid.
|
|
211
364
|
class UnauthorizedError < Error
|
|
212
365
|
def self.default_message # :nodoc:
|
|
@@ -42,8 +42,7 @@ module RubyLLM
|
|
|
42
42
|
def discover(name)
|
|
43
43
|
raise ArgumentError, 'An anonymous evaluation needs an explicit dataset' unless name
|
|
44
44
|
|
|
45
|
-
filename =
|
|
46
|
-
.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
45
|
+
filename = Support::Utils.file_path(name)
|
|
47
46
|
root = Prompt.root.parent.join('evals')
|
|
48
47
|
paths = %w[yml yaml json jsonl].map { |extension| root.join("#{filename}.#{extension}") }.select(&:file?)
|
|
49
48
|
raise ArgumentError, "Dataset not found: #{root.join(filename)}.{yml,yaml,json,jsonl}" if paths.empty?
|
|
@@ -55,8 +54,10 @@ module RubyLLM
|
|
|
55
54
|
def read(path)
|
|
56
55
|
case File.extname(path)
|
|
57
56
|
when '.yml', '.yaml' then YAML.safe_load_file(path, aliases: false)
|
|
58
|
-
when '.json' then JSON.parse(File.read(path))
|
|
59
|
-
when '.jsonl'
|
|
57
|
+
when '.json' then JSON.parse(File.read(path, encoding: Encoding::UTF_8))
|
|
58
|
+
when '.jsonl'
|
|
59
|
+
File.foreach(path, encoding: Encoding::UTF_8).reject { |line| line.strip.empty? }
|
|
60
|
+
.map { |line| JSON.parse(line) }
|
|
60
61
|
else raise ArgumentError, "Unsupported dataset format: #{path}"
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -26,7 +26,7 @@ module RubyLLM
|
|
|
26
26
|
when Message then [conversation]
|
|
27
27
|
else []
|
|
28
28
|
end.freeze
|
|
29
|
-
@tool_calls = messages.flat_map { |message|
|
|
29
|
+
@tool_calls = messages.flat_map { |message| message.tool_calls.to_a }
|
|
30
30
|
@tool_calls << result if result.is_a?(ToolCall)
|
|
31
31
|
@tool_calls.freeze
|
|
32
32
|
@output = primary_output(conversation, result)
|
|
@@ -57,12 +57,13 @@ module RubyLLM
|
|
|
57
57
|
when Attachment then attachment_data(value)
|
|
58
58
|
when Hash then serialize_hash(value, parents)
|
|
59
59
|
when Array then value.map { |item| serialize(item, parents) }
|
|
60
|
+
when Numeric then value.to_f
|
|
60
61
|
else serialize_object(value, parents)
|
|
61
62
|
end
|
|
62
63
|
end
|
|
63
64
|
|
|
64
65
|
def scalar?(value)
|
|
65
|
-
[String, Symbol,
|
|
66
|
+
[String, Symbol, Integer, Float, TrueClass, FalseClass, NilClass].any? { |type| value.is_a?(type) }
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def serialize_hash(value, parents)
|
|
@@ -95,7 +96,8 @@ module RubyLLM
|
|
|
95
96
|
def conversation_data(chat, parents)
|
|
96
97
|
answer = chat.messages.reverse.find { |message| message.role == :assistant }&.content
|
|
97
98
|
{ output: answer, messages: serialize(chat.messages, parents), complete: chat.complete?, waiting: chat.waiting?,
|
|
98
|
-
|
|
99
|
+
pending_questions: chat.pending_questions.map(&:id), cancelled: chat.cancelled?,
|
|
100
|
+
model: chat.model.id }
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
def message_data(message, parents)
|
|
@@ -68,14 +68,18 @@ module RubyLLM
|
|
|
68
68
|
|
|
69
69
|
# Writes a JSON report and returns the supplied path.
|
|
70
70
|
def save(path)
|
|
71
|
-
File.write(path, JSON.pretty_generate(to_h))
|
|
71
|
+
File.write(File.expand_path(path), JSON.pretty_generate(to_h))
|
|
72
72
|
path
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
-
# Returns a readable per-case report followed by counts
|
|
75
|
+
# Returns a readable per-case report followed by counts, and the total
|
|
76
|
+
# cost when every amount is known.
|
|
76
77
|
def to_s
|
|
77
78
|
rows = trials.map { |trial| trial_description(trial) }
|
|
78
|
-
|
|
79
|
+
total = cost.total
|
|
80
|
+
summary = counts.map { |status, count| "#{count} #{status}" }.join(', ')
|
|
81
|
+
summary += format(', cost $%.4f', total) if total
|
|
82
|
+
[name, *rows, summary].join("\n")
|
|
79
83
|
end
|
|
80
84
|
|
|
81
85
|
def inspect_attributes # :nodoc:
|
|
@@ -62,7 +62,7 @@ module RubyLLM
|
|
|
62
62
|
|
|
63
63
|
# Returns the measurement, policy, and outcome as a Hash.
|
|
64
64
|
def to_h
|
|
65
|
-
{ name:, status:, value: value.respond_to?(:to_h) ? value
|
|
65
|
+
{ name:, status:, value: value.nil? || !value.respond_to?(:to_h) ? value : value.to_h,
|
|
66
66
|
reason:, minimum: @minimum, model:, evidence:,
|
|
67
67
|
error: error && { class: error.class.name, message: error.message } }
|
|
68
68
|
end
|
data/lib/ruby_llm/evaluation.rb
CHANGED
|
@@ -63,7 +63,8 @@ module RubyLLM
|
|
|
63
63
|
|
|
64
64
|
@declared_names << name
|
|
65
65
|
backend = Evaluator.new(evaluator) if evaluator
|
|
66
|
-
definitions[name] = { name:, instructions: instructions&.dup&.freeze, minimum
|
|
66
|
+
definitions[name] = { name:, instructions: instructions&.dup&.freeze, minimum: minimum&.to_f,
|
|
67
|
+
evaluator: backend }.freeze
|
|
67
68
|
end
|
|
68
69
|
|
|
69
70
|
# Converts a custom result into evaluation evidence. The original stays available as result.
|
data/lib/ruby_llm/fallback.rb
CHANGED
|
@@ -5,7 +5,10 @@ module RubyLLM
|
|
|
5
5
|
# When the active model fails with a matching error, the chat retries the
|
|
6
6
|
# request with each fallback in order. Instances are yielded to
|
|
7
7
|
# Chat#before_fallback and Chat#after_fallback callbacks, enriched with the
|
|
8
|
-
# details of that attempt.
|
|
8
|
+
# details of that attempt. With <tt>drop_unsupported: true</tt>, a retry
|
|
9
|
+
# of the same model without a parameter it rejected is an attempt too:
|
|
10
|
+
# its #from and #to are that model, and #dropped_parameter names what
|
|
11
|
+
# it left out.
|
|
9
12
|
#
|
|
10
13
|
# chat = RubyLLM.chat(model: "gpt-4.1")
|
|
11
14
|
# .with_fallbacks("gpt-4.1-mini", "claude-haiku-4-5")
|
|
@@ -27,11 +30,11 @@ module RubyLLM
|
|
|
27
30
|
ServerError,
|
|
28
31
|
ServiceUnavailableError,
|
|
29
32
|
OverloadedError,
|
|
30
|
-
|
|
31
|
-
Faraday::ConnectionFailed
|
|
33
|
+
ConnectionError
|
|
32
34
|
].freeze
|
|
33
35
|
|
|
34
|
-
# The model id of the fallback target
|
|
36
|
+
# The model id of the fallback target, or of the model a retry without a
|
|
37
|
+
# rejected parameter runs on.
|
|
35
38
|
attr_reader :id
|
|
36
39
|
|
|
37
40
|
# The provider slug of the fallback target as a String, or +nil+.
|
|
@@ -56,6 +59,11 @@ module RubyLLM
|
|
|
56
59
|
# The fallback attempt number, starting at 1.
|
|
57
60
|
attr_reader :attempt
|
|
58
61
|
|
|
62
|
+
# The parameter a retry on the same model left out, as
|
|
63
|
+
# UnsupportedParameterError#parameter names it, or +nil+ for a fallback
|
|
64
|
+
# to another model. See Chat#with_fallbacks.
|
|
65
|
+
attr_reader :dropped_parameter
|
|
66
|
+
|
|
59
67
|
# The response Message from a successful fallback attempt, or +nil+.
|
|
60
68
|
attr_reader :response
|
|
61
69
|
|
|
@@ -78,6 +86,25 @@ module RubyLLM
|
|
|
78
86
|
end
|
|
79
87
|
end
|
|
80
88
|
|
|
89
|
+
# Returns whether +error+ is one of +errors+ and may move on to a
|
|
90
|
+
# fallback. A cancellation never does.
|
|
91
|
+
def self.error?(error, errors) # :nodoc:
|
|
92
|
+
!error.is_a?(CancelledError) && errors.any? { |error_class| error.is_a?(error_class) }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Takes fallbacks off +queue+ until the block accepts one, skipping
|
|
96
|
+
# those whose model can't be resolved. Returns the fallback and the
|
|
97
|
+
# block's value, or +nil+ when none is left.
|
|
98
|
+
def self.next_usable(queue) # :nodoc:
|
|
99
|
+
while (fallback = queue.shift)
|
|
100
|
+
begin
|
|
101
|
+
return [fallback, yield(fallback)]
|
|
102
|
+
rescue ModelNotFoundError, ConfigurationError => e
|
|
103
|
+
RubyLLM.logger.warn("RubyLLM skipped fallback #{fallback.id}: #{e.message}")
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
81
108
|
def inspect_attributes # :nodoc:
|
|
82
109
|
{ id: id, provider: provider, attempt: attempt }
|
|
83
110
|
end
|
|
@@ -95,6 +122,7 @@ module RubyLLM
|
|
|
95
122
|
@chunks_yielded = attributes.fetch(:chunks_yielded, false)
|
|
96
123
|
@response = attributes[:response]
|
|
97
124
|
@fallback_error = attributes[:fallback_error]
|
|
125
|
+
@dropped_parameter = attributes[:dropped_parameter]
|
|
98
126
|
end
|
|
99
127
|
|
|
100
128
|
def with_attempt(**attributes) # :nodoc:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class FileStore
|
|
5
|
+
module Converters
|
|
6
|
+
# Reads scans, photos, and PDFs with RubyLLM.ocr. Each file costs one
|
|
7
|
+
# OCR request the first time the model reads or searches it, and is
|
|
8
|
+
# kept after that. Register it in Configuration#file_converters for
|
|
9
|
+
# the types it should read:
|
|
10
|
+
#
|
|
11
|
+
# config.file_converters["image/*"] = RubyLLM::FileStore::Converters::OCR.new
|
|
12
|
+
# config.file_converters["application/pdf"] = OCR.new(model: "mistral-ocr-latest")
|
|
13
|
+
#
|
|
14
|
+
class OCR
|
|
15
|
+
# Creates a converter that calls RubyLLM.ocr with +options+, such as
|
|
16
|
+
# +model:+, +provider:+, or +context:+.
|
|
17
|
+
def initialize(**options)
|
|
18
|
+
@options = options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the Markdown RubyLLM.ocr reads from +attachment+.
|
|
22
|
+
def call(attachment)
|
|
23
|
+
RubyLLM.ocr(attachment, **@options).markdown
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class FileStore
|
|
5
|
+
module Converters
|
|
6
|
+
module Office
|
|
7
|
+
# Walks the tags of an Office XML part from left to right, so its
|
|
8
|
+
# time grows with the part's size whatever the part holds: nothing
|
|
9
|
+
# backtracks over unclosed tags. Names lose their prefix, since
|
|
10
|
+
# generators pick their own. Parts are binary Strings.
|
|
11
|
+
class Markup # :nodoc:
|
|
12
|
+
ENTITIES = { 'amp' => '&', 'lt' => '<', 'gt' => '>', 'quot' => '"', 'apos' => "'" }.freeze
|
|
13
|
+
ENTITY = /&(?:#(\d+)|#x(\h+)|(amp|lt|gt|quot|apos));/
|
|
14
|
+
NAME = %r{\A(?:[^\s:/]+:)?([^\s/]+)}
|
|
15
|
+
ATTRIBUTES = Hash.new do |patterns, name|
|
|
16
|
+
patterns[name] = /\s#{Regexp.escape(name)}=(?:"([^"]*)"|'([^']*)')/
|
|
17
|
+
end
|
|
18
|
+
PREFIXED = Hash.new do |patterns, name|
|
|
19
|
+
patterns[name] = /\s(?:[\w.-]+:)?#{Regexp.escape(name)}=(?:"([^"]*)"|'([^']*)')/
|
|
20
|
+
end
|
|
21
|
+
RELATIONSHIP_ID = /\s[\w.-]+:id=(?:"([^"]*)"|'([^']*)')/
|
|
22
|
+
SLASH = 47
|
|
23
|
+
QUESTION = 63
|
|
24
|
+
BANG = 33
|
|
25
|
+
private_constant :ENTITIES, :ENTITY, :NAME, :ATTRIBUTES, :PREFIXED, :RELATIONSHIP_ID, :SLASH, :QUESTION, :BANG
|
|
26
|
+
|
|
27
|
+
class << self
|
|
28
|
+
# Yields <tt>:open</tt>, <tt>:empty</tt>, or <tt>:close</tt> with
|
|
29
|
+
# each tag's name and its source, and <tt>:text</tt> with nil and
|
|
30
|
+
# the text between tags, leaving out the elements named in +skip+.
|
|
31
|
+
def scan(xml, skip: [], &)
|
|
32
|
+
new(xml.to_s.b, skip, &).run
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns the source of each tag named +name+.
|
|
36
|
+
def tags(xml, name)
|
|
37
|
+
found = []
|
|
38
|
+
scan(xml) { |kind, tag_name, source| found << source if tag_name == name && kind != :close }
|
|
39
|
+
found
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns the value of an attribute in a tag's source, unescaped.
|
|
43
|
+
def attribute(tag, name)
|
|
44
|
+
match = ATTRIBUTES[name].match(tag.to_s)
|
|
45
|
+
unescape(match[1] || match[2]) if match
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Returns the value of an attribute under any prefix, or none.
|
|
49
|
+
def prefixed(tag, name)
|
|
50
|
+
match = PREFIXED[name].match(tag.to_s)
|
|
51
|
+
unescape(match[1] || match[2]) if match
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the prefixed +id+ attribute of a tag's source, whatever
|
|
55
|
+
# its prefix, which is how a part refers to a relationship.
|
|
56
|
+
def relationship_id(tag)
|
|
57
|
+
match = RELATIONSHIP_ID.match(tag.to_s)
|
|
58
|
+
unescape(match[1] || match[2]) if match
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns +text+ as UTF-8, with the entities XML defines replaced.
|
|
62
|
+
def unescape(text)
|
|
63
|
+
text = text.to_s.dup.force_encoding(Encoding::UTF_8).scrub
|
|
64
|
+
return text unless text.include?('&')
|
|
65
|
+
|
|
66
|
+
text.gsub(ENTITY) do
|
|
67
|
+
named = Regexp.last_match(3)
|
|
68
|
+
next ENTITIES.fetch(named) if named
|
|
69
|
+
|
|
70
|
+
character(Regexp.last_match(1)&.to_i || Regexp.last_match(2).to_i(16))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def character(code)
|
|
77
|
+
[code].pack('U').then { |text| text.valid_encoding? ? text : '' }
|
|
78
|
+
rescue RangeError
|
|
79
|
+
''
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def initialize(xml, skip, &block)
|
|
84
|
+
@xml = xml
|
|
85
|
+
@skip = skip
|
|
86
|
+
@block = block
|
|
87
|
+
@position = 0
|
|
88
|
+
@skipping = nil
|
|
89
|
+
@depth = 0
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def run
|
|
93
|
+
while (start = @xml.index('<', @position))
|
|
94
|
+
finish = @xml.index('>', start)
|
|
95
|
+
break unless finish
|
|
96
|
+
|
|
97
|
+
@block.call(:text, nil, @xml.byteslice(@position, start - @position)) if start > @position && !@skipping
|
|
98
|
+
@position = finish + 1
|
|
99
|
+
tag(start, finish)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def tag(start, finish)
|
|
106
|
+
first = @xml.getbyte(start + 1)
|
|
107
|
+
return if [QUESTION, BANG].include?(first)
|
|
108
|
+
|
|
109
|
+
if first == SLASH
|
|
110
|
+
closing(name_of(@xml.byteslice(start + 2, finish - start - 2)))
|
|
111
|
+
else
|
|
112
|
+
opening(@xml.byteslice(start + 1, finish - start - 1))
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def opening(source)
|
|
117
|
+
name = name_of(source)
|
|
118
|
+
empty = source.end_with?('/')
|
|
119
|
+
return count(name, empty) if @skipping
|
|
120
|
+
return start_skipping(name) if !empty && @skip.include?(name)
|
|
121
|
+
|
|
122
|
+
@block.call(empty ? :empty : :open, name, source)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def closing(name)
|
|
126
|
+
return @block.call(:close, name, nil) unless @skipping
|
|
127
|
+
|
|
128
|
+
@depth -= 1 if name == @skipping
|
|
129
|
+
@skipping = nil if @depth.zero?
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def count(name, empty)
|
|
133
|
+
@depth += 1 if name == @skipping && !empty
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def start_skipping(name)
|
|
137
|
+
@skipping = name
|
|
138
|
+
@depth = 1
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def name_of(source)
|
|
142
|
+
source[NAME, 1].to_s
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|