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.
Files changed (136) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/README.md +163 -87
  4. data/exe/llm-evals +60 -5
  5. data/lib/.DS_Store +0 -0
  6. data/lib/llm/.DS_Store +0 -0
  7. data/lib/llm/evals/actions/adapter.rb +50 -0
  8. data/lib/llm/evals/actions/error_budget.rb +63 -0
  9. data/lib/llm/evals/actions/error_diagnosis.rb +51 -0
  10. data/lib/llm/evals/actions/error_streak.rb +83 -0
  11. data/lib/llm/evals/actions/experiment.rb +251 -0
  12. data/lib/llm/evals/actions/verify.rb +61 -0
  13. data/lib/llm/evals/actions.rb +9 -0
  14. data/lib/llm/evals/cli/cases.rb +10 -0
  15. data/lib/llm/evals/cli/command.rb +32 -99
  16. data/lib/llm/evals/cli/compare.rb +0 -20
  17. data/lib/llm/evals/cli/contender_selector.rb +61 -0
  18. data/lib/llm/evals/cli/error_pause.rb +81 -0
  19. data/lib/llm/evals/cli/experiment_pool.rb +97 -0
  20. data/lib/llm/evals/cli/formatting.rb +8 -0
  21. data/lib/llm/evals/cli/leaderboard.rb +66 -12
  22. data/lib/llm/evals/cli/model_colors.rb +3 -0
  23. data/lib/llm/evals/cli/model_command.rb +152 -315
  24. data/lib/llm/evals/cli/model_options.rb +80 -0
  25. data/lib/llm/evals/cli/option_groups.rb +32 -0
  26. data/lib/llm/evals/cli/progress_bars.rb +92 -5
  27. data/lib/llm/evals/cli/render.rb +6 -6
  28. data/lib/llm/evals/cli/report_dispatcher/console.rb +65 -0
  29. data/lib/llm/evals/cli/report_dispatcher/html_output.rb +50 -0
  30. data/lib/llm/evals/cli/report_dispatcher/json_output.rb +29 -0
  31. data/lib/llm/evals/cli/report_dispatcher/output.rb +56 -0
  32. data/lib/llm/evals/cli/report_dispatcher/pipeline_output.rb +38 -0
  33. data/lib/llm/evals/cli/report_dispatcher.rb +133 -0
  34. data/lib/llm/evals/cli/run.rb +13 -22
  35. data/lib/llm/evals/cli/run_planner.rb +97 -0
  36. data/lib/llm/evals/cli/run_record.rb +68 -0
  37. data/lib/llm/evals/cli/standings.rb +39 -17
  38. data/lib/llm/evals/cli/status_box.rb +51 -0
  39. data/lib/llm/evals/cli/table.rb +103 -0
  40. data/lib/llm/evals/cli/text.rb +22 -0
  41. data/lib/llm/evals/cli/tracker_resolver.rb +52 -0
  42. data/lib/llm/evals/cli/upgrade_config.rb +60 -0
  43. data/lib/llm/evals/cli/verify.rb +5 -1
  44. data/lib/llm/evals/cli/version.rb +4 -0
  45. data/lib/llm/evals/cli/weights.rb +48 -0
  46. data/lib/llm/evals/cli.rb +39 -6
  47. data/lib/llm/evals/config/contender_builder.rb +30 -0
  48. data/lib/llm/evals/config/error_limits.rb +41 -0
  49. data/lib/llm/evals/config/listing.rb +153 -0
  50. data/lib/llm/evals/config/results.rb +42 -0
  51. data/lib/llm/evals/config/score_weights.rb +25 -0
  52. data/lib/llm/evals/config/upgrade.rb +107 -0
  53. data/lib/llm/evals/config.rb +90 -79
  54. data/lib/llm/evals/datasets/case.rb +46 -0
  55. data/lib/llm/evals/datasets/discovery.rb +143 -0
  56. data/lib/llm/evals/datasets.rb +9 -0
  57. data/lib/llm/evals/launcher.rb +57 -3
  58. data/lib/llm/evals/models/contender.rb +51 -0
  59. data/lib/llm/evals/models/model.rb +87 -0
  60. data/lib/llm/evals/models/throttle.rb +170 -0
  61. data/lib/llm/evals/models.rb +9 -0
  62. data/lib/llm/evals/providers/base.rb +61 -16
  63. data/lib/llm/evals/providers/catalog.rb +5 -2
  64. data/lib/llm/evals/providers/open_router.rb +54 -7
  65. data/lib/llm/evals/providers/registry.rb +49 -16
  66. data/lib/llm/evals/providers/type_safe/client.rb +67 -0
  67. data/lib/llm/evals/providers/type_safe.rb +113 -12
  68. data/lib/llm/evals/providers.rb +9 -0
  69. data/lib/llm/evals/render/reports/call_stats_table.rb +44 -0
  70. data/lib/llm/evals/render/reports/html/dataset_section.rb +51 -0
  71. data/lib/llm/evals/render/reports/html/format.rb +38 -0
  72. data/lib/llm/evals/render/reports/html/global_standings.rb +41 -0
  73. data/lib/llm/evals/render/reports/html/ranking.rb +86 -0
  74. data/lib/llm/evals/render/reports/html/view_model.rb +116 -0
  75. data/lib/llm/evals/render/reports/html.rb +60 -181
  76. data/lib/llm/evals/render/reports/json/decoder.rb +137 -0
  77. data/lib/llm/evals/render/reports/json/encoder.rb +106 -0
  78. data/lib/llm/evals/render/reports/json.rb +38 -141
  79. data/lib/llm/evals/render/reports/pipeline.rb +55 -20
  80. data/lib/llm/evals/render/reports/terminal.rb +8 -1
  81. data/lib/llm/evals/render.rb +9 -0
  82. data/lib/llm/evals/results/provider_response.rb +25 -0
  83. data/lib/llm/evals/results/reply.rb +38 -0
  84. data/lib/llm/evals/results/run_outcome.rb +15 -0
  85. data/lib/llm/evals/results/run_result.rb +40 -0
  86. data/lib/llm/evals/results/score.rb +45 -0
  87. data/lib/llm/evals/results/warnings.rb +149 -0
  88. data/lib/llm/evals/results.rb +9 -0
  89. data/lib/llm/evals/runtime/configuration.rb +295 -0
  90. data/lib/llm/evals/runtime/environment.rb +90 -0
  91. data/lib/llm/evals/runtime/helper.rb +153 -0
  92. data/lib/llm/evals/runtime.rb +2 -67
  93. data/lib/llm/evals/scoring/metric.rb +47 -0
  94. data/lib/llm/evals/scoring/schema_validator.rb +102 -0
  95. data/lib/llm/evals/scoring/scorer.rb +49 -0
  96. data/lib/llm/evals/scoring/weighted_score.rb +148 -0
  97. data/lib/llm/evals/scoring.rb +9 -0
  98. data/lib/llm/evals/stats/call_stats.rb +91 -0
  99. data/lib/llm/evals/stats/usage.rb +46 -0
  100. data/lib/llm/evals/stats.rb +29 -0
  101. data/lib/llm/evals/testing/echo_adapter.rb +14 -9
  102. data/lib/llm/evals/testing/fake_model.rb +8 -6
  103. data/lib/llm/evals/testing/fake_provider.rb +39 -8
  104. data/lib/llm/evals/testing.rb +9 -0
  105. data/lib/llm/evals/trackers/base.rb +110 -0
  106. data/lib/llm/evals/trackers/braintrust.rb +40 -3
  107. data/lib/llm/evals/trackers/local.rb +38 -2
  108. data/lib/llm/evals/trackers/logfire.rb +56 -3
  109. data/lib/llm/evals/trackers/registry.rb +29 -7
  110. data/lib/llm/evals/trackers.rb +9 -0
  111. data/lib/llm/evals/version.rb +3 -1
  112. data/lib/llm/evals.rb +17 -7
  113. metadata +91 -24
  114. data/lib/llm/evals/adapter.rb +0 -47
  115. data/lib/llm/evals/case.rb +0 -36
  116. data/lib/llm/evals/completion.rb +0 -11
  117. data/lib/llm/evals/configuration.rb +0 -173
  118. data/lib/llm/evals/contender.rb +0 -31
  119. data/lib/llm/evals/discovery.rb +0 -95
  120. data/lib/llm/evals/experiment.rb +0 -158
  121. data/lib/llm/evals/helper.rb +0 -98
  122. data/lib/llm/evals/metric.rb +0 -26
  123. data/lib/llm/evals/model.rb +0 -55
  124. data/lib/llm/evals/reply.rb +0 -18
  125. data/lib/llm/evals/run_result.rb +0 -14
  126. data/lib/llm/evals/schema_validator.rb +0 -84
  127. data/lib/llm/evals/score.rb +0 -26
  128. data/lib/llm/evals/scorer.rb +0 -45
  129. data/lib/llm/evals/tracker.rb +0 -80
  130. data/lib/llm/evals/usage.rb +0 -33
  131. data/lib/llm/evals/verify.rb +0 -49
  132. data/lib/llm/evals/warnings.rb +0 -98
  133. data/lib/llm/evals/weighted_score.rb +0 -116
  134. /data/lib/llm/evals/{templates → render/templates}/html_report.css +0 -0
  135. /data/lib/llm/evals/{templates → render/templates}/html_report.html.erb +0 -0
  136. /data/lib/llm/evals/{templates → render/templates}/html_report.js +0 -0
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: llm-evals
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Gredeskoul
@@ -163,6 +163,20 @@ dependencies:
163
163
  - - "~>"
