@@ -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),
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
import { router } from '@/helpers/server/trpc'
|
||||
import { listModels } from './listModels'
|
||||
|
||||
export const openAIRouter = router({
|
||||
listModels,
|
||||
})
|
||||
@@ -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,
|
||||
@@ -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()
|
||||
|
||||
@@ -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 && (
|
||||
@@ -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>
|
||||
)
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
import { useTypebot } from '@/features/editor/providers/TypebotProvider'
|
||||
import { useWorkspace } from '@/features/workspace/WorkspaceProvider'
|
||||
import { trpc } from '@/lib/trpc'
|
||||
|
||||
type Props = {
|
||||
credentialsId: string
|
||||
blockId: string
|
||||
defaultValue: string
|
||||
onChange: (model: string | undefined) => void
|
||||
}
|
||||
|
||||
export const ModelsDropdown = ({
|
||||
defaultValue,
|
||||
onChange,
|
||||
credentialsId,
|
||||
blockId,
|
||||
}: Props) => {
|
||||
const { typebot } = useTypebot()
|
||||
const { workspace } = useWorkspace()
|
||||
|
||||
const { data } = trpc.openAI.listModels.useQuery(
|
||||
{
|
||||
credentialsId,
|
||||
blockId,
|
||||
typebotId: typebot?.id as string,
|
||||
workspaceId: workspace?.id as string,
|
||||
},
|
||||
{
|
||||
enabled: !!typebot && !!workspace,
|
||||
}
|
||||
)
|
||||
|
||||
return (
|
||||
<Select
|
||||
items={data?.models}
|
||||
selectedItem={defaultValue}
|
||||
onSelect={onChange}
|
||||
placeholder="Select a model"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { TableList } from '@/components/TableList'
|
||||
import {
|
||||
chatCompletionModels,
|
||||
ChatCompletionOpenAIOptions,
|
||||
deprecatedCompletionModels,
|
||||
} from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { ChatCompletionOpenAIOptions } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
import { ChatCompletionMessageItem } from './ChatCompletionMessageItem'
|
||||
import {
|
||||
Accordion,
|
||||
@@ -17,24 +13,23 @@ import {
|
||||
import { TextLink } from '@/components/TextLink'
|
||||
import { ChatCompletionResponseItem } from './ChatCompletionResponseItem'
|
||||
import { NumberInput } from '@/components/inputs'
|
||||
import { Select } from '@/components/inputs/Select'
|
||||
import { ModelsDropdown } from './ModelsDropdown'
|
||||
|
||||
const apiReferenceUrl =
|
||||
'https://platform.openai.com/docs/api-reference/chat/create'
|
||||
|
||||
type Props = {
|
||||
blockId: string
|
||||
options: ChatCompletionOpenAIOptions
|
||||
onOptionsChange: (options: ChatCompletionOpenAIOptions) => void
|
||||
}
|
||||
|
||||
export const OpenAIChatCompletionSettings = ({
|
||||
blockId,
|
||||
options,
|
||||
onOptionsChange,
|
||||
}: Props) => {
|
||||
const updateModel = (
|
||||
_: string | undefined,
|
||||
model: (typeof chatCompletionModels)[number] | undefined
|
||||
) => {
|
||||
const updateModel = (model: string | undefined) => {
|
||||
if (!model) return
|
||||
onOptionsChange({
|
||||
...options,
|
||||
@@ -79,68 +74,71 @@ export const OpenAIChatCompletionSettings = ({
|
||||
</TextLink>{' '}
|
||||
to better understand the available options.
|
||||
</Text>
|
||||
<Select
|
||||
selectedItem={options.model}
|
||||
items={chatCompletionModels.filter(
|
||||
(model) => deprecatedCompletionModels.indexOf(model) === -1
|
||||
)}
|
||||
onSelect={updateModel}
|
||||
/>
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Messages
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
{options.credentialsId && (
|
||||
<>
|
||||
<ModelsDropdown
|
||||
credentialsId={options.credentialsId}
|
||||
defaultValue={options.model}
|
||||
onChange={updateModel}
|
||||
blockId={blockId}
|
||||
/>
|
||||
<Accordion allowMultiple>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Messages
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.messages}
|
||||
Item={ChatCompletionMessageItem}
|
||||
onItemsChange={updateMessages}
|
||||
isOrdered
|
||||
addLabel="Add message"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Advanced settings
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<NumberInput
|
||||
label="Temperature"
|
||||
placeholder="1"
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.1}
|
||||
defaultValue={options.advancedSettings?.temperature}
|
||||
onValueChange={updateTemperature}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Save answer
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.responseMapping}
|
||||
Item={ChatCompletionResponseItem}
|
||||
onItemsChange={updateResponseMapping}
|
||||
newItemDefaultProps={{ valueToExtract: 'Message content' }}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.messages}
|
||||
Item={ChatCompletionMessageItem}
|
||||
onItemsChange={updateMessages}
|
||||
isOrdered
|
||||
addLabel="Add message"
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Advanced settings
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel>
|
||||
<NumberInput
|
||||
label="Temperature"
|
||||
placeholder="1"
|
||||
max={2}
|
||||
min={0}
|
||||
step={0.1}
|
||||
defaultValue={options.advancedSettings?.temperature}
|
||||
onValueChange={updateTemperature}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
<AccordionItem>
|
||||
<AccordionButton>
|
||||
<Text w="full" textAlign="left">
|
||||
Save answer
|
||||
</Text>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
<AccordionPanel pt="4">
|
||||
<TableList
|
||||
initialItems={options.responseMapping}
|
||||
Item={ChatCompletionResponseItem}
|
||||
onItemsChange={updateResponseMapping}
|
||||
newItemDefaultProps={{ valueToExtract: 'Message content' }}
|
||||
/>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createTypebots } from '@typebot.io/lib/playwright/databaseActions'
|
||||
import { createId } from '@paralleldrive/cuid2'
|
||||
import { IntegrationBlockType } from '@typebot.io/schemas'
|
||||
import { parseDefaultGroupWithBlock } from '@typebot.io/lib/playwright/databaseHelpers'
|
||||
import { defaultBaseUrl } from '@typebot.io/schemas/features/blocks/integrations/openai'
|
||||
|
||||
const typebotId = createId()
|
||||
|
||||
@@ -12,7 +13,9 @@ test('should be configurable', async ({ page }) => {
|
||||
id: typebotId,
|
||||
...parseDefaultGroupWithBlock({
|
||||
type: IntegrationBlockType.OPEN_AI,
|
||||
options: {},
|
||||
options: {
|
||||
baseUrl: defaultBaseUrl,
|
||||
},
|
||||
}),
|
||||
},
|
||||
])
|
||||
|
||||
@@ -69,8 +69,7 @@ export const SettingsPopoverContent = ({ onExpandClick, ...props }: Props) => {
|
||||
<PopoverContent onMouseDown={handleMouseDown} pos="relative">
|
||||
<PopoverArrow bgColor={arrowColor} />
|
||||
<PopoverBody
|
||||
pt="3"
|
||||
pb="6"
|
||||
py="3"
|
||||
overflowY="scroll"
|
||||
maxH="400px"
|
||||
ref={ref}
|
||||
@@ -305,12 +304,7 @@ export const BlockSettings = ({
|
||||
)
|
||||
}
|
||||
case IntegrationBlockType.OPEN_AI: {
|
||||
return (
|
||||
<OpenAISettings
|
||||
options={block.options}
|
||||
onOptionsChange={updateOptions}
|
||||
/>
|
||||
)
|
||||
return <OpenAISettings block={block} onOptionsChange={updateOptions} />
|
||||
}
|
||||
case IntegrationBlockType.PIXEL: {
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user