Skip to content

Commit

Permalink
🐛 (dify) Fix Dify error when inputs are empty
Browse files Browse the repository at this point in the history
  • Loading branch information
baptisteArno committed Feb 12, 2024
1 parent 5226b06 commit f5bdba5
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
Expand Down Expand Up @@ -57,16 +58,14 @@ export const ZodFieldLayout = ({
propName?: string
onDataChange: (val: any) => void
}) => {
const layout = schema._def.layout as ZodLayoutMetadata<ZodTypeAny> | 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 (
<ZodObjectLayout
schema={schema as z.ZodObject<any>}
schema={innerSchema as z.ZodObject<any>}
data={data}
onDataChange={onDataChange}
isInAccordion={isInAccordion}
Expand All @@ -77,10 +76,12 @@ export const ZodFieldLayout = ({
case 'ZodDiscriminatedUnion': {
return (
<ZodDiscriminatedUnionLayout
discriminant={schema._def.discriminator}
discriminant={innerSchema._def.discriminator}
data={data}
schema={schema as z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>}
dropdownPlaceholder={`Select a ${schema._def.discriminator}`}
schema={
innerSchema as z.ZodDiscriminatedUnion<string, z.ZodObject<any>[]>
}
dropdownPlaceholder={`Select a ${innerSchema._def.discriminator}`}
onDataChange={onDataChange}
/>
)
Expand All @@ -99,7 +100,7 @@ export const ZodFieldLayout = ({
<AccordionPanel as={Stack} pt="4">
<ZodArrayContent
data={data}
schema={schema}
schema={innerSchema}
blockDef={blockDef}
blockOptions={blockOptions}
layout={layout}
Expand All @@ -113,7 +114,7 @@ export const ZodFieldLayout = ({
return (
<ZodArrayContent
data={data}
schema={schema}
schema={innerSchema}
blockDef={blockDef}
blockOptions={blockOptions}
layout={layout}
Expand All @@ -126,7 +127,7 @@ export const ZodFieldLayout = ({
<DropdownList
currentItem={data ?? layout?.defaultValue}
onItemSelect={onDataChange}
items={schema._def.innerType._def.values}
items={innerSchema._def.values}
label={layout?.label}
helperText={
layout?.helperText ? (
Expand Down Expand Up @@ -295,7 +296,7 @@ const ZodArrayContent = ({
isInAccordion?: boolean
onDataChange: (val: any) => 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 (
<Stack spacing={0}>
Expand All @@ -310,7 +311,7 @@ const ZodArrayContent = ({
>
{({ item, onItemChange }) => (
<ZodFieldLayout
schema={schema._def.innerType._def.type}
schema={schema._def.type}
data={item}
blockDef={blockDef}
blockOptions={blockOptions}
Expand All @@ -335,7 +336,7 @@ const ZodArrayContent = ({
{({ item, onItemChange }) => (
<Stack p="4" rounded="md" flex="1" borderWidth="1px" maxW="100%">
<ZodFieldLayout
schema={schema._def.innerType._def.type}
schema={schema._def.type}
blockDef={blockDef}
blockOptions={blockOptions}
data={item}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { ReactNode } from 'react'
import { ZodTypeAny } from 'zod'
import { ZodFieldLayout } from './ZodFieldLayout'
import { ForgedBlockDefinition, ForgedBlock } from '@typebot.io/forge-schemas'
import { getZodInnerSchema } from '../../helpers/getZodInnerSchema'

export const ZodObjectLayout = ({
schema,
Expand All @@ -38,15 +39,15 @@ export const ZodObjectLayout = ({
}>(
(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<ZodTypeAny>
| undefined
if (
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(
Expand Down
18 changes: 18 additions & 0 deletions apps/builder/src/features/forge/helpers/getZodInnerSchema.ts
Original file line number Diff line number Diff line change
@@ -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
}
1 change: 1 addition & 0 deletions apps/docs/deploy/whatsapp/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 0 additions & 4 deletions packages/bot-engine/blocks/inputs/url/validateUrl.ts

This file was deleted.

6 changes: 3 additions & 3 deletions packages/bot-engine/continueBotFlow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 }
}
Expand All @@ -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: {
Expand Down
74 changes: 43 additions & 31 deletions packages/forge/blocks/difyAi/actions/createChatMessage.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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: []
},
})
.json()
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
responseMapping?.forEach((mapping) => {
if (!mapping.variableId) return

const item = mapping.item ?? 'Answer'
if (item === 'Answer') variables.set(mapping.variableId, res.answer)
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 === 'Conversation ID')
variables.set(mapping.variableId, res.conversation_id)

if (item === 'Total Tokens')
variables.set(mapping.variableId, res.metadata.usage.total_tokens)
})
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)
}
},
},
})
24 changes: 17 additions & 7 deletions packages/forge/blocks/difyAi/auth.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
5 changes: 3 additions & 2 deletions packages/lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
}
}
10 changes: 10 additions & 0 deletions packages/lib/validators/isURL.ts
Original file line number Diff line number Diff line change
@@ -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 }
15 changes: 15 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit f5bdba5

Please sign in to comment.