2
0

Add "Generate variables" actions in AI blocks

Closes #1586
This commit is contained in:
Baptiste Arnaud
2024-06-18 12:13:00 +02:00
parent bec9cb68ca
commit 76fcf7ee93
25 changed files with 860 additions and 165 deletions

View File

@ -0,0 +1,173 @@
import {
HStack,
IconButton,
Wrap,
Text,
WrapItem,
Input,
} from '@chakra-ui/react'
import { useRef, useState } from 'react'
import { CloseIcon } from './icons'
import { colors } from '@/lib/theme'
import { AnimatePresence, motion } from 'framer-motion'
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
import { isEmpty } from '@typebot.io/lib/utils'
type Props = {
items?: string[]
placeholder?: string
onChange: (value: string[]) => void
}
export const TagsInput = ({ items, placeholder, onChange }: Props) => {
const inputRef = useRef<HTMLInputElement>(null)
const [inputValue, setInputValue] = useState('')
const [isFocused, setIsFocused] = useState(false)
const [focusedTagIndex, setFocusedTagIndex] = useState<number>()
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault()
setFocusedTagIndex(undefined)
setInputValue(e.target.value)
if (e.target.value.length - inputValue.length > 0) {
const values = convertStrToList(e.target.value)
if (values.length > 1) {
onChange([...(items ?? []), ...convertStrToList(e.target.value)])
setInputValue('')
}
}
}
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (!items) return
if (e.key === 'Backspace') {
if (focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
setFocusedTagIndex((idx) => idx! - 1)
}
removeItem(focusedTagIndex)
return
}
if (inputValue === '' && focusedTagIndex === undefined) {
setFocusedTagIndex(items?.length - 1)
return
}
}
if (e.key === 'ArrowLeft') {
if (focusedTagIndex !== undefined) {
if (focusedTagIndex === 0) return
setFocusedTagIndex(focusedTagIndex - 1)
return
}
if (inputRef.current?.selectionStart === 0 && items) {
setFocusedTagIndex(items.length - 1)
return
}
}
if (e.key === 'ArrowRight' && focusedTagIndex !== undefined) {
if (focusedTagIndex === items.length - 1) {
setFocusedTagIndex(undefined)
return
}
setFocusedTagIndex(focusedTagIndex + 1)
}
}
const removeItem = (index: number) => {
if (!items) return
const newItems = [...items]
newItems.splice(index, 1)
onChange(newItems)
}
const addItem = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isEmpty(inputValue)) return
setInputValue('')
onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()])
}
return (
<Wrap
spacing={1}
borderWidth={1}
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
p="2"
rounded="md"
borderColor={isFocused ? 'blue.500' : 'gray.200'}
transitionProperty="box-shadow, border-color"
transitionDuration="150ms"
transitionTimingFunction="ease-in-out"
onClick={() => inputRef.current?.focus()}
onKeyDown={handleKeyDown}
>
<AnimatePresence mode="popLayout">
{items?.map((item, index) => (
<motion.div
key={index}
initial={{ opacity: 0, transform: 'translateY(5px)' }}
animate={{ opacity: 1, transform: 'translateY(0)' }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1 }}
>
<WrapItem>
<Tag
content={item}
onDeleteClick={() => removeItem(index)}
isFocused={focusedTagIndex === index}
/>
</WrapItem>
</motion.div>
))}
</AnimatePresence>
<WrapItem>
<form onSubmit={addItem}>
<Input
ref={inputRef}
h="24px"
p="0"
borderWidth={0}
focusBorderColor="transparent"
size="sm"
value={inputValue}
onChange={handleInputChange}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder={items && items.length === 0 ? placeholder : undefined}
/>
</form>
</WrapItem>
</Wrap>
)
}
const Tag = ({
isFocused,
content,
onDeleteClick,
}: {
isFocused?: boolean
content: string
onDeleteClick: () => void
}) => (
<HStack
spacing={0.5}
borderWidth="1px"
pl="1"
rounded="sm"
maxW="100%"
borderColor={isFocused ? 'blue.500' : undefined}
boxShadow={isFocused ? `0 0 0 1px ${colors['blue'][500]}` : undefined}
>
<Text fontSize="sm" noOfLines={1}>
{content}
</Text>
<IconButton
size="xs"
icon={<CloseIcon />}
aria-label="Remove tag"
variant="ghost"
onClick={onDeleteClick}
/>
</HStack>
)

View File

