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) }) }, }, })