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