mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-04 05:12:38 +02:00
refactor(chat): implement turn cancellation and status management in new chat routes for improved user experience and error handling
This commit is contained in:
parent
4056bd1d69
commit
af66fbf106
12 changed files with 671 additions and 81 deletions
|
|
@ -147,6 +147,22 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError
|
|||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "TURN_CANCELLING"
|
||||
) {
|
||||
return {
|
||||
kind: "thread_busy",
|
||||
channel: "toast",
|
||||
severity: "info",
|
||||
telemetryEvent: "chat_blocked",
|
||||
isExpected: true,
|
||||
userMessage: "A previous response is still stopping. Please try again in a moment.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "TURN_CANCELLING",
|
||||
details: { flow: input.flow },
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
errorCode === "THREAD_BUSY"
|
||||
) {
|
||||
|
|
@ -156,7 +172,7 @@ export function classifyChatError(input: RawChatErrorInput): NormalizedChatError
|
|||
severity: "warn",
|
||||
telemetryEvent: "chat_blocked",
|
||||
isExpected: true,
|
||||
userMessage: "A previous response is still stopping. Please try again in a moment.",
|
||||
userMessage: "Another response is still finishing for this thread. Please try again in a moment.",
|
||||
rawMessage,
|
||||
errorCode: errorCode ?? "THREAD_BUSY",
|
||||
details: { flow: input.flow },
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export async function toHttpResponseError(
|
||||
response: Response
|
||||
): Promise<Error & { errorCode?: string }> {
|
||||
): Promise<Error & { errorCode?: string; retryAfterMs?: number }> {
|
||||
const statusDefaultCode =
|
||||
response.status === 409
|
||||
? "THREAD_BUSY"
|
||||
|
|
@ -52,13 +52,37 @@ export async function toHttpResponseError(
|
|||
: undefined;
|
||||
|
||||
const errorCode = detailCode ?? topLevelCode ?? statusDefaultCode;
|
||||
|
||||
const detailRetryAfterMs =
|
||||
typeof detailObject?.retry_after_ms === "number"
|
||||
? detailObject.retry_after_ms
|
||||
: typeof detailObject?.retryAfterMs === "number"
|
||||
? detailObject.retryAfterMs
|
||||
: undefined;
|
||||
const topRetryAfterMs =
|
||||
typeof parsedBody?.retry_after_ms === "number"
|
||||
? parsedBody.retry_after_ms
|
||||
: typeof parsedBody?.retryAfterMs === "number"
|
||||
? parsedBody.retryAfterMs
|
||||
: undefined;
|
||||
const headerRetryAfterMsRaw = response.headers.get("retry-after-ms");
|
||||
const headerRetryAfterMs = headerRetryAfterMsRaw ? Number.parseFloat(headerRetryAfterMsRaw) : NaN;
|
||||
const retryAfterHeader = response.headers.get("retry-after");
|
||||
const retryAfterSeconds = retryAfterHeader ? Number.parseFloat(retryAfterHeader) : NaN;
|
||||
const retryAfterMsFromHeader = Number.isFinite(headerRetryAfterMs)
|
||||
? Math.max(0, Math.round(headerRetryAfterMs))
|
||||
: Number.isFinite(retryAfterSeconds)
|
||||
? Math.max(0, Math.round(retryAfterSeconds * 1000))
|
||||
: undefined;
|
||||
const retryAfterMs =
|
||||
detailRetryAfterMs ?? topRetryAfterMs ?? retryAfterMsFromHeader ?? undefined;
|
||||
const message =
|
||||
detailNestedMessage ??
|
||||
detailMessage ??
|
||||
topLevelMessage ??
|
||||
`Backend error: ${response.status}`;
|
||||
|
||||
return Object.assign(new Error(message), { errorCode });
|
||||
return Object.assign(new Error(message), { errorCode, retryAfterMs });
|
||||
}
|
||||
|
||||
export function tagPreAcceptSendFailure(error: unknown): unknown {
|
||||
|
|
@ -68,6 +92,7 @@ export function tagPreAcceptSendFailure(error: unknown): unknown {
|
|||
const passthroughCodes = new Set([
|
||||
"PREMIUM_QUOTA_EXHAUSTED",
|
||||
"THREAD_BUSY",
|
||||
"TURN_CANCELLING",
|
||||
"AUTH_EXPIRED",
|
||||
"UNAUTHORIZED",
|
||||
"RATE_LIMITED",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ export type SharedStreamEventContext = {
|
|||
scheduleFlush: () => void;
|
||||
forceFlush: () => void;
|
||||
onTokenUsage?: (data: Extract<SSEEvent, { type: "data-token-usage" }>["data"]) => void;
|
||||
onTurnStatus?: (data: Extract<SSEEvent, { type: "data-turn-status" }>["data"]) => void;
|
||||
onToolOutputAvailable?: (
|
||||
event: Extract<SSEEvent, { type: "tool-output-available" }>,
|
||||
context: {
|
||||
|
|
@ -173,6 +174,10 @@ export function processSharedStreamEvent(parsed: SSEEvent, context: SharedStream
|
|||
context.onTokenUsage?.(parsed.data);
|
||||
return true;
|
||||
|
||||
case "data-turn-status":
|
||||
context.onTurnStatus?.(parsed.data);
|
||||
return true;
|
||||
|
||||
case "error":
|
||||
throw toStreamTerminalError(parsed);
|
||||
|
||||
|
|
|
|||
|
|
@ -528,6 +528,14 @@ export type SSEEvent =
|
|||
}>;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "data-turn-status";
|
||||
data: {
|
||||
status: "idle" | "busy" | "cancelling";
|
||||
retry_after_ms?: number;
|
||||
retry_after_at?: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "data-token-usage";
|
||||
data: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue