🧑‍💻 (chat) Introduce startChat and continueChat endpoints

Closes #1030
This commit is contained in:
Baptiste Arnaud
2023-11-13 15:27:36 +01:00
parent 63233eb7ee
commit 084588a086
74 changed files with 28426 additions and 645 deletions

View File

@@ -0,0 +1,79 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { continueChatResponseSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'
import { z } from 'zod'
export const continueChat = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/sessions/{sessionId}/continueChat',
summary: 'Continue chat',
description:
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
},
})
.input(
z.object({
message: z.string().optional(),
sessionId: z.string(),
})
)
.output(continueChatResponseSchema)
.mutation(async ({ input: { sessionId, message } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const isSessionExpired =
session &&
isDefined(session.state.expiryTimeout) &&
session.updatedAt.getTime() + session.state.expiryTimeout < Date.now()
if (isSessionExpired)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session expired. You need to start a new session.',
})
const {
messages,
input,
clientSideActions,
newSessionState,
logs,
lastMessageNewFormat,
visitedEdges,
} = await continueBotFlow(message, { version: 2, state: session.state })
if (newSessionState)
await saveStateToDatabase({
session: {
id: session.id,
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
messages,
input,
clientSideActions,
dynamicTheme: parseDynamicTheme(newSessionState),
logs,
lastMessageNewFormat,
}
})

View File

@@ -1,8 +1,8 @@
import { publicProcedure } from '@/helpers/server/trpc'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/schema'
chatReplySchema,
} from '@typebot.io/schemas/features/chat/legacy/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { startSession } from '@typebot.io/bot-engine/startSession'
@@ -16,10 +16,12 @@ export const sendMessageV1 = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/sendMessage',
path: '/v1/sendMessage',
summary: 'Send a message',
description:
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
tags: ['Deprecated'],
deprecated: true,
},
})
.input(sendMessageInputSchema)
@@ -60,8 +62,45 @@ export const sendMessageV1 = publicProcedure
visitedEdges,
} = await startSession({
version: 1,
startParams,
userId: user?.id,
startParams:
startParams.isPreview || typeof startParams.typebot !== 'string'
? {
type: 'preview',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
startFrom:
'startGroupId' in startParams && startParams.startGroupId
? {
type: 'group',
groupId: startParams.startGroupId,
}
: 'startEventId' in startParams &&
startParams.startEventId
? {
type: 'event',
eventId: startParams.startEventId,
}
: undefined,
typebotId:
typeof startParams.typebot === 'string'
? startParams.typebot
: startParams.typebot.id,
typebot:
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
userId: parseUserId(user?.id),
}
: {
type: 'live',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
},
message,
})
@@ -133,3 +172,13 @@ export const sendMessageV1 = publicProcedure
}
}
)
const parseUserId = (userId?: string): string => {
if (!userId)
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You need to be authenticated to perform this action',
})
return userId
}

View File

@@ -1,8 +1,4 @@
import { publicProcedure } from '@/helpers/server/trpc'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { startSession } from '@typebot.io/bot-engine/startSession'
@@ -11,15 +7,21 @@ import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
import { continueBotFlow } from '@typebot.io/bot-engine/continueBotFlow'
import { parseDynamicTheme } from '@typebot.io/bot-engine/parseDynamicTheme'
import { isDefined } from '@typebot.io/lib/utils'
import {
chatReplySchema,
sendMessageInputSchema,
} from '@typebot.io/schemas/features/chat/legacy/schema'
export const sendMessageV2 = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/sendMessage',
path: '/v2/sendMessage',
summary: 'Send a message',
description:
'To initiate a chat, do not provide a `sessionId` nor a `message`.\n\nContinue the conversation by providing the `sessionId` and the `message` that should answer the previous question.\n\nSet the `isPreview` option to `true` to chat with the non-published version of the typebot.',
tags: ['Deprecated'],
deprecated: true,
},
})
.input(sendMessageInputSchema)
@@ -60,8 +62,45 @@ export const sendMessageV2 = publicProcedure
visitedEdges,
} = await startSession({
version: 2,
startParams,
userId: user?.id,
startParams:
startParams.isPreview || typeof startParams.typebot !== 'string'
? {
type: 'preview',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
startFrom:
'startGroupId' in startParams && startParams.startGroupId
? {
type: 'group',
groupId: startParams.startGroupId,
}
: 'startEventId' in startParams &&
startParams.startEventId
? {
type: 'event',
eventId: startParams.startEventId,
}
: undefined,
typebotId:
typeof startParams.typebot === 'string'
? startParams.typebot
: startParams.typebot.id,
typebot:
typeof startParams.typebot === 'string'
? undefined
: startParams.typebot,
message,
userId: parseUserId(user?.id),
}
: {
type: 'live',
isOnlyRegistering: startParams.isOnlyRegistering ?? false,
isStreamEnabled: startParams.isStreamEnabled,
publicId: startParams.typebot,
prefilledVariables: startParams.prefilledVariables,
resultId: startParams.resultId,
message,
},
message,
})
@@ -133,3 +172,13 @@ export const sendMessageV2 = publicProcedure
}
}
)
const parseUserId = (userId?: string): string => {
if (!userId)
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You need to be authenticated to perform this action',
})
return userId
}

View File