@ -22,6 +22,7 @@ import { isEmpty } from '@typebot.io/lib'
import { useGraph } from '@/features/graph/providers/GraphProvider'
import { ButtonsItemSettings } from './ButtonsItemSettings'
import { useTranslate } from '@tolgee/react'
import { convertStrToList } from '@typebot.io/lib/convertStrToList'
type Props = {
item: ButtonItem
@ -70,26 +71,18 @@ export const ButtonsItemNode = ({ item, indices, isMouseOver }: Props) => {
const handleEditableChange = (val: string) => {
if (val.length - itemValue.length && val.endsWith('\n')) return
const splittedBreakLines = val.split('\n')
const splittedCommas = val.split(',')
const isPastingMultipleItems =
val.length - itemValue.length > 1 &&
(splittedBreakLines.length > 2 || splittedCommas.length > 2)
if (isPastingMultipleItems) {
const values =
splittedBreakLines.length > 2 ? splittedBreakLines : splittedCommas
return values.forEach((v, i) => {
if (i === 0) {
setItemValue(v)
if (val.length - itemValue.length === 1) return setItemValue(val)
const values = convertStrToList(val)
if (values.length === 1) {
setItemValue(values[0])
} else {
values.forEach((v, i) => {
createItem(
{ content: v.trim() },
{ content: v },
{ ...indices, itemIndex: indices.itemIndex + i }
)
}
})
}
setItemValue(val)
}
const handlePlusClick = () => {

View File

@ -24,10 +24,11 @@ import {
ForgedBlockDefinition,
ForgedBlock,
} from '@typebot.io/forge-repository/types'
import { PrimitiveList } from '@/components/PrimitiveList'
import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel'
import { CodeEditor } from '@/components/inputs/CodeEditor'
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'
import { TagsInput } from '@/components/TagsInput'
import { PrimitiveList } from '@/components/PrimitiveList'
const mdComponents = {
a: ({ href, children }) => (
@ -316,9 +317,23 @@ const ZodArrayContent = ({
const type = schema._def.type._def.innerType?._def.typeName
if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum')
return (
<Stack spacing={0}>
<Stack
spacing={0}
marginTop={layout?.mergeWithLastField ? '-3' : undefined}
>
{layout?.label && <FormLabel>{layout.label}</FormLabel>}
<Stack p="4" rounded="md" flex="1" borderWidth="1px">
<Stack
p="4"
rounded="md"
flex="1"
borderWidth="1px"
borderTopWidth={layout?.mergeWithLastField ? '0' : undefined}
borderTopRadius={layout?.mergeWithLastField ? '0' : undefined}
pt={layout?.mergeWithLastField ? '5' : undefined}
>
{type === 'ZodString' ? (
<TagsInput items={data} onChange={onDataChange} />
) : (
<PrimitiveList
onItemsChange={(items) => {
onDataChange(items)
@ -338,6 +353,7 @@ const ZodArrayContent = ({
/>
)}
</PrimitiveList>
)}
</Stack>
</Stack>
)

View File

@ -2,10 +2,9 @@ import { FormControl, FormLabel, Stack } from '@chakra-ui/react'
import { Settings } from '@typebot.io/schemas'
import React from 'react'
import { isDefined } from '@typebot.io/lib'
import { TextInput } from '@/components/inputs'
import { env } from '@typebot.io/env'
import { MoreInfoTooltip } from '@/components/MoreInfoTooltip'
import { PrimitiveList } from '@/components/PrimitiveList'
import { TagsInput } from '@/components/TagsInput'
import { env } from '@typebot.io/env'
type Props = {
security: Settings['security']
@ -30,20 +29,11 @@ export const SecurityForm = ({ security, onUpdate }: Props) => {
By default your bot can be executed on any website.
</MoreInfoTooltip>
</FormLabel>
<PrimitiveList
initialItems={security?.allowedOrigins}
onItemsChange={updateItems}
addLabel="Add URL"
>
{({ item, onItemChange }) => (
<TextInput
width="full"
defaultValue={item}
onChange={onItemChange}
<TagsInput
items={security?.allowedOrigins}
onChange={updateItems}
placeholder={env.NEXT_PUBLIC_VIEWER_URL[0]}
/>
)}
</PrimitiveList>
</FormControl>
</Stack>
)

View File

@ -21,7 +21,7 @@
"@typebot.io/js": "workspace:*",
"@typebot.io/nextjs": "workspace:*",
"@typebot.io/prisma": "workspace:*",
"ai": "3.1.12",
"ai": "3.1.34",
"bot-engine": "workspace:*",
"cors": "2.8.5",
"google-spreadsheet": "4.1.1",

View File

@ -20,7 +20,7 @@
"@typebot.io/variables": "workspace:*",
"@udecode/plate-common": "30.4.5",
"@typebot.io/logic": "workspace:*",
"ai": "3.1.12",
"ai": "3.1.34",
"chrono-node": "2.7.6",
"date-fns": "2.30.0",
"date-fns-tz": "2.0.0",

View File

@ -0,0 +1,42 @@
import { createAction } from '@typebot.io/forge'
import { auth } from '../auth'
import { isDefined } from '@typebot.io/lib'
import { createAnthropic } from '@ai-sdk/anthropic'
import { parseGenerateVariablesOptions } from '@typebot.io/openai-block/shared/parseGenerateVariablesOptions'
import { runGenerateVariables } from '@typebot.io/openai-block/shared/runGenerateVariables'
import { anthropicModels } from '../constants'
export const generateVariables = createAction({
name: 'Generate variables',
auth,
options: parseGenerateVariablesOptions({ modelFetch: anthropicModels }),
turnableInto: [
{
blockId: 'openai',
},
{
blockId: 'mistral',
},
],
getSetVariableIds: (options) =>
options.variablesToExtract?.map((v) => v.variableId).filter(isDefined) ??
[],
run: {
server: ({ credentials, options, variables, logs }) => {
if (credentials?.apiKey === undefined)
return logs.add('No API key provided')
if (options.model === undefined) return logs.add('No model provided')
return runGenerateVariables({
model: createAnthropic({
apiKey: credentials.apiKey,
})(options.model),
prompt: options.prompt,
variablesToExtract: options.variablesToExtract,
variables,
logs,
})
},
},
})

View File

@ -2,6 +2,7 @@ import { createBlock } from '@typebot.io/forge'
import { AnthropicLogo } from './logo'
import { auth } from './auth'
import { createChatMessage } from './actions/createChatMessage'
import { generateVariables } from './actions/generateVariables'
export const anthropicBlock = createBlock({
id: 'anthropic',
@ -9,5 +10,5 @@ export const anthropicBlock = createBlock({
tags: ['ai', 'chat', 'completion', 'claude', 'anthropic'],
LightLogo: AnthropicLogo,
auth,
actions: [createChatMessage],
actions: [createChatMessage, generateVariables],
})

View File

@ -16,7 +16,9 @@
},
"dependencies": {
"@anthropic-ai/sdk": "0.20.6",
"ai": "3.1.12",
"@ai-sdk/anthropic": "0.0.21",
"@typebot.io/openai-block": "workspace:*",
"ai": "3.1.34",
"ky": "1.2.4"
}
}

View File

@ -14,6 +14,6 @@
"typescript": "5.4.5"
},
"dependencies": {
"ai": "3.1.12"
"ai": "3.1.34"
}
}

View File

@ -3,9 +3,8 @@ import { isDefined } from '@typebot.io/lib'
import { auth } from '../auth'
import { parseMessages } from '../helpers/parseMessages'
import { createMistral } from '@ai-sdk/mistral'
import { apiBaseUrl } from '../constants'
import ky from 'ky'
import { generateText, streamText } from 'ai'
import { fetchModels } from '../helpers/fetchModels'
const nativeMessageContentSchema = {
content: option.string.layout({
@ -98,19 +97,7 @@ export const createChatCompletion = createAction({
{
id: 'fetchModels',
dependencies: [],
fetch: async ({ credentials }) => {
if (!credentials?.apiKey) return []
const { data } = await ky
.get(apiBaseUrl + '/v1/models', {
headers: {
Authorization: `Bearer ${credentials.apiKey}`,
},
})
.json<{ data: { id: string }[] }>()
return data.map((model) => model.id)
},
fetch: fetchModels,
},
],
run: {

View File

@ -0,0 +1,53 @@
import { createAction } from '@typebot.io/forge'
import { auth } from '../auth'
import { isDefined } from '@typebot.io/lib'
import { createMistral } from '@ai-sdk/mistral'
import { fetchModels } from '../helpers/fetchModels'
import { parseGenerateVariablesOptions } from '@typebot.io/openai-block/shared/parseGenerateVariablesOptions'
import { runGenerateVariables } from '@typebot.io/openai-block/shared/runGenerateVariables'
export const generateVariables = createAction({
name: 'Generate variables',
auth,
options: parseGenerateVariablesOptions({ modelFetch: 'fetchModels' }),
fetchers: [
{
id: 'fetchModels',
dependencies: [],
fetch: fetchModels,
},
],
turnableInto: [
{
blockId: 'openai',
},
{
blockId: 'anthropic',
transform: (options) => ({
...options,
model: undefined,
}),
},
],
getSetVariableIds: (options) =>
options.variablesToExtract?.map((v) => v.variableId).filter(isDefined) ??
[],
run: {
server: ({ credentials, options, variables, logs }) => {
if (credentials?.apiKey === undefined)
return logs.add('No API key provided')
if (options.model === undefined) return logs.add('No model provided')
return runGenerateVariables({
model: createMistral({
apiKey: credentials.apiKey,
})(options.model),
variablesToExtract: options.variablesToExtract,
prompt: options.prompt,
variables,
logs,
})
},
},
})

View File

@ -0,0 +1,20 @@
import ky from 'ky'
import { apiBaseUrl } from '../constants'
export const fetchModels = async ({
credentials,
}: {
credentials?: { apiKey?: string }
}) => {
if (!credentials?.apiKey) return []
const { data } = await ky
.get(apiBaseUrl + '/v1/models', {
headers: {
Authorization: `Bearer ${credentials.apiKey}`,
},
})
.json<{ data: { id: string }[] }>()
return data.map((model) => model.id)
}

View File

@ -2,6 +2,7 @@ import { createBlock } from '@typebot.io/forge'
import { MistralLogo } from './logo'
import { auth } from './auth'
import { createChatCompletion } from './actions/createChatCompletion'
import { generateVariables } from './actions/generateVariables'
export const mistralBlock = createBlock({
id: 'mistral',
@ -9,6 +10,6 @@ export const mistralBlock = createBlock({
tags: ['ai', 'chat', 'completion'],
LightLogo: MistralLogo,
auth,
actions: [createChatCompletion],
actions: [createChatCompletion, generateVariables],
docsUrl: 'https://docs.typebot.io/forge/blocks/mistral',
})

View File

@ -14,8 +14,9 @@
"typescript": "5.4.5"
},
"dependencies": {
"@ai-sdk/mistral": "0.0.11",
"ai": "3.1.12",
"@ai-sdk/mistral": "0.0.18",
"@typebot.io/openai-block": "workspace:*",
"ai": "3.1.34",
"ky": "1.2.4"
}
}

View File

@ -1,5 +1,4 @@
import { createAction } from '@typebot.io/forge'
import OpenAI, { ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '../constants'
import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
@ -8,6 +7,7 @@ import { getChatCompletionSetVarIds } from '../shared/getChatCompletionSetVarIds
import { runChatCompletion } from '../shared/runChatCompletion'
import { runChatCompletionStream } from '../shared/runChatCompletionStream'
import { getChatCompletionStreamVarId } from '../shared/getChatCompletionStreamVarId'
import { fetchGPTModels } from '../helpers/fetchModels'
export const createChatCompletion = createAction({
name: 'Create chat completion',
@ -45,34 +45,12 @@ export const createChatCompletion = createAction({
{
id: 'fetchModels',
dependencies: ['baseUrl', 'apiVersion'],
fetch: async ({ credentials, options }) => {
if (!credentials?.apiKey) return []
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) ?? []
)
},
fetch: ({ credentials, options }) =>
fetchGPTModels({
apiKey: credentials?.apiKey,
baseUrl: options.baseUrl,
apiVersion: options.apiVersion,
}),
},
],
run: {

View File

@ -0,0 +1,61 @@
import { createAction } from '@typebot.io/forge'
import { auth } from '../auth'
import { baseOptions } from '../baseOptions'
import { fetchGPTModels } from '../helpers/fetchModels'
import { isDefined } from '@typebot.io/lib'
import { runGenerateVariables } from '../shared/runGenerateVariables'
import { parseGenerateVariablesOptions } from '../shared/parseGenerateVariablesOptions'
import { createOpenAI } from '@ai-sdk/openai'
export const generateVariables = createAction({
name: 'Generate variables',
auth,
baseOptions,
options: parseGenerateVariablesOptions({ modelFetch: 'fetchModels' }),
fetchers: [
{
id: 'fetchModels',
dependencies: ['baseUrl', 'apiVersion'],
fetch: ({ credentials, options }) =>
fetchGPTModels({
apiKey: credentials?.apiKey,
baseUrl: options.baseUrl,
apiVersion: options.apiVersion,
}),
},
],
turnableInto: [
{
blockId: 'mistral',
},
{
blockId: 'anthropic',
transform: (options) => ({
...options,
model: undefined,
}),
},
],
getSetVariableIds: (options) =>
options.variablesToExtract?.map((v) => v.variableId).filter(isDefined) ??
[],
run: {
server: ({ credentials, options, variables, logs }) => {
if (credentials?.apiKey === undefined)
return logs.add('No API key provided')
if (options.model === undefined) return logs.add('No model provided')
return runGenerateVariables({
model: createOpenAI({
apiKey: credentials.apiKey,
compatibility: 'strict',
})(options.model),
prompt: options.prompt,
variablesToExtract: options.variablesToExtract,
variables,
logs,
})
},
},
})

View File

@ -0,0 +1,40 @@
import OpenAI, { ClientOptions } from 'openai'
import { defaultOpenAIOptions } from '../constants'
type Props = {
apiKey?: string
baseUrl?: string
apiVersion?: string
}
export const fetchGPTModels = async ({
apiKey,
baseUrl = defaultOpenAIOptions.baseUrl,
apiVersion,
}: Props) => {
if (!apiKey) return []
const config = {
apiKey: apiKey,
baseURL: baseUrl ?? defaultOpenAIOptions.baseUrl,
defaultHeaders: {
'api-key': apiKey,
},
defaultQuery: apiVersion
? {
'api-version': 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) ?? []
)
}

View File

@ -5,6 +5,7 @@ import { createBlock } from '@typebot.io/forge'
import { auth } from './auth'
import { baseOptions } from './baseOptions'
import { askAssistant } from './actions/askAssistant'
import { generateVariables } from './actions/generateVariables'
export const openAIBlock = createBlock({
id: 'openai' as const,
@ -14,6 +15,11 @@ export const openAIBlock = createBlock({
DarkLogo: OpenAIDarkLogo,
auth,
options: baseOptions,
actions: [createChatCompletion, askAssistant, createSpeech],
actions: [
createChatCompletion,
askAssistant,
generateVariables,
createSpeech,
],
docsUrl: 'https://docs.typebot.io/forge/blocks/openai',
})

View File

@ -7,16 +7,17 @@
"author": "Baptiste Arnaud",
"license": "AGPL-3.0-or-later",
"dependencies": {
"ai": "3.1.12",
"@ai-sdk/openai": "0.0.31",
"ai": "3.1.34",
"openai": "4.47.1"
},
"devDependencies": {
"@typebot.io/forge": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@types/react": "18.2.15",
"typescript": "5.4.5",
"@typebot.io/lib": "workspace:*",
"@typebot.io/tsconfig": "workspace:*",
"@typebot.io/variables": "workspace:*",
"ky": "1.2.4"
"@types/react": "18.2.15",
"ky": "1.2.4",
"typescript": "5.4.5"
}
}

View File

@ -0,0 +1,96 @@
import { option } from '@typebot.io/forge'
import { z } from '@typebot.io/forge/zod'
import { baseOptions } from '../baseOptions'
const extractInfoBaseShape = {
variableId: option.string.layout({
inputType: 'variableDropdown',
}),
description: option.string.layout({
label: 'Description',
accordion: 'Advanced',
}),
isRequired: option.boolean.layout({
label: 'Is required',
moreInfoTooltip:
'If set to false, there is a chance the variable will be empty',
accordion: 'Advanced',
defaultValue: true,
}),
}
export const toolParametersSchema = option
.array(
option.discriminatedUnion('type', [
option
.object({
type: option.literal('string'),
})
.extend(extractInfoBaseShape),
option
.object({
type: option.literal('number'),
})
.extend(extractInfoBaseShape),
option
.object({
type: option.literal('boolean'),
})
.extend(extractInfoBaseShape),
option
.object({
type: option.literal('enum'),
values: option
.array(option.string)
.layout({ itemLabel: 'possible value', mergeWithLastField: true }),
})
.extend(extractInfoBaseShape),
])
)
.layout({
itemLabel: 'variable mapping',
accordion: 'Schema',
})
type Props = {
defaultModel?: string
modelFetch: string | readonly [string, ...string[]]
modelHelperText?: string
}
export const parseGenerateVariablesOptions = ({
defaultModel,
modelFetch,
modelHelperText,
}: Props) =>
option.object({
model:
typeof modelFetch === 'string'
? option.string.layout({
placeholder: 'Select a model',
label: 'Model',
defaultValue: defaultModel,
fetcher: modelFetch,
helperText: modelHelperText,
})
: option.enum(modelFetch).layout({
placeholder: 'Select a model',
label: 'Model',
defaultValue: defaultModel,
helperText: modelHelperText,
}),
prompt: option.string.layout({
label: 'Prompt',
placeholder: 'Type your text here',
inputType: 'textarea',
isRequired: true,
moreInfoTooltip:
'Meant to guide the model on what to generate. i.e. "Generate a role-playing game character", "Extract the company name from this text", etc.',
}),
variablesToExtract: toolParametersSchema,
})
export type GenerateVariablesOptions = z.infer<
ReturnType<typeof parseGenerateVariablesOptions>
> &
z.infer<typeof baseOptions>

View File

@ -0,0 +1,101 @@
import { LogsStore, VariableStore } from '@typebot.io/forge/types'
import {
GenerateVariablesOptions,
toolParametersSchema,
} from './parseGenerateVariablesOptions'
import { generateObject, LanguageModel } from 'ai'
import { Variable } from '@typebot.io/variables/types'
import { z } from '@typebot.io/forge/zod'
import { isNotEmpty } from '@typebot.io/lib/utils'
type Props = {
model: LanguageModel
variables: VariableStore
logs: LogsStore
} & Pick<GenerateVariablesOptions, 'variablesToExtract' | 'prompt'>
export const runGenerateVariables = async ({
variablesToExtract,
model,
prompt,
variables: variablesStore,
logs,
}: Props) => {
if (!prompt) return logs.add('No prompt provided')
const variables = variablesStore.list()
const schema = convertVariablesToExtractToSchema({
variablesToExtract,
variables,
})
if (!schema) {
logs.add('Could not parse variables to extract')
return
}
const hasOptionalVariables = variablesToExtract?.some(
(variableToExtract) => variableToExtract.isRequired === false
)
const { object } = await generateObject({
model,
schema,
prompt:
`${prompt}\n\nYou should generate a JSON object` +
(hasOptionalVariables
? ' and provide empty values if the information is not there or if you are unsure.'
: '.'),
})
Object.entries(object).forEach(([key, value]) => {
if (value === null) return
const existingVariable = variables.find((v) => v.name === key)
if (!existingVariable) return
variablesStore.set(existingVariable.id, value)
})
}
const convertVariablesToExtractToSchema = ({
variablesToExtract,
variables,
}: {
variablesToExtract: z.infer<typeof toolParametersSchema> | undefined
variables: Variable[]
}): z.ZodTypeAny | undefined => {
if (!variablesToExtract || variablesToExtract?.length === 0) return
const shape: z.ZodRawShape = {}
variablesToExtract.forEach((variableToExtract) => {
if (!variableToExtract) return
const matchingVariable = variables.find(
(v) => v.id === variableToExtract.variableId
)
if (!matchingVariable) return
switch (variableToExtract.type) {
case 'string':
shape[matchingVariable.name] = z.string()
break
case 'number':
shape[matchingVariable.name] = z.number()
break
case 'boolean':
shape[matchingVariable.name] = z.boolean()
break
case 'enum': {
if (!variableToExtract.values || variableToExtract.values.length === 0)
return
shape[matchingVariable.name] = z.enum(variableToExtract.values as any)
break
}
}
if (variableToExtract.isRequired === false)
shape[matchingVariable.name] = shape[matchingVariable.name].optional()
if (isNotEmpty(variableToExtract.description))
shape[matchingVariable.name] = shape[matchingVariable.name].describe(
variableToExtract.description
)
})
return z.object(shape)
}

View File

@ -23,6 +23,7 @@ export interface ZodLayoutMetadata<
isHidden?: boolean | ((currentObj: Record<string, any>) => boolean)
isDebounceDisabled?: boolean
hiddenItems?: string[]
mergeWithLastField?: boolean
}
declare module 'zod' {

View File

@ -0,0 +1,13 @@
export const convertStrToList = (str: string): string[] => {
const splittedBreakLines = str.split('\n')
const splittedCommas = str.split(',')
const isPastingMultipleItems =
str.length > 1 &&
(splittedBreakLines.length >= 2 || splittedCommas.length >= 2)
if (isPastingMultipleItems) {
const values =
splittedBreakLines.length >= 2 ? splittedBreakLines : splittedCommas
return values.map((v) => v.trim())
}
return [str.trim()]
}

213
pnpm-lock.yaml generated
View File

@ -421,8 +421,8 @@ importers:
specifier: workspace:*
version: link:../../packages/prisma
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
bot-engine:
specifier: workspace:*
version: link:../../packages/deprecated/bot-engine
@ -754,8 +754,8 @@ importers:
specifier: 30.4.5
version: 30.4.5(@types/react@18.2.15)(immer@10.0.2)(react-dom@18.2.0)(react@18.2.0)(scheduler@0.23.0)(slate-history@0.100.0)(slate-hyperscript@0.100.0)(slate-react@0.102.0)(slate@0.102.0)
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
chrono-node:
specifier: 2.7.6
version: 2.7.6
@ -1284,12 +1284,18 @@ importers:
packages/forge/blocks/anthropic:
dependencies:
'@ai-sdk/anthropic':
specifier: 0.0.21
version: 0.0.21(zod@3.22.4)
'@anthropic-ai/sdk':
specifier: 0.20.6
version: 0.20.6
'@typebot.io/openai-block':
specifier: workspace:*
version: link:../openai
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
ky:
specifier: 1.2.4
version: 1.2.4
@ -1355,8 +1361,8 @@ importers:
packages/forge/blocks/difyAi:
dependencies:
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
devDependencies:
'@typebot.io/forge':
specifier: workspace:*
@ -1402,11 +1408,14 @@ importers:
packages/forge/blocks/mistral:
dependencies:
'@ai-sdk/mistral':
specifier: 0.0.11
version: 0.0.11(zod@3.22.4)
specifier: 0.0.18
version: 0.0.18(zod@3.22.4)
'@typebot.io/openai-block':
specifier: workspace:*
version: link:../openai
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
ky:
specifier: 1.2.4
version: 1.2.4
@ -1478,9 +1487,12 @@ importers:
packages/forge/blocks/openai:
dependencies:
'@ai-sdk/openai':
specifier: 0.0.31
version: 0.0.31(zod@3.22.4)
ai:
specifier: 3.1.12
version: 3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
specifier: 3.1.34
version: 3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4)
openai:
specifier: 4.47.1
version: 4.47.1
@ -2038,22 +2050,41 @@ packages:
resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==}
engines: {node: '>=0.10.0'}
/@ai-sdk/mistral@0.0.11(zod@3.22.4):
resolution: {integrity: sha512-Y/2V7BMwyaTnVRfhs6YhI5MGcQgjBrTwo2K6CftUAjJvNIDtSJ+eVc9hCuw02uMvTScfeiTB6lZ8OrzGyCNHoQ==}
/@ai-sdk/anthropic@0.0.21(zod@3.22.4):
resolution: {integrity: sha512-QjVnTbfbAmfMjDqLbcZFC4pKBvp4RqzrZJQF3mzulSkeXWqNZo9G9oV7W1PDbMA3o+DJdxRFyTK43aKUBCP31Q==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
peerDependenciesMeta:
zod:
optional: true
dependencies:
'@ai-sdk/provider': 0.0.5
'@ai-sdk/provider-utils': 0.0.8(zod@3.22.4)
'@ai-sdk/provider': 0.0.10
'@ai-sdk/provider-utils': 0.0.15(zod@3.22.4)
zod: 3.22.4
dev: false
/@ai-sdk/provider-utils@0.0.8(zod@3.22.4):
resolution: {integrity: sha512-J/ZNvFhORd3gCeK3jFvNrxp1Dnvy6PvPq21RJ+OsIEjsoHeKQaHALCWG0aJunXDuzd+Mck/lCg7LqA0qmIbHIg==}
/@ai-sdk/mistral@0.0.18(zod@3.22.4):
resolution: {integrity: sha512-aNbdyINZU2Kmv6+uLEEbvQJxHChYf1RofIETYAmCZcOk3wU1gReWSjZK7eP9BzehXg1TkeF1UpT60bnzl0++Pg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
dependencies:
'@ai-sdk/provider': 0.0.10
'@ai-sdk/provider-utils': 0.0.15(zod@3.22.4)
zod: 3.22.4
dev: false
/@ai-sdk/openai@0.0.31(zod@3.22.4):
resolution: {integrity: sha512-7ehX2N0NzCdxUOYXutwYgu6gdWO+zS/v8pWEd7VW8QpNq3equ0VZ0j+pDUNv4f3GJ449QwySb6+V+DHM9W/pLg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
dependencies:
'@ai-sdk/provider': 0.0.10
'@ai-sdk/provider-utils': 0.0.15(zod@3.22.4)
zod: 3.22.4
dev: false
/@ai-sdk/provider-utils@0.0.13(zod@3.22.4):
resolution: {integrity: sha512-cB2dPm9flj+yin5sjBLFcXdW8sZtAXLE/OLKgz9uHpHM55s7mnwZrDGfO6ot/ukHTxDDJunZLW7qSjgK/u0F1g==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -2061,20 +2092,112 @@ packages:
zod:
optional: true
dependencies:
'@ai-sdk/provider': 0.0.5
'@ai-sdk/provider': 0.0.10
eventsource-parser: 1.1.2
nanoid: 3.3.6
secure-json-parse: 2.7.0
zod: 3.22.4
dev: false
/@ai-sdk/provider@0.0.5:
resolution: {integrity: sha512-TZDldBZ5clAsNwJ2PSeo/b1uILj9a2lvi0rnOj2RCNZDgaVqFRVIAnKyeHusCRv2lzhPIw03B3fiGI6VoLzOAA==}
/@ai-sdk/provider-utils@0.0.15(zod@3.22.4):
resolution: {integrity: sha512-eTkIaZc/Ud96DYG40lLuKWJvZ2GoW/wT4KH9r1f3wGUhj5wgQN+bzgdI57z60VOEDuMmDVuILVnTLFe0HNT5Iw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
peerDependenciesMeta:
zod:
optional: true
dependencies:
'@ai-sdk/provider': 0.0.10
eventsource-parser: 1.1.2
nanoid: 3.3.6
secure-json-parse: 2.7.0
zod: 3.22.4
dev: false
/@ai-sdk/provider@0.0.10:
resolution: {integrity: sha512-NzkrtREQpHID1cTqY/C4CI30PVOaXWKYytDR2EcytmFgnP7Z6+CrGIA/YCnNhYAuUm6Nx+nGpRL/Hmyrv7NYzg==}
engines: {node: '>=18'}
dependencies:
json-schema: 0.4.0
dev: false
/@ai-sdk/react@0.0.1(react@18.2.0)(zod@3.22.4):
resolution: {integrity: sha512-y6KXzxRR7vmAgDVnS/hnLPt3RztvWOisANBw47O1o1D2nDeUqTo8E/SNw2J8mzzlRInGaw40EREY8jEf9AcwWQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19
peerDependenciesMeta:
react:
optional: true
dependencies:
'@ai-sdk/provider-utils': 0.0.13(zod@3.22.4)
'@ai-sdk/ui-utils': 0.0.1(zod@3.22.4)
react: 18.2.0
swr: 2.2.0(react@18.2.0)
transitivePeerDependencies:
- zod
dev: false
/@ai-sdk/solid@0.0.1(solid-js@1.7.8)(zod@3.22.4):
resolution: {integrity: sha512-5WWdoqpemYW66rMZUYF4sbDtZfF96Vt8RtrzpLv+95ZUM1nY1elxAWpHCeOyYEjWJE5+eiKpUs6Jr5mP2/gz8Q==}
engines: {node: '>=18'}
peerDependencies:
solid-js: ^1.7.7
peerDependenciesMeta:
solid-js:
optional: true
dependencies:
'@ai-sdk/ui-utils': 0.0.1(zod@3.22.4)
solid-js: 1.7.8
solid-swr-store: 0.10.7(solid-js@1.7.8)(swr-store@0.10.6)
swr-store: 0.10.6
transitivePeerDependencies:
- zod
dev: false
/@ai-sdk/svelte@0.0.1(svelte@4.2.12)(zod@3.22.4):
resolution: {integrity: sha512-bpjTLKOwdcXjJzboq15etT1hdnRI1ErPZweWSsu1/LJlEFzD1M0qpZQwWHwPquYkzeppXOgsLrUZ+9D2RoC47Q==}
engines: {node: '>=18'}
peerDependencies:
svelte: ^3.0.0 || ^4.0.0
peerDependenciesMeta:
svelte:
optional: true
dependencies:
'@ai-sdk/provider-utils': 0.0.13(zod@3.22.4)
'@ai-sdk/ui-utils': 0.0.1(zod@3.22.4)
sswr: 2.1.0(svelte@4.2.12)
svelte: 4.2.12
transitivePeerDependencies:
- zod
dev: false
/@ai-sdk/ui-utils@0.0.1(zod@3.22.4):
resolution: {integrity: sha512-zOr1zIw/EH4fEQvDKsqYG3wY7GW32h8Wrx0lQpSAP59UCA4zgHBH6ogF5oj7+LUuWjT6be9S0G3l/tEPyRyxEw==}
engines: {node: '>=18'}
dependencies:
'@ai-sdk/provider-utils': 0.0.13(zod@3.22.4)
transitivePeerDependencies:
- zod
dev: false
/@ai-sdk/vue@0.0.1(vue@3.4.21)(zod@3.22.4):
resolution: {integrity: sha512-B3qAW22FYGy1ltobnF7LiPAmARTrCkH15qjw4WAXCnvRohsYOFTDACOBEsXRfa1OHmqWsUOYeNtE/oPhK3ybqw==}
engines: {node: '>=18'}
peerDependencies:
vue: ^3.3.4
peerDependenciesMeta:
vue:
optional: true
dependencies:
'@ai-sdk/ui-utils': 0.0.1(zod@3.22.4)
swrv: 1.0.4(vue@3.4.21)
vue: 3.4.21(typescript@5.4.5)
transitivePeerDependencies:
- zod
dev: false
/@alloc/quick-lru@5.2.0:
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
@ -10282,32 +10405,31 @@ packages:
indent-string: 5.0.0
dev: true
/ai@3.1.12(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4):
resolution: {integrity: sha512-XlurBw1sdgQCFmCTPYjKjpm+fPS6iY+tLb/PYNUEjZn3bhqosAkcqUkGJTsFP49OMAWO1Lm2oPthCakKcn6Lzw==}
/ai@3.1.34(openai@4.47.1)(react@18.2.0)(solid-js@1.7.8)(svelte@4.2.12)(vue@3.4.21)(zod@3.22.4):
resolution: {integrity: sha512-cFE33IvG3fcUwvVF932AuvFKbKsKi4YK42Vh95Nh0SpWowEumL99dHh7i3LAjS7yqvC+n8n30t+iVO66Ij5PHg==}
engines: {node: '>=18'}
peerDependencies:
openai: ^4.42.0
react: ^18.2.0
solid-js: ^1.7.7
react: ^18 || ^19
svelte: ^3.0.0 || ^4.0.0
vue: ^3.3.4
zod: ^3.0.0
peerDependenciesMeta:
openai:
optional: true
react:
optional: true
solid-js:
optional: true
svelte:
optional: true
vue:
optional: true
zod:
optional: true
dependencies:
'@ai-sdk/provider': 0.0.5
'@ai-sdk/provider-utils': 0.0.8(zod@3.22.4)
'@ai-sdk/provider': 0.0.10
'@ai-sdk/provider-utils': 0.0.13(zod@3.22.4)
'@ai-sdk/react': 0.0.1(react@18.2.0)(zod@3.22.4)
'@ai-sdk/solid': 0.0.1(solid-js@1.7.8)(zod@3.22.4)
'@ai-sdk/svelte': 0.0.1(svelte@4.2.12)(zod@3.22.4)
'@ai-sdk/ui-utils': 0.0.1(zod@3.22.4)
'@ai-sdk/vue': 0.0.1(vue@3.4.21)(zod@3.22.4)
eventsource-parser: 1.1.2
json-schema: 0.4.0
jsondiffpatch: 0.6.0
@ -10315,16 +10437,13 @@ packages:
openai: 4.47.1
react: 18.2.0
secure-json-parse: 2.7.0
solid-js: 1.7.8
solid-swr-store: 0.10.7(solid-js@1.7.8)(swr-store@0.10.6)
sswr: 2.0.0(svelte@4.2.12)
sswr: 2.1.0(svelte@4.2.12)
svelte: 4.2.12
swr: 2.2.0(react@18.2.0)
swr-store: 0.10.6
swrv: 1.0.4(vue@3.4.21)
vue: 3.4.21(typescript@5.4.5)
zod: 3.22.4
zod-to-json-schema: 3.22.5(zod@3.22.4)
transitivePeerDependencies:
- solid-js
- vue
dev: false
/ajv-draft-04@1.0.0(ajv@8.12.0):
@ -20320,10 +20439,10 @@ packages:
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
dev: true
/sswr@2.0.0(svelte@4.2.12):
resolution: {integrity: sha512-mV0kkeBHcjcb0M5NqKtKVg/uTIYNlIIniyDfSGrSfxpEdM9C365jK0z55pl9K0xAkNTJi2OAOVFQpgMPUk+V0w==}
/sswr@2.1.0(svelte@4.2.12):
resolution: {integrity: sha512-Cqc355SYlTAaUt8iDPaC/4DPPXK925PePLMxyBKuWd5kKc5mwsG3nT9+Mq2tyguL5s7b4Jg+IRMpTRsNTAfpSQ==}
peerDependencies:
svelte: ^4.0.0
svelte: ^4.0.0 || ^5.0.0-next.0
dependencies:
svelte: 4.2.12
swrev: 4.0.0