2
0

(openai) Enable setVariable function in tools

Closes #1178
This commit is contained in:
Baptiste Arnaud
2024-01-22 09:22:28 +01:00
parent b438c174c4
commit 42008f8c18
24 changed files with 258 additions and 43 deletions

View File

@ -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<ExecuteLogicResponse> => {
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

View File

@ -101,6 +101,7 @@ export const executeForgedBlock = async (
newSessionState.typebotsQueue[0].typebot.variables,
params
)(text),
list: () => newSessionState.typebotsQueue[0].typebot.variables,
}
let logs: NonNullable<ContinueChatResponse['logs']> = []
const logsStore: LogsStore = {

View File

@ -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",

View File

@ -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}

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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:*"
}
}

View File

@ -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 = {

View File

@ -2,4 +2,5 @@ import { ScriptBlock } from './schema'
export const defaultScriptOptions = {
name: 'Script',
isExecutedOnClient: true,
} as const satisfies ScriptBlock['options']

View File

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

View File

@ -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<string, unknown>
}
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<string, any> = {}
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<any>, 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
})
}
}