2
0

(openai) Add Messages sequence type

To make it easy to just plug a sequence of user / assistant messages to Chat completion task

Closes #387
This commit is contained in:
Baptiste Arnaud
2023-03-13 16:28:08 +01:00
parent 48db171c1b
commit c4db2f42a6
27 changed files with 468 additions and 153 deletions

View File

@@ -24,7 +24,7 @@ import { useOutsideClick } from '@/hooks/useOutsideClick'
import { useParentModal } from '@/features/graph/providers/ParentModalProvider' import { useParentModal } from '@/features/graph/providers/ParentModalProvider'
type Props = { type Props = {
initialVariableId?: string initialVariableId: string | undefined
autoFocus?: boolean autoFocus?: boolean
onSelectVariable: ( onSelectVariable: (
variable: Pick<Variable, 'id' | 'name'> | undefined variable: Pick<Variable, 'id' | 'name'> | undefined

View File

@@ -164,6 +164,7 @@ const TextBubbleEditorContent = ({
zIndex={10} zIndex={10}
> >
<VariableSearchInput <VariableSearchInput
initialVariableId={undefined}
onSelectVariable={handleVariableSelected} onSelectVariable={handleVariableSelected}
placeholder="Search for a variable" placeholder="Search for a variable"
autoFocus autoFocus

View File

@@ -1,6 +1,13 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
export const DateNodeContent = () => ( type Props = {
variableId?: string
}
export const DateNodeContent = ({ variableId }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>Pick a date...</Text> <Text color={'gray.500'}>Pick a date...</Text>
) )

View File

@@ -1,11 +1,16 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { EmailInputBlock } from 'models' import { EmailInputBlock } from 'models'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
type Props = { type Props = {
variableId?: string
placeholder: EmailInputBlock['options']['labels']['placeholder'] placeholder: EmailInputBlock['options']['labels']['placeholder']
} }
export const EmailInputNodeContent = ({ placeholder }: Props) => ( export const EmailInputNodeContent = ({ variableId, placeholder }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text> <Text color={'gray.500'}>{placeholder}</Text>
) )

View File

@@ -1,3 +1,4 @@
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { FileInputOptions } from 'models' import { FileInputOptions } from 'models'
@@ -5,7 +6,12 @@ type Props = {
options: FileInputOptions options: FileInputOptions
} }
export const FileInputContent = ({ options: { isMultipleAllowed } }: Props) => ( export const FileInputContent = ({
options: { isMultipleAllowed, variableId },
}: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text noOfLines={1} pr="6"> <Text noOfLines={1} pr="6">
Collect {isMultipleAllowed ? 'files' : 'file'} Collect {isMultipleAllowed ? 'files' : 'file'}
</Text> </Text>

View File

@@ -1,11 +1,16 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { NumberInputBlock } from 'models' import { NumberInputBlock } from 'models'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
type Props = { type Props = {
variableId?: string
placeholder: NumberInputBlock['options']['labels']['placeholder'] placeholder: NumberInputBlock['options']['labels']['placeholder']
} }
export const NumberNodeContent = ({ placeholder }: Props) => ( export const NumberNodeContent = ({ variableId, placeholder }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text> <Text color={'gray.500'}>{placeholder}</Text>
) )

View File

@@ -1,11 +1,16 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { PhoneNumberInputOptions } from 'models' import { PhoneNumberInputOptions } from 'models'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
type Props = { type Props = {
variableId?: string
placeholder: PhoneNumberInputOptions['labels']['placeholder'] placeholder: PhoneNumberInputOptions['labels']['placeholder']
} }
export const PhoneNodeContent = ({ placeholder }: Props) => ( export const PhoneNodeContent = ({ variableId, placeholder }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text> <Text color={'gray.500'}>{placeholder}</Text>
) )

View File

@@ -1,11 +1,16 @@
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { RatingInputBlock } from 'models' import { RatingInputBlock } from 'models'
type Props = { type Props = {
variableId?: string
block: RatingInputBlock block: RatingInputBlock
} }
export const RatingInputContent = ({ block }: Props) => ( export const RatingInputContent = ({ variableId, block }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text noOfLines={1} pr="6"> <Text noOfLines={1} pr="6">
Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '} Rate from {block.options.buttonType === 'Icons' ? 1 : 0} to{' '}
{block.options.length} {block.options.length}

View File

@@ -33,9 +33,9 @@ test('options should work', async ({ page }) => {
await page.click('text=Preview') await page.click('text=Preview')
await expect(page.locator(`text=Send`)).toBeHidden() await expect(page.locator(`text=Send`)).toBeHidden()
await page.locator(`text=8`).click() await page.getByRole('button', { name: '8' }).click()
await page.locator(`text=Send`).click() await page.locator(`text=Send`).click()
await expect(page.locator(`text=8`)).toBeVisible() await expect(page.getByTestId('guest-bubble')).toHaveText('8')
await page.click('text=Rate from 0 to 10') await page.click('text=Rate from 0 to 10')
await page.click('text="10"') await page.click('text="10"')
await page.click('text="5"') await page.click('text="5"')
@@ -47,10 +47,8 @@ test('options should work', async ({ page }) => {
await page.fill('[placeholder="Not likely at all"]', 'Not likely at all') await page.fill('[placeholder="Not likely at all"]', 'Not likely at all')
await page.fill('[placeholder="Extremely likely"]', 'Extremely likely') await page.fill('[placeholder="Extremely likely"]', 'Extremely likely')
await page.click('text="Restart"') await page.click('text="Restart"')
await expect(page.locator(`text=8`)).toBeHidden()
await expect(page.locator(`text=4`)).toBeHidden()
await expect(page.locator(`text=Not likely at all`)).toBeVisible() await expect(page.locator(`text=Not likely at all`)).toBeVisible()
await expect(page.locator(`text=Extremely likely`)).toBeVisible() await expect(page.locator(`text=Extremely likely`)).toBeVisible()
await page.locator(`svg >> nth=4`).click() await page.locator('typebot-standard').locator(`svg >> nth=4`).click()
await expect(page.locator(`text=5`)).toBeVisible() await expect(page.locator('typebot-standard').locator(`text=5`)).toBeVisible()
}) })

View File

@@ -1,14 +1,29 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { TextInputOptions } from 'models' import { TextInputOptions } from 'models'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
type Props = { type Props = {
placeholder: TextInputOptions['labels']['placeholder'] placeholder: TextInputOptions['labels']['placeholder']
isLong: TextInputOptions['isLong'] isLong: TextInputOptions['isLong']
variableId?: string
} }
export const TextInputNodeContent = ({ placeholder, isLong }: Props) => ( export const TextInputNodeContent = ({
placeholder,
isLong,
variableId,
}: Props) => {
if (variableId)
return (
<WithVariableContent
variableId={variableId}
h={isLong ? '100px' : 'auto'}
/>
)
return (
<Text color={'gray.500'} h={isLong ? '100px' : 'auto'}> <Text color={'gray.500'} h={isLong ? '100px' : 'auto'}>
{placeholder} {placeholder}
</Text> </Text>
) )
}

View File

@@ -1,11 +1,16 @@
import React from 'react' import React from 'react'
import { Text } from '@chakra-ui/react' import { Text } from '@chakra-ui/react'
import { UrlInputOptions } from 'models' import { UrlInputOptions } from 'models'
import { WithVariableContent } from '@/features/graph/components/Nodes/BlockNode/BlockNodeContent/WithVariableContent'
type Props = { type Props = {
variableId?: string
placeholder: UrlInputOptions['labels']['placeholder'] placeholder: UrlInputOptions['labels']['placeholder']
} }
export const UrlNodeContent = ({ placeholder }: Props) => ( export const UrlNodeContent = ({ placeholder, variableId }: Props) =>
variableId ? (
<WithVariableContent variableId={variableId} />
) : (
<Text color={'gray.500'}>{placeholder}</Text> <Text color={'gray.500'}>{placeholder}</Text>
) )

View File

@@ -1,36 +1,88 @@
import { DropdownList } from '@/components/DropdownList' import { DropdownList } from '@/components/DropdownList'
import { TextInput } from '@/components/inputs' import { TextInput } from '@/components/inputs'
import { VariableSearchInput } from '@/components/inputs/VariableSearchInput'
import { TableListItemProps } from '@/components/TableList' import { TableListItemProps } from '@/components/TableList'
import { Stack } from '@chakra-ui/react' import { Stack } from '@chakra-ui/react'
import { Variable } from 'models'
import { import {
chatCompletionMessageCustomRoles,
chatCompletionMessageRoles, chatCompletionMessageRoles,
ChatCompletionOpenAIOptions, ChatCompletionOpenAIOptions,
} from 'models/features/blocks/integrations/openai' } from 'models/features/blocks/integrations/openai'
type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]> type Props = TableListItemProps<ChatCompletionOpenAIOptions['messages'][number]>
const roles = [
...chatCompletionMessageCustomRoles,
...chatCompletionMessageRoles,
]
export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => { export const ChatCompletionMessageItem = ({ item, onItemChange }: Props) => {
const changeRole = (role: (typeof chatCompletionMessageRoles)[number]) => { const changeRole = (role: (typeof roles)[number]) => {
onItemChange({ ...item, role }) onItemChange({
...item,
role,
content: undefined,
})
} }
const changeContent = (content: string) => { const changeSingleMessageContent = (content: string) => {
if (item.role === 'Messages sequence ✨') return
onItemChange({ ...item, content }) onItemChange({ ...item, content })
} }
const changeAssistantVariableId = (
variable: Pick<Variable, 'id'> | undefined
) => {
if (item.role !== 'Messages sequence ✨') return
onItemChange({
...item,
content: {
...item.content,
assistantMessagesVariableId: variable?.id,
},
})
}
const changeUserVariableId = (variable: Pick<Variable, 'id'> | undefined) => {
if (item.role !== 'Messages sequence ✨') return
onItemChange({
...item,
content: {
...item.content,
userMessagesVariableId: variable?.id,
},
})
}
return ( return (
<Stack p="4" rounded="md" flex="1" borderWidth="1px"> <Stack p="4" rounded="md" flex="1" borderWidth="1px">
<DropdownList <DropdownList
currentItem={item.role} currentItem={item.role}
items={chatCompletionMessageRoles} items={roles}
onItemSelect={changeRole} onItemSelect={changeRole}
placeholder="Select role" placeholder="Select type"
/> />
{item.role === 'Messages sequence ✨' ? (
<>
<VariableSearchInput
initialVariableId={item.content?.assistantMessagesVariableId}
onSelectVariable={changeAssistantVariableId}
placeholder="Assistant messages variable"
/>
<VariableSearchInput
initialVariableId={item.content?.userMessagesVariableId}
onSelectVariable={changeUserVariableId}
placeholder="User messages variable"
/>
</>
) : (
<TextInput <TextInput
defaultValue={item.content} defaultValue={item.content}
onChange={changeContent} onChange={changeSingleMessageContent}
placeholder="Content" placeholder="Content"
/> />
)}
</Stack> </Stack>
) )
} }

View File

@@ -61,11 +61,6 @@ export const OpenAIChatCompletionSettings = ({
</TextLink>{' '} </TextLink>{' '}
to better understand the available options. to better understand the available options.
</Text> </Text>
<DropdownList
currentItem={options.model}
items={chatCompletionModels}
onItemSelect={updateModel}
/>
<Accordion allowToggle allowMultiple> <Accordion allowToggle allowMultiple>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
@@ -85,6 +80,21 @@ export const OpenAIChatCompletionSettings = ({
/> />
</AccordionPanel> </AccordionPanel>
</AccordionItem> </AccordionItem>
<AccordionItem>
<AccordionButton>
<Text w="full" textAlign="left">
Advanced settings
</Text>
<AccordionIcon />
</AccordionButton>
<AccordionPanel>
<DropdownList
currentItem={options.model}
items={chatCompletionModels}
onItemSelect={updateModel}
/>
</AccordionPanel>
</AccordionItem>
<AccordionItem> <AccordionItem>
<AccordionButton> <AccordionButton>
<Text w="full" textAlign="left"> <Text w="full" textAlign="left">

View File

@@ -27,11 +27,11 @@ test('should be configurable', async ({ page }) => {
await page.getByRole('button', { name: 'Select task' }).click() await page.getByRole('button', { name: 'Select task' }).click()
await page.getByRole('menuitem', { name: 'Create chat completion' }).click() await page.getByRole('menuitem', { name: 'Create chat completion' }).click()
await page.getByRole('button', { name: 'Messages' }).click() await page.getByRole('button', { name: 'Messages' }).click()
await page.getByRole('button', { name: 'Select role' }).click() await page.getByRole('button', { name: 'Select type' }).click()
await page.getByRole('menuitem', { name: 'system' }).click() await page.getByRole('menuitem', { name: 'system' }).click()
await page.getByPlaceholder('Content').first().fill('You are a helpful bot') await page.getByPlaceholder('Content').first().fill('You are a helpful bot')
await page.getByRole('button', { name: 'Add message' }).nth(1).click() await page.getByRole('button', { name: 'Add message' }).nth(1).click()
await page.getByRole('button', { name: 'Select role' }).click() await page.getByRole('button', { name: 'Select type' }).click()
await page.getByRole('menuitem', { name: 'assistant' }).click() await page.getByRole('menuitem', { name: 'assistant' }).click()
await page.getByPlaceholder('Content').nth(1).fill('Hi there!') await page.getByPlaceholder('Content').nth(1).fill('Hi there!')
await page.getByRole('button', { name: 'Save answer' }).click() await page.getByRole('button', { name: 'Save answer' }).click()

View File

@@ -24,7 +24,6 @@ import { WebhookContent } from '@/features/blocks/integrations/webhook'
import { ChatwootBlockNodeLabel } from '@/features/blocks/integrations/chatwoot' import { ChatwootBlockNodeLabel } from '@/features/blocks/integrations/chatwoot'
import { RedirectNodeContent } from '@/features/blocks/logic/redirect' import { RedirectNodeContent } from '@/features/blocks/logic/redirect'
import { PabblyConnectContent } from '@/features/blocks/integrations/pabbly' import { PabblyConnectContent } from '@/features/blocks/integrations/pabbly'
import { WithVariableContent } from './WithVariableContent'
import { PaymentInputContent } from '@/features/blocks/inputs/payment' import { PaymentInputContent } from '@/features/blocks/inputs/payment'
import { RatingInputContent } from '@/features/blocks/inputs/rating' import { RatingInputContent } from '@/features/blocks/inputs/rating'
import { FileInputContent } from '@/features/blocks/inputs/fileUpload' import { FileInputContent } from '@/features/blocks/inputs/fileUpload'
@@ -33,7 +32,6 @@ import { GoogleSheetsNodeContent } from '@/features/blocks/integrations/googleSh
import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent' import { GoogleAnalyticsNodeContent } from '@/features/blocks/integrations/googleAnalytics/components/GoogleAnalyticsNodeContent'
import { ZapierContent } from '@/features/blocks/integrations/zapier' import { ZapierContent } from '@/features/blocks/integrations/zapier'
import { SendEmailContent } from '@/features/blocks/integrations/sendEmail' import { SendEmailContent } from '@/features/blocks/integrations/sendEmail'
import { isInputBlock, isChoiceInput } from 'utils'
import { MakeComContent } from '@/features/blocks/integrations/makeCom' import { MakeComContent } from '@/features/blocks/integrations/makeCom'
import { AudioBubbleNode } from '@/features/blocks/bubbles/audio' import { AudioBubbleNode } from '@/features/blocks/bubbles/audio'
import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent' import { WaitNodeContent } from '@/features/blocks/logic/wait/components/WaitNodeContent'
@@ -47,14 +45,6 @@ type Props = {
indices: BlockIndices indices: BlockIndices
} }
export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => { export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
if (
isInputBlock(block) &&
!isChoiceInput(block) &&
block.options.variableId
) {
return <WithVariableContent block={block} />
}
switch (block.type) { switch (block.type) {
case BubbleBlockType.TEXT: { case BubbleBlockType.TEXT: {
return <TextBubbleContent block={block} /> return <TextBubbleContent block={block} />
@@ -74,6 +64,7 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
case InputBlockType.TEXT: { case InputBlockType.TEXT: {
return ( return (
<TextInputNodeContent <TextInputNodeContent
variableId={block.options.variableId}
placeholder={block.options.labels.placeholder} placeholder={block.options.labels.placeholder}
isLong={block.options.isLong} isLong={block.options.isLong}
/> />
@@ -81,31 +72,52 @@ export const BlockNodeContent = ({ block, indices }: Props): JSX.Element => {
} }
case InputBlockType.NUMBER: { case InputBlockType.NUMBER: {
return ( return (
<NumberNodeContent placeholder={block.options.labels.placeholder} /> <NumberNodeContent
placeholder={block.options.labels.placeholder}
variableId={block.options.variableId}
/>
) )
} }
case InputBlockType.EMAIL: { case InputBlockType.EMAIL: {
return ( return (
<EmailInputNodeContent placeholder={block.options.labels.placeholder} /> <EmailInputNodeContent
placeholder={block.options.labels.placeholder}
variableId={block.options.variableId}
/>
) )
} }
case InputBlockType.URL: { case InputBlockType.URL: {
return <UrlNodeContent placeholder={block.options.labels.placeholder} /> return (
<UrlNodeContent
placeholder={block.options.labels.placeholder}
variableId={block.options.variableId}
/>
)
} }
case InputBlockType.CHOICE: { case InputBlockType.CHOICE: {
return <ButtonsBlockNode block={block} indices={indices} /> return <ButtonsBlockNode block={block} indices={indices} />
} }
case InputBlockType.PHONE: { case InputBlockType.PHONE: {
return <PhoneNodeContent placeholder={block.options.labels.placeholder} /> return (
<PhoneNodeContent
placeholder={block.options.labels.placeholder}
variableId={block.options.variableId}
/>
)
} }
case InputBlockType.DATE: { case InputBlockType.DATE: {
return <DateNodeContent /> return <DateNodeContent variableId={block.options.variableId} />
} }
case InputBlockType.PAYMENT: { case InputBlockType.PAYMENT: {
return <PaymentInputContent block={block} /> return <PaymentInputContent block={block} />
} }
case InputBlockType.RATING: { case InputBlockType.RATING: {
return <RatingInputContent block={block} /> return (
<RatingInputContent
block={block}
variableId={block.options.variableId}
/>
)
} }
case InputBlockType.FILE: { case InputBlockType.FILE: {
return <FileInputContent options={block.options} /> return <FileInputContent options={block.options} />

View File

@@ -1,21 +1,18 @@
import { InputBlock } from 'models' import { chakra, Text, TextProps } from '@chakra-ui/react'
import { chakra, Text } from '@chakra-ui/react'
import React from 'react' import React from 'react'
import { useTypebot } from '@/features/editor' import { useTypebot } from '@/features/editor'
import { byId } from 'utils' import { byId } from 'utils'
type Props = { type Props = {
block: InputBlock variableId: string
} } & TextProps
export const WithVariableContent = ({ block }: Props) => { export const WithVariableContent = ({ variableId, ...props }: Props) => {
const { typebot } = useTypebot() const { typebot } = useTypebot()
const variableName = typebot?.variables.find( const variableName = typebot?.variables.find(byId(variableId))?.name
byId(block.options.variableId)
)?.name
return ( return (
<Text w="calc(100% - 25px)"> <Text w="calc(100% - 25px)" {...props}>
Collect{' '} Collect{' '}
<chakra.span <chakra.span
bgColor="orange.400" bgColor="orange.400"

View File

@@ -233,7 +233,7 @@ const NonMemoizedDraggableGroupNode = ({
display: 'block', display: 'block',
position: 'absolute', position: 'absolute',
top: '10px', top: '10px',
width: '100px', width: '50px',
} }
: undefined : undefined
} }

View File

@@ -48,6 +48,7 @@ export const VariablesButton = ({ onSelectVariable, ...props }: Props) => {
<Portal containerRef={parentModalRef}> <Portal containerRef={parentModalRef}>
<PopoverContent w="full" ref={popoverRef}> <PopoverContent w="full" ref={popoverRef}>
<VariableSearchInput <VariableSearchInput
initialVariableId={undefined}
onSelectVariable={(variable) => { onSelectVariable={(variable) => {
onClose() onClose()
if (variable) onSelectVariable(variable) if (variable) onSelectVariable(variable)

View File

@@ -3,7 +3,15 @@
With the OpenAI block, you can create a chat completion based on your user queries and display the answer back to your typebot. With the OpenAI block, you can create a chat completion based on your user queries and display the answer back to your typebot.
<img <img
src="/img/blocks/integrations/openai.png" src="/img/blocks/integrations/openai/overview.png"
width="600" width="600"
alt="OpenAI block" alt="OpenAI block"
/> />
This integration comes with a convenient message type called **Messages sequence ✨**. It allows you to directly pass a sequence of saved assistant / user messages:
<img
src="/img/blocks/integrations/openai/messages-sequence.png"
width="600"
alt="OpenAI messages sequence"
/>

View File

@@ -2957,6 +2957,8 @@
"messages": { "messages": {
"type": "array", "type": "array",
"items": { "items": {
"anyOf": [
{
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
@@ -2978,6 +2980,39 @@
"id" "id"
], ],
"additionalProperties": false "additionalProperties": false
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"role": {
"type": "string",
"enum": [
"Messages sequence ✨"
]
},
"content": {
"type": "object",
"properties": {
"assistantMessagesVariableId": {
"type": "string"
},
"userMessagesVariableId": {
"type": "string"
}
},
"additionalProperties": false
}
},
"required": [
"id",
"role"
],
"additionalProperties": false
}
]
} }
}, },
"responseMapping": { "responseMapping": {

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

View File

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

View File

@@ -1,7 +1,14 @@
import { ExecuteIntegrationResponse } from '@/features/chat/types' import { ExecuteIntegrationResponse } from '@/features/chat/types'
import { saveErrorLog } from '@/features/logs/api'
import { transformStringVariablesToList } from '@/features/variables/transformVariablesToList'
import { parseVariables, updateVariables } from '@/features/variables/utils' import { parseVariables, updateVariables } from '@/features/variables/utils'
import prisma from '@/lib/prisma' import prisma from '@/lib/prisma'
import { SessionState, VariableWithUnknowValue } from 'models' import {
SessionState,
Variable,
VariableWithUnknowValue,
VariableWithValue,
} from 'models'
import { import {
ChatCompletionOpenAIOptions, ChatCompletionOpenAIOptions,
OpenAICredentials, OpenAICredentials,
@@ -20,6 +27,7 @@ export const createChatCompletionOpenAI = async (
const { const {
typebot: { variables }, typebot: { variables },
} = state } = state
let newSessionState = state
if (!options.credentialsId) return { outgoingEdgeId } if (!options.credentialsId) return { outgoingEdgeId }
const credentials = await prisma.credentials.findUnique({ const credentials = await prisma.credentials.findUnique({
where: { where: {
@@ -34,24 +42,23 @@ export const createChatCompletionOpenAI = async (
const configuration = new Configuration({ const configuration = new Configuration({
apiKey, apiKey,
}) })
const { variablesTransformedToList, messages } = parseMessages(variables)(
options.messages
)
if (variablesTransformedToList.length > 0)
newSessionState = await updateVariables(state)(variablesTransformedToList)
const openai = new OpenAIApi(configuration) const openai = new OpenAIApi(configuration)
try {
const { const {
data: { choices, usage }, data: { choices, usage },
} = await openai.createChatCompletion({ } = await openai.createChatCompletion({
model: options.model, model: options.model,
messages: options.messages messages,
.map((message) => ({
role: message.role,
content: parseVariables(variables)(message.content),
}))
.filter(
(message) => isNotEmpty(message.role) && isNotEmpty(message.content)
) as ChatCompletionRequestMessage[],
}) })
const messageContent = choices[0].message?.content const messageContent = choices[0].message?.content
const totalTokens = usage?.total_tokens const totalTokens = usage?.total_tokens
if (!messageContent) { if (!messageContent) {
return { outgoingEdgeId } return { outgoingEdgeId, newSessionState }
} }
const newVariables = options.responseMapping.reduce< const newVariables = options.responseMapping.reduce<
VariableWithUnknowValue[] VariableWithUnknowValue[]
@@ -61,7 +68,9 @@ export const createChatCompletionOpenAI = async (
if (mapping.valueToExtract === 'Message content') { if (mapping.valueToExtract === 'Message content') {
newVariables.push({ newVariables.push({
...existingVariable, ...existingVariable,
value: messageContent, value: Array.isArray(existingVariable.value)
? existingVariable.value.concat(messageContent)
: messageContent,
}) })
} }
if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) { if (mapping.valueToExtract === 'Total tokens' && isDefined(totalTokens)) {
@@ -73,7 +82,7 @@ export const createChatCompletionOpenAI = async (
return newVariables return newVariables
}, []) }, [])
if (newVariables.length > 0) { if (newVariables.length > 0) {
const newSessionState = await updateVariables(state)(newVariables) newSessionState = await updateVariables(newSessionState)(newVariables)
return { return {
outgoingEdgeId, outgoingEdgeId,
newSessionState, newSessionState,
@@ -81,5 +90,99 @@ export const createChatCompletionOpenAI = async (
} }
return { return {
outgoingEdgeId, outgoingEdgeId,
newSessionState,
}
} catch (err) {
const log = {
status: 'error',
description: 'OpenAI block returned error',
details: JSON.stringify(err, null, 2).substring(0, 1000),
}
state.result &&
(await saveErrorLog({
resultId: state.result.id,
message: log.description,
details: log.details,
}))
return {
outgoingEdgeId,
logs: [log],
newSessionState,
}
}
}
const parseMessages =
(variables: Variable[]) =>
(
messages: ChatCompletionOpenAIOptions['messages']
): {
variablesTransformedToList: VariableWithValue[]
messages: ChatCompletionRequestMessage[]
} => {
const variablesTransformedToList: VariableWithValue[] = []
const parsedMessages = messages
.flatMap((message) => {
if (!message.role) return
if (message.role === 'Messages sequence ✨') {
if (
!message.content?.assistantMessagesVariableId ||
!message.content?.userMessagesVariableId
)
return
variablesTransformedToList.push(
...transformStringVariablesToList(variables)([
message.content.assistantMessagesVariableId,
message.content.userMessagesVariableId,
])
)
const updatedVariables = variables.map((variable) => {
const variableTransformedToList = variablesTransformedToList.find(
byId(variable.id)
)
if (variableTransformedToList) return variableTransformedToList
return variable
})
const userMessages = (updatedVariables.find(
(variable) =>
variable.id === message.content?.userMessagesVariableId
)?.value ?? []) as string[]
const assistantMessages = (updatedVariables.find(
(variable) =>
variable.id === message.content?.assistantMessagesVariableId
)?.value ?? []) as string[]
if (userMessages.length > assistantMessages.length)
return userMessages.flatMap((userMessage, index) => [
{
role: 'user',
content: userMessage,
},
{ role: 'assistant', content: assistantMessages[index] },
]) satisfies ChatCompletionRequestMessage[]
else {
return assistantMessages.flatMap((assistantMessage, index) => [
{ role: 'assistant', content: assistantMessage },
{
role: 'user',
content: userMessages[index],
},
]) satisfies ChatCompletionRequestMessage[]
}
}
return {
role: message.role,
content: parseVariables(variables)(message.content),
} satisfies ChatCompletionRequestMessage
})
.filter(
(message) => isNotEmpty(message?.role) && isNotEmpty(message?.content)
) as ChatCompletionRequestMessage[]
return {
variablesTransformedToList,
messages: parsedMessages,
} }
} }

View File

@@ -103,7 +103,12 @@ const saveVariableValueIfAny =
if (!foundVariable) return state if (!foundVariable) return state
const newSessionState = await updateVariables(state)([ const newSessionState = await updateVariables(state)([
{ ...foundVariable, value: reply }, {
...foundVariable,
value: Array.isArray(foundVariable.value)
? foundVariable.value.concat(reply)
: reply,
},
]) ])
return newSessionState return newSessionState

View File

@@ -124,15 +124,12 @@ const computeRuntimeOptions =
const getPrefilledInputValue = const getPrefilledInputValue =
(variables: SessionState['typebot']['variables']) => (block: InputBlock) => { (variables: SessionState['typebot']['variables']) => (block: InputBlock) => {
return ( const variableValue = variables.find(
variables
.find(
(variable) => (variable) =>
variable.id === block.options.variableId && variable.id === block.options.variableId && isDefined(variable.value)
isDefined(variable.value) )?.value
) if (!variableValue || Array.isArray(variableValue)) return
?.value?.toString() ?? undefined return variableValue
)
} }
const parseBubbleBlock = const parseBubbleBlock =

View File

@@ -0,0 +1,26 @@
import { Variable, VariableWithValue } from 'models'
import { isNotDefined } from 'utils'
export const transformStringVariablesToList =
(variables: Variable[]) =>
(variableIds: string[]): VariableWithValue[] => {
const newVariables = variables.reduce<VariableWithValue[]>(
(variables, variable) => {
if (
!variableIds.includes(variable.id) ||
isNotDefined(variable.value) ||
typeof variable.value !== 'string'
)
return variables
return [
...variables,
{
...variable,
value: [variable.value],
},
]
},
[]
)
return newVariables
}

View File

@@ -15,6 +15,10 @@ export const chatCompletionMessageRoles = [
'assistant', 'assistant',
] as const ] as const
export const chatCompletionMessageCustomRoles = [
'Messages sequence ✨',
] as const
export const chatCompletionResponseValues = [ export const chatCompletionResponseValues = [
'Message content', 'Message content',
'Total tokens', 'Total tokens',
@@ -36,11 +40,24 @@ const chatCompletionMessageSchema = z.object({
content: z.string().optional(), content: z.string().optional(),
}) })
const chatCompletionCustomMessageSchema = z.object({
id: z.string(),
role: z.enum(chatCompletionMessageCustomRoles),
content: z
.object({
assistantMessagesVariableId: z.string().optional(),
userMessagesVariableId: z.string().optional(),
})
.optional(),
})
const chatCompletionOptionsSchema = z const chatCompletionOptionsSchema = z
.object({ .object({
task: z.literal(openAITasks[0]), task: z.literal(openAITasks[0]),
model: z.enum(chatCompletionModels), model: z.enum(chatCompletionModels),
messages: z.array(chatCompletionMessageSchema), messages: z.array(
z.union([chatCompletionMessageSchema, chatCompletionCustomMessageSchema])
),
responseMapping: z.array( responseMapping: z.array(
z.object({ z.object({
id: z.string(), id: z.string(),