mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-20 12:56:55 +08:00
### What problem does this PR solve? Feat: Limit the number of Splitter and Parser operators on the canvas to only one #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
@ -1687,15 +1687,20 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
},
|
||||
dataflow: {
|
||||
parser: 'Parser',
|
||||
parserDescription: 'Parser',
|
||||
chunker: 'Chunker',
|
||||
chunkerDescription: 'Chunker',
|
||||
parserDescription:
|
||||
'Extracts raw text and structure from files for downstream processing.',
|
||||
tokenizer: 'Tokenizer',
|
||||
tokenizerDescription: 'Tokenizer',
|
||||
splitter: 'Splitter',
|
||||
splitterDescription: 'Splitter',
|
||||
hierarchicalMergerDescription: 'Hierarchical merger',
|
||||
hierarchicalMerger: 'Hierarchical merger',
|
||||
tokenizerDescription:
|
||||
'Transforms text into the required data structure (e.g., vector embeddings for Embedding Search) depending on the chosen search method.',
|
||||
splitter: 'Token Splitter',
|
||||
splitterDescription:
|
||||
'Split text into chunks by token length with optional delimiters and overlap.',
|
||||
hierarchicalMergerDescription:
|
||||
'Split documents into sections by title hierarchy with regex rules for finer control.',
|
||||
hierarchicalMerger: 'Title Splitter',
|
||||
extractor: 'Context Generator',
|
||||
extractorDescription:
|
||||
'Use an LLM to extract structured insights from document chunks—such as summaries, classifications, etc.',
|
||||
outputFormat: 'Output format',
|
||||
lang: 'Language',
|
||||
fileFormats: 'File formats',
|
||||
@ -1713,8 +1718,6 @@ This delimiter is used to split the input text into several text pieces echo of
|
||||
exportJson: 'Export JSON',
|
||||
viewResult: 'View Result',
|
||||
running: 'Running',
|
||||
extractor: 'Extractor',
|
||||
extractorDescription: 'Extractor',
|
||||
summary: 'Augmented Context',
|
||||
keywords: 'Keywords',
|
||||
questions: 'Questions',
|
||||
|
||||
@ -1605,15 +1605,19 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
},
|
||||
dataflow: {
|
||||
parser: '解析器',
|
||||
parserDescription: '解析器',
|
||||
chunker: '分块器',
|
||||
chunkerDescription: '分块器',
|
||||
parserDescription: '从文件中提取原始文本和结构以供下游处理。',
|
||||
tokenizer: '分词器',
|
||||
tokenizerDescription: '分词器',
|
||||
splitter: '拆分器',
|
||||
splitterDescription: '拆分器',
|
||||
hierarchicalMergerDesription: '分层合并',
|
||||
hierarchicalMerger: '分层合并',
|
||||
tokenizerDescription:
|
||||
'根据所选的搜索方法,将文本转换为所需的数据结构(例如,用于嵌入搜索的向量嵌入)。',
|
||||
splitter: '分词器拆分器',
|
||||
splitterDescription:
|
||||
'根据分词器长度将文本拆分成块,并带有可选的分隔符和重叠。',
|
||||
hierarchicalMergerDescription:
|
||||
'使用正则表达式规则按标题层次结构将文档拆分成多个部分,以实现更精细的控制。',
|
||||
hierarchicalMerger: '标题拆分器',
|
||||
extractor: '提取器',
|
||||
extractorDescription:
|
||||
'使用 LLM 从文档块(例如摘要、分类等)中提取结构化见解。',
|
||||
outputFormat: '输出格式',
|
||||
lang: '语言',
|
||||
fileFormats: '文件格式',
|
||||
@ -1632,8 +1636,6 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
exportJson: '导出 JSON',
|
||||
viewResult: '查看结果',
|
||||
running: '运行中',
|
||||
extractor: '提取器',
|
||||
extractorDescription: '提取器',
|
||||
summary: '增强上下文',
|
||||
keywords: '关键词',
|
||||
questions: '问题',
|
||||
@ -1687,6 +1689,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于
|
||||
},
|
||||
},
|
||||
cancel: '取消',
|
||||
filenameEmbeddingWeight: '文件名嵌入权重',
|
||||
switchPromptMessage: '提示词将发生变化,请确认是否放弃已有提示词?',
|
||||
},
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
ControlButton,
|
||||
Controls,
|
||||
NodeTypes,
|
||||
OnConnectEnd,
|
||||
Position,
|
||||
ReactFlow,
|
||||
ReactFlowInstance,
|
||||
@ -36,12 +37,13 @@ import {
|
||||
useShowDrawer,
|
||||
} from '../hooks/use-show-drawer';
|
||||
import RunSheet from '../run-sheet';
|
||||
import useGraphStore from '../store';
|
||||
import { ButtonEdge } from './edge';
|
||||
import styles from './index.less';
|
||||
import { RagNode } from './node';
|
||||
import { BeginNode } from './node/begin-node';
|
||||
import { ContextNode } from './node/context-node';
|
||||
import { InnerNextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||
import { NextStepDropdown } from './node/dropdown/next-step-dropdown';
|
||||
import { HierarchicalMergerNode } from './node/hierarchical-merger-node';
|
||||
import NoteNode from './node/note-node';
|
||||
import ParserNode from './node/parser-node';
|
||||
@ -116,6 +118,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
useHideFormSheetOnNodeDeletion({ hideFormDrawer });
|
||||
|
||||
const { visible, hideModal, showModal } = useSetModalState();
|
||||
|
||||
const [dropdownPosition, setDropdownPosition] = useState({ x: 0, y: 0 });
|
||||
|
||||
const isConnectedRef = useRef(false);
|
||||
@ -128,6 +131,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
|
||||
const { setActiveDropdown, clearActiveDropdown } = useDropdownManager();
|
||||
|
||||
const { hasChildNode } = useGraphStore((state) => state);
|
||||
|
||||
const onPaneClick = useCallback(() => {
|
||||
hideFormDrawer();
|
||||
if (visible && !preventCloseRef.current) {
|
||||
@ -159,7 +164,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
isConnectedRef.current = true;
|
||||
};
|
||||
|
||||
const OnConnectStart = (event: any, params: any) => {
|
||||
const onConnectStart = (event: any, params: any) => {
|
||||
isConnectedRef.current = false;
|
||||
|
||||
if (params && params.nodeId && params.handleId) {
|
||||
@ -172,7 +177,12 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const OnConnectEnd = (event: MouseEvent | TouchEvent) => {
|
||||
const onConnectEnd: OnConnectEnd = (event, connectionState) => {
|
||||
const nodeId = connectionState.fromNode?.id;
|
||||
// Events triggered by Handle are directly interrupted
|
||||
if (connectionState.toNode !== null || (nodeId && hasChildNode(nodeId))) {
|
||||
return;
|
||||
}
|
||||
if ('clientX' in event && 'clientY' in event) {
|
||||
const { clientX, clientY } = event;
|
||||
setDropdownPosition({ x: clientX, y: clientY });
|
||||
@ -220,8 +230,8 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
onConnect={onConnect}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
onConnectStart={OnConnectStart}
|
||||
onConnectEnd={OnConnectEnd}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
onInit={setReactFlowInstance}
|
||||
@ -268,7 +278,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
isFromConnectionDrag: true,
|
||||
}}
|
||||
>
|
||||
<InnerNextStepDropdown
|
||||
<NextStepDropdown
|
||||
hideModal={() => {
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
@ -276,7 +286,7 @@ function DataFlowCanvas({ drawerVisible, hideDrawer, showLogSheet }: IProps) {
|
||||
position={dropdownPosition}
|
||||
>
|
||||
<span></span>
|
||||
</InnerNextStepDropdown>
|
||||
</NextStepDropdown>
|
||||
</HandleContext.Provider>
|
||||
)}
|
||||
</AgentInstanceContext.Provider>
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
} from '@/components/ui/tooltip';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { useGetNodeDescription, useGetNodeName } from '@/pages/data-flow/hooks';
|
||||
import useGraphStore from '@/pages/data-flow/store';
|
||||
import { Position } from '@xyflow/react';
|
||||
import { t } from 'i18next';
|
||||
import {
|
||||
@ -20,6 +21,7 @@ import {
|
||||
memo,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { Operator } from '../../../constant';
|
||||
@ -110,6 +112,26 @@ function OperatorItemList({
|
||||
return <ul className="space-y-2">{operators.map(renderOperatorItem)}</ul>;
|
||||
}
|
||||
|
||||
const singleOperators = [
|
||||
Operator.Tokenizer,
|
||||
Operator.Splitter,
|
||||
Operator.HierarchicalMerger,
|
||||
Operator.Parser,
|
||||
];
|
||||
// Limit the number of operators of a certain type on the canvas to only one
|
||||
function useRestrictSingleOperatorOnCanvas() {
|
||||
const list: Operator[] = [];
|
||||
const { findNodeByName } = useGraphStore((state) => state);
|
||||
|
||||
singleOperators.forEach((operator) => {
|
||||
if (!findNodeByName(operator)) {
|
||||
list.push(operator);
|
||||
}
|
||||
});
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
function AccordionOperators({
|
||||
isCustomDropdown = false,
|
||||
mousePosition,
|
||||
@ -117,15 +139,16 @@ function AccordionOperators({
|
||||
isCustomDropdown?: boolean;
|
||||
mousePosition?: { x: number; y: number };
|
||||
}) {
|
||||
const singleOperators = useRestrictSingleOperatorOnCanvas();
|
||||
const operators = useMemo(() => {
|
||||
const list = [...singleOperators];
|
||||
list.push(Operator.Extractor);
|
||||
return list;
|
||||
}, [singleOperators]);
|
||||
|
||||
return (
|
||||
<OperatorItemList
|
||||
operators={[
|
||||
Operator.Parser,
|
||||
Operator.Tokenizer,
|
||||
Operator.Splitter,
|
||||
Operator.HierarchicalMerger,
|
||||
Operator.Extractor,
|
||||
]}
|
||||
operators={operators}
|
||||
isCustomDropdown={isCustomDropdown}
|
||||
mousePosition={mousePosition}
|
||||
></OperatorItemList>
|
||||
|
||||
@ -4,8 +4,9 @@ import { Handle, HandleProps } from '@xyflow/react';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { HandleContext } from '../../context';
|
||||
import useGraphStore from '../../store';
|
||||
import { useDropdownManager } from '../context';
|
||||
import { InnerNextStepDropdown } from './dropdown/next-step-dropdown';
|
||||
import { NextStepDropdown } from './dropdown/next-step-dropdown';
|
||||
|
||||
export function CommonHandle({
|
||||
className,
|
||||
@ -17,6 +18,8 @@ export function CommonHandle({
|
||||
const { canShowDropdown, setActiveDropdown, clearActiveDropdown } =
|
||||
useDropdownManager();
|
||||
|
||||
const { hasChildNode } = useGraphStore((state) => state);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
nodeId,
|
||||
@ -39,6 +42,10 @@ export function CommonHandle({
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (hasChildNode(nodeId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!canShowDropdown()) {
|
||||
return;
|
||||
}
|
||||
@ -49,14 +56,14 @@ export function CommonHandle({
|
||||
>
|
||||
<Plus className="size-3 pointer-events-none text-text-title-invert" />
|
||||
{visible && (
|
||||
<InnerNextStepDropdown
|
||||
<NextStepDropdown
|
||||
hideModal={() => {
|
||||
hideModal();
|
||||
clearActiveDropdown();
|
||||
}}
|
||||
>
|
||||
<span></span>
|
||||
</InnerNextStepDropdown>
|
||||
</NextStepDropdown>
|
||||
)}
|
||||
</Handle>
|
||||
</HandleContext.Provider>
|
||||
|
||||
@ -4,7 +4,7 @@ import { memo } from 'react';
|
||||
import { NodeHandleId } from '../../constant';
|
||||
import { needsSingleStepDebugging } from '../../utils';
|
||||
import { CommonHandle } from './handle';
|
||||
import { LeftHandleStyle, RightHandleStyle } from './handle-icon';
|
||||
import { LeftHandleStyle } from './handle-icon';
|
||||
import NodeHeader from './node-header';
|
||||
import { NodeWrapper } from './node-wrapper';
|
||||
import { ToolBar } from './toolbar';
|
||||
@ -31,15 +31,6 @@ function TokenizerNode({
|
||||
style={LeftHandleStyle}
|
||||
nodeId={id}
|
||||
></CommonHandle>
|
||||
<CommonHandle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
isConnectable={isConnectable}
|
||||
id={NodeHandleId.Start}
|
||||
style={RightHandleStyle}
|
||||
nodeId={id}
|
||||
isConnectableEnd={false}
|
||||
></CommonHandle>
|
||||
<NodeHeader id={id} name={data.name} label={data.label}></NodeHeader>
|
||||
</NodeWrapper>
|
||||
</ToolBar>
|
||||
|
||||
@ -14,7 +14,6 @@ import {
|
||||
applyEdgeChanges,
|
||||
applyNodeChanges,
|
||||
} from '@xyflow/react';
|
||||
import { omit } from 'lodash';
|
||||
import differenceWith from 'lodash/differenceWith';
|
||||
import intersectionWith from 'lodash/intersectionWith';
|
||||
import lodashSet from 'lodash/set';
|
||||
@ -59,7 +58,6 @@ export type RFState = {
|
||||
updateNode: (node: RAGFlowNodeType) => void;
|
||||
addEdge: (connection: Connection) => void;
|
||||
getEdge: (id: string) => Edge | undefined;
|
||||
updateFormDataOnConnect: (connection: Connection) => void;
|
||||
updateSwitchFormData: (
|
||||
source: string,
|
||||
sourceHandle?: string | null,
|
||||
@ -67,7 +65,6 @@ export type RFState = {
|
||||
isConnecting?: boolean,
|
||||
) => void;
|
||||
duplicateNode: (id: string, name: string) => void;
|
||||
duplicateIterationNode: (id: string, name: string) => void;
|
||||
deleteEdge: () => void;
|
||||
deleteEdgeById: (id: string) => void;
|
||||
deleteNodeById: (id: string) => void;
|
||||
@ -89,6 +86,7 @@ export type RFState = {
|
||||
) => void; // Deleting a condition of a classification operator will delete the related edge
|
||||
findAgentToolNodeById: (id: string | null) => string | undefined;
|
||||
selectNodeIds: (nodeIds: string[]) => void;
|
||||
hasChildNode: (nodeId: string) => boolean;
|
||||
};
|
||||
|
||||
// this is our useStore hook that we can use in our components to get parts of the store and call actions
|
||||
@ -126,11 +124,9 @@ const useGraphStore = create<RFState>()(
|
||||
setEdges(mapEdgeMouseEvent(edges, edgeId, false));
|
||||
},
|
||||
onConnect: (connection: Connection) => {
|
||||
const { updateFormDataOnConnect } = get();
|
||||
set({
|
||||
edges: addEdge(connection, get().edges),
|
||||
});
|
||||
updateFormDataOnConnect(connection);
|
||||
},
|
||||
onSelectionChange: ({ nodes, edges }: OnSelectionChangeParams) => {
|
||||
set({
|
||||
@ -217,37 +213,14 @@ const useGraphStore = create<RFState>()(
|
||||
set({
|
||||
edges: addEdge(connection, get().edges),
|
||||
});
|
||||
// TODO: This may not be reasonable. You need to choose between listening to changes in the form.
|
||||
get().updateFormDataOnConnect(connection);
|
||||
},
|
||||
getEdge: (id: string) => {
|
||||
return get().edges.find((x) => x.id === id);
|
||||
},
|
||||
updateFormDataOnConnect: (connection: Connection) => {
|
||||
const { getOperatorTypeFromId, updateSwitchFormData } = get();
|
||||
const { source, target, sourceHandle } = connection;
|
||||
const operatorType = getOperatorTypeFromId(source);
|
||||
if (source) {
|
||||
switch (operatorType) {
|
||||
case Operator.Switch: {
|
||||
updateSwitchFormData(source, sourceHandle, target, true);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
duplicateNode: (id: string, name: string) => {
|
||||
const { getNode, addNode, generateNodeName, duplicateIterationNode } =
|
||||
get();
|
||||
const { getNode, addNode, generateNodeName } = get();
|
||||
const node = getNode(id);
|
||||
|
||||
if (node?.data.label === Operator.Iteration) {
|
||||
duplicateIterationNode(id, name);
|
||||
return;
|
||||
}
|
||||
|
||||
addNode({
|
||||
...(node || {}),
|
||||
data: {
|
||||
@ -257,35 +230,6 @@ const useGraphStore = create<RFState>()(
|
||||
...generateDuplicateNode(node?.position, node?.data?.label),
|
||||
});
|
||||
},
|
||||
duplicateIterationNode: (id: string, name: string) => {
|
||||
const { getNode, generateNodeName, nodes } = get();
|
||||
const node = getNode(id);
|
||||
|
||||
const iterationNode: RAGFlowNodeType = {
|
||||
...(node || {}),
|
||||
data: {
|
||||
...(node?.data || { label: Operator.Iteration, form: {} }),
|
||||
name: generateNodeName(name),
|
||||
},
|
||||
...generateDuplicateNode(node?.position, node?.data?.label),
|
||||
};
|
||||
|
||||
const children = nodes
|
||||
.filter((x) => x.parentId === node?.id)
|
||||
.map((x) => ({
|
||||
...(x || {}),
|
||||
data: {
|
||||
...duplicateNodeForm(x?.data),
|
||||
name: generateNodeName(x.data.name),
|
||||
},
|
||||
...omit(generateDuplicateNode(x?.position, x?.data?.label), [
|
||||
'position',
|
||||
]),
|
||||
parentId: iterationNode.id,
|
||||
}));
|
||||
|
||||
set({ nodes: nodes.concat(iterationNode, ...children) });
|
||||
},
|
||||
deleteEdge: () => {
|
||||
const { edges, selectedEdgeIds } = get();
|
||||
set({
|
||||
@ -295,55 +239,15 @@ const useGraphStore = create<RFState>()(
|
||||
});
|
||||
},
|
||||
deleteEdgeById: (id: string) => {
|
||||
const {
|
||||
edges,
|
||||
updateNodeForm,
|
||||
getOperatorTypeFromId,
|
||||
updateSwitchFormData,
|
||||
} = get();
|
||||
const currentEdge = edges.find((x) => x.id === id);
|
||||
const { edges } = get();
|
||||
|
||||
if (currentEdge) {
|
||||
const { source, sourceHandle, target } = currentEdge;
|
||||
const operatorType = getOperatorTypeFromId(source);
|
||||
// After deleting the edge, set the corresponding field in the node's form field to undefined
|
||||
switch (operatorType) {
|
||||
case Operator.Relevant:
|
||||
updateNodeForm(source, {
|
||||
[sourceHandle as string]: undefined,
|
||||
});
|
||||
break;
|
||||
// case Operator.Categorize:
|
||||
// if (sourceHandle)
|
||||
// updateNodeForm(source, undefined, [
|
||||
// 'category_description',
|
||||
// sourceHandle,
|
||||
// 'to',
|
||||
// ]);
|
||||
// break;
|
||||
case Operator.Switch: {
|
||||
updateSwitchFormData(source, sourceHandle, target, false);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
set({
|
||||
edges: edges.filter((edge) => edge.id !== id),
|
||||
});
|
||||
},
|
||||
deleteNodeById: (id: string) => {
|
||||
const {
|
||||
nodes,
|
||||
edges,
|
||||
getOperatorTypeFromId,
|
||||
deleteAgentDownstreamNodesById,
|
||||
} = get();
|
||||
if (getOperatorTypeFromId(id) === Operator.Agent) {
|
||||
deleteAgentDownstreamNodesById(id);
|
||||
return;
|
||||
}
|
||||
const { nodes, edges } = get();
|
||||
|
||||
set({
|
||||
nodes: nodes.filter((node) => node.id !== id),
|
||||
edges: edges
|
||||
@ -526,6 +430,10 @@ const useGraphStore = create<RFState>()(
|
||||
})),
|
||||
);
|
||||
},
|
||||
hasChildNode: (nodeId) => {
|
||||
const { edges } = get();
|
||||
return edges.some((edge) => edge.source === nodeId);
|
||||
},
|
||||
})),
|
||||
{ name: 'graph', trace: true },
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user