From 42008f8c18430e6194d9552b93c8ec04ec484d6e Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 22 Jan 2024 09:22:28 +0100 Subject: [PATCH] :zap: (openai) Enable setVariable function in tools Closes #1178 --- apps/builder/next.config.mjs | 1 + .../script/components/ScriptNodeContent.tsx | 3 +- .../script/components/ScriptSettings.tsx | 14 +++ .../components/SetVariableSettings.tsx | 10 +- .../editor/blocks/integrations/openai.mdx | 4 +- apps/docs/editor/blocks/logic/script.mdx | 20 +++- apps/docs/openapi/builder.json | 18 +++ apps/docs/openapi/viewer.json | 6 + apps/viewer/next.config.mjs | 1 + .../api/integrations/openai/streamer/route.ts | 1 + .../blocks/logic/script/executeScript.ts | 27 ++++- .../bot-engine/forge/executeForgedBlock.ts | 1 + packages/embeds/js/package.json | 2 +- .../ConversationContainer.tsx | 3 +- packages/embeds/nextjs/package.json | 2 +- packages/embeds/react/package.json | 2 +- .../blocks/openai/actions/askAssistant.tsx | 20 ++-- .../openai/actions/createChatCompletion.tsx | 44 ++++--- packages/forge/blocks/openai/package.json | 3 +- packages/forge/core/types.ts | 5 + .../features/blocks/logic/script/constants.ts | 1 + .../features/blocks/logic/script/schema.ts | 1 + packages/variables/executeFunction.ts | 109 ++++++++++++++++++ pnpm-lock.yaml | 3 + 24 files changed, 258 insertions(+), 43 deletions(-) create mode 100644 packages/variables/executeFunction.ts diff --git a/apps/builder/next.config.mjs b/apps/builder/next.config.mjs index 5271157de0..23ac36f836 100644 --- a/apps/builder/next.config.mjs +++ b/apps/builder/next.config.mjs @@ -50,6 +50,7 @@ const nextConfig = { if (nextRuntime === 'edge') { config.resolve.alias['minio'] = false config.resolve.alias['got'] = false + config.resolve.alias['qrcode'] = false return config } // These packages are imports from the integrations definition files that can be ignored for the client. diff --git a/apps/builder/src/features/blocks/logic/script/components/ScriptNodeContent.tsx b/apps/builder/src/features/blocks/logic/script/components/ScriptNodeContent.tsx index 267afba493..cbf9c4613c 100644 --- a/apps/builder/src/features/blocks/logic/script/components/ScriptNodeContent.tsx +++ b/apps/builder/src/features/blocks/logic/script/components/ScriptNodeContent.tsx @@ -1,6 +1,7 @@ import React from 'react' import { Text } from '@chakra-ui/react' import { ScriptBlock } from '@typebot.io/schemas' +import { defaultScriptOptions } from '@typebot.io/schemas/features/blocks/logic/script/constants' type Props = { options: ScriptBlock['options'] @@ -10,6 +11,6 @@ export const ScriptNodeContent = ({ options: { name, content } = {}, }: Props) => ( - {content ? `Run ${name}` : 'Configure...'} + {content ? `Run ${name ?? defaultScriptOptions.name}` : 'Configure...'} ) diff --git a/apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx b/apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx index 36a907c59f..a7ed2f1563 100644 --- a/apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx +++ b/apps/builder/src/features/blocks/logic/script/components/ScriptSettings.tsx @@ -4,6 +4,7 @@ import React from 'react' import { TextInput } from '@/components/inputs' import { ScriptBlock } from '@typebot.io/schemas' import { defaultScriptOptions } from '@typebot.io/schemas/features/blocks/logic/script/constants' +import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' type Props = { options: ScriptBlock['options'] @@ -13,9 +14,13 @@ type Props = { export const ScriptSettings = ({ options, onOptionsChange }: Props) => { const handleNameChange = (name: string) => onOptionsChange({ ...options, name }) + const handleCodeChange = (content: string) => onOptionsChange({ ...options, content }) + const updateClientExecution = (isExecutedOnClient: boolean) => + onOptionsChange({ ...options, isExecutedOnClient }) + return ( { lang="javascript" onChange={handleCodeChange} /> + ) diff --git a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx index bad84590c1..2b2b66fd59 100644 --- a/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx +++ b/apps/builder/src/features/blocks/logic/setVariable/components/SetVariableSettings.tsx @@ -137,11 +137,6 @@ const SetVariableValue = ({ case undefined: return ( <> - + ) case 'Map item with same index': { diff --git a/apps/docs/editor/blocks/integrations/openai.mdx b/apps/docs/editor/blocks/integrations/openai.mdx index a990814023..8bf192c05c 100644 --- a/apps/docs/editor/blocks/integrations/openai.mdx +++ b/apps/docs/editor/blocks/integrations/openai.mdx @@ -43,7 +43,9 @@ A more useful example would be, of course, to call an API to get the weather of OpenAI tools -As you can see, the code block expects the body of the Javascript function. You can use the `return` keyword to return values. +As you can see, the code block expects the body of the Javascript function. You should use the `return` keyword to return value to give back to OpenAI as the result of the function. + +If you'd like to set variables directly in this code block, you can use the [`setVariable` function](../logic/script#setvariable-function). ## Ask assistant diff --git a/apps/docs/editor/blocks/logic/script.mdx b/apps/docs/editor/blocks/logic/script.mdx index 301cca7488..bc9e2468c6 100644 --- a/apps/docs/editor/blocks/logic/script.mdx +++ b/apps/docs/editor/blocks/logic/script.mdx @@ -3,9 +3,9 @@ title: Script block icon: code --- -The "Script" block allows you to execute Javascript code. If you want to set a variable value with Javascript, use the [Set variable block](./set-variable) instead. You can't set a variable with the script block. +The "Script" block allows you to execute Javascript code. -**It doesn't allow you to create a custom visual block** +This block doesn't allow you to create a custom visual block Code block @@ -18,6 +18,22 @@ You need to write `console.log({{My variable}})` instead of `console.log("{{My v +## `setVariable` function + +If you want to set a variable value with Javascript, the [Set variable block](./set-variable) is more appropriate for most cases. + +However, if you'd like to set variables with the script blocks, you can use the `setVariable` function in your script: + +```js +if({{My variable}} === 'foo') { + setVariable('My variable', 'bar') +} else { + setVariable('My variable', 'other') +} +``` + +The `setVariable` function is only available in script executed on the server, so it won't work if the `Execute on client?` is checked. + ## Examples ### Reload page diff --git a/apps/docs/openapi/builder.json b/apps/docs/openapi/builder.json index f7f45208eb..cfbc660c78 100644 --- a/apps/docs/openapi/builder.json +++ b/apps/docs/openapi/builder.json @@ -371,6 +371,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -6580,6 +6583,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -11101,6 +11107,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -24814,6 +24823,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -28073,6 +28085,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -30882,6 +30897,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } diff --git a/apps/docs/openapi/viewer.json b/apps/docs/openapi/viewer.json index 75c4ffc1bb..7151fac8db 100644 --- a/apps/docs/openapi/viewer.json +++ b/apps/docs/openapi/viewer.json @@ -4527,6 +4527,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } @@ -8289,6 +8292,9 @@ "content": { "type": "string" }, + "isExecutedOnClient": { + "type": "boolean" + }, "shouldExecuteInParentContext": { "type": "boolean" } diff --git a/apps/viewer/next.config.mjs b/apps/viewer/next.config.mjs index 97b4d0cc21..30e4270fae 100644 --- a/apps/viewer/next.config.mjs +++ b/apps/viewer/next.config.mjs @@ -50,6 +50,7 @@ const nextConfig = { if (nextRuntime === 'edge') { config.resolve.alias['minio'] = false config.resolve.alias['got'] = false + config.resolve.alias['qrcode'] = false return config } // These packages are imports from the integrations definition files that can be ignored for the client. diff --git a/apps/viewer/src/app/api/integrations/openai/streamer/route.ts b/apps/viewer/src/app/api/integrations/openai/streamer/route.ts index 4c34d2a066..425d93ef12 100644 --- a/apps/viewer/src/app/api/integrations/openai/streamer/route.ts +++ b/apps/viewer/src/app/api/integrations/openai/streamer/route.ts @@ -134,6 +134,7 @@ export async function POST(req: Request) { credentials.iv ) const variables: ReadOnlyVariableStore = { + list: () => state.typebotsQueue[0].typebot.variables, get: (id: string) => { const variable = state.typebotsQueue[0].typebot.variables.find( (variable) => variable.id === id diff --git a/packages/bot-engine/blocks/logic/script/executeScript.ts b/packages/bot-engine/blocks/logic/script/executeScript.ts index f7b6d7c5ea..8ea7ce4ef3 100644 --- a/packages/bot-engine/blocks/logic/script/executeScript.ts +++ b/packages/bot-engine/blocks/logic/script/executeScript.ts @@ -3,15 +3,38 @@ import { ScriptBlock, SessionState, Variable } from '@typebot.io/schemas' import { extractVariablesFromText } from '@typebot.io/variables/extractVariablesFromText' import { parseGuessedValueType } from '@typebot.io/variables/parseGuessedValueType' import { parseVariables } from '@typebot.io/variables/parseVariables' +import { defaultScriptOptions } from '@typebot.io/schemas/features/blocks/logic/script/constants' +import { executeFunction } from '@typebot.io/variables/executeFunction' +import { updateVariablesInSession } from '@typebot.io/variables/updateVariablesInSession' -export const executeScript = ( +export const executeScript = async ( state: SessionState, block: ScriptBlock -): ExecuteLogicResponse => { +): Promise => { const { variables } = state.typebotsQueue[0].typebot if (!block.options?.content || state.whatsApp) return { outgoingEdgeId: block.outgoingEdgeId } + const isExecutedOnClient = + block.options.isExecutedOnClient ?? defaultScriptOptions.isExecutedOnClient + + if (!isExecutedOnClient) { + const { newVariables, error } = await executeFunction({ + variables, + body: block.options.content, + }) + + const newSessionState = newVariables + ? updateVariablesInSession(state)(newVariables) + : state + + return { + outgoingEdgeId: block.outgoingEdgeId, + logs: error ? [{ status: 'error', description: error }] : [], + newSessionState, + } + } + const scriptToExecute = parseScriptToExecuteClientSideAction( variables, block.options.content diff --git a/packages/bot-engine/forge/executeForgedBlock.ts b/packages/bot-engine/forge/executeForgedBlock.ts index cf45ec3a58..3fdcbdf27d 100644 --- a/packages/bot-engine/forge/executeForgedBlock.ts +++ b/packages/bot-engine/forge/executeForgedBlock.ts @@ -101,6 +101,7 @@ export const executeForgedBlock = async ( newSessionState.typebotsQueue[0].typebot.variables, params )(text), + list: () => newSessionState.typebotsQueue[0].typebot.variables, } let logs: NonNullable = [] const logsStore: LogsStore = { diff --git a/packages/embeds/js/package.json b/packages/embeds/js/package.json index 2f06129b35..aa49af6f60 100644 --- a/packages/embeds/js/package.json +++ b/packages/embeds/js/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/js", - "version": "0.2.34", + "version": "0.2.35", "description": "Javascript library to display typebots on your website", "type": "module", "main": "dist/index.js", diff --git a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx index 2367d0104b..3273cfbe60 100644 --- a/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx +++ b/packages/embeds/js/src/components/ConversationContainer/ConversationContainer.tsx @@ -284,8 +284,7 @@ export const ConversationContainer = (props: Props) => { hideAvatar={ !chatChunk.input && ((chatChunks()[index() + 1]?.messages ?? 0).length > 0 || - chatChunks()[index() + 1]?.streamingMessageId !== undefined || - isSending()) + chatChunks()[index() + 1]?.streamingMessageId !== undefined) } hasError={hasError() && index() === chatChunks().length - 1} onNewBubbleDisplayed={handleNewBubbleDisplayed} diff --git a/packages/embeds/nextjs/package.json b/packages/embeds/nextjs/package.json index 59b63c5831..9b987180e1 100644 --- a/packages/embeds/nextjs/package.json +++ b/packages/embeds/nextjs/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/nextjs", - "version": "0.2.34", + "version": "0.2.35", "description": "Convenient library to display typebots on your Next.js website", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/embeds/react/package.json b/packages/embeds/react/package.json index 3b934cf15d..8fede12440 100644 --- a/packages/embeds/react/package.json +++ b/packages/embeds/react/package.json @@ -1,6 +1,6 @@ { "name": "@typebot.io/react", - "version": "0.2.34", + "version": "0.2.35", "description": "Convenient library to display typebots on your React app", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/forge/blocks/openai/actions/askAssistant.tsx b/packages/forge/blocks/openai/actions/askAssistant.tsx index 25f63302b9..d038289245 100644 --- a/packages/forge/blocks/openai/actions/askAssistant.tsx +++ b/packages/forge/blocks/openai/actions/askAssistant.tsx @@ -3,6 +3,7 @@ import { isDefined, isEmpty } from '@typebot.io/lib' import { auth } from '../auth' import { ClientOptions, OpenAI } from 'openai' import { baseOptions } from '../baseOptions' +import { executeFunction } from '@typebot.io/variables/executeFunction' export const askAssistant = createAction({ auth, @@ -206,12 +207,17 @@ export const askAssistant = createAction({ if (!functionToExecute) return const name = toolCall.function.name - if (!name) return - const func = AsyncFunction( - ...Object.keys(parameters), - functionToExecute.code - ) - const output = await func(...Object.values(parameters)) + if (!name || !functionToExecute.code) return + + 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, @@ -262,5 +268,3 @@ export const askAssistant = createAction({ }, }, }) - -const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor diff --git a/packages/forge/blocks/openai/actions/createChatCompletion.tsx b/packages/forge/blocks/openai/actions/createChatCompletion.tsx index 8df72fcb58..5db0ab6b97 100644 --- a/packages/forge/blocks/openai/actions/createChatCompletion.tsx +++ b/packages/forge/blocks/openai/actions/createChatCompletion.tsx @@ -8,6 +8,7 @@ import { auth } from '../auth' import { baseOptions } from '../baseOptions' import { ChatCompletionTool } from 'openai/resources/chat/completions' import { parseToolParameters } from '../helpers/parseToolParameters' +import { executeFunction } from '@typebot.io/variables/executeFunction' const nativeMessageContentSchema = { content: option.string.layout({ @@ -213,17 +214,21 @@ export const createChatCompletion = createAction({ if (!name) continue const toolDefinition = options.tools?.find((t) => t.name === name) if (!toolDefinition?.code || !toolDefinition.parameters) continue - const func = AsyncFunction( - ...toolDefinition.parameters?.map((p) => p.name), - toolDefinition.code - ) - const content = await func( - ...Object.values(JSON.parse(toolCall.function.arguments)) - ) + const toolArgs = toolCall.function?.arguments + ? JSON.parse(toolCall.function?.arguments) + : undefined + if (!toolArgs) continue + const { output, newVariables } = await executeFunction({ + variables: variables.list(), + args: toolArgs, + body: toolDefinition.code, + }) + newVariables?.forEach((v) => variables.set(v.id, v.value)) + messages.push({ tool_call_id: toolCall.id, role: 'tool', - content, + content: output, }) } @@ -304,18 +309,23 @@ export const createChatCompletion = createAction({ if (!name) continue const toolDefinition = options.tools?.find((t) => t.name === name) if (!toolDefinition?.code || !toolDefinition.parameters) continue - const func = AsyncFunction( - ...toolDefinition.parameters?.map((p) => p.name), - toolDefinition.code - ) - const content = await func( - ...Object.values(JSON.parse(toolCall.func.arguments as any)) - ) + + const { output } = await executeFunction({ + variables: variables.list(), + args: + typeof toolCall.func.arguments === 'string' + ? JSON.parse(toolCall.func.arguments) + : toolCall.func.arguments, + body: toolDefinition.code, + }) + + // TO-DO: enable once we're out of edge runtime. + // newVariables?.forEach((v) => variables.set(v.id, v.value)) const newMessages = appendToolCallMessage({ tool_call_id: toolCall.id, function_name: toolCall.func.name, - tool_call_result: content, + tool_call_result: output, }) return openai.chat.completions.create({ @@ -334,5 +344,3 @@ export const createChatCompletion = createAction({ }, }, }) - -const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor diff --git a/packages/forge/blocks/openai/package.json b/packages/forge/blocks/openai/package.json index e00519c434..678eabc6bd 100644 --- a/packages/forge/blocks/openai/package.json +++ b/packages/forge/blocks/openai/package.json @@ -15,6 +15,7 @@ "@typebot.io/tsconfig": "workspace:*", "@types/react": "18.2.15", "typescript": "5.3.2", - "@typebot.io/lib": "workspace:*" + "@typebot.io/lib": "workspace:*", + "@typebot.io/variables": "workspace:*" } } diff --git a/packages/forge/core/types.ts b/packages/forge/core/types.ts index 8e1b98b824..277cb31e45 100644 --- a/packages/forge/core/types.ts +++ b/packages/forge/core/types.ts @@ -6,6 +6,11 @@ export type VariableStore = { get: (variableId: string) => string | (string | null)[] | null | undefined set: (variableId: string, value: unknown) => void parse: (value: string) => string + list: () => { + id: string + name: string + value?: string | (string | null)[] | null | undefined + }[] } export type LogsStore = { diff --git a/packages/schemas/features/blocks/logic/script/constants.ts b/packages/schemas/features/blocks/logic/script/constants.ts index b75ba8e7db..54d850d018 100644 --- a/packages/schemas/features/blocks/logic/script/constants.ts +++ b/packages/schemas/features/blocks/logic/script/constants.ts @@ -2,4 +2,5 @@ import { ScriptBlock } from './schema' export const defaultScriptOptions = { name: 'Script', + isExecutedOnClient: true, } as const satisfies ScriptBlock['options'] diff --git a/packages/schemas/features/blocks/logic/script/schema.ts b/packages/schemas/features/blocks/logic/script/schema.ts index 997ba1f806..52a03fa06b 100644 --- a/packages/schemas/features/blocks/logic/script/schema.ts +++ b/packages/schemas/features/blocks/logic/script/schema.ts @@ -5,6 +5,7 @@ import { LogicBlockType } from '../constants' export const scriptOptionsSchema = z.object({ name: z.string().optional(), content: z.string().optional(), + isExecutedOnClient: z.boolean().optional(), shouldExecuteInParentContext: z.boolean().optional(), }) diff --git a/packages/variables/executeFunction.ts b/packages/variables/executeFunction.ts new file mode 100644 index 0000000000..66aaa2acd3 --- /dev/null +++ b/packages/variables/executeFunction.ts @@ -0,0 +1,109 @@ +import { Variable } from '@typebot.io/schemas' +import { parseVariables } from './parseVariables' +import { extractVariablesFromText } from './extractVariablesFromText' +import { parseGuessedValueType } from './parseGuessedValueType' +import { isDefined } from '@typebot.io/lib' +import { defaultTimeout } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' + +type Props = { + variables: Variable[] + body: string + args?: Record +} + +export const executeFunction = async ({ + variables, + body, + args: initialArgs, +}: Props) => { + const parsedBody = parseVariables(variables, { fieldToParse: 'id' })(body) + + const args = ( + extractVariablesFromText(variables)(body).map((variable) => ({ + id: variable.id, + value: parseGuessedValueType(variable.value), + })) as { id: string; value: unknown }[] + ).concat( + initialArgs + ? Object.entries(initialArgs).map(([id, value]) => ({ id, value })) + : [] + ) + const func = AsyncFunction( + ...args.map(({ id }) => id), + 'setVariable', + parsedBody + ) + + let updatedVariables: Record = {} + + const setVariable = (key: string, value: any) => { + updatedVariables[key] = value + } + const timeout = new Timeout() + + try { + const output = await timeout.wrap( + func(...args.map(({ value }) => value), setVariable), + defaultTimeout * 1000 + ) + timeout.clear() + return { + output, + newVariables: Object.entries(updatedVariables) + .map(([name, value]) => { + const existingVariable = variables.find((v) => v.name === name) + if (!existingVariable) return + return { + id: existingVariable.id, + name: existingVariable.name, + value, + } + }) + .filter(isDefined), + } + } catch (e) { + console.log('Error while executing script') + console.error(e) + + return { + error: + typeof e === 'string' + ? e + : e instanceof Error + ? e.message + : JSON.stringify(e), + } + } +} + +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor + +class Timeout { + private ids: NodeJS.Timeout[] + + constructor() { + this.ids = [] + } + + private set = (delay: number) => + new Promise((_, reject) => { + const id = setTimeout(() => { + reject(`Script ${defaultTimeout}s timeout reached`) + this.clear(id) + }, delay) + this.ids.push(id) + }) + + wrap = (promise: Promise, delay: number) => + Promise.race([promise, this.set(delay)]) + + clear = (...ids: NodeJS.Timeout[]) => { + this.ids = this.ids.filter((id) => { + if (ids.includes(id)) { + clearTimeout(id) + return false + } + return true + }) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d8b0b0d329..f433914f88 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1224,6 +1224,9 @@ importers: '@typebot.io/tsconfig': specifier: workspace:* version: link:../../../tsconfig + '@typebot.io/variables': + specifier: workspace:* + version: link:../../../variables '@types/react': specifier: 18.2.15 version: 18.2.15