182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import { connect } from '@planetscale/database'
|
|
import { env } from '@typebot.io/env'
|
|
import { SessionState } from '@typebot.io/schemas'
|
|
import { StreamingTextResponse } from 'ai'
|
|
import OpenAI from 'openai'
|
|
import { NextResponse } from 'next/dist/server/web/spec-extension/response'
|
|
import { getBlockById } from '@typebot.io/schemas/helpers'
|
|
import { forgedBlocks } from '@typebot.io/forge-repository/definitions'
|
|
import { decryptV2 } from '@typebot.io/lib/api/encryption/decryptV2'
|
|
import { VariableStore } from '@typebot.io/forge'
|
|
import {
|
|
ParseVariablesOptions,
|
|
parseVariables,
|
|
} from '@typebot.io/variables/parseVariables'
|
|
import { IntegrationBlockType } from '@typebot.io/schemas/features/blocks/integrations/constants'
|
|
import { getChatCompletionStream } from '@typebot.io/bot-engine/blocks/integrations/legacy/openai/getChatCompletionStream'
|
|
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai/schema'
|
|
import { isForgedBlockType } from '@typebot.io/schemas/features/blocks/forged/helpers'
|
|
|
|
export const preferredRegion = 'lhr1'
|
|
export const dynamic = 'force-dynamic'
|
|
|
|
const responseHeaders = {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
|
|
Pragma: 'no-cache',
|
|
}
|
|
|
|
export async function OPTIONS() {
|
|
return new Response('ok', {
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'POST',
|
|
'Access-Control-Expose-Headers': 'Content-Length, X-JSON',
|
|
'Access-Control-Allow-Headers': '*',
|
|
},
|
|
})
|
|
}
|
|
|
|
// Deprecated in favor of `/api/v1/sessions/:sessionId/streamMessage`.
|
|
export async function POST(req: Request) {
|
|
const { sessionId, messages } = (await req.json()) as {
|
|
messages: OpenAI.Chat.ChatCompletionMessage[] | undefined
|
|
sessionId: string
|
|
}
|
|
|
|
if (!sessionId)
|
|
return NextResponse.json(
|
|
{ message: 'No session ID provided' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
const conn = connect({ url: env.DATABASE_URL })
|
|
|
|
const chatSession = await conn.execute(
|
|
'select state from ChatSession where id=?',
|
|
[sessionId]
|
|
)
|
|
|
|
const state = (chatSession.rows.at(0) as { state: SessionState } | undefined)
|
|
?.state
|
|
|
|
if (!state || !state.currentBlockId)
|
|
return NextResponse.json(
|
|
{ message: 'No state found' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
const { group, block } = getBlockById(
|
|
state.currentBlockId,
|
|
state.typebotsQueue[0].typebot.groups
|
|
)
|
|
if (!block || !group)
|
|
return NextResponse.json(
|
|
{ message: 'Current block not found' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
if (!('options' in block))
|
|
return NextResponse.json(
|
|
{ message: 'Current block does not have options' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
if (block.type === IntegrationBlockType.OPEN_AI && messages) {
|
|
try {
|
|
const stream = await getChatCompletionStream(conn)(
|
|
state,
|
|
block.options as ChatCompletionOpenAIOptions,
|
|
messages
|
|
)
|
|
if (!stream)
|
|
return NextResponse.json(
|
|
{ message: 'Could not create stream' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
return new StreamingTextResponse(stream, {
|
|
headers: responseHeaders,
|
|
})
|
|
} catch (error) {
|
|
if (error instanceof OpenAI.APIError) {
|
|
const { name, status, message } = error
|
|
return NextResponse.json(
|
|
{ name, status, message },
|
|
{ status, headers: responseHeaders }
|
|
)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
if (!isForgedBlockType(block.type))
|
|
return NextResponse.json(
|
|
{ message: 'Invalid forged block id' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
const blockDef = forgedBlocks[block.type]
|
|
const action = blockDef?.actions.find((a) => a.name === block.options?.action)
|
|
|
|
if (!action || !action.run?.stream)
|
|
return NextResponse.json(
|
|
{ message: 'This action does not have a stream function' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
try {
|
|
if (!block.options.credentialsId) return
|
|
const credentials = (
|
|
await conn.execute('select data, iv from Credentials where id=?', [
|
|
block.options.credentialsId,
|
|
])
|
|
).rows.at(0) as { data: string; iv: string } | undefined
|
|
if (!credentials) {
|
|
console.error('Could not find credentials in database')
|
|
return
|
|
}
|
|
const decryptedCredentials = await decryptV2(
|
|
credentials.data,
|
|
credentials.iv
|
|
)
|
|
const variables: VariableStore = {
|
|
list: () => state.typebotsQueue[0].typebot.variables,
|
|
get: (id: string) => {
|
|
const variable = state.typebotsQueue[0].typebot.variables.find(
|
|
(variable) => variable.id === id
|
|
)
|
|
return variable?.value
|
|
},
|
|
parse: (text: string, params?: ParseVariablesOptions) =>
|
|
parseVariables(state.typebotsQueue[0].typebot.variables, params)(text),
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
set: (_1: string, _2: unknown) => {},
|
|
}
|
|
const stream = await action.run.stream.run({
|
|
credentials: decryptedCredentials,
|
|
options: block.options,
|
|
variables,
|
|
})
|
|
if (!stream)
|
|
return NextResponse.json(
|
|
{ message: 'Could not create stream' },
|
|
{ status: 400, headers: responseHeaders }
|
|
)
|
|
|
|
return new StreamingTextResponse(stream, {
|
|
headers: responseHeaders,
|
|
})
|
|
} catch (error) {
|
|
if (error instanceof OpenAI.APIError) {
|
|
const { name, status, message } = error
|
|
return NextResponse.json(
|
|
{ name, status, message },
|
|
{ status, headers: responseHeaders }
|
|
)
|
|
} else {
|
|
throw error
|
|
}
|
|
}
|
|
}
|