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
onDataChange: (val: any) => void
}) => {
const type = schema._def.innerType
? schema._def.innerType._def.typeName
: schema._def.typeName
const type = schema._def.innerType._def.type._def.innerType?._def.typeName
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
return (
<Stack spacing={0}>

View File

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

View File

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

View File

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

View File

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