164
164
  - !ruby/object:Gem::Version
165
165
  version: '1.6'
166
+ - !ruby/object:Gem::Dependency
167
+ name: ruby-limiter
168
+ requirement: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - "~>"
171
+ - !ruby/object:Gem::Version
172
+ version: '2.3'
173
+ type: :runtime
174
+ prerelease: false
175
+ version_requirements: !ruby/object:Gem::Requirement
176
+ requirements:
177
+ - - "~>"
178
+ - !ruby/object:Gem::Version
179
+ version: '2.3'
166
180
  description: |2-
167
181
 
168
182
  Run multi-case evaluation suites against datasets using any model available
@@ -186,64 +200,117 @@ files:
186
200
  - README.md
187
201
  - exe/evals
188
202
  - exe/llm-evals
203
+ - lib/.DS_Store
204
+ - lib/llm/.DS_Store
189
205
  - lib/llm/evals.rb
190
- - lib/llm/evals/adapter.rb
191
- - lib/llm/evals/case.rb
206
+ - lib/llm/evals/actions.rb
207
+ - lib/llm/evals/actions/adapter.rb
208
+ - lib/llm/evals/actions/error_budget.rb
209
+ - lib/llm/evals/actions/error_diagnosis.rb
210
+ - lib/llm/evals/actions/error_streak.rb
211
+ - lib/llm/evals/actions/experiment.rb
212
+ - lib/llm/evals/actions/verify.rb
192
213
  - lib/llm/evals/cli.rb
