evalcore 0.2.0__tar.gz → 0.3.0__tar.gz

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 (79) hide show
  1. evalcore-0.3.0/CHANGELOG.md +77 -0
  2. {evalcore-0.2.0 → evalcore-0.3.0}/PKG-INFO +1 -1
  3. {evalcore-0.2.0 → evalcore-0.3.0}/docs/design.md +20 -20
  4. {evalcore-0.2.0 → evalcore-0.3.0}/pyproject.toml +1 -1
  5. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/cli.py +36 -19
  6. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/models.py +6 -0
  7. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/runner.py +26 -2
  8. evalcore-0.3.0/src/evalcore/store.py +511 -0
  9. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_runner.py +34 -0
  10. evalcore-0.3.0/tests/test_store.py +376 -0
  11. {evalcore-0.2.0 → evalcore-0.3.0}/uv.lock +1 -1
  12. evalcore-0.2.0/CHANGELOG.md +0 -35
  13. evalcore-0.2.0/src/evalcore/store.py +0 -286
  14. evalcore-0.2.0/tests/test_store.py +0 -173
  15. {evalcore-0.2.0 → evalcore-0.3.0}/.github/workflows/ci.yml +0 -0
  16. {evalcore-0.2.0 → evalcore-0.3.0}/.github/workflows/publish.yml +0 -0
  17. {evalcore-0.2.0 → evalcore-0.3.0}/.gitignore +0 -0
  18. {evalcore-0.2.0 → evalcore-0.3.0}/.pre-commit-config.yaml +0 -0
  19. {evalcore-0.2.0 → evalcore-0.3.0}/LICENSE +0 -0
  20. {evalcore-0.2.0 → evalcore-0.3.0}/README.md +0 -0
  21. {evalcore-0.2.0 → evalcore-0.3.0}/examples/__init__.py +0 -0
  22. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/README.md +0 -0
  23. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/__init__.py +0 -0
  24. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/adapter.py +0 -0
  25. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/datasets/support_reply/v1/cases/angry_complaint.yaml +0 -0
  26. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/datasets/support_reply/v1/cases/billing_question.yaml +0 -0
  27. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/datasets/support_reply/v1/cases/double_charge.yaml +0 -0
  28. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/datasets/support_reply/v1/cases/refund_request.yaml +0 -0
  29. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/fixtures/support_reply_judge.yaml +0 -0
  30. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/fixtures/support_reply_pairwise.yaml +0 -0
  31. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/fixtures/support_reply_replay.yaml +0 -0
  32. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/graders.py +0 -0
  33. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/run_eval.py +0 -0
  34. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/suite.yaml +0 -0
  35. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/tests/__init__.py +0 -0
  36. {evalcore-0.2.0 → evalcore-0.3.0}/examples/quickstart/tests/test_quickstart.py +0 -0
  37. {evalcore-0.2.0 → evalcore-0.3.0}/justfile +0 -0
  38. {evalcore-0.2.0 → evalcore-0.3.0}/pyrightconfig.json +0 -0
  39. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/__init__.py +0 -0
  40. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/adapters/__init__.py +0 -0
  41. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/adapters/base.py +0 -0
  42. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/adapters/env.py +0 -0
  43. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/adapters/http.py +0 -0
  44. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/adapters/replay.py +0 -0
  45. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/compare.py +0 -0
  46. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/errors.py +0 -0
  47. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/__init__.py +0 -0
  48. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/base.py +0 -0
  49. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/classification.py +0 -0
  50. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/deterministic.py +0 -0
  51. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/judge.py +0 -0
  52. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/graders/numeric.py +0 -0
  53. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/loader.py +0 -0
  54. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/pairwise.py +0 -0
  55. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/py.typed +0 -0
  56. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/rating.py +0 -0
  57. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/refs.py +0 -0
  58. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/report.py +0 -0
  59. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/reporters/__init__.py +0 -0
  60. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/reporters/base.py +0 -0
  61. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/reporters/html.py +0 -0
  62. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/reporters/markdown.py +0 -0
  63. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/retry.py +0 -0
  64. {evalcore-0.2.0 → evalcore-0.3.0}/src/evalcore/sweep.py +0 -0
  65. {evalcore-0.2.0 → evalcore-0.3.0}/tests/__init__.py +0 -0
  66. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_adapters.py +0 -0
  67. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_cli.py +0 -0
  68. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_edge_cases.py +0 -0
  69. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_judge.py +0 -0
  70. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_judge_extra.py +0 -0
  71. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_live_clients.py +0 -0
  72. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_pairwise_extra.py +0 -0
  73. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_rating.py +0 -0
  74. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_rating_server.py +0 -0
  75. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_reporters.py +0 -0
  76. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_retry.py +0 -0
  77. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_sweep_pairwise.py +0 -0
  78. {evalcore-0.2.0 → evalcore-0.3.0}/tests/test_unit.py +0 -0
  79. {evalcore-0.2.0 → evalcore-0.3.0}/uv.toml +0 -0
