Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions .github/workflows/evaluation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1829,21 +1829,40 @@ jobs:
continue
}

# Parse once, then group by the executor model recorded inside each
# results.json. For a one-model profile (e.g. opus48) all files share
# one model, so this yields exactly one group.
# Parse once, then group by the (executor model, judge model) pair
# recorded inside each results.json. Grouping by model ALONE collapsed
# every judge of one executor into a single entry stamped with the first
# file's judgeModel — so a dual-judge run mislabelled the second judge and
# concatenated both judges' verdicts under one label. Keying on model+judge
# keeps each judge's verdicts and its correct judgeModel, which is exactly
# the dimension the Skill Value view keys on. For a single-judge profile
# all files share one judge, so this still yields one group per model.
#
# Scope note: only the PRIMARY judge's results.json (vally-results-*) feed
# this benchmark data. The scheduled dual-judge cadence re-scores the same
# executor trajectories and uploads under vally-crossjudge-* (see the
# "Download second-judge" step), which only feeds judge-comparison.json —
# never this loop. So Skill Value and Quality/Efficiency both reflect the
# primary judge, and each executor model here carries a single judge.
# (The model+judge key and -SkillValueOnly dedup below stay correct even
# if a future change ever routes two primary judges into this set.)
$parsed = @($resultsFiles | Sort-Object | ForEach-Object {
[pscustomobject]@{ File = $_; Json = (Get-Content $_ -Raw | ConvertFrom-Json) }
})
$modelGroups = @($parsed | Group-Object -Property { "$($_.Json.model)" })
$modelGroups = @($parsed | Group-Object -Property { "$($_.Json.model)|$($_.Json.judgeModel)" })

