Skip to content

Commit 590c74e

Browse files
committed
Fix scoring in CLI and add scoring on jobs list
1 parent b66dcdd commit 590c74e

3 files changed

Lines changed: 124 additions & 7 deletions

File tree

cli/src/transformerlab_cli/commands/job.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,22 @@ def _compute_duration(job_data: dict) -> str:
248248
return ""
249249

250250

251-
def _format_score(score: dict) -> str:
252-
"""Format a score dict into a compact string for the table view."""
253-
if not score or not isinstance(score, dict):
254-
return ""
255-
parts = [f"{k}={v}" for k, v in score.items() if v is not None]
251+
def _score_items(score) -> list[tuple[str, object]]:
252+
"""Normalize score payload to key/value pairs and hide internal flags."""
253+
if score is None:
254+
return []
255+
if isinstance(score, dict):
256+
return [
257+
(str(key), value)
258+
for key, value in score.items()
259+
if value is not None and str(key).lower() != "discard"
260+
]
261+
return [("score", score)]
262+
263+
264+
def _format_score(score) -> str:
265+
"""Format score payload into a compact string for the table view."""
266+
parts = [f"{k}={v}" for k, v in _score_items(score)]
256267
text = ", ".join(parts)
257268
if len(text) > 30:
258269
text = text[:27] + "…"
@@ -390,8 +401,12 @@ def _render_job(job) -> None:
390401
details["Config"] = f"\n{config_text}"
391402

392403
score = details.pop("Score")
393-
score_text = "\n".join([f" {key}: {value}" for key, value in score.items()])
394-
details["Score"] = f"\n{score_text}"
404+
score_items = _score_items(score)
405+
if score_items:
406+
score_text = "\n".join([f" {key}: {value}" for key, value in score_items])
407+
details["Score"] = f"\n{score_text}"
408+
else:
409+
details["Score"] = "N/A"
395410

396411
# Display details in a panel
397412
detail_text = "\n".join([f"[bold]{key}:[/bold] {value}" for key, value in details.items()])

cli/tests/commands/test_job.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ def test_job_list_shows_score(_mock_check, _mock_require, _mock_api):
209209
assert "eval/" in out
210210
# Job 1 (no score) should have empty score column — just verify Score header is present
211211
assert "Score" in out
212+
# discard flag should not be shown as a score metric
213+
assert "discard" not in out.lower()
212214

213215

214216
@patch("transformerlab_cli.commands.job.api.get", return_value=_mock_api_response(SAMPLE_JOBS))
@@ -221,6 +223,19 @@ def test_job_info_pretty_shows_discarded(_mock_require, _mock_api):
221223
assert "discarded" in out.lower()
222224

223225

226+
@patch("transformerlab_cli.commands.job.api.get", return_value=_mock_api_response(SAMPLE_JOBS))
227+
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
228+
def test_job_info_pretty_shows_score_without_discard(_mock_require, _mock_api):
229+
"""job info pretty output should show score metrics and hide score.discard."""
230+
result = runner.invoke(app, ["job", "info", "2"])
231+
assert result.exit_code == 0
232+
out = strip_ansi(result.output)
233+
assert "Score:" in out
234+
assert "eval/loss" in out
235+
assert "accuracy" in out
236+
assert "discard: True" not in out
237+
238+
224239
@patch("transformerlab_cli.commands.job.api.get", return_value=_mock_api_response(SAMPLE_JOBS))
225240
@patch("transformerlab_cli.commands.job.require_current_experiment", return_value="exp1")
226241
@patch("transformerlab_cli.commands.job.check_configs")