193
214
  - lib/llm/evals/cli/cases.rb
194
215
  - lib/llm/evals/cli/command.rb
195
216
  - lib/llm/evals/cli/compare.rb
217
+ - lib/llm/evals/cli/contender_selector.rb
218
+ - lib/llm/evals/cli/error_pause.rb
219
+ - lib/llm/evals/cli/experiment_pool.rb
196
220
  - lib/llm/evals/cli/formatting.rb
197
221
  - lib/llm/evals/cli/leaderboard.rb
198
222
  - lib/llm/evals/cli/model_colors.rb
199
223
  - lib/llm/evals/cli/model_command.rb
224
+ - lib/llm/evals/cli/model_options.rb
225
+ - lib/llm/evals/cli/option_groups.rb
200
226
  - lib/llm/evals/cli/progress_bars.rb
201
227
  - lib/llm/evals/cli/render.rb
228
+ - lib/llm/evals/cli/report_dispatcher.rb
229
+ - lib/llm/evals/cli/report_dispatcher/console.rb
230
+ - lib/llm/evals/cli/report_dispatcher/html_output.rb
231
+ - lib/llm/evals/cli/report_dispatcher/json_output.rb
232
+ - lib/llm/evals/cli/report_dispatcher/output.rb
233
+ - lib/llm/evals/cli/report_dispatcher/pipeline_output.rb
202
234
  - lib/llm/evals/cli/run.rb
235
+ - lib/llm/evals/cli/run_planner.rb
236
+ - lib/llm/evals/cli/run_record.rb
203
237
  - lib/llm/evals/cli/standings.rb
