2
0

(openai) Add "Ask assistant" action

Closes #1024
This commit is contained in:
Baptiste Arnaud
2024-01-11 08:29:41 +01:00
parent 799c694522
commit 03258e0f64
13 changed files with 257 additions and 42 deletions

View File

@ -76,7 +76,7 @@
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"nprogress": "0.2.0",
"openai": "4.19.0",
"openai": "4.24.1",
"papaparse": "5.4.1",
"posthog-js": "1.77.1",
"posthog-node": "3.1.1",

View File

@ -12,7 +12,10 @@ type Props = {
onOptionsChange: (options: BlockOptions) => void
}
export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
const { blockDef, blockSchema } = useForgedBlock(block.type)
const { blockDef, blockSchema, actionDef } = useForgedBlock(
block.type,
block.options?.action
)
const { isOpen, onOpen, onClose } = useDisclosure()
const updateCredentialsId = (credentialsId?: string) => {
@ -22,6 +25,34 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const resetOptionsAction = (updates: any) => {
if (!actionDef) return
const actionOptionsKeys = Object.keys(actionDef.options?.shape ?? [])
const actionOptions = actionOptionsKeys.reduce(
(acc, key) => ({
...acc,
[key]: undefined,
}),
{}
)
onOptionsChange({
...updates,
...actionOptions,
})
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const updateOptions = (updates: any) => {
const isChangingAction =
actionDef && updates?.action && updates.action !== block.options.action
if (isChangingAction) {
resetOptionsAction(updates)
return
}
onOptionsChange(updates)
}
if (!blockDef || !blockSchema) return null
return (
<Stack spacing={4}>
@ -57,7 +88,7 @@ export const ForgedBlockSettings = ({ block, onOptionsChange }: Props) => {
schema={blockSchema.shape.options}
blockDef={blockDef}
blockOptions={block.options}
onDataChange={onOptionsChange}
onDataChange={updateOptions}
/>
</>
)}

View File

@ -33,6 +33,17 @@ Then you can give the OpenAI block access to this sequence of messages:
/>
</Frame>
## Ask assistant
This action allows you to talk with your [OpenAI assistant](https://platform.openai.com/assistants). All you have to do is to provide its ID.
<Frame>
<img
src="/images/blocks/integrations/openai/ask-assistant.png"
alt="OpenAI ask assistant settings"
/>
</Frame>
## Create speech
This action allows you to transform a text input into an audio URL that you can reuse in your bot.

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -26820,7 +26820,8 @@
"openai",
"zemantic-ai",
"cal-com",
"chat-node"
"chat-node",
"qr-code"
]
},
"options": {}

View File

@ -10289,7 +10289,8 @@
"openai",
"zemantic-ai",
"cal-com",
"chat-node"
"chat-node",
"qr-code"
]
},
"options": {}

View File

@ -20,7 +20,7 @@
"@typebot.io/js": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"ai": "2.2.24",
"ai": "2.2.31",
"bot-engine": "workspace:*",
"cors": "2.8.5",
"google-spreadsheet": "4.0.2",
@ -28,7 +28,7 @@
"next": "14.0.3",
"nextjs-cors": "2.1.2",
"nodemailer": "6.9.3",
"openai": "4.19.0",
"openai": "4.24.1",
"qs": "6.11.2",
"react": "18.2.0",
"react-dom": "18.2.0",

View File

@ -19,7 +19,7 @@
"@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "21.1.5",
"@udecode/plate-serializer-md": "24.4.0",
"ai": "2.2.24",
"ai": "2.2.31",
"chrono-node": "2.7.0",
"date-fns": "2.30.0",
"google-auth-library": "8.9.0",
@ -28,7 +28,7 @@
"libphonenumber-js": "1.10.37",
"node-html-parser": "6.1.5",
"nodemailer": "6.9.3",
"openai": "4.19.0",
"openai": "4.24.1",
"qs": "6.11.2",
"stripe": "12.13.0"
},

View File

