mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-04 05:12:38 +02:00
refactor(chat): implement new error handling utilities and streamline interrupt request processing in NewChatPage for improved performance and maintainability
This commit is contained in:
parent
86f6b285ce
commit
d65a3fdf76
3 changed files with 246 additions and 208 deletions
89
surfsense_web/lib/chat/chat-request-errors.ts
Normal file
89
surfsense_web/lib/chat/chat-request-errors.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
export async function toHttpResponseError(
|
||||
response: Response
|
||||
): Promise<Error & { errorCode?: string }> {
|
||||
const statusDefaultCode =
|
||||
response.status === 409
|
||||
? "THREAD_BUSY"
|
||||
: response.status === 429
|
||||
? "RATE_LIMITED"
|
||||
: response.status === 401 || response.status === 403
|
||||
? "AUTH_EXPIRED"
|
||||
: "SERVER_ERROR";
|
||||
|
||||
let rawBody = "";
|
||||
try {
|
||||
rawBody = await response.text();
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
let parsedBody: Record<string, unknown> | null = null;
|
||||
if (rawBody) {
|
||||
try {
|
||||
const parsed = JSON.parse(rawBody);
|
||||
if (typeof parsed === "object" && parsed !== null) {
|
||||
parsedBody = parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
const detail = parsedBody?.detail;
|
||||
const detailObject =
|
||||
typeof detail === "object" && detail !== null ? (detail as Record<string, unknown>) : null;
|
||||
const detailMessage = typeof detail === "string" ? detail : undefined;
|
||||
const topLevelMessage =
|
||||
typeof parsedBody?.message === "string" ? (parsedBody.message as string) : undefined;
|
||||
const detailNestedMessage =
|
||||
typeof detailObject?.message === "string" ? (detailObject.message as string) : undefined;
|
||||
|
||||
const topLevelCode =
|
||||
typeof parsedBody?.errorCode === "string"
|
||||
? parsedBody.errorCode
|
||||
: typeof parsedBody?.error_code === "string"
|
||||
? parsedBody.error_code
|
||||
: undefined;
|
||||
const detailCode =
|
||||
typeof detailObject?.errorCode === "string"
|
||||
? detailObject.errorCode
|
||||
: typeof detailObject?.error_code === "string"
|
||||
? detailObject.error_code
|
||||
: undefined;
|
||||
|
||||
const errorCode = detailCode ?? topLevelCode ?? statusDefaultCode;
|
||||
const message =
|
||||
detailNestedMessage ??
|
||||
detailMessage ??
|
||||
topLevelMessage ??
|
||||
`Backend error: ${response.status}`;
|
||||
|
||||
return Object.assign(new Error(message), { errorCode });
|
||||
}
|
||||
|
||||
export function tagPreAcceptSendFailure(error: unknown): unknown {
|
||||
if (error instanceof Error) {
|
||||
const withCode = error as Error & { errorCode?: string; code?: string };
|
||||
const existingCode = withCode.errorCode ?? withCode.code;
|
||||
const passthroughCodes = new Set([
|
||||
"PREMIUM_QUOTA_EXHAUSTED",
|
||||
"THREAD_BUSY",
|
||||
"AUTH_EXPIRED",
|
||||
"UNAUTHORIZED",
|
||||
"RATE_LIMITED",
|
||||
"NETWORK_ERROR",
|
||||
"STREAM_PARSE_ERROR",
|
||||
"TOOL_EXECUTION_ERROR",
|
||||
"PERSIST_MESSAGE_FAILED",
|
||||
"SERVER_ERROR",
|
||||
]);
|
||||
if (existingCode && passthroughCodes.has(existingCode)) {
|
||||
return Object.assign(error, { errorCode: existingCode });
|
||||
}
|
||||
return Object.assign(error, { errorCode: "SEND_FAILED_PRE_ACCEPT" });
|
||||
}
|
||||
|
||||
return Object.assign(new Error("Failed to send message before stream acceptance"), {
|
||||
errorCode: "SEND_FAILED_PRE_ACCEPT",
|
||||
});
|
||||
}
|
||||
127
surfsense_web/lib/chat/stream-side-effects.ts
Normal file
127
surfsense_web/lib/chat/stream-side-effects.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
import type { ThreadMessageLike } from "@assistant-ui/react";
|
||||
import {
|
||||
addToolCall,
|
||||
type ContentPartsState,
|
||||
type ToolUIGate,
|
||||
updateToolCall,
|
||||
} from "@/lib/chat/streaming-state";
|
||||
|
||||
type InterruptActionRequest = {
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type EditedInterruptAction = {
|
||||
name: string;
|
||||
args: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function readInterruptActions(
|
||||
interruptData: Record<string, unknown>
|
||||
): InterruptActionRequest[] {
|
||||
return (interruptData.action_requests ?? []) as InterruptActionRequest[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies an interrupt request payload to tool-call parts. Existing tool cards
|
||||
* are updated in-place; missing ones are upserted so approval UI always shows.
|
||||
*/
|
||||
export function applyInterruptRequestToContentParts(
|
||||
contentPartsState: ContentPartsState,
|
||||
toolsWithUI: ToolUIGate,
|
||||
interruptData: Record<string, unknown>
|
||||
): void {
|
||||
const { contentParts, toolCallIndices } = contentPartsState;
|
||||
const actionRequests = readInterruptActions(interruptData);
|
||||
for (const action of actionRequests) {
|
||||
const existingEntry = Array.from(toolCallIndices.entries()).find(([, idx]) => {
|
||||
const part = contentParts[idx];
|
||||
return part?.type === "tool-call" && part.toolName === action.name;
|
||||
});
|
||||
|
||||
if (existingEntry) {
|
||||
updateToolCall(contentPartsState, existingEntry[0], {
|
||||
result: { __interrupt__: true, ...interruptData },
|
||||
});
|
||||
} else {
|
||||
const toolCallId = `interrupt-${action.name}`;
|
||||
addToolCall(contentPartsState, toolsWithUI, toolCallId, action.name, action.args, true);
|
||||
updateToolCall(contentPartsState, toolCallId, {
|
||||
result: { __interrupt__: true, ...interruptData },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mergeEditedInterruptAction(
|
||||
contentParts: ContentPartsState["contentParts"],
|
||||
editedAction: EditedInterruptAction | undefined
|
||||
): void {
|
||||
if (!editedAction) return;
|
||||
for (const part of contentParts) {
|
||||
if (part.type === "tool-call" && part.toolName === editedAction.name) {
|
||||
const mergedArgs = { ...part.args, ...editedAction.args };
|
||||
part.args = mergedArgs;
|
||||
// assistant-ui prefers argsText over JSON.stringify(args)
|
||||
part.argsText = JSON.stringify(mergedArgs, null, 2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function markInterruptDecisionOnContentParts(
|
||||
contentParts: ContentPartsState["contentParts"],
|
||||
decisionType: "approve" | "reject" | undefined
|
||||
): void {
|
||||
if (!decisionType) return;
|
||||
for (const part of contentParts) {
|
||||
if (
|
||||
part.type === "tool-call" &&
|
||||
typeof part.result === "object" &&
|
||||
part.result !== null &&
|
||||
"__interrupt__" in (part.result as Record<string, unknown>)
|
||||
) {
|
||||
part.result = {
|
||||
...(part.result as Record<string, unknown>),
|
||||
__decided__: decisionType,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When a streamed message is persisted, the backend returns the durable
|
||||
* turn_id; merge it into assistant-ui metadata for turn-scoped actions.
|
||||
*/
|
||||
export function mergeChatTurnIdIntoMessage(
|
||||
msg: ThreadMessageLike,
|
||||
turnId: string | null | undefined
|
||||
): ThreadMessageLike {
|
||||
if (!turnId) return msg;
|
||||
const existingMeta = (msg.metadata ?? {}) as { custom?: Record<string, unknown> };
|
||||
const existingCustom = existingMeta.custom ?? {};
|
||||
if ((existingCustom as { chatTurnId?: string }).chatTurnId === turnId) return msg;
|
||||
return {
|
||||
...msg,
|
||||
metadata: {
|
||||
...existingMeta,
|
||||
custom: { ...existingCustom, chatTurnId: turnId },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function readStreamedChatTurnId(data: unknown): string | null {
|
||||
if (typeof data !== "object" || data === null) return null;
|
||||
const value = (data as { chat_turn_id?: unknown }).chat_turn_id;
|
||||
return typeof value === "string" && value.length > 0 ? value : null;
|
||||
}
|
||||
|
||||
export function applyTurnIdToAssistantMessageList(
|
||||
messages: ThreadMessageLike[],
|
||||
assistantMsgId: string,
|
||||
turnId: string
|
||||
): ThreadMessageLike[] {
|
||||
return messages.map((m) =>
|
||||
m.id === assistantMsgId ? mergeChatTurnIdIntoMessage(m, turnId) : m
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue