diff --git a/docSite/assets/imgs/fileinpu-6.jpg b/docSite/assets/imgs/fileinpu-6.jpg deleted file mode 100644 index 5a649e1d3..000000000 Binary files a/docSite/assets/imgs/fileinpu-6.jpg and /dev/null differ diff --git a/docSite/assets/imgs/fileinpu-7.jpg b/docSite/assets/imgs/fileinpu-7.jpg deleted file mode 100644 index 975a5ae26..000000000 Binary files a/docSite/assets/imgs/fileinpu-7.jpg and /dev/null differ diff --git a/docSite/assets/imgs/image-5.png b/docSite/assets/imgs/image-5.png new file mode 100644 index 000000000..40abc7cde Binary files /dev/null and b/docSite/assets/imgs/image-5.png differ diff --git a/docSite/assets/imgs/image-6.png b/docSite/assets/imgs/image-6.png new file mode 100644 index 000000000..2235b9d62 Binary files /dev/null and b/docSite/assets/imgs/image-6.png differ diff --git a/docSite/assets/imgs/image-7.png b/docSite/assets/imgs/image-7.png new file mode 100644 index 000000000..99b26e3ab Binary files /dev/null and b/docSite/assets/imgs/image-7.png differ diff --git a/docSite/content/zh-cn/docs/course/fileInput.md b/docSite/content/zh-cn/docs/course/fileInput.md index d9e9c91bf..5eb88b33a 100644 --- a/docSite/content/zh-cn/docs/course/fileInput.md +++ b/docSite/content/zh-cn/docs/course/fileInput.md @@ -20,10 +20,12 @@ weight: 110 随后,你的调试对话框中,就会出现一个文件选择的 icon,可以点击文件选择 icon,选择你需要上传的文件。 -由于采用的是工具调用模式,所以在提问时候,可能需要加上适当的引导,让模型知道,你需要读取`文档`。 - ![打开文件上传](/imgs/fileinpu-2.png) +**工作模式** + +从 4.8.13 版本起,简易模式的文件读取将会强制解析文件并放入 system 提示词中,避免连续对话时,模型有时候不会主动调用读取文件的工具。 + ## 工作流中使用 工作流中,可以在系统配置中,找到`文件输入`配置项,点击其右侧的`开启`/`关闭`按键,即可打开配置弹窗。 @@ -32,14 +34,13 @@ weight: 110 在工作流中,使用文件的方式很多,最简单的就是类似下图中,直接通过工具调用接入文档解析,实现和简易模式一样的效果。 -![打开文件上传](/imgs/fileinpu-3.jpg) +| | | +| --------------------- | --------------------- | +| ![](/imgs/image-5.png) | ![](/imgs/image-6.png) | -也可以更简单点,强制每轮对话都携带上文档内容进行回答,这样就不需要调用两次 AI 才能读取文档内容了。 - -![打开文件上传](/imgs/fileinpu-5.jpg) - -当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。不过目前版本,`插件`中并未支持文件处理,所以在构建 SOP 的话可能还是有一些麻烦。 +当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。 +![文档解析](/imgs/image-7.png) ## 文档解析工作原理 @@ -73,23 +74,8 @@ type UserChatItemValueItemType = { 文档解析依赖文档解析节点,这个节点会接收一个`array`类型的输入,对应的是文件输入的 URL;输出的是一个`string`,对应的是文档解析后的内容。 -![打开文件上传](/imgs/fileinpu-6.jpg) - * 在文档解析节点中,只会解析`文档`类型的 URL,它是通过文件 URL 解析出来的`文名件后缀`去判断的。如果你同时选择了文档和图片,图片会被忽略。 -* 文档解析节点,除了解析本轮工作流接收的文件外,还会把历史记录中的文档 URL 进行解析。最终会解析至多 n 个文档,n 取决于你配置文件上传时,允许的最大文件数量。 - -{{% alert icon="🤖" context="success" %}} -举例: - -配置了最多允许 5 个文件上传 - -1. 第一轮对话,上传 3 个文档和 1 个图片:文档解析节点,返回 3 个文档内容。 -2. 第二轮对话,不上传任何文件:文档解析节点,返回 3 个文档内容。 -3. 第三轮对话,上传 2 个文档:文档解析节点,返回 5 个文档内容。 -4. 第四轮对话,上传 1 个文档:文档解析节点,返回 5 个文档内容,第一轮对话中的第三个文档会被过滤掉。 - -{{% /alert %}} - +* **文档解析节点,只会解析本轮工作流接收的文件,不会解析历史记录的文件。** * 多个文档内容如何拼接的 按下列的模板,对多个文件进行拼接,即文件名+文件内容的形式组成一个字符串,不同文档之间通过分隔符:`\n******\n` 进行分割。 @@ -101,32 +87,27 @@ ${content} ``` -### 工具调用如何使用文档解析 +### AI节点中如何使用文档解析 -在工具调用中,文档解析节点的调用提示词为:`解析对话中所有上传的文档,并返回对应文档内容`。 +在 AI 节点(AI对话/工具调用)中,新增了一个文档链接的输入,可以直接引用文档的地址,从而实现文档内容的引用。 -作为工具被执行后,文档解析节点会返回解析后的文档内容作为工具响应。 - -### AI对话中如何使用文档解析 - -在 AI 对话节点中,新增了一个文档引用的输入,可以直接引用文档解析节点的输出,从而实现文档内容的引用。 - -它接收一个`string`类型的输入,除了可以引用文档解析结果外,还可以实现自定义内容引用,最终会进行提示词拼接,放置在 role=system 的消息中。提示词模板如下: +它接收一个`Array`类型的输入,最终这些 url 会被解析,并进行提示词拼接,放置在 role=system 的消息中。提示词模板如下: ``` -将 中的内容作为本次对话的参考: - +将 中的内容作为本次对话的参考: + {{quote}} - + ``` -quote 为引用的内容。 +# 4.8.13版本起,关于文件上传的更新 -![打开文件上传](/imgs/fileinpu-7.jpg) +由于与 4.8.9 版本有些差异,尽管我们做了向下兼容,避免工作流立即不可用。但是请尽快的按新版本规则进行调整工作流,后续将会去除兼容性代码。 -## 文件输入后续更新 - -* 插件支持配置文件输入。 -* 子应用和插件调用,支持传递文件输入。 -* 文档解析,结构化解析结果。 -* 更多的文件类型输入以及解析器。 \ No newline at end of file +1. 简易模式中,将会强制进行文件解析,不再由模型决策是否解析,保证每次都能参考文档。 +2. 文档解析:不再解析历史记录中的文件。 +3. 工具调用:支持直接选择文档引用,不需要再挂载文档解析工具。会自动解析历史记录中的文件。 +4. AI 对话:支持直接选择文档引用,不需要进过文档解析节点。会自动解析历史记录中的文件。 +5. 插件单独运行:不再支持全局文件;插件输入支持配置文件类型,可以取代全局文件上传。 +6. **工作流调用插件:不再自动传递工作流上传的文件到插件,需要手动给插件输入指定变量。** +7. **工作流调用工作流:不再自动传递工作流上传的文件到子工作流,可以手动选择需要传递的文件链接。** \ No newline at end of file diff --git a/packages/global/core/workflow/constants.ts b/packages/global/core/workflow/constants.ts index c51d2ff10..578a81b3b 100644 --- a/packages/global/core/workflow/constants.ts +++ b/packages/global/core/workflow/constants.ts @@ -199,6 +199,7 @@ export enum NodeInputKeyEnum { childrenNodeIdList = 'childrenNodeIdList', nodeWidth = 'nodeWidth', nodeHeight = 'nodeHeight', + loopNodeInputHeight = 'loopNodeInputHeight', // loop start loopStartInput = 'loopStartInput', loopStartIndex = 'loopStartIndex', diff --git a/packages/global/core/workflow/template/input.ts b/packages/global/core/workflow/template/input.ts index a585d9651..72b4067c3 100644 --- a/packages/global/core/workflow/template/input.ts +++ b/packages/global/core/workflow/template/input.ts @@ -113,6 +113,13 @@ export const Input_Template_Node_Height: FlowNodeInputItemType = { label: '', value: 600 }; +export const Input_Template_LOOP_NODE_OFFSET: FlowNodeInputItemType = { + key: NodeInputKeyEnum.loopNodeInputHeight, + renderTypeList: [FlowNodeInputTypeEnum.hidden], + valueType: WorkflowIOValueTypeEnum.number, + label: '', + value: 320 +}; export const Input_Template_Stream_MODE: FlowNodeInputItemType = { key: NodeInputKeyEnum.forbidStream, diff --git a/packages/global/core/workflow/template/system/loop/loop.ts b/packages/global/core/workflow/template/system/loop/loop.ts index b99a1453f..2c12f73be 100644 --- a/packages/global/core/workflow/template/system/loop/loop.ts +++ b/packages/global/core/workflow/template/system/loop/loop.ts @@ -14,6 +14,7 @@ import { getHandleConfig } from '../../utils'; import { i18nT } from '../../../../../../web/i18n/utils'; import { Input_Template_Children_Node_List, + Input_Template_LOOP_NODE_OFFSET, Input_Template_Node_Height, Input_Template_Node_Width } from '../../input'; @@ -40,7 +41,8 @@ export const LoopNode: FlowNodeTemplateType = { }, Input_Template_Children_Node_List, Input_Template_Node_Width, - Input_Template_Node_Height + Input_Template_Node_Height, + Input_Template_LOOP_NODE_OFFSET ], outputs: [ { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx index 293dfc1ee..4fa255289 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/hooks/useWorkflow.tsx @@ -24,7 +24,7 @@ import { useKeyboard } from './useKeyboard'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../context'; import { THelperLine } from '@fastgpt/global/core/workflow/type'; -import { NodeInputKeyEnum } from '@fastgpt/global/core/workflow/constants'; +import { NodeInputKeyEnum, NodeOutputKeyEnum } from '@fastgpt/global/core/workflow/constants'; import { useDebounceEffect, useMemoizedFn } from 'ahooks'; import { Input_Template_Node_Height, @@ -304,7 +304,7 @@ export const useWorkflow = () => { } return acc; }, - { childNodes: [] as Node[], loopNode: undefined as Node | undefined } + { childNodes: [] as Node[], loopNode: undefined as Node | undefined } ); if (!loopNode) return; @@ -314,6 +314,10 @@ export const useWorkflow = () => { const width = Math.max(rect.width + 80, 840); const height = Math.max(rect.height + 80, 600); + const offsetHeight = + loopNode.data.inputs.find((input) => input.key === NodeInputKeyEnum.loopNodeInputHeight) + ?.value ?? 83; + // Update parentNode size and position onChangeNode({ nodeId: parentId, @@ -340,7 +344,7 @@ export const useWorkflow = () => { type: 'position', position: { x: rect.x - 70, - y: rect.y - 320 + y: rect.y - offsetHeight - 240 } } ]); @@ -608,8 +612,36 @@ export const useWorkflow = () => { state ) ); + + // Add default input + const node = nodeList.find((n) => n.nodeId === connect.target); + if (!node) return; + + // 1. Add file input + if ( + node.flowNodeType === FlowNodeTypeEnum.chatNode || + node.flowNodeType === FlowNodeTypeEnum.tools || + node.flowNodeType === FlowNodeTypeEnum.appModule + ) { + const input = node.inputs.find((i) => i.key === NodeInputKeyEnum.fileUrlList); + if (input && (!input?.value || input.value.length === 0)) { + const workflowStartNode = nodeList.find( + (n) => n.flowNodeType === FlowNodeTypeEnum.workflowStart + ); + if (!workflowStartNode) return; + onChangeNode({ + nodeId: node.nodeId, + type: 'updateInput', + key: NodeInputKeyEnum.fileUrlList, + value: { + ...input, + value: [[workflowStartNode.nodeId, NodeOutputKeyEnum.userFiles]] + } + }); + } + } }, - [setEdges] + [nodeList, onChangeNode, setEdges] ); const customOnConnect = useCallback( (connect: Connection) => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx index 0fe5f0dae..b79eed6de 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/Flow/nodes/Loop/NodeLoop.tsx @@ -6,7 +6,7 @@ import { FlowNodeItemType } from '@fastgpt/global/core/workflow/type/node'; import React, { useEffect, useMemo, useRef } from 'react'; -import { Background, NodePositionChange, NodeProps } from 'reactflow'; +import { Background, NodeProps } from 'reactflow'; import NodeCard from '../render/NodeCard'; import Container from '../../components/Container'; import IOTitle from '../../components/IOTitle'; @@ -21,7 +21,10 @@ import { VARIABLE_NODE_ID, WorkflowIOValueTypeEnum } from '@fastgpt/global/core/workflow/constants'; -import { Input_Template_Children_Node_List } from '@fastgpt/global/core/workflow/template/input'; +import { + Input_Template_Children_Node_List, + Input_Template_LOOP_NODE_OFFSET +} from '@fastgpt/global/core/workflow/template/input'; import { useContextSelector } from 'use-context-selector'; import { WorkflowContext } from '../../../context'; import { getWorkflowGlobalVariables } from '@/web/core/workflow/utils'; @@ -29,25 +32,30 @@ import { AppContext } from '../../../../context'; import { isValidArrayReferenceValue } from '@fastgpt/global/core/workflow/utils'; import { ReferenceArrayValueType } from '@fastgpt/global/core/workflow/type/io'; import { useWorkflow } from '../../hooks/useWorkflow'; -import { WorkflowNodeEdgeContext } from '../../../context/workflowInitContext'; import { useSize } from 'ahooks'; const NodeLoop = ({ data, selected }: NodeProps) => { const { t } = useTranslation(); const { nodeId, inputs, outputs, isFolded } = data; - const getNodes = useContextSelector(WorkflowNodeEdgeContext, (v) => v.getNodes); - const onNodesChange = useContextSelector(WorkflowNodeEdgeContext, (v) => v.onNodesChange); const nodeList = useContextSelector(WorkflowContext, (v) => v.nodeList); const onChangeNode = useContextSelector(WorkflowContext, (v) => v.onChangeNode); const appDetail = useContextSelector(AppContext, (v) => v.appDetail); const { resetParentNodeSizeAndPosition } = useWorkflow(); - const { nodeWidth, nodeHeight, loopInputArray } = useMemo(() => { + const { + nodeWidth, + nodeHeight, + loopInputArray, + loopNodeInputHeight = Input_Template_LOOP_NODE_OFFSET + } = useMemo(() => { return { nodeWidth: inputs.find((input) => input.key === NodeInputKeyEnum.nodeWidth)?.value, nodeHeight: inputs.find((input) => input.key === NodeInputKeyEnum.nodeHeight)?.value, - loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray) + loopInputArray: inputs.find((input) => input.key === NodeInputKeyEnum.loopInputArray), + loopNodeInputHeight: inputs.find( + (input) => input.key === NodeInputKeyEnum.loopNodeInputHeight + ) }; }, [inputs]); @@ -83,7 +91,6 @@ const NodeLoop = ({ data, selected }: NodeProps) => { })(value[0]); return ArrayTypeMap[valueType as keyof typeof ArrayTypeMap] ?? WorkflowIOValueTypeEnum.arrayAny; }, [appDetail.chatConfig, loopInputArray, nodeList]); - useEffect(() => { if (!loopInputArray) return; onChangeNode({ @@ -103,7 +110,6 @@ const NodeLoop = ({ data, selected }: NodeProps) => { nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId) ); }, [nodeId, nodeList]); - useEffect(() => { onChangeNode({ nodeId, @@ -117,35 +123,25 @@ const NodeLoop = ({ data, selected }: NodeProps) => { resetParentNodeSizeAndPosition(nodeId); }, [childrenNodeIdList]); - // Update child node position + // Update loop node offset value const inputBoxRef = useRef(null); const size = useSize(inputBoxRef); - const prevHeightRef = useRef(); // 添加 ref 来存储前一个高度值 useEffect(() => { if (!size?.height) return; - if (prevHeightRef.current === size.height) return; - const diffHeight = prevHeightRef.current ? size.height - prevHeightRef.current : 0; - prevHeightRef.current = size.height; - if (diffHeight === 0) return; - - // Get the height of the input box - // Computed input - const nodes = getNodes(); - const childNodes = nodes.filter((n) => n.data.parentNodeId === nodeId); - - const childNodesChange: NodePositionChange[] = childNodes.map((node) => { - return { - type: 'position', - id: node.id, - position: { - x: node.position.x, - y: node.position.y + diffHeight - } - }; + onChangeNode({ + nodeId, + type: 'replaceInput', + key: NodeInputKeyEnum.loopNodeInputHeight, + value: { + ...loopNodeInputHeight, + value: size.height + } }); - onNodesChange(childNodesChange); + setTimeout(() => { + resetParentNodeSizeAndPosition(nodeId); + }, 50); }, [size?.height]); const Render = useMemo(() => { diff --git a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx index ec8d0a503..25ad6c2ea 100644 --- a/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx +++ b/projects/app/src/pages/app/detail/components/WorkflowComponents/context/index.tsx @@ -430,9 +430,13 @@ const WorkflowContextProvider = ({ item.key === props.key ? props.value : item ); } else if (type === 'replaceInput') { - updateObj.inputs = updateObj.inputs.map((item) => - item.key === props.key ? props.value : item - ); + if (!updateObj.inputs.find((item) => item.key === props.key)) { + updateObj.inputs.push(props.value); + } else { + updateObj.inputs = updateObj.inputs.map((item) => + item.key === props.key ? props.value : item + ); + } } else if (type === 'addInput') { const input = node.data.inputs.find((input) => input.key === props.value.key); if (input) {