2
0

(openai) Add enum support in function tools

This commit is contained in:
Baptiste Arnaud
2024-01-29 14:45:44 +01:00
parent 4b8b80e996
commit 8d363c0c02
5 changed files with 106 additions and 64 deletions

View File

@ -295,9 +295,7 @@ const ZodArrayContent = ({
isInAccordion?: boolean isInAccordion?: boolean
onDataChange: (val: any) => void onDataChange: (val: any) => void
}) => { }) => {
const type = schema._def.innerType const type = schema._def.innerType._def.type._def.innerType?._def.typeName
? schema._def.innerType._def.typeName
: schema._def.typeName
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum') if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
return ( return (
<Stack spacing={0}> <Stack spacing={0}>

View File

@ -1,12 +1,15 @@
import { option, createAction } from '@typebot.io/forge' import { option, createAction } from '@typebot.io/forge'
import OpenAI, { ClientOptions } from 'openai' import OpenAI, { ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '../constants' import { defaultOpenAIOptions, maxToolCalls } from '../constants'
import { OpenAIStream, ToolCallPayload } from 'ai' import { OpenAIStream, ToolCallPayload } from 'ai'
import { parseChatCompletionMessages } from '../helpers/parseChatCompletionMessages' import { parseChatCompletionMessages } from '../helpers/parseChatCompletionMessages'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { auth } from '../auth' import { auth } from '../auth'
import { baseOptions } from '../baseOptions' import { baseOptions } from '../baseOptions'
import { ChatCompletionTool } from 'openai/resources/chat/completions' import {
ChatCompletionMessage,
ChatCompletionTool,
} from 'openai/resources/chat/completions'
import { parseToolParameters } from '../helpers/parseToolParameters' import { parseToolParameters } from '../helpers/parseToolParameters'
import { executeFunction } from '@typebot.io/variables/executeFunction' import { executeFunction } from '@typebot.io/variables/executeFunction'
@ -35,16 +38,12 @@ const assistantMessageItemSchema = option
}) })
.extend(nativeMessageContentSchema) .extend(nativeMessageContentSchema)
export const parameterSchema = option.object({ const parameterBase = {
name: option.string.layout({ name: option.string.layout({
label: 'Name', label: 'Name',
placeholder: 'myVariable', placeholder: 'myVariable',
withVariableButton: false, withVariableButton: false,
}), }),
type: option.enum(['string', 'number', 'boolean']).layout({
label: 'Type:',
direction: 'row',
}),
description: option.string.layout({ description: option.string.layout({
label: 'Description', label: 'Description',
withVariableButton: false, withVariableButton: false,
@ -52,7 +51,40 @@ export const parameterSchema = option.object({
required: option.boolean.layout({ required: option.boolean.layout({
label: 'Is required?', label: 'Is required?',
}), }),
}) }
export const toolParametersSchema = option
.array(
option.discriminatedUnion('type', [
option
.object({
type: option.literal('string'),
})
.extend(parameterBase),
option
.object({
type: option.literal('number'),
})
.extend(parameterBase),
option
.object({
type: option.literal('boolean'),
})
.extend(parameterBase),
option
.object({
type: option.literal('enum'),
values: option
.array(option.string)
.layout({ itemLabel: 'possible value' }),
})
.extend(parameterBase),
])
)
.layout({
accordion: 'Parameters',
itemLabel: 'parameter',
})
const functionToolItemSchema = option.object({ const functionToolItemSchema = option.object({
type: option.literal('function'), type: option.literal('function'),
@ -66,7 +98,7 @@ const functionToolItemSchema = option.object({
placeholder: 'A brief description of what this function does.', placeholder: 'A brief description of what this function does.',
withVariableButton: false, withVariableButton: false,
}), }),
parameters: option.array(parameterSchema).layout({ accordion: 'Parameters' }), parameters: toolParametersSchema,
code: option.string.layout({ code: option.string.layout({
inputType: 'code', inputType: 'code',
label: 'Code', label: 'Code',
@ -180,40 +212,52 @@ export const createChatCompletion = createAction({
const openai = new OpenAI(config) const openai = new OpenAI(config)
const tools = options.tools const tools = options.tools
? (options.tools ?.filter((t) => t.name && t.parameters)
.filter((t) => t.name && t.parameters) .map((t) => ({
.map((t) => ({ type: 'function',
type: 'function', function: {
function: { name: t.name as string,
name: t.name as string, description: t.description,
description: t.description, parameters: parseToolParameters(t.parameters!),
parameters: parseToolParameters(t.parameters!), },
}, })) satisfies ChatCompletionTool[] | undefined
})) satisfies ChatCompletionTool[])
: undefined
const messages = parseChatCompletionMessages({ options, variables }) const messages = parseChatCompletionMessages({ options, variables })
const response = await openai.chat.completions.create({ const body = {
model: options.model ?? defaultOpenAIOptions.model, model: options.model ?? defaultOpenAIOptions.model,
temperature: options.temperature temperature: options.temperature
? Number(options.temperature) ? Number(options.temperature)
: undefined, : undefined,
messages, messages,
tools, tools,
}) }
let message = response.choices[0].message let totalTokens = 0
let totalTokens = response.usage?.total_tokens let message: ChatCompletionMessage
for (let i = 0; i < maxToolCalls; i++) {
const response = await openai.chat.completions.create(body)
message = response.choices[0].message
totalTokens += response.usage?.total_tokens || 0
if (!message.tool_calls) break
if (message.tool_calls) {
messages.push(message) messages.push(message)
for (const toolCall of message.tool_calls) { for (const toolCall of message.tool_calls) {
const name = toolCall.function?.name const name = toolCall.function?.name
if (!name) continue if (!name) continue
const toolDefinition = options.tools?.find((t) => t.name === name) const toolDefinition = options.tools?.find((t) => t.name === name)
if (!toolDefinition?.code || !toolDefinition.parameters) continue if (!toolDefinition?.code || !toolDefinition.parameters) {
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
content: 'Function not found',
})
continue
}
const toolParams = Object.fromEntries( const toolParams = Object.fromEntries(
toolDefinition.parameters.map(({ name }) => [name, null]) toolDefinition.parameters.map(({ name }) => [name, null])
) )
@ -234,18 +278,6 @@ export const createChatCompletion = createAction({
content: output, content: output,
}) })
} }
const secondResponse = await openai.chat.completions.create({
model: options.model ?? defaultOpenAIOptions.model,
temperature: options.temperature
? Number(options.temperature)
: undefined,
messages,
tools,
})
message = secondResponse.choices[0].message
totalTokens = secondResponse.usage?.total_tokens
} }
options.responseMapping?.forEach((mapping) => { options.responseMapping?.forEach((mapping) => {
@ -278,17 +310,15 @@ export const createChatCompletion = createAction({
const openai = new OpenAI(config) const openai = new OpenAI(config)
const tools = options.tools const tools = options.tools
? (options.tools ?.filter((t) => t.name && t.parameters)
.filter((t) => t.name && t.parameters) .map((t) => ({
.map((t) => ({ type: 'function',
type: 'function', function: {
function: { name: t.name as string,
name: t.name as string, description: t.description,
description: t.description, parameters: parseToolParameters(t.parameters!),
parameters: parseToolParameters(t.parameters!), },
}, })) satisfies ChatCompletionTool[] | undefined
})) satisfies ChatCompletionTool[])
: undefined
const messages = parseChatCompletionMessages({ options, variables }) const messages = parseChatCompletionMessages({ options, variables })
@ -311,7 +341,14 @@ export const createChatCompletion = createAction({
const name = toolCall.func?.name const name = toolCall.func?.name
if (!name) continue if (!name) continue
const toolDefinition = options.tools?.find((t) => t.name === name) const toolDefinition = options.tools?.find((t) => t.name === name)
if (!toolDefinition?.code || !toolDefinition.parameters) continue if (!toolDefinition?.code || !toolDefinition.parameters) {
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
content: 'Function not found',
})
continue
}
const { output } = await executeFunction({ const { output } = await executeFunction({
variables: variables.list(), variables: variables.list(),

View File

@ -13,3 +13,5 @@ export const defaultOpenAIOptions = {
voiceModel: 'tts-1', voiceModel: 'tts-1',
temperature: 1, temperature: 1,
} as const } as const
export const maxToolCalls = 10

View File

@ -1,9 +1,9 @@
import type { OpenAI } from 'openai' import type { OpenAI } from 'openai'
import { parameterSchema } from '../actions/createChatCompletion' import { toolParametersSchema } from '../actions/createChatCompletion'
import { z } from '@typebot.io/forge/zod' import { z } from '@typebot.io/forge/zod'
export const parseToolParameters = ( export const parseToolParameters = (
parameters: z.infer<typeof parameterSchema>[] parameters: z.infer<typeof toolParametersSchema>
): OpenAI.FunctionParameters => ({ ): OpenAI.FunctionParameters => ({
type: 'object', type: 'object',
properties: parameters?.reduce<{ properties: parameters?.reduce<{
@ -11,7 +11,8 @@ export const parseToolParameters = (
}>((acc, param) => { }>((acc, param) => {
if (!param.name) return acc if (!param.name) return acc
acc[param.name] = { acc[param.name] = {
type: param.type, type: param.type === 'enum' ? 'string' : param.type,
enum: param.type === 'enum' ? param.values : undefined,
description: param.description, description: param.description,
} }
return acc return acc

View File

@ -4,6 +4,7 @@ import { extractVariablesFromText } from './extractVariablesFromText'
import { parseGuessedValueType } from './parseGuessedValueType' import { parseGuessedValueType } from './parseGuessedValueType'
import { isDefined } from '@typebot.io/lib' import { isDefined } from '@typebot.io/lib'
import { defaultTimeout } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants' import { defaultTimeout } from '@typebot.io/schemas/features/blocks/integrations/webhook/constants'
import { safeStringify } from '@typebot.io/lib/safeStringify'
type Props = { type Props = {
variables: Variable[] variables: Variable[]
@ -42,13 +43,13 @@ export const executeFunction = async ({
const timeout = new Timeout() const timeout = new Timeout()
try { try {
const output = await timeout.wrap( const output: unknown = await timeout.wrap(
func(...args.map(({ value }) => value), setVariable), func(...args.map(({ value }) => value), setVariable),
defaultTimeout * 1000 defaultTimeout * 1000
) )
timeout.clear() timeout.clear()
return { return {
output, output: safeStringify(output) ?? '',
newVariables: Object.entries(updatedVariables) newVariables: Object.entries(updatedVariables)
.map(([name, value]) => { .map(([name, value]) => {
const existingVariable = variables.find((v) => v.name === name) const existingVariable = variables.find((v) => v.name === name)
@ -65,13 +66,16 @@ export const executeFunction = async ({
console.log('Error while executing script') console.log('Error while executing script')
console.error(e) console.error(e)
const error =
typeof e === 'string'
? e
: e instanceof Error
? e.message
: JSON.stringify(e)
return { return {
error: error,
typeof e === 'string' output: error,
? e
: e instanceof Error
? e.message
: JSON.stringify(e),
} }
} }
} }