2
0

Introducing The Forge (#1072)

The Forge allows anyone to easily create their own Typebot Block.

Closes #380
This commit is contained in:
Baptiste Arnaud
2023-12-13 10:22:02 +01:00
committed by GitHub
parent c373108b55
commit 5e019bbb22
184 changed files with 42659 additions and 37411 deletions

View 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,
})

View 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)
},
},
})