@@ -0,0 +1,77 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project follows
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] - 2026-07-28
10
+
11
+ ### Changed
12
+ - **Breaking:** the results-store outbox is now one feed at
13
+ `(run, case, sample, grader, metric)` grain, targeting a single flat
14
+ `evaluation_scores` table with the run trend and the invocation grain as
15
+ plain views over it. `store.scorecard_rows` and
16
+ `JsonlOutboxExporter.export` are removed; use `store.score_rows` and
17
+ `export_scores`. `--export-scores` is now an alias for `--export`.
18
+ - **Breaking:** a missing measurement is emitted as `null` rather than the
19
+ `(value=0, has_value=false)` sentinel pair, and the `has_value` / `has_stdev`
20
+ companion keys are gone. The store columns are `Nullable`, and a real `0.0` is
21
+ a meaningful score.
22
+ - **Breaking:** outbox row keys are the store column names, so `project` is
23
+ emitted as `application`, `mode` as `adapter_mode`, `created_at` as
24
+ `timestamp`, and `Output.error` as `is_error` plus `error_text`.
25
+ - **Breaking:** outbox rows no longer carry `model_id` or `prompt_version`. Both
26
+ are `variant.knobs.get(...)` projections and travel inside `variant_knobs`.
27
+ - **Breaking:** `run_id` is now a dashed UUIDv7 rather than `uuid4().hex`, so it
28
+ parses as a ClickHouse `UUID` and carries its own creation time. Uses
29
+ `uuid.uuid7()` on 3.14 and an RFC 9562 implementation on 3.11 through 3.13.
30
+ - Outbox rows now carry the gate: `gate_verdict`, `gate_win`,
31
+ `baseline_run_id`, `baseline_variant`, `win_baseline`, `win_candidate`,
32
+ `win_delta` and `gate_summary` at run grain, plus `win`, `guardrail` and
33
+ `guardrail_gap` on the metric each one refers to. Pass the `Comparison` to
34
+ `score_rows`; without it a run reads as ungated.
35
+
36
+ ### Added
37
+ - `RunResult.aggregate_scores`, retaining the `kind='aggregate'` scores so a
38
+ store row keeps the grader that emitted them and what it reported. The
39
+ scorecard kept only their values.
40
+ - `store.grader_lookups`, mapping grader names to a category and to a judge
41
+ scale from a suite's grader specs, since a `Score` carries neither.
42
+ - Three outbox row shapes that previously had no representation: an aggregate
43
+ metric (no `case_id`), an invocation that failed before any grader ran (no
44
+ `grader` or `metric`), and a metric a guardrail or the win metric names but
45
+ never scored (null value).
46
+
47
+ ## [0.2.0] - 2026-07-16
48
+
49
+ ### Changed
50
+ - **Breaking:** the import package and CLI are now `evalcore` (were `evalkit`).
51
+ Update `import evalkit` to `import evalcore` and the `evalkit` command to
52
+ `evalcore`. The distribution name (`evalcore`) is unchanged.
53
+ - Lowered the minimum Python to **3.11** (was 3.14).
54
+
55
+ ### Added
56
+ - `evalcore.__version__`.
57
+ - Public `evalcore.adapters.expand_env` for `${VAR}` expansion in custom
58
+ adapters (replaces the private `adapters._env` module).
59
+ - Exception hierarchy: `EvalcoreError` (base) and `ConfigError` (also a
60
+ `ValueError`, so existing handlers keep working).
61
+ - Top-level convenience entry points: `load_suite`, `load_cases`, `run_suite`,
62
+ `run_suite_sync`.
63
+ - HTML rendering for `sweep` and `pairwise` reports, and `--report` /
64
+ `--report-out` on those CLI commands.
65
+
66
+ ## [0.1.0] - 2026-07-16
67
+
68
+ - Initial public release: adapters (http/replay), graders (deterministic,
69
+ numeric, classification, LLM judge + panel), runner (N-sampling, concurrency,
70
+ retries, checkpoint/resume), compare/gate, sweep, pairwise, blind human
71
+ rating + ranking with judge agreement, Markdown/HTML reporters, JSON +
72
+ column-store outbox, and content-hash provenance.
73
+
74
+ [Unreleased]: https://github.com/scottpmiller/evalcore/compare/0.3.0...HEAD
75
+ [0.3.0]: https://github.com/scottpmiller/evalcore/compare/0.2.0...0.3.0
76
+ [0.2.0]: https://github.com/scottpmiller/evalcore/compare/0.1.0...0.2.0
77
+ [0.1.0]: https://github.com/scottpmiller/evalcore/releases/tag/0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: evalcore
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs.
5
5
  Project-URL: Homepage, https://github.com/scottpmiller/evalcore
