2
0

(openai) Use Vercel's AI SDK for streaming

This commit is contained in:
Baptiste Arnaud
2023-06-20 17:23:14 +02:00
parent 7c2e5740dc
commit 3be39cbc78
7 changed files with 254 additions and 76 deletions

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/js",
"version": "0.0.64",
"version": "0.0.65",
"description": "Javascript library to display typebots on your website",
"type": "module",
"main": "dist/index.js",

View File

@ -1,10 +1,8 @@
import { getOpenAiStreamerQuery } from '@/queries/getOpenAiStreamerQuery'
import { ClientSideActionContext } from '@/types'
import {
createParser,
ParsedEvent,
ReconnectInterval,
} from 'eventsource-parser'
import { guessApiHost } from '@/utils/guessApiHost'
import { isNotEmpty } from '@typebot.io/lib/utils'
let abortController: AbortController | null = null
export const streamChat =
(context: ClientSideActionContext) =>
@ -13,59 +11,76 @@ export const streamChat =
content?: string | undefined
role?: 'system' | 'user' | 'assistant' | undefined
}[],
{
onStreamedMessage,
isRetrying,
}: { onStreamedMessage?: (message: string) => void; isRetrying?: boolean }
{ onStreamedMessage }: { onStreamedMessage?: (message: string) => void }
): Promise<{ message?: string; error?: object }> => {
const data = await getOpenAiStreamerQuery(context)(messages)
try {
abortController = new AbortController()
if (!data) return { error: { message: "Couldn't get streamer data" } }
const apiHost = context.apiHost
let message = ''
const res = await fetch(
`${
isNotEmpty(apiHost) ? apiHost : guessApiHost()
}/api/integrations/openai/streamer`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
sessionId: context.sessionId,
messages,
}),
signal: abortController.signal,
}
)
const reader = data.getReader()
const decoder = new TextDecoder()
const onParse = (event: ParsedEvent | ReconnectInterval) => {
if (event.type === 'event') {
const data = event.data
try {
const json = JSON.parse(data) as {
choices: { delta: { content: string } }[]
}
const text = json.choices.at(0)?.delta.content
if (!text) return
message += text
onStreamedMessage?.(message)
} catch (e) {
console.error(e)
if (!res.ok) {
return {
error: {
message: (await res.text()) || 'Failed to fetch the chat response.',
},
}
}
}
const parser = createParser(onParse)
// eslint-disable-next-line no-constant-condition
while (true) {
const { value, done } = await reader.read()
if (done || !value) break
const dataString = decoder.decode(value)
if (dataString.includes('503 Service Temporarily Unavailable')) {
if (isRetrying)
return { error: { message: "Couldn't get streamer data" } }
await new Promise((resolve) => setTimeout(resolve, 3000))
return streamChat(context)(messages, {
onStreamedMessage,
isRetrying: true,
})
if (!res.body) {
throw new Error('The response body is empty.')
}
if (dataString.includes('[DONE]')) break
if (dataString.includes('"error":')) {
return { error: JSON.parse(dataString).error }
}
parser.feed(dataString)
}
return { message }
let message = ''
const reader = res.body.getReader()
const decoder = new TextDecoder()
// eslint-disable-next-line no-constant-condition
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}
const chunk = decoder.decode(value)
if (onStreamedMessage) onStreamedMessage(chunk)
message += chunk
if (abortController === null) {
reader.cancel()
break
}
}
abortController = null
return { message }
} catch (err) {
console.error(err)
// Ignore abort errors as they are expected.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((err as any).name === 'AbortError') {
abortController = null
return { error: { message: 'Request aborted' } }
}
if (err instanceof Error) return { error: { message: err.message } }
return { error: { message: 'Failed to fetch the chat response.' } }
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@typebot.io/react",
"version": "0.0.64",
"version": "0.0.65",
"description": "React library to display typebots on your website",
"main": "dist/index.js",
"types": "dist/index.d.ts",