Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚡ Introduce a new high-performing standalone chat API #1200

Merged
merged 40 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
686802a
:zap: Introduce a new high-performing standalone chat API
baptisteArno Jan 31, 2024
c10c3d4
:green_heart: Add build and start scripts in root
baptisteArno Jan 31, 2024
9c3e9e6
:green_heart: Add react as a devDependencies
baptisteArno Jan 31, 2024
c63f3c5
:recycle: Export api handlers in bot-engine package
baptisteArno Jan 31, 2024
7a52997
:green_heart: Attempt to fix "Could not resolve react"
baptisteArno Jan 31, 2024
f13639d
:green_heart: Migrate from pnpm to bun
baptisteArno Feb 5, 2024
6bbbfed
Merge commit '9b5436d4db3326a829a18ac449bcfc59d416b7fb' into feat/cha…
baptisteArno Feb 5, 2024
a90854b
:green_heart: Add start task in turbo.json
baptisteArno Feb 5, 2024
5d46503
:loud_sound: Add openssl version print for debugging purpose
baptisteArno Feb 6, 2024
e427e47
:loud_sound: Add prisma debug log
baptisteArno Feb 6, 2024
45842c3
:alembic: Attempt to pass the build with prisma v5.0
baptisteArno Feb 7, 2024
65bd415
Revert ":alembic: Attempt to pass the build with prisma v5.0"
baptisteArno Feb 8, 2024
a90bf52
:construction_worker: Attempt to build app with custom Dockerfile
baptisteArno Feb 15, 2024
d0f4c82
:green_heart: Fix release COPY args
baptisteArno Feb 15, 2024
111e337
Revert ":green_heart: Migrate from pnpm to bun"
baptisteArno Feb 15, 2024
128f5b6
:construction_worker: Attempt with dead simple docker file
baptisteArno Feb 15, 2024
bc97106
:green_heart: Fix docker image
baptisteArno Feb 15, 2024
5ff838f
:recycle: Move from Elysia to Hono
baptisteArno Feb 26, 2024
5016551
:recycle: Remove openssl debug log
baptisteArno Feb 26, 2024
e006241
Merge branch 'main' into feat/chat-api-server
baptisteArno Feb 26, 2024
ce0ec9d
:rewind: Re introduce npmrc to hoist prisma package
baptisteArno Feb 26, 2024
548fddd
:bug: Add dynamic chat api URL for preview deployments
baptisteArno Feb 26, 2024
e458c89
:loud_sound: Debug env not propagating
baptisteArno Feb 26, 2024
e253368
:green_heart: Correctly generate schema based on database URL
baptisteArno Feb 26, 2024
a15168c
:green_heart: Fix prisma exec command
baptisteArno Feb 26, 2024
6d38873
:green_heart: Only set chat api url if not defined in preview
baptisteArno Feb 26, 2024
83caa17
:green_heart: Update ca certificates in chat api docker image
baptisteArno Feb 26, 2024
2109cc6
:green_heart: Reintroduce git install mistakenly removed
baptisteArno Feb 26, 2024
333d4f8
:technologist: Enable dynamic chat api URL with wildcard
baptisteArno Feb 26, 2024
4dd1e28
:chart_with_upwards_trend: Add prometheus metrics
baptisteArno Mar 11, 2024
f11167f
:twisted_rightwards_arrows: Merge main
baptisteArno Mar 20, 2024
8d70e65
Merge branch 'main' into feat/chat-api-server
baptisteArno Mar 20, 2024
bc9c246
:triangular_flag_on_post: Enable feature only for specified URLs for now
baptisteArno Mar 20, 2024
95099fa
:technologist: Add Sentry
baptisteArno Mar 20, 2024
6a338cc
:green_heart: Allow for staging NODE_ENV
baptisteArno Mar 20, 2024
0fb7e6c
:loud_sound: Debug why chat-api image did not changed
baptisteArno Mar 20, 2024
89d40e3
:loud_sound: Debug pr image not updated
baptisteArno Mar 20, 2024
ba6f994
:green_heart: Still attempt to trigger a proper update on deployment
baptisteArno Mar 20, 2024
db94412
:green_heart: cat problematic file
baptisteArno Mar 20, 2024
185e0cc
:recycle: Enable chat api url on preview deployments
baptisteArno Mar 21, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
public-hoist-pattern[]=*prisma*
public-hoist-pattern[]=*prisma*
2 changes: 1 addition & 1 deletion apps/builder/.prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
emojiList.json
iconNames.ts
reporters
reporters
6 changes: 6 additions & 0 deletions apps/builder/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const injectViewerUrlIfVercelPreview = (val) => {
process.env.VERCEL_BUILDER_PROJECT_NAME,
process.env.NEXT_PUBLIC_VERCEL_VIEWER_PROJECT_NAME
)
if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}

injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)
Expand Down
4 changes: 2 additions & 2 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@
"next": "14.1.0",
"next-auth": "4.22.1",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nodemailer": "6.9.8",
"nprogress": "0.2.0",
"openai": "4.28.4",
"papaparse": "5.4.1",
Expand Down Expand Up @@ -108,7 +108,7 @@
"@types/jsonwebtoken": "9.0.2",
"@types/micro-cors": "0.1.3",
"@types/node": "20.4.2",
"@types/nodemailer": "6.4.8",
"@types/nodemailer": "6.4.14",
"@types/nprogress": "0.2.0",
"@types/papaparse": "5.3.7",
"@types/prettier": "2.7.3",
Expand Down
1 change: 1 addition & 0 deletions apps/builder/src/features/whatsapp/startWhatsAppPreview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export const startWhatsAppPreview = authenticatedProcedure
typebotId,
startFrom,
userId: user.id,
isStreamEnabled: false,
},
initialSessionState: {
whatsApp: (existingSession?.state as SessionState | undefined)
Expand Down
36 changes: 36 additions & 0 deletions apps/chat-api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "chat-api",
"version": "1.0.0",
"license": "AGPL-3.0-or-later",
"private": true,
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- bun --hot src/index.ts",
"build": "dotenv -e ./.env -e ../../.env -- bun build --target=bun ./src/index.ts --outdir ./dist",
"start": "bun src/index.ts"
},
"dependencies": {
"@hono/prometheus": "1.0.0",
"@hono/sentry": "1.0.1",
"@hono/typebox-validator": "0.2.2",
"@sinclair/typebox": "0.32.5",
"@trpc/server": "10.40.0",
"@typebot.io/bot-engine": "workspace:*",
"@typebot.io/env": "workspace:*",
"@typebot.io/forge": "workspace:*",
"@typebot.io/forge-repository": "workspace:*",
"@typebot.io/lib": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"@typebot.io/schemas": "workspace:*",
"@typebot.io/variables": "workspace:*",
"ai": "3.0.12",
"hono": "4.0.5",
"openai": "4.28.4",
"prom-client": "15.1.0"
},
"devDependencies": {
"dotenv-cli": "7.2.1",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"react": "18.2.0"
}
}
30 changes: 30 additions & 0 deletions apps/chat-api/src/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { env } from '@typebot.io/env'
import { mockedUser } from '@typebot.io/lib/mockedUser'
import prisma from '@typebot.io/lib/prisma'

export const getAuthenticatedUserId = async (
authorizationHeaderValue: string | undefined
): Promise<string | undefined> => {
if (env.NEXT_PUBLIC_E2E_TEST) return mockedUser.id
const bearerToken = extractBearerToken(authorizationHeaderValue)
if (!bearerToken) return
return authenticateByToken(bearerToken)
}

const authenticateByToken = async (
token: string
): Promise<string | undefined> => {
if (typeof window !== 'undefined') return
const apiToken = await prisma.apiToken.findFirst({
where: {
token,
},
select: {
ownerId: true,
},
})
return apiToken?.ownerId
}

const extractBearerToken = (authorizationHeaderValue: string | undefined) =>
authorizationHeaderValue?.slice(7)
30 changes: 30 additions & 0 deletions apps/chat-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Hono } from 'hono'
import { webRuntime } from './runtimes/web'
import { whatsAppRuntime } from './runtimes/whatsapp'
import { prometheus } from '@hono/prometheus'
import { sentry } from '@hono/sentry'
import { env } from '@typebot.io/env'

const app = new Hono()

app.use(
'*',
sentry({
environment: env.NODE_ENV,
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA + '-chat-api',
})
)

const { printMetrics, registerMetrics } = prometheus()
app.use('*', registerMetrics)
app.get('/metrics', printMetrics)

app.get('/ping', (c) => c.json({ status: 'ok' }, 200))
app.route('/', webRuntime)
app.route('/', whatsAppRuntime)

export default {
port: process.env.PORT ?? 3002,
fetch: app.fetch,
}
132 changes: 132 additions & 0 deletions apps/chat-api/src/runtimes/web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { startChat } from '@typebot.io/bot-engine/apiHandlers/startChat'
import { continueChat } from '@typebot.io/bot-engine/apiHandlers/continueChat'
import { startChatPreview } from '@typebot.io/bot-engine/apiHandlers/startChatPreview'
import { getMessageStream } from '@typebot.io/bot-engine/apiHandlers/getMessageStream'
import { Hono } from 'hono'
import { cors } from 'hono/cors'
import { tbValidator } from '@hono/typebox-validator'
import { Type as t } from '@sinclair/typebox'
import { getAuthenticatedUserId } from '../auth'

export const webRuntime = new Hono()

webRuntime.use('*', cors())

webRuntime.post(
'/api/v1/typebots/:publicId/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await startChat({
...data,
publicId: c.req.param('publicId'),
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)

