From cb6fe9d0da535180eb83ceccff5373bafde7c04c Mon Sep 17 00:00:00 2001 From: papapatrick <109422393+Patrickill@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:37:06 +0800 Subject: [PATCH] feat: support select field as tool output (#2798) * feat: support select field as tool output * defaultOutput --- packages/global/core/workflow/type/io.d.ts | 1 + .../core/workflow/dispatch/plugin/run.ts | 22 +- .../web/components/common/Icon/constants.ts | 4 + .../core/workflow/template/pluginOutput.svg | 21 +- .../core/workflow/template/toolkitActive.svg | 14 ++ .../workflow/template/toolkitInactive.svg | 7 + .../core/workflow/template/workflowStart.svg | 15 +- packages/web/i18n/en/app.json | 1 + packages/web/i18n/en/common.json | 2 + packages/web/i18n/en/workflow.json | 9 + packages/web/i18n/zh/workflow.json | 3 + .../Flow/nodes/NodePluginIO/PluginOutput.tsx | 73 ++++--- .../NodePluginIO/PluginOutputEditModal.tsx | 197 ++++++++++++++++++ 13 files changed, 323 insertions(+), 46 deletions(-) create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/toolkitActive.svg create mode 100644 packages/web/components/common/Icon/icons/core/workflow/template/toolkitInactive.svg create mode 100644 projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutputEditModal.tsx diff --git a/packages/global/core/workflow/type/io.d.ts b/packages/global/core/workflow/type/io.d.ts index 988789ff5..a19fa8eaa 100644 --- a/packages/global/core/workflow/type/io.d.ts +++ b/packages/global/core/workflow/type/io.d.ts @@ -54,6 +54,7 @@ export type FlowNodeInputItemType = InputComponentPropsType & { // render components params canEdit?: boolean; // dynamic inputs isPro?: boolean; // Pro version field + isToolOutput?: boolean; }; export type FlowNodeOutputItemType = { diff --git a/packages/service/core/workflow/dispatch/plugin/run.ts b/packages/service/core/workflow/dispatch/plugin/run.ts index c36b196a2..b73b6c47d 100644 --- a/packages/service/core/workflow/dispatch/plugin/run.ts +++ b/packages/service/core/workflow/dispatch/plugin/run.ts @@ -47,6 +47,16 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise node.flowNodeType === FlowNodeTypeEnum.pluginOutput)! + .inputs.reduce( + (acc, cur) => { + acc[cur.key] = cur.isToolOutput === false ? false : true; + return acc; + }, + {} as Record + ); + const runtimeNodes = storeNodes2RuntimeNodes( plugin.nodes, getWorkflowEntryNodeIds(plugin.nodes) @@ -130,7 +140,17 @@ export const dispatchRunPlugin = async (props: RunPluginProps): Promise outputFilterMap[key]) + .reduce( + (acc, key) => { + acc[key] = output.pluginOutput![key]; + return acc; + }, + {} as Record + ) + : {}, ...(output ? output.pluginOutput : {}) }; }; diff --git a/packages/web/components/common/Icon/constants.ts b/packages/web/components/common/Icon/constants.ts index 6f82a1a6b..abcc7400d 100644 --- a/packages/web/components/common/Icon/constants.ts +++ b/packages/web/components/common/Icon/constants.ts @@ -244,6 +244,10 @@ export const iconPaths = { 'core/workflow/template/reply': () => import('./icons/core/workflow/template/reply.svg'), 'core/workflow/template/runApp': () => import('./icons/core/workflow/template/runApp.svg'), 'core/workflow/template/stopTool': () => import('./icons/core/workflow/template/stopTool.svg'), + 'core/workflow/template/toolkitActive': () => + import('./icons/core/workflow/template/toolkitActive.svg'), + 'core/workflow/template/toolkitInactive': () => + import('./icons/core/workflow/template/toolkitInactive.svg'), 'core/workflow/template/systemConfig': () => import('./icons/core/workflow/template/systemConfig.svg'), 'core/workflow/template/textConcat': () => diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/pluginOutput.svg b/packages/web/components/common/Icon/icons/core/workflow/template/pluginOutput.svg index a5b9ca0b7..3a62a92d4 100644 --- a/packages/web/components/common/Icon/icons/core/workflow/template/pluginOutput.svg +++ b/packages/web/components/common/Icon/icons/core/workflow/template/pluginOutput.svg @@ -1,13 +1,16 @@ - - - - - - + + + + + + - - - + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/toolkitActive.svg b/packages/web/components/common/Icon/icons/core/workflow/template/toolkitActive.svg new file mode 100644 index 000000000..2ca0918b2 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/toolkitActive.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/toolkitInactive.svg b/packages/web/components/common/Icon/icons/core/workflow/template/toolkitInactive.svg new file mode 100644 index 000000000..b81ee0e69 --- /dev/null +++ b/packages/web/components/common/Icon/icons/core/workflow/template/toolkitInactive.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/web/components/common/Icon/icons/core/workflow/template/workflowStart.svg b/packages/web/components/common/Icon/icons/core/workflow/template/workflowStart.svg index 4cd6718fe..3b5d21ffe 100644 --- a/packages/web/components/common/Icon/icons/core/workflow/template/workflowStart.svg +++ b/packages/web/components/common/Icon/icons/core/workflow/template/workflowStart.svg @@ -1,10 +1,13 @@ - - - + + + + + + - - - + + + diff --git a/packages/web/i18n/en/app.json b/packages/web/i18n/en/app.json index 47dfe35f8..8a5e0d0a8 100644 --- a/packages/web/i18n/en/app.json +++ b/packages/web/i18n/en/app.json @@ -62,6 +62,7 @@ "llm_not_support_vision": "This model does not support image recognition", "llm_use_vision": "Enable Image Recognition", "llm_use_vision_tip": "Once image recognition is enabled, this model will automatically receive images uploaded from the 'dialog box' and image links in 'user questions'.", + "logs_chat_user": "user", "logs_empty": "No logs yet~", "logs_message_total": "Total Messages", "logs_title": "Title", diff --git a/packages/web/i18n/en/common.json b/packages/web/i18n/en/common.json index 19f551a57..20463aa66 100644 --- a/packages/web/i18n/en/common.json +++ b/packages/web/i18n/en/common.json @@ -239,6 +239,7 @@ "comon.Continue_Adding": "Continue Adding", "compliance.chat": "The content is generated by third-party AI and cannot be guaranteed to be true and accurate. It is for reference only.", "compliance.compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.", + "compliance.dataset": "Please ensure that your content strictly complies with relevant laws and regulations and avoid containing any illegal or infringing content. \nPlease be careful when uploading materials that may contain sensitive information.", "confirm_choice": "Confirm Choice", "contribute_app_template": "Contribute Template", "core.Chat": "Chat", @@ -732,6 +733,7 @@ "core.module.template.empty_plugin": "Blank plugin", "core.module.template.empty_workflow": "Blank workflow", "core.module.template.http body placeholder": "Same syntax as Apifox", + "core.module.template.self_input": "Plug-in input", "core.module.template.self_output": "Custom plug-in output", "core.module.template.system_config": "System configuration", "core.module.template.system_config_info": "Can configure application system parameters", diff --git a/packages/web/i18n/en/workflow.json b/packages/web/i18n/en/workflow.json index fea83cc4b..74991ae50 100644 --- a/packages/web/i18n/en/workflow.json +++ b/packages/web/i18n/en/workflow.json @@ -3,6 +3,7 @@ "Code": "Code", "Quote_prompt_setting": "Quote prompt", "add_new_input": "Add New Input", + "add_new_output": "New output", "append_application_reply_to_history_as_new_context": "Append the application's reply to the history as new context", "application_call": "Application Call", "assigned_reply": "Assigned Reply", @@ -33,6 +34,7 @@ "dynamic_input_description": "Receive the output value of the previous node as a variable, which can be used by Laf request parameters.", "dynamic_input_description_concat": "You can reference the output of other nodes as variables for text concatenation. Type / to invoke the variable list.", "edit_input": "Edit Input", + "edit_output": "Edit output", "end_with": "Ends With", "error_info_returns_empty_on_success": "Error information of code execution, returns empty on success", "execute_a_simple_script_code_usually_for_complex_data_processing": "Execute a simple script code, usually for complex data processing.", @@ -66,6 +68,7 @@ "intro_http_request": "Can send an HTTP request to perform more complex operations (network search, database query, etc.)", "intro_knowledge_base_search_merge": "Can merge multiple Dataset search results for output. Uses RRF merging method for final sorting output.", "intro_laf_function_call": "Can call cloud functions under the Laf account.", + "intro_loop": "You can input an array, the elements in the array will execute the loop body independently, and output all results as an array.", "intro_plugin_input": "Can configure what inputs the plugin needs and use these inputs to run the plugin.", "intro_question_classification": "Determine the type of question based on the user's history and current question. Multiple question types can be added. Below is a template example:\nType 1: Greeting\nType 2: Questions about product 'usage'\nType 3: Questions about product 'purchase'\nType 4: Other questions", "intro_question_optimization": "Using question optimization can improve the accuracy of Dataset searches during continuous conversations. After using this function, AI will first construct one or more new search terms based on the context, which are more conducive to Dataset searches. This module is already built into the Dataset search module. If you only perform a single Dataset search, you can directly use the built-in completion function of the Dataset.", @@ -76,6 +79,7 @@ "is_equal_to": "Is Equal To", "is_not_empty": "Is Not Empty", "is_not_equal": "Is Not Equal", + "is_tool_output": "Whether to use this field as tool output when the tool is called", "judgment_result": "Judgment Result", "knowledge_base_reference": "Dataset Reference", "knowledge_base_search_merge": "Dataset Search Merge", @@ -89,6 +93,11 @@ "less_than": "Less Than", "less_than_or_equal_to": "Less Than or Equal To", "loop": "Batch execution", + "loop_body": "loop body", + "loop_end": "end of loop", + "loop_input_array": "array", + "loop_result": "Array execution results", + "loop_start": "The loop body begins", "loop_start_tip": "Not input array", "max_dialog_rounds": "Maximum Number of Dialog Rounds", "max_tokens": "Maximum Tokens", diff --git a/packages/web/i18n/zh/workflow.json b/packages/web/i18n/zh/workflow.json index 6382079d8..45a623401 100644 --- a/packages/web/i18n/zh/workflow.json +++ b/packages/web/i18n/zh/workflow.json @@ -3,6 +3,7 @@ "Code": "代码", "Quote_prompt_setting": "引用提示词配置", "add_new_input": "新增输入", + "add_new_output": "新增输出", "append_application_reply_to_history_as_new_context": "将该应用回复内容拼接到历史记录中,作为新的上下文返回", "application_call": "应用调用", "assigned_reply": "指定回复", @@ -33,6 +34,7 @@ "dynamic_input_description": "接收前方节点的输出值作为变量,这些变量可以被 Laf 请求参数使用。", "dynamic_input_description_concat": "可以引用其他节点的输出,作为文本拼接的变量,输入 / 唤起变量列表", "edit_input": "编辑输入", + "edit_output": "编辑输出", "end_with": "结束为", "error_info_returns_empty_on_success": "代码运行错误信息,成功时返回空", "execute_a_simple_script_code_usually_for_complex_data_processing": "执行一段简单的脚本代码,通常用于进行复杂的数据处理。", @@ -77,6 +79,7 @@ "is_equal_to": "等于", "is_not_empty": "不为空", "is_not_equal": "不等于", + "is_tool_output": "是否在工具调用时,将该字段作为工具输出", "judgment_result": "判断结果", "knowledge_base_reference": "知识库引用", "knowledge_base_search_merge": "知识库搜索引用合并", diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx index 548e0bc30..4295dac7c 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutput.tsx @@ -25,13 +25,11 @@ import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; import { useI18n } from '@/web/context/I18n'; import { useConfirm } from '@fastgpt/web/hooks/useConfirm'; import { isWorkflowStartOutput } from '@fastgpt/global/core/workflow/template/system/workflowStart'; -import { defaultInput } from '../render/RenderInput/FieldEditModal'; +import PluginOutputEditModal, { defaultOutput } from './PluginOutputEditModal'; -const FieldEditModal = dynamic(() => import('../render/RenderInput/FieldEditModal')); - -const customInputConfig = { +const customOutputConfig = { selectValueTypeList: Object.values(WorkflowIOValueTypeEnum), - showDescription: true, + showDefaultValue: true }; @@ -62,7 +60,7 @@ const NodePluginOutput = ({ data, selected }: NodeProps) => { leftIcon={} iconSpacing={1} size={'sm'} - onClick={() => setEditField(defaultInput)} + onClick={() => setEditField(defaultOutput)} > {t('common:common.Add New')} @@ -78,9 +76,9 @@ const NodePluginOutput = ({ data, selected }: NodeProps) => { {!!editField && ( - input.key)} onClose={() => setEditField(undefined)} onSubmit={({ data }) => { @@ -171,31 +169,46 @@ function Reference({ return ( <> - - {input.label} - {input.description && } - {/* value */} - + + + {input.label} + {input.description && } + {/* value */} + - setEditField(input)} - /> + setEditField(input)} + /> + + setEditField(input)} /> {!!editField && ( - setEditField(undefined)} onSubmit={onUpdateField} diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutputEditModal.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutputEditModal.tsx new file mode 100644 index 000000000..63c6fedcb --- /dev/null +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/NodePluginIO/PluginOutputEditModal.tsx @@ -0,0 +1,197 @@ +import { FlowValueTypeMap } from '@fastgpt/global/core/workflow/node/constant'; +import { + Box, + Button, + Flex, + Input, + ModalBody, + ModalFooter, + Stack, + Textarea, + Switch +} from '@chakra-ui/react'; +import { WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; +import { FlowNodeInputTypeEnum } from '@fastgpt/global/core/workflow/node/constant'; +import { + CustomFieldConfigType, + FlowNodeInputItemType +} from '@fastgpt/global/core/workflow/type/io'; +import MyModal from '@fastgpt/web/components/common/MyModal'; +import MySelect from '@fastgpt/web/components/common/MySelect'; +import { useToast } from '@fastgpt/web/hooks/useToast'; +import React, { useCallback, useMemo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'next-i18next'; +import { useMount } from 'ahooks'; +import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel'; + +const PluginOutputEditModal = ({ + customOutputConfig, + defaultOutput, + keys, + onClose, + onSubmit +}: { + customOutputConfig: CustomFieldConfigType; + defaultOutput: FlowNodeInputItemType; + keys: string[]; + onClose: () => void; + onSubmit: (e: { data: FlowNodeInputItemType; isChangeKey: boolean }) => void; +}) => { + const { t } = useTranslation(); + const { toast } = useToast(); + const isEdit = !!defaultOutput.key; + + const { register, setValue, handleSubmit, watch } = useForm({ + defaultValues: { ...defaultOutput, isToolOutput: defaultOutput.isToolOutput !== false } + }); + const inputType = FlowNodeInputTypeEnum.reference; + + // value type select + const showValueTypeSelect = useMemo(() => { + if ( + !customOutputConfig.selectValueTypeList || + customOutputConfig.selectValueTypeList.length <= 1 + ) + return false; + if (inputType === FlowNodeInputTypeEnum.reference) return true; + + return false; + }, [customOutputConfig.selectValueTypeList, inputType]); + const valueTypeSelectList = useMemo(() => { + if (!customOutputConfig.selectValueTypeList) return []; + + const dataTypeSelectList = Object.values(FlowValueTypeMap).map((item) => ({ + label: t(item.label as any), + value: item.value + })); + + return dataTypeSelectList.filter((item) => + customOutputConfig.selectValueTypeList?.includes(item.value) + ); + }, [customOutputConfig.selectValueTypeList, t]); + + const valueType = watch('valueType'); + + useMount(() => { + if ( + customOutputConfig.selectValueTypeList && + customOutputConfig.selectValueTypeList.length > 0 && + !valueType + ) { + setValue('valueType', customOutputConfig.selectValueTypeList[0]); + } + }); + + const onSubmitSuccess = useCallback( + (data: FlowNodeInputItemType) => { + const isChangeKey = defaultOutput.key !== data.key; + + if (keys.includes(data.key)) { + if (!isEdit || isChangeKey) { + toast({ + status: 'warning', + title: t('workflow:field_name_already_exists') + }); + return; + } + } + + data.key = data?.key?.trim(); + data.label = data.key; + + onSubmit({ + data, + isChangeKey + }); + onClose(); + }, + [defaultOutput.key, isEdit, keys, onClose, onSubmit, toast, t] + ); + const onSubmitError = useCallback( + (e: Object) => { + for (const item of Object.values(e)) { + if (item.message) { + toast({ + status: 'warning', + title: item.message + }); + break; + } + } + }, + [toast] + ); + + return ( + + + + {showValueTypeSelect && ( + + {t('common:core.module.Data Type')} + + + w={'full'} + list={valueTypeSelectList.filter( + (item) => item.value !== WorkflowIOValueTypeEnum.arrayAny + )} + value={valueType} + onchange={(e) => { + setValue('valueType', e); + }} + /> + + + )} + {/* key */} + + + {t('common:core.module.Field Name')} + + + + + + {t('workflow:input_description')} + + + + {t('workflow:is_tool_output')} + + + + + + + + + + ); +}; + +export default PluginOutputEditModal; + +export const defaultOutput: FlowNodeInputItemType = { + renderTypeList: [FlowNodeInputTypeEnum.reference], + valueType: WorkflowIOValueTypeEnum.string, + canEdit: true, + key: '', + label: '', + isToolOutput: true +};