2
0
Files
bot/packages/forge/blocks/openai/shared/runChatCompletion.ts

126 lines
3.6 KiB
TypeScript
Raw Normal View History

import OpenAI, { ClientOptions } from 'openai'
import { parseToolParameters } from '../helpers/parseToolParameters'
import { executeFunction } from '@typebot.io/variables/executeFunction'
import { ChatCompletionTool, ChatCompletionMessage } from 'openai/resources'
import { maxToolCalls } from '../constants'
import { parseChatCompletionMessages } from '../helpers/parseChatCompletionMessages'
import { ChatCompletionOptions } from './parseChatCompletionOptions'
import { LogsStore, VariableStore } from '@typebot.io/forge/types'
type OpenAIConfig = {
baseUrl: string
defaultModel?: string
}
type Props = {
credentials: {
apiKey?: string
}
options: ChatCompletionOptions
variables: VariableStore
logs: LogsStore
config: OpenAIConfig
}
export const runChatCompletion = async ({
credentials: { apiKey },
options,
variables,
config: openAIConfig,
logs,
}: Props) => {
const model = options.model?.trim() ?? openAIConfig.defaultModel
if (!model) return logs.add('No model provided')
const config = {
apiKey,
baseURL: openAIConfig.baseUrl ?? options.baseUrl,
defaultHeaders: options.baseUrl
? {
'api-key': apiKey,
}
: undefined,
defaultQuery: options.apiVersion
? {
'api-version': options.apiVersion,
}
: undefined,
} satisfies ClientOptions
const openai = new OpenAI(config)
const 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
const messages = await parseChatCompletionMessages({ options, variables })
const body = {
model,
temperature: options.temperature ? Number(options.temperature) : undefined,
messages,
tools: (tools?.length ?? 0) > 0 ? tools : undefined,
}
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
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) {
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
content: 'Function not found',
})
continue
}
const toolParams = Object.fromEntries(
toolDefinition.parameters.map(({ name }) => [name, null])
)
const toolArgs = toolCall.function?.arguments
? JSON.parse(toolCall.function?.arguments)
: undefined
if (!toolArgs) continue
const { output, newVariables } = await executeFunction({
variables: variables.list(),
args: { ...toolParams, ...toolArgs },
body: toolDefinition.code,
})
newVariables?.forEach((v) => variables.set(v.id, v.value))
messages.push({
tool_call_id: toolCall.id,
role: 'tool',
content: output,
})
}
}
options.responseMapping?.forEach((mapping) => {
if (!mapping.variableId) return
if (!mapping.item || mapping.item === 'Message content')
variables.set(mapping.variableId, message.content)
if (mapping.item === 'Total tokens')
variables.set(mapping.variableId, totalTokens)
})
}