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 -->
181 lines
4.5 KiB
TypeScript
181 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { getThreadPartsFromThread } from './trace-utils'
|
|
|
|
// Sanitized mock of the thread shape returned by trace.getThread().
|
|
const MOCK_THREAD = [
|
|
{
|
|
role: 'system',
|
|
content: 'System instructions omitted for fixture.',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
content: "The user's current project is Acme Analytics.",
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'What did we decide earlier?',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
content: [
|
|
{
|
|
type: 'text',
|
|
text: 'We decided to add an orders table with RLS policies before generating sample data.',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: 'user',
|
|
content: 'Can you create that orders table now?',
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
id: null,
|
|
content: [
|
|
{
|
|
type: 'tool_call',
|
|
tool_name: 'rename_chat',
|
|
tool_call_id: 'call_dummy_rename',
|
|
arguments: {
|
|
type: 'valid',
|
|
value: {
|
|
newName: 'Create Orders Table',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: 'tool',
|
|
content: [
|
|
{
|
|
type: 'tool_result',
|
|
tool_name: 'rename_chat',
|
|
tool_call_id: 'call_dummy_rename',
|
|
output: {
|
|
status: 'Chat request sent to client',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
id: null,
|
|
content: [
|
|
{
|
|
type: 'tool_call',
|
|
tool_name: 'load_knowledge',
|
|
tool_call_id: 'call_dummy_knowledge',
|
|
arguments: {
|
|
type: 'valid',
|
|
value: {
|
|
name: 'database',
|
|
},
|
|
},
|
|
},
|
|
{
|
|
type: 'tool_call',
|
|
tool_name: 'execute_sql',
|
|
tool_call_id: 'call_dummy_sql',
|
|
arguments: {
|
|
type: 'valid',
|
|
value: {
|
|
sql: 'create table public.orders (id bigint generated by default as identity primary key);',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: 'tool',
|
|
content: [
|
|
{
|
|
type: 'tool_result',
|
|
tool_name: 'load_knowledge',
|
|
tool_call_id: 'call_dummy_knowledge',
|
|
output: 'Knowledge fixture omitted.',
|
|
},
|
|
{
|
|
type: 'tool_result',
|
|
tool_name: 'execute_sql',
|
|
tool_call_id: 'call_dummy_sql',
|
|
output: {
|
|
type: 'text',
|
|
text: 'SQL executed successfully.',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
id: null,
|
|
content:
|
|
'I created the public.orders table. You should add RLS policies before exposing it to users.',
|
|
},
|
|
]
|
|
|
|
describe('getThreadPartsFromThread', () => {
|
|
it('parses a sanitized Braintrust trace.getThread payload, computing both tool-input variants', () => {
|
|
expect(getThreadPartsFromThread(MOCK_THREAD)).toEqual({
|
|
currentUserInput: 'Can you create that orders table now?',
|
|
priorConversation:
|
|
'[user]\nWhat did we decide earlier?\n\n[assistant]\nWe decided to add an orders table with RLS policies before generating sample data.',
|
|
lastAssistantTurn:
|
|
'[assistant]\n[called rename_chat]\n\n[assistant]\n[called load_knowledge]\n[called execute_sql]\n\n[assistant]\nI created the public.orders table. You should add RLS policies before exposing it to users.',
|
|
lastAssistantTurnWithToolInputs: `\
|
|
[assistant]
|
|
[called rename_chat]
|
|
{
|
|
"newName": "Create Orders Table"
|
|
}
|
|
|
|
[assistant]
|
|
[called load_knowledge]
|
|
{
|
|
"name": "database"
|
|
}
|
|
[called execute_sql]
|
|
{
|
|
"sql": "create table public.orders (id bigint generated by default as identity primary key);"
|
|
}
|
|
|
|
[assistant]
|
|
I created the public.orders table. You should add RLS policies before exposing it to users.`,
|
|
})
|
|
})
|
|
|
|
it('filters out project-context messages so they never leak into prior conversation', () => {
|
|
const threadWithExtraProjectContext = [
|
|
{
|
|
role: 'assistant',
|
|
content: "The user's current project is Old Project.",
|
|
},
|
|
...MOCK_THREAD,
|
|
]
|
|
|
|
expect(getThreadPartsFromThread(threadWithExtraProjectContext)).toEqual(
|
|
getThreadPartsFromThread(MOCK_THREAD)
|
|
)
|
|
expect(getThreadPartsFromThread(threadWithExtraProjectContext).priorConversation).not.toContain(
|
|
'Old Project'
|
|
)
|
|
})
|
|
|
|
it('treats all messages as prior conversation when there is no user message', () => {
|
|
expect(
|
|
getThreadPartsFromThread([
|
|
{
|
|
role: 'assistant',
|
|
content: 'I can help with your Supabase project.',
|
|
},
|
|
])
|
|
).toEqual({
|
|
currentUserInput: '',
|
|
priorConversation: '[assistant]\nI can help with your Supabase project.',
|
|
lastAssistantTurn: null,
|
|
lastAssistantTurnWithToolInputs: null,
|
|
})
|
|
})
|
|
})
|