2024-04-24 16:11:06 +02:00
|
|
|
import {
|
|
|
|
LogsStore,
|
|
|
|
VariableStore,
|
|
|
|
createAction,
|
|
|
|
option,
|
|
|
|
} from '@typebot.io/forge'
|
|
|
|
import { isDefined, isEmpty, isNotEmpty } from '@typebot.io/lib'
|
2024-01-11 08:29:41 +01:00
|
|
|
import { auth } from '../auth'
|
|
|
|
import { ClientOptions, OpenAI } from 'openai'
|
|
|
|
import { baseOptions } from '../baseOptions'
|
2024-01-22 09:22:28 +01:00
|
|
|
import { executeFunction } from '@typebot.io/variables/executeFunction'
|
2024-04-24 16:11:06 +02:00
|
|
|
import { readDataStream } from 'ai'
|
|
|
|
import { deprecatedAskAssistantOptions } from '../deprecated'
|
2024-07-15 14:32:42 +02:00
|
|
|
import { AssistantStream } from '../helpers/AssistantStream'
|
2024-06-11 10:51:02 +02:00
|
|
|
import { isModelCompatibleWithVision } from '../helpers/isModelCompatibleWithVision'
|
2024-07-15 14:32:42 +02:00
|
|
|
import { splitUserTextMessageIntoOpenAIBlocks } from '../helpers/splitUserTextMessageIntoOpenAIBlocks'
|
2024-01-11 08:29:41 +01:00
|
|
|
|
|
|
|
export const askAssistant = createAction({
|
|
|
|
auth,
|
|
|
|
baseOptions,
|
|
|
|
name: 'Ask Assistant',
|
2024-04-24 16:11:06 +02:00
|
|
|
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',
|
|
|
|
}),
|
|
|
|
threadVariableId: option.string.layout({
|
|
|
|
label: 'Thread ID',
|
|
|
|
moreInfoTooltip:
|
|
|
|
'Used to remember the conversation with the user. If empty, a new thread is created.',
|
|
|
|
inputType: 'variableDropdown',
|
|
|
|
}),
|
|
|
|
|
|
|
|
message: option.string.layout({
|
|
|
|
label: 'Message',
|
|
|
|
inputType: 'textarea',
|
2024-01-11 08:29:41 +01:00
|
|
|
}),
|
2024-04-24 16:11:06 +02:00
|
|
|
functions: option
|
|
|
|
.array(
|
|
|
|
option.object({
|
|
|
|
name: option.string.layout({
|
|
|
|
fetcher: 'fetchAssistantFunctions',
|
|
|
|
label: 'Name',
|
|
|
|
}),
|
|
|
|
code: option.string.layout({
|
|
|
|
inputType: 'code',
|
|
|
|
label: 'Code',
|
|
|
|
lang: 'javascript',
|
|
|
|
moreInfoTooltip:
|
|
|
|
'A javascript code snippet that can use the defined parameters. It should return a value.',
|
|
|
|
withVariableButton: false,
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
)
|
|
|
|
.layout({ accordion: 'Functions', itemLabel: 'function' }),
|
|
|
|
responseMapping: option
|
|
|
|
.saveResponseArray(['Message', 'Thread ID'] as const, {
|
|
|
|
item: { hiddenItems: ['Thread ID'] },
|
|
|
|
})
|
|
|
|
.layout({
|
|
|
|
accordion: 'Save response',
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
.merge(deprecatedAskAssistantOptions),
|
2024-01-11 08:29:41 +01:00
|
|
|
fetchers: [
|
|
|
|
{
|
|
|
|
id: 'fetchAssistants',
|
|
|
|
fetch: async ({ options, credentials }) => {
|
2024-05-21 16:08:35 +02:00
|
|
|
if (!credentials?.apiKey) return []
|
|
|
|
|
2024-01-11 08:29:41 +01:00
|
|
|
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)
|
|
|
|
|
2024-06-04 17:36:21 +02:00
|
|
|
const response = await openai.beta.assistants.list({
|
|
|
|
limit: 100,
|
|
|
|
})
|
2024-01-11 08:29:41 +01:00
|
|
|
|
|
|
|
return response.data
|
|
|
|
.map((assistant) =>
|
|
|
|
assistant.name
|
|
|
|
? {
|
|
|
|
label: assistant.name,
|
|
|
|
value: assistant.id,
|
|
|
|
}
|
|
|
|
: undefined
|
|
|
|
)
|
|
|
|
.filter(isDefined)
|
|
|
|
},
|
|
|
|
dependencies: ['baseUrl', 'apiVersion'],
|
|
|
|
},
|
2024-01-19 08:05:38 +01:00
|
|
|
{
|
|
|
|
id: 'fetchAssistantFunctions',
|
|
|
|
fetch: async ({ options, credentials }) => {
|
2024-05-21 16:08:35 +02:00
|
|
|
if (!options.assistantId || !credentials?.apiKey) return []
|
|
|
|
|
2024-01-19 08:05:38 +01:00
|
|
|
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.retrieve(
|
|
|
|
options.assistantId
|
|
|
|
)
|
|
|
|
|
|
|
|
return response.tools
|
|
|
|
.filter((tool) => tool.type === 'function')
|
|
|
|
.map((tool) =>
|
|
|
|
tool.type === 'function' && tool.function.name
|
|
|
|
? tool.function.name
|
|
|
|
: undefined
|
|
|
|
)
|
|
|
|
.filter(isDefined)
|
|
|
|
},
|
|
|
|
dependencies: ['baseUrl', 'apiVersion', 'assistantId'],
|
|
|
|
},
|
2024-01-11 08:29:41 +01:00
|
|
|
],
|
|
|
|
getSetVariableIds: ({ responseMapping }) =>
|
|
|
|
responseMapping?.map((r) => r.variableId).filter(isDefined) ?? [],
|
|
|
|
run: {
|
2024-04-24 16:11:06 +02:00
|
|
|
stream: {
|
|
|
|
getStreamVariableId: ({ responseMapping }) =>
|
|
|
|
responseMapping?.find((m) => !m.item || m.item === 'Message')
|
|
|
|
?.variableId,
|
2024-05-25 19:59:34 +02:00
|
|
|
run: async ({ credentials, options, variables }) => ({
|
|
|
|
stream: await createAssistantStream({
|
2024-04-24 16:11:06 +02:00
|
|
|
apiKey: credentials.apiKey,
|
|
|
|
assistantId: options.assistantId,
|
|
|
|
message: options.message,
|
|
|
|
baseUrl: options.baseUrl,
|
|
|
|
apiVersion: options.apiVersion,
|
|
|
|
threadVariableId: options.threadVariableId,
|
|
|
|
variables,
|
|
|
|
functions: options.functions,
|
|
|
|
responseMapping: options.responseMapping,
|
|
|
|
}),
|
2024-05-25 19:59:34 +02:00
|
|
|
}),
|
2024-04-24 16:11:06 +02:00
|
|
|
},
|
2024-01-11 08:29:41 +01:00
|
|
|
server: async ({
|
|
|
|
credentials: { apiKey },
|
|
|
|
options: {
|
|
|
|
baseUrl,
|
|
|
|
apiVersion,
|
|
|
|
assistantId,
|
|
|
|
message,
|
|
|
|
responseMapping,
|
|
|
|
threadId,
|
2024-04-24 16:11:06 +02:00
|
|
|
threadVariableId,
|
2024-01-19 08:05:38 +01:00
|
|
|
functions,
|
2024-01-11 08:29:41 +01:00
|
|
|
},
|
|
|
|
variables,
|
|
|
|
logs,
|
|
|
|
}) => {
|
2024-04-24 16:11:06 +02:00
|
|
|
const stream = await createAssistantStream({
|
2024-01-11 08:29:41 +01:00
|
|
|
apiKey,
|
2024-04-24 16:11:06 +02:00
|
|
|
assistantId,
|
|
|
|
logs,
|
|
|
|
message,
|
|
|
|
baseUrl,
|
|
|
|
apiVersion,
|
|
|
|
threadVariableId,
|
|
|
|
variables,
|
|
|
|
threadId,
|
|
|
|
functions,
|
|
|
|
})
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
if (!stream) return
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
let writingMessage = ''
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
for await (const { type, value } of readDataStream(stream.getReader())) {
|
|
|
|
if (type === 'text') {
|
|
|
|
writingMessage += value
|
2024-01-11 08:29:41 +01:00
|
|
|
}
|
2024-04-24 16:11:06 +02:00
|
|
|
}
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
responseMapping?.forEach((mapping) => {
|
|
|
|
if (!mapping.variableId) return
|
|
|
|
if (!mapping.item || mapping.item === 'Message') {
|
|
|
|
variables.set(
|
|
|
|
mapping.variableId,
|
|
|
|
writingMessage.replace(/【.+】/g, '')
|
|
|
|
)
|
|
|
|
}
|
2024-01-11 08:29:41 +01:00
|
|
|
})
|
2024-04-24 16:11:06 +02:00
|
|
|
},
|
|
|
|
},
|
|
|
|
})
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
const createAssistantStream = async ({
|
|
|
|
apiKey,
|
|
|
|
assistantId,
|
|
|
|
logs,
|
|
|
|
message,
|
|
|
|
baseUrl,
|
|
|
|
apiVersion,
|
|
|
|
threadVariableId,
|
|
|
|
variables,
|
|
|
|
threadId,
|
|
|
|
functions,
|
|
|
|
responseMapping,
|
|
|
|
}: {
|
|
|
|
apiKey?: string
|
|
|
|
assistantId?: string
|
|
|
|
message?: string
|
|
|
|
baseUrl?: string
|
|
|
|
apiVersion?: string
|
|
|
|
threadVariableId?: string
|
|
|
|
threadId?: string
|
|
|
|
functions?: { name?: string; code?: string }[]
|
|
|
|
responseMapping?: {
|
|
|
|
item?: 'Thread ID' | 'Message' | undefined
|
|
|
|
variableId?: string | undefined
|
|
|
|
}[]
|
|
|
|
logs?: LogsStore
|
|
|
|
variables: VariableStore
|
|
|
|
}): Promise<ReadableStream | undefined> => {
|
|
|
|
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,
|
2024-01-11 08:29:41 +01:00
|
|
|
}
|
2024-04-24 16:11:06 +02:00
|
|
|
: undefined,
|
|
|
|
} satisfies ClientOptions
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
const openai = new OpenAI(config)
|
2024-01-19 08:05:38 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
let currentThreadId: string | undefined
|
2024-01-19 08:05:38 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
if (
|
|
|
|
threadVariableId &&
|
|
|
|
isNotEmpty(variables.get(threadVariableId)?.toString())
|
|
|
|
) {
|
|
|
|
currentThreadId = variables.get(threadVariableId)?.toString()
|
|
|
|
} else if (isNotEmpty(threadId)) {
|
|
|
|
currentThreadId = threadId
|
|
|
|
} else {
|
|
|
|
currentThreadId = (await openai.beta.threads.create({})).id
|
|
|
|
const threadIdResponseMapping = responseMapping?.find(
|
|
|
|
(mapping) => mapping.item === 'Thread ID'
|
|
|
|
)
|
|
|
|
if (threadIdResponseMapping?.variableId)
|
|
|
|
variables.set(threadIdResponseMapping.variableId, currentThreadId)
|
2024-04-26 09:58:28 +02:00
|
|
|
else if (threadVariableId) variables.set(threadVariableId, currentThreadId)
|
2024-04-24 16:11:06 +02:00
|
|
|
}
|
2024-01-22 09:22:28 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
if (!currentThreadId) {
|
|
|
|
logs?.add('Could not get thread ID')
|
|
|
|
return
|
|
|
|
}
|
2024-01-19 08:05:38 +01:00
|
|
|
|
2024-06-11 10:51:02 +02:00
|
|
|
const assistant = await openai.beta.assistants.retrieve(assistantId)
|
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
// Add a message to the thread
|
|
|
|
const createdMessage = await openai.beta.threads.messages.create(
|
|
|
|
currentThreadId,
|
|
|
|
{
|
|
|
|
role: 'user',
|
2024-06-11 10:51:02 +02:00
|
|
|
content: isModelCompatibleWithVision(assistant.model)
|
2024-07-15 14:32:42 +02:00
|
|
|
? await splitUserTextMessageIntoOpenAIBlocks(message)
|
2024-06-11 10:51:02 +02:00
|
|
|
: message,
|
2024-04-24 16:11:06 +02:00
|
|
|
}
|
|
|
|
)
|
2024-07-15 14:32:42 +02:00
|
|
|
return AssistantStream(
|
2024-04-24 16:11:06 +02:00
|
|
|
{ threadId: currentThreadId, messageId: createdMessage.id },
|
|
|
|
async ({ forwardStream }) => {
|
2024-07-15 14:32:42 +02:00
|
|
|
const runStream = openai.beta.threads.runs.stream(currentThreadId, {
|
|
|
|
assistant_id: assistantId,
|
|
|
|
})
|
2024-01-19 08:05:38 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
let runResult = await forwardStream(runStream)
|
2024-01-19 08:05:38 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
while (
|
|
|
|
runResult?.status === 'requires_action' &&
|
|
|
|
runResult.required_action?.type === 'submit_tool_outputs'
|
|
|
|
) {
|
|
|
|
const tool_outputs = (
|
|
|
|
await Promise.all(
|
|
|
|
runResult.required_action.submit_tool_outputs.tool_calls.map(
|
|
|
|
async (toolCall) => {
|
|
|
|
const parameters = JSON.parse(toolCall.function.arguments)
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
const functionToExecute = functions?.find(
|
|
|
|
(f) => f.name === toolCall.function.name
|
|
|
|
)
|
|
|
|
if (!functionToExecute) return
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
const name = toolCall.function.name
|
|
|
|
if (!name || !functionToExecute.code) return
|
2024-01-11 08:29:41 +01:00
|
|
|
|
2024-04-24 16:11:06 +02:00
|
|
|
const { output, newVariables } = await executeFunction({
|
|
|
|
variables: variables.list(),
|
|
|
|
body: functionToExecute.code,
|
|
|
|
args: parameters,
|
|
|
|
})
|
|
|
|
|
|
|
|
newVariables?.forEach((variable) => {
|
|
|
|
variables.set(variable.id, variable.value)
|
|
|
|
})
|
|
|
|
|
|
|
|
return {
|
|
|
|
tool_call_id: toolCall.id,
|
|
|
|
output,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
)
|
|
|
|
).filter(isDefined)
|
|
|
|
runResult = await forwardStream(
|
|
|
|
openai.beta.threads.runs.submitToolOutputsStream(
|
|
|
|
currentThreadId,
|
|
|
|
runResult.id,
|
|
|
|
{ tool_outputs }
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|