Add debug transcript components
This commit is contained in:
637
web/components/debug-transcript/message-utils.ts
Normal file
637
web/components/debug-transcript/message-utils.ts
Normal file
@@ -0,0 +1,637 @@
|
||||
import type {
|
||||
DebugTranscriptRow,
|
||||
DebugTranscriptTextRole,
|
||||
DebugTranscriptTextRow,
|
||||
DebugTranscriptToolRow,
|
||||
DebugTranscriptToolStatus,
|
||||
} from './types';
|
||||
|
||||
let rowCounter = 0;
|
||||
|
||||
const createRowId = (prefix: string) => `${prefix}_${Date.now()}_${++rowCounter}`;
|
||||
|
||||
const isTextRow = (
|
||||
row: DebugTranscriptRow | undefined | null
|
||||
): row is DebugTranscriptTextRow => row?.kind === 'text';
|
||||
|
||||
const isToolRow = (
|
||||
row: DebugTranscriptRow | undefined | null
|
||||
): row is DebugTranscriptToolRow => row?.kind === 'tool';
|
||||
|
||||
const findRowIndexById = (rows: DebugTranscriptRow[], rowId?: string | null) =>
|
||||
rowId ? rows.findIndex((row) => row.id === rowId) : -1;
|
||||
|
||||
const findAssistantRowIndexByResponseId = (rows: DebugTranscriptRow[], responseId?: string) => {
|
||||
if (!responseId) return -1;
|
||||
return rows.findIndex(
|
||||
(row) => isTextRow(row) && row.role === 'assistant' && row.responseId === responseId
|
||||
);
|
||||
};
|
||||
|
||||
const findLastAssistantCandidateIndex = (
|
||||
rows: DebugTranscriptRow[],
|
||||
responseId?: string
|
||||
) => {
|
||||
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
||||
const row = rows[i];
|
||||
if (isTextRow(row) && row.role === 'assistant') {
|
||||
if (responseId && row.responseId && row.responseId !== responseId) {
|
||||
break;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
if (isTextRow(row) && row.role === 'user') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
};
|
||||
|
||||
const createTextRow = ({
|
||||
role,
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
isStreaming,
|
||||
}: {
|
||||
role: DebugTranscriptTextRole;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
ttfbMs?: number;
|
||||
isStreaming?: boolean;
|
||||
}): DebugTranscriptTextRow => ({
|
||||
kind: 'text',
|
||||
id: createRowId(role),
|
||||
role,
|
||||
text,
|
||||
...(turnId ? { turnId } : {}),
|
||||
...(utteranceId ? { utteranceId } : {}),
|
||||
...(responseId ? { responseId } : {}),
|
||||
...(typeof ttfbMs === 'number' ? { ttfbMs } : {}),
|
||||
...(typeof isStreaming === 'boolean' ? { isStreaming } : {}),
|
||||
});
|
||||
|
||||
const createToolRow = ({
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
source,
|
||||
status,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
rawCall,
|
||||
rawResult,
|
||||
}: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
toolDisplayName: string;
|
||||
executor: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
source?: string;
|
||||
status: DebugTranscriptToolStatus;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
rawCall?: unknown;
|
||||
rawResult?: unknown;
|
||||
}): DebugTranscriptToolRow => ({
|
||||
kind: 'tool',
|
||||
id: toolCallId || createRowId('tool'),
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
...(turnId ? { turnId } : {}),
|
||||
...(utteranceId ? { utteranceId } : {}),
|
||||
...(responseId ? { responseId } : {}),
|
||||
...(source ? { source } : {}),
|
||||
status,
|
||||
...(args !== undefined ? { args } : {}),
|
||||
...(result !== undefined ? { result } : {}),
|
||||
...(error !== undefined ? { error } : {}),
|
||||
...(rawCall !== undefined ? { rawCall } : {}),
|
||||
...(rawResult !== undefined ? { rawResult } : {}),
|
||||
});
|
||||
|
||||
export const resetTranscriptRows = (): DebugTranscriptRow[] => [];
|
||||
|
||||
export const appendTextRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
role,
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
isStreaming,
|
||||
}: {
|
||||
role: DebugTranscriptTextRole;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
ttfbMs?: number;
|
||||
isStreaming?: boolean;
|
||||
}
|
||||
): DebugTranscriptRow[] => [
|
||||
...rows,
|
||||
createTextRow({ role, text, turnId, utteranceId, responseId, ttfbMs, isStreaming }),
|
||||
];
|
||||
|
||||
export const appendNoticeRow = (rows: DebugTranscriptRow[], text: string) =>
|
||||
appendTextRow(rows, { role: 'notice', text, isStreaming: false });
|
||||
|
||||
export const updateUserDraftRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
draftRowId,
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
}: {
|
||||
draftRowId?: string | null;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
}
|
||||
): { rows: DebugTranscriptRow[]; draftRowId: string } => {
|
||||
const rowIndex = findRowIndexById(rows, draftRowId);
|
||||
if (rowIndex !== -1) {
|
||||
const row = rows[rowIndex];
|
||||
if (isTextRow(row) && row.role === 'user') {
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
text,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
isStreaming: true,
|
||||
};
|
||||
return { rows: nextRows, draftRowId: row.id };
|
||||
}
|
||||
}
|
||||
|
||||
const nextRow = createTextRow({
|
||||
role: 'user',
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
isStreaming: true,
|
||||
});
|
||||
return {
|
||||
rows: [...rows, nextRow],
|
||||
draftRowId: nextRow.id,
|
||||
};
|
||||
};
|
||||
|
||||
export const finalizeUserDraftRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
draftRowId,
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
}: {
|
||||
draftRowId?: string | null;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
}
|
||||
): { rows: DebugTranscriptRow[]; draftRowId: null } => {
|
||||
const rowIndex = findRowIndexById(rows, draftRowId);
|
||||
if (rowIndex !== -1) {
|
||||
const row = rows[rowIndex];
|
||||
if (isTextRow(row) && row.role === 'user') {
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
text: text || row.text,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
isStreaming: false,
|
||||
};
|
||||
return { rows: nextRows, draftRowId: null };
|
||||
}
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return { rows, draftRowId: null };
|
||||
}
|
||||
|
||||
return {
|
||||
rows: appendTextRow(rows, {
|
||||
role: 'user',
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
isStreaming: false,
|
||||
}),
|
||||
draftRowId: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const updateAssistantDeltaRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
draftRowId,
|
||||
delta,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
}: {
|
||||
draftRowId?: string | null;
|
||||
delta: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
ttfbMs?: number;
|
||||
}
|
||||
): { rows: DebugTranscriptRow[]; draftRowId: string } => {
|
||||
let rowIndex = findRowIndexById(rows, draftRowId);
|
||||
if (
|
||||
rowIndex !== -1 &&
|
||||
(!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant')
|
||||
) {
|
||||
rowIndex = -1;
|
||||
}
|
||||
|
||||
if (rowIndex === -1) {
|
||||
rowIndex = findAssistantRowIndexByResponseId(rows, responseId);
|
||||
}
|
||||
|
||||
if (rowIndex === -1) {
|
||||
rowIndex = findLastAssistantCandidateIndex(rows, responseId);
|
||||
}
|
||||
|
||||
if (rowIndex === -1) {
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (
|
||||
isTextRow(lastRow) &&
|
||||
lastRow.role === 'assistant' &&
|
||||
lastRow.text === delta &&
|
||||
lastRow.responseId === responseId
|
||||
) {
|
||||
return {
|
||||
rows,
|
||||
draftRowId: lastRow.id,
|
||||
};
|
||||
}
|
||||
|
||||
const nextRow = createTextRow({
|
||||
role: 'assistant',
|
||||
text: delta,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
isStreaming: true,
|
||||
});
|
||||
return {
|
||||
rows: [...rows, nextRow],
|
||||
draftRowId: nextRow.id,
|
||||
};
|
||||
}
|
||||
|
||||
const row = rows[rowIndex];
|
||||
if (!isTextRow(row) || row.role !== 'assistant') {
|
||||
return {
|
||||
rows,
|
||||
draftRowId: draftRowId || createRowId('assistant'),
|
||||
};
|
||||
}
|
||||
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
text: row.text + delta,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
responseId: row.responseId || responseId,
|
||||
ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs,
|
||||
isStreaming: true,
|
||||
};
|
||||
|
||||
return {
|
||||
rows: nextRows,
|
||||
draftRowId: row.id,
|
||||
};
|
||||
};
|
||||
|
||||
export const finalizeAssistantTextRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
draftRowId,
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
}: {
|
||||
draftRowId?: string | null;
|
||||
text: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
ttfbMs?: number;
|
||||
}
|
||||
): { rows: DebugTranscriptRow[]; draftRowId: null } => {
|
||||
let rowIndex = findRowIndexById(rows, draftRowId);
|
||||
if (
|
||||
rowIndex !== -1 &&
|
||||
(!isTextRow(rows[rowIndex]) || rows[rowIndex].role !== 'assistant')
|
||||
) {
|
||||
rowIndex = -1;
|
||||
}
|
||||
|
||||
if (rowIndex === -1) {
|
||||
rowIndex = findAssistantRowIndexByResponseId(rows, responseId);
|
||||
}
|
||||
|
||||
if (rowIndex === -1) {
|
||||
rowIndex = findLastAssistantCandidateIndex(rows, responseId);
|
||||
}
|
||||
|
||||
if (rowIndex !== -1) {
|
||||
const row = rows[rowIndex];
|
||||
if (isTextRow(row) && row.role === 'assistant') {
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
text: text || row.text,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
responseId: row.responseId || responseId,
|
||||
ttfbMs: typeof row.ttfbMs === 'number' ? row.ttfbMs : ttfbMs,
|
||||
isStreaming: false,
|
||||
};
|
||||
return {
|
||||
rows: nextRows,
|
||||
draftRowId: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (!text) {
|
||||
return { rows, draftRowId: null };
|
||||
}
|
||||
|
||||
const lastRow = rows[rows.length - 1];
|
||||
if (
|
||||
isTextRow(lastRow) &&
|
||||
lastRow.role === 'assistant' &&
|
||||
(!responseId || !lastRow.responseId || lastRow.responseId === responseId)
|
||||
) {
|
||||
if (lastRow.text === text) {
|
||||
return { rows, draftRowId: null };
|
||||
}
|
||||
|
||||
if (text.startsWith(lastRow.text) || lastRow.text.startsWith(text)) {
|
||||
const nextRows = [...rows];
|
||||
nextRows[nextRows.length - 1] = {
|
||||
...lastRow,
|
||||
text,
|
||||
turnId: lastRow.turnId || turnId,
|
||||
utteranceId: lastRow.utteranceId || utteranceId,
|
||||
responseId: lastRow.responseId || responseId,
|
||||
ttfbMs: typeof lastRow.ttfbMs === 'number' ? lastRow.ttfbMs : ttfbMs,
|
||||
isStreaming: false,
|
||||
};
|
||||
return {
|
||||
rows: nextRows,
|
||||
draftRowId: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
rows: appendTextRow(rows, {
|
||||
role: 'assistant',
|
||||
text,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
ttfbMs,
|
||||
isStreaming: false,
|
||||
}),
|
||||
draftRowId: null,
|
||||
};
|
||||
};
|
||||
|
||||
export const attachAssistantTtfb = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
responseId,
|
||||
ttfbMs,
|
||||
}: {
|
||||
responseId?: string;
|
||||
ttfbMs: number;
|
||||
}
|
||||
) => {
|
||||
const rowIndex =
|
||||
findAssistantRowIndexByResponseId(rows, responseId) !== -1
|
||||
? findAssistantRowIndexByResponseId(rows, responseId)
|
||||
: findLastAssistantCandidateIndex(rows, responseId);
|
||||
|
||||
if (rowIndex === -1) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const row = rows[rowIndex];
|
||||
if (!isTextRow(row) || row.role !== 'assistant') {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
ttfbMs,
|
||||
};
|
||||
return nextRows;
|
||||
};
|
||||
|
||||
export const trimInterruptedResponseRows = (
|
||||
rows: DebugTranscriptRow[],
|
||||
responseId?: string
|
||||
) => {
|
||||
if (!responseId) return rows;
|
||||
return rows.filter((row) => row.responseId !== responseId);
|
||||
};
|
||||
|
||||
export const upsertToolCallRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
args,
|
||||
rawCall,
|
||||
}: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
toolDisplayName: string;
|
||||
executor: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
args?: unknown;
|
||||
rawCall?: unknown;
|
||||
}
|
||||
) => {
|
||||
const rowIndex = rows.findIndex(
|
||||
(row) => isToolRow(row) && row.toolCallId === toolCallId
|
||||
);
|
||||
if (rowIndex === -1) {
|
||||
return [
|
||||
...rows,
|
||||
createToolRow({
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
status: 'pending',
|
||||
args,
|
||||
rawCall,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const row = rows[rowIndex];
|
||||
if (!isToolRow(row)) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
responseId: row.responseId || responseId,
|
||||
status: 'pending',
|
||||
args: args !== undefined ? args : row.args,
|
||||
rawCall: rawCall !== undefined ? rawCall : row.rawCall,
|
||||
};
|
||||
return nextRows;
|
||||
};
|
||||
|
||||
export const normalizeToolStatus = (
|
||||
statusCode?: number,
|
||||
statusMessage?: string
|
||||
): DebugTranscriptToolStatus => {
|
||||
if (statusCode === 504 || String(statusMessage || '').toLowerCase().includes('timeout')) {
|
||||
return 'timeout';
|
||||
}
|
||||
|
||||
if (typeof statusCode === 'number' && statusCode >= 200 && statusCode < 300) {
|
||||
return 'success';
|
||||
}
|
||||
|
||||
return 'error';
|
||||
};
|
||||
|
||||
export const resolveToolResultRow = (
|
||||
rows: DebugTranscriptRow[],
|
||||
{
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor,
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
source,
|
||||
status,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
rawCall,
|
||||
rawResult,
|
||||
}: {
|
||||
toolCallId: string;
|
||||
toolName: string;
|
||||
toolDisplayName: string;
|
||||
executor?: string;
|
||||
turnId?: string;
|
||||
utteranceId?: string;
|
||||
responseId?: string;
|
||||
source?: string;
|
||||
status: DebugTranscriptToolStatus;
|
||||
args?: unknown;
|
||||
result?: unknown;
|
||||
error?: unknown;
|
||||
rawCall?: unknown;
|
||||
rawResult?: unknown;
|
||||
}
|
||||
) => {
|
||||
const rowIndex = rows.findIndex(
|
||||
(row) => isToolRow(row) && row.toolCallId === toolCallId
|
||||
);
|
||||
if (rowIndex === -1) {
|
||||
return [
|
||||
...rows,
|
||||
createToolRow({
|
||||
toolCallId,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
executor: executor || 'server',
|
||||
turnId,
|
||||
utteranceId,
|
||||
responseId,
|
||||
source,
|
||||
status,
|
||||
args,
|
||||
result,
|
||||
error,
|
||||
rawCall,
|
||||
rawResult,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
const row = rows[rowIndex];
|
||||
if (!isToolRow(row)) {
|
||||
return rows;
|
||||
}
|
||||
|
||||
const nextRows = [...rows];
|
||||
nextRows[rowIndex] = {
|
||||
...row,
|
||||
toolName: toolName || row.toolName,
|
||||
toolDisplayName: toolDisplayName || row.toolDisplayName,
|
||||
executor: executor || row.executor,
|
||||
turnId: row.turnId || turnId,
|
||||
utteranceId: row.utteranceId || utteranceId,
|
||||
responseId: row.responseId || responseId,
|
||||
source: source || row.source,
|
||||
status,
|
||||
args: args !== undefined ? args : row.args,
|
||||
result: result !== undefined ? result : row.result,
|
||||
error,
|
||||
rawCall: rawCall !== undefined ? rawCall : row.rawCall,
|
||||
rawResult: rawResult !== undefined ? rawResult : row.rawResult,
|
||||
};
|
||||
return nextRows;
|
||||
};
|
||||
Reference in New Issue
Block a user