From f5bdba53b954bcfc0d0b85b7ccbe7040e66e9678 Mon Sep 17 00:00:00 2001 From: Baptiste Arnaud Date: Mon, 12 Feb 2024 11:41:57 +0100 Subject: [PATCH] :bug: (dify) Fix Dify error when inputs are empty --- .../components/zodLayouts/ZodFieldLayout.tsx | 31 +++---- .../components/zodLayouts/ZodObjectLayout.tsx | 5 +- .../forge/helpers/getZodInnerSchema.ts | 18 +++++ apps/docs/deploy/whatsapp/overview.mdx | 1 + .../blocks/inputs/url/validateUrl.ts | 4 - packages/bot-engine/continueBotFlow.ts | 6 +- .../difyAi/actions/createChatMessage.ts | 80 +++++++++++-------- packages/forge/blocks/difyAi/auth.ts | 24 ++++-- packages/lib/package.json | 5 +- packages/lib/validators/isURL.ts | 10 +++ pnpm-lock.yaml | 15 ++++ 11 files changed, 132 insertions(+), 67 deletions(-) create mode 100644 apps/builder/src/features/forge/helpers/getZodInnerSchema.ts delete mode 100644 packages/bot-engine/blocks/inputs/url/validateUrl.ts create mode 100644 packages/lib/validators/isURL.ts diff --git a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx index 19e4b67be..bf47e02c7 100644 --- a/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx +++ b/apps/builder/src/features/forge/components/zodLayouts/ZodFieldLayout.tsx @@ -23,6 +23,7 @@ import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas' import { PrimitiveList } from '@/components/PrimitiveList' import { SwitchWithLabel } from '@/components/inputs/SwitchWithLabel' import { CodeEditor } from '@/components/inputs/CodeEditor' +import { getZodInnerSchema } from '../../helpers/getZodInnerSchema' const mdComponents = { a: ({ href, children }) => ( @@ -57,16 +58,14 @@ export const ZodFieldLayout = ({ propName?: string onDataChange: (val: any) => void }) => { - const layout = schema._def.layout as ZodLayoutMetadata | undefined - const type = schema._def.innerType - ? schema._def.innerType._def.typeName - : schema._def.typeName + const innerSchema = getZodInnerSchema(schema) + const layout = innerSchema._def.layout - switch (type) { + switch (innerSchema._def.typeName) { case 'ZodObject': return ( } + schema={innerSchema as z.ZodObject} data={data} onDataChange={onDataChange} isInAccordion={isInAccordion} @@ -77,10 +76,12 @@ export const ZodFieldLayout = ({ case 'ZodDiscriminatedUnion': { return ( []>} - dropdownPlaceholder={`Select a ${schema._def.discriminator}`} + schema={ + innerSchema as z.ZodDiscriminatedUnion[]> + } + dropdownPlaceholder={`Select a ${innerSchema._def.discriminator}`} onDataChange={onDataChange} /> ) @@ -99,7 +100,7 @@ export const ZodFieldLayout = ({ void }) => { - const type = schema._def.innerType._def.type._def.innerType?._def.typeName + const type = schema._def.type._def.innerType?._def.typeName if (type === 'ZodString' || type === 'ZodNumber' || type === 'ZodEnum') return ( @@ -310,7 +311,7 @@ const ZodArrayContent = ({ > {({ item, onItemChange }) => ( ( ( (nodes, key, index) => { if (ignoreKeys?.includes(key)) return nodes - const keySchema = schema.shape[key] + const keySchema = getZodInnerSchema(schema.shape[key]) const layout = keySchema._def.layout as | ZodLayoutMetadata | undefined @@ -46,7 +47,7 @@ export const ZodObjectLayout = ({ layout && layout.accordion && !isInAccordion && - keySchema._def.innerType._def.typeName !== 'ZodArray' + keySchema._def.typeName !== 'ZodArray' ) { if (nodes.accordionsCreated.includes(layout.accordion)) return nodes const accordionKeys = getObjectKeysWithSameAccordionAttr( diff --git a/apps/builder/src/features/forge/helpers/getZodInnerSchema.ts b/apps/builder/src/features/forge/helpers/getZodInnerSchema.ts new file mode 100644 index 000000000..32fd0677c --- /dev/null +++ b/apps/builder/src/features/forge/helpers/getZodInnerSchema.ts @@ -0,0 +1,18 @@ +import { z } from '@typebot.io/forge/zod' + +export const getZodInnerSchema = (schema: z.ZodTypeAny): z.ZodTypeAny => { + if (schema._def.typeName === 'ZodEffects') + return getZodInnerSchema(schema._def.schema) + if (schema._def.typeName === 'ZodOptional') { + const innerSchema = getZodInnerSchema(schema._def.innerType) + return { + ...innerSchema, + _def: { + ...innerSchema._def, + layout: schema._def.layout, + }, + } as z.ZodTypeAny + } + + return schema +} diff --git a/apps/docs/deploy/whatsapp/overview.mdx b/apps/docs/deploy/whatsapp/overview.mdx index 66684ac98..09280ab30 100644 --- a/apps/docs/deploy/whatsapp/overview.mdx +++ b/apps/docs/deploy/whatsapp/overview.mdx @@ -20,6 +20,7 @@ WhatsApp environment have some limitations that you need to keep in mind when bu - GIF and SVG image files are not supported. They won't be displayed. - Buttons content can't be longer than 20 characters. If the content is longer, it will be truncated. +- WhatsApp only allows to display 3 buttons at a time. So we work around that by adding "..." messages to display more buttons. - Incompatible blocks, if present, they will be skipped: - Payment input block diff --git a/packages/bot-engine/blocks/inputs/url/validateUrl.ts b/packages/bot-engine/blocks/inputs/url/validateUrl.ts deleted file mode 100644 index aff577c68..000000000 --- a/packages/bot-engine/blocks/inputs/url/validateUrl.ts +++ /dev/null @@ -1,4 +0,0 @@ -const urlRegex = - /^https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/ - -export const validateUrl = (url: string) => urlRegex.test(url) diff --git a/packages/bot-engine/continueBotFlow.ts b/packages/bot-engine/continueBotFlow.ts index 3f31296e6..1ccd48d04 100644 --- a/packages/bot-engine/continueBotFlow.ts +++ b/packages/bot-engine/continueBotFlow.ts @@ -11,7 +11,6 @@ import { executeGroup, parseInput } from './executeGroup' import { getNextGroup } from './getNextGroup' import { validateEmail } from './blocks/inputs/email/validateEmail' import { formatPhoneNumber } from './blocks/inputs/phone/formatPhoneNumber' -import { validateUrl } from './blocks/inputs/url/validateUrl' import { resumeWebhookExecution } from './blocks/integrations/webhook/resumeWebhookExecution' import { upsertAnswer } from './queries/upsertAnswer' import { parseButtonsReply } from './blocks/inputs/buttons/parseButtonsReply' @@ -42,6 +41,7 @@ import { resumeChatCompletion } from './blocks/integrations/legacy/openai/resume import { env } from '@typebot.io/env' import { downloadMedia } from './whatsapp/downloadMedia' import { uploadFileToBucket } from '@typebot.io/lib/s3/uploadFileToBucket' +import { isURL } from '@typebot.io/lib/validators/isURL' type Params = { version: 1 | 2 @@ -450,7 +450,7 @@ const parseReply = } case InputBlockType.URL: { if (!reply) return { status: 'fail' } - const isValid = validateUrl(reply) + const isValid = isURL(reply, { require_protocol: false }) if (!isValid) return { status: 'fail' } return { status: 'success', reply: reply } } @@ -477,7 +477,7 @@ const parseReply = ? { status: 'fail' } : { status: 'skip' } const urls = reply.split(', ') - const status = urls.some((url) => validateUrl(url)) ? 'success' : 'fail' + const status = urls.some((url) => isURL(url)) ? 'success' : 'fail' return { status, reply: reply } } case InputBlockType.PAYMENT: { diff --git a/packages/forge/blocks/difyAi/actions/createChatMessage.ts b/packages/forge/blocks/difyAi/actions/createChatMessage.ts index c0062071d..38be1d520 100644 --- a/packages/forge/blocks/difyAi/actions/createChatMessage.ts +++ b/packages/forge/blocks/difyAi/actions/createChatMessage.ts @@ -1,6 +1,6 @@ import { createAction, option } from '@typebot.io/forge' import { isDefined, isEmpty } from '@typebot.io/lib' -import { got } from 'got' +import { HTTPError, got } from 'got' import { auth } from '../auth' import { DifyResponse } from '../types' import { defaultBaseUrl } from '../constants' @@ -41,41 +41,53 @@ export const createChatMessage = createAction({ credentials: { apiEndpoint, apiKey }, options: { conversation_id, query, user, inputs, responseMapping }, variables, + logs, }) => { - const res: DifyResponse = await got - .post((apiEndpoint ?? defaultBaseUrl) + '/v1/chat-messages', { - headers: { - Authorization: `Bearer ${apiKey}`, - }, - json: { - inputs: inputs?.reduce((acc, { key, value }) => { - if (isEmpty(key) || isEmpty(value)) return acc - return { - ...acc, - [key]: value, - } - }, {}), - query, - response_mode: 'blocking', - conversation_id, - user, - files: [] - }, + try { + const res: DifyResponse = await got + .post((apiEndpoint ?? defaultBaseUrl) + '/v1/chat-messages', { + headers: { + Authorization: `Bearer ${apiKey}`, + }, + json: { + inputs: + inputs?.reduce((acc, { key, value }) => { + if (isEmpty(key) || isEmpty(value)) return acc + return { + ...acc, + [key]: value, + } + }, {}) ?? {}, + query, + response_mode: 'blocking', + conversation_id, + user, + files: [], + }, + }) + .json() + + responseMapping?.forEach((mapping) => { + if (!mapping.variableId) return + + const item = mapping.item ?? 'Answer' + if (item === 'Answer') variables.set(mapping.variableId, res.answer) + + if (item === 'Conversation ID') + variables.set(mapping.variableId, res.conversation_id) + + if (item === 'Total Tokens') + variables.set(mapping.variableId, res.metadata.usage.total_tokens) }) - .json() - - responseMapping?.forEach((mapping) => { - if (!mapping.variableId) return - - const item = mapping.item ?? 'Answer' - if (item === 'Answer') variables.set(mapping.variableId, res.answer) - - if (item === 'Conversation ID') - variables.set(mapping.variableId, res.conversation_id) - - if (item === 'Total Tokens') - variables.set(mapping.variableId, res.metadata.usage.total_tokens) - }) + } catch (error) { + if (error instanceof HTTPError) + return logs.add({ + status: 'error', + description: error.message, + details: error.response.body, + }) + console.error(error) + } }, }, }) diff --git a/packages/forge/blocks/difyAi/auth.ts b/packages/forge/blocks/difyAi/auth.ts index af3585c29..1db1abac1 100644 --- a/packages/forge/blocks/difyAi/auth.ts +++ b/packages/forge/blocks/difyAi/auth.ts @@ -1,17 +1,27 @@ import { option, AuthDefinition } from '@typebot.io/forge' import { defaultBaseUrl } from './constants' +import { isURL } from '@typebot.io/lib/validators/isURL' + +const extractBaseUrl = (val: string | undefined) => { + if (!val) return val + const url = new URL(val) + return url.origin +} export const auth = { type: 'encryptedCredentials', name: 'Dify.AI account', schema: option.object({ - apiEndpoint: option.string.layout({ - label: 'API Endpoint', - isRequired: true, - helperText: 'URI where the Service API is hosted.', - withVariableButton: false, - defaultValue: defaultBaseUrl, - }), + apiEndpoint: option.string + .layout({ + label: 'API Endpoint', + isRequired: true, + helperText: 'URI where the Service API is hosted.', + withVariableButton: false, + defaultValue: defaultBaseUrl, + }) + .refine((val) => !val || isURL(val)) + .transform(extractBaseUrl), apiKey: option.string.layout({ label: 'App API key', isRequired: true, diff --git a/packages/lib/package.json b/packages/lib/package.json index c64455e92..d91ea8137 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -15,6 +15,7 @@ "@typebot.io/tsconfig": "workspace:*", "@types/escape-html": "^1.0.4", "@types/nodemailer": "6.4.8", + "@types/validator": "13.11.9", "next": "14.1.0", "nodemailer": "6.9.3", "typescript": "5.3.2" @@ -44,7 +45,7 @@ "remark-parse": "11.0.0", "stripe": "12.13.0", "unified": "11.0.4", - "zod": "3.22.4", - "ky": "1.1.3" + "validator": "13.11.0", + "zod": "3.22.4" } } diff --git a/packages/lib/validators/isURL.ts b/packages/lib/validators/isURL.ts new file mode 100644 index 000000000..cf173cce6 --- /dev/null +++ b/packages/lib/validators/isURL.ts @@ -0,0 +1,10 @@ +import isURL, { IsURLOptions } from 'validator/lib/isURL' + +const customIsURL = (val: string, options?: IsURLOptions) => + isURL(val, { + protocols: ['https', 'http'], + require_protocol: true, + ...options, + }) + +export { customIsURL as isURL } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5aedade1b..bd166e78c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1479,6 +1479,9 @@ importers: unified: specifier: 11.0.4 version: 11.0.4 + validator: + specifier: ^13.11.0 + version: 13.11.0 zod: specifier: 3.22.4 version: 3.22.4 @@ -1510,6 +1513,9 @@ importers: '@types/nodemailer': specifier: 6.4.8 version: 6.4.8 + '@types/validator': + specifier: ^13.11.9 + version: 13.11.9 next: specifier: 14.1.0 version: 14.1.0(@babel/core@7.22.9)(react-dom@18.2.0)(react@18.2.0) @@ -9661,6 +9667,10 @@ packages: /@types/unist@3.0.2: resolution: {integrity: sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==} + /@types/validator@13.11.9: + resolution: {integrity: sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==} + dev: true + /@types/webpack@5.28.5(@swc/core@1.3.101)(esbuild@0.19.11): resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} dependencies: @@ -23236,6 +23246,11 @@ packages: spdx-correct: 3.2.0 spdx-expression-parse: 3.0.1 + /validator@13.11.0: + resolution: {integrity: sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==} + engines: {node: '>= 0.10'} + dev: false + /vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'}