238
+ - lib/llm/evals/cli/status_box.rb
239
+ - lib/llm/evals/cli/table.rb
240
+ - lib/llm/evals/cli/text.rb
241
+ - lib/llm/evals/cli/tracker_resolver.rb
242
+ - lib/llm/evals/cli/upgrade_config.rb
204
243
  - lib/llm/evals/cli/verify.rb
205
244
  - lib/llm/evals/cli/version.rb
206
- - lib/llm/evals/completion.rb
245
+ - lib/llm/evals/cli/weights.rb
207
246
  - lib/llm/evals/config.rb
208
- - lib/llm/evals/configuration.rb
209
- - lib/llm/evals/contender.rb
210
- - lib/llm/evals/discovery.rb
211
- - lib/llm/evals/experiment.rb
212
- - lib/llm/evals/helper.rb
247
+ - lib/llm/evals/config/contender_builder.rb
248
+ - lib/llm/evals/config/error_limits.rb
249
+ - lib/llm/evals/config/listing.rb
250
+ - lib/llm/evals/config/results.rb
251
+ - lib/llm/evals/config/score_weights.rb
252
+ - lib/llm/evals/config/upgrade.rb
253
+ - lib/llm/evals/datasets.rb
254
+ - lib/llm/evals/datasets/case.rb
255
+ - lib/llm/evals/datasets/discovery.rb
213
256
  - lib/llm/evals/launcher.rb
214
- - lib/llm/evals/metric.rb
215
- - lib/llm/evals/model.rb
257
+ - lib/llm/evals/models.rb
258
+ - lib/llm/evals/models/contender.rb
259
+ - lib/llm/evals/models/model.rb
260
+ - lib/llm/evals/models/throttle.rb
261
+ - lib/llm/evals/providers.rb
216
262
  - lib/llm/evals/providers/base.rb
217
263
  - lib/llm/evals/providers/catalog.rb
218
264
  - lib/llm/evals/providers/open_router.rb
219
265
  - lib/llm/evals/providers/registry.rb
220
266
  - lib/llm/evals/providers/type_safe.rb
267
+ - lib/llm/evals/providers/type_safe/client.rb
268
+ - lib/llm/evals/render.rb
269
+ - lib/llm/evals/render/reports/call_stats_table.rb
221
270
  - lib/llm/evals/render/reports/html.rb
271
+ - lib/llm/evals/render/reports/html/dataset_section.rb
272
+ - lib/llm/evals/render/reports/html/format.rb
273
+ - lib/llm/evals/render/reports/html/global_standings.rb
274
+ - lib/llm/evals/render/reports/html/ranking.rb
275
+ - lib/llm/evals/render/reports/html/view_model.rb
222
276
  - lib/llm/evals/render/reports/json.rb
277
+ - lib/llm/evals/render/reports/json/decoder.rb
278
+ - lib/llm/evals/render/reports/json/encoder.rb
223
279
  - lib/llm/evals/render/reports/pipeline.rb
224
280
  - lib/llm/evals/render/reports/terminal.rb
225
- - lib/llm/evals/reply.rb
226
- - lib/llm/evals/run_result.rb
281
+ - lib/llm/evals/render/templates/html_report.css
282
+ - lib/llm/evals/render/templates/html_report.html.erb
283
+ - lib/llm/evals/render/templates/html_report.js
284
+ - lib/llm/evals/results.rb
285
+ - lib/llm/evals/results/provider_response.rb
286
+ - lib/llm/evals/results/reply.rb
287
+ - lib/llm/evals/results/run_outcome.rb
288
+ - lib/llm/evals/results/run_result.rb
289
+ - lib/llm/evals/results/score.rb
290
+ - lib/llm/evals/results/warnings.rb
227
291
  - lib/llm/evals/runtime.rb
