Skip to content

Commit

Permalink
⚡ (openai) Add custom provider and custom models
Browse files Browse the repository at this point in the history
Closes #532
  • Loading branch information
baptisteArno committed Sep 1, 2023
1 parent 436fa25 commit 27a5f4e
Show file tree
Hide file tree
Showing 21 changed files with 684 additions and 278 deletions.
1 change: 1 addition & 0 deletions apps/builder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
"nextjs-cors": "^2.1.2",
"nodemailer": "6.9.3",
"nprogress": "0.2.0",
"openai-edge": "1.2.2",
"papaparse": "5.4.1",
"posthog-js": "^1.77.1",
"posthog-node": "3.1.1",
Expand Down
9 changes: 5 additions & 4 deletions apps/builder/src/components/inputs/AutocompleteInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { MoreInfoTooltip } from '../MoreInfoTooltip'
import { env } from '@typebot.io/env'

type Props = {
items: string[]
items: string[] | undefined
value?: string
defaultValue?: string
debounceTimeout?: number
Expand Down Expand Up @@ -77,9 +77,9 @@ export const AutocompleteInput = ({

const filteredItems = (
inputValue === ''
? items
? items ?? []
: [
...items.filter(
...(items ?? []).filter(
(item) =>
item.toLowerCase().startsWith((inputValue ?? '').toLowerCase()) &&
item.toLowerCase() !== inputValue.toLowerCase()
Expand Down Expand Up @@ -186,7 +186,8 @@ export const AutocompleteInput = ({
onFocus={onOpen}
onBlur={updateCarretPosition}
onKeyDown={updateFocusedDropdownItem}
placeholder={placeholder}
placeholder={!items ? 'Loading...' : placeholder}
isDisabled={!items}
/>
</PopoverAnchor>
{filteredItems.length > 0 && (
Expand Down
15 changes: 10 additions & 5 deletions apps/builder/src/components/inputs/Select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type Item =
type Props<T extends Item> = {
isPopoverMatchingInputWidth?: boolean
selectedItem?: string
items: readonly T[]
items: readonly T[] | undefined
placeholder?: string
onSelect?: (value: string | undefined, item?: T) => void
}
Expand All @@ -53,7 +53,7 @@ export const Select = <T extends Item>({
const { onOpen, onClose, isOpen } = useDisclosure()
const [inputValue, setInputValue] = useState(
getItemLabel(
items.find((item) =>
items?.find((item) =>
typeof item === 'string'
? selectedItem === item
: selectedItem === item.value
Expand All @@ -72,13 +72,13 @@ export const Select = <T extends Item>({
const filteredItems = (
isTouched
? [
...items.filter((item) =>
...(items ?? []).filter((item) =>
getItemLabel(item)
.toLowerCase()
.includes((inputValue ?? '').toLowerCase())
),
]
: items
: items ?? []
).slice(0, 50)

const closeDropdown = () => {
Expand Down Expand Up @@ -181,12 +181,17 @@ export const Select = <T extends Item>({
className="select-input"
value={isTouched ? inputValue : ''}
placeholder={
!isTouched && inputValue !== '' ? undefined : placeholder
!items
? 'Loading...'
: !isTouched && inputValue !== ''
? undefined
: placeholder
}
onChange={updateInputValue}
onFocus={onOpen}
onKeyDown={updateFocusedDropdownItem}
pr={selectedItem ? 16 : undefined}
isDisabled={!items}
/>

<InputRightElement
Expand Down
134 changes: 134 additions & 0 deletions apps/builder/src/features/blocks/integrations/openai/api/listModels.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import prisma from '@/lib/prisma'
import { authenticatedProcedure } from '@/helpers/server/trpc'
import { TRPCError } from '@trpc/server'
import { z } from 'zod'
import { isReadWorkspaceFobidden } from '@/features/workspace/helpers/isReadWorkspaceFobidden'
import { Configuration, OpenAIApi, ResponseTypes } from 'openai-edge'
import { decrypt } from '@typebot.io/lib/api'
import { OpenAICredentials } from '@typebot.io/schemas/features/blocks/integrations/openai'
import { IntegrationBlockType, typebotSchema } from '@typebot.io/schemas'
import { isNotEmpty } from '@typebot.io/lib/utils'

export const listModels = authenticatedProcedure
.meta({
openapi: {
method: 'GET',
path: '/typebots/{typebotId}/blocks/{blockId}/openai/models',
protect: true,
summary: 'List OpenAI models',
tags: ['OpenAI'],
},
})
.input(
z.object({
typebotId: z.string(),
blockId: z.string(),
credentialsId: z.string(),
workspaceId: z.string(),
})
)
.output(
z.object({
models: z.array(z.string()),
})
)
.query(
async ({
input: { credentialsId, workspaceId, typebotId, blockId },
ctx: { user },
}) => {
const workspace = await prisma.workspace.findFirst({
where: { id: workspaceId },
select: {
members: {
select: {
userId: true,
},
},
typebots: {
where: {
id: typebotId,
},
select: {
groups: true,
},
},
credentials: {
where: {
id: credentialsId,
},
select: {
id: true,
data: true,
iv: true,
},
},
},
})

if (!workspace || isReadWorkspaceFobidden(workspace, user))
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No workspace found',
})

const credentials = workspace.credentials.at(0)

if (!credentials)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'No credentials found',
})

const typebot = workspace.typebots.at(0)

if (!typebot)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Typebot not found',
})

const block = typebotSchema._def.schema.shape.groups
.parse(workspace.typebots.at(0)?.groups)
.flatMap((group) => group.blocks)
.find((block) => block.id === blockId)

if (!block || block.type !== IntegrationBlockType.OPEN_AI)
throw new TRPCError({
code: 'NOT_FOUND',
message: 'OpenAI block not found',
})

const data = (await decrypt(
credentials.data,
credentials.iv
)) as OpenAICredentials['data']

const config = new Configuration({
apiKey: data.apiKey,
basePath: block.options.baseUrl,
baseOptions: {
headers: {
'api-key': data.apiKey,
},
},
defaultQueryParams: isNotEmpty(block.options.apiVersion)
? new URLSearchParams({
'api-version': block.options.apiVersion,
})
: undefined,
})

const openai = new OpenAIApi(config)

const response = await openai.listModels()

const modelsData = (await response.json()) as ResponseTypes['listModels']

return {
models: modelsData.data
.sort((a, b) => b.created - a.created)
.map((model) => model.id),
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { router } from '@/helpers/server/trpc'
import { listModels } from './listModels'

export const openAIRouter = router({
listModels,
})
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { Stack, useDisclosure } from '@chakra-ui/react'
import {
Accordion,
AccordionButton,
AccordionIcon,
AccordionItem,
AccordionPanel,
Stack,
useDisclosure,
Text,
} from '@chakra-ui/react'
import React from 'react'
import { CredentialsDropdown } from '@/features/credentials/components/CredentialsDropdown'
import {
ChatCompletionOpenAIOptions,
CreateImageOpenAIOptions,
defaultBaseUrl,
defaultChatCompletionOptions,
OpenAIBlock,
openAITasks,
Expand All @@ -13,15 +23,19 @@ import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
import { DropdownList } from '@/components/DropdownList'
import { OpenAIChatCompletionSettings } from './createChatCompletion/OpenAIChatCompletionSettings'
import { createId } from '@paralleldrive/cuid2'
import { TextInput } from '@/components/inputs'

type OpenAITask = (typeof openAITasks)[number]

type Props = {
options: OpenAIBlock['options']
block: OpenAIBlock
onOptionsChange: (options: OpenAIBlock['options']) => void
}

export const OpenAISettings = ({ options, onOptionsChange }: Props) => {
export const OpenAISettings = ({
block: { options, id },
onOptionsChange,
}: Props) => {
const { workspace } = useWorkspace()
const { isOpen, onOpen, onClose } = useDisclosure()

Expand All @@ -44,6 +58,20 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => {
}
}

const updateBaseUrl = (baseUrl: string) => {
onOptionsChange({
...options,
baseUrl,
})
}

const updateApiVersion = (apiVersion: string) => {
onOptionsChange({
...options,
apiVersion,
})
}

return (
<Stack>
{workspace && (
Expand All @@ -56,22 +84,51 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => {
credentialsName="OpenAI account"
/>
)}
<OpenAICredentialsModal
isOpen={isOpen}
onClose={onClose}
onNewCredentials={updateCredentialsId}
/>
<DropdownList
currentItem={options.task}
items={openAITasks.slice(0, -1)}
onItemSelect={updateTask}
placeholder="Select task"
/>
{options.task && (
<OpenAITaskSettings
options={options}
onOptionsChange={onOptionsChange}
/>
{options.credentialsId && (
<>
<Accordion allowToggle>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Customize provider
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel as={Stack} spacing={4}>
<TextInput
label="Base URL"
defaultValue={options.baseUrl}
onChange={updateBaseUrl}
/>
{options.baseUrl !== defaultBaseUrl && (
<TextInput
label="API version"
defaultValue={options.apiVersion}
onChange={updateApiVersion}
/>
)}
</AccordionPanel>
</AccordionItem>
</Accordion>
<OpenAICredentialsModal
isOpen={isOpen}
onClose={onClose}
onNewCredentials={updateCredentialsId}
/>
<DropdownList
currentItem={options.task}
items={openAITasks.slice(0, -1)}
onItemSelect={updateTask}
placeholder="Select task"
/>
{options.task && (
<OpenAITaskSettings
blockId={id}
options={options}
onOptionsChange={onOptionsChange}
/>
)}
</>
)}
</Stack>
)
Expand All @@ -80,14 +137,17 @@ export const OpenAISettings = ({ options, onOptionsChange }: Props) => {
const OpenAITaskSettings = ({
options,
onOptionsChange,
blockId,
}: {
options: ChatCompletionOpenAIOptions | CreateImageOpenAIOptions
blockId: string
onOptionsChange: (options: OpenAIBlock['options']) => void
}) => {
switch (options.task) {
case 'Create chat completion': {
return (
<OpenAIChatCompletionSettings
blockId={blockId}
options={options}
onOptionsChange={onOptionsChange}
/>
Expand Down
Loading

4 comments on commit 27a5f4e

@vercel
Copy link

@vercel vercel bot commented on 27a5f4e Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

viewer-v2 – ./apps/viewer

bii.bj
1stop.au
wasap.nl
x.cr8.ai
yobot.me
klujo.com
me.cr8.ai
sifuim.co
secretespiao.online
start.belenmotz.com
support.wawplus.com
survey1.digienge.io
surveys.essiell.com
test.botscientis.us
test.getreview.help
test.reventepro.com
typebot.stillio.app
typebot.stillio.com
vg.onewebcenter.com
wa.onewebcenter.com
web.draleticiah.com
whatsdigital.online
wordsandimagery.com
88584434.therpm.club
92109660.therpm.club
app.horadelucrar.com
assistent.m-vogel.de
ativandograna.online
bium.gratirabbit.com
bot.ansuraniphone.my
bot.barrettamario.it
bot.buenanoticia.fun
bot.conhecaojogo.com
bot.cotemeuplano.com
bot.gameincrivel.com
bot.gamesimples.club
bot.grupodojo.com.br
bot.jogoquelucra.com
bot.leadbooster.help
bot.mycompay.reviews
bot.socialcliques.me
cha.onewebcenter.com
chat.gnipharmahq.com
chat.hayurihijab.com
chat.jottagreens.com
chatbee.agfunnel.com
click.sevenoways.com
connect.growthguy.in
detetivepatricia.com
drapamela.gikpro.com
drgisellegarcia.site
forms.bonanza.design
hello.advergreen.com
infomakeracademy.com
kuiz.sistemniaga.com
leoborges-app.online
linspecteuremma.site
malayanboosterhq.com
viewer-v2-typebot-io.vercel.app
mdb.assessoria.rodrigo.progenbr.com
register.thailandmicespecialist.com
mdb.assessoria.desideri.progenbr.com
mdb.assessoria.fernanda.progenbr.com
mdb.assessoria.jbatista.progenbr.com
mdb.assessoria.mauricio.progenbr.com
mdb.evento.autocadastro.progenbr.com
form.shopmercedesbenzsouthorlando.com
mdb.evento.equipeinterna.progenbr.com
bot.studiotecnicoimmobiliaremerelli.it
mdb.assessoria.boaventura.progenbr.com
mdb.assessoria.jtrebesqui.progenbr.com
pesquisa.escolamodacomproposito.com.br
anamnese.clinicaramosodontologia.com.br
gabinete.baleia.formulario.progenbr.com
mdb.assessoria.carreirinha.progenbr.com
chrome-os-inquiry-system.itschromeos.com
mdb.assessoria.paulomarques.progenbr.com
viewer-v2-git-main-typebot-io.vercel.app
main-menu-for-itschromeos.itschromeos.com
mdb.assessoria.qrcode.ademir.progenbr.com
mdb.assessoria.qrcode.arthur.progenbr.com
mdb.assessoria.qrcode.danilo.progenbr.com
mdb.assessoria.qrcode.marcao.progenbr.com
mdb.assessoria.qrcode.marcio.progenbr.com
mdb.assessoria.qrcode.aloisio.progenbr.com
mdb.assessoria.qrcode.girotto.progenbr.com
mdb.assessoria.qrcode.marinho.progenbr.com
mdb.assessoria.qrcode.rodrigo.progenbr.com
mdb.assessoria.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.desideri.progenbr.com
mdb.assessoria.qrcode.fernanda.progenbr.com
mdb.assessoria.qrcode.jbatista.progenbr.com
mdb.assessoria.qrcode.mauricio.progenbr.com
mdb.assessoria.fernanda.regional.progenbr.com
mdb.assessoria.qrcode.boaventura.progenbr.com
mdb.assessoria.qrcode.jtrebesqui.progenbr.com
mdb.assessoria.qrcode.carreirinha.progenbr.com
mdb.assessoria.qrcode.paulomarques.progenbr.com
mdb.assessoria.qrcode.carlosalexandre.progenbr.com
mdb.assessoria.qrcode.fernanda.regional.progenbr.com

@vercel
Copy link

@vercel vercel bot commented on 27a5f4e Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

builder-v2 – ./apps/builder

builder-v2-typebot-io.vercel.app
builder-v2-git-main-typebot-io.vercel.app
app.typebot.io

@vercel
Copy link

@vercel vercel bot commented on 27a5f4e Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vercel
Copy link

@vercel vercel bot commented on 27a5f4e Sep 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

docs – ./apps/docs

docs-typebot-io.vercel.app
docs-git-main-typebot-io.vercel.app
docs.typebot.io

Please sign in to comment.