diff --git a/web/src/components/bool-segmented.tsx b/web/src/components/bool-segmented.tsx new file mode 100644 index 000000000..3d84e38a7 --- /dev/null +++ b/web/src/components/bool-segmented.tsx @@ -0,0 +1,18 @@ +import { omit } from 'lodash'; +import { Segmented, SegmentedProps } from './ui/segmented'; + +export function BoolSegmented({ ...props }: Omit) { + return ( + + ); +} diff --git a/web/src/components/logical-operator.tsx b/web/src/components/logical-operator.tsx new file mode 100644 index 000000000..7b37a2567 --- /dev/null +++ b/web/src/components/logical-operator.tsx @@ -0,0 +1,24 @@ +import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options'; +import { RAGFlowFormItem } from './ragflow-form'; +import { RAGFlowSelect } from './ui/select'; + +type LogicalOperatorProps = { name: string }; + +export function LogicalOperator({ name }: LogicalOperatorProps) { + const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions(); + + return ( +
+ + + +
+
+ ); +} diff --git a/web/src/components/metadata-filter/metadata-filter-conditions.tsx b/web/src/components/metadata-filter/metadata-filter-conditions.tsx index aee103a1f..599a6ed80 100644 --- a/web/src/components/metadata-filter/metadata-filter-conditions.tsx +++ b/web/src/components/metadata-filter/metadata-filter-conditions.tsx @@ -17,15 +17,13 @@ import { Input } from '@/components/ui/input'; import { Separator } from '@/components/ui/separator'; import { SwitchLogicOperator, SwitchOperatorOptions } from '@/constants/agent'; import { useBuildSwitchOperatorOptions } from '@/hooks/logic-hooks/use-build-operator-options'; -import { useBuildSwitchLogicOperatorOptions } from '@/hooks/logic-hooks/use-build-options'; import { useFetchKnowledgeMetadata } from '@/hooks/use-knowledge-request'; import { PromptEditor } from '@/pages/agent/form/components/prompt-editor'; import { Plus, X } from 'lucide-react'; import { useCallback } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { RAGFlowFormItem } from '../ragflow-form'; -import { RAGFlowSelect } from '../ui/select'; +import { LogicalOperator } from '../logical-operator'; export function MetadataFilterConditions({ kbIds, @@ -44,8 +42,6 @@ export function MetadataFilterConditions({ const switchOperatorOptions = useBuildSwitchOperatorOptions(); - const switchLogicOperatorOptions = useBuildSwitchLogicOperatorOptions(); - const { fields, remove, append } = useFieldArray({ name, control: form.control, @@ -53,14 +49,16 @@ export function MetadataFilterConditions({ const add = useCallback( (key: string) => () => { - form.setValue(logic, SwitchLogicOperator.And); + if (fields.length === 1) { + form.setValue(logic, SwitchLogicOperator.And); + } append({ key, value: '', op: SwitchOperatorOptions[0].value, }); }, - [append, form, logic], + [append, fields.length, form, logic], ); return ( @@ -85,20 +83,7 @@ export function MetadataFilterConditions({
- {fields.length > 1 && ( -
- - - -
-
- )} + {fields.length > 1 && }
{fields.map((field, index) => { const typeField = `${name}.${index}.key`; diff --git a/web/src/constants/agent.tsx b/web/src/constants/agent.tsx index e1c1574d5..ee10e3a29 100644 --- a/web/src/constants/agent.tsx +++ b/web/src/constants/agent.tsx @@ -75,7 +75,6 @@ export enum Operator { Message = 'Message', Relevant = 'Relevant', RewriteQuestion = 'RewriteQuestion', - KeywordExtract = 'KeywordExtract', DuckDuckGo = 'DuckDuckGo', Wikipedia = 'Wikipedia', PubMed = 'PubMed', @@ -84,14 +83,10 @@ export enum Operator { Bing = 'Bing', GoogleScholar = 'GoogleScholar', GitHub = 'GitHub', - QWeather = 'QWeather', ExeSQL = 'ExeSQL', Switch = 'Switch', WenCai = 'WenCai', - AkShare = 'AkShare', YahooFinance = 'YahooFinance', - Jin10 = 'Jin10', - TuShare = 'TuShare', Note = 'Note', Crawler = 'Crawler', Invoke = 'Invoke', @@ -118,6 +113,9 @@ export enum Operator { Splitter = 'Splitter', HierarchicalMerger = 'HierarchicalMerger', Extractor = 'Extractor', + Loop = 'Loop', + LoopStart = 'LoopItem', + ExitLoop = 'ExitLoop', } export enum ComparisonOperator { diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index 225d8c127..59f2bf73c 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1170,8 +1170,13 @@ Example: Virtual Hosted Style`, addField: 'Add option', addMessage: 'Add message', loop: 'Loop', - loopTip: + loopDescription: 'Loop is the upper limit of the number of loops of the current component, when the number of loops exceeds the value of loop, it means that the component can not complete the current task, please re-optimize agent', + exitLoop: 'Exit loop', + exitLoopDescription: `Equivalent to "break". This node has no configuration items. When the loop body reaches this node, the loop terminates.`, + loopVariables: 'Loop Variables', + maximumLoopCount: 'Maximum loop count', + loopTerminationCondition: 'Loop termination condition', yes: 'Yes', no: 'No', key: 'Key', @@ -1655,9 +1660,8 @@ This delimiter is used to split the input text into several text pieces echo of variableAssignerDescription: 'This component performs operations on Data objects, including extracting, filtering, and editing keys and values in the Data.', variableAggregator: 'Variable aggregator', - variableAggregatorDescription: `This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes. - -The variable aggregation node (originally the variable assignment node) is a crucial node in the workflow. It is responsible for integrating the output results of different branches, ensuring that regardless of which branch is executed, its result can be referenced and accessed through a unified variable. This is extremely useful in multi-branch scenarios, as it maps variables with the same function across different branches to a single output variable, avoiding redundant definitions in downstream nodes.`, + variableAggregatorDescription: ` +This process aggregates variables from multiple branches into a single variable to achieve unified configuration for downstream nodes.`, inputVariables: 'Input variables', runningHintText: 'is running...🕞', openingSwitch: 'Opening switch', @@ -1886,10 +1890,10 @@ Important structured information may include: names, dates, locations, events, k overwrite: 'Overwritten By', clear: 'Clear', set: 'Set', - '+=': 'Add', - '-=': 'Subtract', - '*=': 'Multiply', - '/=': 'Divide', + add: 'Add', + subtract: 'Subtract', + multiply: 'Multiply', + divide: 'Divide', append: 'Append', extend: 'Extend', removeFirst: 'Remove first', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index e5b48bf56..4156e5b34 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -1102,9 +1102,14 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 messageMsg: '请输入消息或删除此字段。', addField: '新增字段', addMessage: '新增消息', - loop: '循环上限', - loopTip: + loop: '循环', + loopDescription: 'loop为当前组件循环次数上限,当循环次数超过loop的值时,说明组件不能完成当前任务,请重新优化agent', + exitLoop: '退出循环', + exitLoopDescription: `等同于 "break"。此节点没有配置项。当循环体到达此节点时,循环终止。`, + loopVariables: '循环变量', + maximumLoopCount: '最大循环次数', + loopTerminationCondition: '循环终止条件', yes: '是', no: '否', key: '键', @@ -1499,7 +1504,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 contentTip: 'content: 邮件内容(可选)', jsonUploadTypeErrorMessage: '请上传json文件', jsonUploadContentErrorMessage: 'json 文件错误', - iteration: '循环', + iteration: '迭代', iterationDescription: `该组件负责迭代生成新的内容,对列表对象执行多次步骤直至输出所有结果。`, delimiterTip: `该分隔符用于将输入文本分割成几个文本片段,每个文本片段的回显将作为每次迭代的输入项。`, delimiterOptions: { @@ -1545,8 +1550,7 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 variableAssignerDescription: '此组件对数据对象执行操作,包括提取、筛选和编辑数据中的键和值。', variableAggregator: '变量聚合', - variableAggregatorDescription: `将多路分支的变量聚合为一个变量,以实现下游节点统一配置。 -变量聚合节点(原变量赋值节点)是工作流程中的一个关键节点,它负责整合不同分支的输出结果,确保无论哪个分支被执行,其结果都能通过一个统一的变量来引用和访问。这在多分支的情况下非常有用,可将不同分支下相同作用的变量映射为一个输出变量,避免下游节点重复定义。`, + variableAggregatorDescription: `该过程将来自多个分支的变量聚合到一个变量中,以实现下游节点的统一配置。`, inputVariables: '输入变量', addVariable: '新增变量', runningHintText: '正在运行中...🕞', diff --git a/web/src/pages/agent/canvas/index.tsx b/web/src/pages/agent/canvas/index.tsx index f2fc983e2..6c088809d 100644 --- a/web/src/pages/agent/canvas/index.tsx +++ b/web/src/pages/agent/canvas/index.tsx @@ -56,12 +56,14 @@ import { BeginNode } from './node/begin-node'; import { CategorizeNode } from './node/categorize-node'; import { DataOperationsNode } from './node/data-operations-node'; import { NextStepDropdown } from './node/dropdown/next-step-dropdown'; +import { ExitLoopNode } from './node/exit-loop-node'; import { ExtractorNode } from './node/extractor-node'; import { FileNode } from './node/file-node'; import { InvokeNode } from './node/invoke-node'; import { IterationNode, IterationStartNode } from './node/iteration-node'; import { KeywordNode } from './node/keyword-node'; import { ListOperationsNode } from './node/list-operations-node'; +import { LoopNode, LoopStartNode } from './node/loop-node'; import { MessageNode } from './node/message-node'; import NoteNode from './node/note-node'; import ParserNode from './node/parser-node'; @@ -105,6 +107,9 @@ export const nodeTypes: NodeTypes = { listOperationsNode: ListOperationsNode, variableAssignerNode: VariableAssignerNode, variableAggregatorNode: VariableAggregatorNode, + loopNode: LoopNode, + loopStartNode: LoopStartNode, + exitLoopNode: ExitLoopNode, }; const edgeTypes = { diff --git a/web/src/pages/agent/canvas/node/dropdown.tsx b/web/src/pages/agent/canvas/node/dropdown.tsx deleted file mode 100644 index dd5263abc..000000000 --- a/web/src/pages/agent/canvas/node/dropdown.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import OperateDropdown from '@/components/operate-dropdown'; -import { CopyOutlined } from '@ant-design/icons'; -import { Flex, MenuProps } from 'antd'; -import { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Operator } from '../../constant'; -import { useDuplicateNode } from '../../hooks'; -import useGraphStore from '../../store'; - -interface IProps { - id: string; - iconFontColor?: string; - label: string; -} - -const NodeDropdown = ({ id, iconFontColor, label }: IProps) => { - const { t } = useTranslation(); - const deleteNodeById = useGraphStore((store) => store.deleteNodeById); - const deleteIterationNodeById = useGraphStore( - (store) => store.deleteIterationNodeById, - ); - - const deleteNode = useCallback(() => { - if (label === Operator.Iteration) { - deleteIterationNodeById(id); - } else { - deleteNodeById(id); - } - }, [label, deleteIterationNodeById, id, deleteNodeById]); - - const duplicateNode = useDuplicateNode(); - - const items: MenuProps['items'] = [ - { - key: '2', - onClick: () => duplicateNode(id, label), - label: ( - - {t('common.copy')} - - - ), - }, - ]; - - return ( - - ); -}; - -export default NodeDropdown; diff --git a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx index 6021420c5..5aa5c2873 100644 --- a/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx +++ b/web/src/pages/agent/canvas/node/dropdown/accordion-operators.tsx @@ -21,11 +21,23 @@ function OperatorAccordionTrigger({ children }: PropsWithChildren) { export function AccordionOperators({ isCustomDropdown = false, mousePosition, + nodeId, }: { isCustomDropdown?: boolean; mousePosition?: { x: number; y: number }; + nodeId?: string; }) { const { t } = useTranslation(); + const { getOperatorTypeFromId, getParentIdById } = useGraphStore( + (state) => state, + ); + + const exitLoopList = useMemo(() => { + if (getOperatorTypeFromId(getParentIdById(nodeId)) === Operator.Loop) { + return [Operator.ExitLoop]; + } + return []; + }, [getOperatorTypeFromId, getParentIdById, nodeId]); return ( )} @@ -101,9 +102,11 @@ export function InnerNextStepDropdown({ {isPipeline ? ( - + ) : ( - + )} diff --git a/web/src/pages/agent/canvas/node/exit-loop-node.tsx b/web/src/pages/agent/canvas/node/exit-loop-node.tsx new file mode 100644 index 000000000..e6bd6ba3d --- /dev/null +++ b/web/src/pages/agent/canvas/node/exit-loop-node.tsx @@ -0,0 +1,23 @@ +import { BaseNode } from '@/interfaces/database/agent'; +import { NodeProps } from '@xyflow/react'; +import { LeftEndHandle } from './handle'; +import NodeHeader from './node-header'; +import { NodeWrapper } from './node-wrapper'; +import { ToolBar } from './toolbar'; + +export function ExitLoopNode({ id, data, selected }: NodeProps>) { + return ( + + + + + + + ); +} diff --git a/web/src/pages/agent/canvas/node/iteration-node.tsx b/web/src/pages/agent/canvas/node/iteration-node.tsx index a11da33dc..c893e7723 100644 --- a/web/src/pages/agent/canvas/node/iteration-node.tsx +++ b/web/src/pages/agent/canvas/node/iteration-node.tsx @@ -56,7 +56,7 @@ export function InnerIterationNode({ ); } -function InnerIterationStartNode({ +export function InnerIterationStartNode({ isConnectable = true, id, selected, diff --git a/web/src/pages/agent/canvas/node/labeled-group-node.tsx b/web/src/pages/agent/canvas/node/labeled-group-node.tsx new file mode 100644 index 000000000..b7f4f1d75 --- /dev/null +++ b/web/src/pages/agent/canvas/node/labeled-group-node.tsx @@ -0,0 +1,75 @@ +import { Panel, type NodeProps, type PanelPosition } from '@xyflow/react'; +import { type ComponentProps, type ReactNode } from 'react'; + +import { BaseNode } from '@/components/xyflow/base-node'; +import { cn } from '@/lib/utils'; + +/* GROUP NODE Label ------------------------------------------------------- */ + +export type GroupNodeLabelProps = ComponentProps<'div'>; + +export function GroupNodeLabel({ + children, + className, + ...props +}: GroupNodeLabelProps) { + return ( +
+
+ {children} +
+
+ ); +} + +export type GroupNodeProps = Partial & { + label?: ReactNode; + position?: PanelPosition; +}; + +/* GROUP NODE -------------------------------------------------------------- */ + +export function LabeledGroupNode({ + label = '', + position, + ...props +}: GroupNodeProps) { + const getLabelClassName = (position?: PanelPosition) => { + switch (position) { + case 'top-left': + return 'rounded-br-sm'; + case 'top-center': + return 'rounded-b-sm'; + case 'top-right': + return 'rounded-bl-sm'; + case 'bottom-left': + return 'rounded-tr-sm'; + case 'bottom-right': + return 'rounded-tl-sm'; + case 'bottom-center': + return 'rounded-t-sm'; + default: + return 'rounded-br-sm'; + } + }; + + return ( + + + {label && ( + + {label} + + )} + + + ); +} diff --git a/web/src/pages/agent/canvas/node/loop-node.tsx b/web/src/pages/agent/canvas/node/loop-node.tsx new file mode 100644 index 000000000..6afbe841a --- /dev/null +++ b/web/src/pages/agent/canvas/node/loop-node.tsx @@ -0,0 +1,16 @@ +import { BaseNode } from '@/interfaces/database/agent'; +import { NodeProps } from '@xyflow/react'; +import { memo } from 'react'; +import { InnerIterationNode, InnerIterationStartNode } from './iteration-node'; + +export function InnerLoopNode({ ...props }: NodeProps>) { + return ; +} + +export const LoopNode = memo(InnerLoopNode); + +export function InnerLoopStartNode({ ...props }: NodeProps>) { + return ; +} + +export const LoopStartNode = memo(InnerLoopStartNode); diff --git a/web/src/pages/agent/canvas/node/retrieval-node.tsx b/web/src/pages/agent/canvas/node/retrieval-node.tsx index 068c81735..9c6b76110 100644 --- a/web/src/pages/agent/canvas/node/retrieval-node.tsx +++ b/web/src/pages/agent/canvas/node/retrieval-node.tsx @@ -23,7 +23,7 @@ function InnerRetrievalNode({ const knowledgeBaseIds: string[] = get(data, 'form.kb_ids', []); const { list: knowledgeList } = useFetchKnowledgeList(true); - const { getLabel } = useGetVariableLabelOrTypeByValue(id); + const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId: id }); return ( diff --git a/web/src/pages/agent/canvas/node/switch-node.tsx b/web/src/pages/agent/canvas/node/switch-node.tsx index 006afd843..7b15c02b3 100644 --- a/web/src/pages/agent/canvas/node/switch-node.tsx +++ b/web/src/pages/agent/canvas/node/switch-node.tsx @@ -27,7 +27,7 @@ const ConditionBlock = ({ nodeId, }: { condition: ISwitchCondition } & { nodeId: string }) => { const items = condition?.items ?? []; - const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId); + const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId }); const renderOperatorIcon = useCallback((operator?: string) => { const item = SwitchOperatorOptions.find((x) => x.value === operator); diff --git a/web/src/pages/agent/canvas/node/toolbar.tsx b/web/src/pages/agent/canvas/node/toolbar.tsx index 74f6ae3db..775ba228d 100644 --- a/web/src/pages/agent/canvas/node/toolbar.tsx +++ b/web/src/pages/agent/canvas/node/toolbar.tsx @@ -58,7 +58,7 @@ export function ToolBar({ const deleteNode: MouseEventHandler = useCallback( (e) => { e.stopPropagation(); - if (label === Operator.Iteration) { + if ([Operator.Iteration, Operator.Loop].includes(label as Operator)) { deleteIterationNodeById(id); } else { deleteNodeById(id); diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index 4a442271b..283a3d718 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -61,7 +61,6 @@ export const AgentOperatorList = [ Operator.Categorize, Operator.Message, Operator.RewriteQuestion, - Operator.KeywordExtract, Operator.Switch, Operator.Iteration, Operator.WaitingDialogue, @@ -79,10 +78,6 @@ export const DataOperationsOperatorOptions = [ export const SwitchElseTo = 'end_cpn_ids'; -const initialQueryBaseValues = { - query: [], -}; - export const initialRetrievalValues = { query: AgentGlobalsSysQueryWithBrace, top_n: 8, @@ -139,11 +134,6 @@ export const initialMessageValues = { content: [''], }; -export const initialKeywordExtractValues = { - ...initialLlmBaseValues, - top_n: 3, - ...initialQueryBaseValues, -}; export const initialDuckValues = { top_n: 10, channel: Channel.Text, @@ -275,14 +265,6 @@ export const initialGithubValues = { }, }; -export const initialQWeatherValues = { - web_apikey: 'xxx', - type: 'weather', - user_type: 'free', - time_period: 'now', - ...initialQueryBaseValues, -}; - export const initialExeSqlValues = { sql: '', db_type: 'mysql', @@ -331,8 +313,6 @@ export const initialWenCaiValues = { }, }; -export const initialAkShareValues = { top_n: 10, ...initialQueryBaseValues }; - export const initialYahooFinanceValues = { stock_code: '', info: true, @@ -349,22 +329,6 @@ export const initialYahooFinanceValues = { }, }; -export const initialJin10Values = { - type: 'flash', - secret_key: 'xxx', - flash_type: '1', - contain: '', - filter: '', - ...initialQueryBaseValues, -}; - -export const initialTuShareValues = { - token: 'xxx', - src: 'eastmoney', - start_date: '2024-01-01 09:00:00', - ...initialQueryBaseValues, -}; - export const initialNoteValues = { text: '', }; @@ -624,6 +588,13 @@ export const initialVariableAssignerValues = {}; export const initialVariableAggregatorValues = { outputs: {}, groups: [] }; +export const initialLoopValues = { + loop_variables: [], + loop_termination_condition: [], + maximum_loop_count: 10, + outputs: {}, +}; + export const CategorizeAnchorPointPositions = [ { top: 1, right: 34 }, { top: 8, right: 18 }, @@ -659,11 +630,6 @@ export const RestrictedUpstreamMap = { Operator.RewriteQuestion, Operator.Relevant, ], - [Operator.KeywordExtract]: [ - Operator.Begin, - Operator.Message, - Operator.Relevant, - ], [Operator.DuckDuckGo]: [Operator.Begin, Operator.Retrieval], [Operator.Wikipedia]: [Operator.Begin, Operator.Retrieval], [Operator.PubMed]: [Operator.Begin, Operator.Retrieval], @@ -672,15 +638,11 @@ export const RestrictedUpstreamMap = { [Operator.Bing]: [Operator.Begin, Operator.Retrieval], [Operator.GoogleScholar]: [Operator.Begin, Operator.Retrieval], [Operator.GitHub]: [Operator.Begin, Operator.Retrieval], - [Operator.QWeather]: [Operator.Begin, Operator.Retrieval], [Operator.SearXNG]: [Operator.Begin, Operator.Retrieval], [Operator.ExeSQL]: [Operator.Begin], [Operator.Switch]: [Operator.Begin], [Operator.WenCai]: [Operator.Begin], - [Operator.AkShare]: [Operator.Begin], [Operator.YahooFinance]: [Operator.Begin], - [Operator.Jin10]: [Operator.Begin], - [Operator.TuShare]: [Operator.Begin], [Operator.Crawler]: [Operator.Begin], [Operator.Note]: [], [Operator.Invoke]: [Operator.Begin], @@ -706,6 +668,9 @@ export const RestrictedUpstreamMap = { [Operator.Tokenizer]: [Operator.Begin], [Operator.Extractor]: [Operator.Begin], [Operator.File]: [Operator.Begin], + [Operator.Loop]: [Operator.Begin], + [Operator.LoopStart]: [Operator.Begin], + [Operator.ExitLoop]: [Operator.Begin], }; export const NodeMap = { @@ -715,7 +680,6 @@ export const NodeMap = { [Operator.Message]: 'messageNode', [Operator.Relevant]: 'relevantNode', [Operator.RewriteQuestion]: 'rewriteNode', - [Operator.KeywordExtract]: 'keywordNode', [Operator.DuckDuckGo]: 'ragNode', [Operator.Wikipedia]: 'ragNode', [Operator.PubMed]: 'ragNode', @@ -724,15 +688,11 @@ export const NodeMap = { [Operator.Bing]: 'ragNode', [Operator.GoogleScholar]: 'ragNode', [Operator.GitHub]: 'ragNode', - [Operator.QWeather]: 'ragNode', [Operator.SearXNG]: 'ragNode', [Operator.ExeSQL]: 'ragNode', [Operator.Switch]: 'switchNode', [Operator.WenCai]: 'ragNode', - [Operator.AkShare]: 'ragNode', [Operator.YahooFinance]: 'ragNode', - [Operator.Jin10]: 'ragNode', - [Operator.TuShare]: 'ragNode', [Operator.Note]: 'noteNode', [Operator.Crawler]: 'ragNode', [Operator.Invoke]: 'ragNode', @@ -758,6 +718,9 @@ export const NodeMap = { [Operator.ListOperations]: 'listOperationsNode', [Operator.VariableAssigner]: 'variableAssignerNode', [Operator.VariableAggregator]: 'variableAggregatorNode', + [Operator.Loop]: 'loopNode', + [Operator.LoopStart]: 'loopStartNode', + [Operator.ExitLoop]: 'exitLoopNode', }; export enum BeginQueryType { @@ -891,3 +854,82 @@ export const ArrayFields = [ TypesWithArray.ArrayString, TypesWithArray.ArrayObject, ]; + +export enum InputMode { + Constant = 'constant', + Variable = 'variable', +} + +export enum LoopTerminationComparisonOperator { + Contains = ComparisonOperator.Contains, + NotContains = ComparisonOperator.NotContains, + StartWith = ComparisonOperator.StartWith, + EndWith = ComparisonOperator.EndWith, + Is = 'is', + IsNot = 'is not', +} + +export const LoopTerminationStringComparisonOperator = [ + LoopTerminationComparisonOperator.Contains, + LoopTerminationComparisonOperator.NotContains, + LoopTerminationComparisonOperator.StartWith, + LoopTerminationComparisonOperator.EndWith, + LoopTerminationComparisonOperator.Is, + LoopTerminationComparisonOperator.IsNot, + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; + +export const LoopTerminationBooleanComparisonOperator = [ + LoopTerminationComparisonOperator.Is, + LoopTerminationComparisonOperator.IsNot, + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; +// object or object array +export const LoopTerminationObjectComparisonOperator = [ + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; + +// string array or number array +export const LoopTerminationStringArrayComparisonOperator = [ + LoopTerminationComparisonOperator.Contains, + LoopTerminationComparisonOperator.NotContains, + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; + +export const LoopTerminationBooleanArrayComparisonOperator = [ + LoopTerminationComparisonOperator.Is, + LoopTerminationComparisonOperator.IsNot, + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; + +export const LoopTerminationNumberComparisonOperator = [ + ComparisonOperator.Equal, + ComparisonOperator.NotEqual, + ComparisonOperator.GreatThan, + ComparisonOperator.LessThan, + ComparisonOperator.GreatEqual, + ComparisonOperator.LessEqual, + ComparisonOperator.Empty, + ComparisonOperator.NotEmpty, +]; + +export const LoopTerminationStringComparisonOperatorMap = { + [TypesWithArray.String]: LoopTerminationStringComparisonOperator, + [TypesWithArray.Number]: LoopTerminationNumberComparisonOperator, + [TypesWithArray.Boolean]: LoopTerminationBooleanComparisonOperator, + [TypesWithArray.Object]: LoopTerminationObjectComparisonOperator, + [TypesWithArray.ArrayString]: LoopTerminationStringArrayComparisonOperator, + [TypesWithArray.ArrayNumber]: LoopTerminationStringArrayComparisonOperator, + [TypesWithArray.ArrayBoolean]: LoopTerminationBooleanArrayComparisonOperator, + [TypesWithArray.ArrayObject]: LoopTerminationObjectComparisonOperator, +}; + +export enum AgentVariableType { + Begin = 'begin', + Conversation = 'conversation', +} diff --git a/web/src/pages/agent/form-sheet/form-config-map.tsx b/web/src/pages/agent/form-sheet/form-config-map.tsx index 37ab4cf2f..a1a68bb21 100644 --- a/web/src/pages/agent/form-sheet/form-config-map.tsx +++ b/web/src/pages/agent/form-sheet/form-config-map.tsx @@ -1,6 +1,5 @@ import { Operator } from '../constant'; import AgentForm from '../form/agent-form'; -import AkShareForm from '../form/akshare-form'; import ArXivForm from '../form/arxiv-form'; import BeginForm from '../form/begin-form'; import BingForm from '../form/bing-form'; @@ -19,13 +18,11 @@ import HierarchicalMergerForm from '../form/hierarchical-merger-form'; import InvokeForm from '../form/invoke-form'; import IterationForm from '../form/iteration-form'; import IterationStartForm from '../form/iteration-start-from'; -import Jin10Form from '../form/jin10-form'; -import KeywordExtractForm from '../form/keyword-extract-form'; import ListOperationsForm from '../form/list-operations-form'; +import LoopForm from '../form/loop-form'; import MessageForm from '../form/message-form'; import ParserForm from '../form/parser-form'; import PubMedForm from '../form/pubmed-form'; -import QWeatherForm from '../form/qweather-form'; import RelevantForm from '../form/relevant-form'; import RetrievalForm from '../form/retrieval-form/next'; import RewriteQuestionForm from '../form/rewrite-question-form'; @@ -37,7 +34,6 @@ import TavilyExtractForm from '../form/tavily-extract-form'; import TavilyForm from '../form/tavily-form'; import TokenizerForm from '../form/tokenizer-form'; import ToolForm from '../form/tool-form'; -import TuShareForm from '../form/tushare-form'; import UserFillUpForm from '../form/user-fill-up-form'; import VariableAggregatorForm from '../form/variable-aggregator-form'; import VariableAssignerForm from '../form/variable-assigner-form'; @@ -76,9 +72,6 @@ export const FormConfigMap = { [Operator.DuckDuckGo]: { component: DuckDuckGoForm, }, - [Operator.KeywordExtract]: { - component: KeywordExtractForm, - }, [Operator.Wikipedia]: { component: WikipediaForm, }, @@ -100,9 +93,6 @@ export const FormConfigMap = { [Operator.GitHub]: { component: GithubForm, }, - [Operator.QWeather]: { - component: QWeatherForm, - }, [Operator.ExeSQL]: { component: ExeSQLForm, }, @@ -112,18 +102,9 @@ export const FormConfigMap = { [Operator.WenCai]: { component: WenCaiForm, }, - [Operator.AkShare]: { - component: AkShareForm, - }, [Operator.YahooFinance]: { component: YahooFinanceForm, }, - [Operator.Jin10]: { - component: Jin10Form, - }, - [Operator.TuShare]: { - component: TuShareForm, - }, [Operator.Crawler]: { component: CrawlerForm, }, @@ -191,8 +172,13 @@ export const FormConfigMap = { [Operator.VariableAssigner]: { component: VariableAssignerForm, }, - [Operator.VariableAggregator]: { component: VariableAggregatorForm, }, + [Operator.Loop]: { + component: LoopForm, + }, + [Operator.ExitLoop]: { + component: () => <>, + }, }; diff --git a/web/src/pages/agent/form/akshare-form/index.tsx b/web/src/pages/agent/form/akshare-form/index.tsx deleted file mode 100644 index 1cfd554b1..000000000 --- a/web/src/pages/agent/form/akshare-form/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { TopNFormField } from '@/components/top-n-item'; -import { Form } from '@/components/ui/form'; -import { INextOperatorForm } from '../../interface'; -import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; - -const AkShareForm = ({ form, node }: INextOperatorForm) => { - return ( -
- { - e.preventDefault(); - }} - > - - -
- - ); -}; - -export default AkShareForm; diff --git a/web/src/pages/agent/form/components/dynamic-input-variable.tsx b/web/src/pages/agent/form/components/dynamic-input-variable.tsx deleted file mode 100644 index a5781fd16..000000000 --- a/web/src/pages/agent/form/components/dynamic-input-variable.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons'; -import { Button, Collapse, Flex, Form, Input, Select } from 'antd'; -import { PropsWithChildren, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; - -import styles from './index.less'; - -interface IProps { - node?: RAGFlowNodeType; -} - -enum VariableType { - Reference = 'reference', - Input = 'input', -} - -const getVariableName = (type: string) => - type === VariableType.Reference ? 'component_id' : 'value'; - -const DynamicVariableForm = ({ node }: IProps) => { - const { t } = useTranslation(); - const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); - const form = Form.useFormInstance(); - - const options = [ - { value: VariableType.Reference, label: t('flow.reference') }, - { value: VariableType.Input, label: t('flow.text') }, - ]; - - const handleTypeChange = useCallback( - (name: number) => () => { - setTimeout(() => { - form.setFieldValue(['query', name, 'component_id'], undefined); - form.setFieldValue(['query', name, 'value'], undefined); - }, 0); - }, - [form], - ); - - return ( - - {(fields, { add, remove }) => ( - <> - {fields.map(({ key, name, ...restField }) => ( - - - - - - {({ getFieldValue }) => { - const type = getFieldValue(['query', name, 'type']); - return ( - - {type === VariableType.Reference ? ( - - ) : ( - - )} - - ); - }} - - remove(name)} /> - - ))} - - - - - )} - - ); -}; - -export function FormCollapse({ - children, - title, -}: PropsWithChildren<{ title: string }>) { - return ( - {title}, - children, - }, - ]} - /> - ); -} - -const DynamicInputVariable = ({ node }: IProps) => { - const { t } = useTranslation(); - return ( - - - - ); -}; - -export default DynamicInputVariable; diff --git a/web/src/pages/agent/form/components/next-dynamic-input-variable.tsx b/web/src/pages/agent/form/components/next-dynamic-input-variable.tsx deleted file mode 100644 index 8b4cbd8a9..000000000 --- a/web/src/pages/agent/form/components/next-dynamic-input-variable.tsx +++ /dev/null @@ -1,135 +0,0 @@ -'use client'; - -import { SideDown } from '@/assets/icon/next-icon'; -import { Button } from '@/components/ui/button'; -import { - Collapsible, - CollapsibleContent, - CollapsibleTrigger, -} from '@/components/ui/collapsible'; -import { - FormControl, - FormDescription, - FormField, - FormItem, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { RAGFlowSelect } from '@/components/ui/select'; -import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { Plus, Trash2 } from 'lucide-react'; -import { useFieldArray, useFormContext } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { useBuildVariableOptions } from '../../hooks/use-get-begin-query'; - -interface IProps { - node?: RAGFlowNodeType; -} - -enum VariableType { - Reference = 'reference', - Input = 'input', -} - -const getVariableName = (type: string) => - type === VariableType.Reference ? 'component_id' : 'value'; - -export function DynamicVariableForm({ node }: IProps) { - const { t } = useTranslation(); - const form = useFormContext(); - const { fields, remove, append } = useFieldArray({ - name: 'query', - control: form.control, - }); - - const valueOptions = useBuildVariableOptions(node?.id, node?.parentId); - - const options = [ - { value: VariableType.Reference, label: t('flow.reference') }, - { value: VariableType.Input, label: t('flow.text') }, - ]; - - return ( -
- {fields.map((field, index) => { - const typeField = `query.${index}.type`; - const typeValue = form.watch(typeField); - return ( -
- ( - - - - { - field.onChange(val); - form.resetField(`query.${index}.value`); - form.resetField(`query.${index}.component_id`); - }} - > - - - - )} - /> - ( - - - - {typeValue === VariableType.Reference ? ( - - ) : ( - - )} - - - - )} - /> - remove(index)} - /> -
- ); - })} - -
- ); -} - -export function DynamicInputVariable({ node }: IProps) { - const { t } = useTranslation(); - - return ( - - - - {t('flow.input')} - - - - - - - - ); -} diff --git a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx index d80d0c2df..7b86a90e9 100644 --- a/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx +++ b/web/src/pages/agent/form/components/prompt-editor/variable-picker-plugin.tsx @@ -192,7 +192,7 @@ export default function VariablePickerMenuPlugin({ const [queryString, setQueryString] = React.useState(''); - let options = useFilterQueryVariableOptionsByTypes(types); + let options = useFilterQueryVariableOptionsByTypes({ types }); if (baseOptions) { options = baseOptions as typeof options; diff --git a/web/src/pages/agent/form/components/query-variable-list.tsx b/web/src/pages/agent/form/components/query-variable-list.tsx index a4a76781d..d2ed52fce 100644 --- a/web/src/pages/agent/form/components/query-variable-list.tsx +++ b/web/src/pages/agent/form/components/query-variable-list.tsx @@ -20,7 +20,7 @@ export function QueryVariableList({ const form = useFormContext(); const name = 'query'; - let options = useFilterQueryVariableOptionsByTypes(types); + let options = useFilterQueryVariableOptionsByTypes({ types }); const secondOptions = flatOptions(options); diff --git a/web/src/pages/agent/form/components/query-variable.tsx b/web/src/pages/agent/form/components/query-variable.tsx index fc76c2554..8c8f8d08f 100644 --- a/web/src/pages/agent/form/components/query-variable.tsx +++ b/web/src/pages/agent/form/components/query-variable.tsx @@ -9,7 +9,10 @@ import { ReactNode } from 'react'; import { useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { JsonSchemaDataType } from '../../constant'; -import { useFilterQueryVariableOptionsByTypes } from '../../hooks/use-get-begin-query'; +import { + BuildQueryVariableOptions, + useFilterQueryVariableOptionsByTypes, +} from '../../hooks/use-get-begin-query'; import { GroupedSelectWithSecondaryMenu } from './select-with-secondary-menu'; type QueryVariableProps = { @@ -21,7 +24,7 @@ type QueryVariableProps = { onChange?: (value: string) => void; pureQuery?: boolean; value?: string; -}; +} & BuildQueryVariableOptions; export function QueryVariable({ name = 'query', @@ -32,11 +35,17 @@ export function QueryVariable({ onChange, pureQuery = false, value, + nodeIds = [], + variablesExceptOperatorOutputs, }: QueryVariableProps) { const { t } = useTranslation(); const form = useFormContext(); - const finalOptions = useFilterQueryVariableOptionsByTypes(types); + const finalOptions = useFilterQueryVariableOptionsByTypes({ + types, + nodeIds, + variablesExceptOperatorOutputs, + }); const renderWidget = ( value?: string, diff --git a/web/src/pages/agent/form/invoke-form/variable-table.tsx b/web/src/pages/agent/form/invoke-form/variable-table.tsx index 8ca794bde..68fbf0a9c 100644 --- a/web/src/pages/agent/form/invoke-form/variable-table.tsx +++ b/web/src/pages/agent/form/invoke-form/variable-table.tsx @@ -49,7 +49,7 @@ export function VariableTable({ nodeId, }: IProps) { const { t } = useTranslation(); - const { getLabel } = useGetVariableLabelOrTypeByValue(nodeId!); + const { getLabel } = useGetVariableLabelOrTypeByValue({ nodeId: nodeId! }); const [sorting, setSorting] = React.useState([]); const [columnFilters, setColumnFilters] = React.useState( diff --git a/web/src/pages/agent/form/iteration-form/dynamic-output.tsx b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx index 2bc4ab6f4..8cb8a4b48 100644 --- a/web/src/pages/agent/form/iteration-form/dynamic-output.tsx +++ b/web/src/pages/agent/form/iteration-form/dynamic-output.tsx @@ -2,7 +2,6 @@ import { FormContainer } from '@/components/form-container'; import { KeyInput } from '@/components/key-input'; -import { SelectWithSearch } from '@/components/originui/select-with-search'; import { BlockButton, Button } from '@/components/ui/button'; import { FormControl, @@ -11,13 +10,17 @@ import { FormMessage, } from '@/components/ui/form'; import { Separator } from '@/components/ui/separator'; +import { Operator } from '@/constants/agent'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { t } from 'i18next'; +import { isEmpty } from 'lodash'; import { X } from 'lucide-react'; -import { ReactNode, useCallback, useMemo } from 'react'; +import { ReactNode } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; -import { useBuildSubNodeOutputOptions } from './use-build-options'; +import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query'; +import useGraphStore from '../../store'; +import { QueryVariable } from '../components/query-variable'; interface IProps { node?: RAGFlowNodeType; @@ -26,28 +29,22 @@ interface IProps { export function DynamicOutputForm({ node }: IProps) { const { t } = useTranslation(); const form = useFormContext(); - const options = useBuildSubNodeOutputOptions(node?.id); + const { nodes } = useGraphStore((state) => state); + + const childNodeIds = nodes + .filter( + (x) => + x.parentId === node?.id && + x.data.label !== Operator.IterationStart && + !isEmpty(x.data?.form?.outputs), + ) + .map((x) => x.id); + const name = 'outputs'; - const flatOptions = useMemo(() => { - return options.reduce<{ label: string; value: string; type: string }[]>( - (pre, cur) => { - pre.push(...cur.options); - return pre; - }, - [], - ); - }, [options]); - - const findType = useCallback( - (val: string) => { - const type = flatOptions.find((x) => x.value === val)?.type; - if (type) { - return `Array<${type}>`; - } - }, - [flatOptions], - ); + const { getType } = useGetVariableLabelOrTypeByValue({ + nodeIds: childNodeIds, + }); const { fields, remove, append } = useFieldArray({ name: name, @@ -77,25 +74,15 @@ export function DynamicOutputForm({ node }: IProps) { )} /> - ( - - - { - form.setValue(typeField, findType(val)); - field.onChange(val); - }} - > - - - - )} - /> + hideLabel + className="w-2/5" + onChange={(val) => { + form.setValue(typeField, `Array<${getType(val)}>`); + }} + nodeIds={childNodeIds} + > - diff --git a/web/src/pages/agent/form/iteration-form/use-build-options.ts b/web/src/pages/agent/form/iteration-form/use-build-options.ts deleted file mode 100644 index 46fa7ee30..000000000 --- a/web/src/pages/agent/form/iteration-form/use-build-options.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { buildOutputOptions } from '@/utils/canvas-util'; -import { isEmpty } from 'lodash'; -import { useMemo } from 'react'; -import { Operator } from '../../constant'; -import useGraphStore from '../../store'; - -export function useBuildSubNodeOutputOptions(nodeId?: string) { - const { nodes } = useGraphStore((state) => state); - - const nodeOutputOptions = useMemo(() => { - if (!nodeId) { - return []; - } - - const subNodeWithOutputList = nodes.filter( - (x) => - x.parentId === nodeId && - x.data.label !== Operator.IterationStart && - !isEmpty(x.data?.form?.outputs), - ); - - return subNodeWithOutputList.map((x) => ({ - label: x.data.name, - value: x.id, - title: x.data.name, - options: buildOutputOptions(x.data.form.outputs, x.id), - })); - }, [nodeId, nodes]); - - return nodeOutputOptions; -} diff --git a/web/src/pages/agent/form/jin10-form/index.tsx b/web/src/pages/agent/form/jin10-form/index.tsx deleted file mode 100644 index 2bc6d774a..000000000 --- a/web/src/pages/agent/form/jin10-form/index.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { Form, Input, Select } from 'antd'; -import { useMemo } from 'react'; -import { IOperatorForm } from '../../interface'; -import { - Jin10CalendarDatashapeOptions, - Jin10CalendarTypeOptions, - Jin10FlashTypeOptions, - Jin10SymbolsDatatypeOptions, - Jin10SymbolsTypeOptions, - Jin10TypeOptions, -} from '../../options'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const Jin10Form = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const jin10TypeOptions = useMemo(() => { - return Jin10TypeOptions.map((x) => ({ - value: x, - label: t(`jin10TypeOptions.${x}`), - })); - }, [t]); - - const jin10FlashTypeOptions = useMemo(() => { - return Jin10FlashTypeOptions.map((x) => ({ - value: x, - label: t(`jin10FlashTypeOptions.${x}`), - })); - }, [t]); - - const jin10CalendarTypeOptions = useMemo(() => { - return Jin10CalendarTypeOptions.map((x) => ({ - value: x, - label: t(`jin10CalendarTypeOptions.${x}`), - })); - }, [t]); - - const jin10CalendarDatashapeOptions = useMemo(() => { - return Jin10CalendarDatashapeOptions.map((x) => ({ - value: x, - label: t(`jin10CalendarDatashapeOptions.${x}`), - })); - }, [t]); - - const jin10SymbolsTypeOptions = useMemo(() => { - return Jin10SymbolsTypeOptions.map((x) => ({ - value: x, - label: t(`jin10SymbolsTypeOptions.${x}`), - })); - }, [t]); - - const jin10SymbolsDatatypeOptions = useMemo(() => { - return Jin10SymbolsDatatypeOptions.map((x) => ({ - value: x, - label: t(`jin10SymbolsDatatypeOptions.${x}`), - })); - }, [t]); - - return ( -
- - - - - - - - - {({ getFieldValue }) => { - const type = getFieldValue('type'); - switch (type) { - case 'flash': - return ( - <> - - - - - - - - - - - ); - - case 'calendar': - return ( - <> - - - - - - - - ); - - case 'symbols': - return ( - <> - - - - - - - - ); - - case 'news': - return ( - <> - - - - - - - - ); - - default: - return <>; - } - }} - -
- ); -}; - -export default Jin10Form; diff --git a/web/src/pages/agent/form/keyword-extract-form/index.tsx b/web/src/pages/agent/form/keyword-extract-form/index.tsx deleted file mode 100644 index bda5d44f5..000000000 --- a/web/src/pages/agent/form/keyword-extract-form/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { NextLLMSelect } from '@/components/llm-select/next'; -import { TopNFormField } from '@/components/top-n-item'; -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { useTranslation } from 'react-i18next'; -import { INextOperatorForm } from '../../interface'; -import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; - -const KeywordExtractForm = ({ form, node }: INextOperatorForm) => { - const { t } = useTranslation(); - - return ( -
- { - e.preventDefault(); - }} - > - - ( - - - {t('chat.model')} - - - - - - - )} - /> - - - - ); -}; - -export default KeywordExtractForm; diff --git a/web/src/pages/agent/form/iteration-form/dynamic-variables.tsx b/web/src/pages/agent/form/loop-form/dynamic-variables.tsx similarity index 79% rename from web/src/pages/agent/form/iteration-form/dynamic-variables.tsx rename to web/src/pages/agent/form/loop-form/dynamic-variables.tsx index 62840d5a7..04318af10 100644 --- a/web/src/pages/agent/form/iteration-form/dynamic-variables.tsx +++ b/web/src/pages/agent/form/loop-form/dynamic-variables.tsx @@ -1,33 +1,27 @@ +import { BoolSegmented } from '@/components/bool-segmented'; import { KeyInput } from '@/components/key-input'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; import { useIsDarkTheme } from '@/components/theme-provider'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Separator } from '@/components/ui/separator'; import { Textarea } from '@/components/ui/textarea'; -import { buildOptions } from '@/utils/form'; import { Editor, loader } from '@monaco-editor/react'; -import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'; import { X } from 'lucide-react'; import { ReactNode, useCallback } from 'react'; import { useFieldArray, useFormContext } from 'react-hook-form'; -import { TypesWithArray } from '../../constant'; -import { buildConversationVariableSelectOptions } from '../../utils'; +import { InputMode, TypesWithArray } from '../../constant'; +import { + InputModeOptions, + buildConversationVariableSelectOptions, +} from '../../utils'; import { DynamicFormHeader } from '../components/dynamic-fom-header'; import { QueryVariable } from '../components/query-variable'; +import { useInitializeConditions } from './use-watch-form-change'; loader.config({ paths: { vs: '/vs' } }); -enum InputMode { - Constant = 'constant', - Variable = 'variable', -} - -const InputModeOptions = buildOptions(InputMode); - type SelectKeysProps = { name: string; label: ReactNode; @@ -35,42 +29,15 @@ type SelectKeysProps = { keyField?: string; valueField?: string; operatorField?: string; + nodeId?: string; }; -type RadioGroupProps = React.ComponentProps; - -type RadioButtonProps = Partial< - Omit & { - onChange: RadioGroupProps['onValueChange']; - } ->; - -function RadioButton({ value, onChange }: RadioButtonProps) { - return ( - -
- - -
-
- - -
-
- ); -} - const VariableTypeOptions = buildConversationVariableSelectOptions(); -const modeField = 'mode'; +const modeField = 'input_mode'; const ConstantValueMap = { - [TypesWithArray.Boolean]: 'yes', + [TypesWithArray.Boolean]: true, [TypesWithArray.Number]: 0, [TypesWithArray.String]: '', [TypesWithArray.ArrayBoolean]: '[]', @@ -85,8 +52,9 @@ export function DynamicVariables({ label, tooltip, keyField = 'variable', - valueField = 'parameter', - operatorField = 'operator', + valueField = 'value', + operatorField = 'type', + nodeId, }: SelectKeysProps) { const form = useFormContext(); const isDarkTheme = useIsDarkTheme(); @@ -96,6 +64,9 @@ export function DynamicVariables({ control: form.control, }); + const { initializeVariableRelatedConditions } = + useInitializeConditions(nodeId); + const initializeValue = useCallback( (mode: string, variableType: string, valueFieldAlias: string) => { if (mode === InputMode.Variable) { @@ -112,23 +83,27 @@ export function DynamicVariables({ (mode: string, valueFieldAlias: string, operatorFieldAlias: string) => { const variableType = form.getValues(operatorFieldAlias); initializeValue(mode, variableType, valueFieldAlias); - // if (mode === InputMode.Variable) { - // form.setValue(valueFieldAlias, ''); - // } else { - // const val = ConstantValueMap[variableType as TypesWithArray]; - // form.setValue(valueFieldAlias, val); - // } }, [form, initializeValue], ); const handleVariableTypeChange = useCallback( - (variableType: string, valueFieldAlias: string, modeFieldAlias: string) => { + ( + variableType: string, + valueFieldAlias: string, + modeFieldAlias: string, + keyFieldAlias: string, + ) => { const mode = form.getValues(modeFieldAlias); + initializeVariableRelatedConditions( + form.getValues(keyFieldAlias), + variableType, + ); + initializeValue(mode, variableType, valueFieldAlias); }, - [form, initializeValue], + [form, initializeValue, initializeVariableRelatedConditions], ); const renderParameter = useCallback( @@ -138,7 +113,7 @@ export function DynamicVariables({ if (mode === InputMode.Constant) { if (logicalOperator === TypesWithArray.Boolean) { - return ; + return ; } if (logicalOperator === TypesWithArray.Number) { @@ -211,6 +186,7 @@ export function DynamicVariables({ val, valueFieldAlias, modeFieldAlias, + keyFieldAlias, ); field.onChange(val); }} diff --git a/web/src/pages/agent/form/loop-form/index.tsx b/web/src/pages/agent/form/loop-form/index.tsx new file mode 100644 index 000000000..6a5e30f38 --- /dev/null +++ b/web/src/pages/agent/form/loop-form/index.tsx @@ -0,0 +1,52 @@ +import { SliderInputFormField } from '@/components/slider-input-form-field'; +import { Form } from '@/components/ui/form'; +import { FormLayout } from '@/constants/form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { memo } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { initialLoopValues } from '../../constant'; +import { INextOperatorForm } from '../../interface'; +import { FormWrapper } from '../components/form-wrapper'; +import { DynamicVariables } from './dynamic-variables'; +import { LoopTerminationCondition } from './loop-termination-condition'; +import { FormSchema, LoopFormSchemaType } from './schema'; +import { useFormValues } from './use-values'; +import { useWatchFormChange } from './use-watch-form-change'; + +function LoopForm({ node }: INextOperatorForm) { + const defaultValues = useFormValues(initialLoopValues, node); + const { t } = useTranslation(); + + const form = useForm({ + defaultValues: defaultValues, + resolver: zodResolver(FormSchema), + }); + + useWatchFormChange(node?.id, form); + + return ( +
+ + + + + +
+ ); +} + +export default memo(LoopForm); diff --git a/web/src/pages/agent/form/loop-form/loop-termination-condition.tsx b/web/src/pages/agent/form/loop-form/loop-termination-condition.tsx new file mode 100644 index 000000000..52d598711 --- /dev/null +++ b/web/src/pages/agent/form/loop-form/loop-termination-condition.tsx @@ -0,0 +1,316 @@ +import { BoolSegmented } from '@/components/bool-segmented'; +import { LogicalOperator } from '@/components/logical-operator'; +import { SelectWithSearch } from '@/components/originui/select-with-search'; +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Separator } from '@/components/ui/separator'; +import { ComparisonOperator, SwitchLogicOperator } from '@/constants/agent'; +import { loader } from '@monaco-editor/react'; +import { toLower } from 'lodash'; +import { X } from 'lucide-react'; +import { ReactNode, useCallback, useMemo } from 'react'; +import { useFieldArray, useFormContext } from 'react-hook-form'; +import { + AgentVariableType, + InputMode, + JsonSchemaDataType, +} from '../../constant'; +import { useFilterChildNodeIds } from '../../hooks/use-filter-child-node-ids'; +import { useGetVariableLabelOrTypeByValue } from '../../hooks/use-get-begin-query'; +import { InputModeOptions } from '../../utils'; +import { DynamicFormHeader } from '../components/dynamic-fom-header'; +import { QueryVariable } from '../components/query-variable'; +import { LoopFormSchemaType } from './schema'; +import { useBuildLogicalOptions } from './use-build-logical-options'; +import { + ConditionKeyType, + ConditionModeType, + ConditionOperatorType, + ConditionValueType, + useInitializeConditions, +} from './use-watch-form-change'; + +loader.config({ paths: { vs: '/vs' } }); + +const VariablesExceptOperatorOutputs = [AgentVariableType.Conversation]; + +type LoopTerminationConditionProps = { + label: ReactNode; + tooltip?: string; + keyField?: string; + valueField?: string; + operatorField?: string; + modeField?: string; + nodeId?: string; +}; + +const EmptyFields = [ComparisonOperator.Empty, ComparisonOperator.NotEmpty]; + +const LogicalOperatorFieldName = 'logical_operator'; + +const name = 'loop_termination_condition'; + +export function LoopTerminationCondition({ + label, + tooltip, + keyField = 'variable', + valueField = 'value', + operatorField = 'operator', + modeField = 'input_mode', + nodeId, +}: LoopTerminationConditionProps) { + const form = useFormContext(); + const childNodeIds = useFilterChildNodeIds(nodeId); + + const nodeIds = useMemo(() => { + if (!nodeId) return []; + return [nodeId, ...childNodeIds]; + }, [childNodeIds, nodeId]); + + const { getType } = useGetVariableLabelOrTypeByValue({ + nodeIds: nodeIds, + variablesExceptOperatorOutputs: VariablesExceptOperatorOutputs, + }); + + const { + initializeConditionMode, + initializeConditionOperator, + initializeConditionValue, + } = useInitializeConditions(nodeId); + + const { fields, remove, append } = useFieldArray({ + name: name, + control: form.control, + }); + + const { buildLogicalOptions } = useBuildLogicalOptions(); + + const getVariableType = useCallback( + (keyFieldName: ConditionKeyType) => { + const key = form.getValues(keyFieldName); + return toLower(getType(key)); + }, + [form, getType], + ); + + const initializeMode = useCallback( + (modeFieldAlias: ConditionModeType, keyFieldAlias: ConditionKeyType) => { + const keyType = getVariableType(keyFieldAlias); + + initializeConditionMode(modeFieldAlias, keyType); + }, + [getVariableType, initializeConditionMode], + ); + + const initializeValue = useCallback( + (valueFieldAlias: ConditionValueType, keyFieldAlias: ConditionKeyType) => { + const keyType = getVariableType(keyFieldAlias); + + initializeConditionValue(valueFieldAlias, keyType); + }, + [getVariableType, initializeConditionValue], + ); + + const handleVariableChange = useCallback( + ( + operatorFieldAlias: ConditionOperatorType, + valueFieldAlias: ConditionValueType, + keyFieldAlias: ConditionKeyType, + modeFieldAlias: ConditionModeType, + ) => { + return () => { + initializeConditionOperator( + operatorFieldAlias, + getVariableType(keyFieldAlias), + ); + + initializeMode(modeFieldAlias, keyFieldAlias); + + initializeValue(valueFieldAlias, keyFieldAlias); + }; + }, + [ + getVariableType, + initializeConditionOperator, + initializeMode, + initializeValue, + ], + ); + + const handleOperatorChange = useCallback( + ( + valueFieldAlias: ConditionValueType, + keyFieldAlias: ConditionKeyType, + modeFieldAlias: ConditionModeType, + ) => { + initializeMode(modeFieldAlias, keyFieldAlias); + initializeValue(valueFieldAlias, keyFieldAlias); + }, + [initializeMode, initializeValue], + ); + + const handleModeChange = useCallback( + (mode: string, valueFieldAlias: ConditionValueType) => { + form.setValue(valueFieldAlias, mode === InputMode.Constant ? 0 : '', { + shouldDirty: true, + }); + }, + [form], + ); + + const renderParameterPanel = useCallback( + ( + keyFieldName: ConditionKeyType, + valueFieldAlias: ConditionValueType, + modeFieldAlias: ConditionModeType, + operatorFieldAlias: ConditionOperatorType, + ) => { + const type = getVariableType(keyFieldName); + const mode = form.getValues(modeFieldAlias); + const operator = form.getValues(operatorFieldAlias); + + if (EmptyFields.includes(operator as ComparisonOperator)) { + return null; + } + + if (type === JsonSchemaDataType.Number) { + return ( +
+ + {(field) => ( + { + handleModeChange(val, valueFieldAlias); + field.onChange(val); + }} + options={InputModeOptions} + > + )} + + + {mode === InputMode.Constant ? ( + + + + ) : ( + + )} +
+ ); + } + + if (type === JsonSchemaDataType.Boolean) { + return ( + + + + ); + } + + return ( + + + + ); + }, + [form, getVariableType, handleModeChange], + ); + + return ( +
+ { + if (fields.length === 1) { + form.setValue(LogicalOperatorFieldName, SwitchLogicOperator.And); + } + append({ [keyField]: '', [valueField]: '' }); + }} + > +
+ {fields.length > 1 && ( + + )} +
+ {fields.map((field, index) => { + const keyFieldAlias = + `${name}.${index}.${keyField}` as ConditionKeyType; + const valueFieldAlias = + `${name}.${index}.${valueField}` as ConditionValueType; + const operatorFieldAlias = + `${name}.${index}.${operatorField}` as ConditionOperatorType; + const modeFieldAlias = + `${name}.${index}.${modeField}` as ConditionModeType; + + return ( +
+
+
+ + + + + + {({ onChange, value }) => ( + { + handleOperatorChange( + valueFieldAlias, + keyFieldAlias, + modeFieldAlias, + ); + onChange(val); + }} + options={buildLogicalOptions( + getVariableType(keyFieldAlias), + )} + > + )} + +
+ {renderParameterPanel( + keyFieldAlias, + valueFieldAlias, + modeFieldAlias, + operatorFieldAlias, + )} +
+ + +
+ ); + })} +
+
+
+ ); +} diff --git a/web/src/pages/agent/form/loop-form/schema.ts b/web/src/pages/agent/form/loop-form/schema.ts new file mode 100644 index 000000000..982cbb305 --- /dev/null +++ b/web/src/pages/agent/form/loop-form/schema.ts @@ -0,0 +1,24 @@ +import { z } from 'zod'; + +export const FormSchema = z.object({ + loop_variables: z.array( + z.object({ + variable: z.string().optional(), + type: z.string().optional(), + value: z.string().or(z.number()).or(z.boolean()).optional(), + input_mode: z.string(), + }), + ), + logical_operator: z.string(), + loop_termination_condition: z.array( + z.object({ + variable: z.string().optional(), + operator: z.string().optional(), + value: z.string().or(z.number()).or(z.boolean()).optional(), + input_mode: z.string().optional(), + }), + ), + maximum_loop_count: z.number(), +}); + +export type LoopFormSchemaType = z.infer; diff --git a/web/src/pages/agent/form/loop-form/use-build-logical-options.ts b/web/src/pages/agent/form/loop-form/use-build-logical-options.ts new file mode 100644 index 000000000..35aae3f8d --- /dev/null +++ b/web/src/pages/agent/form/loop-form/use-build-logical-options.ts @@ -0,0 +1,27 @@ +import { SwitchOperatorOptions } from '@/constants/agent'; +import { camelCase, toLower } from 'lodash'; +import { useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { LoopTerminationStringComparisonOperatorMap } from '../../constant'; + +export function useBuildLogicalOptions() { + const { t } = useTranslation(); + + const buildLogicalOptions = useCallback( + (type: string) => { + return LoopTerminationStringComparisonOperatorMap[ + toLower(type) as keyof typeof LoopTerminationStringComparisonOperatorMap + ]?.map((x) => ({ + label: t( + `flow.switchOperatorOptions.${camelCase(SwitchOperatorOptions.find((y) => y.value === x)?.label || x)}`, + ), + value: x, + })); + }, + [t], + ); + + return { + buildLogicalOptions, + }; +} diff --git a/web/src/pages/agent/form/loop-form/use-values.ts b/web/src/pages/agent/form/loop-form/use-values.ts new file mode 100644 index 000000000..cf7a1054a --- /dev/null +++ b/web/src/pages/agent/form/loop-form/use-values.ts @@ -0,0 +1,20 @@ +import { RAGFlowNodeType } from '@/interfaces/database/flow'; +import { isEmpty, omit } from 'lodash'; +import { useMemo } from 'react'; + +export function useFormValues( + defaultValues: Record, + node?: RAGFlowNodeType, +) { + const values = useMemo(() => { + const formData = node?.data?.form; + + if (isEmpty(formData)) { + return omit(defaultValues, 'outputs'); + } + + return omit(formData, 'outputs'); + }, [defaultValues, node?.data?.form]); + + return values; +} diff --git a/web/src/pages/agent/form/loop-form/use-watch-form-change.ts b/web/src/pages/agent/form/loop-form/use-watch-form-change.ts new file mode 100644 index 000000000..f3b707c44 --- /dev/null +++ b/web/src/pages/agent/form/loop-form/use-watch-form-change.ts @@ -0,0 +1,116 @@ +import { JsonSchemaDataType } from '@/constants/agent'; +import { buildVariableValue } from '@/utils/canvas-util'; +import { useCallback, useEffect } from 'react'; +import { UseFormReturn, useFormContext, useWatch } from 'react-hook-form'; +import { InputMode } from '../../constant'; +import { IOutputs } from '../../interface'; +import useGraphStore from '../../store'; +import { LoopFormSchemaType } from './schema'; +import { useBuildLogicalOptions } from './use-build-logical-options'; + +export function useWatchFormChange( + id?: string, + form?: UseFormReturn, +) { + let values = useWatch({ control: form?.control }); + const { replaceNodeForm } = useGraphStore((state) => state); + + useEffect(() => { + if (id) { + let nextValues = { + ...values, + outputs: values.loop_variables?.reduce((pre, cur) => { + const variable = cur.variable; + if (variable) { + pre[variable] = { + type: cur.type, + value: '', + }; + } + return pre; + }, {} as IOutputs), + }; + + replaceNodeForm(id, nextValues); + } + }, [form?.formState.isDirty, id, replaceNodeForm, values]); +} + +type ConditionPrefixType = `loop_termination_condition.${number}.`; +export type ConditionKeyType = `${ConditionPrefixType}variable`; +export type ConditionModeType = `${ConditionPrefixType}input_mode`; +export type ConditionValueType = `${ConditionPrefixType}value`; +export type ConditionOperatorType = `${ConditionPrefixType}operator`; +export function useInitializeConditions(id?: string) { + const form = useFormContext(); + const { buildLogicalOptions } = useBuildLogicalOptions(); + + const initializeConditionMode = useCallback( + (modeFieldAlias: ConditionModeType, keyType: string) => { + if (keyType === JsonSchemaDataType.Number) { + form.setValue(modeFieldAlias, InputMode.Constant, { + shouldDirty: true, + shouldValidate: true, + }); + } + }, + [form], + ); + + const initializeConditionValue = useCallback( + (valueFieldAlias: ConditionValueType, keyType: string) => { + let initialValue: string | boolean | number = ''; + + if (keyType === JsonSchemaDataType.Number) { + initialValue = 0; + } else if (keyType === JsonSchemaDataType.Boolean) { + initialValue = true; + } + + form.setValue(valueFieldAlias, initialValue, { + shouldDirty: true, + shouldValidate: true, + }); + }, + [form], + ); + + const initializeConditionOperator = useCallback( + (operatorFieldAlias: ConditionOperatorType, keyType: string) => { + const logicalOptions = buildLogicalOptions(keyType); + + form.setValue(operatorFieldAlias, logicalOptions?.at(0)?.value, { + shouldDirty: true, + shouldValidate: true, + }); + }, + [buildLogicalOptions, form], + ); + + const initializeVariableRelatedConditions = useCallback( + (variable: string, variableType: string) => { + form?.getValues('loop_termination_condition').forEach((x, idx) => { + if (variable && x.variable === buildVariableValue(variable, id)) { + const prefix: ConditionPrefixType = `loop_termination_condition.${idx}.`; + initializeConditionMode(`${prefix}input_mode`, variableType); + initializeConditionValue(`${prefix}value`, variableType); + initializeConditionOperator(`${prefix}operator`, variableType); + } + }); + }, + [ + form, + id, + initializeConditionMode, + initializeConditionOperator, + initializeConditionValue, + ], + ); + + return { + initializeVariableRelatedConditions, + initializeConditionMode, + initializeConditionValue, + initializeConditionOperator, + }; +} diff --git a/web/src/pages/agent/form/qweather-form/index.tsx b/web/src/pages/agent/form/qweather-form/index.tsx deleted file mode 100644 index eee088762..000000000 --- a/web/src/pages/agent/form/qweather-form/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from '@/components/ui/form'; -import { Input } from '@/components/ui/input'; -import { RAGFlowSelect } from '@/components/ui/select'; -import { useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { INextOperatorForm } from '../../interface'; -import { - QWeatherLangOptions, - QWeatherTimePeriodOptions, - QWeatherTypeOptions, - QWeatherUserTypeOptions, -} from '../../options'; -import { DynamicInputVariable } from '../components/next-dynamic-input-variable'; - -enum FormFieldName { - Type = 'type', - UserType = 'user_type', -} - -const QWeatherForm = ({ form, node }: INextOperatorForm) => { - const { t } = useTranslation(); - const typeValue = form.watch(FormFieldName.Type); - - const qWeatherLangOptions = useMemo(() => { - return QWeatherLangOptions.map((x) => ({ - value: x, - label: t(`flow.qWeatherLangOptions.${x}`), - })); - }, [t]); - - const qWeatherTypeOptions = useMemo(() => { - return QWeatherTypeOptions.map((x) => ({ - value: x, - label: t(`flow.qWeatherTypeOptions.${x}`), - })); - }, [t]); - - const qWeatherUserTypeOptions = useMemo(() => { - return QWeatherUserTypeOptions.map((x) => ({ - value: x, - label: t(`flow.qWeatherUserTypeOptions.${x}`), - })); - }, [t]); - - const getQWeatherTimePeriodOptions = useCallback(() => { - let options = QWeatherTimePeriodOptions; - const userType = form.getValues(FormFieldName.UserType); - if (userType === 'free') { - options = options.slice(0, 3); - } - return options.map((x) => ({ - value: x, - label: t(`flow.qWeatherTimePeriodOptions.${x}`), - })); - }, [form, t]); - - return ( -
- { - e.preventDefault(); - }} - > - - ( - - {t('flow.webApiKey')} - - - - - - )} - /> - ( - - {t('flow.lang')} - - - - - - )} - /> - ( - - {t('flow.type')} - - - - - - )} - /> - ( - - {t('flow.userType')} - - - - - - )} - /> - {typeValue === 'weather' && ( - ( - - {t('flow.timePeriod')} - - - - - - )} - /> - )} - - - ); -}; - -export default QWeatherForm; diff --git a/web/src/pages/agent/form/tool-form/constant.tsx b/web/src/pages/agent/form/tool-form/constant.tsx index fc5f4e94c..4f93ddb50 100644 --- a/web/src/pages/agent/form/tool-form/constant.tsx +++ b/web/src/pages/agent/form/tool-form/constant.tsx @@ -1,5 +1,4 @@ import { Operator } from '../../constant'; -import AkShareForm from '../akshare-form'; import ArXivForm from './arxiv-form'; import BingForm from './bing-form'; import CrawlerForm from './crawler-form'; @@ -29,7 +28,6 @@ export const ToolFormConfigMap = { [Operator.GoogleScholar]: GoogleScholarForm, [Operator.GitHub]: GithubForm, [Operator.ExeSQL]: ExeSQLForm, - [Operator.AkShare]: AkShareForm, [Operator.YahooFinance]: YahooFinanceForm, [Operator.Crawler]: CrawlerForm, [Operator.Email]: EmailForm, diff --git a/web/src/pages/agent/form/tushare-form/index.tsx b/web/src/pages/agent/form/tushare-form/index.tsx deleted file mode 100644 index a64bf25bf..000000000 --- a/web/src/pages/agent/form/tushare-form/index.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { useTranslate } from '@/hooks/common-hooks'; -import { DatePicker, DatePickerProps, Form, Input, Select } from 'antd'; -import dayjs from 'dayjs'; -import { useCallback, useMemo } from 'react'; -import { IOperatorForm } from '../../interface'; -import { TuShareSrcOptions } from '../../options'; -import DynamicInputVariable from '../components/dynamic-input-variable'; - -const DateTimePicker = ({ - onChange, - value, -}: { - onChange?: (val: number | undefined) => void; - value?: number | undefined; -}) => { - const handleChange: DatePickerProps['onChange'] = useCallback( - (val: any) => { - const nextVal = val?.format('YYYY-MM-DD HH:mm:ss'); - onChange?.(nextVal ? nextVal : undefined); - }, - [onChange], - ); - // The value needs to be converted into a string and saved to the backend - const nextValue = useMemo(() => { - if (value) { - return dayjs(value); - } - return undefined; - }, [value]); - - return ( - - ); -}; - -const TuShareForm = ({ onValuesChange, form, node }: IOperatorForm) => { - const { t } = useTranslate('flow'); - - const tuShareSrcOptions = useMemo(() => { - return TuShareSrcOptions.map((x) => ({ - value: x, - label: t(`tuShareSrcOptions.${x}`), - })); - }, [t]); - - return ( -
- - - - - - - - - - - - - - - - -
- ); -}; - -export default TuShareForm; diff --git a/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx b/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx index c41e766f2..07cc48764 100644 --- a/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx +++ b/web/src/pages/agent/gobal-variable-sheet/hooks/use-object-fields.tsx @@ -1,7 +1,7 @@ +import { BoolSegmented } from '@/components/bool-segmented'; import JsonEditor from '@/components/json-edit'; import { BlockButton, Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Segmented } from '@/components/ui/segmented'; import { t } from 'i18next'; import { isEmpty } from 'lodash'; import { Trash2, X } from 'lucide-react'; @@ -15,7 +15,7 @@ export const useObjectFields = () => { (field: FieldValues, className?: string) => { const fieldValue = field.value ? true : false; return ( - { onChange={field.onChange} className={className} itemClassName="justify-center flex-1" - > + > ); }, [], diff --git a/web/src/pages/agent/hooks/use-add-node.ts b/web/src/pages/agent/hooks/use-add-node.ts index 44091f1b1..36aae5d4f 100644 --- a/web/src/pages/agent/hooks/use-add-node.ts +++ b/web/src/pages/agent/hooks/use-add-node.ts @@ -10,7 +10,6 @@ import { NodeMap, Operator, initialAgentValues, - initialAkShareValues, initialArXivValues, initialBeginValues, initialBingValues, @@ -29,14 +28,12 @@ import { initialInvokeValues, initialIterationStartValues, initialIterationValues, - initialJin10Values, - initialKeywordExtractValues, initialListOperationsValues, + initialLoopValues, initialMessageValues, initialNoteValues, initialParserValues, initialPubMedValues, - initialQWeatherValues, initialRelevantValues, initialRetrievalValues, initialRewriteQuestionValues, @@ -47,7 +44,6 @@ import { initialTavilyExtractValues, initialTavilyValues, initialTokenizerValues, - initialTuShareValues, initialUserFillUpValues, initialVariableAggregatorValues, initialVariableAssignerValues, @@ -68,6 +64,63 @@ function isBottomSubAgent(type: string, position: Position) { type === Operator.Tool ); } + +const GroupStartNodeMap = { + [Operator.Iteration]: { + id: `${Operator.IterationStart}:${humanId()}`, + type: 'iterationStartNode', + position: { x: 50, y: 100 }, + data: { + label: Operator.IterationStart, + name: Operator.IterationStart, + form: initialIterationStartValues, + }, + extent: 'parent' as 'parent', + }, + [Operator.Loop]: { + id: `${Operator.LoopStart}:${humanId()}`, + type: 'loopStartNode', + position: { x: 50, y: 100 }, + data: { + label: Operator.LoopStart, + name: Operator.LoopStart, + form: {}, + }, + extent: 'parent' as 'parent', + }, +}; + +function useAddGroupNode() { + const { addEdge, addNode } = useGraphStore((state) => state); + + const addGroupNode = useCallback( + (operatorType: string, newNode: Node, nodeId?: string) => { + newNode.width = 500; + newNode.height = 250; + + const startNode: Node = + GroupStartNodeMap[operatorType as keyof typeof GroupStartNodeMap]; + + startNode.parentId = newNode.id; + + addNode(newNode); + addNode(startNode); + + if (nodeId) { + addEdge({ + source: nodeId, + target: newNode.id, + sourceHandle: NodeHandleId.Start, + targetHandle: NodeHandleId.End, + }); + } + return newNode.id; + }, + [addEdge, addNode], + ); + + return { addGroupNode }; +} export const useInitializeOperatorParams = () => { const llmId = useFetchModelId(); @@ -82,10 +135,6 @@ export const useInitializeOperatorParams = () => { llm_id: llmId, }, [Operator.Message]: initialMessageValues, - [Operator.KeywordExtract]: { - ...initialKeywordExtractValues, - llm_id: llmId, - }, [Operator.DuckDuckGo]: initialDuckValues, [Operator.Wikipedia]: initialWikipediaValues, [Operator.PubMed]: initialPubMedValues, @@ -95,14 +144,10 @@ export const useInitializeOperatorParams = () => { [Operator.GoogleScholar]: initialGoogleScholarValues, [Operator.SearXNG]: initialSearXNGValues, [Operator.GitHub]: initialGithubValues, - [Operator.QWeather]: initialQWeatherValues, [Operator.ExeSQL]: initialExeSqlValues, [Operator.Switch]: initialSwitchValues, [Operator.WenCai]: initialWenCaiValues, - [Operator.AkShare]: initialAkShareValues, [Operator.YahooFinance]: initialYahooFinanceValues, - [Operator.Jin10]: initialJin10Values, - [Operator.TuShare]: initialTuShareValues, [Operator.Note]: initialNoteValues, [Operator.Crawler]: initialCrawlerValues, [Operator.Invoke]: initialInvokeValues, @@ -133,6 +178,9 @@ export const useInitializeOperatorParams = () => { [Operator.ListOperations]: initialListOperationsValues, [Operator.VariableAssigner]: initialVariableAssignerValues, [Operator.VariableAggregator]: initialVariableAggregatorValues, + [Operator.Loop]: initialLoopValues, + [Operator.LoopStart]: {}, + [Operator.ExitLoop]: {}, }; }, [llmId]); @@ -311,6 +359,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { const { addChildEdge } = useAddChildEdge(); const { addToolNode } = useAddToolNode(); const { resizeIterationNode } = useResizeIterationNode(); + const { addGroupNode } = useAddGroupNode(); // const [reactFlowInstance, setReactFlowInstance] = // useState>(); @@ -376,33 +425,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { } } - if (type === Operator.Iteration) { - newNode.width = 500; - newNode.height = 250; - const iterationStartNode: Node = { - id: `${Operator.IterationStart}:${humanId()}`, - type: 'iterationStartNode', - position: { x: 50, y: 100 }, - // draggable: false, - data: { - label: Operator.IterationStart, - name: Operator.IterationStart, - form: initialIterationStartValues, - }, - parentId: newNode.id, - extent: 'parent', - }; - addNode(newNode); - addNode(iterationStartNode); - if (nodeId) { - addEdge({ - source: nodeId, - target: newNode.id, - sourceHandle: NodeHandleId.Start, - targetHandle: NodeHandleId.End, - }); - } - return newNode.id; + if ([Operator.Iteration, Operator.Loop].includes(type as Operator)) { + return addGroupNode(type, newNode, nodeId); } else if ( type === Operator.Agent && params.position === Position.Bottom @@ -456,6 +480,7 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance) { [ addChildEdge, addEdge, + addGroupNode, addNode, addToolNode, calculateNewlyBackChildPosition, diff --git a/web/src/pages/agent/hooks/use-build-options.tsx b/web/src/pages/agent/hooks/use-build-options.tsx index d1214f729..10523178b 100644 --- a/web/src/pages/agent/hooks/use-build-options.tsx +++ b/web/src/pages/agent/hooks/use-build-options.tsx @@ -1,4 +1,4 @@ -import { buildNodeOutputOptions } from '@/utils/canvas-util'; +import { buildUpstreamNodeOutputOptions } from '@/utils/canvas-util'; import { useMemo } from 'react'; import { Operator } from '../constant'; import OperatorIcon from '../operator-icon'; @@ -9,7 +9,7 @@ export function useBuildNodeOutputOptions(nodeId?: string) { const edges = useGraphStore((state) => state.edges); return useMemo(() => { - return buildNodeOutputOptions({ + return buildUpstreamNodeOutputOptions({ nodes, edges, nodeId, diff --git a/web/src/pages/agent/hooks/use-filter-child-node-ids.ts b/web/src/pages/agent/hooks/use-filter-child-node-ids.ts new file mode 100644 index 000000000..a60622a70 --- /dev/null +++ b/web/src/pages/agent/hooks/use-filter-child-node-ids.ts @@ -0,0 +1,10 @@ +import { filterChildNodeIds } from '@/utils/canvas-util'; +import useGraphStore from '../store'; + +export function useFilterChildNodeIds(nodeId?: string) { + const nodes = useGraphStore((state) => state.nodes); + + const childNodeIds = filterChildNodeIds(nodes, nodeId); + + return childNodeIds ?? []; +} diff --git a/web/src/pages/agent/hooks/use-get-begin-query.tsx b/web/src/pages/agent/hooks/use-get-begin-query.tsx index 3826f318e..c18a72029 100644 --- a/web/src/pages/agent/hooks/use-get-begin-query.tsx +++ b/web/src/pages/agent/hooks/use-get-begin-query.tsx @@ -1,15 +1,21 @@ import { AgentGlobals, AgentStructuredOutputField } from '@/constants/agent'; import { useFetchAgent } from '@/hooks/use-agent-request'; import { RAGFlowNodeType } from '@/interfaces/database/flow'; -import { buildNodeOutputOptions, isAgentStructured } from '@/utils/canvas-util'; +import { + buildNodeOutputOptions, + buildOutputOptions, + buildUpstreamNodeOutputOptions, + isAgentStructured, +} from '@/utils/canvas-util'; import { DefaultOptionType } from 'antd/es/select'; import { t } from 'i18next'; -import { isEmpty, toLower } from 'lodash'; +import { flatten, isEmpty, toLower } from 'lodash'; import get from 'lodash/get'; import { MessageSquareCode } from 'lucide-react'; import { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { AgentDialogueMode, + AgentVariableType, BeginId, BeginQueryType, JsonSchemaDataType, @@ -85,20 +91,39 @@ export const useGetBeginNodeDataQueryIsSafe = () => { return isBeginNodeDataQuerySafe; }; -export function useBuildNodeOutputOptions(nodeId?: string) { +export function useBuildUpstreamNodeOutputOptions(nodeId?: string) { const nodes = useGraphStore((state) => state.nodes); const edges = useGraphStore((state) => state.edges); return useMemo(() => { - return buildNodeOutputOptions({ + return buildUpstreamNodeOutputOptions({ nodes, edges, nodeId, - Icon: ({ name }) => , }); }, [edges, nodeId, nodes]); } +export function useBuildParentOutputOptions(parentId?: string) { + const { getNode, getOperatorTypeFromId } = useGraphStore((state) => state); + const parentNode = getNode(parentId); + + const parentType = getOperatorTypeFromId(parentId); + + if ( + parentType && + [Operator.Loop].includes(parentType as Operator) && + parentNode + ) { + const options = buildOutputOptions(parentNode); + if (options) { + return [options]; + } + } + + return []; +} + // exclude nodes with branches const ExcludedNodes = [ Operator.Categorize, @@ -120,7 +145,7 @@ function transferToVariableType(type: string) { return type; } -export function useBuildBeginVariableOptions() { +export function useBuildBeginDynamicVariableOptions() { const inputs = useSelectBeginNodeDataInputs(); const options = useMemo(() => { @@ -144,6 +169,30 @@ export function useBuildBeginVariableOptions() { const Env = 'env.'; +export function useBuildGlobalWithBeginVariableOptions() { + const { data } = useFetchAgent(); + const dynamicBeginOptions = useBuildBeginDynamicVariableOptions(); + const globals = data?.dsl?.globals ?? {}; + const globalOptions = Object.entries(globals) + .filter(([key]) => !key.startsWith(Env)) + .map(([key, value]) => ({ + label: key, + value: key, + icon: , + parentLabel: {t('flow.beginInput')}, + type: Array.isArray(value) + ? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '' : ''}` + : typeof value, + })); + + return [ + { + ...dynamicBeginOptions[0], + options: [...(dynamicBeginOptions[0]?.options ?? []), ...globalOptions], + }, + ]; +} + export function useBuildConversationVariableOptions() { const { data } = useFetchAgent(); @@ -175,55 +224,88 @@ export function useBuildConversationVariableOptions() { } export const useBuildVariableOptions = (nodeId?: string, parentId?: string) => { - const nodeOutputOptions = useBuildNodeOutputOptions(nodeId); - const parentNodeOutputOptions = useBuildNodeOutputOptions(parentId); - const beginOptions = useBuildBeginVariableOptions(); + const upstreamNodeOutputOptions = useBuildUpstreamNodeOutputOptions(nodeId); + const parentNodeOutputOptions = useBuildParentOutputOptions(parentId); + const parentUpstreamNodeOutputOptions = + useBuildUpstreamNodeOutputOptions(parentId); const options = useMemo(() => { - return [...beginOptions, ...nodeOutputOptions, ...parentNodeOutputOptions]; - }, [beginOptions, nodeOutputOptions, parentNodeOutputOptions]); + return [ + ...upstreamNodeOutputOptions, + ...parentNodeOutputOptions, + ...parentUpstreamNodeOutputOptions, + ]; + }, [ + upstreamNodeOutputOptions, + parentNodeOutputOptions, + parentUpstreamNodeOutputOptions, + ]); return options; }; -export function useBuildQueryVariableOptions(n?: RAGFlowNodeType) { - const { data } = useFetchAgent(); +export type BuildQueryVariableOptions = { + nodeIds?: string[]; + variablesExceptOperatorOutputs?: AgentVariableType[]; +}; + +export function useBuildQueryVariableOptions({ + n, + nodeIds = [], + variablesExceptOperatorOutputs, // Variables other than operator output variables +}: { + n?: RAGFlowNodeType; +} & BuildQueryVariableOptions = {}) { const node = useContext(AgentFormContext) || n; + const nodes = useGraphStore((state) => state.nodes); + const options = useBuildVariableOptions(node?.id, node?.parentId); const conversationOptions = useBuildConversationVariableOptions(); + const globalWithBeginVariableOptions = + useBuildGlobalWithBeginVariableOptions(); + + const AgentVariableOptionsMap = { + [AgentVariableType.Begin]: globalWithBeginVariableOptions, + [AgentVariableType.Conversation]: conversationOptions, + }; + const nextOptions = useMemo(() => { - const globals = data?.dsl?.globals ?? {}; - const globalOptions = Object.entries(globals) - .filter(([key]) => !key.startsWith(Env)) - .map(([key, value]) => ({ - label: key, - value: key, - icon: , - parentLabel: {t('flow.beginInput')}, - type: Array.isArray(value) - ? `${VariableType.Array}${key === AgentGlobals.SysFiles ? '' : ''}` - : typeof value, - })); + return [ + ...globalWithBeginVariableOptions, + ...conversationOptions, + ...options, + ]; + }, [conversationOptions, globalWithBeginVariableOptions, options]); + + // Which options are entirely under external control? + if (!isEmpty(nodeIds) || !isEmpty(variablesExceptOperatorOutputs)) { + const nodeOutputOptions = buildNodeOutputOptions({ nodes, nodeIds }); + + const variablesExceptOperatorOutputsOptions = + variablesExceptOperatorOutputs?.map((x) => AgentVariableOptionsMap[x]) ?? + []; return [ - { - ...options[0], - options: [...options[0]?.options, ...globalOptions], - }, - ...options.slice(1), - ...conversationOptions, + ...flatten(variablesExceptOperatorOutputsOptions), + ...nodeOutputOptions, ]; - }, [conversationOptions, data?.dsl?.globals, options]); - + } return nextOptions; } -export function useFilterQueryVariableOptionsByTypes( - types?: JsonSchemaDataType[], -) { - const nextOptions = useBuildQueryVariableOptions(); +export function useFilterQueryVariableOptionsByTypes({ + types, + nodeIds = [], + variablesExceptOperatorOutputs, +}: { + types?: JsonSchemaDataType[]; +} & BuildQueryVariableOptions) { + const nextOptions = useBuildQueryVariableOptions({ + nodeIds, + variablesExceptOperatorOutputs, + }); const filteredOptions = useMemo(() => { return !isEmpty(types) @@ -294,7 +376,7 @@ export function useBuildComponentIdAndBeginOptions( parentId?: string, ) { const componentIdOptions = useBuildComponentIdOptions(nodeId, parentId); - const beginOptions = useBuildBeginVariableOptions(); + const beginOptions = useBuildBeginDynamicVariableOptions(); return [...beginOptions, ...componentIdOptions]; } @@ -323,9 +405,19 @@ export function flatOptions(options: DefaultOptionType[]) { }, []); } -export function useFlattenQueryVariableOptions(nodeId?: string) { +export function useFlattenQueryVariableOptions({ + nodeId, + nodeIds = [], + variablesExceptOperatorOutputs, +}: { + nodeId?: string; +} & BuildQueryVariableOptions = {}) { const { getNode } = useGraphStore((state) => state); - const nextOptions = useBuildQueryVariableOptions(getNode(nodeId)); + const nextOptions = useBuildQueryVariableOptions({ + n: getNode(nodeId), + nodeIds, + variablesExceptOperatorOutputs, + }); const flattenOptions = useMemo(() => { return flatOptions(nextOptions); @@ -334,8 +426,18 @@ export function useFlattenQueryVariableOptions(nodeId?: string) { return flattenOptions; } -export function useGetVariableLabelOrTypeByValue(nodeId?: string) { - const flattenOptions = useFlattenQueryVariableOptions(nodeId); +export function useGetVariableLabelOrTypeByValue({ + nodeId, + nodeIds = [], + variablesExceptOperatorOutputs, +}: { + nodeId?: string; +} & BuildQueryVariableOptions = {}) { + const flattenOptions = useFlattenQueryVariableOptions({ + nodeId, + nodeIds, + variablesExceptOperatorOutputs, + }); const findAgentStructuredOutputTypeByValue = useFindAgentStructuredOutputTypeByValue(); const findAgentStructuredOutputLabel = diff --git a/web/src/pages/agent/hooks/use-show-drawer.tsx b/web/src/pages/agent/hooks/use-show-drawer.tsx index a350af074..8804c6a21 100644 --- a/web/src/pages/agent/hooks/use-show-drawer.tsx +++ b/web/src/pages/agent/hooks/use-show-drawer.tsx @@ -14,6 +14,7 @@ export const useShowFormDrawer = () => { setClickedNodeId, getNode, setClickedToolId, + getOperatorTypeFromId, } = useGraphStore((state) => state); const { visible: formDrawerVisible, @@ -25,14 +26,20 @@ export const useShowFormDrawer = () => { (e: React.MouseEvent, nodeId: string) => { const tool = get(e.target, 'dataset.tool'); // TODO: Operator type judgment should be used - if (nodeId.startsWith(Operator.Tool) && !tool) { + const operatorType = getOperatorTypeFromId(nodeId); + if ( + (operatorType === Operator.Tool && !tool) || + [Operator.LoopStart, Operator.ExitLoop].includes( + operatorType as Operator, + ) + ) { return; } setClickedNodeId(nodeId); setClickedToolId(tool); showFormDrawer(); }, - [setClickedNodeId, setClickedToolId, showFormDrawer], + [getOperatorTypeFromId, setClickedNodeId, setClickedToolId, showFormDrawer], ); return { diff --git a/web/src/pages/agent/interface.ts b/web/src/pages/agent/interface.ts index 0a6cba2f0..ed823535b 100644 --- a/web/src/pages/agent/interface.ts +++ b/web/src/pages/agent/interface.ts @@ -41,3 +41,11 @@ export type IInputs = { prologue: string; mode: string; }; + +export type IOutputs = Record< + string, + { + type?: string; + value?: string; + } +>; diff --git a/web/src/pages/agent/operator-icon.tsx b/web/src/pages/agent/operator-icon.tsx index bca93d7fa..b390507a4 100644 --- a/web/src/pages/agent/operator-icon.tsx +++ b/web/src/pages/agent/operator-icon.tsx @@ -14,7 +14,12 @@ import { ReactComponent as YahooFinanceIcon } from '@/assets/svg/yahoo-finance.s import { IconFontFill } from '@/components/icon-font'; import { cn } from '@/lib/utils'; -import { FileCode, HousePlus } from 'lucide-react'; +import { + FileCode, + HousePlus, + Infinity as InfinityIcon, + LogOut, +} from 'lucide-react'; import { Operator } from './constant'; interface IProps { @@ -60,6 +65,8 @@ export const SVGIconMap = { }; export const LucideIconMap = { [Operator.DataOperations]: FileCode, + [Operator.Loop]: InfinityIcon, + [Operator.ExitLoop]: LogOut, }; const Empty = () => { diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index 6ae2935b4..da2fbf267 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -8,7 +8,7 @@ import { } from '@/interfaces/database/agent'; import { DSLComponents, RAGFlowNodeType } from '@/interfaces/database/flow'; import { buildSelectOptions } from '@/utils/component-util'; -import { removeUselessFieldsFromValues } from '@/utils/form'; +import { buildOptions, removeUselessFieldsFromValues } from '@/utils/form'; import { Edge, Node, XYPosition } from '@xyflow/react'; import { FormInstance, FormListFieldData } from 'antd'; import { humanId } from 'human-id'; @@ -27,6 +27,7 @@ import { CategorizeAnchorPointPositions, FileType, FileTypeSuffixMap, + InputMode, NoCopyOperatorsList, NoDebugOperatorsList, NodeHandleId, @@ -772,3 +773,5 @@ export function getArrayElementType(type: string) { export function buildConversationVariableSelectOptions() { return buildSelectOptions(Object.values(TypesWithArray)); } + +export const InputModeOptions = buildOptions(InputMode); diff --git a/web/src/utils/canvas-util.tsx b/web/src/utils/canvas-util.tsx index d55862d3e..d3b825607 100644 --- a/web/src/utils/canvas-util.tsx +++ b/web/src/utils/canvas-util.tsx @@ -4,10 +4,11 @@ import { Operator, } from '@/constants/agent'; import { BaseNode } from '@/interfaces/database/agent'; +import OperatorIcon from '@/pages/agent/operator-icon'; import { Edge } from '@xyflow/react'; import { get, isEmpty } from 'lodash'; -import { ComponentType, ReactNode } from 'react'; +import { ReactNode } from 'react'; export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { return nodeIds.reduce((pre, nodeId) => { @@ -29,13 +30,21 @@ export function filterAllUpstreamNodeIds(edges: Edge[], nodeIds: string[]) { }, []); } +export function filterChildNodeIds(nodes: BaseNode[], nodeId?: string) { + return nodes.filter((x) => x.parentId === nodeId).map((x) => x.id); +} + export function isAgentStructured(id?: string, label?: string) { return ( label === AgentStructuredOutputField && id?.startsWith(`${Operator.Agent}:`) ); } -export function buildOutputOptions( +export function buildVariableValue(value: string, nodeId?: string) { + return `${nodeId}@${value}`; +} + +export function buildSecondaryOutputOptions( outputs: Record = {}, nodeId?: string, parentLabel?: string | ReactNode, @@ -43,7 +52,7 @@ export function buildOutputOptions( ) { return Object.keys(outputs).map((x) => ({ label: x, - value: `${nodeId}@${x}`, + value: buildVariableValue(x, nodeId), parentLabel, icon, type: isAgentStructured(nodeId, x) @@ -52,40 +61,63 @@ export function buildOutputOptions( })); } +export function buildOutputOptions(x: BaseNode) { + return { + label: x.data.name, + value: x.id, + title: x.data.name, + options: buildSecondaryOutputOptions( + x.data.form.outputs, + x.id, + x.data.name, + , + ), + }; +} + export function buildNodeOutputOptions({ + nodes, // all nodes + nodeIds, // Need to obtain the output node IDs +}: { + nodes: BaseNode[]; + nodeIds: string[]; +}) { + const nodeWithOutputList = nodes.filter( + (x) => nodeIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs), + ); + + return nodeWithOutputList.map((x) => buildOutputOptions(x)); +} + +export function buildUpstreamNodeOutputOptions({ nodes, edges, nodeId, - Icon, }: { nodes: BaseNode[]; edges: Edge[]; nodeId?: string; - Icon: ComponentType<{ name: string }>; }) { if (!nodeId) { return []; } const upstreamIds = filterAllUpstreamNodeIds(edges, [nodeId]); + return buildNodeOutputOptions({ nodes, nodeIds: upstreamIds }); +} + +export function buildChildOutputOptions({ + nodes, + nodeId, +}: { + nodes: BaseNode[]; + nodeId?: string; +}) { const nodeWithOutputList = nodes.filter( - (x) => - upstreamIds.some((y) => y === x.id) && !isEmpty(x.data?.form?.outputs), + (x) => x.parentId === nodeId && !isEmpty(x.data?.form?.outputs), ); - return nodeWithOutputList - .filter((x) => x.id !== nodeId) - .map((x) => ({ - label: x.data.name, - value: x.id, - title: x.data.name, - options: buildOutputOptions( - x.data.form.outputs, - x.id, - x.data.name, - , - ), - })); + return nodeWithOutputList.map((x) => buildOutputOptions(x)); } export function getStructuredDatatype(value: Record | unknown) {