4.8.13 perf (#3112)
* fix: share page load title error * update file input doc * perf: auto add file urls * perf: auto ser loop node offset height
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 151 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 171 KiB |
BIN
docSite/assets/imgs/image-5.png
Normal file
BIN
docSite/assets/imgs/image-5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 372 KiB |
BIN
docSite/assets/imgs/image-6.png
Normal file
BIN
docSite/assets/imgs/image-6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 471 KiB |
BIN
docSite/assets/imgs/image-7.png
Normal file
BIN
docSite/assets/imgs/image-7.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 416 KiB |
@@ -20,10 +20,12 @@ weight: 110
|
||||
|
||||
随后,你的调试对话框中,就会出现一个文件选择的 icon,可以点击文件选择 icon,选择你需要上传的文件。
|
||||
|
||||
由于采用的是工具调用模式,所以在提问时候,可能需要加上适当的引导,让模型知道,你需要读取`文档`。
|
||||
|
||||

|
||||
|
||||
**工作模式**
|
||||
|
||||
从 4.8.13 版本起,简易模式的文件读取将会强制解析文件并放入 system 提示词中,避免连续对话时,模型有时候不会主动调用读取文件的工具。
|
||||
|
||||
## 工作流中使用
|
||||
|
||||
工作流中,可以在系统配置中,找到`文件输入`配置项,点击其右侧的`开启`/`关闭`按键,即可打开配置弹窗。
|
||||
@@ -32,14 +34,13 @@ weight: 110
|
||||
|
||||
在工作流中,使用文件的方式很多,最简单的就是类似下图中,直接通过工具调用接入文档解析,实现和简易模式一样的效果。
|
||||
|
||||

|
||||
| | |
|
||||
| --------------------- | --------------------- |
|
||||
|  |  |
|
||||
|
||||
也可以更简单点,强制每轮对话都携带上文档内容进行回答,这样就不需要调用两次 AI 才能读取文档内容了。
|
||||
|
||||

|
||||
|
||||
当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。不过目前版本,`插件`中并未支持文件处理,所以在构建 SOP 的话可能还是有一些麻烦。
|
||||
当然,你也可以在工作流中,对文档进行内容提取、内容分析等,然后将分析的结果传递给 HTTP 或者其他模块,从而实现文件处理的 SOP。
|
||||
|
||||

|
||||
|
||||
## 文档解析工作原理
|
||||
|
||||
@@ -73,23 +74,8 @@ type UserChatItemValueItemType = {
|
||||
|
||||
文档解析依赖文档解析节点,这个节点会接收一个`array<string>`类型的输入,对应的是文件输入的 URL;输出的是一个`string`,对应的是文档解析后的内容。
|
||||
|
||||

|
||||
|
||||
* 在文档解析节点中,只会解析`文档`类型的 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}
|
||||
</Content>
|
||||
```
|
||||
|
||||
### 工具调用如何使用文档解析
|
||||
### AI节点中如何使用文档解析
|
||||
|
||||
在工具调用中,文档解析节点的调用提示词为:`解析对话中所有上传的文档,并返回对应文档内容`。
|
||||
在 AI 节点(AI对话/工具调用)中,新增了一个文档链接的输入,可以直接引用文档的地址,从而实现文档内容的引用。
|
||||
|
||||
作为工具被执行后,文档解析节点会返回解析后的文档内容作为工具响应。
|
||||
|
||||
### AI对话中如何使用文档解析
|
||||
|
||||
在 AI 对话节点中,新增了一个文档引用的输入,可以直接引用文档解析节点的输出,从而实现文档内容的引用。
|
||||
|
||||
它接收一个`string`类型的输入,除了可以引用文档解析结果外,还可以实现自定义内容引用,最终会进行提示词拼接,放置在 role=system 的消息中。提示词模板如下:
|
||||
它接收一个`Array<string>`类型的输入,最终这些 url 会被解析,并进行提示词拼接,放置在 role=system 的消息中。提示词模板如下:
|
||||
|
||||
```
|
||||
将 <Quote></Quote> 中的内容作为本次对话的参考:
|
||||
<Quote>
|
||||
将 <FilesContent></FilesContent> 中的内容作为本次对话的参考:
|
||||
<FilesContent>
|
||||
{{quote}}
|
||||
</Quote>
|
||||
</FilesContent>
|
||||
```
|
||||
|
||||
quote 为引用的内容。
|
||||
# 4.8.13版本起,关于文件上传的更新
|
||||
|
||||

|
||||
由于与 4.8.9 版本有些差异,尽管我们做了向下兼容,避免工作流立即不可用。但是请尽快的按新版本规则进行调整工作流,后续将会去除兼容性代码。
|
||||
|
||||
## 文件输入后续更新
|
||||
|
||||
* 插件支持配置文件输入。
|
||||
* 子应用和插件调用,支持传递文件输入。
|
||||
* 文档解析,结构化解析结果。
|
||||
* 更多的文件类型输入以及解析器。
|
||||
1. 简易模式中,将会强制进行文件解析,不再由模型决策是否解析,保证每次都能参考文档。
|
||||
2. 文档解析:不再解析历史记录中的文件。
|
||||
3. 工具调用:支持直接选择文档引用,不需要再挂载文档解析工具。会自动解析历史记录中的文件。
|
||||
4. AI 对话:支持直接选择文档引用,不需要进过文档解析节点。会自动解析历史记录中的文件。
|
||||
5. 插件单独运行:不再支持全局文件;插件输入支持配置文件类型,可以取代全局文件上传。
|
||||
6. **工作流调用插件:不再自动传递工作流上传的文件到插件,需要手动给插件输入指定变量。**
|
||||
7. **工作流调用工作流:不再自动传递工作流上传的文件到子工作流,可以手动选择需要传递的文件链接。**
|
||||
@@ -199,6 +199,7 @@ export enum NodeInputKeyEnum {
|
||||
childrenNodeIdList = 'childrenNodeIdList',
|
||||
nodeWidth = 'nodeWidth',
|
||||
nodeHeight = 'nodeHeight',
|
||||
loopNodeInputHeight = 'loopNodeInputHeight',
|
||||
// loop start
|
||||
loopStartInput = 'loopStartInput',
|
||||
loopStartIndex = 'loopStartIndex',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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<FlowNodeItemType> | 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) => {
|
||||
|
||||
@@ -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<FlowNodeItemType>) => {
|
||||
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<FlowNodeItemType>) => {
|
||||
})(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<FlowNodeItemType>) => {
|
||||
nodeList.filter((node) => node.parentNodeId === nodeId).map((node) => node.nodeId)
|
||||
);
|
||||
}, [nodeId, nodeList]);
|
||||
|
||||
useEffect(() => {
|
||||
onChangeNode({
|
||||
nodeId,
|
||||
@@ -117,35 +123,25 @@ const NodeLoop = ({ data, selected }: NodeProps<FlowNodeItemType>) => {
|
||||
resetParentNodeSizeAndPosition(nodeId);
|
||||
}, [childrenNodeIdList]);
|
||||
|
||||
// Update child node position
|
||||
// Update loop node offset value
|
||||
const inputBoxRef = useRef<HTMLDivElement>(null);
|
||||
const size = useSize(inputBoxRef);
|
||||
const prevHeightRef = useRef<number>(); // 添加 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(() => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user