ruby_llm 2.0.0 → 2.1.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 +65 -8
- data/lib/generators/ruby_llm/generator_helpers.rb +68 -6
- data/lib/generators/ruby_llm/install/templates/create_messages_migration.rb.tt +1 -0
- data/lib/generators/ruby_llm/install/templates/create_ruby_llm_records_migration.rb.tt +36 -3
- data/lib/generators/ruby_llm/provider/scaffold.rb +32 -22
- data/lib/generators/ruby_llm/provider/templates/gem/ci.yml.erb +1 -5
- data/lib/generators/ruby_llm/provider/templates/gem/gemfile.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/gemspec.erb +1 -1
- data/lib/generators/ruby_llm/provider/templates/gem/rubocop.yml.erb +1 -1
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_ruby_llm_to_2_1.rb.tt +78 -0
- data/lib/generators/ruby_llm/upgrade/upgrade_generator.rb +6 -163
- data/lib/ruby_llm/accounting/usage.rb +86 -49
- data/lib/ruby_llm/active_record/acts_as.rb +3 -0
- data/lib/ruby_llm/active_record/attachment_helpers.rb +50 -25
- data/lib/ruby_llm/active_record/batch.rb +1 -1
- data/lib/ruby_llm/active_record/chat_methods.rb +136 -40
- data/lib/ruby_llm/active_record/mcp_credential.rb +41 -0
- data/lib/ruby_llm/active_record/message_methods.rb +32 -7
- data/lib/ruby_llm/active_record/model.rb +1 -1
- data/lib/ruby_llm/active_record/provider_file.rb +64 -0
- data/lib/ruby_llm/active_record/record.rb +19 -0
- data/lib/ruby_llm/active_record/tool_call.rb +1 -1
- data/lib/ruby_llm/active_record/usage.rb +53 -3
- data/lib/ruby_llm/agent.rb +198 -33
- data/lib/ruby_llm/aliases.json +66 -8
- data/lib/ruby_llm/attachment.rb +54 -10
- data/lib/ruby_llm/batch.rb +26 -3
- data/lib/ruby_llm/chat/tool_concurrency.rb +47 -23
- data/lib/ruby_llm/chat/tool_search.rb +26 -0
- data/lib/ruby_llm/chat.rb +488 -82
- data/lib/ruby_llm/choice.rb +38 -0
- data/lib/ruby_llm/configuration.rb +36 -11
- data/lib/ruby_llm/context.rb +45 -32
- data/lib/ruby_llm/cost.rb +42 -1
- data/lib/ruby_llm/embedding.rb +9 -2
- data/lib/ruby_llm/error.rb +34 -2
- data/lib/ruby_llm/evaluation/assertions.rb +40 -0
- data/lib/ruby_llm/evaluation/case.rb +52 -0
- data/lib/ruby_llm/evaluation/dataset.rb +65 -0
- data/lib/ruby_llm/evaluation/evaluator.rb +126 -0
- data/lib/ruby_llm/evaluation/evidence.rb +125 -0
- data/lib/ruby_llm/evaluation/minitest.rb +26 -0
- data/lib/ruby_llm/evaluation/report.rb +103 -0
- data/lib/ruby_llm/evaluation/result.rb +75 -0
- data/lib/ruby_llm/evaluation/reviewer.rb +20 -0
- data/lib/ruby_llm/evaluation/rspec.rb +23 -0
- data/lib/ruby_llm/evaluation/runner.rb +93 -0
- data/lib/ruby_llm/evaluation/trial.rb +101 -0
- data/lib/ruby_llm/evaluation.rb +289 -0
- data/lib/ruby_llm/image.rb +12 -4
- data/lib/ruby_llm/judge/builder.rb +51 -0
- data/lib/ruby_llm/judge/data.rb +43 -0
- data/lib/ruby_llm/judge/question.rb +101 -0
- data/lib/ruby_llm/judge.rb +179 -0
- data/lib/ruby_llm/judgment.rb +125 -0
- data/lib/ruby_llm/mcp/apps.rb +25 -0
- data/lib/ruby_llm/mcp/client.rb +213 -0
- data/lib/ruby_llm/mcp/collection.rb +56 -0
- data/lib/ruby_llm/mcp/content.rb +44 -0
- data/lib/ruby_llm/mcp/error.rb +27 -0
- data/lib/ruby_llm/mcp/http.rb +379 -0
- data/lib/ruby_llm/mcp/input_request.rb +118 -0
- data/lib/ruby_llm/mcp/input_required_error.rb +29 -0
- data/lib/ruby_llm/mcp/listener.rb +168 -0
- data/lib/ruby_llm/mcp/oauth/key.rb +78 -0
- data/lib/ruby_llm/mcp/oauth/memory_store.rb +28 -0
- data/lib/ruby_llm/mcp/oauth/proofs.rb +39 -0
- data/lib/ruby_llm/mcp/oauth.rb +653 -0
- data/lib/ruby_llm/mcp/param_headers.rb +83 -0
- data/lib/ruby_llm/mcp/prompt.rb +71 -0
- data/lib/ruby_llm/mcp/resource.rb +84 -0
- data/lib/ruby_llm/mcp/resource_template.rb +99 -0
- data/lib/ruby_llm/mcp/result.rb +76 -0
- data/lib/ruby_llm/mcp/session_expired.rb +10 -0
- data/lib/ruby_llm/mcp/stdio.rb +250 -0
- data/lib/ruby_llm/mcp/task.rb +151 -0
- data/lib/ruby_llm/mcp/tool.rb +130 -0
- data/lib/ruby_llm/mcp.rb +1101 -0
- data/lib/ruby_llm/message.rb +41 -6
- data/lib/ruby_llm/model.rb +2 -1
- data/lib/ruby_llm/models/aliases.rb +5 -2
- data/lib/ruby_llm/models/lookup.rb +80 -0
- data/lib/ruby_llm/models/registry.rb +2 -2
- data/lib/ruby_llm/models/schema.rb +1 -1
- data/lib/ruby_llm/models.json +23082 -10158
- data/lib/ruby_llm/models.rb +20 -69
- data/lib/ruby_llm/moderation.rb +7 -2
- data/lib/ruby_llm/ocr.rb +7 -2
- data/lib/ruby_llm/open_telemetry/attributes.rb +111 -0
- data/lib/ruby_llm/open_telemetry.rb +82 -0
- data/lib/ruby_llm/probability.rb +30 -0
- data/lib/ruby_llm/progress.rb +44 -0
- data/lib/ruby_llm/prompt/context.rb +55 -0
- data/lib/ruby_llm/prompt.rb +1 -1
- data/lib/ruby_llm/protocol/binary_streaming.rb +1 -1
- data/lib/ruby_llm/protocol/request_shapes/analysis.rb +190 -0
- data/lib/ruby_llm/protocol/request_shapes.rb +183 -0
- data/lib/ruby_llm/protocol/stored_uploads.rb +62 -0
- data/lib/ruby_llm/protocol/stream_accumulator.rb +9 -25
- data/lib/ruby_llm/protocol/streaming.rb +38 -31
- data/lib/ruby_llm/protocol.rb +192 -40
- data/lib/ruby_llm/protocols/anthropic/chat.rb +39 -32
- data/lib/ruby_llm/protocols/anthropic/media.rb +6 -2
- data/lib/ruby_llm/protocols/anthropic/models.rb +16 -1
- data/lib/ruby_llm/protocols/anthropic/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/anthropic/streaming.rb +23 -9
- data/lib/ruby_llm/protocols/anthropic/tools.rb +43 -0
- data/lib/ruby_llm/protocols/anthropic.rb +20 -1
- data/lib/ruby_llm/protocols/bedrock/async_videos.rb +9 -17
- data/lib/ruby_llm/protocols/bedrock/guardrails.rb +8 -9
- data/lib/ruby_llm/protocols/bedrock/rerank.rb +1 -17
- data/lib/ruby_llm/protocols/chat_completions/chat.rb +5 -7
- data/lib/ruby_llm/protocols/chat_completions/media.rb +34 -8
- data/lib/ruby_llm/protocols/chat_completions/request_shapes.rb +121 -0
- data/lib/ruby_llm/protocols/chat_completions/streaming.rb +3 -0
- data/lib/ruby_llm/protocols/chat_completions.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/batch_requests.rb +0 -5
- data/lib/ruby_llm/protocols/cohere/batches.rb +3 -7
- data/lib/ruby_llm/protocols/cohere/chat.rb +8 -0
- data/lib/ruby_llm/protocols/cohere/embeddings.rb +7 -4
- data/lib/ruby_llm/protocols/cohere/media.rb +6 -2
- data/lib/ruby_llm/protocols/cohere/ocr.rb +1 -1
- data/lib/ruby_llm/protocols/cohere/streaming.rb +1 -0
- data/lib/ruby_llm/protocols/cohere/tools.rb +5 -1
- data/lib/ruby_llm/protocols/cohere.rb +1 -0
- data/lib/ruby_llm/protocols/converse/chat.rb +22 -131
- data/lib/ruby_llm/protocols/converse/media.rb +16 -2
- data/lib/ruby_llm/protocols/converse/reasoning.rb +194 -0
- data/lib/ruby_llm/protocols/converse/request_shapes.rb +99 -0
- data/lib/ruby_llm/protocols/converse/streaming.rb +2 -2
- data/lib/ruby_llm/protocols/converse/thinking_stream.rb +2 -1
- data/lib/ruby_llm/protocols/converse.rb +3 -5
- data/lib/ruby_llm/protocols/deepseek/files.rb +2 -9
- data/lib/ruby_llm/protocols/elevenlabs/flows/images.rb +1 -7
- data/lib/ruby_llm/protocols/elevenlabs/flows/videos.rb +11 -17
- data/lib/ruby_llm/protocols/elevenlabs/streaming_transcription.rb +1 -4
- data/lib/ruby_llm/protocols/gemini/chat.rb +50 -21
- data/lib/ruby_llm/protocols/gemini/embeddings.rb +2 -2
- data/lib/ruby_llm/protocols/gemini/files.rb +1 -1
- data/lib/ruby_llm/protocols/gemini/images.rb +5 -1
- data/lib/ruby_llm/protocols/gemini/media.rb +15 -3
- data/lib/ruby_llm/protocols/gemini/request_shapes.rb +111 -0
- data/lib/ruby_llm/protocols/gemini/streaming.rb +2 -1
- data/lib/ruby_llm/protocols/gemini/tools.rb +17 -11
- data/lib/ruby_llm/protocols/gemini/transcription.rb +13 -0
- data/lib/ruby_llm/protocols/gemini/videos.rb +7 -2
- data/lib/ruby_llm/protocols/gemini.rb +1 -0
- data/lib/ruby_llm/protocols/gpustack/videos.rb +7 -9
- data/lib/ruby_llm/protocols/interactions/chat.rb +46 -18
- data/lib/ruby_llm/protocols/interactions/content.rb +9 -2
- data/lib/ruby_llm/protocols/interactions/request_shapes.rb +62 -0
- data/lib/ruby_llm/protocols/interactions/streaming.rb +15 -0
- data/lib/ruby_llm/protocols/interactions/tools.rb +28 -4
- data/lib/ruby_llm/protocols/interactions/transcription.rb +1 -11
- data/lib/ruby_llm/protocols/interactions.rb +1 -0
- data/lib/ruby_llm/protocols/invoke_model/cohere_embeddings.rb +2 -11
- data/lib/ruby_llm/protocols/invoke_model/embedding_batches.rb +2 -2
- data/lib/ruby_llm/protocols/invoke_model/stability_images.rb +2 -6
- data/lib/ruby_llm/protocols/invoke_model/titan_text_embeddings.rb +2 -8
- data/lib/ruby_llm/protocols/invoke_model.rb +1 -4
- data/lib/ruby_llm/protocols/mistral/content.rb +13 -0
- data/lib/ruby_llm/protocols/mistral/conversations/chat.rb +11 -1
- data/lib/ruby_llm/protocols/mistral/conversations/images.rb +5 -1
- data/lib/ruby_llm/protocols/mistral/conversations/request_shapes.rb +45 -0
- data/lib/ruby_llm/protocols/mistral/conversations.rb +1 -0
- data/lib/ruby_llm/protocols/mistral/multi_completion.rb +11 -1
- data/lib/ruby_llm/protocols/openai/decisions.rb +135 -0
- data/lib/ruby_llm/protocols/openai/files.rb +0 -7
- data/lib/ruby_llm/protocols/openrouter/batches.rb +13 -8
- data/lib/ruby_llm/protocols/openrouter/responses.rb +5 -1
- data/lib/ruby_llm/protocols/perplexity/agent.rb +116 -0
- data/lib/ruby_llm/protocols/perplexity/router.rb +2 -43
- data/lib/ruby_llm/protocols/responses/chat.rb +56 -22
- data/lib/ruby_llm/protocols/responses/media.rb +11 -7
- data/lib/ruby_llm/protocols/responses/request_shapes.rb +70 -0
- data/lib/ruby_llm/protocols/responses/streaming.rb +23 -7
- data/lib/ruby_llm/protocols/responses/token_counting.rb +2 -2
- data/lib/ruby_llm/protocols/responses/tools.rb +27 -0
- data/lib/ruby_llm/protocols/responses.rb +1 -0
- data/lib/ruby_llm/protocols/system_one/judgments.rb +42 -0
- data/lib/ruby_llm/protocols/system_one/models.rb +27 -0
- data/lib/ruby_llm/protocols/system_one/responses.rb +55 -0
- data/lib/ruby_llm/protocols/system_one.rb +27 -0
- data/lib/ruby_llm/protocols/vertexai/embedding_prediction/requests.rb +2 -2
- data/lib/ruby_llm/protocols/vertexai/files.rb +34 -6
- data/lib/ruby_llm/protocols/vertexai/ranking.rb +9 -30
- data/lib/ruby_llm/protocols/vertexai/research.rb +9 -41
- data/lib/ruby_llm/protocols/xai/streaming_transcription.rb +2 -5
- data/lib/ruby_llm/provider.rb +87 -15
- data/lib/ruby_llm/providers/anthropic.rb +23 -0
- data/lib/ruby_llm/providers/azure/chat.rb +4 -0
- data/lib/ruby_llm/providers/azure/images.rb +2 -2
- data/lib/ruby_llm/providers/azure/media.rb +8 -3
- data/lib/ruby_llm/providers/azure/videos.rb +7 -2
- data/lib/ruby_llm/providers/azure.rb +24 -4
- data/lib/ruby_llm/providers/bedrock/auth.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/anthropic.rb +1 -1
- data/lib/ruby_llm/providers/bedrock/mantle/voxtral.rb +1 -3
- data/lib/ruby_llm/providers/bedrock/mantle.rb +5 -9
- data/lib/ruby_llm/providers/bedrock.rb +38 -6
- data/lib/ruby_llm/providers/deepseek/chat.rb +6 -1
- data/lib/ruby_llm/providers/deepseek/responses.rb +6 -0
- data/lib/ruby_llm/providers/deepseek.rb +4 -0
- data/lib/ruby_llm/providers/gemini.rb +4 -0
- data/lib/ruby_llm/providers/gpustack/media.rb +8 -5
- data/lib/ruby_llm/providers/hetzner/chat.rb +39 -0
- data/lib/ruby_llm/providers/hetzner.rb +41 -0
- data/lib/ruby_llm/providers/mistral/chat.rb +16 -12
- data/lib/ruby_llm/providers/mistral/media.rb +8 -4
- data/lib/ruby_llm/providers/mistral/ocr.rb +1 -5
- data/lib/ruby_llm/providers/ollama/judgments.rb +26 -0
- data/lib/ruby_llm/providers/ollama/media.rb +6 -2
- data/lib/ruby_llm/providers/ollama/models.rb +2 -0
- data/lib/ruby_llm/providers/ollama.rb +11 -0
- data/lib/ruby_llm/providers/openai/capabilities.rb +3 -1
- data/lib/ruby_llm/providers/openai.rb +14 -0
- data/lib/ruby_llm/providers/openrouter/chat.rb +20 -24
- data/lib/ruby_llm/providers/openrouter/embeddings.rb +2 -0
- data/lib/ruby_llm/providers/openrouter/media.rb +5 -1
- data/lib/ruby_llm/providers/openrouter/streaming.rb +4 -1
- data/lib/ruby_llm/providers/openrouter/videos.rb +10 -5
- data/lib/ruby_llm/providers/openrouter.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/chat.rb +4 -0
- data/lib/ruby_llm/providers/perplexity/media.rb +5 -11
- data/lib/ruby_llm/providers/perplexity/models.rb +8 -4
- data/lib/ruby_llm/providers/perplexity.rb +11 -0
- data/lib/ruby_llm/providers/typesafe.rb +32 -0
- data/lib/ruby_llm/providers/vertexai/credentials.rb +63 -0
- data/lib/ruby_llm/providers/vertexai/gemini.rb +2 -2
- data/lib/ruby_llm/providers/vertexai/transcription.rb +1 -0
- data/lib/ruby_llm/providers/vertexai.rb +27 -28
- data/lib/ruby_llm/providers/xai/chat.rb +6 -0
- data/lib/ruby_llm/providers/xai/images.rb +9 -10
- data/lib/ruby_llm/providers/xai/responses.rb +10 -8
- data/lib/ruby_llm/providers/xai/videos.rb +11 -9
- data/lib/ruby_llm/providers/xai.rb +4 -0
- data/lib/ruby_llm/railtie.rb +11 -0
- data/lib/ruby_llm/request_shape/part.rb +117 -0
- data/lib/ruby_llm/request_shape/problem.rb +63 -0
- data/lib/ruby_llm/request_shape/tool_round.rb +64 -0
- data/lib/ruby_llm/request_shape/turn.rb +54 -0
- data/lib/ruby_llm/request_shape.rb +160 -0
- data/lib/ruby_llm/rerank.rb +9 -2
- data/lib/ruby_llm/research_job.rb +21 -5
- data/lib/ruby_llm/score.rb +42 -0
- data/lib/ruby_llm/search_results.rb +2 -2
- data/lib/ruby_llm/server_tool_call.rb +10 -2
- data/lib/ruby_llm/speech.rb +8 -1
- data/lib/ruby_llm/support/cancellation.rb +39 -0
- data/lib/ruby_llm/support/inspectable.rb +3 -2
- data/lib/ruby_llm/support/instrumentation.rb +38 -1
- data/lib/ruby_llm/support/process_cache.rb +60 -0
- data/lib/ruby_llm/support/progress_reporter.rb +33 -0
- data/lib/ruby_llm/thinking.rb +2 -1
- data/lib/ruby_llm/tokens.rb +57 -14
- data/lib/ruby_llm/tool.rb +50 -3
- data/lib/ruby_llm/tool_call.rb +17 -2
- data/lib/ruby_llm/transcription.rb +10 -3
- data/lib/ruby_llm/transport/connection.rb +127 -44
- data/lib/ruby_llm/transport/error_middleware.rb +90 -24
- data/lib/ruby_llm/transport/event_stream_parser.rb +121 -0
- data/lib/ruby_llm/transport/json_response.rb +33 -0
- data/lib/ruby_llm/transport/signing_middleware.rb +29 -0
- data/lib/ruby_llm/version.rb +1 -1
- data/lib/ruby_llm/video.rb +5 -3
- data/lib/ruby_llm/video_job.rb +48 -5
- data/lib/ruby_llm.rb +106 -5
- data/lib/tasks/ruby_llm/evaluations.rb +43 -0
- data/lib/tasks/ruby_llm.rake +6 -9
- data/skills/rubyllm/SKILL.md +33 -12
- metadata +95 -37
- data/lib/generators/ruby_llm/upgrade/legacy_content_sql.rb +0 -34
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/data.rb +0 -341
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/journal.rb +0 -171
- data/lib/generators/ruby_llm/upgrade/online_copy_migration/verification.rb +0 -197
- data/lib/generators/ruby_llm/upgrade/online_copy_migration.rb +0 -174
- data/lib/generators/ruby_llm/upgrade/templates/backfill_v2_data.rb.tt +0 -468
- data/lib/generators/ruby_llm/upgrade/templates/cleanup_v2_upgrade.rb.tt +0 -101
- data/lib/generators/ruby_llm/upgrade/templates/finish_v2_upgrade.rb.tt +0 -247
- data/lib/generators/ruby_llm/upgrade/templates/prepare_v2_upgrade.rb.tt +0 -662
- data/lib/generators/ruby_llm/upgrade/templates/ruby_llm_upgrade.rb.tt +0 -251
- data/lib/generators/ruby_llm/upgrade/templates/upgrade_initializer.rb.tt +0 -13
- data/lib/generators/ruby_llm/upgrade/upgrade_migration.rb +0 -359
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# A selected judgment option and its full probability distribution.
|
|
5
|
+
class Choice
|
|
6
|
+
include Support::Inspectable
|
|
7
|
+
|
|
8
|
+
# Returns the selected option, preserving its declared String or Symbol type.
|
|
9
|
+
attr_reader :choice
|
|
10
|
+
|
|
11
|
+
# Returns each declared option's probability.
|
|
12
|
+
attr_reader :probabilities
|
|
13
|
+
|
|
14
|
+
# Returns the provider's confidence in the distribution, between 0 and 1.
|
|
15
|
+
attr_reader :confidence
|
|
16
|
+
|
|
17
|
+
def initialize(choice:, probabilities:, confidence:) # :nodoc:
|
|
18
|
+
@choice = choice.is_a?(String) ? choice.dup.freeze : choice
|
|
19
|
+
@probabilities = probabilities.dup.freeze
|
|
20
|
+
@confidence = confidence
|
|
21
|
+
freeze
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns :choice.
|
|
25
|
+
def type
|
|
26
|
+
:choice
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the answer as a Hash.
|
|
30
|
+
def to_h
|
|
31
|
+
{ type:, choice:, probabilities:, confidence: }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def inspect_attributes # :nodoc:
|
|
35
|
+
{ choice:, confidence: }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -82,6 +82,13 @@ module RubyLLM
|
|
|
82
82
|
# Default: <tt>'omni-moderation-latest'</tt>.
|
|
83
83
|
option :default_moderation_model, 'omni-moderation-latest'
|
|
84
84
|
|
|
85
|
+
##
|
|
86
|
+
# :attr_accessor: default_judgment_model
|
|
87
|
+
#
|
|
88
|
+
# The model id used by RubyLLM.judge and Judge when no model is given.
|
|
89
|
+
# Default: <tt>'jev-latest'</tt>.
|
|
90
|
+
option :default_judgment_model, 'jev-latest'
|
|
91
|
+
|
|
85
92
|
##
|
|
86
93
|
# :attr_accessor: default_image_model
|
|
87
94
|
#
|
|
@@ -152,6 +159,33 @@ module RubyLLM
|
|
|
152
159
|
# Optional persistence adapter installed by the Rails integration.
|
|
153
160
|
option :batch_store, nil # :nodoc:
|
|
154
161
|
|
|
162
|
+
##
|
|
163
|
+
# :attr_accessor: mcp_credential_store
|
|
164
|
+
#
|
|
165
|
+
# Store for the OAuth credentials of MCP servers. Rails apps keep them
|
|
166
|
+
# in the +ruby_llm_mcp_credentials+ table automatically. A store
|
|
167
|
+
# responds to +read(key)+, +write(key, data, owner:)+, and
|
|
168
|
+
# +delete(key)+. A store that several processes share also responds
|
|
169
|
+
# to +synchronize(key)+, running the block while no other process
|
|
170
|
+
# holds +key+, so only one of them refreshes a token at a time.
|
|
171
|
+
# Default: +nil+ (keep them in memory).
|
|
172
|
+
option :mcp_credential_store, nil
|
|
173
|
+
|
|
174
|
+
##
|
|
175
|
+
# :attr_accessor: mcp_client_name
|
|
176
|
+
#
|
|
177
|
+
# The client name RubyLLM gives when it registers with an MCP server's
|
|
178
|
+
# authorization server. Default: <tt>"RubyLLM"</tt>.
|
|
179
|
+
option :mcp_client_name, 'RubyLLM'
|
|
180
|
+
|
|
181
|
+
##
|
|
182
|
+
# :attr_accessor: mcp_client_id
|
|
183
|
+
#
|
|
184
|
+
# The HTTPS URL of your OAuth client metadata document. Authorization
|
|
185
|
+
# servers that support client ID metadata documents use it instead of
|
|
186
|
+
# registering RubyLLM. Default: +nil+.
|
|
187
|
+
option :mcp_client_id, nil
|
|
188
|
+
|
|
155
189
|
# Keeps custom 1.x initializers bootable long enough to run the 2.0
|
|
156
190
|
# upgrade generator. The registry is always RubyLLM-owned in 2.0.
|
|
157
191
|
def model_registry_class=(_value) # :nodoc:
|
|
@@ -288,9 +322,8 @@ module RubyLLM
|
|
|
288
322
|
#
|
|
289
323
|
# Timeout in seconds for the regular expressions that scrub logged
|
|
290
324
|
# payloads. Defaults to the global <tt>Regexp.timeout</tt>, or 1.0
|
|
291
|
-
# when none is set.
|
|
292
|
-
|
|
293
|
-
option :log_regexp_timeout, -> { Regexp.respond_to?(:timeout) ? (Regexp.timeout || 1.0) : nil }
|
|
325
|
+
# when none is set.
|
|
326
|
+
option :log_regexp_timeout, -> { Regexp.timeout || 1.0 }
|
|
294
327
|
|
|
295
328
|
def initialize # :nodoc:
|
|
296
329
|
self.class.send(:defaults).each do |key, default|
|
|
@@ -311,14 +344,6 @@ module RubyLLM
|
|
|
311
344
|
"#<#{self.class.name} #{attributes.join(', ')}>"
|
|
312
345
|
end
|
|
313
346
|
|
|
314
|
-
remove_method :log_regexp_timeout=
|
|
315
|
-
def log_regexp_timeout=(value) # :nodoc:
|
|
316
|
-
if value && !Regexp.respond_to?(:timeout)
|
|
317
|
-
RubyLLM.logger.warn("log_regexp_timeout is not supported on Ruby #{RUBY_VERSION}")
|
|
318
|
-
end
|
|
319
|
-
@log_regexp_timeout = value
|
|
320
|
-
end
|
|
321
|
-
|
|
322
347
|
private
|
|
323
348
|
|
|
324
349
|
# Whether the +name+ environment variable is set to something that means
|
data/lib/ruby_llm/context.rb
CHANGED
|
@@ -35,8 +35,8 @@ module RubyLLM
|
|
|
35
35
|
|
|
36
36
|
# Creates a new Chat that uses this context's configuration.
|
|
37
37
|
# Accepts the same arguments as RubyLLM.chat.
|
|
38
|
-
def chat(
|
|
39
|
-
Chat.new(
|
|
38
|
+
def chat(*, **, &)
|
|
39
|
+
Chat.new(*, **, context: self, &)
|
|
40
40
|
end
|
|
41
41
|
|
|
42
42
|
# Counts tokens using this context's configuration.
|
|
@@ -47,8 +47,8 @@ module RubyLLM
|
|
|
47
47
|
|
|
48
48
|
# Tokenizes plain text using this context's configuration.
|
|
49
49
|
# Accepts the same arguments as RubyLLM.tokenize.
|
|
50
|
-
def tokenize(
|
|
51
|
-
Tokenization.tokenize(
|
|
50
|
+
def tokenize(*, **)
|
|
51
|
+
Tokenization.tokenize(*, **, context: self)
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Runs a named workflow using this context's instrumenter. Accepts the same
|
|
@@ -59,8 +59,8 @@ module RubyLLM
|
|
|
59
59
|
|
|
60
60
|
# Generates embeddings using this context's configuration.
|
|
61
61
|
# Accepts the same arguments as RubyLLM.embed.
|
|
62
|
-
def embed(
|
|
63
|
-
Embedding.embed(
|
|
62
|
+
def embed(*, **, &)
|
|
63
|
+
Embedding.embed(*, **, context: self, &)
|
|
64
64
|
end
|
|
65
65
|
|
|
66
66
|
# Stages an embedding request using this context's configuration.
|
|
@@ -71,81 +71,94 @@ module RubyLLM
|
|
|
71
71
|
|
|
72
72
|
# Generates an image using this context's configuration.
|
|
73
73
|
# Accepts the same arguments as RubyLLM.paint.
|
|
74
|
-
def paint(
|
|
75
|
-
Image.paint(
|
|
74
|
+
def paint(*, **, &)
|
|
75
|
+
Image.paint(*, **, context: self, &)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
78
|
# Generates a video using this context's configuration, blocking
|
|
79
79
|
# until it is ready. Accepts the same arguments as RubyLLM.animate.
|
|
80
|
-
def animate(
|
|
81
|
-
Video.animate(
|
|
80
|
+
def animate(*, **, &)
|
|
81
|
+
Video.animate(*, **, context: self, &)
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
# Submits a video generation job using this context's configuration.
|
|
85
85
|
# Accepts the same arguments as RubyLLM.animate_later.
|
|
86
|
-
def animate_later(
|
|
87
|
-
VideoJob.animate_later(
|
|
86
|
+
def animate_later(*, **, &)
|
|
87
|
+
VideoJob.animate_later(*, **, context: self, &)
|
|
88
88
|
end
|
|
89
89
|
|
|
90
90
|
# Runs content moderation using this context's configuration.
|
|
91
91
|
# Accepts the same arguments as RubyLLM.moderate.
|
|
92
|
-
def moderate(
|
|
93
|
-
Moderation.moderate(
|
|
92
|
+
def moderate(*, **, &)
|
|
93
|
+
Moderation.moderate(*, **, context: self, &)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Judges text, structured data, or images using this context's configuration.
|
|
97
|
+
# Accepts the same arguments as RubyLLM.judge.
|
|
98
|
+
def judge(*, **, &)
|
|
99
|
+
Judge.judge(*, **, context: self, &)
|
|
94
100
|
end
|
|
95
101
|
|
|
96
102
|
# Runs hosted research using this context's configuration.
|
|
97
103
|
# Accepts the same arguments as RubyLLM.research.
|
|
98
|
-
def research(
|
|
99
|
-
ResearchJob.research(
|
|
104
|
+
def research(*, **)
|
|
105
|
+
ResearchJob.research(*, **, context: self)
|
|
100
106
|
end
|
|
101
107
|
|
|
102
108
|
# Submits hosted research using this context's configuration.
|
|
103
109
|
# Accepts the same arguments as RubyLLM.research_later.
|
|
104
|
-
def research_later(
|
|
105
|
-
ResearchJob.research_later(
|
|
110
|
+
def research_later(*, **)
|
|
111
|
+
ResearchJob.research_later(*, **, context: self)
|
|
106
112
|
end
|
|
107
113
|
|
|
108
114
|
# Generates speech audio using this context's configuration. Given a
|
|
109
115
|
# block, yields SpeechChunk objects and returns the complete Speech.
|
|
110
116
|
# Accepts the same arguments as RubyLLM.speak.
|
|
111
|
-
def speak(
|
|
112
|
-
Speech.speak(
|
|
117
|
+
def speak(*, **, &)
|
|
118
|
+
Speech.speak(*, **, context: self, &)
|
|
113
119
|
end
|
|
114
120
|
|
|
115
121
|
# Transcribes audio using this context's configuration.
|
|
116
122
|
# Accepts the same arguments as RubyLLM.transcribe.
|
|
117
|
-
def transcribe(
|
|
118
|
-
Transcription.transcribe(
|
|
123
|
+
def transcribe(*, **, &)
|
|
124
|
+
Transcription.transcribe(*, **, context: self, &)
|
|
119
125
|
end
|
|
120
126
|
|
|
121
127
|
# Extracts document text using this context's configuration.
|
|
122
128
|
# Accepts the same arguments as RubyLLM.ocr.
|
|
123
|
-
def ocr(
|
|
124
|
-
OCR.ocr(
|
|
129
|
+
def ocr(*, **, &)
|
|
130
|
+
OCR.ocr(*, **, context: self, &)
|
|
125
131
|
end
|
|
126
132
|
|
|
127
133
|
# Ranks documents using this context's configuration.
|
|
128
134
|
# Accepts the same arguments as RubyLLM.rerank.
|
|
129
|
-
def rerank(
|
|
130
|
-
Rerank.rerank(
|
|
135
|
+
def rerank(*, **, &)
|
|
136
|
+
Rerank.rerank(*, **, context: self, &)
|
|
131
137
|
end
|
|
132
138
|
|
|
133
139
|
# Uploads a file to a provider using this context's configuration.
|
|
134
140
|
# Accepts the same arguments as RubyLLM.upload.
|
|
135
|
-
def upload(
|
|
136
|
-
UploadedFile.upload(
|
|
141
|
+
def upload(*, **, &)
|
|
142
|
+
UploadedFile.upload(*, **, context: self, &)
|
|
137
143
|
end
|
|
138
144
|
|
|
139
145
|
# Downloads a provider-hosted file using this context's configuration.
|
|
140
146
|
# Accepts the same arguments as RubyLLM.download.
|
|
141
|
-
def download(
|
|
142
|
-
UploadedFile.download(
|
|
147
|
+
def download(*, **, &)
|
|
148
|
+
UploadedFile.download(*, **, context: self, &)
|
|
143
149
|
end
|
|
144
150
|
|
|
145
151
|
# Creates a provider-side prompt cache using this context's
|
|
146
152
|
# configuration. Accepts the same arguments as RubyLLM.cache.
|
|
147
|
-
def cache(
|
|
148
|
-
CachedContent.create(
|
|
153
|
+
def cache(*, **, &)
|
|
154
|
+
CachedContent.create(*, **, context: self, &)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Connects to an MCP server using this context's configuration, so its
|
|
158
|
+
# connection settings apply to the server's requests and to its OAuth
|
|
159
|
+
# requests. Accepts the same arguments as RubyLLM.mcp.
|
|
160
|
+
def mcp(...)
|
|
161
|
+
MCP.define(...).new(context: self)
|
|
149
162
|
end
|
|
150
163
|
end
|
|
151
164
|
end
|
data/lib/ruby_llm/cost.rb
CHANGED
|
@@ -130,7 +130,9 @@ module RubyLLM
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
# Returns the cost of cache-write input tokens in US dollars, or +nil+
|
|
133
|
-
# when the token count or its pricing is unavailable.
|
|
133
|
+
# when the token count or its pricing is unavailable. Writes with a
|
|
134
|
+
# longer lifetime, reported in Tokens#cache_write_by_ttl, are priced at
|
|
135
|
+
# the provider's rate for that lifetime.
|
|
134
136
|
def cache_write
|
|
135
137
|
@amounts[:cache_write]
|
|
136
138
|
end
|
|
@@ -199,6 +201,7 @@ module RubyLLM
|
|
|
199
201
|
|
|
200
202
|
def missing_component?(component)
|
|
201
203
|
return image_input_missing? if component == :input && detailed_image_input?
|
|
204
|
+
return cache_write_missing? if component == :cache_write && repriced_cache_writes?
|
|
202
205
|
return false if component == :thinking && !thinking_priced_separately?
|
|
203
206
|
|
|
204
207
|
tokens_for(component).to_i.positive? && price_for(component).nil?
|
|
@@ -206,6 +209,7 @@ module RubyLLM
|
|
|
206
209
|
|
|
207
210
|
def amount_for(component)
|
|
208
211
|
return image_input_amount if component == :input && detailed_image_input?
|
|
212
|
+
return cache_write_amount if component == :cache_write && repriced_cache_writes?
|
|
209
213
|
|
|
210
214
|
token_count = tokens_for(component)
|
|
211
215
|
return nil if token_count.nil?
|
|
@@ -299,6 +303,43 @@ module RubyLLM
|
|
|
299
303
|
pricing.public_send(@category)
|
|
300
304
|
end
|
|
301
305
|
|
|
306
|
+
def repriced_cache_writes?
|
|
307
|
+
cache_write_multipliers.any?
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def cache_write_amount
|
|
311
|
+
cache_write_parts.sum { |count, price| count * price / PER_MILLION } unless cache_write_missing?
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def cache_write_missing?
|
|
315
|
+
cache_write_parts.any? { |_, price| price.nil? }
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Cache writes as [count, price] pairs: lifetimes the provider prices
|
|
319
|
+
# from the input price, then the rest at the registry's cache-write price.
|
|
320
|
+
def cache_write_parts
|
|
321
|
+
parts = cache_write_multipliers.map do |ttl, multiplier|
|
|
322
|
+
[cache_write_by_ttl[ttl], input_price && (input_price * multiplier)]
|
|
323
|
+
end
|
|
324
|
+
standard = @tokens.cache_write.to_i - parts.sum(&:first)
|
|
325
|
+
parts << [standard, price_for(:cache_write)] if standard.positive?
|
|
326
|
+
parts
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def cache_write_multipliers
|
|
330
|
+
@cache_write_multipliers ||= begin
|
|
331
|
+
provider = @model.provider_class if @model.respond_to?(:provider_class)
|
|
332
|
+
cache_write_by_ttl.filter_map do |ttl, _|
|
|
333
|
+
multiplier = provider&.cache_write_input_multiplier(@model.id, ttl)
|
|
334
|
+
[ttl, multiplier] if multiplier
|
|
335
|
+
end.to_h
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def cache_write_by_ttl
|
|
340
|
+
(@tokens.cache_write_by_ttl if @tokens.respond_to?(:cache_write_by_ttl)) || {}
|
|
341
|
+
end
|
|
342
|
+
|
|
302
343
|
def image_cost?
|
|
303
344
|
%i[image images].include?(@category)
|
|
304
345
|
end
|
data/lib/ruby_llm/embedding.rb
CHANGED
|
@@ -54,6 +54,8 @@ module RubyLLM
|
|
|
54
54
|
Cost.new(tokens:, model: model_info, category: :embeddings)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
attr_writer :model_info # :nodoc:
|
|
58
|
+
|
|
57
59
|
def model_info # :nodoc:
|
|
58
60
|
@model_info ||= RubyLLM.models.find(model)
|
|
59
61
|
rescue ModelNotFoundError
|
|
@@ -97,6 +99,8 @@ module RubyLLM
|
|
|
97
99
|
# in the provider's request vocabulary and merges them into the
|
|
98
100
|
# request as-is. +metadata:+ is not sent to the provider; it is
|
|
99
101
|
# attached to the emitted +embedding.ruby_llm+ instrumentation event.
|
|
102
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
103
|
+
# over RubyLLM.with_usage_owner.
|
|
100
104
|
def self.embed(text,
|
|
101
105
|
model: nil,
|
|
102
106
|
provider: nil,
|
|
@@ -107,7 +111,8 @@ module RubyLLM
|
|
|
107
111
|
title: nil,
|
|
108
112
|
with: nil,
|
|
109
113
|
provider_options: {},
|
|
110
|
-
metadata: nil
|
|
114
|
+
metadata: nil,
|
|
115
|
+
owner: nil)
|
|
111
116
|
config = context&.config || RubyLLM.config
|
|
112
117
|
model ||= config.default_embedding_model
|
|
113
118
|
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
@@ -132,7 +137,9 @@ module RubyLLM
|
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
RubyLLM.instrument('embedding.ruby_llm', payload, config: config) do |event|
|
|
135
|
-
result =
|
|
140
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
141
|
+
provider_instance.embed(text, model:, dimensions:, task_type:, title:, with:, provider_options:)
|
|
142
|
+
end
|
|
136
143
|
event[:result] = result
|
|
137
144
|
event[:response_model] = result.model
|
|
138
145
|
event[:tokens] = result.tokens
|
data/lib/ruby_llm/error.rb
CHANGED
|
@@ -4,8 +4,9 @@ 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
|
-
#
|
|
7
|
+
# normalizes the message across providers. When the provider refuses a
|
|
8
|
+
# conversation, #request_shape describes what it was sent. Subclasses map
|
|
9
|
+
# common HTTP status codes: BadRequestError (400), UnauthorizedError (401),
|
|
9
10
|
# PaymentRequiredError (402), ForbiddenError (403), RateLimitError (429),
|
|
10
11
|
# ServerError (500), ServiceUnavailableError (502 to 504), and
|
|
11
12
|
# OverloadedError (529).
|
|
@@ -27,6 +28,13 @@ module RubyLLM
|
|
|
27
28
|
# available. Its +status+ and +body+ carry the provider's reply.
|
|
28
29
|
attr_reader :response
|
|
29
30
|
|
|
31
|
+
# The Protocol that rendered the request the error answered, which
|
|
32
|
+
# reads its payload back for #request_shape.
|
|
33
|
+
attr_accessor :request_protocol # :nodoc:
|
|
34
|
+
|
|
35
|
+
# The payload request_protocol rendered for the request.
|
|
36
|
+
attr_accessor :request_payload # :nodoc:
|
|
37
|
+
|
|
30
38
|
def self.default_message # :nodoc:
|
|
31
39
|
nil
|
|
32
40
|
end
|
|
@@ -37,6 +45,24 @@ module RubyLLM
|
|
|
37
45
|
@response = response
|
|
38
46
|
super(message || response&.body || self.class.default_message)
|
|
39
47
|
end
|
|
48
|
+
|
|
49
|
+
# Returns a RequestShape describing the conversation request the error
|
|
50
|
+
# answered: each turn's parts and their sizes, never their contents,
|
|
51
|
+
# and the problems providers are known to refuse. Returns +nil+ for
|
|
52
|
+
# errors raised before a request goes out, such as an unsupported
|
|
53
|
+
# attachment, and for operations that send no conversation, such as
|
|
54
|
+
# embeddings.
|
|
55
|
+
#
|
|
56
|
+
# rescue RubyLLM::BadRequestError => e
|
|
57
|
+
# Rails.logger.error("#{e.message}\n#{e.request_shape}")
|
|
58
|
+
# Rails.error.report(e, context: { request_shape: e.request_shape&.to_h })
|
|
59
|
+
#
|
|
60
|
+
def request_shape
|
|
61
|
+
return unless request_protocol
|
|
62
|
+
return @request_shape if defined?(@request_shape)
|
|
63
|
+
|
|
64
|
+
@request_shape = request_protocol.request_shape(request_payload)
|
|
65
|
+
end
|
|
40
66
|
end
|
|
41
67
|
|
|
42
68
|
# Raised when a deprecated API is used and
|
|
@@ -137,6 +163,12 @@ module RubyLLM
|
|
|
137
163
|
end
|
|
138
164
|
end
|
|
139
165
|
|
|
166
|
+
# Raised when the provider's content filters block a transcription and
|
|
167
|
+
# no transcript comes back. The message names the provider's reason,
|
|
168
|
+
# such as +SAFETY+. Chat responses report a block through
|
|
169
|
+
# Message#content_filtered? instead of raising.
|
|
170
|
+
class ContentFilterError < Error; end
|
|
171
|
+
|
|
140
172
|
# Raised for HTTP 529 responses when the provider is temporarily
|
|
141
173
|
# overloaded.
|
|
142
174
|
class OverloadedError < Error
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Evaluation
|
|
5
|
+
class Assertions # :nodoc:
|
|
6
|
+
METHOD = /\A(?:assert|refute)(?:_|\z)/
|
|
7
|
+
|
|
8
|
+
# Matches Minitest assertion failures without loading Minitest.
|
|
9
|
+
module Failure # :nodoc:
|
|
10
|
+
def self.===(error)
|
|
11
|
+
defined?(::Minitest::Assertion) && error.is_a?(::Minitest::Assertion)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_accessor :assertions
|
|
16
|
+
|
|
17
|
+
class << self
|
|
18
|
+
def method?(name)
|
|
19
|
+
METHOD.match?(name) && available? && ::Minitest::Assertions.method_defined?(name)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def available?
|
|
23
|
+
require 'minitest'
|
|
24
|
+
true
|
|
25
|
+
rescue LoadError
|
|
26
|
+
false
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def initialize
|
|
31
|
+
raise LoadError, "Evaluation assertions require the 'minitest' gem. Add it to your Gemfile." unless
|
|
32
|
+
self.class.available?
|
|
33
|
+
|
|
34
|
+
extend ::Minitest::Assertions
|
|
35
|
+
|
|
36
|
+
@assertions = 0
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Evaluation
|
|
5
|
+
# One dataset example, with application inputs and optional reference evidence.
|
|
6
|
+
class Case
|
|
7
|
+
include Support::Inspectable
|
|
8
|
+
|
|
9
|
+
# Returns the stable case name used to compare runs.
|
|
10
|
+
attr_reader :name
|
|
11
|
+
# Returns the value passed to Evaluation#perform.
|
|
12
|
+
attr_reader :inputs
|
|
13
|
+
# Returns the reference answer, or nil when none was supplied.
|
|
14
|
+
attr_reader :expected_output
|
|
15
|
+
# Returns application-defined reference evidence and category labels.
|
|
16
|
+
attr_reader :metadata
|
|
17
|
+
|
|
18
|
+
# Creates a case. Inputs and reference evidence must be JSON-compatible.
|
|
19
|
+
def initialize(name:, inputs:, metadata: {}, **reference)
|
|
20
|
+
unless (reference.keys - [:expected_output]).empty?
|
|
21
|
+
raise ArgumentError,
|
|
22
|
+
"Unknown case fields: #{reference.keys - [:expected_output]}"
|
|
23
|
+
end
|
|
24
|
+
raise ArgumentError, 'A case name cannot be empty' if name.to_s.empty?
|
|
25
|
+
raise ArgumentError, 'Case metadata must be a Hash' unless metadata.is_a?(Hash)
|
|
26
|
+
|
|
27
|
+
@name = name.to_s.dup.freeze
|
|
28
|
+
@inputs = Judge::Data.copy(inputs)
|
|
29
|
+
@metadata = Judge::Data.copy(metadata)
|
|
30
|
+
@expected_output = Judge::Data.copy(reference[:expected_output])
|
|
31
|
+
@reference_supplied = reference.key?(:expected_output)
|
|
32
|
+
freeze
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Returns whether a reference was supplied, including an explicit nil.
|
|
36
|
+
def expected_output?
|
|
37
|
+
@reference_supplied
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns the portable dataset representation of this case.
|
|
41
|
+
def to_h
|
|
42
|
+
data = { name:, inputs:, metadata: }
|
|
43
|
+
data[:expected_output] = expected_output if expected_output?
|
|
44
|
+
data
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def inspect_attributes # :nodoc:
|
|
48
|
+
{ name: }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
require 'pathname'
|
|
5
|
+
|
|
6
|
+
module RubyLLM
|
|
7
|
+
class Evaluation
|
|
8
|
+
module Dataset # :nodoc:
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def load(source, name:)
|
|
12
|
+
source = rows(source, name:)
|
|
13
|
+
cases = source.map do |row|
|
|
14
|
+
row.is_a?(Case) ? row : Case.new(**row.transform_keys(&:to_sym))
|
|
15
|
+
end
|
|
16
|
+
raise ArgumentError, 'A dataset cannot be empty' if cases.empty?
|
|
17
|
+
raise ArgumentError, 'Dataset case names must be unique' unless cases.map(&:name).uniq.size == cases.size
|
|
18
|
+
|
|
19
|
+
cases
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def rows(source, name:)
|
|
23
|
+
source = source.call if source.respond_to?(:call)
|
|
24
|
+
source ||= discover(name)
|
|
25
|
+
source = read(source) if source.is_a?(String) || source.is_a?(Pathname)
|
|
26
|
+
source = cases_from_hash(source) if source.is_a?(Hash)
|
|
27
|
+
raise ArgumentError, 'A dataset must contain enumerable cases' unless source.respond_to?(:map)
|
|
28
|
+
|
|
29
|
+
source
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cases_from_hash(source)
|
|
33
|
+
unknown = source.keys.map(&:to_s) - %w[name cases]
|
|
34
|
+
if unknown.any?
|
|
35
|
+
raise ArgumentError,
|
|
36
|
+
"Unsupported dataset fields: #{unknown.join(', ')}; declare evaluators in Ruby"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
source.fetch('cases') { source.fetch(:cases) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def discover(name)
|
|
43
|
+
raise ArgumentError, 'An anonymous evaluation needs an explicit dataset' unless name
|
|
44
|
+
|
|
45
|
+
filename = name.gsub('::', '/').gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
46
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase
|
|
47
|
+
root = Prompt.root.parent.join('evals')
|
|
48
|
+
paths = %w[yml yaml json jsonl].map { |extension| root.join("#{filename}.#{extension}") }.select(&:file?)
|
|
49
|
+
raise ArgumentError, "Dataset not found: #{root.join(filename)}.{yml,yaml,json,jsonl}" if paths.empty?
|
|
50
|
+
raise ArgumentError, "Ambiguous dataset: #{paths.join(', ')}" if paths.size > 1
|
|
51
|
+
|
|
52
|
+
paths.first
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def read(path)
|
|
56
|
+
case File.extname(path)
|
|
57
|
+
when '.yml', '.yaml' then YAML.safe_load_file(path, aliases: false)
|
|
58
|
+
when '.json' then JSON.parse(File.read(path))
|
|
59
|
+
when '.jsonl' then File.foreach(path).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) }
|
|
60
|
+
else raise ArgumentError, "Unsupported dataset format: #{path}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|