$existingFile = "/tmp/eval-data/data/$plugin.json"
# Quality/Efficiency views key on executor model alone, so emit them
# once per model. Track which executor models already emitted them; the
# second+ judge of one model runs SkillValue-only to avoid duplicate
# same-model Quality/Efficiency points that would inflate those windows.
$qeEmittedModels = @{}
foreach ($mg in $modelGroups) {
$group = @($mg.Group)
# Merge this model's per-skill/per-shard verdicts into a single
# synthetic results.json. Schema: { model, verdicts[], ... }. Concat
# the verdicts arrays and keep top-level scalars from the first file
# of the group (all share the same model/judgeModel).
# Merge this (model, judge) pair's per-skill/per-shard verdicts into a
# single synthetic results.json. Schema: { model, judgeModel, verdicts[], ... }.
# Concat the verdicts arrays and keep top-level scalars from the first
# file of the group (all now share the same model AND judgeModel).
$merged = $null
$allVerdicts = [System.Collections.Generic.List[object]]::new()
foreach ($item in $group) {
Expand All @@ -1854,12 +1873,16 @@ jobs:
$merged.verdicts = $allVerdicts.ToArray()
$safeModel = ("$($merged.model)" -replace '[^A-Za-z0-9._-]', '_')
if (-not $safeModel) { $safeModel = 'default' }
# Include the judge in the filename so two judges of one executor write
# to distinct synthetic files instead of overwriting each other.
$safeJudge = ("$($merged.judgeModel)" -replace '[^A-Za-z0-9._-]', '_')
if (-not $safeJudge) { $safeJudge = 'nojudge' }
$mergedDir = "all-results/_merged"
New-Item -ItemType Directory -Force -Path $mergedDir | Out-Null
$resultsFile = Join-Path $mergedDir "$plugin--$safeModel.results.json"
$resultsFile = Join-Path $mergedDir "$plugin--$safeModel--$safeJudge.results.json"
$merged | ConvertTo-Json -Depth 100 | Out-File -FilePath $resultsFile -Encoding utf8

Write-Host "`n=== Generating benchmark data for: $plugin (model '$($merged.model)', $($allVerdicts.Count) verdicts from $($group.Count) results.json) ==="
Write-Host "`n=== Generating benchmark data for: $plugin (model '$($merged.model)', judge '$($merged.judgeModel)', $($allVerdicts.Count) verdicts from $($group.Count) results.json) ==="
$params = @{
ResultsFile = $resultsFile
PluginName = $plugin
Expand All @@ -1869,6 +1892,15 @@ jobs:
Source = 'scheduled'
SkipTokenUsage = $true
}
# Emit Quality/Efficiency once per executor model; SkillValue every
# (model, judge). The first judge of a model writes all three; later
# judges of the same model write SkillValue only.
$execModel = "$($merged.model)"
if ($qeEmittedModels.ContainsKey($execModel)) {
$params.SkillValueOnly = $true
} else {
$qeEmittedModels[$execModel] = $true
}
# Accumulate: each model's entry appends to $plugin.json. The first
# iteration reads the fetched history; later iterations read the file
# the previous iteration just wrote (same path as OutputDir/$plugin.json).
Expand Down Expand Up @@ -2156,6 +2188,7 @@ jobs:
cp ${{ github.workspace }}/eng/dashboard/dashboard.html index.html
cp ${{ github.workspace }}/eng/dashboard/dashboard.js dashboard.js
cp ${{ github.workspace }}/eng/dashboard/token-usage.js token-usage.js
cp ${{ github.workspace }}/eng/dashboard/skill-value.js skill-value.js

# Deploy AGENTVIZ SPA (skip if already present and unchanged)
if [ "${{ steps.check-replay.outputs.skip }}" != "true" ]; then
Expand Down
25 changes: 24 additions & 1 deletion eng/dashboard/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,28 @@
@media (max-width: 600px) {
.charts-grid { grid-template-columns: 1fr; }
}
/* Skill Value view */
.sv-controls { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; margin-bottom: 16px; }
.sv-controls label { font-size: 13px; color: var(--text-muted); }
.sv-controls select {
background: var(--surface); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 6px 8px; font-size: 13px; margin-left: 4px;
}
.sv-sub { display: block; color: var(--text-muted); font-size: 11px; font-weight: 400; margin-top: 2px; }
.sv-controls .sv-sub { display: inline; margin: 0; }
.sv-table td { vertical-align: top; }
.sv-value { font-size: 13px; max-width: 360px; }
.sv-insufficient { color: var(--text-muted); font-style: italic; }
.sv-drill { padding: 4px 0; }
.sv-arm { font-size: 13px; padding: 3px 0; font-family: 'SF Mono', SFMono-Regular, Consolas, monospace; }
.sv-arm-label { display: inline-block; min-width: 110px; color: var(--text-muted); font-family: inherit; }
.sv-diluted { opacity: 0.6; }
.sv-dilute-mark { color: var(--text-muted); margin-right: 3px; cursor: help; font-weight: 600; }
.sv-btn { background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; font-size: 12px; cursor: pointer; }
.sv-btn:hover { border-color: var(--skilled); color: var(--skilled); }
/* Model leaf rows carry the real numbers, so undo the token-table level-2 muting. */
.sv-table tr.level-2 td { color: var(--text); }
.sv-table tr.sv-detail td { padding-left: 88px; }
</style>
</head>
<body>
Expand All @@ -235,7 +257,8 @@ <h1>📊 Skills Evaluation Dashboard</h1>
Generated by the skills evaluation pipeline · Verdict evidence shows the latest retained run per model; 0–10 score averages are triage only, not the pass gate.
</div>

<script src="dashboard.js"></script>
<script src="token-usage.js"></script>
<script src="skill-value.js"></script>
<script src="dashboard.js"></script>
</body>
</html>
41 changes: 32 additions & 9 deletions eng/dashboard/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,44 +34,63 @@
const loadedPlugins = new Map(); // track loaded plugin data

// Build tabs and placeholder panels
plugins.forEach((plugin, idx) => {
plugins.forEach((plugin) => {
const tab = document.createElement('div');
tab.className = 'tab' + (idx === 0 ? ' active' : '');
tab.className = 'tab';
tab.textContent = plugin;
tab.dataset.plugin = plugin;
tab.addEventListener('click', () => switchTab(plugin));
tabBar.appendChild(tab);

const panel = document.createElement('div');
panel.className = 'tab-content' + (idx === 0 ? ' active' : '');
panel.className = 'tab-content';
panel.id = `panel-${plugin}`;
panel.innerHTML = '<p style="color:#8b949e;text-align:center;padding:2rem;">Loading...</p>';
tabContentContainer.appendChild(panel);
});

// Add Token Usage tab at the end
const tokenTabId = '__token-usage__';
const noPlugins = plugins.length === 0;
const tokenTab = document.createElement('div');
tokenTab.className = 'tab' + (noPlugins ? ' active' : '');
tokenTab.className = 'tab';
tokenTab.textContent = '🔢 Token Usage';
tokenTab.dataset.plugin = tokenTabId;
tokenTab.addEventListener('click', () => switchTab(tokenTabId));
tabBar.appendChild(tokenTab);

const tokenPanel = document.createElement('div');
tokenPanel.className = 'tab-content' + (noPlugins ? ' active' : '');
tokenPanel.className = 'tab-content';
tokenPanel.id = `panel-${tokenTabId}`;
tokenPanel.innerHTML = '<div id="token-usage-content"><p style="color:#8b949e;text-align:center;padding:2rem;">Loading…</p></div>';
tabContentContainer.appendChild(tokenPanel);

// Skill Value is the default landing tab, placed FIRST in the tab bar so the
// per-skill value story is the first thing a viewer sees.
const skillValueTabId = '__skill-value__';
const skillValueTab = document.createElement('div');
skillValueTab.className = 'tab active';
skillValueTab.textContent = '💡 Skill Value';
skillValueTab.dataset.plugin = skillValueTabId;
skillValueTab.addEventListener('click', () => switchTab(skillValueTabId));
tabBar.insertBefore(skillValueTab, tabBar.firstChild);

const skillValuePanel = document.createElement('div');
skillValuePanel.className = 'tab-content active';
skillValuePanel.id = `panel-${skillValueTabId}`;
skillValuePanel.innerHTML = '<div id="skill-value-content"><p style="color:#8b949e;text-align:center;padding:2rem;">Loading…</p></div>';
tabContentContainer.appendChild(skillValuePanel);

async function switchTab(plugin) {
tabBar.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t.dataset.plugin === plugin));
tabContentContainer.querySelectorAll('.tab-content').forEach(p => p.classList.toggle('active', p.id === `panel-${plugin}`));
if (plugin === tokenTabId) {
if (window.initTokenUsage) window.initTokenUsage();
return;
}
if (plugin === skillValueTabId) {
if (window.initSkillValue) window.initSkillValue();
return;
}
if (!loadedPlugins.has(plugin)) {
await loadPlugin(plugin);
}
Expand Down Expand Up @@ -1126,8 +1145,12 @@
]);
}

// Load first plugin immediately (skip if no evaluation plugins)
if (plugins.length > 0) {
await loadPlugin(plugins[0]);
// Skill Value is the default active tab, so render it immediately. Plugin tabs
// load lazily on first click; Token Usage self-inits when its tab is shown.
if (window.initSkillValue) {
window.initSkillValue();
} else if (plugins.length > 0) {
// Defensive fallback: if skill-value.js failed to load, activate the first plugin.
await switchTab(plugins[0]);
}
Comment thread
AbhitejJohn marked this conversation as resolved.
})();
Loading