228
- - lib/llm/evals/schema_validator.rb
229
- - lib/llm/evals/score.rb
230
- - lib/llm/evals/scorer.rb
231
- - lib/llm/evals/templates/html_report.css
232
- - lib/llm/evals/templates/html_report.html.erb
233
- - lib/llm/evals/templates/html_report.js
292
+ - lib/llm/evals/runtime/configuration.rb
293
+ - lib/llm/evals/runtime/environment.rb
294
+ - lib/llm/evals/runtime/helper.rb
295
+ - lib/llm/evals/scoring.rb
296
+ - lib/llm/evals/scoring/metric.rb
297
+ - lib/llm/evals/scoring/schema_validator.rb
298
+ - lib/llm/evals/scoring/scorer.rb
299
+ - lib/llm/evals/scoring/weighted_score.rb
300
+ - lib/llm/evals/stats.rb
301
+ - lib/llm/evals/stats/call_stats.rb
302
+ - lib/llm/evals/stats/usage.rb
303
+ - lib/llm/evals/testing.rb
234
304
  - lib/llm/evals/testing/echo_adapter.rb
235
305
  - lib/llm/evals/testing/fake_model.rb
236
306
  - lib/llm/evals/testing/fake_provider.rb
237
- - lib/llm/evals/tracker.rb
307
+ - lib/llm/evals/trackers.rb
308
+ - lib/llm/evals/trackers/base.rb
238
309
  - lib/llm/evals/trackers/braintrust.rb
239
310
  - lib/llm/evals/trackers/local.rb
240
311
  - lib/llm/evals/trackers/logfire.rb
241
312
  - lib/llm/evals/trackers/registry.rb
242
- - lib/llm/evals/usage.rb
243
- - lib/llm/evals/verify.rb
244
313
  - lib/llm/evals/version.rb
245
- - lib/llm/evals/warnings.rb
246
- - lib/llm/evals/weighted_score.rb
247
314
  homepage: https://github.com/kigster/llm-evals
248
315
  licenses:
249
316
  - MIT
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # What a suite implements to run one case: everything the model sees, and
6
- # everything done to its answer before it is scored. An adapter never
7
- # scores; that is the {Scorer}'s, and keeping them apart is what lets one
8
- # suite compare prompts and another compare models with the same runner.
9
- #
10
- # @abstract Subclass and override {#call}.
11
- class Adapter
12
- # Called once per dataset before its cases run; load what the dataset
13
- # shares (a schema, a flow definition) here, outside the threads.
14
- #
15
- # @param dataset [String]
16
- # @return [void]
17
- def prepare(_dataset) = nil
18
-
19
- # Runs one case against one model.
20
- #
21
- # @param kase [Case]
22
- # @param model [Model]
23
- # @return [Reply] with `error:` set when the model failed to answer; raise for a transport failure
24
- def call(kase, model:)
25
- raise NotImplementedError, "#{self.class}#call(kase, model:) must return an LLM::Evals::Reply"
26
- end
27
-
28
- # What the scorer needs to know about a case beyond the case itself: a
29
- # schema, field types, the prompt. Passed to {Scorer#call} as `context:`.
30
- #
31
- # @param kase [Case]
32
- # @return [Hash]
33
- def context(_kase) = {}
34
-
35
- # Offline checks of a dataset's shared files. `llm-evals verify` prints them.
36
- #
37
- # @param dataset [String]
38
- # @return [Array<String>] problems; empty when the dataset is sound
39
- def verify(_dataset) = []
40
-
41
- # Recorded with each Braintrust experiment, e.g. which prompt version ran.
42
- #
43
- # @return [Hash]
44
- def metadata = {}
45
- end
46
- end
47
- end
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # One eval case, as {Discovery} found it on disk: the text the model is
6
- # given, the answer a perfect reply would produce, and where it came from.
7
- #
8
- # @!attribute [r] id
9
- # @return [String] unique within its dataset, e.g. "clear/003"
10
- # @!attribute [r] dataset
11
- # @return [String] the folder under datasets/, e.g. "colors"
12
- # @!attribute [r] variant
13
- # @return [String] the folder under cases/, e.g. "clear" or "vague"
14
- # @!attribute [r] input
15
- # @return [String] input.txt, stripped
16
- # @!attribute [r] expected
17
- # @return [Object, nil] expected.json parsed; nil when missing or unparsable
18
- # @!attribute [r] path
19
- # @return [String] the case's directory
20
- # @!attribute [r] metadata
21
- # @return [Hash{Symbol => Object}] number, difficulty, and any `expected_error`
22
- Case = Data.define(:id, :dataset, :variant, :input, :expected, :path, :metadata) do
23
- # @return [Integer, nil] the NNN/ level as a number, when the case has one
24
- def number = metadata[:number]
25
-
26
- # @return [Integer] the number, else 0; what the tables sort and show difficulty by
27
- def difficulty = metadata.fetch(:difficulty, number || 0)
28
-
29
- # @return [String] "dataset/id", unique across the suite
30
- def key = "#{dataset}/#{id}"
31
-
32
- # @return [Boolean] whether expected.json was there and parsed
33
- def expected? = !expected.nil?
34
- end
35
- end
36
- end
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # What a {Model} returns from one call: the text, what it cost, and the
6
- # provider's raw response for an adapter that needs more than the text.
7
- Completion = Data.define(:text, :usage, :raw) do
8
- def initialize(text:, usage: Usage.zero, raw: nil) = super(text: text.to_s, usage: Usage.from(usage), raw: raw)
9
- end
10
- end
11
- end
@@ -1,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # Everything a suite tells the gem, through {LLM::Evals.configure}.
6
- # Paths are relative to `root`, the directory the eval helper lives in.
7
- class Configuration
8
- DEFAULT_GLOB = "datasets/*/cases/**/input.txt"
9
- DEFAULT_NAME = "llm-evals"
10
- DEFAULT_CONFIG = ".llm-evals.config.yml"
11
-
12
- attr_writer :adapter, :scorer
13
- attr_reader :name, :root, :env_files, :cases_glob, :hooks, :providers, :trackers, :description
14
-
15
- def initialize
16
- @name = DEFAULT_NAME
17
- @root = Dir.pwd
18
- @env_files = []
19
- @cases_glob = DEFAULT_GLOB
20
- @config_path = DEFAULT_CONFIG
21
- @hooks = []
22
- @providers = Providers::Registry.default
23
- @trackers = Trackers::Registry.default
24
- @description = "Run a suite of cases against one or more models and score the replies."
25
- @project = nil
26
- end
27
-
28
- # Names the suite. The name titles the help and tags the experiments.
29
- #
30
- # @param name [String]
31
- # @param root [String] where .llm-evals.config.yml and datasets/ live; the helper's directory
32
- # @param env_files [Array<String>] dotenv files to load, relative to root; the first that exists wins nothing,
33
- # all that exist load, and an exported variable beats them all
34
- # @param description [String, nil] one line for the help's header
35
- # @return [void]
36
- def suite(name, root:, env_files: [], description: nil)
37
- @name = name
38
- @root = File.expand_path(root)
39
- @env_files = env_files.map { File.expand_path(it, @root) }
40
- @description = description if description
41
- end
42
-
43
- # Where the cases are: a glob relative to root ending in the input file.
44
- # Each match is a case; the expected answer is `expected.json` beside it.
45
- #
46
- # @param glob [String]
47
- # @return [void]
48
- def cases(glob)
49
- @cases_glob = glob
50
- end
51
-
52
- # @param object [Adapter]
53
- # @return [void]
54
-
55
- # @param object [Adapter, nil] setting; reading when nil
56
- # @return [Adapter]
57
- def adapter(object = nil)
58
- @adapter = object unless object.nil?
59
- @adapter
60
- end
61
-
62
- # @param object [Scorer, nil] setting; reading when nil
63
- # @return [Scorer]
64
- def scorer(object = nil)
65
- @scorer = object unless object.nil?
66
- @scorer
67
- end
68
-
69
- # @param object [Scorer]
70
- # @return [void]
71
-
72
- # Runs once, after the env files are loaded and before any model is
73
- # called. The place to reset a library that read ENV at require time.
74
- #
75
- # @yield
76
- # @return [void]
77
- def before_run(&block)
78
- @hooks << block
79
- end
80
-
81
- # Adds a provider, ahead of the defaults, so a suite's own transport
82
- # wins the ids it claims.
83
- #
84
- # @param provider [Providers::Base]
85
- # @return [void]
86
- def provider(provider)
87
- @providers.register(provider)
88
- end
89
-
90
- # Adds a tracker class, so a run can choose it by name with `--tracker`
91
- # or `tracker:`, or have its env key select it.
92
- #
93
- # @param klass [Class<Tracker>]
94
- # @return [void]
95
- def tracker(klass)
96
- @trackers.register(klass)
97
- end
98
-
99
- # @param path [String, nil] setting, relative to root; reading when nil
100
- # @return [String] the absolute path of the lineup file, `.llm-evals.config.yml` at the root by default
101
- def config(path = nil)
102
- @config_path = path unless path.nil?
103
- File.expand_path(@config_path, root)
104
- end
105
-
106
- # @param name [String, nil] setting; reading when nil
107
- # @return [String, nil] the Braintrust project, when .llm-evals.config.yml does not name one
108
- def project(name = nil)
109
- @project = name unless name.nil?
110
- @project
111
- end
112
-
113
- # @return [Discovery] the cases, as the glob finds them
114
- def discovery = Discovery.new(root: root, glob: cases_glob)
115
-
116
- # @return [Config] the lineup file
117
- def lineup = Config.new(config, direct_providers: providers.direct_names)
118
-
119
- # @return [Runtime]
120
- def runtime = Runtime.new(env_files: env_files, hooks: hooks, app_name: name)
121
-
122
- # @return [Boolean] whether an adapter and a scorer are registered
123
- def complete? = !adapter.nil? && !scorer.nil?
124
-
125
- # Fails a command that needs the suite when no helper was loaded, with
126
- # the fix in the message, instead of a NoMethodError on nil.
127
- #
128
- # @return [self]
129
- # @raise [Error]
130
- def require_suite!
131
- resolve_from_config!
132
- return self if complete?
133
-
134
- raise Error,
135
- "no suite is configured: register an adapter and a scorer in evals/eval_helper.rb, or set " \
136
- "adapter: and scorer: (with require:) in #{config}; --helper PATH or LLM_EVALS_HELPER names another helper"
137
- end
138
-
139
- # Fills whatever the helper left unset from the config file's `require:`,
140
- # `adapter:` and `scorer:` keys, so a suite can be config-only: its Ruby
141
- # lives in its own gem and `evals/` holds the file and the datasets. The
142
- # helper, when there is one, was loaded first and wins.
143
- #
144
- # @return [void]
145
- # @raise [Error] naming the key and the value when a require fails or a constant is unknown
146
- def resolve_from_config!
147
- return if complete? || !File.exist?(config)
148
-
149
- file = lineup
150
- file.requires.each do |feature|
151
- require feature
152
- rescue LoadError => e
153
- raise Error, "#{config}: require: #{feature} could not be loaded (#{e.message}); is its gem in the bundle?"
154
- end
155
- @adapter ||= instantiate("adapter", file.adapter_name)
156
- @scorer ||= instantiate("scorer", file.scorer_name) # rubocop:disable Naming/MemoizedInstanceVariableName
157
- end
158
-
159
- private
160
-
161
- # @return [Object, nil] a new instance of the named constant; nil when no name is set
162
- def instantiate(key, name)
163
- return nil if name.nil? || name.empty?
164
-
165
- Object.const_get(name).new
166
- rescue NameError, ArgumentError => e
167
- raise Error,
168
- "#{config}: #{key}: #{name} could not be instantiated (#{e.class}: #{e.message}); " \
169
- "name a constant the require: key loads, taking no arguments"
170
- end
171
- end
172
- end
173
- end
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # One model at one setting: what a single experiment runs. Built by
6
- # {Config} from the lineup file, or from `-m` on the command line.
7
- #
8
- # `provider` is nil for a `vendors:` entry, which OpenRouter serves, and
9
- # the provider's name for a `direct:` entry. It travels with the
10
- # contender rather than being read back out of the id, so `typesafe/jev-router`
11
- # (Jev on OpenRouter) and a direct `typesafe/jev-latest` can never be confused.
12
- Contender = Data.define(:model, :effort, :temperature, :max_tokens, :provider) do
13
- def initialize(model:, effort: "default", temperature: "default", max_tokens: nil, provider: nil) = super
14
-
15
- # @return [Boolean] whether the entry came from `direct:`
16
- def direct? = !provider.nil?
17
-
18
- # @return [String] unique across the lineup: "typesafe:typesafe/jev-latest" for a direct entry
19
- def key = direct? ? "#{provider}:#{model}" : model
20
-
21
- # The settings as a {Model} takes them: "default" means nil, send nothing.
22
- #
23
- # @return [Hash{Symbol => Object}]
24
- def settings
25
- { effort: effort.to_s == "default" || effort.nil? ? nil : effort.to_s,
26
- temperature: temperature.to_s == "default" || temperature.nil? ? nil : Float(temperature),
27
- max_tokens: max_tokens.nil? ? nil : Integer(max_tokens) }
28
- end
29
- end
30
- end
31
- end
@@ -1,95 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module LLM
4
- module Evals
5
- # Finds the cases under a root with a glob and decodes each path into a
6
- # {Case}. The default layout is
7
- #
8
- # datasets/<dataset>/cases/<variant>/[NNN/]input.txt
9
- # datasets/<dataset>/cases/<variant>/[NNN/]expected.json
10
- #
11
- # The folder after `datasets/` is the dataset, the folder holding the
12
- # input is the variant, unless it is a number, in which case it is the
13
- # case number and its parent is the variant.
14
- class Discovery
15
- EXPECTED = "expected.json"
16
- METADATA = "metadata.json"
17
-
18
- attr_reader :root, :glob
19
-
20
- # @param root [String] an absolute directory
21
- # @param glob [String] relative to root
22
- def initialize(root:, glob: Configuration::DEFAULT_GLOB)
23
- @root = root
24
- @glob = glob
25
- end
26
-
27
- # @return [Array<String>] every dataset that has at least one case, sorted
28
- def datasets = all.map(&:dataset).uniq.sort
29
-
30
- # @param dataset [String, nil] one dataset; nil for all of them
31
- # @param only [Array<String>] id prefixes to keep ("clear", "clear/00"); empty keeps all
32
- # @return [Array<Case>] sorted by dataset, variant and number
33
- # @raise [Error] when the dataset does not exist
34
- def cases(dataset: nil, only: [])
35
- found = all
36
- if dataset
37
- raise Error, "no dataset #{dataset.inspect}; have #{datasets.join(", ")}" unless datasets.include?(dataset)
38
-
39
- found = found.select { it.dataset == dataset }
40
- end
41
- only = only.flat_map { it.split(",") }.map(&:strip).reject(&:empty?)
42
- found = found.select { |kase| only.any? { kase.id.start_with?(it) || kase.key.start_with?(it) } } unless only.empty?
43
- found
44
- end
45
-
46
- # @return [Array<Case>]
47
- def all
48
- @all ||= Dir[File.join(root, glob)].map { decode(it) }
49
- end
50
-
51
- # Builds the case at one input file.
52
- #
53
- # @param input_path [String]
54
- # @return [Case]
55
- def decode(input_path)
56
- dir = File.dirname(input_path)
57
- parts = Pathname(dir).relative_path_from(Pathname(root)).each_filename.to_a
58
- number = parts.last.match?(/\A\d+\z/) ? parts.last : nil
59
- variant = number ? parts[-2] : parts.last
60
- expected, problem = read_expected(File.join(dir, EXPECTED))
61
- Case.new(
62
- id: [variant, number].compact.join("/"),
63
- dataset: dataset_of(parts),
64
- variant: variant,
65
- input: File.read(input_path).strip,
66
- expected: expected,
67
- path: dir,
68
- metadata: metadata(dir, number, problem)
69
- )
70
- end
71
-
72
- private
73
-
74
- # The folder after `datasets/`, else the first folder, so a glob that
75
- # does not say `datasets` still groups by its top level.
76
- def dataset_of(parts)
77
- index = parts.index("datasets")
78
- index && parts[index + 1] ? parts[index + 1] : parts.first
79
- end
80
-
81
- def read_expected(path)
82
- return [nil, "#{EXPECTED} is missing"] unless File.exist?(path)
83
-
84
- [JSON.parse(File.read(path)), nil]
85
- rescue JSON::ParserError => e
86
- [nil, "#{EXPECTED} is not valid JSON: #{e.message.lines.first&.strip}"]
87
- end
88
-
89
- def metadata(dir, number, problem)
90
- extra = File.exist?(File.join(dir, METADATA)) ? JSON.parse(File.read(File.join(dir, METADATA))).transform_keys(&:to_sym) : {}
91
- { number: number&.to_i, difficulty: number.to_i, expected_error: problem }.compact.merge(extra)
92
- end
93
- end
94
- end
95
- end