### What problem does this PR solve? Feat: Add IterationNode component #3221 ### Type of change - [x] New Feature (non-breaking change which adds functionality)tags/v0.20.0
| @@ -3,6 +3,7 @@ | |||
| height: 100%; | |||
| :global(.react-flow__node-group) { | |||
| .commonNode(); | |||
| border-radius: 0 0 10px 10px; | |||
| padding: 0; | |||
| border: 0; | |||
| background-color: transparent; | |||
| @@ -1,15 +1,17 @@ | |||
| import { useTheme } from '@/components/theme-provider'; | |||
| import { | |||
| IIterationNode, | |||
| IIterationStartNode, | |||
| } from '@/interfaces/database/flow'; | |||
| import { cn } from '@/lib/utils'; | |||
| import { Handle, NodeProps, NodeResizeControl, Position } from '@xyflow/react'; | |||
| import { ListRestart } from 'lucide-react'; | |||
| import { NodeProps, NodeResizeControl, Position } from '@xyflow/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 NodeHeader from './node-header'; | |||
| import { NodeWrapper } from './node-wrapper'; | |||
| function ResizeIcon() { | |||
| return ( | |||
| @@ -50,47 +52,43 @@ export function InnerIterationNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| }: NodeProps<IIterationNode>) { | |||
| const { theme } = useTheme(); | |||
| // const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={cn( | |||
| 'w-full h-full bg-zinc-200 opacity-70', | |||
| styles.iterationNode, | |||
| { | |||
| ['bg-gray-800']: theme === 'dark', | |||
| [styles.selectedIterationNode]: selected, | |||
| }, | |||
| )} | |||
| className={cn('h-full bg-transparent rounded-b-md', { | |||
| [styles.selectedHeader]: selected, | |||
| })} | |||
| > | |||
| <NodeResizeControl style={controlStyle} minWidth={100} minHeight={50}> | |||
| <ResizeIcon /> | |||
| </NodeResizeControl> | |||
| <Handle | |||
| id="c" | |||
| type="source" | |||
| <CommonHandle | |||
| id={NodeHandleId.End} | |||
| type="target" | |||
| position={Position.Left} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={LeftHandleStyle} | |||
| ></Handle> | |||
| <Handle | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <CommonHandle | |||
| id={NodeHandleId.Start} | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| id="b" | |||
| style={RightHandleStyle} | |||
| ></Handle> | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <NodeHeader | |||
| id={id} | |||
| name={data.name} | |||
| label={data.label} | |||
| wrapperClassName={cn( | |||
| 'p-2 bg-white rounded-t-[10px] absolute w-full top-[-60px] left-[-0.3px]', | |||
| styles.iterationHeader, | |||
| 'bg-background-header-bar p-2 rounded-t-[10px] absolute w-full top-[-44px] left-[-0.3px]', | |||
| // styles.iterationHe ader, | |||
| { | |||
| [`${styles.dark} text-white`]: theme === 'dark', | |||
| // [`${styles.dark} text-white`]: theme === 'dark', | |||
| [styles.selectedHeader]: selected, | |||
| }, | |||
| )} | |||
| @@ -101,29 +99,24 @@ export function InnerIterationNode({ | |||
| function InnerIterationStartNode({ | |||
| isConnectable = true, | |||
| selected, | |||
| id, | |||
| }: NodeProps<IIterationStartNode>) { | |||
| const { theme } = useTheme(); | |||
| return ( | |||
| <section | |||
| className={cn('bg-white p-2 rounded-xl', { | |||
| [styles.dark]: theme === 'dark', | |||
| [styles.selectedNode]: selected, | |||
| })} | |||
| > | |||
| <Handle | |||
| <NodeWrapper className="w-20"> | |||
| <CommonHandle | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable={isConnectable} | |||
| className={styles.handle} | |||
| style={RightHandleStyle} | |||
| isConnectableEnd={false} | |||
| ></Handle> | |||
| id={NodeHandleId.Start} | |||
| nodeId={id} | |||
| ></CommonHandle> | |||
| <div> | |||
| <ListRestart className="size-7" /> | |||
| <OperatorIcon name={Operator.Begin}></OperatorIcon> | |||
| </div> | |||
| </section> | |||
| </NodeWrapper> | |||
| ); | |||
| } | |||
| @@ -644,7 +644,7 @@ export const initialEmailValues = { | |||
| }; | |||
| export const initialIterationValues = { | |||
| delimiter: ',', | |||
| items_ref: '', | |||
| }; | |||
| export const initialIterationStartValues = {}; | |||
| @@ -665,6 +665,7 @@ export const initialWaitingDialogueValues = {}; | |||
| export const initialAgentValues = { | |||
| ...initialLlmBaseValues, | |||
| description: '', | |||
| sys_prompt: ``, | |||
| prompts: [{ role: PromptRole.User, content: `{${AgentGlobals.SysQuery}}` }], | |||
| message_history_window_size: 12, | |||
| @@ -23,7 +23,7 @@ import GithubForm from '../form/github-form'; | |||
| import GoogleForm from '../form/google-form'; | |||
| import GoogleScholarForm from '../form/google-scholar-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 KeywordExtractForm from '../form/keyword-extract-form'; | |||
| import MessageForm from '../form/message-form'; | |||
| @@ -10,15 +10,17 @@ import { | |||
| FormItem, | |||
| FormLabel, | |||
| } from '@/components/ui/form'; | |||
| import { Textarea } from '@/components/ui/textarea'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { Position } from '@xyflow/react'; | |||
| import { useContext, useMemo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { Operator, initialAgentValues } from '../../constant'; | |||
| import { NodeHandleId, Operator, initialAgentValues } from '../../constant'; | |||
| import { AgentInstanceContext } from '../../context'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import useGraphStore from '../../store'; | |||
| import { Output } from '../components/output'; | |||
| import { PromptEditor } from '../components/prompt-editor'; | |||
| import { AgentTools } from './agent-tools'; | |||
| @@ -27,6 +29,7 @@ import { useWatchFormChange } from './use-watch-change'; | |||
| const FormSchema = z.object({ | |||
| sys_prompt: z.string(), | |||
| description: z.string().optional(), | |||
| prompts: z.string().optional(), | |||
| // prompts: z | |||
| // .array( | |||
| @@ -49,9 +52,17 @@ const FormSchema = z.object({ | |||
| const AgentForm = ({ node }: INextOperatorForm) => { | |||
| const { t } = useTranslation(); | |||
| const { edges } = useGraphStore((state) => state); | |||
| 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(() => { | |||
| return [ | |||
| { title: 'content', type: initialAgentValues.outputs.content.type }, | |||
| @@ -76,6 +87,20 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| }} | |||
| > | |||
| <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> | |||
| <FormField | |||
| control={form.control} | |||
| @@ -95,23 +120,28 @@ const AgentForm = ({ node }: INextOperatorForm) => { | |||
| /> | |||
| <MessageHistoryWindowSizeFormField></MessageHistoryWindowSizeFormField> | |||
| </FormContainer> | |||
| <FormContainer> | |||
| {/* <DynamicPrompt></DynamicPrompt> */} | |||
| <FormField | |||
| control={form.control} | |||
| name={`prompts`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <FormLabel>User Prompt</FormLabel> | |||
| <FormControl> | |||
| <section> | |||
| <PromptEditor {...field} showToolbar={false}></PromptEditor> | |||
| </section> | |||
| </FormControl> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </FormContainer> | |||
| {isSubAgent || ( | |||
| <FormContainer> | |||
| {/* <DynamicPrompt></DynamicPrompt> */} | |||
| <FormField | |||
| control={form.control} | |||
| name={`prompts`} | |||
| render={({ field }) => ( | |||
| <FormItem className="flex-1"> | |||
| <FormLabel>User Prompt</FormLabel> | |||
| <FormControl> | |||
| <section> | |||
| <PromptEditor | |||
| {...field} | |||
| showToolbar={false} | |||
| ></PromptEditor> | |||
| </section> | |||
| </FormControl> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| </FormContainer> | |||
| )} | |||
| <FormContainer> | |||
| <AgentTools></AgentTools> | |||
| <BlockButton | |||
| @@ -10,7 +10,9 @@ import { useFormContext } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| 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 form = useFormContext(); | |||
| @@ -19,7 +21,7 @@ export function QueryVariable() { | |||
| return ( | |||
| <FormField | |||
| control={form.control} | |||
| name="query" | |||
| name={name} | |||
| render={({ field }) => ( | |||
| <FormItem> | |||
| <FormLabel tooltip={t('chat.modelTip')}>{t('flow.query')}</FormLabel> | |||
| @@ -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; | |||
| @@ -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; | |||
| } | |||
| @@ -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; | |||
| @@ -51,7 +51,6 @@ import useGraphStore from '../store'; | |||
| import { | |||
| generateNodeNamesWithIncreasingIndex, | |||
| getNodeDragHandle, | |||
| getRelativePositionToIterationNode, | |||
| } from '../utils'; | |||
| export const useInitializeOperatorParams = () => { | |||
| @@ -234,11 +233,9 @@ function useAddToolNode() { | |||
| } | |||
| export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| const addNode = useGraphStore((state) => state.addNode); | |||
| const getNode = useGraphStore((state) => state.getNode); | |||
| const addEdge = useGraphStore((state) => state.addEdge); | |||
| const nodes = useGraphStore((state) => state.nodes); | |||
| const edges = useGraphStore((state) => state.edges); | |||
| const { edges, nodes, addEdge, addNode, getNode } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| const getNodeName = useGetNodeName(); | |||
| const initializeOperatorParams = useInitializeOperatorParams(); | |||
| const { calculateNewlyBackChildPosition } = useCalculateNewlyChildPosition(); | |||
| @@ -257,6 +254,8 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| (event?: React.MouseEvent<HTMLElement>) => { | |||
| const nodeId = params.nodeId; | |||
| const node = getNode(nodeId); | |||
| // reactFlowInstance.project was renamed to reactFlowInstance.screenToFlowPosition | |||
| // and you don't need to subtract the reactFlowBounds.left/top anymore | |||
| // details: https://@xyflow/react.dev/whats-new/2023-11-10 | |||
| @@ -289,6 +288,11 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| dragHandle: getNodeDragHandle(type), | |||
| }; | |||
| if (node && node.parentId) { | |||
| newNode.parentId = node.parentId; | |||
| newNode.extent = 'parent'; | |||
| } | |||
| if (type === Operator.Iteration) { | |||
| newNode.width = 500; | |||
| newNode.height = 250; | |||
| @@ -307,6 +311,14 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| }; | |||
| addNode(newNode); | |||
| addNode(iterationStartNode); | |||
| if (nodeId) { | |||
| addEdge({ | |||
| source: nodeId, | |||
| target: newNode.id, | |||
| sourceHandle: NodeHandleId.Start, | |||
| targetHandle: NodeHandleId.End, | |||
| }); | |||
| } | |||
| } else if ( | |||
| type === Operator.Agent && | |||
| params.position === Position.Bottom | |||
| @@ -345,15 +357,6 @@ export function useAddNode(reactFlowInstance?: ReactFlowInstance<any, any>) { | |||
| } else if (type === Operator.Tool) { | |||
| addToolNode(newNode, params.nodeId); | |||
| } else { | |||
| const subNodeOfIteration = getRelativePositionToIterationNode( | |||
| nodes, | |||
| position, | |||
| ); | |||
| if (subNodeOfIteration) { | |||
| newNode.parentId = subNodeOfIteration.parentId; | |||
| newNode.position = subNodeOfIteration.position; | |||
| newNode.extent = 'parent'; | |||
| } | |||
| addNode(newNode); | |||
| addChildEdge(params.position, { | |||
| source: params.nodeId, | |||