diff --git a/apps/builder/src/components/TagsInput.tsx b/apps/builder/src/components/TagsInput.tsx new file mode 100644 index 000000000..c35ac010a --- /dev/null +++ b/apps/builder/src/components/TagsInput.tsx @@ -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(null) + const [inputValue, setInputValue] = useState('') + const [isFocused, setIsFocused] = useState(false) + const [focusedTagIndex, setFocusedTagIndex] = useState() + + const handleInputChange = (e: React.ChangeEvent) => { + 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) => { + 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) => { + e.preventDefault() + if (isEmpty(inputValue)) return + setInputValue('') + onChange(items ? [...items, inputValue.trim()] : [inputValue.trim()]) + } + + return ( + inputRef.current?.focus()} + onKeyDown={handleKeyDown} + > + + {items?.map((item, index) => ( + + + removeItem(index)} + isFocused={focusedTagIndex === index} + /> + + + ))} + + +
+ setIsFocused(true)} + onBlur={() => setIsFocused(false)} + placeholder={items && items.length === 0 ? placeholder : undefined} + /> +
+
+
+ ) +} + +const Tag = ({ + isFocused, + content, + onDeleteClick, +}: { + isFocused?: boolean + content: string + onDeleteClick: () => void +}) => ( + + + {content} + + } + aria-label="Remove tag" + variant="ghost" + onClick={onDeleteClick} + /> + +) diff --git a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx index 778631bf1..a6db87283 100644 --- a/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx +++ b/apps/builder/src/features/blocks/inputs/buttons/components/ButtonsItemNode.tsx @@ -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) - } else { - createItem( - { content: v.trim() }, - { ...indices, itemIndex: indices.itemIndex + i } - ) - } + 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 }, + { ...indices, itemIndex: indices.itemIndex + i } + ) }) } - setItemValue(val) } const handlePlusClick = () => { diff --git a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx index da36873f5..3d3ff2f7c 100644 --- a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx +++ b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx @@ -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,28 +317,43 @@ const ZodArrayContent = ({ const type = schema._def.type._def.innerType?._def.typeName if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum') return ( - + {layout?.label && {layout.label}} - - { - onDataChange(items) - }} - initialItems={data} - addLabel={`Add ${layout?.itemLabel ?? ''}`} - > - {({ item, onItemChange }) => ( - - )} - + + {type === 'ZodString' ? ( + + ) : ( + { + onDataChange(items) + }} + initialItems={data} + addLabel={`Add ${layout?.itemLabel ?? ''}`} + > + {({ item, onItemChange }) => ( + + )} + + )} ) diff --git a/apps/builder/src/features/settings/components/SecurityForm.tsx b/apps/builder/src/features/settings/components/SecurityForm.tsx index 694a0898f..bc439632f 100644 --- a/apps/builder/src/features/settings/components/SecurityForm.tsx +++ b/apps/builder/src/features/settings/components/SecurityForm.tsx @@ -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. - - {({ item, onItemChange }) => ( - - )} - + ) diff --git a/apps/viewer/package.json b/apps/viewer/package.json index e431f81f3..db82221bf 100644 --- a/apps/viewer/package.json +++ b/apps/viewer/package.json @@ -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", diff --git a/packages/bot-engine/package.json b/packages/bot-engine/package.json index c87f5e225..b80a1f032 100644 --- a/packages/bot-engine/package.json +++ b/packages/bot-engine/package.json @@ -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", diff --git a/packages/forge/blocks/anthropic/actions/generateVariables.ts b/packages/forge/blocks/anthropic/actions/generateVariables.ts new file mode 100644 index 000000000..fcb0be079 --- /dev/null +++ b/packages/forge/blocks/anthropic/actions/generateVariables.ts @@ -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, + }) + }, + }, +}) diff --git a/packages/forge/blocks/anthropic/index.ts b/packages/forge/blocks/anthropic/index.ts index 1ea3cac94..23235626c 100644 --- a/packages/forge/blocks/anthropic/index.ts +++ b/packages/forge/blocks/anthropic/index.ts @@ -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], }) diff --git a/packages/forge/blocks/anthropic/package.json b/packages/forge/blocks/anthropic/package.json index ad1db6cc0..2f80a49a5 100644 --- a/packages/forge/blocks/anthropic/package.json +++ b/packages/forge/blocks/anthropic/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/forge/blocks/difyAi/package.json b/packages/forge/blocks/difyAi/package.json index 6e50d6f20..ff60ed53f 100644 --- a/packages/forge/blocks/difyAi/package.json +++ b/packages/forge/blocks/difyAi/package.json @@ -14,6 +14,6 @@ "typescript": "5.4.5" }, "dependencies": { - "ai": "3.1.12" + "ai": "3.1.34" } } \ No newline at end of file diff --git a/packages/forge/blocks/mistral/actions/createChatCompletion.ts b/packages/forge/blocks/mistral/actions/createChatCompletion.ts index a9327fb4a..f4d0ddf18 100644 --- a/packages/forge/blocks/mistral/actions/createChatCompletion.ts +++ b/packages/forge/blocks/mistral/actions/createChatCompletion.ts @@ -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: { diff --git a/packages/forge/blocks/mistral/actions/generateVariables.ts b/packages/forge/blocks/mistral/actions/generateVariables.ts new file mode 100644 index 000000000..9ec2f40e7 --- /dev/null +++ b/packages/forge/blocks/mistral/actions/generateVariables.ts @@ -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, + }) + }, + }, +}) diff --git a/packages/forge/blocks/mistral/helpers/fetchModels.ts b/packages/forge/blocks/mistral/helpers/fetchModels.ts new file mode 100644 index 000000000..7be74226a --- /dev/null +++ b/packages/forge/blocks/mistral/helpers/fetchModels.ts @@ -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) +} diff --git a/packages/forge/blocks/mistral/index.ts b/packages/forge/blocks/mistral/index.ts index a3fbfbb79..9bc24cc2a 100644 --- a/packages/forge/blocks/mistral/index.ts +++ b/packages/forge/blocks/mistral/index.ts @@ -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', }) diff --git a/packages/forge/blocks/mistral/package.json b/packages/forge/blocks/mistral/package.json index 52a1a215c..1a219159d 100644 --- a/packages/forge/blocks/mistral/package.json +++ b/packages/forge/blocks/mistral/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/forge/blocks/openai/actions/createChatCompletion.tsx b/packages/forge/blocks/openai/actions/createChatCompletion.tsx index fdb0e413b..4f3bd29df 100644 --- a/packages/forge/blocks/openai/actions/createChatCompletion.tsx +++ b/packages/forge/blocks/openai/actions/createChatCompletion.tsx @@ -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: { diff --git a/packages/forge/blocks/openai/actions/generateVariables.ts b/packages/forge/blocks/openai/actions/generateVariables.ts new file mode 100644 index 000000000..12da94946 --- /dev/null +++ b/packages/forge/blocks/openai/actions/generateVariables.ts @@ -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, + }) + }, + }, +}) diff --git a/packages/forge/blocks/openai/helpers/fetchModels.ts b/packages/forge/blocks/openai/helpers/fetchModels.ts new file mode 100644 index 000000000..4c1377f37 --- /dev/null +++ b/packages/forge/blocks/openai/helpers/fetchModels.ts @@ -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) ?? [] + ) +} diff --git a/packages/forge/blocks/openai/index.ts b/packages/forge/blocks/openai/index.ts index a03142a01..ff1d256e7 100644 --- a/packages/forge/blocks/openai/index.ts +++ b/packages/forge/blocks/openai/index.ts @@ -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', }) diff --git a/packages/forge/blocks/openai/package.json b/packages/forge/blocks/openai/package.json index ae9610a9b..8fe464792 100644 --- a/packages/forge/blocks/openai/package.json +++ b/packages/forge/blocks/openai/package.json @@ -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" } -} \ No newline at end of file +} diff --git a/packages/forge/blocks/openai/shared/parseGenerateVariablesOptions.ts b/packages/forge/blocks/openai/shared/parseGenerateVariablesOptions.ts new file mode 100644 index 000000000..9db111833 --- /dev/null +++ b/packages/forge/blocks/openai/shared/parseGenerateVariablesOptions.ts @@ -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 +> & + z.infer diff --git a/packages/forge/blocks/openai/shared/runGenerateVariables.ts b/packages/forge/blocks/openai/shared/runGenerateVariables.ts new file mode 100644 index 000000000..75fa4b80c --- /dev/null +++ b/packages/forge/blocks/openai/shared/runGenerateVariables.ts @@ -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 + +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 | 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) +} diff --git a/packages/forge/core/zod/extendWithTypebotLayout.ts b/packages/forge/core/zod/extendWithTypebotLayout.ts index ee41291d7..dac1832aa 100644 --- a/packages/forge/core/zod/extendWithTypebotLayout.ts +++ b/packages/forge/core/zod/extendWithTypebotLayout.ts @@ -23,6 +23,7 @@ export interface ZodLayoutMetadata< isHidden?: boolean | ((currentObj: Record) => boolean) isDebounceDisabled?: boolean hiddenItems?: string[] + mergeWithLastField?: boolean } declare module 'zod' { diff --git a/packages/lib/convertStrToList.ts b/packages/lib/convertStrToList.ts new file mode 100644 index 000000000..feda82493 --- /dev/null +++ b/packages/lib/convertStrToList.ts @@ -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()] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 054e672bf..bd73006f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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