⚡ (openai) Use Vercel's AI SDK for streaming
This commit is contained in:
@ -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",
|
||||
|
@ -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.' } }
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
Reference in New Issue
Block a user