Skip to content

Commit 5907bc4

Browse files
SushantGautamclaude
andcommitted
fix(results): readable trace labels, check rows collapse only with content
- Span labels: conv-tool-N shows as tool-N and conv-response-N as response. Display only; the span ids stay the link anchors. - Timeline chips show the label; evidence links show labels too. - Check rows are collapsible only when they have expected, observed, or evidence. - trace_evidence is hidden from the generic rows of the result panel. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 34dd540 commit 5907bc4

3 files changed

Lines changed: 31 additions & 86 deletions

File tree

‎infra/ui.py‎

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3405,6 +3405,7 @@ def post(self, request, run_id):
34053405
"severity", "rationale", "evidence", "judge_rationale", "judgment", "scenario_name",
34063406
"scenario_description", "expected_behavior", "file_uri", "_rep_index", "_language", "error",
34073407
"trace_ids",
3408+
"trace_evidence",
34083409
"agentic_evaluation",
34093410
*(f"{r}_{d}_tokens" for r in _REP_TOKEN_ROLES for d in ("input", "output")),
34103411
}
@@ -3555,6 +3556,20 @@ def _as_text_list(value) -> list[str]:
35553556
}
35563557

35573558

3559+
def _span_label(sid: str) -> str:
3560+
"""Short display label for a synthetic conversation span id.
3561+
3562+
Display only: the id itself stays the link anchor, so these links keep working.
3563+
"""
3564+
if sid.startswith("conv-tool-result-"):
3565+
return f"tool-result-{sid.rsplit('-', 1)[-1]}"
3566+
if sid.startswith("conv-tool-"):
3567+
return f"tool-{sid.rsplit('-', 1)[-1]}"
3568+
if sid.startswith("conv-response-"):
3569+
return "response"
3570+
return sid
3571+
3572+
35583573
def _agentic_span_view(span: dict) -> dict | None:
35593574
"""One evidence span as a timeline row (chronological ordering key included)."""
35603575
if not isinstance(span, dict) or not span.get("span_id"):
@@ -3572,6 +3587,7 @@ def _agentic_span_view(span: dict) -> dict | None:
35723587
start = span.get("start_time")
35733588
return {
35743589
"span_id": str(span["span_id"]),
3590+
"label": _span_label(str(span["span_id"])),
35753591
"kind": span.get("kind") or "",
35763592
"icon": icon,
35773593
"tone": tone,
@@ -4101,6 +4117,13 @@ def _token_count(value: object) -> int:
41014117
)
41024118
grade = _judge_grade(rep.get("judgment"))
41034119
agentic = rep.get("agentic_evaluation") if isinstance(rep.get("agentic_evaluation"), dict) else None
4120+
if agentic and isinstance(agentic.get("checks"), list):
4121+
# Each check's evidence ids come with display labels; the ids stay the link anchors.
4122+
agentic = {**agentic, "checks": [
4123+
{**c, "evidence_spans": [{"id": str(sid), "label": _span_label(str(sid))} for sid in (c.get("evidence_span_ids") or [])]}
4124+
if isinstance(c, dict) else c
4125+
for c in agentic["checks"]
4126+
]}
41044127
timeline = _agentic_timeline_view(rep)
41054128
# Finding cards and judge dimensions cite span ids; resolve each to its
41064129
# unique DOM anchor so a jump link lands on the correct row even when span
@@ -4109,16 +4132,6 @@ def _token_count(value: object) -> int:
41094132
judge = _agentic_judge_view(agentic or {})
41104133
anchor_for = timeline["anchor_for"] if timeline else {}
41114134

