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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1df9a8ec28e61b18c3b929fbf1aff863f01b4083e762dcc8dfa24b0a4c107da7
|
|
4
|
+
data.tar.gz: ae89677e193f2c6369663df56d92276de19b2d4be3de5c83502612840f79d96e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 719e4775de99d347928c359df4a92ae253c5686a153e9970acb02e0baf23a8dd17c0811e2d6476993f1b563b1bef28d02efc721a0b44440a47db25999fc90bec
|
|
7
|
+
data.tar.gz: 1cd81a2ca9d699ca1b1f15ce4ac3fca1af31288ada47e4fbff395255ad997e04d2a4e9b37e7303696c4b0fe9b2c1bb7dce22b2b133ca93a9aa7f5d8c54bfc8a3
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
- Breaking: `results.display` now defaults to `both`: every run prints the terminal summary, builds the HTML report from its JSON and opens it in the browser. Use `--display tui` or `--no-open` to turn that off.
|
|
6
|
+
|
|
7
|
+
- Group the `run` and `compare` flags in `--help` under General, TUI and Browser Report.
|
|
8
|
+
|
|
9
|
+
- Breaking: move every class out of the top of `LLM::Evals` into a module of its own. There are no aliases; rename as follows:
|
|
10
|
+
|
|
11
|
+
| Module | Constants |
|
|
12
|
+
| ---------------------- | ---------------------------------------------------------------------------------------------- |
|
|
13
|
+
| `LLM::Evals::Runtime` | `Configuration`, `Helper`; `Runtime` is now `Runtime::Environment` |
|
|
14
|
+
| `LLM::Evals::Datasets` | `Case`, `Discovery` |
|
|
15
|
+
| `LLM::Evals::Scoring` | `Scorer`, `Metric`, `WeightedScore`, `SchemaValidator` |
|
|
16
|
+
| `LLM::Evals::Models` | `Model`, `Contender`, `Throttle` |
|
|
17
|
+
| `LLM::Evals::Actions` | `Experiment`, `Adapter`, `Verify`, `ErrorBudget` |
|
|
18
|
+
| `LLM::Evals::Results` | `Reply`, `Score`, `ProviderResponse` (was `Completion`), `RunResult`, `RunOutcome`, `Warnings` |
|
|
19
|
+
| `LLM::Evals::Stats` | `CallStats`, `Usage` (`Stats` itself stays) |
|
|
20
|
+
|
|
21
|
+
`LLM::Evals::Tracker` is now `LLM::Evals::Trackers::Base`, and `LLM::Evals::ConfigUpgrade` is `LLM::Evals::Config::Upgrade`. `LLM::Evals.configure` and `.configuration` are unchanged.
|
|
22
|
+
|
|
23
|
+
- Add `Model#ask` and `Providers::TypeSafe#ask`: typed questions to TypeSafe's API, through the same limits and retries.
|
|
24
|
+
|
|
25
|
+
- Add `evals upgrade-config`, which rewrites a lineup to the current format, keeping its comments and leaving `<file>.bak`.
|
|
26
|
+
|
|
27
|
+
- Breaking: the lineup lists models under a provider section (`openrouter: { vendors: ... }`, `typesafe: { models: ... }`). Top-level `vendors:` and `direct:` are errors.
|
|
28
|
+
|
|
29
|
+
- Add `rate_limit: { burst, per_minute }` per provider, vendor and model, through `ruby-limiter`. Waiting is left out of `time`.
|
|
30
|
+
|
|
31
|
+
- Turn RubyLLM's retries off and retry here, honoring `Retry-After` across a vendor's calls, up to 5 attempts.
|
|
32
|
+
|
|
33
|
+
- Add `errors.abort_after` to the lineup: a run stops once it has `total` errors and, if set, `percent` of finished calls have failed. Partial results are kept and the command exits non-zero.
|
|
34
|
+
|
|
35
|
+
- Add `evals weights`, which prints each weighted column's direction, weight and real share of the score, and names in red any scorer metric the weights leave out.
|
|
36
|
+
|
|
37
|
+
- Report calls per provider, vendor and model: requests, errors, requests per second, average and 95th percentile latency, in the terminal and as `call_stats` in the JSON. Add `latency` and `p95` as columns `weighted_score` can weight.
|
|
38
|
+
|
|
39
|
+
- Draw failed cases in red on the progress bars of `run` and `compare`, so a failing model or account shows while the run is still going.
|
|
40
|
+
|
|
3
41
|
## 0.4.0 (2026-10-30)
|
|
4
42
|
|
|
5
43
|
Renamed from `agent-evals`: gem `llm-evals`, namespace `LLM::Evals`, executable `evals` (with `llm-evals` as an identical spare, for a system where `evals` is taken; shell completion targets `evals`), lineup file `.llm-evals.config.yml`. The old names are gone. The version continues from `agent-evals` 0.3.1.
|
data/README.md
CHANGED
|
@@ -8,15 +8,20 @@ A flexible CLI for running multi-case evaluation suites and comparing LLMs and p
|
|
|
8
8
|
|
|
9
9
|
> [!IMPORTANT]
|
|
10
10
|
> This gem is not affiliated with the excellent [RubyLLM](https://rubyllm.com/) gem, though it uses RubyLLM under the hood.
|
|
11
|
-
|
|
12
|
-
> [!NOTE]
|
|
13
|
-
> The `llm-evals` gem installs two commands: `evals` and `llm-evals`. Use `llm-evals` if another installed package already provides the `evals` command. The CLI and runner use [Dry.rb](https://dry-rb.org/), and the default model connection uses OpenRouter.
|
|
14
11
|
>
|
|
15
|
-
> The gem
|
|
12
|
+
> The `llm-evals` gem installs two commands: `evals` and `llm-evals`. Use `llm-evals` if another installed package already provides the `evals` command. The CLI and the runner uses [dry-rb](https://dry-rb.org/) and my extensions [dry-cli.tools](https://dry-cli.tools).
|
|
13
|
+
>
|
|
14
|
+
> In order run evals against pretty much any model, you need an account, with credits, on an [OpenRouter](https://openrouter.ai), and set the `OPENROUTER_API_KEY` to the key generated in OpenRouter.
|
|
16
15
|
>
|
|
17
|
-
>
|
|
16
|
+
> If you also want to include [Typesafe's Jev model](https://typesafe.ai/blog/introducing-system-one-models-and-jev), this gem has a direct connector for Jev, so you'll need to set `TYPESAFE_API_KEY` in your environment as well.
|
|
17
|
+
|
|
18
|
+
______________________________________________________________________
|
|
19
|
+
|
|
20
|
+
The gem also supports OpenTelemetry and can send evaluation data to services such as [Braintrust](https://braintrust.ai) and [W&B Weave](https://wandb.ai/site/weave/). Some design ideas came from [Pydantic Evals](https://pydantic.dev/logfire/evals).
|
|
21
|
+
|
|
22
|
+
> _I originally developed this gem for my fremium product [Qualified.At](https://qualified.at), a Ruby on Rails SaaS product._
|
|
18
23
|
|
|
19
|
-
## Evals are unit tests for probabilistic software
|
|
24
|
+
## Evals are unit tests for probabilistic software made by AI
|
|
20
25
|
|
|
21
26
|
An eval checks whether a model meets the behavior you expect on a set of representative cases. Repeating the same cases lets you measure how prompts, parameters, and models affect the results. Your scorer decides whether those results are good; the eval makes the comparison consistent.
|
|
22
27
|
|
|
@@ -29,6 +34,8 @@ An eval checks whether a model meets the behavior you expect on a set of represe
|
|
|
29
34
|
- [Independent Eval Research](https://www.braintrust.dev/evals) (Braintrust)
|
|
30
35
|
- [How to Build an LLM Eval Pipeline in GitHub Actions](https://www.braintrust.dev/articles/llm-eval-pipeline-github-actions) (Braintrust)
|
|
31
36
|
|
|
37
|
+
______________________________________________________________________
|
|
38
|
+
|
|
32
39
|
### A real-world example: Qualified.At
|
|
33
40
|
|
|
34
41
|
> [!NOTE]
|
|
@@ -49,16 +56,50 @@ A tax customer might write: *"I'm married with three children, filing jointly, h
|
|
|
49
56
|
When we built Qualified.At, it wasn't clear which provider or model would extract answers most accurately from unstructured text and map them to a JSON schema. Accuracy matters, but so do latency and cost. At 1,000 leads, a cost of $0.0002 per call adds up to $0.20; a cost of $0.50 per call adds up to $500.
|
|
50
57
|
|
|
51
58
|
> [!IMPORTANT]
|
|
52
|
-
>
|
|
59
|
+
> So, **which model captures the most answers**, **assigns them correctly**, **avoids hallucinations**, and **doesn't miss details**? We want accuracy with low cost and low latency. If a user waits 30 seconds to get to the next question, that's a problem. **That very question is why I built this gem.**
|
|
60
|
+
|
|
61
|
+
______________________________________________________________________
|
|
62
|
+
|
|
63
|
+
## Weighted Scoring
|
|
64
|
+
|
|
65
|
+
Quite importantly, and often skipped over in the Eval design, is a concept of a **single weighted score**, that attaches fixed weights to various metrics.
|
|
66
|
+
|
|
67
|
+
Evals are naturally a **multi-variate optimization problem**, something that has been solved in statistics and operations research many decades ago.
|
|
53
68
|
|
|
54
|
-
|
|
69
|
+
> [!NOTE]
|
|
70
|
+
> So if the **cost** is the most important factor, we give the cost the largest weight. If the **accuracy** is the most important, then it gets the highest weight. The weights configuration handles metrics that improve when they increase, as well as those that improve when they decrease.
|
|
71
|
+
|
|
72
|
+
Here is a weight section definitions from the YAML configuration file `evals/.llm-evals.config.yml` (see below) that generated the HTML report which follows:
|
|
73
|
+
|
|
74
|
+

|
|
55
75
|
|
|
56
|
-
|
|
76
|
+
## Running Evals
|
|
77
|
+
|
|
78
|
+
Considering your `evals` folder is setup correctly, and the config file lives right under the `evals`, as `.llm-evals.config.yml`, you should just be able to run:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export OPENROUTER_API_KEY=....
|
|
82
|
+
export TYPESAFE_API_KEY=...
|
|
83
|
+
|
|
84
|
+
evals run
|
|
85
|
+
```
|
|
57
86
|
|
|
58
87
|
> [!NOTE]
|
|
59
|
-
>
|
|
88
|
+
> You need just those two because the evals go through ***OpenRouter***, and the only exception at the moment is Jev, which has a direct driver.
|
|
89
|
+
|
|
90
|
+
The following is a screenshot of an HTML report from running evals in the [Qualified.At](https://qualified.at) Rails application, which compares various models on accuracy, completeness, latency, cost, and other metrics for the extraction task of the first question the lead answers.
|
|
91
|
+
|
|
92
|
+
I set the *temperature* to 0.1 to get the least "creative" and most "precise" results, and for most models I am trying two sets of effort: "*high*" and "*low*":
|
|
93
|
+
|
|
94
|
+

|
|
60
95
|
|
|
61
|
-
|
|
96
|
+
### A Specific Experiment
|
|
97
|
+
|
|
98
|
+
While the above shows the summarized results across 10 DSL examples, each with 4 prompts (so 40 total), the following screenshot from the same HTML report shows just one DSL use-case (tax collection) and how different the results are.
|
|
99
|
+
|
|
100
|
+
I've highlighted in green the top models, their low pricing, and the overall winner for tax preparation — and not that in this case it's not the same model as the global winner. This is important.
|
|
101
|
+
|
|
102
|
+

|
|
62
103
|
|
|
63
104
|
## Datasets
|
|
64
105
|
|
|
@@ -142,14 +183,20 @@ The recipes change into each suite's folder and set `RUBYLIB` for the config-onl
|
|
|
142
183
|
|
|
143
184
|
### Where to run from
|
|
144
185
|
|
|
145
|
-
`llm-evals` finds a suite by searching **upward from the current directory** for `evals/eval_helper.rb` or `evals/.llm-evals.config.yml`. The repository root
|
|
186
|
+
`llm-evals` finds a suite by searching **upward from the current directory** for `evals/eval_helper.rb` or `evals/.llm-evals.config.yml`. If that finds nothing, it searches **up to four directories below** the current directory for `.llm-evals.config.yml`, skipping hidden directories and `node_modules`, `vendor`, `tmp`, `log`, `pkg` and `coverage`. The shallowest file wins, then the first by name. Run from the repository root, it finds a fixture:
|
|
146
187
|
|
|
147
188
|
```text
|
|
148
189
|
$ bundle exec exe/evals cases
|
|
149
|
-
|
|
190
|
+
|
|
191
|
+
┌ Info ────────────────────────────────────────────────────────────────────────┐
|
|
192
|
+
│ Evals configuration: config_only_suite │
|
|
193
|
+
│ File: spec/fixtures/config_only_suite/evals/.llm-evals.config.yml │
|
|
194
|
+
└──────────────────────────────────────────────────────────────────────────────┘
|
|
150
195
|
```
|
|
151
196
|
|
|
152
|
-
|
|
197
|
+
#### What is this?
|
|
198
|
+
|
|
199
|
+
A file the search above ran, and found the eval suite four directories deep. Your shell stays where it was. That green box is the first thing a run prints, and it follows the help. When there is no file, the box is a yellow warning. `--no-search` turns the search off, and `-C`/`--config` skips it.
|
|
153
200
|
|
|
154
201
|
### The toy suite (`eval_helper.rb` style)
|
|
155
202
|
|
|
@@ -188,7 +235,7 @@ Files to look at:
|
|
|
188
235
|
| --------------------------------------- | ---------------------------------------------------------- |
|
|
189
236
|
| `evals/eval_helper.rb` | `LLM::Evals.configure`: suite, cases glob, adapter, scorer |
|
|
190
237
|
| `lib/toy_suite/evals.rb` | the adapter and scorer classes |
|
|
191
|
-
| `evals/.llm-evals.config.yml` | the lineup, including a
|
|
238
|
+
| `evals/.llm-evals.config.yml` | the lineup, including a TypeSafe entry the suite refuses |
|
|
192
239
|
| `evals/datasets/` | the case layout on disk |
|
|
193
240
|
|
|
194
241
|
The warning `typesafe/jev-latest refused: needs typesafe; this suite produces prompt` is intentional: it shows a contender being left out rather than scored 0.
|
|
@@ -211,7 +258,7 @@ Use the toy suite as a template:
|
|
|
211
258
|
|
|
212
259
|
1. Copy its `evals/` folder into your project.
|
|
213
260
|
1. Replace the adapter and scorer with your own (see [Writing a suite](#writing-a-suite)).
|
|
214
|
-
1. List real models under `
|
|
261
|
+
1. List real models under `openrouter:` in `.llm-evals.config.yml` and drop the `fake:` section.
|
|
215
262
|
1. Add `gem "ruby_llm"` to your Gemfile and set `OPENROUTER_API_KEY`, in your shell or the suite's `.env` (see [Environment](#environment)).
|
|
216
263
|
|
|
217
264
|
## Install
|
|
@@ -257,43 +304,51 @@ require "bundler/setup"
|
|
|
257
304
|
require "llm/evals"
|
|
258
305
|
require "my_gem"
|
|
259
306
|
|
|
260
|
-
class MyAdapter < LLM::Evals::Adapter
|
|
307
|
+
class MyAdapter < LLM::Evals::Actions::Adapter
|
|
261
308
|
# Once per dataset, outside the threads.
|
|
262
309
|
def prepare(dataset) = @schema = MyGem.schema_for(dataset)
|
|
263
310
|
|
|
264
311
|
def call(kase, model:)
|
|
265
|
-
|
|
266
|
-
system: MyGem.system_prompt(@schema), user: kase.input, json: true
|
|
267
|
-
|
|
312
|
+
provider_response = model.complete(
|
|
313
|
+
system: MyGem.system_prompt(@schema), user: kase.input, json: true
|
|
314
|
+
)
|
|
315
|
+
|
|
268
316
|
# The production parser, so scores reflect production.
|
|
269
|
-
output = MyGem.parse(
|
|
270
|
-
LLM::Evals::Reply.new(
|
|
317
|
+
output = MyGem.parse(provider_response.text)
|
|
318
|
+
LLM::Evals::Results::Reply.new(
|
|
319
|
+
output: output,
|
|
320
|
+
raw: provider_response.text,
|
|
321
|
+
usage: provider_response.usage
|
|
322
|
+
)
|
|
271
323
|
rescue MyGem::NotJSON => e
|
|
272
324
|
# The model's failure: scored 0.
|
|
273
|
-
LLM::Evals::Reply.new(
|
|
325
|
+
LLM::Evals::Results::Reply.new(
|
|
326
|
+
error: e.message,
|
|
327
|
+
raw: provider_response&.text
|
|
328
|
+
)
|
|
274
329
|
end
|
|
275
330
|
|
|
276
331
|
# What the scorer needs beyond the case.
|
|
277
|
-
def context(
|
|
332
|
+
def context(_kase) = { schema: @schema }
|
|
278
333
|
|
|
279
334
|
# Offline, for `llm-evals verify`.
|
|
280
335
|
def verify(dataset) = MyGem.schema_problems(dataset)
|
|
281
336
|
end
|
|
282
337
|
|
|
283
|
-
class MyScorer < LLM::Evals::Scorer
|
|
338
|
+
class MyScorer < LLM::Evals::Scoring::Scorer
|
|
284
339
|
def metrics
|
|
285
340
|
[
|
|
286
|
-
LLM::Evals::Metric.new(
|
|
287
|
-
name:
|
|
341
|
+
LLM::Evals::Scoring::Metric.new(
|
|
342
|
+
name: "field_accuracy",
|
|
288
343
|
description: "Mean per-field score.",
|
|
289
|
-
primary:
|
|
344
|
+
primary: true
|
|
290
345
|
),
|
|
291
|
-
LLM::Evals::Metric.new(
|
|
292
|
-
name:
|
|
346
|
+
LLM::Evals::Scoring::Metric.new(
|
|
347
|
+
name: "exact_match",
|
|
293
348
|
description: "1 when every field is right."
|
|
294
349
|
),
|
|
295
|
-
LLM::Evals::Metric.new(
|
|
296
|
-
name:
|
|
350
|
+
LLM::Evals::Scoring::Metric.new(
|
|
351
|
+
name: "restraint",
|
|
297
352
|
description: "Of the fields the text never answers, the share left null."
|
|
298
353
|
)
|
|
299
354
|
]
|
|
@@ -304,22 +359,22 @@ class MyScorer < LLM::Evals::Scorer
|
|
|
304
359
|
mean = scores.values.sum / scores.size
|
|
305
360
|
misses = scores.reject { |_, v| v == 1.0 }.keys
|
|
306
361
|
|
|
307
|
-
LLM::Evals::Score.new(
|
|
308
|
-
value:
|
|
309
|
-
metrics:
|
|
362
|
+
LLM::Evals::Results::Score.new(
|
|
363
|
+
value: mean,
|
|
364
|
+
metrics: {
|
|
310
365
|
"field_accuracy" => mean,
|
|
311
|
-
"exact_match"
|
|
366
|
+
"exact_match" => scores.values.all?(1.0) ? 1.0 : 0.0,
|
|
312
367
|
# nil: this case does not apply; it is skipped, not zeroed.
|
|
313
|
-
"restraint"
|
|
368
|
+
"restraint" => nil
|
|
314
369
|
},
|
|
315
|
-
diagnostics: misses.to_h do |f|
|
|
316
|
-
[f, { want: kase.expected[f], got: reply.output[f] }]
|
|
370
|
+
diagnostics: misses.to_h do |f|
|
|
371
|
+
[f, { want: kase.expected[f], got: reply.output[f] }]
|
|
317
372
|
end
|
|
318
373
|
)
|
|
319
374
|
end
|
|
320
375
|
|
|
321
376
|
def verify(kase, context:)
|
|
322
|
-
LLM::Evals::SchemaValidator.new(context[:schema]).problems(kase.expected)
|
|
377
|
+
LLM::Evals::Scoring::SchemaValidator.new(context[:schema]).problems(kase.expected)
|
|
323
378
|
end
|
|
324
379
|
|
|
325
380
|
def summary(kase) = "#{kase.expected.count { |_, v| !v.nil? }}/#{kase.expected.size} answered"
|
|
@@ -333,6 +388,7 @@ LLM::Evals.configure do |c|
|
|
|
333
388
|
# After the env files load, before any model is called.
|
|
334
389
|
c.before_run { MyGem.reset_configuration! }
|
|
335
390
|
end
|
|
391
|
+
|
|
336
392
|
```
|
|
337
393
|
|
|
338
394
|
A complete, runnable suite lives in [`spec/fixtures/toy_suite/evals/`](spec/fixtures/toy_suite/evals/): two datasets, two variants, the gem's own `Testing::EchoAdapter` and a fake provider. It runs without a network.
|
|
@@ -399,42 +455,45 @@ results:
|
|
|
399
455
|
autosave: true # save every completed run as JSON (default)
|
|
400
456
|
destination: ./evals/results/
|
|
401
457
|
temp_dir: ./tmp # temporary JSON when autosave is false
|
|
402
|
-
display:
|
|
403
|
-
|
|
404
|
-
#
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
direct: # a vendor's own API, one request type each
|
|
428
|
-
typesafe: # request type "typesafe": noul, choice, score
|
|
429
|
-
- model: jev-latest # and this requires TYPESAFE_API_KEY
|
|
458
|
+
display: both # none, tui, browser or both (default)
|
|
459
|
+
|
|
460
|
+
# A section named after a provider states the key it needs.
|
|
461
|
+
openrouter: # requires OPENROUTER_API_KEY; every entry takes a prompt
|
|
462
|
+
rate_limit: { burst: 1000, per_minute: 500 }
|
|
463
|
+
vendors:
|
|
464
|
+
anthropic:
|
|
465
|
+
rate_limit: { burst: 10, per_minute: 50 }
|
|
466
|
+
models:
|
|
467
|
+
- model: claude-opus-5 # the vendor's limit only
|
|
468
|
+
- model: claude-fable-5 # both limits
|
|
469
|
+
rate_limit: { burst: 5, per_minute: 20 }
|
|
470
|
+
- model: claude-haiku-5.5
|
|
471
|
+
# a list expands into one contender per value
|
|
472
|
+
effort: [default, low, high]
|
|
473
|
+
openai: # no limit of its own
|
|
474
|
+
models:
|
|
475
|
+
- model: gpt-6.1-sol
|
|
476
|
+
typesafe:
|
|
477
|
+
models:
|
|
478
|
+
- model: jev-router # Jev on OpenRouter, a chat model
|
|
479
|
+
|
|
480
|
+
typesafe: # requires TYPESAFE_API_KEY; request type "typesafe": noul, choice, score
|
|
481
|
+
models:
|
|
482
|
+
- jev-latest # a bare name is the same as `- model: jev-latest`
|
|
430
483
|
```
|
|
431
484
|
|
|
432
|
-
|
|
485
|
+
**Provider sections.** Each top-level key with `vendors:` or `models:` names a provider. An entry under `vendors:` becomes `<vendor>/<model>`, the OpenRouter id. An entry under `models:` becomes the provider's prefix and the name, such as `typesafe/jev-latest`. The contender carries its provider, so a direct `typesafe/jev-latest` and the OpenRouter chat model `typesafe/jev-router` never get confused. An unknown provider or key is a configuration error naming the file. The old top-level `vendors:` and `direct:` are errors too; `evals upgrade-config` rewrites them, keeping your comments and leaving `<file>.bak`, and `-n` prints the result instead.
|
|
486
|
+
|
|
487
|
+
**Settings.** `effort` and `temperature` take one value or a list, and lists multiply: `effort: [low, high]` with `temperature: [0.0, 0.7]` runs four experiments for that model. Anything left out falls back to `defaults`. Flags (`-e`, `-t`, `--max-tokens`, `-p`) beat both.
|
|
433
488
|
|
|
434
|
-
**`
|
|
489
|
+
**`rate_limit`.** It goes on a provider, a vendor or a model, and needs both numbers: the first `burst` calls of the run go at once, then `per_minute`, evenly spaced. A burst is spent once per run and does not refill. A model listed twice shares one limit, so setting it on both entries is an error. A call waits on every limit that matches it. Limits hold for one process across all `-j` and `-p`, and models given with `-m` obey them too. Time spent waiting is left out of `time`, `latency` and `p95`; `time` subtracts the wait averaged across the experiment's workers.
|
|
490
|
+
|
|
491
|
+
**Retries.** RubyLLM's retries are off. Rate limits, overloads, server errors and dropped connections are retried up to 5 attempts in all, and a retry shuts that vendor for every thread, for as long as `Retry-After` says or for 1, 2, 4 then 8 seconds when it says nothing. A provider asking for more than 5 minutes is not retried; the case counts as an error.
|
|
435
492
|
|
|
436
493
|
#### Weighted Score
|
|
437
494
|
|
|
495
|
+
We already chatted about the weghted score above, but this is a deeper dive.
|
|
496
|
+
|
|
438
497
|
> [!IMPORTANT]
|
|
439
498
|
> This is one of those features you never think you'd need, until you realize your results are sortable by a single column and it does not incorporate all of the others! What to do!? The answer is right here. Create a new score, weighted because it gives more or less weight to various resulting scores, and supports both results that are better when they are smaller (latency) and the ones that are better when they are larger (match %, accuracy %), etc.s
|
|
440
499
|
|
|
@@ -452,16 +511,33 @@ weighted_score:
|
|
|
452
511
|
|
|
453
512
|
`wscore = Σ(|weight| × goodness) / Σ|weight|`, from 0% to 100%, higher is better. Metrics count as they are, or as `1 - value` under a negative weight. When `errors` is weighted, errors come off the total: 20 points for one, 25 for two, 30 for three, and so on, down to 0%. A failed case has no metric scores, so the other weighted metrics do not reflect that failure. A contender with no errors earns the full positive contribution from the `errors` weight. `cost` and `time` go on a log scale between the lowest and highest in the table, so each doubling costs the same, however expensive the priciest model is. That makes a weighted score comparable only within one table.
|
|
454
513
|
|
|
455
|
-
|
|
514
|
+
`evals weights` prints each column's direction and real share of the score: `↓` in bold blue where low is good, `↑` in bold yellow where high is good. A scorer metric the weights leave out is listed in red with no values. Weights need not add up to 1.
|
|
515
|
+
|
|
516
|
+
You can weight `score`, `errors`, `time`, `cost`, `latency`, `p95` and your scorer's metrics. `latency` and `p95` are the average and 95th percentile seconds of the calls that worked (across datasets, a contender's `p95` is the average of each dataset's, an approximation good enough to rank by), while `time` is how long the experiment took, waiting included. Anything else, tokens included, is refused before a model is called. A contender missing a weighted value shows `-`, and one with errors cannot be named best. Without `weighted_score`, nothing changes.
|
|
517
|
+
|
|
518
|
+
#### Aborting a failing run
|
|
519
|
+
|
|
520
|
+
```yaml
|
|
521
|
+
errors:
|
|
522
|
+
abort_after:
|
|
523
|
+
total: 1000 # required
|
|
524
|
+
percent: 60 # optional: also needs 60% of finished calls to have failed
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Both limits must be met. A case counts as an error when the call raises or the model's reply is an error, such as one that is not JSON. Calls in flight finish, nothing new starts, the report is written from what finished, and the command exits non-zero. Without `errors`, a run never aborts.
|
|
528
|
+
|
|
529
|
+
#### When every call fails
|
|
530
|
+
|
|
531
|
+
With no configuration, 100 failed calls in a row pause the run: no new calls start, and it prints the error messages and which models, datasets and cases are affected, or that all of them are. It then asks whether to keep running. If you carry on, another 100 failures in a row ask again. If you stop, the partial results are reported and the command exits 1. `-y` / `--yes` prints the same details and keeps running. When stdin is not a terminal and `--yes` is absent, the run stops, since there is nobody to ask.
|
|
456
532
|
|
|
457
533
|
#### Request types
|
|
458
534
|
|
|
459
535
|
`request_types` is required. It identifies the kind of request the suite's adapter sends. The types belong to the providers and show in `llm-evals --help`; a suite only names them:
|
|
460
536
|
|
|
461
|
-
| Type | Provider
|
|
462
|
-
| ---------- |
|
|
463
|
-
| `prompt` | OpenRouter (everything under `
|
|
464
|
-
| `typesafe` | `
|
|
537
|
+
| Type | Provider | Meaning |
|
|
538
|
+
| ---------- | ------------------------------------------- | ---------------------------------------------------- |
|
|
539
|
+
| `prompt` | OpenRouter (everything under `openrouter:`) | a chat completion: system and user text in, text out |
|
|
540
|
+
| `typesafe` | `typesafe:` | typed questions: noul, choice, score |
|
|
465
541
|
|
|
466
542
|
A contender runs only when its provider's type is in the suite's list. Otherwise it is **refused**:
|
|
467
543
|
|
|
@@ -503,7 +579,7 @@ evals completion zsh # shell completion (bash too)
|
|
|
503
579
|
|
|
504
580
|
Flags shared by `run` and `compare`: `-m/--models`, `-d/--dataset` (every dataset when omitted), `-c/--case`, `-e/--effort`, `-t/--temperature`, `--max-tokens`, `-p/--parallelism`, `-j/--concurrency`, `-C/--config FILE`, `--project NAME`, `--tracker NAME`, `--local`, `--no-open`, `-n/--dry-run`, `-o/--output [FILE]`, `--format tui|html|json`, `--totals overall|variant|both`, `--display none|tui|browser`, `--autosave/--no-autosave`, `--destination DIR` and `--temp-dir DIR`. `--totals` controls the TUI totals tables: the default `overall` compares contenders across all cases, `variant` compares each case folder (such as `complete` or `vague`) across datasets, and `both` prints both views. Completed runs are saved as timestamped JSON by default, independently of the selected display. Browser and TUI reports are rendered from that JSON; `--no-autosave` uses a temporary JSON snapshot under `temp_dir`. Use `llm-evals render results.json` to regenerate the HTML report without making model calls again. `-l` and `-L` are reserved for logging and not yet implemented.
|
|
505
581
|
|
|
506
|
-
**Reading the output.** `results.display` selects no report, a terminal leaderboard,
|
|
582
|
+
**Reading the output.** `results.display` selects no report, a terminal leaderboard, a browser report, or, by default, both: the HTML report is built from the run's JSON and opened in the browser. `--display tui` skips the page, and `--no-open` writes it without opening it. The TUI has one column for each metric declared by the scorer and ranks by the primary metric. It also shows a glossary built from each metric's `description`. Browser reports show global and dataset standings, sortable tables, case inputs, expected answers, scores, and diagnostics. Provider names and model IDs use separate colors. Headline standings show effort and temperature alongside each model. Every JSON snapshot records both parsed CLI options and effective run settings, including expanded model settings and selected cases. `render` turns a saved snapshot into HTML later. Progress bars and warnings go to STDERR. Output from libraries is saved to `log/<command>.warnings.<stamp>.log` so it doesn't garble the terminal display.
|
|
507
583
|
|
|
508
584
|
## Trackers
|
|
509
585
|
|
|
@@ -537,7 +613,7 @@ Logfire has no Ruby gem of its own. It is an OTLP backend for the standard OpenT
|
|
|
537
613
|
A tracker is a class with three callbacks. Register it with `c.tracker` in the helper (or in a plugin gem loaded through `require:`), then choose it by name:
|
|
538
614
|
|
|
539
615
|
```ruby
|
|
540
|
-
class Weave < LLM::Evals::
|
|
616
|
+
class Weave < LLM::Evals::Trackers::Base
|
|
541
617
|
# Selects it when set and nothing else is chosen.
|
|
542
618
|
def self.env_key = "WANDB_API_KEY"
|
|
543
619
|
|
|
@@ -582,8 +658,8 @@ LLM::Evals.configure { |c| c.tracker Weave }
|
|
|
582
658
|
|
|
583
659
|
| Variable | Used for |
|
|
584
660
|
| ----------------------------- | --------------------------------------------------------- |
|
|
585
|
-
| `OPENROUTER_API_KEY` | required for any `
|
|
586
|
-
| `TYPESAFE_API_KEY` | required for a `
|
|
661
|
+
| `OPENROUTER_API_KEY` | required for any `openrouter:` model |
|
|
662
|
+
| `TYPESAFE_API_KEY` | required for a `typesafe:` model |
|
|
587
663
|
| `BRAINTRUST_API_KEY` | the Braintrust tracker; selects it when nothing else does |
|
|
588
664
|
| `BRAINTRUST_PROJECT_ID` | Braintrust project UUID; beats the lineup's `project` |
|
|
589
665
|
| `LOGFIRE_TOKEN` | the Logfire tracker (a write token); selects it likewise |
|
|
@@ -598,9 +674,9 @@ The suite loads files listed in `env_files`, without overriding variables alread
|
|
|
598
674
|
|
|
599
675
|
A provider wraps an API. It declares the request types it supports, required keys, available models, and a `complete` method.
|
|
600
676
|
|
|
601
|
-
- A provider
|
|
602
|
-
- A `direct?` provider is reached only through
|
|
603
|
-
- Register it with `c.provider`. Providers are checked in reverse registration order for a
|
|
677
|
+
- A provider declares the ids it serves with `prefix`, and its section in the lineup carries its `name`.
|
|
678
|
+
- A `direct?` provider is reached only through its own section.
|
|
679
|
+
- Register it with `c.provider`. Providers are checked in reverse registration order for an ID outside a direct provider's section. OpenRouter claims every ID, so it is always checked last.
|
|
604
680
|
|
|
605
681
|
```ruby
|
|
606
682
|
class Ollama < LLM::Evals::Providers::Base
|
|
@@ -640,7 +716,7 @@ class Ollama < LLM::Evals::Providers::Base
|
|
|
640
716
|
Net::HTTP.post(URI("#{HOST}/api/generate"), JSON.generate(body)).body
|
|
641
717
|
)
|
|
642
718
|
|
|
643
|
-
LLM::Evals::
|
|
719
|
+
LLM::Evals::Results::ProviderResponse.new(
|
|
644
720
|
text: response.fetch("response"),
|
|
645
721
|
usage: {
|
|
646
722
|
input_tokens: response["prompt_eval_count"],
|
|
@@ -657,7 +733,7 @@ LLM::Evals.configure { |c| c.provider Ollama.new }
|
|
|
657
733
|
|
|
658
734
|
`model.effort`, `model.temperature` and `model.max_tokens` carry the contender's settings; nil means "send nothing". A transport failure should raise: the run counts it as an error, not as a wrong answer.
|
|
659
735
|
|
|
660
|
-
`Providers::TypeSafe` is a direct provider (`
|
|
736
|
+
`Providers::TypeSafe` is a direct provider (`typesafe:`, request type `typesafe`). It ships with TypeSafe's model list, key and pricing, which is enough for `verify` and the cost column. Its `complete` is not implemented yet and raises "no typesafe consumer yet": the typed request shape will be defined by the first suite that declares `request_types: [typesafe]`.
|
|
661
737
|
|
|
662
738
|
## Public API
|
|
663
739
|
|
|
@@ -667,7 +743,7 @@ module LLM::Evals
|
|
|
667
743
|
:id, :dataset, :variant, :input, :expected, :path, :metadata)
|
|
668
744
|
Usage = Data.define(
|
|
669
745
|
:input_tokens, :output_tokens, :cost_usd)
|
|
670
|
-
|
|
746
|
+
ProviderResponse = Data.define(
|
|
671
747
|
:text, :usage, :raw)
|
|
672
748
|
# error: the model failed, scored 0. A raise: transport failure.
|
|
673
749
|
Reply = Data.define(
|
|
@@ -680,13 +756,13 @@ module LLM::Evals
|
|
|
680
756
|
:value, :metrics, :outcomes, :diagnostics)
|
|
681
757
|
RunResult = Data.define(
|
|
682
758
|
:experiment, :case, :reply, :score, :duration, :error)
|
|
683
|
-
# provider: nil
|
|
759
|
+
# provider: nil, or a direct provider's name.
|
|
684
760
|
Contender = Data.define(
|
|
685
761
|
:model, :effort, :temperature, :max_tokens, :provider)
|
|
686
762
|
|
|
687
763
|
class Model
|
|
688
764
|
attr_reader :id, :provider, :settings
|
|
689
|
-
def complete(system: nil, user:, json: true) =
|
|
765
|
+
def complete(system: nil, user:, json: true) = ProviderResponse
|
|
690
766
|
end
|
|
691
767
|
|
|
692
768
|
class Adapter
|
data/exe/llm-evals
CHANGED
|
@@ -4,11 +4,66 @@
|
|
|
4
4
|
# No Bundler here: a Gemfile in the user's directory must not decide which
|
|
5
5
|
# gems load. The suite's own eval_helper.rb may `require "bundler/setup"`.
|
|
6
6
|
$LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
|
|
7
|
-
require "llm/evals"
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
# warn prints nothing under -W0, and the welcome is only ever shown once.
|
|
9
|
+
# rubocop:disable Style/StderrPuts
|
|
10
|
+
require "fileutils"
|
|
11
|
+
require "yaml"
|
|
12
|
+
require "date"
|
|
13
|
+
|
|
14
|
+
module LLM
|
|
15
|
+
module Evals
|
|
16
|
+
# The key under which the semaphore file records the first run.
|
|
17
|
+
WARNING_HASH_KEY = "warning_shown_on"
|
|
18
|
+
# Where the semaphore file lives.
|
|
19
|
+
SEMAPHORE_DIR = "#{Dir.home}/.local/share/llm-evals".freeze
|
|
20
|
+
# Remembers that the welcome was shown.
|
|
21
|
+
SEMAPHORE_FILE = "#{SEMAPHORE_DIR}/warning-semaphore.yml".freeze
|
|
22
|
+
|
|
23
|
+
# What the semaphore file held.
|
|
24
|
+
SEMAPHORE_CONFIG = if File.size?(SEMAPHORE_FILE)
|
|
25
|
+
YAML.safe_load_file(SEMAPHORE_FILE, aliases: true)
|
|
26
|
+
else
|
|
27
|
+
{}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
unless SEMAPHORE_CONFIG.is_a?(Hash) && SEMAPHORE_CONFIG[WARNING_HASH_KEY]
|
|
31
|
+
FileUtils.mkdir_p(SEMAPHORE_DIR)
|
|
32
|
+
File.write(SEMAPHORE_FILE, { WARNING_HASH_KEY => DateTime.now.to_s }.to_yaml)
|
|
33
|
+
|
|
34
|
+
$stderr.puts "\e[0;33mWelcome to the llm-evals CLI!\n\n"
|
|
11
35
|
|
|
12
|
-
puts "
|
|
36
|
+
$stderr.puts "\e[1;31mNOTE: \e[0;33mThis gem comes with an executable named \e[1;32mevals\e[0;33m.\n"
|
|
37
|
+
$stderr.puts " You invoked 'llm-evals'. While we prefer that you use 'evals' for the CLI,"
|
|
38
|
+
$stderr.puts " we understand that sometimes commands such as 'evals' may clash with others."
|
|
39
|
+
$stderr.puts " For this reason you can use \e[1;32mllm-evals\e[0;33m if you prefer moving forward.\e[0m"
|
|
40
|
+
$stderr.puts
|
|
41
|
+
$stderr.puts "This message won't be shown again, and we thank you for using llm-evals!"
|
|
13
42
|
|
|
14
|
-
|
|
43
|
+
require "io/console"
|
|
44
|
+
|
|
45
|
+
$stdin.tty? && $stdin.raw do
|
|
46
|
+
$stderr.print " Press any key to continue..."
|
|
47
|
+
waited = 0
|
|
48
|
+
delta = 0.1
|
|
49
|
+
loop do
|
|
50
|
+
if $stdin.wait_readable(0)
|
|
51
|
+
$stdin.read_nonblock(1)
|
|
52
|
+
break
|
|
53
|
+
end
|
|
54
|
+
sleep delta
|
|
55
|
+
waited += delta
|
|
56
|
+
if waited > 20
|
|
57
|
+
$stderr.puts "\n No key pressed after 20 seconds, continuing..."
|
|
58
|
+
break
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# rubocop:enable Style/StderrPuts
|
|
67
|
+
|
|
68
|
+
require "llm/evals"
|
|
69
|
+
LLM::Evals::Launcher.new(ARGV).execute!
|
data/lib/.DS_Store
ADDED
|
Binary file
|
data/lib/llm/.DS_Store
ADDED
|
Binary file
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module LLM
|
|
4
|
+
module Evals
|
|
5
|
+
module Actions
|
|
6
|
+
# What a suite implements to run one case: everything the model sees, and
|
|
7
|
+
# everything done to its answer before it is scored. An adapter never
|
|
8
|
+
# scores; that is the {Scoring::Scorer}'s, and keeping them apart is what lets one
|
|
9
|
+
# suite compare prompts and another compare models with the same runner.
|
|
10
|
+
#
|
|
11
|
+
# @abstract Subclass and override {#call}.
|
|
12
|
+
class Adapter
|
|
13
|
+
# Called once per dataset before its cases run; load what the dataset
|
|
14
|
+
# shares (a schema, a flow definition) here, outside the threads.
|
|
15
|
+
#
|
|
16
|
+
# @param _dataset [String] the dataset's name
|
|
17
|
+
# @return [void]
|
|
18
|
+
def prepare(_dataset) = nil
|
|
19
|
+
|
|
20
|
+
# Runs one case against one model.
|
|
21
|
+
#
|
|
22
|
+
# @param kase [Datasets::Case] the case to run
|
|
23
|
+
# @param model [Models::Model] the model at the setting under test
|
|
24
|
+
# @return [Results::Reply] with `error:` set when the model failed to answer; raise for a transport failure
|
|
25
|
+
# @raise [NotImplementedError] until a subclass overrides it
|
|
26
|
+
def call(kase, model:)
|
|
27
|
+
raise NotImplementedError, "#{self.class}#call(kase, model:) must return an LLM::Evals::Results::Reply"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# What the scorer needs to know about a case beyond the case itself: a
|
|
31
|
+
# schema, field types, the prompt. Passed to {Scoring::Scorer#call} as `context:`.
|
|
32
|
+
#
|
|
33
|
+
# @param _kase [Datasets::Case] the case about to be scored
|
|
34
|
+
# @return [Hash]
|
|
35
|
+
def context(_kase) = {}
|
|
36
|
+
|
|
37
|
+
# Offline checks of a dataset's shared files. `llm-evals verify` prints them.
|
|
38
|
+
#
|
|
39
|
+
# @param _dataset [String] the dataset's name
|
|
40
|
+
# @return [Array<String>] problems; empty when the dataset is sound
|
|
41
|
+
def verify(_dataset) = []
|
|
42
|
+
|
|
43
|
+
# Recorded with each Braintrust experiment, e.g. which prompt version ran.
|
|
44
|
+
#
|
|
45
|
+
# @return [Hash]
|
|
46
|
+
def metadata = {}
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|