✨ Introducing The Forge (#1072)
The Forge allows anyone to easily create their own Typebot Block. Closes #380
This commit is contained in:
180
packages/forge/blocks/openai/actions/createChatCompletion.tsx
Normal file
180
packages/forge/blocks/openai/actions/createChatCompletion.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import { option, createAction } from '@typebot.io/forge'
|
||||
import OpenAI, { ClientOptions } from 'openai'
|
||||
import { defaultOpenAIOptions } from '../constants'
|
||||
import { OpenAIStream } from 'ai'
|
||||
import { parseChatCompletionMessages } from '../helpers/parseChatCompletionMessages'
|
||||
import { isDefined } from '@typebot.io/lib'
|
||||
import { auth } from '../auth'
|
||||
import { baseOptions } from '../baseOptions'
|
||||
|
||||
const nativeMessageContentSchema = {
|
||||
content: option.string.layout({ input: 'textarea', placeholder: 'Content' }),
|
||||
}
|
||||
|
||||
const systemMessageItemSchema = option
|
||||
.object({
|
||||
role: option.literal('system'),
|
||||
})
|
||||
.extend(nativeMessageContentSchema)
|
||||
|
||||
const userMessageItemSchema = option
|
||||
.object({
|
||||
role: option.literal('user'),
|
||||
})
|
||||
.extend(nativeMessageContentSchema)
|
||||
|
||||
const assistantMessageItemSchema = option
|
||||
.object({
|
||||
role: option.literal('assistant'),
|
||||
})
|
||||
.extend(nativeMessageContentSchema)
|
||||
|
||||
const dialogueMessageItemSchema = option.object({
|
||||
role: option.literal('Dialogue'),
|
||||
dialogueVariableId: option.string.layout({
|
||||
input: 'variableDropdown',
|
||||
placeholder: 'Dialogue variable',
|
||||
}),
|
||||
startsBy: option.enum(['user', 'assistant']).layout({
|
||||
label: 'starts by',
|
||||
direction: 'row',
|
||||
defaultValue: 'user',
|
||||
}),
|
||||
})
|
||||
|
||||
export const options = option.object({
|
||||
model: option.string.layout({
|
||||
placeholder: 'Select a model',
|
||||
defaultValue: defaultOpenAIOptions.model,
|
||||
fetcher: 'fetchModels',
|
||||
}),
|
||||
messages: option
|
||||
.array(
|
||||
option.discriminatedUnion('role', [
|
||||
systemMessageItemSchema,
|
||||
userMessageItemSchema,
|
||||
assistantMessageItemSchema,
|
||||
dialogueMessageItemSchema,
|
||||
])
|
||||
)
|
||||
.layout({ accordion: 'Messages', itemLabel: 'message', isOrdered: true }),
|
||||
temperature: option.number.layout({
|
||||
accordion: 'Advanced settings',
|
||||
label: 'Temperature',
|
||||
direction: 'row',
|
||||
defaultValue: defaultOpenAIOptions.temperature,
|
||||
}),
|
||||
responseMapping: option
|
||||
.saveResponseArray(['Message content', 'Total tokens'])
|
||||
.layout({
|
||||
accordion: 'Save response',
|
||||
}),
|
||||
})
|
||||
|
||||
export const createChatCompletion = createAction({
|
||||
name: 'Create chat completion',
|
||||
auth,
|
||||
baseOptions,
|
||||
getSetVariableIds: (options) =>
|
||||
options.responseMapping?.map((res) => res.variableId).filter(isDefined) ??
|
||||
[],
|
||||
fetchers: [
|
||||
{
|
||||
id: 'fetchModels',
|
||||
dependencies: ['baseUrl', 'apiVersion'],
|
||||
fetch: async ({ credentials, options }) => {
|
||||
const baseUrl = options?.baseUrl ?? defaultOpenAIOptions.baseUrl
|
||||
const config = {
|
||||
apiKey: credentials.apiKey,
|
||||
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
|
||||
defaultHeaders: {
|
||||
'api-key': credentials.apiKey,
|
||||
},
|
||||
defaultQuery: options?.apiVersion
|
||||
? {
|
||||
'api-version': options.apiVersion,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ClientOptions
|
||||
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const models = await openai.models.list()
|
||||
|
||||
return (
|
||||
models.data
|
||||
.filter((model) => model.id.includes('gpt'))
|
||||
.sort((a, b) => b.created - a.created)
|
||||
.map((model) => model.id) ?? []
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
run: {
|
||||
server: async ({ credentials: { apiKey }, options, variables }) => {
|
||||
const config = {
|
||||
apiKey,
|
||||
baseURL: options.baseUrl,
|
||||
defaultHeaders: {
|
||||
'api-key': apiKey,
|
||||
},
|
||||
defaultQuery: options.apiVersion
|
||||
? {
|
||||
'api-version': options.apiVersion,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ClientOptions
|
||||
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: options.model ?? defaultOpenAIOptions.model,
|
||||
temperature: options.temperature
|
||||
? Number(options.temperature)
|
||||
: undefined,
|
||||
messages: parseChatCompletionMessages({ options, variables }),
|
||||
})
|
||||
|
||||
options.responseMapping?.forEach((mapping) => {
|
||||
if (!mapping.variableId) return
|
||||
if (!mapping.item || mapping.item === 'Message content')
|
||||
variables.set(mapping.variableId, response.choices[0].message.content)
|
||||
if (mapping.item === 'Total tokens')
|
||||
variables.set(mapping.variableId, response.usage?.total_tokens)
|
||||
})
|
||||
},
|
||||
stream: {
|
||||
getStreamVariableId: (options) =>
|
||||
options.responseMapping?.find((res) => res.item === 'Message content')
|
||||
?.variableId,
|
||||
run: async ({ credentials: { apiKey }, options, variables }) => {
|
||||
const config = {
|
||||
apiKey,
|
||||
baseURL: options.baseUrl,
|
||||
defaultHeaders: {
|
||||
'api-key': apiKey,
|
||||
},
|
||||
defaultQuery: options.apiVersion
|
||||
? {
|
||||
'api-version': options.apiVersion,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ClientOptions
|
||||
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: options.model ?? defaultOpenAIOptions.model,
|
||||
temperature: options.temperature
|
||||
? Number(options.temperature)
|
||||
: undefined,
|
||||
stream: true,
|
||||
messages: parseChatCompletionMessages({ options, variables }),
|
||||
})
|
||||
|
||||
return OpenAIStream(response)
|
||||
},
|
||||
},
|
||||
},
|
||||
options,
|
||||
})
|
105
packages/forge/blocks/openai/actions/createSpeech.tsx
Normal file
105
packages/forge/blocks/openai/actions/createSpeech.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { option, createAction } from '@typebot.io/forge'
|
||||
import { defaultOpenAIOptions, openAIVoices } from '../constants'
|
||||
import OpenAI, { ClientOptions } from 'openai'
|
||||
import { isNotEmpty, createId } from '@typebot.io/lib'
|
||||
import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket'
|
||||
import { auth } from '../auth'
|
||||
import { baseOptions } from '../baseOptions'
|
||||
|
||||
export const createSpeech = createAction({
|
||||
name: 'Create speech',
|
||||
auth,
|
||||
baseOptions,
|
||||
options: option.object({
|
||||
model: option.string.layout({
|
||||
fetcher: 'fetchSpeechModels',
|
||||
defaultValue: 'tts-1',
|
||||
placeholder: 'Select a model',
|
||||
}),
|
||||
input: option.string.layout({
|
||||
label: 'Input',
|
||||
input: 'textarea',
|
||||
}),
|
||||
voice: option.enum(openAIVoices).layout({
|
||||
label: 'Voice',
|
||||
placeholder: 'Select a voice',
|
||||
}),
|
||||
saveUrlInVariableId: option.string.layout({
|
||||
input: 'variableDropdown',
|
||||
label: 'Save URL in variable',
|
||||
}),
|
||||
}),
|
||||
getSetVariableIds: (options) =>
|
||||
options.saveUrlInVariableId ? [options.saveUrlInVariableId] : [],
|
||||
fetchers: [
|
||||
{
|
||||
id: 'fetchSpeechModels',
|
||||
dependencies: ['baseUrl', 'apiVersion'],
|
||||
fetch: async ({ credentials, options }) => {
|
||||
const baseUrl = options?.baseUrl ?? defaultOpenAIOptions.baseUrl
|
||||
const config = {
|
||||
apiKey: credentials.apiKey,
|
||||
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
|
||||
defaultHeaders: {
|
||||
'api-key': credentials.apiKey,
|
||||
},
|
||||
defaultQuery: options?.apiVersion
|
||||
? {
|
||||
'api-version': options.apiVersion,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ClientOptions
|
||||
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const models = await openai.models.list()
|
||||
|
||||
return (
|
||||
models.data
|
||||
.filter((model) => model.id.includes('tts'))
|
||||
.sort((a, b) => b.created - a.created)
|
||||
.map((model) => model.id) ?? []
|
||||
)
|
||||
},
|
||||
},
|
||||
],
|
||||
run: {
|
||||
server: async ({ credentials: { apiKey }, options, variables, logs }) => {
|
||||
if (!options.input) return logs.add('Create speech input is empty')
|
||||
if (!options.voice) return logs.add('Create speech voice is empty')
|
||||
if (!options.saveUrlInVariableId)
|
||||
return logs.add('Create speech save variable is empty')
|
||||
|
||||
const config = {
|
||||
apiKey,
|
||||
baseURL: options.baseUrl ?? defaultOpenAIOptions.baseUrl,
|
||||
defaultHeaders: {
|
||||
'api-key': apiKey,
|
||||
},
|
||||
defaultQuery: isNotEmpty(options.apiVersion)
|
||||
? {
|
||||
'api-version': options.apiVersion,
|
||||
}
|
||||
: undefined,
|
||||
} satisfies ClientOptions
|
||||
|
||||
const openai = new OpenAI(config)
|
||||
|
||||
const model = options.model ?? defaultOpenAIOptions.voiceModel
|
||||
|
||||
const rawAudio = (await openai.audio.speech.create({
|
||||
input: options.input,
|
||||
voice: options.voice,
|
||||
model,
|
||||
})) as any
|
||||
|
||||
const url = await uploadFileToBucket({
|
||||
file: Buffer.from((await rawAudio.arrayBuffer()) as ArrayBuffer),
|
||||
key: `tmp/openai/audio/${createId() + createId()}.mp3`,
|
||||
mimeType: 'audio/mpeg',
|
||||
})
|
||||
|
||||
variables.set(options.saveUrlInVariableId, url)
|
||||
},
|
||||
},
|
||||
})
|
Reference in New Issue
Block a user