webRuntime.post(
'/api/v1/typebots/:id/preview/startChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
isStreamEnabled: t.Optional(t.Boolean()),
resultId: t.Optional(t.String()),
isOnlyRegistering: t.Optional(t.Boolean()),
prefilledVariables: t.Optional(t.Record(t.String(), t.Unknown())),
startFrom: t.Optional(
t.Union([
t.Object({
type: t.Literal('group'),
groupId: t.String(),
}),
t.Object({
type: t.Literal('event'),
eventId: t.String(),
}),
])
),
typebot: t.Optional(t.Any()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const userId = !data.typebot
? await getAuthenticatedUserId(c.req.header('Authorization'))
: undefined
return c.json(
await startChatPreview({
...data,
typebotId: c.req.param('id'),
userId,
isStreamEnabled: data.isStreamEnabled ?? true,
isOnlyRegistering: data.isOnlyRegistering ?? false,
})
)
}
)

webRuntime.post(
'/api/v1/sessions/:sessionId/continueChat',
tbValidator(
'json',
t.Object({
message: t.Optional(t.String()),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { corsOrigin, ...response } = await continueChat({
...data,
sessionId: c.req.param('sessionId'),
origin: c.req.header('origin'),
})
if (corsOrigin) c.res.headers.set('Access-Control-Allow-Origin', corsOrigin)
return c.json(response)
}
)

webRuntime.post(
'/api/v1/sessions/:sessionId/streamMessage',
tbValidator(
'json',
t.Object({
messages: t.Optional(t.Array(t.Any())),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
const { stream, status, message } = await getMessageStream({
sessionId: c.req.param('sessionId'),
messages: data.messages,
})
if (!stream) return c.json({ message }, (status ?? 400) as ResponseInit)
return new Response(stream)
}
)
51 changes: 51 additions & 0 deletions apps/chat-api/src/runtimes/whatsapp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { receiveMessage } from '@typebot.io/bot-engine/apiHandlers/receiveMessage'
import { receiveMessagePreview } from '@typebot.io/bot-engine/apiHandlers/receiveMessagePreview'
import { tbValidator } from '@hono/typebox-validator'
import { Hono } from 'hono'
import { Type as t } from '@sinclair/typebox'

export const whatsAppRuntime = new Hono()

whatsAppRuntime.post(
'/api/v1/workspaces/:workspaceId/whatsapp/:credentialsId/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessage({
workspaceId: c.req.param('workspaceId'),
credentialsId: c.req.param('credentialsId'),
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)

whatsAppRuntime.post(
'/api/v1/whatsapp/preview/webhook',
tbValidator(
'json',
t.Object({
object: t.String(),
entry: t.Any(),
}),
(result, c) => {
if (!result.success) return c.json({ message: 'Invalid input' }, 400)
}
),
async (c) => {
const data = c.req.valid('json')
receiveMessagePreview({
...data,
})
return c.json({ message: 'Webhook received' }, 200)
}
)
8 changes: 8 additions & 0 deletions apps/chat-api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "@typebot.io/tsconfig/base.json",
"include": ["src/**/*.ts"],
"exclude": ["node_modules"],
"compilerOptions": {
"jsx": "react"
}
}
6 changes: 4 additions & 2 deletions apps/docs/openapi/viewer.json
Original file line number Diff line number Diff line change
Expand Up @@ -1772,14 +1772,16 @@
"type": "object",
"properties": {
"isStreamEnabled": {
"type": "boolean"
"type": "boolean",
"default": false
},
"message": {
"type": "string"
},
"isOnlyRegistering": {
"type": "boolean",
"description": "If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message."
"description": "If set to `true`, it will only register the session and not start the bot. This is used for 3rd party chat platforms as it can require a session to be registered before sending the first message.",
"default": false
},
"typebot": {
"oneOf": [
Expand Down
2 changes: 1 addition & 1 deletion apps/landing-page/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "landing-page",
"version": "1.0.0",
"scripts": {
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3002",
"dev": "dotenv -e ./.env -e ../../.env -- next dev -p 3003",
"start": "dotenv -e ./.env -e ../../.env -- next start",
"build": "dotenv -e ./.env -e ../../.env -- next build",
"lint": "next lint",
Expand Down
6 changes: 6 additions & 0 deletions apps/viewer/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const injectViewerUrlIfVercelPreview = (val) => {
)
return
process.env.NEXT_PUBLIC_VIEWER_URL = `https://${process.env.VERCEL_BRANCH_URL}`
if (process.env.NEXT_PUBLIC_CHAT_API_URL.includes('{{pr_id}}'))
process.env.NEXT_PUBLIC_CHAT_API_URL =
process.env.NEXT_PUBLIC_CHAT_API_URL.replace(
'{{pr_id}}',
process.env.VERCEL_GIT_PULL_REQUEST_ID
)
}

injectViewerUrlIfVercelPreview(process.env.NEXT_PUBLIC_VIEWER_URL)
Expand Down
Loading