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,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Evaluation
|
|
5
|
+
# The recorded outcome of one case and repetition, including failed attempts.
|
|
6
|
+
class Trial
|
|
7
|
+
include Support::Inspectable
|
|
8
|
+
include Accounting::Usage::Result
|
|
9
|
+
|
|
10
|
+
# Returns the case whose inputs were executed.
|
|
11
|
+
attr_reader :test_case
|
|
12
|
+
# Returns the one-based repetition number.
|
|
13
|
+
attr_reader :repetition
|
|
14
|
+
# Returns the original value returned by perform, when it completed.
|
|
15
|
+
attr_reader :result
|
|
16
|
+
# Returns the primary output extracted from the returned value.
|
|
17
|
+
attr_reader :output
|
|
18
|
+
# Returns the frozen evidence supplied to evaluators.
|
|
19
|
+
attr_reader :evidence
|
|
20
|
+
# Returns each criterion's Evaluation::Result.
|
|
21
|
+
attr_reader :evaluations
|
|
22
|
+
# Returns the number of Ruby assertions attempted.
|
|
23
|
+
attr_reader :assertion_count
|
|
24
|
+
# Returns an assertion failure, or nil.
|
|
25
|
+
attr_reader :assertion_failure
|
|
26
|
+
# Returns an execution or evidence-conversion error, or nil.
|
|
27
|
+
attr_reader :error
|
|
28
|
+
# Returns elapsed wall-clock seconds, including evaluators.
|
|
29
|
+
attr_reader :duration
|
|
30
|
+
|
|
31
|
+
def initialize(test_case:, repetition:, task_usage: [], evaluator_usage: [], **attributes) # :nodoc:
|
|
32
|
+
@test_case = test_case
|
|
33
|
+
@repetition = repetition
|
|
34
|
+
@task_usage = task_usage.freeze
|
|
35
|
+
@evaluator_usage = evaluator_usage.freeze
|
|
36
|
+
@ruby_llm_usage_entries = (task_usage + evaluator_usage).freeze
|
|
37
|
+
attributes.each { |key, value| instance_variable_set("@#{key}", value) }
|
|
38
|
+
@evaluations = Array(@evaluations).freeze
|
|
39
|
+
freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Returns a Tokens aggregate for every provider attempt made during this trial.
|
|
43
|
+
def tokens
|
|
44
|
+
ruby_llm_usage_tokens
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Returns a Cost aggregate for this trial, including task and evaluator requests.
|
|
48
|
+
def cost
|
|
49
|
+
ruby_llm_usage_cost
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns the task's tokens, including setup, assertions, and teardown requests.
|
|
53
|
+
def task_tokens
|
|
54
|
+
Tokens.aggregate(@task_usage.map(&:tokens))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns the tokens used by the evaluators and their tools.
|
|
58
|
+
def evaluator_tokens
|
|
59
|
+
Tokens.aggregate(@evaluator_usage.map(&:tokens))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns the task's cost, including setup, assertions, and teardown requests.
|
|
63
|
+
def task_cost
|
|
64
|
+
Cost.aggregate(@task_usage.map(&:cost), complete: @task_usage.all?(&:cost_available?))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Returns the cost of the evaluators and their tools.
|
|
68
|
+
def evaluator_cost
|
|
69
|
+
Cost.aggregate(@evaluator_usage.map(&:cost), complete: @evaluator_usage.all?(&:cost_available?))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns :passed, :failed, :measured, :unassessed, or :error. Empty trials are never passes.
|
|
73
|
+
def status
|
|
74
|
+
statuses = evaluations.map(&:status)
|
|
75
|
+
statuses << :error if error
|
|
76
|
+
statuses << :failed if assertion_failure
|
|
77
|
+
statuses << :measured if evaluations.empty? && assertion_count.to_i.zero?
|
|
78
|
+
%i[error failed unassessed measured].find { |status| statuses.include?(status) } || :passed
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns whether every assessment passed and no error occurred.
|
|
82
|
+
def passed?
|
|
83
|
+
status == :passed
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Returns the portable trial record, excluding the live application object.
|
|
87
|
+
def to_h
|
|
88
|
+
{ case: test_case.to_h, repetition:, status:, evidence:, evaluations: evaluations.map(&:to_h),
|
|
89
|
+
assertion_count:, assertion_failure: assertion_failure&.message, duration:,
|
|
90
|
+
error: error && { class: error.class.name, message: error.message },
|
|
91
|
+
tokens: tokens.to_h, cost: cost.to_h,
|
|
92
|
+
task_tokens: task_tokens.to_h, evaluator_tokens: evaluator_tokens.to_h,
|
|
93
|
+
task_cost: task_cost.to_h, evaluator_cost: evaluator_cost.to_h }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def inspect_attributes # :nodoc:
|
|
97
|
+
{ case: test_case.name, repetition:, status: }
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
# Defines reusable evaluations with a dataset, semantic criteria, and Ruby assertions.
|
|
5
|
+
# Datasets default to app/evals/<class_name>.yml, .yaml, .json, or .jsonl.
|
|
6
|
+
# Without declared criteria, evaluates correctness against each case's expected_output.
|
|
7
|
+
#
|
|
8
|
+
# class SupportEvaluation < RubyLLM::Evaluation
|
|
9
|
+
# def perform(input)
|
|
10
|
+
# SupportAgent.new.ask(input)
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
# SupportEvaluation.run.save("tmp/support.json")
|
|
15
|
+
class Evaluation
|
|
16
|
+
CORRECTNESS = 'The answer agrees with the expected output'
|
|
17
|
+
private_constant :CORRECTNESS
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
def inherited(subclass) # :nodoc:
|
|
21
|
+
super
|
|
22
|
+
subclass.instance_variable_set(:@dataset, @dataset)
|
|
23
|
+
subclass.instance_variable_set(:@evaluator, @evaluator)
|
|
24
|
+
subclass.instance_variable_set(:@definitions, definitions.dup)
|
|
25
|
+
subclass.instance_variable_set(:@adapters, adapters.dup)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Sets the dataset path, enumerable of cases, or block returning cases.
|
|
29
|
+
# With no argument, returns the configured source. The default is discovered by class name.
|
|
30
|
+
def dataset(source = nil, &block)
|
|
31
|
+
return @dataset if source.nil? && !block
|
|
32
|
+
|
|
33
|
+
raise ArgumentError, 'Pass a dataset or a block, not both' if source && block
|
|
34
|
+
|
|
35
|
+
@dataset = block || source
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Sets the default evaluator: a model, Agent class, or Judge class.
|
|
39
|
+
# Model keywords accept provider, protocol, and context as Chat does.
|
|
40
|
+
# Without a declaration, uses the default chat model and built-in reviewer.
|
|
41
|
+
# Pass false to disable model grading and run only Ruby assertions.
|
|
42
|
+
def evaluator(target = nil, **options)
|
|
43
|
+
if target.nil? && options.empty?
|
|
44
|
+
@evaluator = Evaluator.new if @evaluator.nil?
|
|
45
|
+
return @evaluator
|
|
46
|
+
end
|
|
47
|
+
raise ArgumentError, 'A disabled evaluator cannot have model options' if target == false && options.any?
|
|
48
|
+
|
|
49
|
+
@evaluator = target == false ? false : Evaluator.new(target, **options)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Declares a semantic criterion. A minimum applies to a native probability or score;
|
|
53
|
+
# without one, numeric decisions are measured but do not count as passes.
|
|
54
|
+
# Omit instructions to set a minimum on a question already defined by a Judge.
|
|
55
|
+
# Declared criteria replace implicit correctness. Use evaluation :correctness
|
|
56
|
+
# without instructions to include the built-in reference comparison explicitly.
|
|
57
|
+
# Supply evaluator to override the class evaluator for this criterion.
|
|
58
|
+
def evaluation(name, instructions = nil, minimum: nil, evaluator: nil)
|
|
59
|
+
validate_criterion(name, minimum)
|
|
60
|
+
name = name.to_sym
|
|
61
|
+
@declared_names ||= []
|
|
62
|
+
raise ArgumentError, "Duplicate evaluation: #{name}" if @declared_names.include?(name)
|
|
63
|
+
|
|
64
|
+
@declared_names << name
|
|
65
|
+
backend = Evaluator.new(evaluator) if evaluator
|
|
66
|
+
definitions[name] = { name:, instructions: instructions&.dup&.freeze, minimum:, evaluator: backend }.freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Converts a custom result into evaluation evidence. The original stays available as result.
|
|
70
|
+
#
|
|
71
|
+
# adapt Invoice do |invoice|
|
|
72
|
+
# invoice.attributes.slice("total", "currency")
|
|
73
|
+
# end
|
|
74
|
+
def adapt(type, &block)
|
|
75
|
+
raise ArgumentError, 'An adapter needs a class and a block' unless type.is_a?(Module) && block
|
|
76
|
+
|
|
77
|
+
adapters[type] = block
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Returns the dataset cases without running the application or its evaluators.
|
|
81
|
+
# Supply only to select one or more case names, raising when any name is missing.
|
|
82
|
+
def cases(dataset: nil, only: nil)
|
|
83
|
+
loaded = Dataset.load(dataset || self.dataset, name: name)
|
|
84
|
+
return loaded if only.nil?
|
|
85
|
+
|
|
86
|
+
names = Array(only).map(&:to_s)
|
|
87
|
+
missing = names - loaded.map(&:name)
|
|
88
|
+
raise ArgumentError, "Unknown evaluation cases: #{missing.join(', ')}" if missing.any? || names.empty?
|
|
89
|
+
|
|
90
|
+
loaded.select { |test_case| names.include?(test_case.name) }
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Executes fresh instances for every case and repetition and returns an Evaluation::Report.
|
|
94
|
+
# A supplied dataset overrides discovery for this run. Configuration errors raise;
|
|
95
|
+
# task, assertion, and evaluator failures are recorded per case.
|
|
96
|
+
# Yields each completed Trial before starting the next. Exceptions from the block propagate.
|
|
97
|
+
# Supply id to correlate reports and instrumentation with an application record or job.
|
|
98
|
+
def run(dataset: nil, only: nil, repetitions: 1, id: nil)
|
|
99
|
+
validate_run(repetitions)
|
|
100
|
+
id = (id || SecureRandom.uuid).to_s
|
|
101
|
+
raise ArgumentError, 'An evaluation run id cannot be empty' if id.empty?
|
|
102
|
+
|
|
103
|
+
cases = self.cases(dataset:, only:)
|
|
104
|
+
groups = evaluation_groups(cases)
|
|
105
|
+
definitions = Judge::Data.copy(groups.flat_map do |backend, criteria|
|
|
106
|
+
criteria.map { |criterion| criterion.merge(evaluator: backend.description) }
|
|
107
|
+
end)
|
|
108
|
+
started_at = Time.now.utc
|
|
109
|
+
name = self.name || 'Anonymous evaluation'
|
|
110
|
+
payload = { evaluation_id: id, evaluation_name: name,
|
|
111
|
+
total: cases.size * repetitions, completed: 0, started_at: }
|
|
112
|
+
RubyLLM.instrument('evaluation.ruby_llm', payload) do |event|
|
|
113
|
+
trials = cases.flat_map do |test_case|
|
|
114
|
+
Array.new(repetitions) do |index|
|
|
115
|
+
trial = run_trial(test_case, index + 1, groups, event)
|
|
116
|
+
yield trial if block_given?
|
|
117
|
+
trial
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
event[:report] = Report.new(name:, trials:, id:, started_at:, definitions:)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def definitions # :nodoc:
|
|
125
|
+
@definitions ||= {}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def adapters # :nodoc:
|
|
129
|
+
@adapters ||= {}
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def run_trial(test_case, repetition, groups, run)
|
|
135
|
+
payload = run.slice(:evaluation_id, :evaluation_name, :total).merge(case: test_case.name, repetition:)
|
|
136
|
+
RubyLLM.instrument('evaluation_trial.ruby_llm', payload) do |event|
|
|
137
|
+
trial = new.run_case(test_case, repetition, groups, run_id: run[:evaluation_id])
|
|
138
|
+
event[:completed] = run[:completed] += 1
|
|
139
|
+
event[:trial] = trial
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def validate_run(repetitions)
|
|
144
|
+
raise ArgumentError, 'Define perform(input) in your evaluation' if instance_method(:perform).owner == Evaluation
|
|
145
|
+
if evaluator == false && definitions.any?
|
|
146
|
+
raise ArgumentError, 'Cannot declare semantic evaluations with evaluator false'
|
|
147
|
+
end
|
|
148
|
+
return if repetitions.is_a?(Integer) && repetitions.positive?
|
|
149
|
+
|
|
150
|
+
raise ArgumentError, 'Repetitions must be a positive Integer'
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validate_criterion(name, minimum)
|
|
154
|
+
raise ArgumentError, 'An evaluation name cannot be empty' if name.to_s.empty?
|
|
155
|
+
return if minimum.nil? || (minimum.is_a?(Numeric) && minimum.finite?)
|
|
156
|
+
|
|
157
|
+
raise ArgumentError, 'Minimum must be a finite number'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def evaluation_groups(cases)
|
|
161
|
+
return {} unless evaluator
|
|
162
|
+
|
|
163
|
+
all = evaluator.question_names.to_h do |name|
|
|
164
|
+
[name, { name:, instructions: nil, minimum: nil }]
|
|
165
|
+
end.merge(definitions)
|
|
166
|
+
all[:correctness] = { name: :correctness, instructions: nil, minimum: nil } if all.empty?
|
|
167
|
+
groups = all.values.group_by { |definition| definition[:evaluator] || evaluator }
|
|
168
|
+
groups.to_h do |backend, criteria|
|
|
169
|
+
[backend, criteria.map { |criterion| prepare_criterion(backend, criterion, cases) }]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def prepare_criterion(backend, criterion, cases)
|
|
174
|
+
existing = backend.question_names.include?(criterion[:name])
|
|
175
|
+
raise ArgumentError, "Duplicate Judge question: #{criterion[:name]}" if existing && criterion[:instructions]
|
|
176
|
+
return criterion if existing || !criterion[:instructions].to_s.empty?
|
|
177
|
+
|
|
178
|
+
if criterion[:name] == :correctness && criterion[:instructions].nil?
|
|
179
|
+
return criterion.merge(instructions: correctness_instructions(cases))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
raise ArgumentError, "Missing instructions: #{criterion[:name]}"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def correctness_instructions(cases)
|
|
186
|
+
missing = cases.reject(&:expected_output?).map(&:name)
|
|
187
|
+
raise ArgumentError, "Correctness requires expected_output for cases: #{missing.join(', ')}" if missing.any?
|
|
188
|
+
|
|
189
|
+
CORRECTNESS
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Returns the current case's input, reference output, and metadata, respectively.
|
|
194
|
+
attr_reader :input, :expected_output, :metadata
|
|
195
|
+
# Returns the original value returned by perform.
|
|
196
|
+
attr_reader :result
|
|
197
|
+
|
|
198
|
+
# Runs the application under evaluation. Override in your evaluation class.
|
|
199
|
+
def perform(_input)
|
|
200
|
+
raise NotImplementedError, 'Define perform(input) in your evaluation'
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Runs Ruby assertions after perform. Override to use assert, refute, and the Minitest assertion family.
|
|
204
|
+
def assertions; end
|
|
205
|
+
|
|
206
|
+
# Runs before perform on each fresh case instance. Override for application fixtures.
|
|
207
|
+
def setup; end
|
|
208
|
+
|
|
209
|
+
# Runs after each case, even when setup, perform, or an assertion fails.
|
|
210
|
+
def teardown; end
|
|
211
|
+
|
|
212
|
+
# Returns the primary answer or value extracted from result.
|
|
213
|
+
def output
|
|
214
|
+
@evidence.output
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Returns retained messages from a Chat, Agent, or Message result.
|
|
218
|
+
def messages
|
|
219
|
+
@evidence.messages
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Returns tool calls recorded in the returned conversation or message.
|
|
223
|
+
def tool_calls
|
|
224
|
+
@evidence.tool_calls
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# :method: assert
|
|
228
|
+
# :call-seq:
|
|
229
|
+
# assert(condition, message = nil)
|
|
230
|
+
#
|
|
231
|
+
# Asserts that condition is truthy. Delegates to Minitest::Assertions and
|
|
232
|
+
# records its assertion count. Requires the minitest gem, which Rails
|
|
233
|
+
# applications already include. The other Minitest assert_* and refute_*
|
|
234
|
+
# methods use the same contract. Call these from #assertions.
|
|
235
|
+
|
|
236
|
+
# :method: refute
|
|
237
|
+
# :call-seq:
|
|
238
|
+
# refute(condition, message = nil)
|
|
239
|
+
#
|
|
240
|
+
# Asserts that condition is false or nil. See #assert.
|
|
241
|
+
|
|
242
|
+
# :method: assert_equal
|
|
243
|
+
# :call-seq:
|
|
244
|
+
# assert_equal(expected, actual, message = nil)
|
|
245
|
+
#
|
|
246
|
+
# Asserts equality through Minitest::Assertions. See #assert.
|
|
247
|
+
|
|
248
|
+
# :method: assert_includes
|
|
249
|
+
# :call-seq:
|
|
250
|
+
# assert_includes(collection, value, message = nil)
|
|
251
|
+
#
|
|
252
|
+
# Asserts that the collection includes value. See #assert.
|
|
253
|
+
|
|
254
|
+
# :stopdoc:
|
|
255
|
+
def method_missing(name, ...)
|
|
256
|
+
return super unless Assertions::METHOD.match?(name) && assertion_context.respond_to?(name)
|
|
257
|
+
|
|
258
|
+
assertion_context.public_send(name, ...)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def respond_to_missing?(name, include_private = nil)
|
|
262
|
+
Assertions.method?(name) || super
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def run_case(test_case, repetition, groups, run_id:) # :nodoc:
|
|
266
|
+
Runner.new(self, test_case, repetition, groups, run_id:).run
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def prepare(test_case) # :nodoc:
|
|
270
|
+
@input = JSON.parse(JSON.generate(test_case.inputs))
|
|
271
|
+
@expected_output = test_case.expected_output
|
|
272
|
+
@metadata = test_case.metadata
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def execute # :nodoc:
|
|
276
|
+
@result = perform(input)
|
|
277
|
+
@evidence = Evidence.new(result, adapters: self.class.adapters)
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def assertion_context # :nodoc:
|
|
281
|
+
@assertion_context ||= Assertions.new
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def assertion_count # :nodoc:
|
|
285
|
+
@assertion_context&.assertions.to_i
|
|
286
|
+
end
|
|
287
|
+
# :startdoc:
|
|
288
|
+
end
|
|
289
|
+
end
|
data/lib/ruby_llm/image.rb
CHANGED
|
@@ -43,6 +43,8 @@ module RubyLLM
|
|
|
43
43
|
# request vocabulary and merges them into the request as-is.
|
|
44
44
|
# +context:+ supplies a Context whose configuration replaces the
|
|
45
45
|
# global one. +metadata:+ is included in the instrumentation payload.
|
|
46
|
+
# +owner:+ attributes the usage to a record, such as a user, and wins
|
|
47
|
+
# over RubyLLM.with_usage_owner.
|
|
46
48
|
#
|
|
47
49
|
# image = RubyLLM.paint("A small watercolor robot", model: "gpt-image-2")
|
|
48
50
|
#
|
|
@@ -67,7 +69,8 @@ module RubyLLM
|
|
|
67
69
|
with: nil,
|
|
68
70
|
mask: nil,
|
|
69
71
|
provider_options: {},
|
|
70
|
-
metadata: nil
|
|
72
|
+
metadata: nil,
|
|
73
|
+
owner: nil)
|
|
71
74
|
config = context&.config || RubyLLM.config
|
|
72
75
|
model ||= config.default_image_model
|
|
73
76
|
model, provider_instance = Models.resolve(model, provider: provider, assume_model_exists: assume_model_exists,
|
|
@@ -88,7 +91,9 @@ module RubyLLM
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
RubyLLM.instrument('image.ruby_llm', payload, config: config) do |event|
|
|
91
|
-
result =
|
|
94
|
+
result = Accounting::Usage.owned_by(owner) do
|
|
95
|
+
provider_instance.paint(prompt, model:, size:, count:, with:, mask:, provider_options:)
|
|
96
|
+
end
|
|
92
97
|
images = Support::Utils.to_safe_array(result)
|
|
93
98
|
event[:result] = result
|
|
94
99
|
event[:response_model] = images.first&.model
|
|
@@ -174,8 +179,11 @@ module RubyLLM
|
|
|
174
179
|
Cost.new(tokens:, model: model_info, category: :images, input_details: input_tokens_details)
|
|
175
180
|
end
|
|
176
181
|
|
|
177
|
-
|
|
178
|
-
|
|
182
|
+
attr_writer :model_info # :nodoc:
|
|
183
|
+
|
|
184
|
+
# Returns the image's Model from its provider's registry, falling back
|
|
185
|
+
# to the requested model when the response ID is unknown. Images built
|
|
186
|
+
# without a request look up #model, or return +nil+ if unknown.
|
|
179
187
|
def model_info
|
|
180
188
|
return unless model
|
|
181
189
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Judge
|
|
5
|
+
class Builder # :nodoc:
|
|
6
|
+
def self.resolve(value, scope:)
|
|
7
|
+
return value unless value.is_a?(::Proc)
|
|
8
|
+
|
|
9
|
+
new(scope).evaluate(value)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(scope)
|
|
13
|
+
@scope = scope
|
|
14
|
+
@data = {}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def evaluate(block)
|
|
18
|
+
result = block.arity.zero? ? instance_exec(&block) : instance_exec(self, &block)
|
|
19
|
+
return result if @data.empty?
|
|
20
|
+
|
|
21
|
+
unless result.equal?(self) || result.nil?
|
|
22
|
+
::Kernel.raise ::ArgumentError, 'A judgment block must declare fields or return a value, not both'
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
@data
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_missing(name, *values, &block)
|
|
29
|
+
return @scope.public_send(name) if runtime_input?(name, values, block)
|
|
30
|
+
|
|
31
|
+
if values.size > 1 || (!values.empty? && block)
|
|
32
|
+
::Kernel.raise ::ArgumentError, "#{name} accepts one value or a block"
|
|
33
|
+
end
|
|
34
|
+
::Kernel.raise ::ArgumentError, "Duplicate judgment field: #{name}" if @data.key?(name)
|
|
35
|
+
|
|
36
|
+
@data[name] = block ? ::RubyLLM::Judge::Builder.resolve(block, scope: @scope) : values.first
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def respond_to_missing?(_name, _include_private = nil)
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def runtime_input?(name, values, block)
|
|
47
|
+
values.empty? && !block && @scope.respond_to?(name)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Judge
|
|
5
|
+
module Data # :nodoc:
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def copy(value, &resolver)
|
|
9
|
+
case value
|
|
10
|
+
when Proc then resolver ? copy(resolver.call(value), &resolver) : value
|
|
11
|
+
when Hash then copy_hash(value, &resolver)
|
|
12
|
+
when Array then value.map { |item| copy(item, &resolver) }.freeze
|
|
13
|
+
when String then value.dup.freeze
|
|
14
|
+
when Symbol then value.to_s.freeze
|
|
15
|
+
when Float
|
|
16
|
+
raise ArgumentError, 'Judgment data must contain finite numbers' unless value.finite?
|
|
17
|
+
|
|
18
|
+
value
|
|
19
|
+
when Integer, TrueClass, FalseClass, NilClass then value
|
|
20
|
+
else raise ArgumentError, "Unsupported judgment data: #{value.class}; pass JSON-compatible values"
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def copy_hash(value, &resolver)
|
|
25
|
+
seen = {}
|
|
26
|
+
value.each_with_object({}) do |(key, item), result|
|
|
27
|
+
name = key.to_s
|
|
28
|
+
unless key.is_a?(String) || key.is_a?(Symbol) || key == true || key == false
|
|
29
|
+
raise ArgumentError, 'Judgment data keys must be Strings, Symbols, or booleans'
|
|
30
|
+
end
|
|
31
|
+
raise ArgumentError, "Duplicate judgment key: #{name}" if seen[name]
|
|
32
|
+
|
|
33
|
+
seen[name] = true
|
|
34
|
+
result[key.is_a?(String) ? key.dup.freeze : key] = copy(item, &resolver)
|
|
35
|
+
end.freeze
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def description?(value)
|
|
39
|
+
value.nil? || value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Judge
|
|
5
|
+
class Question # :nodoc:
|
|
6
|
+
CRITERIA_KEYS = { probability: :criteria, choice: :options, score: :levels }.freeze
|
|
7
|
+
|
|
8
|
+
attr_reader :name, :type, :instructions, :criteria
|
|
9
|
+
|
|
10
|
+
def initialize(name, type:, instructions: nil, criteria: nil, &block)
|
|
11
|
+
validate_name!(name)
|
|
12
|
+
raise ArgumentError, "Unknown judgment type: #{type.inspect}" unless CRITERIA_KEYS.key?(type)
|
|
13
|
+
raise ArgumentError, 'Pass question criteria or a block, not both' if !criteria.nil? && block
|
|
14
|
+
|
|
15
|
+
@name = name.is_a?(String) ? name.dup.freeze : name
|
|
16
|
+
@type = type
|
|
17
|
+
@instructions = Data.copy(instructions)
|
|
18
|
+
@criteria = Data.copy(block || criteria)
|
|
19
|
+
freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.from_h(name, definition)
|
|
23
|
+
raise ArgumentError, 'Each question must be a Hash' unless definition.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
definition = definition.transform_keys(&:to_sym)
|
|
26
|
+
type = definition[:type]
|
|
27
|
+
type = type.to_sym if type.is_a?(String)
|
|
28
|
+
key = CRITERIA_KEYS.fetch(type) { raise ArgumentError, "Unknown judgment type: #{type.inspect}" }
|
|
29
|
+
extra = definition.keys - [:type, :instructions, key]
|
|
30
|
+
raise ArgumentError, "Unknown question options: #{extra.join(', ')}" unless extra.empty?
|
|
31
|
+
|
|
32
|
+
new(name, type:, instructions: definition[:instructions], criteria: definition[key])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def resolve(scope)
|
|
36
|
+
resolver = ->(value) { Builder.resolve(value, scope:) }
|
|
37
|
+
resolved = self.class.new(name, type:, instructions: Data.copy(instructions, &resolver),
|
|
38
|
+
criteria: Data.copy(criteria, &resolver))
|
|
39
|
+
resolved.validate!
|
|
40
|
+
resolved
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def validate!
|
|
44
|
+
unless Data.description?(instructions)
|
|
45
|
+
raise ArgumentError,
|
|
46
|
+
'Question instructions must be text, a Hash, an Array, or nil'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
case type
|
|
50
|
+
when :probability then validate_probability!
|
|
51
|
+
when :choice then validate_choice!
|
|
52
|
+
when :score then validate_score!
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def validate_name!(name)
|
|
59
|
+
unless name.is_a?(String) || name.is_a?(Symbol)
|
|
60
|
+
raise ArgumentError, 'A question name must be a String or Symbol'
|
|
61
|
+
end
|
|
62
|
+
raise ArgumentError, 'A question name cannot be empty' if name.to_s.empty?
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def validate_probability!
|
|
66
|
+
return if criteria.nil?
|
|
67
|
+
|
|
68
|
+
unless criteria.is_a?(Hash) && (criteria.keys.map(&:to_s) - %w[yes no true false]).empty?
|
|
69
|
+
raise ArgumentError, 'Probability criteria must describe yes and no'
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
positive = %w[yes true]
|
|
73
|
+
aliases = criteria.keys.map { |key| positive.include?(key.to_s) }
|
|
74
|
+
raise ArgumentError, 'Probability criteria contain duplicate outcomes' unless aliases.uniq.size == aliases.size
|
|
75
|
+
|
|
76
|
+
validate_descriptions!(criteria.values)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def validate_choice!
|
|
80
|
+
raise ArgumentError, 'A choice needs a nonempty Hash of options' unless criteria.is_a?(Hash) && !criteria.empty?
|
|
81
|
+
unless criteria.keys.all? { |key| (key.is_a?(String) || key.is_a?(Symbol)) && !key.to_s.empty? }
|
|
82
|
+
raise ArgumentError, 'Choice options must have nonempty String or Symbol names'
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
validate_descriptions!(criteria.values)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def validate_score!
|
|
89
|
+
raise ArgumentError, 'A score needs a nonempty Array of levels' unless criteria.is_a?(Array) && !criteria.empty?
|
|
90
|
+
|
|
91
|
+
validate_descriptions!(criteria)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def validate_descriptions!(values)
|
|
95
|
+
return if values.all? { |value| Data.description?(value) }
|
|
96
|
+
|
|
97
|
+
raise ArgumentError, 'Descriptions must be text, a Hash, an Array, or nil'
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|