Eval score summaries: report percentiles, not just the mean - #1636
Conversation
Numeric (custom-type) eval scores - tool-call counts, tokens per call, per-turn latency - are heavily right-skewed over a large sweep, and a mean alone hides the tail that dominates cost and risk. ScoreSummary now carries min / p25 / median / p75 / p90 / max alongside mean_score. Strictly additive: mean_score, n_used and n_excluded are unchanged in name and semantics, and every new field is optional with a None default, so existing API and TypeScript consumers keep working untouched. Both aggregation sites (compute_score_summary and the run-config eval_scores endpoint) now accumulate the individual scores rather than a running total, and share one score_summary_from_values helper - so the two sites cannot drift on the percentile definition. Skipped runs are excluded exactly as they already were for the mean. Percentiles reuse the existing kiln_server.statistics_lib.percentile: linear interpolation between the two nearest order statistics, matching the numpy.percentile / statistics.quantiles default. Empty input yields None for every statistic, never 0.0 - a 0 would flow into downstream aggregation as if it were a real datum. Tests cover empty / single / odd / even / all-skipped at the helper, at compute_score_summary and through the eval_scores endpoint, plus the previously untested statistics_lib.percentile. api_schema.d.ts is regenerated with generate_schema.sh (additive, all fields optional). Committed with --no-verify: the committed .git/hooks/pre-commit cd's to the main checkout's hook directory, so from a worktree it lints the wrong tree. The checks were instead run directly against this tree: ruff check and ruff format --check pass repo-wide, check_schema.sh reports the OpenAPI schema up to date, and the touched test modules pass. The one failing test under checks.sh (test_document_api content-type detection for .md) reproduces on the pristine branch with these changes stashed, and the web checks fail only because node_modules is not installed in a fresh worktree. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review. WalkthroughThe evaluation API now calculates minimum, percentile, median, maximum, and mean scores from retained values. Task-level and run-config-level summaries use the shared aggregation helper. Tests and the web API schema cover populated and empty summaries. ChangesScore distribution aggregation
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to This PR adds optional percentile fields while preserving existing score-summary behavior and reports passing targeted tests. No actionable merge-blocking risk remains beyond normal checks and review. Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
📊 Coverage ReportOverall Coverage: 92% Diff: origin/scosman/evals_v2...HEAD
Summary
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
app/desktop/studio_server/eval_api.py (1)
651-681: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winSort
valuesonce inscore_summary_from_values.
statistics_lib.percentile()sorts its input internally, so the four percentile calls re-sortvalues. Sorting once inscore_summary-from_valuesletsmin_score/max_scorecome from the already-sorted array and removes the extra O(n log n) passes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/desktop/studio_server/eval_api.py` around lines 651 - 681, Update score_summary_from_values to sort values once after the empty-input check, then use the sorted sequence for min_score, max_score, and all percentile calculations. Ensure the percentile calls use the sorting-bypassing option supported by statistics_lib.percentile so they do not re-sort the data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@app/desktop/studio_server/eval_api.py`:
- Around line 651-681: Update score_summary_from_values to sort values once
after the empty-input check, then use the sorted sequence for min_score,
max_score, and all percentile calculations. Ensure the percentile calls use the
sorting-bypassing option supported by statistics_lib.percentile so they do not
re-sort the data.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 85bc21f9-21ad-4d7e-8789-d5b24bfa32b9
📒 Files selected for processing (4)
app/desktop/studio_server/eval_api.pyapp/desktop/studio_server/test_eval_api.pyapp/web_ui/src/lib/api_schema.d.tslibs/server/kiln_server/test_statistics_lib.py
Production code and the generated schema merged cleanly. Two test-side conflicts with the run/score split that landed on the base: - import list: union the new base exports with score_summary_from_values. - test_score_summary_percentiles: compute_score_summary now takes a ResolvedSplit rather than a bare id set, so build the split with the base's stub_split helper. The item set (four scored + one skipped) and every distribution assertion are unchanged. app/desktop/studio_server/test_eval_api.py + libs/server/kiln_server/test_statistics_lib.py: 224 passed.
|
Merged Production code (
Both aggregation sites still route through
Generated by Claude Code |
The branch already carried evals_v2 at 4ee3cb9, the merge-base with eb-v2-merge, so this brings eb-v2's 197 commits on top rather than reconciling two lineages. One conflict, an import-list collision in test_eval_api.py: eb-v2 added `scored_trace_usage` where this branch added `score_summary_from_values`. Union of the two. Both aggregation sites still route through score_summary_from_values after the merge — compute_score_summary and the run-config eval_scores endpoint — and the only ScoreSummary constructions left in the file are the two inside the helper, so eb-v2's rework of eval_api didn't introduce a third site that would bypass the shared percentile definition. api_schema.d.ts regenerated against the new base: the same 30-line additive diff, every field optional. No new endpoints, so the agent-policy annotations are byte-identical to the base's. app/desktop/studio_server/test_eval_api.py + test_statistics_lib.py: 240 passed. Full libs/ + app/desktop: 8146 passed, with 5 failures and 5 errors that all reproduce on the base (vector-store, chunker, model-cache benchmark, document-api, and tkinter-less desktop imports). ruff format clean on all three touched files.
|
Retargeted to Clean merge, same shape as #1591's retarget: the branch already carried evals_v2 at One conflict, an import-list collision in The thing worth checking on a base that reworked
Verification on the new base:
Generated by Claude Code |
What
Adds six optional distribution fields to
ScoreSummaryinapp/desktop/studio_server/eval_api.py:min_score,p25_score,median_score,p75_score,p90_score,max_score— alongside the existingmean_score.Both aggregation sites now route through one shared helper,
score_summary_from_values(values, n_excluded):compute_score_summary()eval_scoresendpointBoth previously kept a running total; they now accumulate the individual scores into a list so the summary can report the distribution. Skipped runs are excluded exactly as they already were for the mean, and the
count > 0 or n_excluded > 0emit condition is unchanged.Why
Eval score aggregation currently reports only a mean. For numeric (custom-type) scores — tool-call counts, tokens per call, per-turn latency — the distribution is heavily right-skewed, and the mean badly misrepresents it.
Measured on a 140-conversation sweep: on one lane the mean and median diverged by 1.70× — 224.5s mean vs 132.2s median — driven by a single ~30-minute outlier conversation. Reading the mean alone, that lane looks uniformly slow; it is actually a well-behaved lane with one pathological run. The mean is the wrong summary for deciding whether a regression is broad or a tail event, and the tail is what dominates cost and risk.
Sharing one helper between the two sites is deliberate: it makes it impossible for them to drift onto different percentile definitions.
Percentile definition
Reuses the existing
kiln_server.statistics_lib.percentile— linear interpolation between the two nearest order statistics, matching thenumpy.percentile/statistics.quantiles(method="inclusive")default. Chosen because it is the definition already in the codebase and the one most readers assume, so an even-length list's median is the average of the two middle values rather than the lower one, and p90 of a short list is interpolated rather than snapped to an existing datum. No new dependency.Empty input yields
Nonefor every statistic, never0.0— a0would flow into downstream aggregation as if it were a real datum. The percentiles followmean_score's existing convention here exactly.Merge with the run/score split
scosman/evals_v2merged in. Production code andapi_schema.d.tsmerged cleanly; onlytest_eval_api.pyconflicted, in two places:scored_trace_usage_for_run_config,split_size,summary_eval_config) with this branch'sscore_summary_from_values.test_score_summary_percentiles—compute_score_summarynow takes aResolvedSplitrather than a bare id set, so the split is built with the base'sstub_splithelper. The item set (four scored + one skipped) and every distribution assertion are unchanged.Both aggregation sites still route through the shared helper afterwards, including the
eval_scoresendpoint, which the base reworked aroundeval_run_item_key/ResolvedSplitbut left on the same score-accumulation shape.Back-compat
Strictly additive:
mean_score,n_used,n_excludedare unchanged in name and semantics.Nonedefault, so older stored payloads and existing API/TypeScript consumers keep working untouched.api_schema.d.tswas regenerated with the sanctionedapp/web_ui/src/lib/generate_schema.sh(never hand-edited) — a 30-line additive diff, all fields emitted as optional (?). Re-verified after the merge: the regenerated file byte-matches the checked-in one on everyScoreSummaryfield.Tests
libs/server/kiln_server/test_statistics_lib.py— newTestPercentilecovering the previously untestedpercentile(): empty, single value, odd/even length, unsorted input, all-equal, out-of-range raises, and a parametrized case pinned to numpy's default gold values.app/desktop/studio_server/test_eval_api.py—test_score_summary_percentilesatcompute_score_summary; all-skipped assertions confirming percentiles areNonerather than0.0at both the helper and the endpoint; and endpoint-level distribution assertions over three scores (3.5 / 4.0 / 4.5), which exercise both the exact-index and interpolation branches and pin a case the mean alone cannot distinguish from any other set summing to 12.0.Post-merge:
test_eval_api.py+test_statistics_lib.py— 224 passed.ruff format --checkclean on both touched files.