Feat: Add IterationNode component #3221 (#8461)

### What problem does this PR solve?

Feat: Add IterationNode component #3221
### Type of change


- [x] New Feature (non-breaking change which adds functionality)
This commit is contained in:
balibabu 2025-06-24 18:01:30 +08:00 committed by GitHub
parent bc1b837616
commit 1c68c9ebd6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 191 additions and 169 deletions

View File

@ -3,6 +3,7 @@
height: 100%; height: 100%;
:global(.react-flow__node-group) { :global(.react-flow__node-group) {
.commonNode(); .commonNode();
border-radius: 0 0 10px 10px;
padding: 0; padding: 0;
border: 0; border: 0;
background-color: transparent; background-color: transparent;

View File

@ -1,15 +1,17 @@
import { useTheme } from '@/components/theme-provider';
import { import {
IIterationNode, IIterationNode,
IIterationStartNode, IIterationStartNode,
} from '@/interfaces/database/flow'; } from '@/interfaces/database/flow';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react'; import { NodeProps, NodeResizeControl, Position } from '@xyflow/react';
import { ListRestart } from 'lucide-react';
import { memo } from 'react'; import { memo } from 'react';
import { LeftHandleStyle, RightHandleStyle } from './handle-icon'; import { NodeHandleId, Operator } from '../../constant';
import OperatorIcon from '../../operator-icon';
import { CommonHandle } from './handle';
import { RightHandleStyle } from './handle-icon';
import styles from './index.less'; import styles from './index.less';
import NodeHeader from './node-header'; import NodeHeader from './node-header';
import { NodeWrapper } from './node-wrapper';
function ResizeIcon() { function ResizeIcon() {
return ( return (
@ -50,47 +52,43 @@ export function InnerIterationNode({
isConnectable = true, isConnectable = true,
selected, selected,
}: NodeProps<IIterationNode>) { }: NodeProps<IIterationNode>) {
const { theme } = useTheme(); // const { theme } = useTheme();
return ( return (
<section <section
className={cn( className={cn('h-full bg-transparent rounded-b-md', {
'w-full h-full bg-zinc-200 opacity-70', [styles.selectedHeader]: selected,
styles.iterationNode, })}
{
['bg-gray-800']: theme === 'dark',
[styles.selectedIterationNode]: selected,
},
)}
> >
<NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}> <NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}>
<ResizeIcon /> <ResizeIcon />
</NodeResizeControl> </NodeResizeControl>
<Handle <CommonHandle
id="c" id={NodeHandleId.End}
type="source" type="target"
position={Position.Left} position={Position.Left}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
style={LeftHandleStyle} nodeId={id}
></Handle> ></CommonHandle>
<Handle <CommonHandle
id={NodeHandleId.Start}
type="source" type="source"
position={Position.Right} position={Position.Right}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
id="b" nodeId={id}
style={RightHandleStyle} ></CommonHandle>
></Handle>
<NodeHeader <NodeHeader
id={id} id={id}
name={data.name} name={data.name}
label={data.label} label={data.label}
wrapperClassName={cn( wrapperClassName={cn(
'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]', 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]',
styles.iterationHeader, // styles.iterationHe ader,
{ {
[`${styles.dark} text-white`]: theme === 'dark', // [`${styles.dark} text-white`]: theme === 'dark',
[styles.selectedHeader]: selected, [styles.selectedHeader]: selected,
}, },
)} )}
@ -101,29 +99,24 @@ export function InnerIterationNode({
function InnerIterationStartNode({ function InnerIterationStartNode({
isConnectable = true, isConnectable = true,
selected, id,
}: NodeProps<IIterationStartNode>) { }: NodeProps<IIterationStartNode>) {
const { theme } = useTheme();
return ( return (
<section <NodeWrapper className="w-20">
className={cn('bg-white p-2 rounded-xl', { <CommonHandle
[styles.dark]: theme === 'dark',
[styles.selectedNode]: selected,
})}
>
<Handle
type="source" type="source"
position={Position.Right} position={Position.Right}
isConnectable={isConnectable} isConnectable={isConnectable}
className={styles.handle} className={styles.handle}
style={RightHandleStyle} style={RightHandleStyle}
isConnectableEnd={false} isConnectableEnd={false}
></Handle> id={NodeHandleId.Start}
nodeId={id}
></CommonHandle>
<div> <div>
<ListRestart className="size-7" /> <OperatorIcon name={Operator.Begin}></OperatorIcon>
</div> </div>
</section> </NodeWrapper>
); );
} }

View File

@ -644,7 +644,7 @@ export const initialEmailValues = {
}; };
export const initialIterationValues = { export const initialIterationValues = {
delimiter: ',', items_ref: '',
}; };
export const initialIterationStartValues = {}; export const initialIterationStartValues = {};
@ -665,6 +665,7 @@ export const initialWaitingDialogueValues = {};
export const initialAgentValues = { export const initialAgentValues = {
...initialLlmBaseValues, ...initialLlmBaseValues,
description: '',
sys_prompt: ``, sys_prompt: ``,
prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }], prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }],
message_history_window_size: 12, message_history_window_size: 12,

View File

@ -23,7 +23,7 @@ import GithubForm from '../form/github-form';
import GoogleForm from '../form/google-form'; import GoogleForm from '../form/google-form';
import GoogleScholarForm from '../form/google-scholar-form'; import GoogleScholarForm from '../form/google-scholar-form';
import InvokeForm from '../form/invoke-form'; import InvokeForm from '../form/invoke-form';
import IterationForm from '../form/iteration-from'; import IterationForm from '../form/iteration-form';
import Jin10Form from '../form/jin10-form'; import Jin10Form from '../form/jin10-form';
import KeywordExtractForm from '../form/keyword-extract-form'; import KeywordExtractForm from '../form/keyword-extract-form';
import MessageForm from '../form/message-form'; import MessageForm from '../form/message-form';

View File

@ -10,15 +10,17 @@ import {
FormItem, FormItem,
FormLabel, FormLabel,
} from '@/components/ui/form'; } from '@/components/ui/form';
import { Textarea } from '@/components/ui/textarea';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { Position } from '@xyflow/react'; import { Position } from '@xyflow/react';
import { useContext, useMemo } from 'react'; import { useContext, useMemo } from 'react';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { z } from 'zod'; import { z } from 'zod';
import { Operator, initialAgentValues } from '../../constant'; import { NodeHandleId, Operator, initialAgentValues } from '../../constant';
import { AgentInstanceContext } from '../../context'; import { AgentInstanceContext } from '../../context';
import { INextOperatorForm } from '../../interface'; import { INextOperatorForm } from '../../interface';
import useGraphStore from '../../store';
import { Output } from '../components/output'; import { Output } from '../components/output';
import { PromptEditor } from '../components/prompt-editor'; import { PromptEditor } from '../components/prompt-editor';
import { AgentTools } from './agent-tools'; import { AgentTools } from './agent-tools';
@ -27,6 +29,7 @@ import { useWatchFormChange } from './use-watch-change';
const FormSchema = z.object({ const FormSchema = z.object({
sys_prompt: z.string(), sys_prompt: z.string(),
description: z.string().optional(),
prompts: z.string().optional(), prompts: z.string().optional(),
// prompts: z // prompts: z
// .array( // .array(
@ -49,9 +52,17 @@ const FormSchema = z.object({
const AgentForm = ({ node }: INextOperatorForm) => { const AgentForm = ({ node }: INextOperatorForm) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { edges } = useGraphStore((state) => state);
const defaultValues = useValues(node); const defaultValues = useValues(node);
const isSubAgent = useMemo(() => {
const edge = edges.find(
(x) => x.target === node?.id && x.targetHandle === NodeHandleId.AgentTop,
);
return !!edge;
}, [edges, node?.id]);
const outputList = useMemo(() => { const outputList = useMemo(() => {
return [ return [
{ title: 'content', type: initialAgentValues.outputs.content.type }, { title: 'content', type: initialAgentValues.outputs.content.type },
@ -76,6 +87,20 @@ const AgentForm = ({ node }: INextOperatorForm) => {
}} }}
> >
<FormContainer> <FormContainer>
{isSubAgent && (
<FormField
control={form.control}
name={`description`}
render={({ field }) => (
<FormItem className="flex-1">
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea {...field}></Textarea>
</FormControl>
</FormItem>
)}
/>
)}
<LargeModelFormField></LargeModelFormField> <LargeModelFormField></LargeModelFormField>
<FormField <FormField
control={form.control} control={form.control}
@ -95,23 +120,28 @@ const AgentForm = ({ node }: INextOperatorForm) => {
/> />
<MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField>
</FormContainer> </FormContainer>
<FormContainer> {isSubAgent || (
{/* <DynamicPrompt></DynamicPrompt> */} <FormContainer>
<FormField {/* <DynamicPrompt></DynamicPrompt> */}
control={form.control} <FormField
name={`prompts`} control={form.control}
render={({ field }) => ( name={`prompts`}
<FormItem className="flex-1"> render={({ field }) => (
<FormLabel>User Prompt</FormLabel> <FormItem className="flex-1">
<FormControl> <FormLabel>User Prompt</FormLabel>
<section> <FormControl>
<PromptEditor {...field} showToolbar={false}></PromptEditor> <section>
</section> <PromptEditor
</FormControl> {...field}
</FormItem> showToolbar={false}
)} ></PromptEditor>
/> </section>
</FormContainer> </FormControl>
</FormItem>
)}
/>
</FormContainer>
)}
<FormContainer> <FormContainer>
<AgentTools></AgentTools> <AgentTools></AgentTools>
<BlockButton <BlockButton

View File

@ -10,7 +10,9 @@ import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query'; import { useBuildQueryVariableOptions } from '../../hooks/use-get-begin-query';
export function QueryVariable() { type QueryVariableProps = { name?: string };
export function QueryVariable({ name = 'query' }: QueryVariableProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useFormContext(); const form = useFormContext();
@ -19,7 +21,7 @@ export function QueryVariable() {
return ( return (
<FormField <FormField
control={form.control} control={form.control}
name="query" name={name}
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel tooltip={t('chat.modelTip')}>{t('flow.query')}</FormLabel> <FormLabel tooltip={t('chat.modelTip')}>{t('flow.query')}</FormLabel>

View File

@ -0,0 +1,61 @@
import { FormContainer } from '@/components/form-container';
import { Form } from '@/components/ui/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { useMemo } from 'react';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { initialRetrievalValues } from '../../constant';
import { useWatchFormChange } from '../../hooks/use-watch-form-change';
import { INextOperatorForm } from '../../interface';
import { Output } from '../components/output';
import { QueryVariable } from '../components/query-variable';
import { useValues } from './use-values';
const FormSchema = z.object({
query: z.string().optional(),
similarity_threshold: z.coerce.number(),
keywords_similarity_weight: z.coerce.number(),
top_n: z.coerce.number(),
top_k: z.coerce.number(),
kb_ids: z.array(z.string()),
rerank_id: z.string(),
empty_response: z.string(),
});
const IterationForm = ({ node }: INextOperatorForm) => {
const outputList = useMemo(() => {
return [
{
title: 'formalized_content',
type: initialRetrievalValues.outputs.formalized_content.type,
},
];
}, []);
const defaultValues = useValues(node);
const form = useForm({
defaultValues: defaultValues,
resolver: zodResolver(FormSchema),
});
useWatchFormChange(node?.id, form);
return (
<Form {...form}>
<form
className="space-y-6 p-4"
onSubmit={(e) => {
e.preventDefault();
}}
>
<FormContainer>
<QueryVariable name="items_ref"></QueryVariable>
</FormContainer>
<Output list={outputList}></Output>
</form>
</Form>
);
};
export default IterationForm;

View File

@ -0,0 +1,25 @@
import { RAGFlowNodeType } from '@/interfaces/database/flow';
import { isEmpty } from 'lodash';
import { useMemo } from 'react';
import { initialIterationValues } from '../../constant';
export function useValues(node?: RAGFlowNodeType) {
const defaultValues = useMemo(
() => ({
...initialIterationValues,
}),
[],
);
const values = useMemo(() => {
const formData = node?.data?.form;
if (isEmpty(formData)) {
return defaultValues;
}
return formData;
}, [defaultValues, node?.data?.form]);
return values;
}

View File

@ -1,94 +0,0 @@
import { CommaIcon, SemicolonIcon } from '@/assets/icon/Icon';
import { Form, Select } from 'antd';
import {
CornerDownLeft,
IndentIncrease,
Minus,
Slash,
Underline,
} from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { IOperatorForm } from '../../interface';
import DynamicInputVariable from '../components/dynamic-input-variable';
const optionList = [
{
value: ',',
icon: CommaIcon,
text: 'comma',
},
{
value: '\n',
icon: CornerDownLeft,
text: 'lineBreak',
},
{
value: 'tab',
icon: IndentIncrease,
text: 'tab',
},
{
value: '_',
icon: Underline,
text: 'underline',
},
{
value: '/',
icon: Slash,
text: 'diagonal',
},
{
value: '-',
icon: Minus,
text: 'minus',
},
{
value: ';',
icon: SemicolonIcon,
text: 'semicolon',
},
];
const IterationForm = ({ onValuesChange, form, node }: IOperatorForm) => {
const { t } = useTranslation();
const options = useMemo(() => {
return optionList.map((x) => {
let Icon = x.icon;
return {
value: x.value,
label: (
<div className="flex items-center gap-2">
<Icon className={'size-4'}></Icon>
{t(`flow.delimiterOptions.${x.text}`)}
</div>
),
};
});
}, [t]);
return (
<Form
name="basic"
autoComplete="off"
form={form}
onValuesChange={onValuesChange}
layout={'vertical'}
>
<DynamicInputVariable node={node}></DynamicInputVariable>
<Form.Item
name={['delimiter']}
label={t('knowledgeDetails.delimiter')}
initialValue={`\\n!?;。;!?`}
rules={[{ required: true }]}
tooltip={t('flow.delimiterTip')}
>
<Select options={options}></Select>
</Form.Item>
</Form>
);
};
export default IterationForm;

View File

@ -51,7 +51,6 @@ import useGraphStore from '../store';
import { import {
generateNodeNamesWithIncreasingIndex, generateNodeNamesWithIncreasingIndex,
getNodeDragHandle, getNodeDragHandle,
getRelativePositionToIterationNode,
} from '../utils'; } from '../utils';
export const useInitializeOperatorParams = () => { export const useInitializeOperatorParams = () => {
@ -234,11 +233,9 @@ function useAddToolNode() {
} }
export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
const addNode = useGraphStore((state) => state.addNode); const { edges, nodes, addEdge, addNode, getNode } = useGraphStore(
const getNode = useGraphStore((state) => state.getNode); (state) => state,
const addEdge = useGraphStore((state) => state.addEdge); );
const nodes = useGraphStore((state) => state.nodes);
const edges = useGraphStore((state) => state.edges);
const getNodeName = useGetNodeName(); const getNodeName = useGetNodeName();
const initializeOperatorParams = useInitializeOperatorParams(); const initializeOperatorParams = useInitializeOperatorParams();
const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition();
@ -257,6 +254,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
(event?: React.MouseEvent<HTMLElement>) => { (event?: React.MouseEvent<HTMLElement>) => {
const nodeId = params.nodeId; const nodeId = params.nodeId;
const node = getNode(nodeId);
// reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition
// and you don't need to subtract the reactFlowBounds.left/top anymore // and you don't need to subtract the reactFlowBounds.left/top anymore
// details: https://@xyflow/react.dev/whats-new/2023-11-10 // details: https://@xyflow/react.dev/whats-new/2023-11-10
@ -289,6 +288,11 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
dragHandle: getNodeDragHandle(type), dragHandle: getNodeDragHandle(type),
}; };
if (node && node.parentId) {
newNode.parentId = node.parentId;
newNode.extent = 'parent';
}
if (type === Operator.Iteration) { if (type === Operator.Iteration) {
newNode.width = 500; newNode.width = 500;
newNode.height = 250; newNode.height = 250;
@ -307,6 +311,14 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
}; };
addNode(newNode); addNode(newNode);
addNode(iterationStartNode); addNode(iterationStartNode);
if (nodeId) {
addEdge({
source: nodeId,
target: newNode.id,
sourceHandle: NodeHandleId.Start,
targetHandle: NodeHandleId.End,
});
}
} else if ( } else if (
type === Operator.Agent && type === Operator.Agent &&
params.position === Position.Bottom params.position === Position.Bottom
@ -345,15 +357,6 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) {
} else if (type === Operator.Tool) { } else if (type === Operator.Tool) {
addToolNode(newNode, params.nodeId); addToolNode(newNode, params.nodeId);
} else { } else {
const subNodeOfIteration = getRelativePositionToIterationNode(
nodes,
position,
);
if (subNodeOfIteration) {
newNode.parentId = subNodeOfIteration.parentId;
newNode.position = subNodeOfIteration.position;
newNode.extent = 'parent';
}
addNode(newNode); addNode(newNode);
addChildEdge(params.position, { addChildEdge(params.position, {
source: params.nodeId, source: params.nodeId,