@@ -0,0 +1,63 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { chatLogSchema } from '@typebot.io/schemas/features/chat/schema'
import { TRPCError } from '@trpc/server'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
import { z } from 'zod'
import { saveLogs } from '@typebot.io/bot-engine/queries/saveLogs'
import { formatLogDetails } from '@typebot.io/bot-engine/logs/helpers/formatLogDetails'
import * as Sentry from '@sentry/nextjs'
export const saveClientLogs = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/sessions/{sessionId}/clientLogs',
summary: 'Save client logs',
},
})
.input(
z.object({
sessionId: z.string(),
clientLogs: z.array(chatLogSchema),
})
)
.output(z.object({ message: z.string() }))
.mutation(async ({ input: { sessionId, clientLogs } }) => {
const session = await getSession(sessionId)
if (!session) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Session not found.',
})
}
const resultId = session.state.typebotsQueue[0].resultId
if (!resultId) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Result not found.',
})
}
try {
await saveLogs(
clientLogs.map((log) => ({
...log,
resultId,
details: formatLogDetails(log.details),
}))
)
return {
message: 'Logs successfully saved.',
}
} catch (e) {
console.error('Failed to save logs', e)
Sentry.captureException(e)
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Failed to save logs.',
})
}
})

View File

@@ -0,0 +1,83 @@
import { publicProcedure } from '@/helpers/server/trpc'
import {
startChatInputSchema,
startChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
export const startChat = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/typebots/{publicId}/startChat',
summary: 'Start chat',
},
})
.input(startChatInputSchema)
.output(startChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
publicId,
isStreamEnabled,
prefilledVariables,
resultId: startResultId,
},
}) => {
const {
typebot,
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'live',
isOnlyRegistering,
isStreamEnabled,
publicId,
prefilledVariables,
resultId: startResultId,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
resultId,
dynamicTheme,
logs,
clientSideActions,
}
}
)

View File

@@ -0,0 +1,83 @@
import { authenticatedProcedure } from '@/helpers/server/trpc'
import {
startPreviewChatInputSchema,
startPreviewChatResponseSchema,
} from '@typebot.io/schemas/features/chat/schema'
import { startSession } from '@typebot.io/bot-engine/startSession'
import { saveStateToDatabase } from '@typebot.io/bot-engine/saveStateToDatabase'
import { restartSession } from '@typebot.io/bot-engine/queries/restartSession'
export const startChatPreview = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/v1/typebots/{typebotId}/preview/startChat',
summary: 'Start preview chat',
},
})
.input(startPreviewChatInputSchema)
.output(startPreviewChatResponseSchema)
.mutation(
async ({
input: {
message,
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
},
ctx: { user },
}) => {
const {
typebot,
messages,
input,
dynamicTheme,
logs,
clientSideActions,
newSessionState,
visitedEdges,
} = await startSession({
version: 2,
startParams: {
type: 'preview',
isOnlyRegistering,
isStreamEnabled,
startFrom,
typebotId,
typebot: startTypebot,
userId: user.id,
},
message,
})
const session = isOnlyRegistering
? await restartSession({
state: newSessionState,
})
: await saveStateToDatabase({
session: {
state: newSessionState,
},
input,
logs,
clientSideActions,
visitedEdges,
})
return {
sessionId: session.id,
typebot: {
id: typebot.id,
theme: typebot.theme,
settings: typebot.settings,
},
messages,
input,
dynamicTheme,
logs,
clientSideActions,
}
}
)

View File

@@ -1,4 +1,3 @@
import { publicProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { getSession } from '@typebot.io/bot-engine/queries/getSession'
@@ -9,12 +8,13 @@ import {
Variable,
} from '@typebot.io/schemas'
import prisma from '@typebot.io/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
export const updateTypebotInSession = publicProcedure
export const updateTypebotInSession = authenticatedProcedure
.meta({
openapi: {
method: 'POST',
path: '/sessions/{sessionId}/updateTypebot',
path: '/v1/sessions/{sessionId}/updateTypebot',
summary: 'Update typebot in session',
description:
'Update chat session with latest typebot modifications. This is useful when you want to update the typebot in an ongoing session after making changes to it.',
@@ -28,8 +28,6 @@ export const updateTypebotInSession = publicProcedure
)
.output(z.object({ message: z.literal('success') }))
.mutation(async ({ input: { sessionId }, ctx: { user } }) => {
if (!user)
throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Unauthorized' })
const session = await getSession(sessionId)
if (!session)
throw new TRPCError({ code: 'NOT_FOUND', message: 'Session not found' })

View File

@@ -19,10 +19,11 @@ export const getUploadUrl = publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
path: '/v1/typebots/{typebotId}/blocks/{blockId}/storage/upload-url',
summary: 'Get upload URL for a file',
description: 'Used for the web client to get the bucket upload file.',
deprecated: true,
tags: ['Deprecated'],
},
})
.input(

View File

@@ -13,7 +13,7 @@ export const generateUploadUrl = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/generate-upload-url',
path: '/v1/generate-upload-url',
summary: 'Generate upload URL',
description: 'Used to upload anything from the client to S3 bucket',
},

View File

@@ -8,7 +8,7 @@ export const receiveMessage = publicProcedure
.meta({
openapi: {
method: 'POST',
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
summary: 'Message webhook',
tags: ['WhatsApp'],
},

View File

@@ -7,7 +7,7 @@ export const subscribeWebhook = publicProcedure
.meta({
openapi: {
method: 'GET',
path: '/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
path: '/v1/workspaces/{workspaceId}/whatsapp/{credentialsId}/webhook',
summary: 'Subscribe webhook',
tags: ['WhatsApp'],
protect: true,