src/renderer/components/Experiment/Tasks/JobsList.tsx

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import MenuItem from '@mui/joy/MenuItem';
1111
import Dropdown from '@mui/joy/Dropdown';
1212
import Checkbox from '@mui/joy/Checkbox';
1313
import Tooltip from '@mui/joy/Tooltip';
14+
import Chip from '@mui/joy/Chip';
1415
import {
1516
Trash2Icon,
1617
LineChartIcon,
@@ -122,6 +123,61 @@ const JobsList: React.FC<JobsListProps> = ({
122123
return type.charAt(0).toUpperCase() + type.slice(1);
123124
};
124125

126+
const formatScoreValue = (value: number): string => {
127+
if (!Number.isFinite(value)) return String(value);
128+
if (Math.abs(value) >= 1000) return value.toFixed(1);
129+
if (Math.abs(value) >= 100) return value.toFixed(2);
130+
if (Math.abs(value) >= 1) return value.toFixed(3);
131+
return value.toFixed(4);
132+
};
133+
134+
const getScoreDisplay = (
135+
score: unknown,
136+
): { label: string; tooltip?: string } | null => {
137+
if (typeof score === 'number') {
138+
return { label: `Score: ${formatScoreValue(score)}` };
139+
}
140+
141+
if (typeof score === 'string') {
142+
const parsed = Number.parseFloat(score);
143+
if (Number.isFinite(parsed)) {
144+
return { label: `Score: ${formatScoreValue(parsed)}` };
145+
}
146+
if (score.trim()) {
147+
return { label: `Score: ${score.trim()}` };
148+
}
149+
return null;
150+
}
151+
152+
if (score && typeof score === 'object') {
153+
const numericEntries = Object.entries(score as Record<string, unknown>)
154+
.filter(([key]) => key.toLowerCase() !== 'discard')
155+
.map(([key, val]) => [key, Number(val)] as const)
156+
.filter(([, val]) => Number.isFinite(val));
157+
158+
if (numericEntries.length === 0) return null;
159+
160+
const preferredMetric =
161+
numericEntries.find(([key]) => key.toLowerCase() === 'score') ??
162+
numericEntries[0];
163+
const [firstMetric, firstValue] = preferredMetric;
164+
165+
const tooltip =
166+
numericEntries.length > 1
167+
? numericEntries
168+
.map(([metric, val]) => `${metric}: ${formatScoreValue(val)}`)
169+
.join(' | ')
170+
: undefined;
171+
172+
return {
173+
label: `${firstMetric}: ${formatScoreValue(firstValue)}`,
174+
tooltip,
175+
};
176+
}
177+
178+
return null;
179+
};
180+
125181
const formatJobConfig = (job: any) => {
126182
const jobData = job?.job_data || {};
127183
const interactiveType =
@@ -176,6 +232,7 @@ const JobsList: React.FC<JobsListProps> = ({
176232
const userInfo = jobData.user_info || {};
177233
const userDisplay = userInfo.name || userInfo.email || '';
178234
const providerDisplay = jobData.provider_name || job?.provider_name || '';
235+
const scoreDisplay = getScoreDisplay(jobData?.score);
179236

180237
if (job?.placeholder) {
181238
return (
@@ -215,6 +272,21 @@ const JobsList: React.FC<JobsListProps> = ({
215272
<b>Title:</b> {taskName}
216273
</Typography>
217274
)}
275+
{scoreDisplay && (
276+
<Tooltip
277+
title={scoreDisplay.tooltip || ''}
278+
disableHoverListener={!scoreDisplay.tooltip}
279+
>
280+
<Chip
281+
size="sm"
282+
color="success"
283+
variant="soft"
284+
sx={{ mt: 0.5, width: 'fit-content' }}
285+
>
286+
{scoreDisplay.label}
287+
</Chip>
288+
</Tooltip>
289+
)}
218290
</>
219291
);
220292
}
@@ -242,6 +314,21 @@ const JobsList: React.FC<JobsListProps> = ({
242314
<b>Provider:</b> {providerDisplay}
243315
</Typography>
244316
)}
317+
{scoreDisplay && (
318+
<Tooltip
319+
title={scoreDisplay.tooltip || ''}
320+
disableHoverListener={!scoreDisplay.tooltip}
321+
>
322+
<Chip
323+
size="sm"
324+
color="success"
325+
variant="soft"
326+
sx={{ mt: 0.5, width: 'fit-content' }}
327+
>
328+
{scoreDisplay.label}
329+
</Chip>
330+
</Tooltip>
331+
)}
245332
</>
246333
);
247334
}

0 commit comments

Comments
 (0)