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:
Archer
2024-11-11 14:35:17 +08:00
committed by archer
parent acdf6a0dd9
commit d758ddb47c
12 changed files with 106 additions and 83 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 416 KiB

View File

@@ -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<string>`类型的输入,对应的是文件输入的 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}
</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版本起,关于文件上传的更新
![打开文件上传](/imgs/fileinpu-7.jpg)
由于与 4.8.9 版本有些差异,尽管我们做了向下兼容,避免工作流立即不可用。但是请尽快的按新版本规则进行调整工作流,后续将会去除兼容性代码。
## 文件输入后续更新
* 插件支持配置文件输入
* 子应用和插件调用,支持传递文件输入
* 文档解析,结构化解析结果
* 更多的文件类型输入以及解析器。
1. 简易模式中,将会强制进行文件解析,不再由模型决策是否解析,保证每次都能参考文档。
2. 文档解析:不再解析历史记录中的文件。
3. 工具调用:支持直接选择文档引用,不需要再挂载文档解析工具。会自动解析历史记录中的文件
4. AI 对话:支持直接选择文档引用,不需要进过文档解析节点。会自动解析历史记录中的文件
5. 插件单独运行:不再支持全局文件;插件输入支持配置文件类型,可以取代全局文件上传
6. **工作流调用插件:不再自动传递工作流上传的文件到插件,需要手动给插件输入指定变量。**
7. **工作流调用工作流:不再自动传递工作流上传的文件到子工作流,可以手动选择需要传递的文件链接。**

View File

@@ -199,6 +199,7 @@ export enum NodeInputKeyEnum {
childrenNodeIdList = 'childrenNodeIdList',
nodeWidth = 'nodeWidth',
nodeHeight = 'nodeHeight',
loopNodeInputHeight = 'loopNodeInputHeight',
// loop start
loopStartInput = 'loopStartInput',
loopStartIndex = 'loopStartIndex',

View File

@@ -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,

View File

@@ -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: [
{

View File

@@ -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) => {

View File

@@ -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(() => {

View File

@@ -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) {