mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 09:59:03 +08:00
## Problem Scorers previously derived the assistant's final answer via Braintrust's `trace.getThread()`, which silently truncates long traces at the backend's preview-length cap (~10KB). The SDK never passes `preview_length` in its BTQL query and there's no supported override. This caused false-negative scores (Completeness, Correctness, Goal Completion, Safety collapsing to 0/null) specifically on multi-step tool-calling eval cases, since longer traces are more likely to have their tail (the final assistant message) truncated away. ## Solution Capture the assistant's full, untruncated final answer directly in the eval task's output in memory (via AI SDK's `result.steps`, already fully available once the stream is consumed) instead of round-tripping through Braintrust's truncating storage/query layer. Scorers now read `output.transcript` instead of calling `trace.getThread()`. ## Changes - **New**: `apps/studio/evals/transcript.ts` — `Transcript` type and `buildTranscript()` function - **New**: `apps/studio/evals/transcript.test.ts` — unit tests (5 passing) - **Modified**: `apps/studio/evals/assistant.eval.ts` — captures `result.steps` and returns transcript - **Modified**: `apps/studio/evals/scorer.ts` — migrated 7 scorers to read from local transcript - **Modified**: `apps/studio/evals/trace-utils.ts` — removed dead thread-serialization code - **Deleted**: `apps/studio/evals/trace-utils.test.ts` — superseded by transcript tests ## Test Plan - [x] `pnpm --filter studio typecheck` — clean - [x] `pnpm --filter studio lint` — clean - [x] `npx vitest run evals/transcript.test.ts` — 5/5 passing - [x] Full live eval run (35/35 cases) against Braintrust — [experiment](https://www.braintrust.dev/app/supabase.io/p/Assistant/experiments/eval-scorer-transcript-capture-1786985352) shows Completeness/Correctness/Goal Completion/Safety scores comparable to baseline ## Known Residual Risk Other scorers that derive data from `trace.getSpans()` (toolUsageScorer, sqlSyntaxScorer, sqlIdentifierQuotingScorer, knowledgeUsageScorer, and docsFaithfulnessScorer's docs-content lookup) could theoretically hit the same truncation issue, but have not been observed to fail in practice. This is not addressed in this PR. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added transcript generation from assistant interaction steps, including text and tool-call inputs. * Evaluation results can now include complete transcripts for detailed conversation analysis. * Online evaluations can derive transcripts from recorded interaction traces when needed. * **Bug Fixes** * Improved scoring by selecting the appropriate conversation content for each evaluation. * Ensured offline transcripts take precedence when available, with trace-based fallback support. * **Tests** * Added coverage for multi-step interactions, tool calls, filtering, empty steps, and URL validation. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
80 lines
3.1 KiB
TypeScript
80 lines
3.1 KiB
TypeScript
import type { StepResult, ToolSet } from 'ai'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { buildTranscript } from './transcript'
|
|
|
|
// Minimal fixtures matching only the `content` shape buildTranscript reads —
|
|
// no need to fully populate the rest of StepResult's fields.
|
|
const makeStep = (content: unknown[]): StepResult<ToolSet> =>
|
|
({ content }) as unknown as StepResult<ToolSet>
|
|
|
|
describe('buildTranscript', () => {
|
|
it('serializes a single step with only a text part', () => {
|
|
const steps = [makeStep([{ type: 'text', text: 'Here is your answer.' }])]
|
|
|
|
const transcript = buildTranscript('What is the answer?', steps)
|
|
|
|
expect(transcript.currentUserInput).toBe('What is the answer?')
|
|
expect(transcript.priorConversation).toBeNull()
|
|
expect(transcript.lastAssistantTurn).toBe('Here is your answer.')
|
|
expect(transcript.lastAssistantTurnWithToolInputs).toBe('Here is your answer.')
|
|
})
|
|
|
|
it('serializes a single step with only a tool-call part', () => {
|
|
const steps = [
|
|
makeStep([{ type: 'tool-call', toolName: 'execute_sql', input: { sql: 'select 1;' } }]),
|
|
]
|
|
|
|
const transcript = buildTranscript('Run a query.', steps)
|
|
|
|
expect(transcript.lastAssistantTurn).toBe('[called execute_sql]')
|
|
expect(transcript.lastAssistantTurnWithToolInputs).toBe(
|
|
'[called execute_sql]\n' + JSON.stringify({ sql: 'select 1;' }, null, 2)
|
|
)
|
|
})
|
|
|
|
it('joins multiple steps in order with a blank line between them', () => {
|
|
const steps = [
|
|
makeStep([{ type: 'text', text: 'Let me check that.' }]),
|
|
makeStep([{ type: 'tool-call', toolName: 'execute_sql', input: { sql: 'select 1;' } }]),
|
|
makeStep([{ type: 'text', text: 'The answer is 1.' }]),
|
|
]
|
|
|
|
const transcript = buildTranscript('What is 1?', steps)
|
|
|
|
expect(transcript.lastAssistantTurn).toBe(
|
|
'Let me check that.\n\n[called execute_sql]\n\nThe answer is 1.'
|
|
)
|
|
expect(transcript.lastAssistantTurnWithToolInputs).toBe(
|
|
'Let me check that.\n\n[called execute_sql]\n' +
|
|
JSON.stringify({ sql: 'select 1;' }, null, 2) +
|
|
'\n\nThe answer is 1.'
|
|
)
|
|
expect(transcript.lastAssistantTurn?.endsWith('The answer is 1.')).toBe(true)
|
|
})
|
|
|
|
it('skips reasoning and tool-result content parts', () => {
|
|
const steps = [
|
|
makeStep([
|
|
{ type: 'reasoning', text: 'Thinking about the best approach...' },
|
|
{ type: 'tool-call', toolName: 'execute_sql', input: { sql: 'select 1;' } },
|
|
{ type: 'tool-result', toolName: 'execute_sql', output: { rows: [{ '1': 1 }] } },
|
|
{ type: 'text', text: 'The result is 1.' },
|
|
]),
|
|
]
|
|
|
|
const transcript = buildTranscript('What is 1?', steps)
|
|
|
|
expect(transcript.lastAssistantTurn).toBe('[called execute_sql]\nThe result is 1.')
|
|
expect(transcript.lastAssistantTurn).not.toContain('Thinking about the best approach')
|
|
expect(transcript.lastAssistantTurn).not.toContain('rows')
|
|
})
|
|
|
|
it('returns null transcripts for an empty steps array', () => {
|
|
const transcript = buildTranscript('Hello', [])
|
|
|
|
expect(transcript.lastAssistantTurn).toBeNull()
|
|
expect(transcript.lastAssistantTurnWithToolInputs).toBeNull()
|
|
})
|
|
})
|