@ -0,0 +1,172 @@
import { createAction, option } from '@typebot.io/forge'
import { isDefined, isEmpty } from '@typebot.io/lib'
import { auth } from '../auth'
import { ClientOptions, OpenAI } from 'openai'
import { baseOptions } from '../baseOptions'
export const askAssistant = createAction({
auth,
baseOptions,
name: 'Ask Assistant',
options: option.object({
assistantId: option.string.layout({
label: 'Assistant ID',
placeholder: 'Select an assistant',
moreInfoTooltip: 'The OpenAI assistant you want to ask question to.',
fetcher: 'fetchAssistants',
}),
threadId: option.string.layout({
label: 'Thread ID',
moreInfoTooltip:
'Used to remember the conversation with the user. If empty, a new thread is created.',
}),
message: option.string.layout({
label: 'Message',
inputType: 'textarea',
}),
responseMapping: option
.saveResponseArray(['Message', 'Thread ID'] as const)
.layout({
accordion: 'Save response',
}),
}),
fetchers: [
{
id: 'fetchAssistants',
fetch: async ({ options, credentials }) => {
const config = {
apiKey: credentials.apiKey,
baseURL: options.baseUrl,
defaultHeaders: {
'api-key': credentials.apiKey,
},
defaultQuery: options.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const response = await openai.beta.assistants.list()
return response.data
.map((assistant) =>
assistant.name
? {
label: assistant.name,
value: assistant.id,
}
: undefined
)
.filter(isDefined)
},
dependencies: ['baseUrl', 'apiVersion'],
},
],
getSetVariableIds: ({ responseMapping }) =>
responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
run: {
server: async ({
credentials: { apiKey },
options: {
baseUrl,
apiVersion,
assistantId,
message,
responseMapping,
threadId,
},
variables,
logs,
}) => {
if (isEmpty(assistantId)) {
logs.add('Assistant ID is empty')
return
}
if (isEmpty(message)) {
logs.add('Message is empty')
return
}
const config = {
apiKey,
baseURL: baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: apiVersion
? {
'api-version': apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
// Create a thread if needed
const currentThreadId = isEmpty(threadId)
? (await openai.beta.threads.create({})).id
: threadId
// Add a message to the thread
const createdMessage = await openai.beta.threads.messages.create(
currentThreadId,
{
role: 'user',
content: message,
}
)
const run = await openai.beta.threads.runs.create(currentThreadId, {
assistant_id: assistantId,
})
async function waitForRun(run: OpenAI.Beta.Threads.Runs.Run) {
// Poll for status change
while (run.status === 'queued' || run.status === 'in_progress') {
await new Promise((resolve) => setTimeout(resolve, 500))
run = await openai.beta.threads.runs.retrieve(currentThreadId, run.id)
}
// Check the run status
if (
run.status === 'cancelled' ||
run.status === 'cancelling' ||
run.status === 'failed' ||
run.status === 'expired'
) {
throw new Error(run.status)
}
}
await waitForRun(run)
const responseMessages = (
await openai.beta.threads.messages.list(currentThreadId, {
after: createdMessage.id,
order: 'asc',
})
).data
responseMapping?.forEach((mapping) => {
if (!mapping.variableId) return
if (!mapping.item || mapping.item === 'Message') {
let message = ''
const messageContents = responseMessages[0].content
for (const content of messageContents) {
switch (content.type) {
case 'text':
message += (message !== '' ? '\n\n' : '') + content.text.value
break
}
}
variables.set(mapping.variableId, message)
}
if (mapping.item === 'Thread ID')
variables.set(mapping.variableId, currentThreadId)
})
},
},
})

View File

@ -20,22 +20,20 @@ export const parseChatCompletionMessages = ({
const dialogue = variables.get(message.dialogueVariableId) ?? []
const dialogueArr = Array.isArray(dialogue) ? dialogue : [dialogue]
return dialogueArr.map<OpenAI.Chat.ChatCompletionMessageParam>(
(dialogueItem, index) => {
if (index === 0 && message.startsBy === 'assistant')
return {
role: 'assistant',
content: dialogueItem,
}
return dialogueArr.map((dialogueItem, index) => {
if (index === 0 && message.startsBy === 'assistant')
return {
role:
index % (message.startsBy === 'assistant' ? 1 : 2) === 0
? 'user'
: 'assistant',
role: 'assistant',
content: dialogueItem,
}
return {
role:
index % (message.startsBy === 'assistant' ? 1 : 2) === 0
? 'user'
: 'assistant',
content: dialogueItem,
}
)
})
}
if (!message.content) return

View File

@ -4,6 +4,7 @@ import { createSpeech } from './actions/createSpeech'
import { createBlock } from '@typebot.io/forge'
import { auth } from './auth'
import { baseOptions } from './baseOptions'
import { askAssistant } from './actions/askAssistant'
export const openAIBlock = createBlock({
id: 'openai' as const,
@ -13,5 +14,5 @@ export const openAIBlock = createBlock({
DarkLogo: OpenAIDarkLogo,
auth,
options: baseOptions,
actions: [createChatCompletion, createSpeech],
actions: [createChatCompletion, askAssistant, createSpeech],
})

View File

@ -7,8 +7,8 @@
"author": "Baptiste Arnaud",
"license": "ISC",
"dependencies": {
"ai": "2.2.24",
"openai": "4.19.0"
"ai": "2.2.31",
"openai": "4.24.1"
},
"devDependencies": {
"@typebot.io/forge": "workspace:*",

36
pnpm-lock.yaml generated
View File

@ -222,8 +222,8 @@ importers:
specifier: 0.2.0
version: 0.2.0
openai:
specifier: 4.19.0
version: 4.19.0
specifier: 4.24.1
version: 4.24.1
papaparse:
specifier: 5.4.1
version: 5.4.1
@ -494,8 +494,8 @@ importers:
specifier: workspace:*
version: link:../../packages/prisma
ai:
specifier: 2.2.24
version: 2.2.24(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
specifier: 2.2.31
version: 2.2.31(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
bot-engine:
specifier: workspace:*
version: link:../../packages/deprecated/bot-engine
@ -518,8 +518,8 @@ importers:
specifier: 6.9.3
version: 6.9.3
openai:
specifier: 4.19.0
version: 4.19.0
specifier: 4.24.1
version: 4.24.1
qs:
specifier: 6.11.2
version: 6.11.2
@ -657,8 +657,8 @@ importers:
specifier: 24.4.0
version: 24.4.0(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.93.0)(slate-hyperscript@0.77.0)(slate-react@0.99.0)(slate@0.94.1)
ai:
specifier: 2.2.24
version: 2.2.24(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
specifier: 2.2.31
version: 2.2.31(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
chrono-node:
specifier: 2.7.0
version: 2.7.0
@ -684,8 +684,8 @@ importers:
specifier: 6.9.3
version: 6.9.3
openai:
specifier: 4.19.0
version: 4.19.0
specifier: 4.24.1
version: 4.24.1
qs:
specifier: 6.11.2
version: 6.11.2
@ -1209,11 +1209,11 @@ importers:
packages/forge/blocks/openai:
dependencies:
ai:
specifier: 2.2.24
version: 2.2.24(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
specifier: 2.2.31
version: 2.2.31(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4)
openai:
specifier: 4.19.0
version: 4.19.0
specifier: 4.24.1
version: 4.24.1
devDependencies:
'@typebot.io/forge':
specifier: workspace:*
@ -9825,8 +9825,8 @@ packages:
indent-string: 5.0.0
dev: true
/ai@2.2.24(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4):
resolution: {integrity: sha512-j9f89SC08SxN50wKp4ccIGniUMtCsslvOBbYGBCSIvDbMBQaNTZuKW4Id8tmfEeNMV9mwZZ5wRNj6/1Z/pFwYA==}
/ai@2.2.31(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.1)(vue@3.3.4):
resolution: {integrity: sha512-WQH13RxP+RYo9IE/FX8foNQh9gcKO/dhl9OGy5JL2bHJVBlnugPmH2CYJWaRt+mvjXHaU8txB+jzGo/fbtH2HA==}
engines: {node: '>=14.6'}
peerDependencies:
react: ^18.2.0
@ -17088,8 +17088,8 @@ packages:
is-docker: 2.2.1
is-wsl: 2.2.0
/openai@4.19.0:
resolution: {integrity: sha512-cJbl0noZyAaXVKBTMMq6X5BAvP1pm2rWYDBnZes99NL+Zh5/4NmlAwyuhTZEru5SqGGZIoiYKeMPXy4bm9DI0w==}
/openai@4.24.1:
resolution: {integrity: sha512-ezm/O3eiZMnyBqirUnWm9N6INJU1WhNtz+nK/Zj/2oyKvRz9pgpViDxa5wYOtyGYXPn1sIKBV0I/S4BDhtydqw==}
hasBin: true
dependencies:
'@types/node': 18.11.18