4112-
def _span_label(sid: str) -> str:
4113-
"""Human-friendly label for a synthetic span id."""
4114-
if sid.startswith("conv-tool-"):
4115-
# Look up the tool name from the timeline rows
4116-
row = next((r for r in (timeline or {}).get("rows", []) if r.get("span_id") == sid), None)
4117-
return (row or {}).get("tool_name") or f"Tool {sid.rsplit('-', 1)[-1]}"
4118-
if sid.startswith("conv-response-"):
4119-
return "Response"
4120-
return sid
4121-
41224135
for card in findings:
41234136
card["spans"] = [
41244137
{"span_id": sid, "label": _span_label(sid), "anchor": anchor_for.get(sid, sid)}

‎templates/partials/agentic_timeline.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ <h3 class="text-sm font-semibold text-violet-300">Trajectory timeline</h3>
2626
</div>
2727
<details class="agentic-tl-card min-w-0 flex-1 rounded-lg border border-gray-800/70 bg-surface-overlay/40 group-open:border-violet-800/50">
2828
<summary class="cursor-pointer select-none flex flex-wrap items-center gap-x-2 gap-y-1 px-3 py-2">
29-
<span class="shrink-0 px-1.5 py-0.5 rounded bg-surface-overlay/60 border border-gray-800 text-[10px] uppercase tracking-wide {{ s.tone }}" title="{{ s.kind }}">{{ s.kind|default:"STEP" }}</span>
29+
<span class="shrink-0 px-1.5 py-0.5 rounded bg-surface-overlay/60 border border-gray-800 text-[10px] uppercase tracking-wide {{ s.tone }}" title="{{ s.kind }}">{{ s.label|default:s.kind|default:"STEP" }}</span>
3030
<span class="min-w-0 truncate text-sm text-gray-200" title="{{ s.name }}">{{ s.name }}</span>
3131
{% if s.tool_name %}<span class="min-w-0 truncate text-xs text-gray-500" title="{{ s.tool_name }}">· {{ s.tool_name }}</span>{% endif %}
3232
{% if s.kb_sources %}

‎templates/partials/result_rep_panel.html‎

Lines changed: 7 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ <h4 class="text-xs font-semibold text-gray-400 uppercase tracking-wide">Determin
5656
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-2.5">
5757
{% for f in rep.agentic_findings %}
5858
<details class="agentic-finding-card min-w-0 rounded-lg border px-3 py-2.5 {% if f.status == 'FAIL' or f.status == 'ERROR' %}border-red-900/50 bg-red-950/20{% elif f.status == 'INCONCLUSIVE' %}border-amber-900/50 bg-amber-950/10{% else %}border-gray-800 bg-surface-overlay/30{% endif %}" data-severity="{{ f.severity }}" data-status="{{ f.status }}">
59-
<summary class="cursor-pointer select-none list-none">
59+
<summary class="cursor-pointer select-none list-none [&::-webkit-details-marker]:hidden">
6060
<div class="flex items-center gap-2 min-w-0">
6161
<span class="shrink-0 px-1.5 py-0.5 rounded text-[10px] font-semibold {% if f.status == 'PASS' %}bg-emerald-900/40 text-emerald-300{% elif f.status == 'INCONCLUSIVE' %}bg-amber-900/40 text-amber-300{% else %}bg-red-900/40 text-red-300{% endif %}">{{ f.status }}</span>
6262
{% if f.severity %}<span class="shrink-0 px-1.5 py-0.5 rounded text-[10px] uppercase tracking-wide {% if f.severity == 'critical' %}bg-red-950 text-red-300{% elif f.severity == 'high' %}bg-orange-950 text-orange-300{% elif f.severity == 'medium' %}bg-amber-950 text-amber-300{% else %}bg-gray-800 text-gray-400{% endif %}">{{ f.severity }}</span>{% endif %}
@@ -91,13 +91,17 @@ <h4 class="text-xs font-semibold text-gray-400 uppercase tracking-wide">Determin
9191
{% endif %}
9292
<div class="space-y-1.5">
9393
{% for check in rep.agentic.checks %}
94+
{% if check.expected or check.observed or check.evidence_span_ids %}
9495
<details class="rounded bg-surface-overlay/50 px-3 py-2">
95-
<summary class="cursor-pointer text-xs text-gray-300"><span class="mr-2 {% if check.status == 'PASS' %}text-emerald-400{% elif check.status == 'INCONCLUSIVE' %}text-amber-400{% else %}text-red-400{% endif %}">{{ check.status }}</span>{{ check.id }} — {{ check.summary }}</summary>
96+
<summary class="cursor-pointer text-xs text-gray-300"> <span class="mr-2 {% if check.status == 'PASS' %}text-emerald-400{% elif check.status == 'INCONCLUSIVE' %}text-amber-400{% else %}text-red-400{% endif %}">{{ check.status }}</span>{{ check.id }} — {{ check.summary }}</summary>
9697
{% if check.expected or check.observed %}<div class="mt-2 text-[11px] text-gray-500">Expected: {{ check.expected|default:'—' }} · Observed: {{ check.observed|default:'—' }}</div>{% endif %}
9798
{% if check.evidence_span_ids %}<div class="mt-1 text-[11px] text-gray-500">Evidence spans:
98-
{% for span_id in check.evidence_span_ids %}<a class="text-violet-300 hover:text-violet-200 underline" href="#trace-span-{{ rep.index }}-{{ span_id }}">{{ span_id }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}
99+
{% for sp in check.evidence_spans %}<a class="text-violet-300 hover:text-violet-200 underline" href="#trace-span-{{ rep.index }}-{{ sp.id }}">{{ sp.label }}</a>{% if not forloop.last %}, {% endif %}{% endfor %}
99100
</div>{% endif %}
100101
</details>
102+
{% else %}
103+
<div class="rounded bg-surface-overlay/50 px-3 py-2 text-xs text-gray-300"> <span class="mr-2 {% if check.status == 'PASS' %}text-emerald-400{% elif check.status == 'INCONCLUSIVE' %}text-amber-400{% else %}text-red-400{% endif %}">{{ check.status }}</span>{{ check.id }} — {{ check.summary }}</div>
104+
{% endif %}
101105
{% endfor %}
102106

103107
{% if rep.agentic_judge %}
@@ -211,78 +215,6 @@ <h3 class="text-sm font-semibold text-gray-300 mb-2">Judge rationale</h3>
211215
</div>
212216
{% endif %}
213217

214-
{% if rep.trace %}
215-
<details class="bg-surface-raised rounded-xl border border-violet-900/40 p-4 group">
216-
<summary class="flex cursor-pointer select-none flex-wrap items-center gap-3">
217-
<h3 class="text-sm font-semibold text-violet-300">Target trace</h3>
218-
<span class="text-xs text-gray-500">{{ rep.trace.spans|length }} span{{ rep.trace.spans|length|pluralize }} exported under {{ rep.trace.trace_ids|length }} trace{{ rep.trace.trace_ids|length|pluralize }}</span>
219-
<span class="text-[10px] text-gray-600 group-open:hidden">click to expand</span>
220-
</summary>
221-
<div class="mt-3">
222-
{% if rep.trace.spans %}
223-
{% if rep.trace.trace_ids %}
224-
<div class="flex flex-wrap gap-1.5 mb-3">
225-
{% for tid in rep.trace.trace_ids %}
226-
<code class="px-1.5 py-0.5 rounded bg-surface-overlay/70 border border-gray-800 text-[11px] text-gray-300" title="{{ tid }}">{{ tid|slice:":12" }}…</code>
227-
{% endfor %}
228-
</div>
229-
{% endif %}
230-
<ul class="space-y-1.5">
231-
{% for s in rep.trace.spans %}
232-
<li class="scroll-mt-4">
233-
<details class="rounded border border-gray-800/70 bg-surface-overlay/40">
234-
<summary id="trace-span-{{ rep.index }}-{{ s.span_id }}" class="flex cursor-pointer select-none items-center gap-2 px-2 py-1.5 text-sm">
235-
<span class="shrink-0 px-1.5 py-0.5 rounded bg-surface-overlay/60 border border-gray-800 text-[10px] uppercase tracking-wide {% if s.kind == 'LLM' %}text-sky-300{% elif s.kind == 'RETRIEVER' %}text-emerald-300{% elif s.kind == 'TOOL' %}text-amber-300{% elif s.kind == 'AGENT' %}text-fuchsia-300{% elif s.kind == 'GUARDRAIL' %}text-fuchsia-300{% elif s.kind == 'EVALUATOR' %}text-fuchsia-300{% elif s.kind == 'SPAN_KIND_SERVER' %}text-violet-300{% elif s.kind == 'SPAN_KIND_CLIENT' %}text-cyan-300{% elif s.kind == 'SPAN_KIND_INTERNAL' %}text-gray-400{% elif s.kind == 'CHAIN' %}text-gray-400{% else %}text-gray-300{% endif %}" title="{{ s.kind }}">{{ s.kind }}</span>
236-
<span class="text-gray-200 truncate" title="{{ s.name }}">{{ s.name }}</span>
237-
{% if s.attributes.tool_name %}<span class="text-gray-500 text-xs truncate" title="{{ s.attributes.tool_name }}">· {{ s.attributes.tool_name }}</span>{% endif %}
238-
<span class="shrink-0 ml-auto flex items-center gap-2 text-[11px] text-gray-500 tabular-nums">
239-
{% if s.offset_ms is not None %}<span title="ms after the first span in this trace">+{{ s.offset_ms }} ms</span>{% endif %}
240-
{% if s.dur_ms > 0 %}<span>{{ s.dur_ms }} ms</span>{% endif %}
241-
</span>
242-
</summary>
243-
{% if s.content_blocks or s.attributes %}
244-
<div class="space-y-2 border-t border-gray-800/70 px-3 py-2">
245-
{% for label, value in s.content_blocks %}
246-
<div>
247-
<p class="mb-1 text-[10px] uppercase tracking-wide text-gray-500">{{ label }}</p>
248-
<pre class="max-h-56 overflow-auto whitespace-pre-wrap break-words rounded bg-surface p-2 text-[11px] text-gray-300">{{ value }}</pre>
249-
</div>
250-
{% endfor %}
251-
<details>
252-
<summary class="cursor-pointer select-none text-[10px] uppercase tracking-wide text-gray-500 hover:text-gray-300">All attributes ({{ s.attributes|length }})</summary>
253-
<pre class="mt-1 max-h-64 overflow-auto rounded bg-surface p-2 text-[11px] text-gray-400">{{ s.attributes|jsonify }}</pre>
254-
</details>
255-
</div>
256-
{% endif %}
257-
</details>
258-
</li>
259-
{% endfor %}
260-
</ul>
261-
{% if rep.trace.hidden_spans %}
262-
<p class="mt-2 text-xs text-gray-500">+ {{ rep.trace.hidden_spans }} more span{{ rep.trace.hidden_spans|pluralize }} (shorter / beyond preview).</p>
263-
{% endif %}
264-
<p class="mt-2 text-[11px] text-gray-600">Click a span for its tool arguments / results and raw attributes. Evidence links from the checks above jump to their spans.</p>
265-
<script>
266-
// Evidence-link clicks expand the target span's detail (idempotent: the
267-
// panel is included once per repetition on the same page).
268-
(function() {
269-
if (window.__traceSpanExpandBound) return;
270-
window.__traceSpanExpandBound = true;
271-
document.addEventListener('click', function(e) {
272-
var a = e.target.closest ? e.target.closest('a[href^="#trace-span-"]') : null;
273-
if (!a) return;
274-
var el = document.getElementById(a.getAttribute('href').slice(1));
275-
var d = el && el.closest ? el.closest('details') : null;
276-
if (d) d.open = true;
277-
});
278-
})();
279-
</script>
280-
{% else %}
281-
<p class="text-xs text-gray-500">The target exported no spans under this rep's trace{{ rep.trace.trace_ids|length|default:0|pluralize }} (tracing was on; the target's OTel may not be wired for this deployment).</p>
282-
{% endif %}
283-
</div>
284-
</details>
285-
{% endif %}
286218

287219
{% include "partials/agentic_timeline.html" %}
288220

0 commit comments

Comments
 (0)