Spaces:
Running
Running
Fix validated evaluator displays
Browse files- app/evals/page.tsx +10 -4
- components/evaluator-table.tsx +20 -7
- lib/clean-hierarchy.ts +62 -0
- tests/clean-hierarchy.test.ts +103 -0
app/evals/page.tsx
CHANGED
|
@@ -66,6 +66,12 @@ function EvalsPageInner() {
|
|
| 66 |
)
|
| 67 |
const [evaluatorSortCol, setEvaluatorSortCol] = useState<EvaluatorTableSortCol>("evals")
|
| 68 |
const [evaluatorSortDir, setEvaluatorSortDir] = useState<"asc" | "desc">("desc")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
const deferredSearchQuery = useDeferredValue(searchQuery)
|
| 70 |
|
| 71 |
const handleEvaluatorSort = useCallback((col: EvaluatorTableSortCol) => {
|
|
@@ -178,13 +184,13 @@ function EvalsPageInner() {
|
|
| 178 |
const dirMul = evaluatorSortDir === "asc" ? 1 : -1
|
| 179 |
return list.slice().sort((a, b) => {
|
| 180 |
let cmp = 0
|
| 181 |
-
if (
|
| 182 |
-
else if (
|
| 183 |
else cmp = a.evalCount - b.evalCount
|
| 184 |
if (cmp === 0) cmp = a.name.localeCompare(b.name)
|
| 185 |
return cmp * dirMul
|
| 186 |
})
|
| 187 |
-
}, [evaluatorGroups, deferredSearchQuery,
|
| 188 |
|
| 189 |
const families = hierarchy?.families ?? []
|
| 190 |
|
|
@@ -502,7 +508,7 @@ function EvalsPageInner() {
|
|
| 502 |
) : groupBy === "evaluator" ? (
|
| 503 |
<EvaluatorTable
|
| 504 |
rows={visibleEvaluators}
|
| 505 |
-
sortCol={
|
| 506 |
sortDir={evaluatorSortDir}
|
| 507 |
onSort={handleEvaluatorSort}
|
| 508 |
verifiedOnly={verifiedOnly}
|
|
|
|
| 66 |
)
|
| 67 |
const [evaluatorSortCol, setEvaluatorSortCol] = useState<EvaluatorTableSortCol>("evals")
|
| 68 |
const [evaluatorSortDir, setEvaluatorSortDir] = useState<"asc" | "desc">("desc")
|
| 69 |
+
// In verified-only mode the Verified column is hidden (it equals
|
| 70 |
+
// Evaluations reported), so a stale "verified" sort would otherwise sit
|
| 71 |
+
// active on a column the user can no longer see or toggle. Clamp it to
|
| 72 |
+
// "evals" — same ordering, but on a visible, interactive header.
|
| 73 |
+
const effectiveEvaluatorSortCol: EvaluatorTableSortCol =
|
| 74 |
+
verifiedOnly && evaluatorSortCol === "verified" ? "evals" : evaluatorSortCol
|
| 75 |
const deferredSearchQuery = useDeferredValue(searchQuery)
|
| 76 |
|
| 77 |
const handleEvaluatorSort = useCallback((col: EvaluatorTableSortCol) => {
|
|
|
|
| 184 |
const dirMul = evaluatorSortDir === "asc" ? 1 : -1
|
| 185 |
return list.slice().sort((a, b) => {
|
| 186 |
let cmp = 0
|
| 187 |
+
if (effectiveEvaluatorSortCol === "name") cmp = a.name.localeCompare(b.name)
|
| 188 |
+
else if (effectiveEvaluatorSortCol === "verified") cmp = a.verifiedCount - b.verifiedCount
|
| 189 |
else cmp = a.evalCount - b.evalCount
|
| 190 |
if (cmp === 0) cmp = a.name.localeCompare(b.name)
|
| 191 |
return cmp * dirMul
|
| 192 |
})
|
| 193 |
+
}, [evaluatorGroups, deferredSearchQuery, effectiveEvaluatorSortCol, evaluatorSortDir])
|
| 194 |
|
| 195 |
const families = hierarchy?.families ?? []
|
| 196 |
|
|
|
|
| 508 |
) : groupBy === "evaluator" ? (
|
| 509 |
<EvaluatorTable
|
| 510 |
rows={visibleEvaluators}
|
| 511 |
+
sortCol={effectiveEvaluatorSortCol}
|
| 512 |
sortDir={evaluatorSortDir}
|
| 513 |
onSort={handleEvaluatorSort}
|
| 514 |
verifiedOnly={verifiedOnly}
|
components/evaluator-table.tsx
CHANGED
|
@@ -67,7 +67,11 @@ export function EvaluatorTable({ rows, sortCol, sortDir, onSort, verifiedOnly }:
|
|
| 67 |
<tr>
|
| 68 |
<SortTh col="name" style={{ width: "55%" }}>Evaluator</SortTh>
|
| 69 |
<SortTh col="evals" className="num">Evaluations reported</SortTh>
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
<th style={{ width: 90 }} />
|
| 72 |
</tr>
|
| 73 |
</thead>
|
|
@@ -85,18 +89,27 @@ export function EvaluatorTable({ rows, sortCol, sortDir, onSort, verifiedOnly }:
|
|
| 85 |
</Link>
|
| 86 |
</td>
|
| 87 |
<td className="num font-mono text-[13px]">
|
| 88 |
-
{
|
| 89 |
-
</td>
|
| 90 |
-
<td className="num font-mono text-[13px]">
|
| 91 |
-
{row.verifiedCount > 0 ? (
|
| 92 |
<span className="inline-flex items-center gap-1 text-[color:var(--accent)]">
|
| 93 |
-
{row.
|
| 94 |
<VerifiedBadge verified size="sm" withTooltip={false} />
|
| 95 |
</span>
|
| 96 |
) : (
|
| 97 |
-
|
| 98 |
)}
|
| 99 |
</td>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
<td>
|
| 101 |
<Link
|
| 102 |
href={hrefFor(row.slug)}
|
|
|
|
| 67 |
<tr>
|
| 68 |
<SortTh col="name" style={{ width: "55%" }}>Evaluator</SortTh>
|
| 69 |
<SortTh col="evals" className="num">Evaluations reported</SortTh>
|
| 70 |
+
{/* In verified-only mode every (eval, org) membership is already
|
| 71 |
+
verified, so verifiedCount === evalCount for every row — the
|
| 72 |
+
two columns are identical. Drop the redundant Verified column
|
| 73 |
+
there and keep it only when the counts can differ. */}
|
| 74 |
+
{!verifiedOnly && <SortTh col="verified" className="num">Verified</SortTh>}
|
| 75 |
<th style={{ width: 90 }} />
|
| 76 |
</tr>
|
| 77 |
</thead>
|
|
|
|
| 89 |
</Link>
|
| 90 |
</td>
|
| 91 |
<td className="num font-mono text-[13px]">
|
| 92 |
+
{verifiedOnly ? (
|
|
|
|
|
|
|
|
|
|
| 93 |
<span className="inline-flex items-center gap-1 text-[color:var(--accent)]">
|
| 94 |
+
{row.evalCount.toLocaleString()}
|
| 95 |
<VerifiedBadge verified size="sm" withTooltip={false} />
|
| 96 |
</span>
|
| 97 |
) : (
|
| 98 |
+
row.evalCount.toLocaleString()
|
| 99 |
)}
|
| 100 |
</td>
|
| 101 |
+
{!verifiedOnly && (
|
| 102 |
+
<td className="num font-mono text-[13px]">
|
| 103 |
+
{row.verifiedCount > 0 ? (
|
| 104 |
+
<span className="inline-flex items-center gap-1 text-[color:var(--accent)]">
|
| 105 |
+
{row.verifiedCount.toLocaleString()}
|
| 106 |
+
<VerifiedBadge verified size="sm" withTooltip={false} />
|
| 107 |
+
</span>
|
| 108 |
+
) : (
|
| 109 |
+
<span className="text-[color:var(--fg-subtle)]">—</span>
|
| 110 |
+
)}
|
| 111 |
+
</td>
|
| 112 |
+
)}
|
| 113 |
<td>
|
| 114 |
<Link
|
| 115 |
href={hrefFor(row.slug)}
|
lib/clean-hierarchy.ts
CHANGED
|
@@ -214,6 +214,7 @@ export function cleanHierarchy(
|
|
| 214 |
collapseValsAiSetupVariants(h)
|
| 215 |
dedupValsAiAliasedBenches(h)
|
| 216 |
flattenSplitFamilies(h)
|
|
|
|
| 217 |
if (comparisonIndex) {
|
| 218 |
dedupAggregatorBenchesByScore(h, comparisonIndex)
|
| 219 |
}
|
|
@@ -1556,6 +1557,67 @@ function flattenSplitFamilies(h: CleanableHierarchy) {
|
|
| 1556 |
}
|
| 1557 |
}
|
| 1558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1559 |
export function isHierarchyCleaned(h: EvalHierarchy | null | undefined): boolean {
|
| 1560 |
return Boolean((h as CleanableHierarchy | null | undefined)?.[CLEANED_MARKER])
|
| 1561 |
}
|
|
|
|
| 214 |
collapseValsAiSetupVariants(h)
|
| 215 |
dedupValsAiAliasedBenches(h)
|
| 216 |
flattenSplitFamilies(h)
|
| 217 |
+
dropGroupingLeaderboardRollups(h)
|
| 218 |
if (comparisonIndex) {
|
| 219 |
dedupAggregatorBenchesByScore(h, comparisonIndex)
|
| 220 |
}
|
|
|
|
| 1557 |
}
|
| 1558 |
}
|
| 1559 |
|
| 1560 |
+
/**
|
| 1561 |
+
* Drop a grouping's own aggregate "leaderboard" rollup benchmark.
|
| 1562 |
+
*
|
| 1563 |
+
* Some sources ship, inside a composite (or a multi-benchmark family), an
|
| 1564 |
+
* extra benchmark that is just the aggregate score for the whole group —
|
| 1565 |
+
* e.g. HELM's `helm-safety` composite ("HELM Safety") carries a
|
| 1566 |
+
* `helm-safety-leaderboard` benchmark ("HELM-Safety-Leaderboard") that is
|
| 1567 |
+
* the composite's own rollup. Listing it as a sibling benchmark makes the
|
| 1568 |
+
* grouping show up as BOTH a family/group AND a benchmark — a semantic
|
| 1569 |
+
* duplicate ("HELM Safety" is only a family, not a benchmark). We strip the
|
| 1570 |
+
* rollup leaf so the group is only ever a group; the real member benchmarks
|
| 1571 |
+
* (BBQ, HarmBench, …) stay.
|
| 1572 |
+
*
|
| 1573 |
+
* Detection is deliberately narrow: a leaf qualifies only when its key is
|
| 1574 |
+
* the parent grouping's key plus a `-leaderboard` suffix
|
| 1575 |
+
* (`${parentKey}-leaderboard`). That suffix is an unambiguous rollup signal
|
| 1576 |
+
* — it catches all six HELM composites without touching real sibling
|
| 1577 |
+
* benchmarks whose slug merely coincides with the family (e.g.
|
| 1578 |
+
* `reward-bench`'s genuine `rewardbench` benchmark sitting beside
|
| 1579 |
+
* RewardBench 2 / Safety / Reasoning). We only strip within groups that
|
| 1580 |
+
* keep at least one other benchmark, so single-benchmark families — where
|
| 1581 |
+
* the lone bench legitimately IS the family — are never touched.
|
| 1582 |
+
*
|
| 1583 |
+
* We drop only the benchmark leaf, NOT the rollup's eval ids from
|
| 1584 |
+
* `family.constituent_evaluation_ids`. Those ids stay so the rollup eval row
|
| 1585 |
+
* still resolves to its family/composite in the hierarchy lookup (it just
|
| 1586 |
+
* no longer carries a benchmark-leaf label) — the row is an aggregate, not
|
| 1587 |
+
* a distinct benchmark, which is exactly the outcome we want.
|
| 1588 |
+
*/
|
| 1589 |
+
function dropGroupingLeaderboardRollups(h: CleanableHierarchy) {
|
| 1590 |
+
const slug = (s: string) => (s ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "")
|
| 1591 |
+
const isSelfRollup = (b: HierarchyBenchmark, parentKey: string): boolean =>
|
| 1592 |
+
Boolean(b.key) &&
|
| 1593 |
+
b.key.endsWith("-leaderboard") &&
|
| 1594 |
+
slug(b.key.replace(/-leaderboard$/, "")) === slug(parentKey)
|
| 1595 |
+
|
| 1596 |
+
const strip = (
|
| 1597 |
+
benches: HierarchyBenchmark[],
|
| 1598 |
+
parentKey: string,
|
| 1599 |
+
): HierarchyBenchmark[] => {
|
| 1600 |
+
if (benches.length < 2) return benches
|
| 1601 |
+
const kept = benches.filter((b) => !isSelfRollup(b, parentKey))
|
| 1602 |
+
// Never empty a group; only apply when something actually dropped.
|
| 1603 |
+
return kept.length > 0 && kept.length < benches.length ? kept : benches
|
| 1604 |
+
}
|
| 1605 |
+
|
| 1606 |
+
for (const fam of h.families ?? []) {
|
| 1607 |
+
// Composites are always groupings — strip their self-rollup leaf.
|
| 1608 |
+
for (const c of fam.composites ?? []) {
|
| 1609 |
+
if (c.benchmarks) c.benchmarks = strip(c.benchmarks, c.key)
|
| 1610 |
+
}
|
| 1611 |
+
// Family-level rollup (a `${family.key}-leaderboard` bench sitting
|
| 1612 |
+
// directly under a multi-benchmark family). None in the current
|
| 1613 |
+
// snapshot, but keep the hierarchy consistent if one appears.
|
| 1614 |
+
if (fam.benchmarks) fam.benchmarks = strip(fam.benchmarks, fam.key)
|
| 1615 |
+
if (fam.standalone_benchmarks) {
|
| 1616 |
+
fam.standalone_benchmarks = strip(fam.standalone_benchmarks, fam.key)
|
| 1617 |
+
}
|
| 1618 |
+
}
|
| 1619 |
+
}
|
| 1620 |
+
|
| 1621 |
export function isHierarchyCleaned(h: EvalHierarchy | null | undefined): boolean {
|
| 1622 |
return Boolean((h as CleanableHierarchy | null | undefined)?.[CLEANED_MARKER])
|
| 1623 |
}
|
tests/clean-hierarchy.test.ts
CHANGED
|
@@ -457,6 +457,109 @@ describe("cleanHierarchy", () => {
|
|
| 457 |
)
|
| 458 |
})
|
| 459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
it("is idempotent: re-applying produces identical output", () => {
|
| 461 |
const raw: EvalHierarchy = {
|
| 462 |
families: [family("aime", "AIME")],
|
|
|
|
| 457 |
)
|
| 458 |
})
|
| 459 |
|
| 460 |
+
it("drops a grouping's own *-leaderboard rollup but keeps real members", () => {
|
| 461 |
+
// HELM's `helm-safety` composite ("HELM Safety", a grouping) ships a
|
| 462 |
+
// `helm-safety-leaderboard` benchmark that is the composite's own
|
| 463 |
+
// aggregate — it makes the group show up as both a family and a
|
| 464 |
+
// benchmark. The rollup leaf is dropped; the real members survive.
|
| 465 |
+
const raw: EvalHierarchy = {
|
| 466 |
+
families: [
|
| 467 |
+
family("helm", "HELM", {
|
| 468 |
+
composites: [
|
| 469 |
+
{
|
| 470 |
+
key: "helm-safety",
|
| 471 |
+
display_name: "HELM Safety",
|
| 472 |
+
category: "Safety",
|
| 473 |
+
tags: { domains: [], languages: [], tasks: [] },
|
| 474 |
+
benchmarks: [
|
| 475 |
+
bench("bbq", "BBQ"),
|
| 476 |
+
bench("harmbench", "HarmBench"),
|
| 477 |
+
bench("helm-safety-leaderboard", "HELM-Safety-Leaderboard"),
|
| 478 |
+
],
|
| 479 |
+
},
|
| 480 |
+
],
|
| 481 |
+
}),
|
| 482 |
+
],
|
| 483 |
+
}
|
| 484 |
+
|
| 485 |
+
const cleaned = cleanHierarchy(raw)
|
| 486 |
+
const helm = cleaned.families.find((f) => f.key === "helm")!
|
| 487 |
+
const safety = helm.composites!.find((c) => c.key === "helm-safety")!
|
| 488 |
+
const keys = (safety.benchmarks ?? []).map((b) => b.key)
|
| 489 |
+
expect(keys).not.toContain("helm-safety-leaderboard")
|
| 490 |
+
expect(keys).toEqual(expect.arrayContaining(["bbq", "harmbench"]))
|
| 491 |
+
})
|
| 492 |
+
|
| 493 |
+
it("keeps a real sibling whose slug matches the family but lacks the -leaderboard suffix", () => {
|
| 494 |
+
// reward-bench's genuine `rewardbench` benchmark slugifies the same as
|
| 495 |
+
// the `reward-bench` family but is a real member, not a rollup. The
|
| 496 |
+
// -leaderboard-only rule must leave it (and its siblings) alone.
|
| 497 |
+
const raw: EvalHierarchy = {
|
| 498 |
+
families: [
|
| 499 |
+
family("foo-bench", "Foo Bench", {
|
| 500 |
+
benchmarks: [bench("foobench", "Foo Bench"), bench("foobench-2", "Foo Bench 2")],
|
| 501 |
+
}),
|
| 502 |
+
],
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
const cleaned = cleanHierarchy(raw)
|
| 506 |
+
const fam = cleaned.families.find((f) => f.key === "foo-bench")!
|
| 507 |
+
const keys = [...(fam.benchmarks ?? []), ...(fam.standalone_benchmarks ?? [])].map((b) => b.key)
|
| 508 |
+
expect(keys).toEqual(expect.arrayContaining(["foobench", "foobench-2"]))
|
| 509 |
+
})
|
| 510 |
+
|
| 511 |
+
it("drops a family-level rollup and preserves its eval ids on the family", () => {
|
| 512 |
+
const raw: EvalHierarchy = {
|
| 513 |
+
families: [
|
| 514 |
+
family("widget-bench", "Widget Bench", {
|
| 515 |
+
constituent_evaluation_ids: [
|
| 516 |
+
"widget-bench%2Fwidget-bench-leaderboard",
|
| 517 |
+
"widget-bench%2Fwidget-a",
|
| 518 |
+
],
|
| 519 |
+
standalone_benchmarks: [
|
| 520 |
+
bench("widget-bench-leaderboard", "Widget Bench Leaderboard"),
|
| 521 |
+
bench("widget-a", "Widget A"),
|
| 522 |
+
],
|
| 523 |
+
}),
|
| 524 |
+
],
|
| 525 |
+
}
|
| 526 |
+
|
| 527 |
+
const cleaned = cleanHierarchy(raw)
|
| 528 |
+
const fam = cleaned.families.find((f) => f.key === "widget-bench")!
|
| 529 |
+
const keys = [...(fam.benchmarks ?? []), ...(fam.standalone_benchmarks ?? [])].map((b) => b.key)
|
| 530 |
+
// Rollup leaf gone, real member kept.
|
| 531 |
+
expect(keys).not.toContain("widget-bench-leaderboard")
|
| 532 |
+
expect(keys).toContain("widget-a")
|
| 533 |
+
// …but the rollup's eval id stays on the family so it still resolves.
|
| 534 |
+
expect(fam.constituent_evaluation_ids).toContain(
|
| 535 |
+
"widget-bench%2Fwidget-bench-leaderboard",
|
| 536 |
+
)
|
| 537 |
+
})
|
| 538 |
+
|
| 539 |
+
it("never empties a group: a lone *-leaderboard bench is kept", () => {
|
| 540 |
+
const raw: EvalHierarchy = {
|
| 541 |
+
families: [
|
| 542 |
+
family("solo", "Solo", {
|
| 543 |
+
composites: [
|
| 544 |
+
{
|
| 545 |
+
key: "solo-grp",
|
| 546 |
+
display_name: "Solo Grp",
|
| 547 |
+
category: "General",
|
| 548 |
+
tags: { domains: [], languages: [], tasks: [] },
|
| 549 |
+
benchmarks: [bench("solo-grp-leaderboard", "Solo Grp Leaderboard")],
|
| 550 |
+
},
|
| 551 |
+
],
|
| 552 |
+
}),
|
| 553 |
+
],
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
const cleaned = cleanHierarchy(raw)
|
| 557 |
+
const grp = cleaned.families
|
| 558 |
+
.find((f) => f.key === "solo")
|
| 559 |
+
?.composites?.find((c) => c.key === "solo-grp")
|
| 560 |
+
expect(grp?.benchmarks?.map((b) => b.key)).toEqual(["solo-grp-leaderboard"])
|
| 561 |
+
})
|
| 562 |
+
|
| 563 |
it("is idempotent: re-applying produces identical output", () => {
|
| 564 |
const raw: EvalHierarchy = {
|
| 565 |
families: [family("aime", "AIME")],
|