6
6
  Project-URL: Repository, https://github.com/scottpmiller/evalcore
@@ -20,7 +20,7 @@ adapter + datasets + graders config," not "fork the framework." If it leaks,
20
20
  every new consumer reopens the core.
21
21
 
22
22
  This shape is well-trodden - promptfoo, OpenAI Evals, Braintrust, and LangSmith
23
- all converge on the same pipeline (case target graders comparison
23
+ all converge on the same pipeline (case -> target -> graders -> comparison ->
24
24
  gate). evalcore is not inventing the abstraction; it is drawing the line so the
25
25
  engine stays reusable across unrelated systems.
26
26
 
@@ -28,21 +28,21 @@ engine stays reusable across unrelated systems.
28
28
 
29
29
  | Concern | **Engine** | **Consumer** |
30
30
  |---|---|---|
31
- | Case / suite schema + loading | | uses it |
32
- | Runner: sampling (N), concurrency, live/replay modes | | picks mode/config |
33
- | Target-adapter protocol + built-ins (HTTP, replay) | | configures/extends |
34
- | Variant model (baseline vs candidate knobs) | opaque dict | defines its knobs |
35
- | Grader protocol + registry | | registers its graders |
36
- | Generic graders: deterministic, LLM-judge, classification, numeric | | configures + writes custom |
37
- | Comparison / regression engine (win metric, guardrails) | | sets thresholds |
38
- | Gate decision (comparison pass/warn/fail) | | sets policy |
39
- | Results store + row schema / outbox | | gets a `project` namespace |
40
- | Reporters (Markdown / HTML) | | gets them free |
41
- | Datasets (cases + labels) | | owns |
42
- | Judge **rubrics** | | owns |
43
- | Custom graders / adapters | | owns |
44
- | Thresholds / guardrail config | | owns |
45
- | Which suite runs on which change trigger | | owns |
31
+ | Case / suite schema + loading | yes | uses it |
32
+ | Runner: sampling (N), concurrency, live/replay modes | yes | picks mode/config |
33
+ | Target-adapter protocol + built-ins (HTTP, replay) | yes | configures/extends |
34
+ | Variant model (baseline vs candidate knobs) | yes opaque dict | defines its knobs |
35
+ | Grader protocol + registry | yes | registers its graders |
36
+ | Generic graders: deterministic, LLM-judge, classification, numeric | yes | configures + writes custom |
37
+ | Comparison / regression engine (win metric, guardrails) | yes | sets thresholds |
38
+ | Gate decision (comparison -> pass/warn/fail) | yes | sets policy |
39
+ | Results store + row schema / outbox | yes | gets a `project` namespace |
40
+ | Reporters (Markdown / HTML) | yes | gets them free |
41
+ | Datasets (cases + labels) | no | yes owns |
42
+ | Judge **rubrics** | no | yes owns |
43
+ | Custom graders / adapters | no | yes owns |
44
+ | Thresholds / guardrail config | no | yes owns |
45
+ | Which suite runs on which change trigger | no | yes owns |
46
46
 
47
47
  Everything in the left column is consumer-agnostic. The right column is what a
48
48
  new team writes.
@@ -66,12 +66,12 @@ Eight concepts, none of which mention any particular system:
66
66
  own for anything else.
67
67
  - **Grader** - `grade(case, output) -> [Score]` where
68
68
  `Score = {metric, value, passed?, detail}`. Registry-based; tiered
69
- (deterministic numeric classification LLM judge).
70
- - **Run** - execute `suite × variant` over all cases × N samples outputs +
69
+ (deterministic -> numeric -> classification -> LLM judge).
70
+ - **Run** - execute `suite × variant` over all cases × N samples -> outputs +
71
71
  scores, persisted as a `RunResult`.
72
72
  - **Scorecard** - aggregated scores for one run (mean/stdev per metric).
73
- - **Comparison + Gate** - candidate scorecard vs baseline deltas, guardrail
74
- checks, win metric `pass | warn | fail`.
73
+ - **Comparison + Gate** - candidate scorecard vs baseline -> deltas, guardrail
74
+ checks, win metric -> `pass | warn | fail`.
75
75
 
76
76
  ```python
77
77
  class TargetAdapter(typing.Protocol):
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "evalcore"
3
- version = "0.2.0"
3
+ version = "0.3.0"
4
4
  description = "A generic, consumer-agnostic evaluation engine for prompt, model, and API outputs."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -160,19 +160,28 @@ def _cmd_gate(args: argparse.Namespace) -> int:
160
160
  )
161
161
  _emit_report(args, reporters.wrap_document(rep, body))
162
162
 
163
- if args.export:
164
- exporter = store.JsonlOutboxExporter(args.export)
165
- exporter.export(baseline.scorecard)
166
- exporter.export(candidate.scorecard)
167
- print(f'\nexported scorecards -> {args.export}', file=sys.stderr)
168
- if args.export_scores:
169
- exporter = store.JsonlOutboxExporter(args.export_scores)
170
- rows = exporter.export_scores(baseline)
171
- rows += exporter.export_scores(candidate)
172
- print(
173
- f'exported {rows} score rows -> {args.export_scores}',
174
- file=sys.stderr,
163
+ # One feed now, so --export and --export-scores name the same rows;
164
+ # --export-scores is kept as an alias for existing invocations.
165
+ outboxes = dict.fromkeys(
166
+ path for path in (args.export, args.export_scores) if path
167
+ )
168
+ if outboxes:
169
+ types, scales = store.grader_lookups(suite.graders)
170
+ for outbox in outboxes:
171
+ exporter = store.JsonlOutboxExporter(outbox)
172
+ # The baseline half of a pair is not itself gated, so it carries no
173
+ # comparison; the candidate carries the verdict and the guardrails.
174
+ rows = exporter.export_scores(
175
+ baseline, grader_types=types, judge_scales=scales
176
+ )
177
+ rows += exporter.export_scores(
178
+ candidate,
179
+ result,
180
+ baseline_run_id=baseline.run_id,
181
+ grader_types=types,
182
+ judge_scales=scales,
175
183
  )
184
+ print(f'\nexported {rows} store rows -> {outbox}', file=sys.stderr)
176
185
 
177
186
  return 0 if result.verdict != 'fail' else 1
178
187
 
@@ -201,12 +210,16 @@ def _cmd_sweep(args: argparse.Namespace) -> int:
201
210
  args, reporters.wrap_document(rep, reporters.render_sweep(rep, result))
202
211
  )
203
212
  if args.export:
213
+ types, scales = store.grader_lookups(suite.graders)
204
214
  exporter = store.JsonlOutboxExporter(args.export)
205
- for run in runs:
206
- exporter.export(run.scorecard)
215
+ rows = sum(
216
+ exporter.export_scores(
217
+ run, grader_types=types, judge_scales=scales
218
+ )
219
+ for run in runs
220
+ )
207
221
  print(
208
- f'\nexported {len(runs)} scorecards -> {args.export}',
209
- file=sys.stderr,
222
+ f'\nexported {rows} store rows -> {args.export}', file=sys.stderr
210
223
  )
211
224
  return 0
212
225
 
@@ -438,11 +451,13 @@ def build_parser() -> argparse.ArgumentParser:
438
451
  help='grader mode when it differs from --mode '
439
452
  '(e.g. live judges on replayed data)',
440
453
  )
441
- gate.add_argument('--export', help='append scorecards to a JSONL outbox')
454
+ gate.add_argument(
455
+ '--export', help='append results-store rows to a JSONL outbox'
456
+ )
442
457
  gate.add_argument(
443
458
  '--export-scores',
444
459
  dest='export_scores',
445
- help='append per-case score rows to a JSONL outbox (eval_scores)',
460
+ help='alias for --export, kept for existing invocations',
446
461
  )
447
462
  gate.add_argument(
448
463
  '--revision', help='opaque provenance id stamped on both scorecards'
@@ -474,7 +489,9 @@ def build_parser() -> argparse.ArgumentParser:
474
489
  help='grader mode when it differs from --mode '
475
490
  '(e.g. live judges on replayed data)',
476
491
  )
477
- sweep.add_argument('--export', help='append scorecards to a JSONL outbox')
492
+ sweep.add_argument(
493
+ '--export', help='append results-store rows to a JSONL outbox'
494
+ )
478
495
  sweep.add_argument('--revision')
479
496
  sweep.add_argument(
480
497
  '--report',
@@ -162,11 +162,17 @@ class RunResult(pydantic.BaseModel):
162
162
  one entry per (case, sample) with the full output, artifacts, and
163
163
  per-case scores. Persisting it is what makes transcript review,
164
164
  human rating, and judge-agreement analysis possible after the fact.
165
+
166
+ ``aggregate_scores`` holds the ``kind='aggregate'`` scores computed once
167
+ over the whole run. The scorecard keeps their values but not which grader
168
+ emitted them or what it reported, so they are retained here too - a
169
+ results-store row needs the grader attribution.
165
170
  """
166
171
 
167
172
  run_id: str
168
173
  scorecard: Scorecard
169
174
  results: list[CaseResult] = pydantic.Field(default_factory=list)
175
+ aggregate_scores: list[Score] = pydantic.Field(default_factory=list)
170
176
 
171
177
 
172
178
  class Rating(pydantic.BaseModel):
@@ -13,7 +13,9 @@ concurrent calls.
13
13
 
14
14
  import asyncio
15
15
  import inspect
16
+ import secrets
16
17
  import statistics
18
+ import time
17
19
  import uuid
18
20
 
19
21
  from evalcore import loader, models, store
@@ -47,6 +49,25 @@ async def _invoke_with_retry(
47
49
  attempt += 1
48
50
 
49
51
 
52
+ def _uuid7() -> uuid.UUID:
53
+ """A UUIDv7 (RFC 9562): 48-bit millisecond timestamp, then random.
54
+
55
+ Time-ordered, so a run id carries its own creation time and ids sort in
56
+ the order the runs happened. ``uuid.uuid7`` is 3.14; this is the fallback
57
+ for 3.11 through 3.13.
58
+ """
59
+ if hasattr(uuid, 'uuid7'):
60
+ return uuid.uuid7()
61
+ ms = int(time.time() * 1000) & 0xFFFFFFFFFFFF
62
+ return uuid.UUID(
63
+ int=(ms << 80)
64
+ | (0x7 << 76)
65
+ | (secrets.randbits(12) << 64)
66
+ | (0b10 << 62)
67
+ | secrets.randbits(62)
68
+ )
69
+
70
+
50
71
  def _aggregate_metrics(
51
72
  results: list[models.CaseResult], agg_scores: list[models.Score]
52
73
  ) -> dict[str, models.MetricValue]:
@@ -140,7 +161,7 @@ async def run_suite(
140
161
 
141
162
  cases = loader.load_cases(suite.dataset)
142
163
  dataset_hash = loader.dataset_hash(cases)
143
- run_id = uuid.uuid4().hex
164
+ run_id = str(_uuid7())
144
165
 
145
166
  # Resume: reuse already-completed (case, sample) results from a checkpoint
146
167
  # of the *same* eval; otherwise start (or restart) the checkpoint fresh.
@@ -272,7 +293,10 @@ async def run_suite(
272
293
  metrics=_aggregate_metrics(results, agg_scores),
273
294
  )
274
295
  return models.RunResult(
275
- run_id=run_id, scorecard=scorecard, results=results
296
+ run_id=run_id,
297
+ scorecard=scorecard,
298
+ results=results,
299
+ aggregate_scores=agg_scores,
276
300
  )
277
301
 
278
302