Skip to content

Commit 3d25ab9

Browse files
committed
feat(dashboard): link raw transcript artifacts
1 parent 5001fa6 commit 3d25ab9

5 files changed

Lines changed: 152 additions & 0 deletions

File tree

apps/cli/src/commands/results/serve.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,16 @@ async function readArtifactCatalogEntryText(
975975
return { content };
976976
}
977977

978+
function readableLocalCatalogEntry(
979+
meta: SourcedResultFileMeta,
980+
entry: ArtifactCatalogEntry | undefined,
981+
): ArtifactCatalogEntry | undefined {
982+
if (!entry || entry.storage !== 'local') return entry;
983+
const baseDir = runWorkspaceDirFromManifestPath(meta.path);
984+
const resolved = resolveReadableRunArtifactFile(baseDir, entry.displayPath);
985+
return resolved.absolutePath ? entry : undefined;
986+
}
987+
978988
function artifactFileContentResponse(c: C, filePath: string, fileContent: string) {
979989
if (c.req.query('raw') === '1' || c.req.query('download') === '1') {
980990
c.header('Content-Type', inferRawContentType(filePath));
@@ -2143,6 +2153,12 @@ async function handleEvalTranscript(c: C, { searchDir, projectId }: DataContext)
21432153
runPath,
21442154
);
21452155
const answerEntry = findPointerArtifactCatalogEntry(catalog, answer, 'answer', runPath);
2156+
const transcriptRawEntry = readableLocalCatalogEntry(
2157+
meta,
2158+
record.transcript_raw_path
2159+
? findArtifactCatalogEntry(catalog, record.transcript_raw_path)
2160+
: undefined,
2161+
);
21462162

21472163
if (!transcriptEntry && !transcript.path) {
21482164
return c.json({
@@ -2194,6 +2210,7 @@ async function handleEvalTranscript(c: C, { searchDir, projectId }: DataContext)
21942210
return c.json({
21952211
status: 'ok',
21962212
transcript_path: transcriptEntry.displayPath,
2213+
...(transcriptRawEntry && { transcript_raw_path: transcriptRawEntry.displayPath }),
21972214
content,
21982215
language: inferLanguage(transcriptEntry.displayPath),
21992216
...(answerEntry && { answer_path: answerEntry.displayPath }),

apps/cli/test/commands/results/serve.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3115,8 +3115,10 @@ describe('serve app', () => {
31153115
const timestampDir = path.join(runsDir, runId);
31163116
const resultDir = 'demo-test-greeting--111111111111';
31173117
const transcriptArtifactPath = `${resultDir}/sample-1/transcript.json`;
3118+
const transcriptRawArtifactPath = `${resultDir}/sample-1/transcript-raw.jsonl`;
31183119
const answerArtifactPath = `${resultDir}/sample-1/outputs/answer.md`;
31193120
const transcriptPath = path.join(timestampDir, transcriptArtifactPath);
3121+
const transcriptRawPath = path.join(timestampDir, transcriptRawArtifactPath);
31203122
const answerPath = path.join(timestampDir, answerArtifactPath);
31213123
const transcriptJson = `${JSON.stringify(
31223124
{
@@ -3142,9 +3144,14 @@ describe('serve app', () => {
31423144
null,
31433145
2,
31443146
)}\n`;
3147+
const transcriptRawJsonl = `${JSON.stringify({
3148+
type: 'raw_event',
3149+
message: 'provider event',
3150+
})}\n`;
31453151

31463152
mkdirSync(path.dirname(transcriptPath), { recursive: true });
31473153
writeFileSync(transcriptPath, transcriptJson);
3154+
writeFileSync(transcriptRawPath, transcriptRawJsonl);
31483155
mkdirSync(path.dirname(answerPath), { recursive: true });
31493156
writeFileSync(answerPath, 'done');
31503157
mkdirSync(path.join(timestampDir, '.internal'), { recursive: true });
@@ -3155,6 +3162,7 @@ describe('serve app', () => {
31553162
experiment: 'canonical-transcript',
31563163
result_dir: resultDir,
31573164
transcript_path: transcriptArtifactPath,
3165+
transcript_raw_path: transcriptRawArtifactPath,
31583166
answer_path: answerArtifactPath,
31593167
}),
31603168
);
@@ -3168,13 +3176,15 @@ describe('serve app', () => {
31683176
const transcriptData = (await transcriptRes.json()) as {
31693177
status: string;
31703178
transcript_path: string;
3179+
transcript_raw_path: string;
31713180
content: string;
31723181
answer_path: string;
31733182
answer_content: string;
31743183
};
31753184
expect(transcriptData).toMatchObject({
31763185
status: 'ok',
31773186
transcript_path: transcriptArtifactPath,
3187+
transcript_raw_path: transcriptRawArtifactPath,
31783188
content: transcriptJson,
31793189
answer_path: answerArtifactPath,
31803190
answer_content: 'done',
@@ -3186,6 +3196,68 @@ describe('serve app', () => {
31863196

31873197
expect(rawRes.status).toBe(200);
31883198
expect(await rawRes.text()).toBe(transcriptJson);
3199+
3200+
const rawSidecarRes = await app.request(
3201+
`/api/runs/${encodeURIComponent(runId)}/evals/test-greeting/files/${transcriptRawArtifactPath}?result_dir=${encodeURIComponent(resultDir)}&raw=1`,
3202+
);
3203+
3204+
expect(rawSidecarRes.status).toBe(200);
3205+
expect(await rawSidecarRes.text()).toBe(transcriptRawJsonl);
3206+
});
3207+
3208+
it('omits missing raw transcript sidecars from the transcript response', async () => {
3209+
const runsDir = localResultsExperimentDir(tempDir, 'canonical-transcript-normalized-only');
3210+
const runId = '2026-03-25T10-45-00-000Z';
3211+
const timestampDir = path.join(runsDir, runId);
3212+
const resultDir = 'demo-test-greeting--222222222222';
3213+
const transcriptArtifactPath = `${resultDir}/sample-1/transcript.json`;
3214+
const transcriptRawArtifactPath = `${resultDir}/sample-1/transcript-raw.jsonl`;
3215+
const transcriptPath = path.join(timestampDir, transcriptArtifactPath);
3216+
const transcriptJson = `${JSON.stringify({
3217+
schema_version: 'agentv.normalized_transcript.v1',
3218+
target: 'codex',
3219+
turns: [
3220+
{
3221+
v: 1,
3222+
agent: 'codex',
3223+
type: 'assistant',
3224+
content: [{ type: 'text', text: 'done' }],
3225+
},
3226+
],
3227+
})}\n`;
3228+
3229+
mkdirSync(path.dirname(transcriptPath), { recursive: true });
3230+
writeFileSync(transcriptPath, transcriptJson);
3231+
mkdirSync(path.join(timestampDir, '.internal'), { recursive: true });
3232+
writeFileSync(
3233+
path.join(timestampDir, '.internal', 'index.jsonl'),
3234+
toJsonl({
3235+
...RESULT_A,
3236+
experiment: 'canonical-transcript-normalized-only',
3237+
result_dir: resultDir,
3238+
transcript_path: transcriptArtifactPath,
3239+
transcript_raw_path: transcriptRawArtifactPath,
3240+
}),
3241+
);
3242+
3243+
const app = createApp([], tempDir, tempDir, undefined, { studioDir });
3244+
const transcriptRes = await app.request(
3245+
`/api/runs/${encodeURIComponent(runId)}/evals/test-greeting/transcript?result_dir=${encodeURIComponent(resultDir)}`,
3246+
);
3247+
3248+
expect(transcriptRes.status).toBe(200);
3249+
const transcriptData = (await transcriptRes.json()) as {
3250+
status: string;
3251+
transcript_path: string;
3252+
transcript_raw_path?: string;
3253+
content: string;
3254+
};
3255+
expect(transcriptData).toMatchObject({
3256+
status: 'ok',
3257+
transcript_path: transcriptArtifactPath,
3258+
content: transcriptJson,
3259+
});
3260+
expect(transcriptData.transcript_raw_path).toBeUndefined();
31893261
});
31903262

31913263
it('loads pointer-shaped transcript metadata when it resolves to a local artifact path', async () => {

apps/dashboard/src/components/EvalDetail.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,7 @@ function selectedTrialResult(result: EvalResult, trial: EvalCaseTrial): EvalResu
133133
timing_path: trial.timing_path,
134134
metrics_path: trial.metrics_path,
135135
transcript_path: trial.transcript_path,
136+
transcript_raw_path: trial.transcript_raw_path,
136137
output_path: trial.answer_path,
137138
answer_path: trial.answer_path,
138139
};
@@ -902,6 +903,7 @@ function TrialTranscriptTab({
902903
const evalId = result.testId;
903904
const resultDir = result.result_dir;
904905
const transcriptPath = trial.transcript_path;
906+
const transcriptRawPath = trial.transcript_raw_path;
905907
const answerPath = trial.answer_path;
906908
const { data: transcriptContent, isLoading: isLoadingTranscript } =
907909
projectId && transcriptPath
@@ -987,16 +989,28 @@ function TrialTranscriptTab({
987989
resultDir,
988990
download: true,
989991
});
992+
const transcriptRawHref = transcriptRawPath
993+
? artifactFileContentUrl({
994+
projectId,
995+
runId,
996+
evalId,
997+
filePath: transcriptRawPath,
998+
resultDir,
999+
raw: true,
1000+
})
1001+
: undefined;
9901002

9911003
return (
9921004
<TranscriptTimeline
9931005
entries={parsedTranscript.entries}
9941006
finalAnswer={answerPath ? (answerContent?.content ?? result.output) : undefined}
9951007
answerPath={answerPath}
9961008
transcriptPath={transcriptPath}
1009+
transcriptRawPath={transcriptRawPath}
9971010
answerHref={answerHref}
9981011
transcriptHref={transcriptHref}
9991012
transcriptDownloadHref={transcriptDownloadHref}
1013+
transcriptRawHref={transcriptRawHref}
10001014
onOpenFile={onOpenFile}
10011015
/>
10021016
);
@@ -1023,6 +1037,7 @@ function TranscriptTab({
10231037
? useQuery(projectEvalTranscriptOptions(projectId, runId, evalId, resultDir))
10241038
: useEvalTranscript(runId, evalId, resultDir);
10251039
const transcriptPath = transcriptData?.transcript_path;
1040+
const transcriptRawPath = transcriptData?.transcript_raw_path;
10261041
const answerPath = transcriptData?.answer_path;
10271042
const transcriptContent = transcriptData?.status === 'ok' ? (transcriptData.content ?? '') : '';
10281043

@@ -1159,16 +1174,28 @@ function TranscriptTab({
11591174
download: true,
11601175
})
11611176
: undefined;
1177+
const transcriptRawHref = transcriptRawPath
1178+
? artifactFileContentUrl({
1179+
projectId,
1180+
runId,
1181+
evalId,
1182+
filePath: transcriptRawPath,
1183+
resultDir,
1184+
raw: true,
1185+
})
1186+
: undefined;
11621187

11631188
return (
11641189
<TranscriptTimeline
11651190
entries={parsedTranscript.entries}
11661191
finalAnswer={answerPath ? (transcriptData.answer_content ?? result.output) : undefined}
11671192
answerPath={answerPath}
11681193
transcriptPath={transcriptPath}
1194+
transcriptRawPath={transcriptRawPath}
11691195
answerHref={answerHref}
11701196
transcriptHref={transcriptHref}
11711197
transcriptDownloadHref={transcriptDownloadHref}
1198+
transcriptRawHref={transcriptRawHref}
11721199
onOpenFile={onOpenFile}
11731200
/>
11741201
);

apps/dashboard/src/components/TranscriptTimeline.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,11 @@ interface TranscriptTimelineProps {
5959
finalAnswer?: string;
6060
answerPath?: string;
6161
transcriptPath?: string;
62+
transcriptRawPath?: string;
6263
answerHref?: string;
6364
transcriptHref?: string;
6465
transcriptDownloadHref?: string;
66+
transcriptRawHref?: string;
6567
onOpenFile?: (path: string) => void;
6668
}
6769

@@ -833,9 +835,11 @@ export function TranscriptTimeline({
833835
finalAnswer,
834836
answerPath,
835837
transcriptPath,
838+
transcriptRawPath,
836839
answerHref,
837840
transcriptHref,
838841
transcriptDownloadHref,
842+
transcriptRawHref,
839843
onOpenFile,
840844
}: TranscriptTimelineProps) {
841845
const messages = useMemo(() => buildTranscriptViewModel(entries), [entries]);
@@ -943,6 +947,15 @@ export function TranscriptTimeline({
943947
<ActionLink href={transcriptDownloadHref} download>
944948
Download normalized JSON
945949
</ActionLink>
950+
<OpenFileButton path={transcriptRawPath} onOpenFile={onOpenFile}>
951+
Open transcript-raw.jsonl in Files
952+
</OpenFileButton>
953+
<ActionLink href={transcriptRawHref}>Open raw JSONL</ActionLink>
954+
{!transcriptRawPath && (
955+
<span className="rounded-md border border-gray-800 bg-gray-950 px-3 py-1.5 text-sm text-gray-500">
956+
Raw JSONL unavailable
957+
</span>
958+
)}
946959
</div>
947960
</div>
948961
</section>

apps/dashboard/src/components/transcript-timeline.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ describe('TranscriptTimeline', () => {
2222
finalAnswer={'{"answer":42,"source":"src/app.ts"}'}
2323
answerPath="final-json-answer__codex/outputs/answer.md"
2424
transcriptPath="final-json-answer__codex/transcript.json"
25+
transcriptRawPath="final-json-answer__codex/transcript-raw.jsonl"
2526
answerHref="/api/raw-answer"
2627
transcriptHref="/api/raw-transcript"
2728
transcriptDownloadHref="/api/download-transcript"
29+
transcriptRawHref="/api/raw-transcript-jsonl"
30+
onOpenFile={() => undefined}
2831
/>,
2932
);
3033
}
@@ -187,6 +190,9 @@ describe('TranscriptTimeline', () => {
187190
expect(html).toContain('success');
188191
expect(html).toContain('Open normalized JSON');
189192
expect(html).toContain('Download normalized JSON');
193+
expect(html).toContain('Open transcript-raw.jsonl in Files');
194+
expect(html).toContain('Open raw JSONL');
195+
expect(html).toContain('/api/raw-transcript-jsonl');
190196
expect(html).toContain('{&quot;answer&quot;:42,&quot;source&quot;:&quot;src/app.ts&quot;}');
191197
});
192198

@@ -207,4 +213,21 @@ describe('TranscriptTimeline', () => {
207213
'very-long-project-name/very-long-test-case-name/sample-1/transcript.json',
208214
);
209215
});
216+
217+
it('shows a clear unavailable state when only normalized transcript JSON exists', () => {
218+
const parsed = parseTranscriptJsonl(structuredTranscriptJsonl);
219+
const html = renderToStaticMarkup(
220+
<TranscriptTimeline
221+
entries={parsed.entries}
222+
transcriptPath="final-json-answer__codex/sample-1/transcript.json"
223+
transcriptHref="/api/raw-transcript"
224+
transcriptDownloadHref="/api/download-transcript"
225+
/>,
226+
);
227+
228+
expect(html).toContain('Open normalized JSON');
229+
expect(html).toContain('Raw JSONL unavailable');
230+
expect(html).not.toContain('Open transcript-raw.jsonl in Files');
231+
expect(html).not.toContain('Open raw JSONL');
232+
});
210233
});

0 commit comments

Comments
 (0)