diff --git a/packages/backend/server/src/plugins/copilot/controller.ts b/packages/backend/server/src/plugins/copilot/controller.ts index 9d65404031..61203648d6 100644 --- a/packages/backend/server/src/plugins/copilot/controller.ts +++ b/packages/backend/server/src/plugins/copilot/controller.ts @@ -37,6 +37,8 @@ import { Config, CopilotFailedToGenerateText, CopilotSessionNotFound, + InternalServerError, + mapAnyError, mapSseError, metrics, NoCopilotProviderAvailable, @@ -148,8 +150,8 @@ export class CopilotController implements BeforeApplicationShutdown { if (!messageId || retry) { // revert the latest message generated by the assistant // if messageId is provided, we will also revert latest user message - await this.chatSession.revertLatestMessage(sessionId, !messageId); - session.revertLatestMessage(!messageId); + await this.chatSession.revertLatestMessage(sessionId, !!messageId); + session.revertLatestMessage(!!messageId); } if (messageId) { @@ -209,31 +211,34 @@ export class CopilotController implements BeforeApplicationShutdown { @Param('sessionId') sessionId: string, @Query() params: Record ): Promise { - const { messageId, retry } = this.prepareParams(params); + const info: any = { sessionId, params }; - const provider = await this.chooseTextProvider( - user.id, - sessionId, - messageId - ); - - const session = await this.appendSessionMessage( - sessionId, - messageId, - retry - ); try { - metrics.ai.counter('chat_calls').add(1, { model: session.model }); - const content = await provider.generateText( - session.finish(params), - session.model, - { - ...session.config.promptConfig, - signal: this.getSignal(req), - user: user.id, - } + const { messageId, retry } = this.prepareParams(params); + + const provider = await this.chooseTextProvider( + user.id, + sessionId, + messageId ); + const session = await this.appendSessionMessage( + sessionId, + messageId, + retry + ); + + info.model = session.model; + metrics.ai.counter('chat_calls').add(1, { model: session.model }); + const finalMessage = session.finish(params); + info.finalMessage = finalMessage; + + const content = await provider.generateText(finalMessage, session.model, { + ...session.config.promptConfig, + signal: this.getSignal(req), + user: user.id, + }); + session.push({ role: 'assistant', content, @@ -243,8 +248,13 @@ export class CopilotController implements BeforeApplicationShutdown { return content; } catch (e: any) { - metrics.ai.counter('chat_errors').add(1, { model: session.model }); - throw new CopilotFailedToGenerateText(e.message); + metrics.ai.counter('chat_errors').add(1); + let error = mapAnyError(e); + if (error instanceof InternalServerError) { + error = new CopilotFailedToGenerateText(e.message); + } + error.log('CopilotChat', info); + throw error; } } @@ -276,8 +286,10 @@ export class CopilotController implements BeforeApplicationShutdown { metrics.ai.counter('chat_stream_calls').add(1, { model: session.model }); this.ongoingStreamCount$.next(this.ongoingStreamCount$.value + 1); + const finalMessage = session.finish(params); + info.finalMessage = finalMessage; const source$ = from( - provider.generateTextStream(session.finish(params), session.model, { + provider.generateTextStream(finalMessage, session.model, { ...session.config.promptConfig, signal: this.getSignal(req), user: user.id, diff --git a/packages/backend/server/src/plugins/copilot/providers/perplexity.ts b/packages/backend/server/src/plugins/copilot/providers/perplexity.ts index 100a75d814..d355644743 100644 --- a/packages/backend/server/src/plugins/copilot/providers/perplexity.ts +++ b/packages/backend/server/src/plugins/copilot/providers/perplexity.ts @@ -21,15 +21,24 @@ export type PerplexityConfig = { endpoint?: string; }; -const PerplexityErrorSchema = z.object({ - detail: z.array( - z.object({ - loc: z.array(z.string()), - msg: z.string(), +const PerplexityErrorSchema = z.union([ + z.object({ + detail: z.array( + z.object({ + loc: z.array(z.string()), + msg: z.string(), + type: z.string(), + }) + ), + }), + z.object({ + error: z.object({ + message: z.string(), type: z.string(), - }) - ), -}); + code: z.number(), + }), + }), +]); const PerplexityDataSchema = z.object({ citations: z.array(z.string()), @@ -50,6 +59,8 @@ const PerplexityDataSchema = z.object({ const PerplexitySchema = z.union([PerplexityDataSchema, PerplexityErrorSchema]); +type PerplexityError = z.infer; + export class CitationParser { private readonly SQUARE_BRACKET_OPEN = '['; @@ -214,12 +225,8 @@ export class PerplexityProvider implements CopilotTextToTextProvider { params ); const data = PerplexitySchema.parse(await response.json()); - if ('detail' in data) { - throw new CopilotProviderSideError({ - provider: this.type, - kind: 'unexpected_response', - message: data.detail[0].msg || 'Unexpected perplexity response', - }); + if ('detail' in data || 'error' in data) { + throw this.convertError(data); } else { const citationParser = new CitationParser(); const { content } = data.choices[0].message; @@ -264,9 +271,9 @@ export class PerplexityProvider implements CopilotTextToTextProvider { this.config.endpoint || 'https://api.perplexity.ai/chat/completions', params ); - if (response.body) { + const errorHandler = this.convertError; + if (response.ok && response.body) { const citationParser = new CitationParser(); - const provider = this.type; const eventStream = response.body .pipeThrough(new TextDecoderStream()) .pipeThrough(new EventSourceParserStream()) @@ -280,13 +287,8 @@ export class PerplexityProvider implements CopilotTextToTextProvider { const json = JSON.parse(chunk.data); if (json) { const data = PerplexitySchema.parse(json); - if ('detail' in data) { - throw new CopilotProviderSideError({ - provider, - kind: 'unexpected_response', - message: - data.detail[0].msg || 'Unexpected perplexity response', - }); + if ('detail' in data || 'error' in data) { + throw errorHandler(data); } const { content } = data.choices[0].delta; const { citations } = data; @@ -331,6 +333,24 @@ export class PerplexityProvider implements CopilotTextToTextProvider { } } + private convertError(e: PerplexityError) { + function getErrMessage(e: PerplexityError) { + let err = 'Unexpected perplexity response'; + if ('detail' in e) { + err = e.detail[0].msg || err; + } else if ('error' in e) { + err = e.error.message || err; + } + return err; + } + + throw new CopilotProviderSideError({ + provider: this.type, + kind: 'unexpected_response', + message: getErrMessage(e), + }); + } + private handleError(e: any) { if (e instanceof CopilotProviderSideError) { return e;