llm-evals 0.4.0 → 0.5.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/CHANGELOG.md +38 -0
- data/README.md +163 -87
- data/exe/llm-evals +60 -5
- data/lib/.DS_Store +0 -0
- data/lib/llm/.DS_Store +0 -0
- data/lib/llm/evals/actions/adapter.rb +50 -0
- data/lib/llm/evals/actions/error_budget.rb +63 -0
- data/lib/llm/evals/actions/error_diagnosis.rb +51 -0
- data/lib/llm/evals/actions/error_streak.rb +83 -0
- data/lib/llm/evals/actions/experiment.rb +251 -0
- data/lib/llm/evals/actions/verify.rb +61 -0
- data/lib/llm/evals/actions.rb +9 -0
- data/lib/llm/evals/cli/cases.rb +10 -0
- data/lib/llm/evals/cli/command.rb +32 -99
- data/lib/llm/evals/cli/compare.rb +0 -20
- data/lib/llm/evals/cli/contender_selector.rb +61 -0
- data/lib/llm/evals/cli/error_pause.rb +81 -0
- data/lib/llm/evals/cli/experiment_pool.rb +97 -0
- data/lib/llm/evals/cli/formatting.rb +8 -0
- data/lib/llm/evals/cli/leaderboard.rb +66 -12
- data/lib/llm/evals/cli/model_colors.rb +3 -0
- data/lib/llm/evals/cli/model_command.rb +152 -315
- data/lib/llm/evals/cli/model_options.rb +80 -0
- data/lib/llm/evals/cli/option_groups.rb +32 -0
- data/lib/llm/evals/cli/progress_bars.rb +92 -5
- data/lib/llm/evals/cli/render.rb +6 -6
- data/lib/llm/evals/cli/report_dispatcher/console.rb +65 -0
- data/lib/llm/evals/cli/report_dispatcher/html_output.rb +50 -0
- data/lib/llm/evals/cli/report_dispatcher/json_output.rb +29 -0
- data/lib/llm/evals/cli/report_dispatcher/output.rb +56 -0
- data/lib/llm/evals/cli/report_dispatcher/pipeline_output.rb +38 -0
- data/lib/llm/evals/cli/report_dispatcher.rb +133 -0
- data/lib/llm/evals/cli/run.rb +13 -22
- data/lib/llm/evals/cli/run_planner.rb +97 -0
- data/lib/llm/evals/cli/run_record.rb +68 -0
- data/lib/llm/evals/cli/standings.rb +39 -17
- data/lib/llm/evals/cli/status_box.rb +51 -0
- data/lib/llm/evals/cli/table.rb +103 -0
- data/lib/llm/evals/cli/text.rb +22 -0
- data/lib/llm/evals/cli/tracker_resolver.rb +52 -0
- data/lib/llm/evals/cli/upgrade_config.rb +60 -0
- data/lib/llm/evals/cli/verify.rb +5 -1
- data/lib/llm/evals/cli/version.rb +4 -0
- data/lib/llm/evals/cli/weights.rb +48 -0
- data/lib/llm/evals/cli.rb +39 -6
- data/lib/llm/evals/config/contender_builder.rb +30 -0
- data/lib/llm/evals/config/error_limits.rb +41 -0
- data/lib/llm/evals/config/listing.rb +153 -0
- data/lib/llm/evals/config/results.rb +42 -0
- data/lib/llm/evals/config/score_weights.rb +25 -0
- data/lib/llm/evals/config/upgrade.rb +107 -0
- data/lib/llm/evals/config.rb +90 -79
- data/lib/llm/evals/datasets/case.rb +46 -0
- data/lib/llm/evals/datasets/discovery.rb +143 -0
- data/lib/llm/evals/datasets.rb +9 -0
- data/lib/llm/evals/launcher.rb +57 -3
- data/lib/llm/evals/models/contender.rb +51 -0
- data/lib/llm/evals/models/model.rb +87 -0
- data/lib/llm/evals/models/throttle.rb +170 -0
- data/lib/llm/evals/models.rb +9 -0
- data/lib/llm/evals/providers/base.rb +61 -16
- data/lib/llm/evals/providers/catalog.rb +5 -2
- data/lib/llm/evals/providers/open_router.rb +54 -7
- data/lib/llm/evals/providers/registry.rb +49 -16
- data/lib/llm/evals/providers/type_safe/client.rb +67 -0
- data/lib/llm/evals/providers/type_safe.rb +113 -12
- data/lib/llm/evals/providers.rb +9 -0
- data/lib/llm/evals/render/reports/call_stats_table.rb +44 -0
- data/lib/llm/evals/render/reports/html/dataset_section.rb +51 -0
- data/lib/llm/evals/render/reports/html/format.rb +38 -0
- data/lib/llm/evals/render/reports/html/global_standings.rb +41 -0
- data/lib/llm/evals/render/reports/html/ranking.rb +86 -0
- data/lib/llm/evals/render/reports/html/view_model.rb +116 -0
- data/lib/llm/evals/render/reports/html.rb +60 -181
- data/lib/llm/evals/render/reports/json/decoder.rb +137 -0
- data/lib/llm/evals/render/reports/json/encoder.rb +106 -0
- data/lib/llm/evals/render/reports/json.rb +38 -141
- data/lib/llm/evals/render/reports/pipeline.rb +55 -20
- data/lib/llm/evals/render/reports/terminal.rb +8 -1
- data/lib/llm/evals/render.rb +9 -0
- data/lib/llm/evals/results/provider_response.rb +25 -0
- data/lib/llm/evals/results/reply.rb +38 -0
- data/lib/llm/evals/results/run_outcome.rb +15 -0
- data/lib/llm/evals/results/run_result.rb +40 -0
- data/lib/llm/evals/results/score.rb +45 -0
- data/lib/llm/evals/results/warnings.rb +149 -0
- data/lib/llm/evals/results.rb +9 -0
- data/lib/llm/evals/runtime/configuration.rb +295 -0
- data/lib/llm/evals/runtime/environment.rb +90 -0
- data/lib/llm/evals/runtime/helper.rb +153 -0
- data/lib/llm/evals/runtime.rb +2 -67
- data/lib/llm/evals/scoring/metric.rb +47 -0
- data/lib/llm/evals/scoring/schema_validator.rb +102 -0
- data/lib/llm/evals/scoring/scorer.rb +49 -0
- data/lib/llm/evals/scoring/weighted_score.rb +148 -0
- data/lib/llm/evals/scoring.rb +9 -0
- data/lib/llm/evals/stats/call_stats.rb +91 -0
- data/lib/llm/evals/stats/usage.rb +46 -0
- data/lib/llm/evals/stats.rb +29 -0
- data/lib/llm/evals/testing/echo_adapter.rb +14 -9
- data/lib/llm/evals/testing/fake_model.rb +8 -6
- data/lib/llm/evals/testing/fake_provider.rb +39 -8
- data/lib/llm/evals/testing.rb +9 -0
- data/lib/llm/evals/trackers/base.rb +110 -0
- data/lib/llm/evals/trackers/braintrust.rb +40 -3
- data/lib/llm/evals/trackers/local.rb +38 -2
- data/lib/llm/evals/trackers/logfire.rb +56 -3
- data/lib/llm/evals/trackers/registry.rb +29 -7
- data/lib/llm/evals/trackers.rb +9 -0
- data/lib/llm/evals/version.rb +3 -1
- data/lib/llm/evals.rb +17 -7
- metadata +91 -24
- data/lib/llm/evals/adapter.rb +0 -47
- data/lib/llm/evals/case.rb +0 -36
- data/lib/llm/evals/completion.rb +0 -11
- data/lib/llm/evals/configuration.rb +0 -173
- data/lib/llm/evals/contender.rb +0 -31
- data/lib/llm/evals/discovery.rb +0 -95
- data/lib/llm/evals/experiment.rb +0 -158
- data/lib/llm/evals/helper.rb +0 -98
- data/lib/llm/evals/metric.rb +0 -26
- data/lib/llm/evals/model.rb +0 -55
- data/lib/llm/evals/reply.rb +0 -18
- data/lib/llm/evals/run_result.rb +0 -14
- data/lib/llm/evals/schema_validator.rb +0 -84
- data/lib/llm/evals/score.rb +0 -26
- data/lib/llm/evals/scorer.rb +0 -45
- data/lib/llm/evals/tracker.rb +0 -80
- data/lib/llm/evals/usage.rb +0 -33
- data/lib/llm/evals/verify.rb +0 -49
- data/lib/llm/evals/warnings.rb +0 -98
- data/lib/llm/evals/weighted_score.rb +0 -116
- /data/lib/llm/evals/{templates → render/templates}/html_report.css +0 -0
- /data/lib/llm/evals/{templates → render/templates}/html_report.html.erb +0 -0
- /data/lib/llm/evals/{templates → render/templates}/html_report.js +0 -0
data/lib/llm/evals/verify.rb
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LLM
|
|
4
|
-
module Evals
|
|
5
|
-
# Offline consistency checks over every case, so a typo in the suite is
|
|
6
|
-
# caught before it is billed as a model's mistake. The gem checks what
|
|
7
|
-
# every suite has: an input and a parsable expected answer. The adapter
|
|
8
|
-
# checks each dataset's shared files, and the scorer each case against
|
|
9
|
-
# what it will demand of it.
|
|
10
|
-
class Verify
|
|
11
|
-
# @param configuration [Configuration]
|
|
12
|
-
def initialize(configuration = LLM::Evals.configuration)
|
|
13
|
-
@configuration = configuration
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
# @return [Hash{String => Array<String>}] problems by "dataset/id"; empty when all is sound
|
|
17
|
-
def problems
|
|
18
|
-
discovery = @configuration.discovery
|
|
19
|
-
failures = {}
|
|
20
|
-
discovery.datasets.each do |dataset|
|
|
21
|
-
Array(adapter.verify(dataset)).each { (failures[dataset.to_s] ||= []) << it }
|
|
22
|
-
discovery.cases(dataset: dataset).each do |kase|
|
|
23
|
-
list = case_problems(kase)
|
|
24
|
-
failures[kase.key] = list unless list.empty?
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
failures
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
# @param kase [Case]
|
|
31
|
-
# @return [Array<String>]
|
|
32
|
-
def case_problems(kase)
|
|
33
|
-
problems = []
|
|
34
|
-
problems << "input is empty" if kase.input.empty?
|
|
35
|
-
problems << kase.metadata[:expected_error] if kase.metadata[:expected_error]
|
|
36
|
-
return problems unless kase.expected?
|
|
37
|
-
|
|
38
|
-
problems.concat(Array(scorer.verify(kase, context: adapter.context(kase))))
|
|
39
|
-
rescue StandardError => e
|
|
40
|
-
problems << "#{e.class}: #{e.message}"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
private
|
|
44
|
-
|
|
45
|
-
def adapter = @configuration.adapter
|
|
46
|
-
def scorer = @configuration.scorer
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
data/lib/llm/evals/warnings.rb
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "logger"
|
|
4
|
-
|
|
5
|
-
module LLM
|
|
6
|
-
module Evals
|
|
7
|
-
# Everything a run would otherwise print as a warning while its progress
|
|
8
|
-
# bars redraw: cases that failed, and what Braintrust, RubyLLM, OpenTelemetry
|
|
9
|
-
# (whose exporters report failures on $stdout) and Ruby itself log. Printed among the bars, each one breaks the redraw; collected
|
|
10
|
-
# here, they are saved to one file, and the run prints only how many.
|
|
11
|
-
#
|
|
12
|
-
# It quacks as an IO, so a Logger, and $stderr, can write to it. Threads
|
|
13
|
-
# share it.
|
|
14
|
-
class Warnings
|
|
15
|
-
def initialize
|
|
16
|
-
@lines = []
|
|
17
|
-
@lock = Mutex.new
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
# @param text [#to_s]
|
|
21
|
-
# @return [self]
|
|
22
|
-
def record(text)
|
|
23
|
-
text = text.to_s.chomp
|
|
24
|
-
@lock.synchronize { @lines << text } unless text.strip.empty?
|
|
25
|
-
self
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# @return [Integer] the bytes "written", as IO#write does
|
|
29
|
-
def write(*texts)
|
|
30
|
-
texts.each { record(it) }
|
|
31
|
-
texts.sum { it.to_s.bytesize }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def puts(*texts) = texts.each { record(it) } && nil
|
|
35
|
-
def print(*texts) = write(*texts) && nil
|
|
36
|
-
def <<(text) = record(text)
|
|
37
|
-
def flush = self
|
|
38
|
-
def sync = true
|
|
39
|
-
def sync=(_value); end
|
|
40
|
-
def close = nil
|
|
41
|
-
def tty? = false
|
|
42
|
-
|
|
43
|
-
# @return [Integer]
|
|
44
|
-
def count = @lock.synchronize { @lines.size }
|
|
45
|
-
|
|
46
|
-
# @return [Array<String>]
|
|
47
|
-
def to_a = @lock.synchronize { @lines.dup }
|
|
48
|
-
|
|
49
|
-
# @return [Boolean] whether nothing was recorded
|
|
50
|
-
def none? = count.zero?
|
|
51
|
-
|
|
52
|
-
# @param path [String]
|
|
53
|
-
# @return [String] the path
|
|
54
|
-
def save(path)
|
|
55
|
-
File.write(path, "#{to_a.join("\n\n")}\n")
|
|
56
|
-
path
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Runs the block with $stderr, Ruby's own warnings, and the loggers of
|
|
60
|
-
# whichever optional gems are loaded writing here, and puts them back after.
|
|
61
|
-
#
|
|
62
|
-
# @return [Object] what the block returns
|
|
63
|
-
def capture
|
|
64
|
-
logger = Logger.new(self, level: Logger::WARN)
|
|
65
|
-
stderr = $stderr
|
|
66
|
-
kernel_warn = Kernel.instance_method(:warn)
|
|
67
|
-
warnings = self
|
|
68
|
-
restore = redirect_loggers(logger)
|
|
69
|
-
$stderr = self
|
|
70
|
-
Kernel.define_method(:warn) { |*messages, **_options| warnings.write(messages.join("\n")) }
|
|
71
|
-
Kernel.send(:private, :warn)
|
|
72
|
-
yield
|
|
73
|
-
ensure
|
|
74
|
-
Kernel.define_method(:warn, kernel_warn) if kernel_warn
|
|
75
|
-
Kernel.send(:private, :warn)
|
|
76
|
-
restore&.call
|
|
77
|
-
$stderr = stderr
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
private
|
|
81
|
-
|
|
82
|
-
# @return [Proc] puts the loggers back
|
|
83
|
-
def redirect_loggers(logger)
|
|
84
|
-
braintrust = defined?(Braintrust::Log) ? Braintrust::Log.logger : nil
|
|
85
|
-
ruby_llm = defined?(RubyLLM) ? RubyLLM.instance_variable_get(:@logger) : nil
|
|
86
|
-
open_telemetry = defined?(OpenTelemetry) ? OpenTelemetry.logger : nil
|
|
87
|
-
Braintrust::Log.logger = logger if defined?(Braintrust::Log)
|
|
88
|
-
RubyLLM.instance_variable_set(:@logger, logger) if defined?(RubyLLM)
|
|
89
|
-
OpenTelemetry.logger = logger if defined?(OpenTelemetry)
|
|
90
|
-
lambda do
|
|
91
|
-
Braintrust::Log.logger = braintrust if defined?(Braintrust::Log)
|
|
92
|
-
RubyLLM.instance_variable_set(:@logger, ruby_llm) if defined?(RubyLLM)
|
|
93
|
-
OpenTelemetry.logger = open_telemetry if defined?(OpenTelemetry)
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module LLM
|
|
4
|
-
module Evals
|
|
5
|
-
# One number from the leaderboard's columns, weighted as the lineup file
|
|
6
|
-
# says, for a suite to state what matters most to it. A positive weight
|
|
7
|
-
# means higher is better; a negative one, lower is better:
|
|
8
|
-
#
|
|
9
|
-
# weighted_score:
|
|
10
|
-
# weights:
|
|
11
|
-
# score: 0.4
|
|
12
|
-
# hallucinated_rate: -0.2
|
|
13
|
-
# cost: -0.2
|
|
14
|
-
#
|
|
15
|
-
# weighted_score = Σ(|weight| × goodness) / Σ|weight|
|
|
16
|
-
#
|
|
17
|
-
# Goodness runs from 0 to 1. Metrics are used as they are, or as 1 - value
|
|
18
|
-
# under a negative weight. {MAGNITUDES}
|
|
19
|
-
# are placed on a log scale between the lowest and highest in the group, so
|
|
20
|
-
# that each doubling costs the same: on a linear scale, one model a hundred
|
|
21
|
-
# times the price would squash all the others together. Where the whole
|
|
22
|
-
# group is equal, each gets 1.
|
|
23
|
-
#
|
|
24
|
-
# Errors come off the total, when weighted: 20 points for one, 25 for two, 30
|
|
25
|
-
# for three, and so on, down to 0. A failed case has no scores, so no other
|
|
26
|
-
# column notices it, and a single failure must show. A clean contender gets
|
|
27
|
-
# the whole of the weight on errors.
|
|
28
|
-
#
|
|
29
|
-
# Magnitudes are relative, so a weighted score compares within one group
|
|
30
|
-
# only: another model in the lineup moves everyone's.
|
|
31
|
-
class WeightedScore
|
|
32
|
-
# Columns compared on a log scale.
|
|
33
|
-
MAGNITUDES = %w[time cost].freeze
|
|
34
|
-
|
|
35
|
-
# The gem's columns that can be weighted, beside the metrics. `temp` is a
|
|
36
|
-
# setting, not a result, and tokens are already in the cost.
|
|
37
|
-
COLUMNS = ["score", "errors", *MAGNITUDES].freeze
|
|
38
|
-
|
|
39
|
-
# What errors take off the total: {ERROR_BASE} + {ERROR_STEP} per error.
|
|
40
|
-
ERROR_BASE = 0.15
|
|
41
|
-
ERROR_STEP = 0.05
|
|
42
|
-
|
|
43
|
-
attr_reader :weights
|
|
44
|
-
|
|
45
|
-
# @param weights [Hash{String => Numeric}]
|
|
46
|
-
# @param metrics [Array<String>] the scorer's metric names
|
|
47
|
-
# @raise [Error] on a weight that is not a number, an unknown column, or no weight at all
|
|
48
|
-
def initialize(weights, metrics:)
|
|
49
|
-
known = (metrics + COLUMNS).uniq
|
|
50
|
-
unknown = weights.keys - known
|
|
51
|
-
raise Error, "weighted_score: unknown columns #{unknown.join(", ")}; known: #{known.join(", ")}" if unknown.any?
|
|
52
|
-
|
|
53
|
-
invalid = weights.reject { |_name, weight| weight.is_a?(Numeric) }.keys
|
|
54
|
-
raise Error, "weighted_score: weights must be numbers: #{invalid.join(", ")}" if invalid.any?
|
|
55
|
-
|
|
56
|
-
raise Error, "weighted_score: errors must have a negative weight, since fewer is better" if weights.fetch("errors", -1).positive?
|
|
57
|
-
|
|
58
|
-
@weights = weights.reject { |_name, weight| weight.zero? }
|
|
59
|
-
raise Error, "weighted_score: every weight is zero" if @weights.empty?
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
# A column no row has does not count. A row missing a column the others
|
|
63
|
-
# have gets nil: it was not measured on everything they were.
|
|
64
|
-
#
|
|
65
|
-
# @param rows [Array<Hash{String => Numeric, nil}>] one per contender, compared with each other
|
|
66
|
-
# @return [Array<Float, nil>] in the order given
|
|
67
|
-
def call(rows)
|
|
68
|
-
present = weights.select { |name, _weight| rows.any? { it[name] } }
|
|
69
|
-
total = present.values.sum(&:abs)
|
|
70
|
-
rows.map do |row|
|
|
71
|
-
next if present.empty? || present.keys.any? { row[it].nil? }
|
|
72
|
-
|
|
73
|
-
score = present.sum { |name, weight| weight.abs * goodness(name, row[name], weight, rows) } / total
|
|
74
|
-
[score - penalty(row["errors"], present), 0.0].max
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# @return [String] the formula, with this lineup's weights, for the glossary
|
|
79
|
-
def description
|
|
80
|
-
"Σ(|weight| × goodness) / Σ|weight|, from 0% to 100%, and higher is better. A negative weight means that column is better low. " \
|
|
81
|
-
"Errors come off the total: 20 points for one, then 5 more for each further error. " \
|
|
82
|
-
"#{MAGNITUDES.join(", ")} are on a log scale within the group, so compare wscore within one table only. " \
|
|
83
|
-
"Weights: #{weights.map { |name, weight| "#{name} #{weight}" }.join(", ")}."
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
private
|
|
87
|
-
|
|
88
|
-
def penalty(errors, present)
|
|
89
|
-
return 0.0 unless present.key?("errors") && errors.positive?
|
|
90
|
-
|
|
91
|
-
ERROR_BASE + (ERROR_STEP * errors)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def goodness(name, value, weight, rows)
|
|
95
|
-
return 1.0 if name == "errors"
|
|
96
|
-
|
|
97
|
-
position = MAGNITUDES.include?(name) ? position(value, rows.filter_map { it[name] }) : value.to_f
|
|
98
|
-
return 1.0 if position.nil?
|
|
99
|
-
|
|
100
|
-
weight.negative? ? 1.0 - position : position
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
# A zero has no logarithm, so it stands at half the lowest above zero.
|
|
104
|
-
#
|
|
105
|
-
# @return [Float, nil] from 0 at the lowest to 1 at the highest; nil when all are equal
|
|
106
|
-
def position(value, values)
|
|
107
|
-
highest = values.max
|
|
108
|
-
return nil if values.min == highest
|
|
109
|
-
|
|
110
|
-
lowest = values.select(&:positive?).min
|
|
111
|
-
lowest /= 2.0 if values.any?(&:zero?)
|
|
112
|
-
(Math.log([value, lowest].max) - Math.log(lowest)) / (Math.log(highest) - Math.log(lowest))
|
|
113
|
-
end
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
end
|
|
File without changes
|
|
File without changes
|
|
File without changes
|