= ({
}
if (!data)
return null
+ const previewGraph = normalizeWorkflowPreviewGraph(data.graph)
return (
diff --git a/web/contract/console/try-app.ts b/web/contract/console/try-app.ts
index 76e01ca12c3..a824b04b2da 100644
--- a/web/contract/console/try-app.ts
+++ b/web/contract/console/try-app.ts
@@ -1,69 +1 @@
-import type { ChatConfig } from '@/app/components/base/chat/types'
-import type { DataSetListResponse } from '@/models/datasets'
-import type { TryAppFlowPreview, TryAppInfo } from '@/models/try-app'
-import { trialApps } from '@dify/contracts/api/console/trial-apps/orpc.gen'
-import { type } from '@orpc/contract'
-import { base } from '../base'
-
-export const trialAppInfoContract = base
- .route({
- path: '/trial-apps/{appId}',
- method: 'GET',
- })
- .input(type<{
- params: {
- appId: string
- }
- }>())
- .output(type())
-
-export const trialAppDatasetsContract = base
- .route({
- path: '/trial-apps/{appId}/datasets',
- method: 'GET',
- })
- .input(type<{
- params: {
- appId: string
- }
- query: {
- ids: string[]
- }
- }>())
- .output(type())
-
-export const trialAppWorkflowsContract = base
- .route({
- path: '/trial-apps/{appId}/workflows',
- method: 'GET',
- })
- .input(type<{
- params: {
- appId: string
- }
- }>())
- .output(type())
-
-export const trialAppParametersContract = base
- .route({
- path: '/trial-apps/{appId}/parameters',
- method: 'GET',
- })
- .input(type<{
- params: {
- appId: string
- }
- }>())
- .output(type())
-
-export const trialAppsRouterContract = {
- ...trialApps,
- info: trialAppInfoContract,
- datasets: trialAppDatasetsContract,
- parameters: trialAppParametersContract,
- workflows: trialAppWorkflowsContract,
-}
-
-export const trialAppsConsoleRouterContract = {
- trialApps: trialAppsRouterContract,
-}
+export const trialAppsConsoleRouterContract = {}
diff --git a/web/contract/router.ts b/web/contract/router.ts
index efb24c3057c..c72b56b8fcf 100644
--- a/web/contract/router.ts
+++ b/web/contract/router.ts
@@ -40,6 +40,7 @@ import { systemFeatures } from '@dify/contracts/api/console/system-features/orpc
import { tagBindings } from '@dify/contracts/api/console/tag-bindings/orpc.gen'
import { tags } from '@dify/contracts/api/console/tags/orpc.gen'
import { test } from '@dify/contracts/api/console/test/orpc.gen'
+import { trialApps } from '@dify/contracts/api/console/trial-apps/orpc.gen'
import { trialModels } from '@dify/contracts/api/console/trial-models/orpc.gen'
import { website } from '@dify/contracts/api/console/website/orpc.gen'
import { workflowGenerate } from '@dify/contracts/api/console/workflow-generate/orpc.gen'
@@ -94,6 +95,7 @@ const communityContract = {
tagBindings,
tags,
test,
+ trialApps,
trialModels,
website,
workflow,
diff --git a/web/models/try-app.ts b/web/models/try-app.ts
deleted file mode 100644
index eb9369d7f4a..00000000000
--- a/web/models/try-app.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { Viewport } from 'reactflow'
-import type { Edge, Node } from '@/app/components/workflow/types'
-import type { SiteInfo } from '@/models/share'
-import type { AppModeEnum, ModelConfig } from '@/types/app'
-
-export type TryAppInfo = {
- name: string
- description: string
- mode: AppModeEnum
- site: SiteInfo
- model_config: ModelConfig
- deleted_tools: { id: string, tool_name: string }[]
-}
-
-export type TryAppFlowPreview = {
- graph: {
- nodes: Node[]
- edges: Edge[]
- viewport: Viewport
- }
-}
diff --git a/web/service/client.spec.ts b/web/service/client.spec.ts
index 57aedf9f97c..5eedf391176 100644
--- a/web/service/client.spec.ts
+++ b/web/service/client.spec.ts
@@ -255,6 +255,43 @@ describe('consoleQuery transport context', () => {
}),
)
})
+
+ it('should serialize trial app dataset ids as repeated query params', async () => {
+ const request = vi.fn().mockResolvedValue(new Response(JSON.stringify({
+ data: [],
+ has_more: false,
+ limit: 20,
+ page: 1,
+ total: 0,
+ }), {
+ status: 200,
+ headers: {
+ 'content-type': 'application/json',
+ },
+ }))
+ const consoleQuery = await loadConsoleQueryWithRequest(request)
+ const queryOptions = consoleQuery.trialApps.byAppId.datasets.get.queryOptions({
+ input: {
+ params: {
+ app_id: 'app-1',
+ },
+ query: {
+ ids: ['id-1', 'id-2'],
+ },
+ },
+ })
+
+ await queryOptions.queryFn({ signal: new AbortController().signal } as QueryFunctionContext)
+
+ expect(request).toHaveBeenCalledWith(
+ expect.stringContaining('/trial-apps/app-1/datasets?ids=id-1&ids=id-2'),
+ expect.any(Object),
+ expect.objectContaining({
+ fetchCompat: true,
+ }),
+ )
+ expect(request.mock.calls[0]![0]).not.toContain('ids%5B0%5D')
+ })
})
// Scenario: oRPC mutation defaults own shared Agent roster cache behavior.
diff --git a/web/service/client.ts b/web/service/client.ts
index eb9d9bbffc8..37072779e57 100644
--- a/web/service/client.ts
+++ b/web/service/client.ts
@@ -44,6 +44,38 @@ function isURL(path: string) {
}
}
+const trialAppDatasetsPathPattern = /\/trial-apps\/[^/]+\/datasets$/
+const indexedIdsQueryParamPattern = /^ids\[(\d+)\]$/
+
+function normalizeConsoleOpenAPIURL(url: string | URL) {
+ const normalizedUrl = new URL(url)
+
+ if (!trialAppDatasetsPathPattern.test(normalizedUrl.pathname))
+ return normalizedUrl.href
+
+ const ids: Array<{ index: number, value: string }> = []
+ const indexedKeys = new Set()
+
+ normalizedUrl.searchParams.forEach((value, key) => {
+ const match = indexedIdsQueryParamPattern.exec(key)
+ if (!match)
+ return
+
+ indexedKeys.add(key)
+ ids.push({ index: Number(match[1]), value })
+ })
+
+ if (!ids.length)
+ return normalizedUrl.href
+
+ indexedKeys.forEach(key => normalizedUrl.searchParams.delete(key))
+ ids
+ .sort((a, b) => a.index - b.index)
+ .forEach(({ value }) => normalizedUrl.searchParams.append('ids', value))
+
+ return normalizedUrl.href
+}
+
export function getBaseURL(path: string) {
const url = new URL(path, isURL(path) ? undefined : isClient ? window.location.origin : 'http://localhost')
@@ -69,7 +101,7 @@ function createConsoleOpenAPILink(contract: AnyContractRouter): ConsoleClientLin
url: getBaseURL(API_PREFIX),
fetch: (input, init, options) => {
return request(
- input.url,
+ normalizeConsoleOpenAPIURL(input.url),
init,
{
fetchCompat: true,
diff --git a/web/service/try-app.spec.ts b/web/service/try-app.spec.ts
index 9e9cd8d04af..d0d18cc4d72 100644
--- a/web/service/try-app.spec.ts
+++ b/web/service/try-app.spec.ts
@@ -1,26 +1,33 @@
-import { get } from './base'
+import { consoleClient } from '@/service/client'
import { fetchTryAppDatasets } from './try-app'
-vi.mock('./base', () => ({
- get: vi.fn(),
-}))
-
vi.mock('@/service/client', () => ({
consoleClient: {
trialApps: {
- info: vi.fn(),
- workflows: vi.fn(),
- parameters: vi.fn(),
+ byAppId: {
+ datasets: {
+ get: vi.fn(),
+ },
+ },
},
},
}))
describe('fetchTryAppDatasets', () => {
- it('serializes ids as repeated query params', async () => {
- vi.mocked(get).mockResolvedValue({ data: [] })
+ it('passes ids to the generated client', async () => {
+ vi.mocked(consoleClient.trialApps.byAppId.datasets.get).mockResolvedValue({
+ data: [],
+ has_more: false,
+ limit: 20,
+ page: 1,
+ total: 0,
+ })
await fetchTryAppDatasets('app-1', ['id-1', 'id-2'])
- expect(get).toHaveBeenCalledWith('/trial-apps/app-1/datasets?ids=id-1&ids=id-2')
+ expect(consoleClient.trialApps.byAppId.datasets.get).toHaveBeenCalledWith({
+ params: { app_id: 'app-1' },
+ query: { ids: ['id-1', 'id-2'] },
+ })
})
})
diff --git a/web/service/try-app.ts b/web/service/try-app.ts
index 7b81e7c4faa..99a2f46aa7c 100644
--- a/web/service/try-app.ts
+++ b/web/service/try-app.ts
@@ -1,28 +1,292 @@
import type { ChatConfig } from '@/app/components/base/chat/types'
-import type { DataSetListResponse } from '@/models/datasets'
-import type { TryAppFlowPreview, TryAppInfo } from '@/models/try-app'
-import qs from 'qs'
+import { SupportUploadFileTypes } from '@/app/components/workflow/types'
+import { ANNOTATION_DEFAULT, DEFAULT_AGENT_SETTING } from '@/config'
+import { PromptMode } from '@/models/debug'
import { consoleClient } from '@/service/client'
-import { get } from './base'
+import { Resolution, RETRIEVE_TYPE, TransferMethod, TtsAutoPlay } from '@/types/app'
-export const fetchTryAppInfo = (appId: string): Promise => {
- return consoleClient.trialApps.info({ params: { appId } })
+type TryAppParameters = import('@dify/contracts/api/console/trial-apps/types.gen').Parameters
+
+const transferMethodValues = new Set(Object.values(TransferMethod))
+const supportUploadFileTypeValues = new Set(Object.values(SupportUploadFileTypes))
+
+const isRecord = (value: unknown): value is Record => {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
}
-export const fetchTryAppDatasets = (appId: string, ids: string[]): Promise => {
- const queryString = qs.stringify({ ids }, { indices: false })
- const url = `/trial-apps/${encodeURIComponent(appId)}/datasets${queryString ? `?${queryString}` : ''}`
-
- return get(url)
+const getString = (value: unknown, fallback = '') => {
+ return typeof value === 'string' ? value : fallback
}
-export const fetchTryAppFlowPreview = (appId: string): Promise => {
- return consoleClient.trialApps.workflows({ params: { appId } })
- .then(res => res as TryAppFlowPreview)
+const getOptionalString = (value: unknown) => {
+ return typeof value === 'string' ? value : undefined
}
-export const fetchTryAppParams = (appId: string): Promise => {
- return consoleClient.trialApps.parameters({ params: { appId } })
+const getBoolean = (value: unknown, fallback = false) => {
+ return typeof value === 'boolean' ? value : fallback
}
-export type { TryAppInfo } from '@/models/try-app'
+const getNumber = (value: unknown, fallback: number) => {
+ return typeof value === 'number' ? value : fallback
+}
+
+const getStringArray = (value: unknown) => {
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
+}
+
+const getStringRecord = (value: unknown) => {
+ if (!isRecord(value))
+ return undefined
+
+ const record: Record = {}
+ Object.entries(value).forEach(([key, item]) => {
+ if (typeof item === 'string')
+ record[key] = item
+ })
+ return record
+}
+
+const normalizeEnabledConfig = (value: Record, fallback = false) => {
+ return {
+ ...value,
+ enabled: getBoolean(value.enabled, fallback),
+ }
+}
+
+const normalizeTextToSpeechConfig = (value: Record): ChatConfig['text_to_speech'] => {
+ const autoPlay = getString(value.autoPlay)
+ const config = { ...value }
+ delete config.autoPlay
+ return {
+ ...normalizeEnabledConfig(config),
+ voice: getOptionalString(value.voice),
+ language: getOptionalString(value.language),
+ ...(autoPlay === TtsAutoPlay.enabled || autoPlay === TtsAutoPlay.disabled ? { autoPlay } : {}),
+ }
+}
+
+const normalizeAnnotationReplyConfig = (value: Record): ChatConfig['annotation_reply'] => {
+ const embeddingModel = isRecord(value.embedding_model) ? value.embedding_model : {}
+ return {
+ id: getString(value.id),
+ enabled: getBoolean(value.enabled),
+ score_threshold: getNumber(value.score_threshold, ANNOTATION_DEFAULT.score_threshold),
+ embedding_model: {
+ embedding_provider_name: getString(embeddingModel.embedding_provider_name),
+ embedding_model_name: getString(embeddingModel.embedding_model_name),
+ },
+ }
+}
+
+const getTransferMethods = (value: unknown, fallback: TransferMethod[]) => {
+ if (!Array.isArray(value))
+ return fallback
+
+ const methods = value.filter((item): item is TransferMethod => {
+ return typeof item === 'string' && transferMethodValues.has(item)
+ })
+ return methods.length > 0 ? methods : fallback
+}
+
+const getSupportUploadFileTypes = (value: unknown): SupportUploadFileTypes[] => {
+ if (!Array.isArray(value))
+ return []
+
+ return value.filter((item): item is SupportUploadFileTypes => {
+ return typeof item === 'string' && supportUploadFileTypeValues.has(item)
+ })
+}
+
+const normalizeVisionSettings = (value: unknown): NonNullable['image'] => {
+ const image = isRecord(value) ? value : {}
+ return {
+ enabled: getBoolean(image.enabled),
+ number_limits: getNumber(image.number_limits, 3),
+ detail: getString(image.detail) === Resolution.low ? Resolution.low : Resolution.high,
+ transfer_methods: getTransferMethods(image.transfer_methods, [TransferMethod.local_file, TransferMethod.remote_url]),
+ }
+}
+
+const normalizeFileUploadConfig = (value: Record): ChatConfig['file_upload'] => {
+ const allowedUploadMethods = getTransferMethods(value.allowed_upload_methods, [TransferMethod.local_file, TransferMethod.remote_url])
+ const allowedFileUploadMethods = getTransferMethods(value.allowed_file_upload_methods, allowedUploadMethods)
+
+ return {
+ image: normalizeVisionSettings(value.image),
+ allowed_file_upload_methods: allowedFileUploadMethods,
+ allowed_upload_methods: allowedUploadMethods,
+ allowed_file_types: getSupportUploadFileTypes(value.allowed_file_types),
+ allowed_file_extensions: getStringArray(value.allowed_file_extensions),
+ max_length: getNumber(value.max_length, 1),
+ number_limits: getNumber(value.number_limits, 1),
+ }
+}
+
+const defaultDatasetConfigs: ChatConfig['dataset_configs'] = {
+ retrieval_model: RETRIEVE_TYPE.oneWay,
+ reranking_model: {
+ reranking_provider_name: '',
+ reranking_model_name: '',
+ },
+ top_k: 4,
+ score_threshold_enabled: false,
+ score_threshold: null,
+ datasets: {
+ datasets: [],
+ },
+}
+
+const normalizeBaseInputForm = (value: Record) => {
+ return {
+ default: getString(value.default),
+ label: getString(value.label),
+ variable: getString(value.variable),
+ required: getBoolean(value.required, true),
+ hide: getBoolean(value.hide),
+ }
+}
+
+const normalizeTextInputForm = (value: Record) => {
+ return {
+ ...normalizeBaseInputForm(value),
+ max_length: getNumber(value.max_length, 0),
+ }
+}
+
+const normalizeFileInputForm = (value: Record) => {
+ return {
+ ...normalizeBaseInputForm(value),
+ max_length: getNumber(value.max_length, 1),
+ allowed_file_upload_methods: getTransferMethods(value.allowed_file_upload_methods, [
+ TransferMethod.local_file,
+ TransferMethod.remote_url,
+ ]),
+ allowed_upload_methods: getTransferMethods(value.allowed_upload_methods, [
+ TransferMethod.local_file,
+ TransferMethod.remote_url,
+ ]),
+ allowed_file_types: getSupportUploadFileTypes(value.allowed_file_types),
+ allowed_file_extensions: getStringArray(value.allowed_file_extensions),
+ }
+}
+
+const normalizeUserInputFormItem = (item: Record): ChatConfig['user_input_form'][number] | null => {
+ if (isRecord(item['text-input']))
+ return { 'text-input': normalizeTextInputForm(item['text-input']) }
+
+ if (isRecord(item.paragraph))
+ return { paragraph: normalizeTextInputForm(item.paragraph) }
+
+ if (isRecord(item.select)) {
+ return {
+ select: {
+ ...normalizeBaseInputForm(item.select),
+ options: getStringArray(item.select.options),
+ },
+ }
+ }
+
+ if (isRecord(item.number)) {
+ return {
+ number: {
+ ...normalizeBaseInputForm(item.number),
+ max_length: getNumber(item.number.max_length, 0),
+ },
+ }
+ }
+
+ if (isRecord(item.checkbox)) {
+ return {
+ checkbox: {
+ ...normalizeBaseInputForm(item.checkbox),
+ default: getBoolean(item.checkbox.default),
+ },
+ }
+ }
+
+ if (isRecord(item.file))
+ return { file: normalizeFileInputForm(item.file) }
+
+ if (isRecord(item['file-list']))
+ return { 'file-list': normalizeFileInputForm(item['file-list']) }
+
+ if (isRecord(item.external_data_tool)) {
+ return {
+ external_data_tool: {
+ label: getString(item.external_data_tool.label),
+ variable: getString(item.external_data_tool.variable),
+ required: getBoolean(item.external_data_tool.required, true),
+ hide: getBoolean(item.external_data_tool.hide),
+ type: getOptionalString(item.external_data_tool.type),
+ enabled: getBoolean(item.external_data_tool.enabled),
+ icon: getOptionalString(item.external_data_tool.icon),
+ icon_background: getOptionalString(item.external_data_tool.icon_background),
+ config: getStringRecord(item.external_data_tool.config),
+ },
+ }
+ }
+
+ if (isRecord(item.json_object)) {
+ const jsonSchema = item.json_object.json_schema
+ return {
+ json_object: {
+ ...normalizeBaseInputForm(item.json_object),
+ json_schema: typeof jsonSchema === 'string' || isRecord(jsonSchema) ? jsonSchema : undefined,
+ },
+ }
+ }
+
+ return null
+}
+
+const normalizeUserInputForm = (items: TryAppParameters['user_input_form']): ChatConfig['user_input_form'] => {
+ return items.reduce((result, item) => {
+ const normalized = normalizeUserInputFormItem(item)
+ if (normalized)
+ result.push(normalized)
+ return result
+ }, [])
+}
+
+const normalizeTryAppParams = (params: TryAppParameters): ChatConfig => {
+ return {
+ opening_statement: params.opening_statement ?? '',
+ suggested_questions: params.suggested_questions,
+ suggested_questions_after_answer: normalizeEnabledConfig(params.suggested_questions_after_answer),
+ speech_to_text: normalizeEnabledConfig(params.speech_to_text),
+ text_to_speech: normalizeTextToSpeechConfig(params.text_to_speech),
+ retriever_resource: normalizeEnabledConfig(params.retriever_resource),
+ annotation_reply: normalizeAnnotationReplyConfig(params.annotation_reply),
+ more_like_this: normalizeEnabledConfig(params.more_like_this),
+ sensitive_word_avoidance: normalizeEnabledConfig(params.sensitive_word_avoidance),
+ file_upload: normalizeFileUploadConfig(params.file_upload),
+ user_input_form: normalizeUserInputForm(params.user_input_form),
+ system_parameters: params.system_parameters,
+ pre_prompt: '',
+ prompt_type: PromptMode.simple,
+ agent_mode: DEFAULT_AGENT_SETTING,
+ dataset_configs: defaultDatasetConfigs,
+ }
+}
+
+export const fetchTryAppInfo = (appId: string) => {
+ return consoleClient.trialApps.byAppId.get({ params: { app_id: appId } })
+}
+
+export const fetchTryAppDatasets = (appId: string, ids: string[]) => {
+ return consoleClient.trialApps.byAppId.datasets.get({
+ params: { app_id: appId },
+ query: { ids },
+ })
+}
+
+export const fetchTryAppFlowPreview = (appId: string) => {
+ return consoleClient.trialApps.byAppId.workflows.get({ params: { app_id: appId } })
+}
+
+export const fetchTryAppParams = (appId: string) => {
+ return consoleClient.trialApps.byAppId.parameters.get({ params: { app_id: appId } })
+ .then(normalizeTryAppParams)
+}
+
+export type TryAppInfo = Awaited>
diff --git a/web/service/use-try-app.ts b/web/service/use-try-app.ts
index b82290594c9..856e17812f5 100644
--- a/web/service/use-try-app.ts
+++ b/web/service/use-try-app.ts
@@ -1,11 +1,10 @@
-import type { DataSetListResponse } from '@/models/datasets'
import { useQuery } from '@tanstack/react-query'
import { consoleQuery } from '@/service/client'
import { fetchTryAppDatasets, fetchTryAppFlowPreview, fetchTryAppInfo, fetchTryAppParams } from './try-app'
export const useGetTryAppInfo = (appId: string) => {
return useQuery({
- queryKey: consoleQuery.trialApps.info.queryKey({ input: { params: { appId } } }),
+ queryKey: consoleQuery.trialApps.byAppId.get.queryKey({ input: { params: { app_id: appId } } }),
queryFn: () => {
return fetchTryAppInfo(appId)
},
@@ -15,7 +14,7 @@ export const useGetTryAppInfo = (appId: string) => {
export const useGetTryAppParams = (appId: string) => {
return useQuery({
- queryKey: consoleQuery.trialApps.parameters.queryKey({ input: { params: { appId } } }),
+ queryKey: consoleQuery.trialApps.byAppId.parameters.get.queryKey({ input: { params: { app_id: appId } } }),
queryFn: () => {
return fetchTryAppParams(appId)
},
@@ -24,8 +23,8 @@ export const useGetTryAppParams = (appId: string) => {
}
export const useGetTryAppDataSets = (appId: string, ids: string[]) => {
- return useQuery({
- queryKey: consoleQuery.trialApps.datasets.queryKey({ input: { params: { appId }, query: { ids } } }),
+ return useQuery({
+ queryKey: consoleQuery.trialApps.byAppId.datasets.get.queryKey({ input: { params: { app_id: appId }, query: { ids } } }),
queryFn: () => {
return fetchTryAppDatasets(appId, ids)
},
@@ -35,7 +34,7 @@ export const useGetTryAppDataSets = (appId: string, ids: string[]) => {
export const useGetTryAppFlowPreview = (appId: string, disabled?: boolean) => {
return useQuery({
- queryKey: consoleQuery.trialApps.workflows.queryKey({ input: { params: { appId } } }),
+ queryKey: consoleQuery.trialApps.byAppId.workflows.get.queryKey({ input: { params: { app_id: appId } } }),
enabled: !disabled,
queryFn: () => {
return fetchTryAppFlowPreview(appId)
diff --git a/web/types/app.ts b/web/types/app.ts
index 1804acbfaca..c55745e76f1 100644
--- a/web/types/app.ts
+++ b/web/types/app.ts
@@ -85,22 +85,47 @@ export type PromptVariable = {
}
type TextTypeFormItem = {
- default: string
+ default?: string
label: string
variable: string
required: boolean
- max_length: number
- hide: boolean
+ max_length?: number
+ hide?: boolean
}
type SelectTypeFormItem = {
- default: string
+ default?: string
label: string
variable: string
required: boolean
- options: string[]
- hide: boolean
+ options?: string[]
+ hide?: boolean
}
+
+type NumberTypeFormItem = Omit & {
+ default?: string | number
+ max_length?: number
+}
+
+type CheckboxTypeFormItem = Omit & {
+ default?: string | boolean
+}
+
+type FileTypeFormItem = Omit & Partial & {
+ max_length?: number
+}
+
+type ExternalDataToolFormItem = ExternalDataTool & {
+ label: string
+ variable: string
+ required?: boolean
+ hide?: boolean
+}
+
+type JsonObjectFormItem = Omit & {
+ json_schema?: string | Record
+}
+
/**
* User Input Form Item
*/
@@ -110,6 +135,18 @@ export type UserInputFormItem = {
select: SelectTypeFormItem
} | {
paragraph: TextTypeFormItem
+} | {
+ number: NumberTypeFormItem
+} | {
+ checkbox: CheckboxTypeFormItem
+} | {
+ file: FileTypeFormItem
+} | {
+ 'file-list': FileTypeFormItem
+} | {
+ external_data_tool: ExternalDataToolFormItem
+} | {
+ json_object: JsonObjectFormItem
}
export type AgentTool = {
@@ -118,7 +155,7 @@ export type AgentTool = {
provider_name: string
tool_name: string
tool_label: string
- tool_parameters: Record
+ tool_parameters: Record
enabled: boolean
isDeleted?: boolean
notAuthor?: boolean
@@ -380,7 +417,7 @@ export type App = {
updated_at: number
updated_by?: string
}
- deleted_tools?: Array<{ id: string, tool_name: string }>
+ deleted_tools?: Array<{ type: string, provider_id: string, tool_name: string }>
/** access control */
access_mode: AccessMode
max_active_requests?: number | null
diff --git a/web/utils/model-config.spec.ts b/web/utils/model-config.spec.ts
index f4b6b642784..de3a3ea5913 100644
--- a/web/utils/model-config.spec.ts
+++ b/web/utils/model-config.spec.ts
@@ -1,5 +1,6 @@
import type { PromptVariable } from '@/models/debug'
import type { UserInputFormItem } from '@/types/app'
+import { SupportUploadFileTypes } from '@/app/components/workflow/types'
/**
* Test suite for model configuration transformation utilities
*
@@ -18,6 +19,20 @@ import {
userInputsFormToPromptVariables,
} from './model-config'
+const getTextInput = (item: UserInputFormItem | undefined) => {
+ if (!item || !('text-input' in item))
+ throw new Error('Expected text-input user input form item')
+
+ return item['text-input']
+}
+
+const getSelectInput = (item: UserInputFormItem | undefined) => {
+ if (!item || !('select' in item))
+ throw new Error('Expected select user input form item')
+
+ return item.select
+}
+
describe('Model Config Utilities', () => {
describe('userInputsFormToPromptVariables', () => {
/**
@@ -110,7 +125,7 @@ describe('Model Config Utilities', () => {
default: '',
hide: false,
},
- } as any,
+ },
]
const result = userInputsFormToPromptVariables(userInputs)
@@ -140,7 +155,7 @@ describe('Model Config Utilities', () => {
default: '',
hide: false,
},
- } as any,
+ },
]
const result = userInputsFormToPromptVariables(userInputs)
@@ -199,13 +214,13 @@ describe('Model Config Utilities', () => {
label: 'Profile Picture',
variable: 'profile_pic',
required: false,
- allowed_file_types: ['image'],
+ allowed_file_types: [SupportUploadFileTypes.image],
allowed_file_extensions: ['.jpg', '.png'],
allowed_file_upload_methods: ['local_file', 'remote_url'],
default: '',
hide: false,
},
- } as any,
+ },
]
const result = userInputsFormToPromptVariables(userInputs)
@@ -237,14 +252,14 @@ describe('Model Config Utilities', () => {
label: 'Documents',
variable: 'documents',
required: true,
- allowed_file_types: ['document'],
+ allowed_file_types: [SupportUploadFileTypes.document],
allowed_file_extensions: ['.pdf', '.docx'],
allowed_file_upload_methods: ['local_file'],
max_length: 5,
default: '',
hide: false,
},
- } as any,
+ },
]
const result = userInputsFormToPromptVariables(userInputs)
@@ -283,7 +298,7 @@ describe('Model Config Utilities', () => {
icon_background: '#FF5733',
hide: false,
},
- } as any,
+ },
]
const result = userInputsFormToPromptVariables(userInputs)
@@ -349,7 +364,7 @@ describe('Model Config Utilities', () => {
default: '',
hide: false,
},
- } as any,
+ },
{
select: {
label: 'Gender',
@@ -553,7 +568,7 @@ describe('Model Config Utilities', () => {
const result = promptVariablesToUserInputsForm(promptVariables)
expect(result).toHaveLength(1)
- expect((result[0] as any)['text-input']?.variable).toBe('valid_key')
+ expect(getTextInput(result[0]).variable).toBe('valid_key')
})
/**
@@ -613,8 +628,8 @@ describe('Model Config Utilities', () => {
const result = promptVariablesToUserInputsForm(promptVariables)
- expect((result[0] as any)['text-input']?.required).toBe(true)
- expect((result[1] as any)['text-input']?.required).toBe(false)
+ expect(getTextInput(result[0]).required).toBe(true)
+ expect(getTextInput(result[1]).required).toBe(false)
})
})
@@ -743,7 +758,7 @@ describe('Model Config Utilities', () => {
bool1: 1,
bool2: 0,
bool3: 'yes',
- bool4: null as any,
+ bool4: null,
}
const result = formatBooleanInputs(useInputs, inputs)
@@ -811,9 +826,9 @@ describe('Model Config Utilities', () => {
const backToUserInputs = promptVariablesToUserInputsForm(promptVars)
expect(backToUserInputs).toHaveLength(2)
- expect((backToUserInputs[0] as any)['text-input']?.variable).toBe('name')
- expect((backToUserInputs[1] as any).select?.variable).toBe('type')
- expect((backToUserInputs[1] as any).select?.options).toEqual(['A', 'B', 'C'])
+ expect(getTextInput(backToUserInputs[0]).variable).toBe('name')
+ expect(getSelectInput(backToUserInputs[1]).variable).toBe('type')
+ expect(getSelectInput(backToUserInputs[1]).options).toEqual(['A', 'B', 'C'])
})
})
})
diff --git a/web/utils/model-config.ts b/web/utils/model-config.ts
index 6ed408b37f0..ee5322a7a06 100644
--- a/web/utils/model-config.ts
+++ b/web/utils/model-config.ts
@@ -1,94 +1,139 @@
import type { PromptVariable } from '@/models/debug'
import type { UserInputFormItem } from '@/types/app'
-export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] | null, dataset_query_variable?: string) => {
+const isRecord = (value: unknown): value is Record => {
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
+}
+
+const getString = (value: unknown) => {
+ return typeof value === 'string' ? value : ''
+}
+
+const getOptionalString = (value: unknown) => {
+ return typeof value === 'string' ? value : undefined
+}
+
+const getBoolean = (value: unknown, fallback = false) => {
+ return typeof value === 'boolean' ? value : fallback
+}
+
+const getNumber = (value: unknown) => {
+ return typeof value === 'number' ? value : undefined
+}
+
+const getDefaultValue = (value: unknown) => {
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean' ? value : undefined
+}
+
+const getStringArray = (value: unknown) => {
+ return Array.isArray(value) ? value.filter((item): item is string => typeof item === 'string') : []
+}
+
+const getStringRecord = (value: unknown) => {
+ if (!isRecord(value))
+ return undefined
+
+ const record: Record = {}
+ Object.entries(value).forEach(([key, item]) => {
+ if (typeof item === 'string')
+ record[key] = item
+ })
+ return record
+}
+
+const getRecord = (value: unknown) => {
+ return isRecord(value) ? value : {}
+}
+
+const getInputFormContent = (item: Record) => {
+ if (isRecord(item.paragraph))
+ return { type: 'paragraph', content: item.paragraph }
+
+ if (isRecord(item['text-input']))
+ return { type: 'string', content: item['text-input'] }
+
+ if (isRecord(item.number))
+ return { type: 'number', content: item.number }
+
+ if (isRecord(item.checkbox))
+ return { type: 'boolean', content: item.checkbox }
+
+ if (isRecord(item.file))
+ return { type: 'file', content: item.file }
+
+ if (isRecord(item['file-list']))
+ return { type: 'file-list', content: item['file-list'] }
+
+ if (isRecord(item.external_data_tool))
+ return { type: getString(item.external_data_tool.type), content: item.external_data_tool }
+
+ if (isRecord(item.json_object))
+ return { type: 'json_object', content: item.json_object }
+
+ return { type: 'select', content: getRecord(item.select) }
+}
+
+export const userInputsFormToPromptVariables = (useInputs: Record[] | null, dataset_query_variable?: string) => {
if (!useInputs)
return []
const promptVariables: PromptVariable[] = []
- useInputs.forEach((item: any) => {
- const isParagraph = !!item.paragraph
-
- const [type, content] = (() => {
- if (isParagraph)
- return ['paragraph', item.paragraph]
-
- if (item['text-input'])
- return ['string', item['text-input']]
-
- if (item.number)
- return ['number', item.number]
-
- if (item.checkbox)
- return ['boolean', item.checkbox]
-
- if (item.file)
- return ['file', item.file]
-
- if (item['file-list'])
- return ['file-list', item['file-list']]
-
- if (item.external_data_tool)
- return [item.external_data_tool.type, item.external_data_tool]
-
- if (item.json_object)
- return ['json_object', item.json_object]
-
- return ['select', item.select || {}]
- })()
- const is_context_var = dataset_query_variable === content?.variable
+ useInputs.forEach((item) => {
+ const { type, content } = getInputFormContent(item)
+ const variable = getString(content.variable)
+ const is_context_var = dataset_query_variable === variable
if (type === 'string' || type === 'paragraph') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type,
- max_length: content.max_length,
+ max_length: getNumber(content.max_length),
options: [],
is_context_var,
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else if (type === 'number') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type,
options: [],
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else if (type === 'boolean') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type: 'checkbox',
options: [],
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else if (type === 'select') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type: 'select',
- options: content.options,
+ options: getStringArray(content.options),
is_context_var,
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else if (type === 'file') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type,
config: {
allowed_file_types: content.allowed_file_types,
@@ -96,38 +141,38 @@ export const userInputsFormToPromptVariables = (useInputs: UserInputFormItem[] |
allowed_file_upload_methods: content.allowed_file_upload_methods,
number_limits: 1,
},
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else if (type === 'file-list') {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
type,
config: {
allowed_file_types: content.allowed_file_types,
allowed_file_extensions: content.allowed_file_extensions,
allowed_file_upload_methods: content.allowed_file_upload_methods,
- number_limits: content.max_length,
+ number_limits: getNumber(content.max_length),
},
- hide: content.hide,
- default: content.default,
+ hide: getBoolean(content.hide),
+ default: getDefaultValue(content.default),
})
}
else {
promptVariables.push({
- key: content.variable,
- name: content.label,
- required: content.required,
- type: content.type,
- enabled: content.enabled,
- config: content.config,
- icon: content.icon,
- icon_background: content.icon_background,
+ key: variable,
+ name: getString(content.label),
+ required: getBoolean(content.required, true),
+ type: getString(content.type || type),
+ enabled: getBoolean(content.enabled),
+ config: getRecord(content.config),
+ icon: getOptionalString(content.icon),
+ icon_background: getOptionalString(content.icon_background),
is_context_var,
- hide: content.hide,
+ hide: getBoolean(content.hide),
})
}
})
@@ -138,10 +183,10 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
const userInputs: UserInputFormItem[] = []
promptVariables.filter(({ key, name }) => {
return key && key.trim() && name && name.trim()
- }).forEach((item: any) => {
- if (item.type === 'string' || item.type === 'paragraph') {
+ }).forEach((item) => {
+ if (item.type === 'string') {
userInputs.push({
- [item.type === 'string' ? 'text-input' : 'paragraph']: {
+ 'text-input': {
label: item.name,
variable: item.key,
required: item.required !== false, // default true
@@ -149,19 +194,43 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
default: '',
hide: item.hide,
},
- } as any)
+ })
return
}
- if (item.type === 'number' || item.type === 'checkbox') {
+ if (item.type === 'paragraph') {
userInputs.push({
- [item.type]: {
+ paragraph: {
+ label: item.name,
+ variable: item.key,
+ required: item.required !== false, // default true
+ max_length: item.max_length,
+ default: '',
+ hide: item.hide,
+ },
+ })
+ return
+ }
+ if (item.type === 'number') {
+ userInputs.push({
+ number: {
label: item.name,
variable: item.key,
required: item.required !== false, // default true
default: '',
hide: item.hide,
},
- } as any)
+ })
+ }
+ else if (item.type === 'checkbox') {
+ userInputs.push({
+ checkbox: {
+ label: item.name,
+ variable: item.key,
+ required: item.required !== false, // default true
+ default: '',
+ hide: item.hide,
+ },
+ })
}
else if (item.type === 'select') {
userInputs.push({
@@ -170,10 +239,10 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
variable: item.key,
required: item.required !== false, // default true
options: item.options,
- default: item.default ?? '',
+ default: getString(item.default),
hide: item.hide,
},
- } as any)
+ })
}
else {
userInputs.push({
@@ -182,20 +251,20 @@ export const promptVariablesToUserInputsForm = (promptVariables: PromptVariable[
variable: item.key,
enabled: item.enabled,
type: item.type,
- config: item.config,
+ config: getStringRecord(item.config),
required: item.required,
icon: item.icon,
icon_background: item.icon_background,
hide: item.hide,
},
- } as any)
+ })
}
})
return userInputs
}
-export const formatBooleanInputs = (useInputs?: PromptVariable[] | null, inputs?: Record | null) => {
+export const formatBooleanInputs = (useInputs?: PromptVariable[] | null, inputs?: Record | null) => {
if (!useInputs)
return inputs
const res = { ...inputs }