### What problem does this PR solve? Fix: Fixed the issue that the condition of deleting the classification operator cannot be connected anymore #3221 ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue)tags/v0.20.0
| @@ -4,11 +4,12 @@ export interface ICategorizeItem { | |||
| examples?: { value: string }[]; | |||
| index: number; | |||
| to: string[]; | |||
| uuid: string; | |||
| } | |||
| export type ICategorizeItemResult = Record< | |||
| string, | |||
| Omit<ICategorizeItem, 'name' | 'examples'> & { examples: string[] } | |||
| Omit<ICategorizeItem, 'name' | 'examples' | 'uuid'> & { examples: string[] } | |||
| >; | |||
| export interface ISwitchCondition { | |||
| @@ -101,6 +102,7 @@ export interface IGenerateForm { | |||
| export interface ICategorizeForm extends IGenerateForm { | |||
| category_description: ICategorizeItemResult; | |||
| items: ICategorizeItem[]; | |||
| } | |||
| export interface IRelevantForm extends IGenerateForm { | |||
| @@ -5,6 +5,7 @@ import { | |||
| EdgeProps, | |||
| getBezierPath, | |||
| } from '@xyflow/react'; | |||
| import { memo } from 'react'; | |||
| import useGraphStore from '../../store'; | |||
| import { useFetchAgent } from '@/hooks/use-agent-request'; | |||
| @@ -12,7 +13,7 @@ import { cn } from '@/lib/utils'; | |||
| import { useMemo } from 'react'; | |||
| import { NodeHandleId, Operator } from '../../constant'; | |||
| export function ButtonEdge({ | |||
| function InnerButtonEdge({ | |||
| id, | |||
| sourceX, | |||
| sourceY, | |||
| @@ -77,7 +78,8 @@ export function ButtonEdge({ | |||
| const visible = useMemo(() => { | |||
| return ( | |||
| data?.isHovered && | |||
| sourceHandleId !== NodeHandleId.Tool && // The connection between the agent node and the tool node does not need to display the delete button | |||
| sourceHandleId !== NodeHandleId.Tool && | |||
| sourceHandleId !== NodeHandleId.AgentBottom && // The connection between the agent node and the tool node does not need to display the delete button | |||
| !target.startsWith(Operator.Tool) | |||
| ); | |||
| }, [data?.isHovered, sourceHandleId, target]); | |||
| @@ -120,3 +122,5 @@ export function ButtonEdge({ | |||
| </> | |||
| ); | |||
| } | |||
| export const ButtonEdge = memo(InnerButtonEdge); | |||
| @@ -1,40 +0,0 @@ | |||
| import { Handle, Position } from '@xyflow/react'; | |||
| import React, { memo } from 'react'; | |||
| import styles from './index.less'; | |||
| const DEFAULT_HANDLE_STYLE = { | |||
| width: 6, | |||
| height: 6, | |||
| bottom: -5, | |||
| fontSize: 8, | |||
| }; | |||
| interface IProps extends React.PropsWithChildren { | |||
| top: number; | |||
| right: number; | |||
| id: string; | |||
| idx?: number; | |||
| } | |||
| const CategorizeHandle = ({ top, right, id, children }: IProps) => { | |||
| return ( | |||
| <Handle | |||
| type="source" | |||
| position={Position.Right} | |||
| id={id} | |||
| isConnectable | |||
| style={{ | |||
| ...DEFAULT_HANDLE_STYLE, | |||
| top: `${top}%`, | |||
| right: `${right}%`, | |||
| background: 'red', | |||
| color: 'black', | |||
| }} | |||
| > | |||
| <span className={styles.categorizeAnchorPointText}>{children || id}</span> | |||
| </Handle> | |||
| ); | |||
| }; | |||
| export default memo(CategorizeHandle); | |||
| @@ -34,15 +34,15 @@ export function InnerCategorizeNode({ | |||
| <div className={'bg-background-card rounded-sm px-1'}> | |||
| <LLMLabel value={get(data, 'form.llm_id')}></LLMLabel> | |||
| </div> | |||
| {positions.map((position, idx) => { | |||
| {positions.map((position) => { | |||
| return ( | |||
| <div key={idx}> | |||
| <div className={'bg-background-card rounded-sm p-1'}> | |||
| {position.text} | |||
| <div key={position.uuid}> | |||
| <div className={'bg-background-card rounded-sm p-1 truncate'}> | |||
| {position.name} | |||
| </div> | |||
| <CommonHandle | |||
| key={position.text} | |||
| id={position.text} | |||
| // key={position.text} | |||
| id={position.uuid} | |||
| type="source" | |||
| position={Position.Right} | |||
| isConnectable | |||
| @@ -1,8 +1,9 @@ | |||
| import { ICategorizeItemResult } from '@/interfaces/database/agent'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { useUpdateNodeInternals } from '@xyflow/react'; | |||
| import { get } from 'lodash'; | |||
| import { useEffect, useMemo } from 'react'; | |||
| import { z } from 'zod'; | |||
| import { useCreateCategorizeFormSchema } from '../../form/categorize-form/use-form-schema'; | |||
| export const useBuildCategorizeHandlePositions = ({ | |||
| data, | |||
| @@ -13,33 +14,35 @@ export const useBuildCategorizeHandlePositions = ({ | |||
| }) => { | |||
| const updateNodeInternals = useUpdateNodeInternals(); | |||
| const categoryData: ICategorizeItemResult = useMemo(() => { | |||
| return get(data, `form.category_description`, {}); | |||
| const FormSchema = useCreateCategorizeFormSchema(); | |||
| type FormSchemaType = z.infer<typeof FormSchema>; | |||
| const items: Required<FormSchemaType['items']> = useMemo(() => { | |||
| return get(data, `form.items`, []); | |||
| }, [data]); | |||
| const positions = useMemo(() => { | |||
| const list: Array<{ | |||
| text: string; | |||
| top: number; | |||
| idx: number; | |||
| }> = []; | |||
| Object.keys(categoryData) | |||
| .sort((a, b) => categoryData[a].index - categoryData[b].index) | |||
| .forEach((x, idx) => { | |||
| list.push({ | |||
| text: x, | |||
| idx, | |||
| top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24, | |||
| }); | |||
| name: string; | |||
| uuid: string; | |||
| }> & | |||
| Required<FormSchemaType['items']> = []; | |||
| items.forEach((x, idx) => { | |||
| list.push({ | |||
| ...x, | |||
| top: idx === 0 ? 86 : list[idx - 1].top + 8 + 24, | |||
| }); | |||
| }); | |||
| return list; | |||
| }, [categoryData]); | |||
| }, [items]); | |||
| useEffect(() => { | |||
| updateNodeInternals(id); | |||
| }, [id, updateNodeInternals, categoryData]); | |||
| }, [id, updateNodeInternals, items]); | |||
| return { positions }; | |||
| }; | |||
| @@ -321,7 +321,7 @@ export const initialCategorizeValues = { | |||
| query: AgentGlobals.SysQuery, | |||
| parameter: ModelVariableType.Precise, | |||
| message_history_window_size: 1, | |||
| category_description: {}, | |||
| items: [], | |||
| outputs: { | |||
| category_name: { | |||
| type: 'string', | |||
| @@ -760,6 +760,7 @@ export const RestrictedUpstreamMap = { | |||
| [Operator.TavilyExtract]: [Operator.Begin], | |||
| [Operator.StringTransform]: [Operator.Begin], | |||
| [Operator.UserFillUp]: [Operator.Begin], | |||
| [Operator.Tool]: [Operator.Begin], | |||
| }; | |||
| export const NodeMap = { | |||
| @@ -28,7 +28,11 @@ import { | |||
| useState, | |||
| } from 'react'; | |||
| import { UseFormReturn, useFieldArray, useFormContext } from 'react-hook-form'; | |||
| import { v4 as uuid } from 'uuid'; | |||
| import { z } from 'zod'; | |||
| import useGraphStore from '../../store'; | |||
| import DynamicExample from './dynamic-example'; | |||
| import { useCreateCategorizeFormSchema } from './use-form-schema'; | |||
| interface IProps { | |||
| nodeId?: string; | |||
| @@ -155,6 +159,12 @@ const InnerFormSet = ({ index }: IProps & { index: number }) => { | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| {/* Create a hidden field to make Form instance record this */} | |||
| <FormField | |||
| control={form.control} | |||
| name={'uuid'} | |||
| render={() => <div></div>} | |||
| /> | |||
| <DynamicExample name={buildFieldName('examples')}></DynamicExample> | |||
| </section> | |||
| ); | |||
| @@ -164,21 +174,38 @@ const FormSet = memo(InnerFormSet); | |||
| const DynamicCategorize = ({ nodeId }: IProps) => { | |||
| const updateNodeInternals = useUpdateNodeInternals(); | |||
| const form = useFormContext(); | |||
| const FormSchema = useCreateCategorizeFormSchema(); | |||
| const deleteCategorizeCaseEdges = useGraphStore( | |||
| (state) => state.deleteCategorizeCaseEdges, | |||
| ); | |||
| const form = useFormContext<z.infer<typeof FormSchema>>(); | |||
| const { t } = useTranslate('flow'); | |||
| const { fields, remove, append } = useFieldArray({ | |||
| name: 'items', | |||
| control: form.control, | |||
| }); | |||
| const handleAdd = () => { | |||
| const handleAdd = useCallback(() => { | |||
| append({ | |||
| name: humanId(), | |||
| description: '', | |||
| uuid: uuid(), | |||
| examples: [{ value: '' }], | |||
| }); | |||
| if (nodeId) updateNodeInternals(nodeId); | |||
| }; | |||
| }, [append, nodeId, updateNodeInternals]); | |||
| const handleRemove = useCallback( | |||
| (index: number) => () => { | |||
| remove(index); | |||
| if (nodeId) { | |||
| const uuid = fields[index].uuid; | |||
| deleteCategorizeCaseEdges(nodeId, uuid); | |||
| } | |||
| }, | |||
| [deleteCategorizeCaseEdges, fields, nodeId, remove], | |||
| ); | |||
| return ( | |||
| <div className="flex flex-col gap-4 "> | |||
| @@ -194,7 +221,7 @@ const DynamicCategorize = ({ nodeId }: IProps) => { | |||
| variant="ghost" | |||
| size="sm" | |||
| className="w-9 p-0" | |||
| onClick={() => remove(index)} | |||
| onClick={handleRemove(index)} | |||
| > | |||
| <X className="h-4 w-4" /> | |||
| </Button> | |||
| @@ -1,50 +1,26 @@ | |||
| import { FormContainer } from '@/components/form-container'; | |||
| import { LargeModelFormField } from '@/components/large-model-form-field'; | |||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | |||
| import { MessageHistoryWindowSizeFormField } from '@/components/message-history-window-size-item'; | |||
| import { Form } from '@/components/ui/form'; | |||
| import { zodResolver } from '@hookform/resolvers/zod'; | |||
| import { memo } from 'react'; | |||
| import { useForm } from 'react-hook-form'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| import { initialCategorizeValues } from '../../constant'; | |||
| import { INextOperatorForm } from '../../interface'; | |||
| import { buildOutputList } from '../../utils/build-output-list'; | |||
| import { Output } from '../components/output'; | |||
| import { QueryVariable } from '../components/query-variable'; | |||
| import DynamicCategorize from './dynamic-categorize'; | |||
| import { useCreateCategorizeFormSchema } from './use-form-schema'; | |||
| import { useValues } from './use-values'; | |||
| import { useWatchFormChange } from './use-watch-change'; | |||
| const outputList = buildOutputList(initialCategorizeValues.outputs); | |||
| function CategorizeForm({ node }: INextOperatorForm) { | |||
| const { t } = useTranslation(); | |||
| const values = useValues(node); | |||
| const FormSchema = z.object({ | |||
| query: z.string().optional(), | |||
| parameter: z.string().optional(), | |||
| ...LlmSettingSchema, | |||
| message_history_window_size: z.coerce.number(), | |||
| items: z.array( | |||
| z | |||
| .object({ | |||
| name: z.string().min(1, t('flow.nameMessage')).trim(), | |||
| description: z.string().optional(), | |||
| examples: z | |||
| .array( | |||
| z.object({ | |||
| value: z.string(), | |||
| }), | |||
| ) | |||
| .optional(), | |||
| }) | |||
| .optional(), | |||
| ), | |||
| }); | |||
| const FormSchema = useCreateCategorizeFormSchema(); | |||
| const form = useForm({ | |||
| defaultValues: values, | |||
| @@ -0,0 +1,32 @@ | |||
| import { LlmSettingSchema } from '@/components/llm-setting-items/next'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| import { z } from 'zod'; | |||
| export function useCreateCategorizeFormSchema() { | |||
| const { t } = useTranslation(); | |||
| const FormSchema = z.object({ | |||
| query: z.string().optional(), | |||
| parameter: z.string().optional(), | |||
| ...LlmSettingSchema, | |||
| message_history_window_size: z.coerce.number(), | |||
| items: z.array( | |||
| z | |||
| .object({ | |||
| name: z.string().min(1, t('flow.nameMessage')).trim(), | |||
| description: z.string().optional(), | |||
| uuid: z.string(), | |||
| examples: z | |||
| .array( | |||
| z.object({ | |||
| value: z.string(), | |||
| }), | |||
| ) | |||
| .optional(), | |||
| }) | |||
| .optional(), | |||
| ), | |||
| }); | |||
| return FormSchema; | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| import { ModelVariableType } from '@/constants/knowledge'; | |||
| import { RAGFlowNodeType } from '@/interfaces/database/flow'; | |||
| import { get, isEmpty, isPlainObject, omit } from 'lodash'; | |||
| import { get, isEmpty, isPlainObject } from 'lodash'; | |||
| import { useMemo } from 'react'; | |||
| import { buildCategorizeListFromObject } from '../../utils'; | |||
| @@ -25,12 +25,12 @@ export function useValues(node?: RAGFlowNodeType) { | |||
| get(node, 'data.form.category_description', {}), | |||
| ); | |||
| if (isPlainObject(formData)) { | |||
| const nextValues = { | |||
| ...omit(formData, 'category_description'), | |||
| items, | |||
| }; | |||
| // const nextValues = { | |||
| // ...omit(formData, 'category_description'), | |||
| // items, | |||
| // }; | |||
| return nextValues; | |||
| return formData; | |||
| } | |||
| }, [node]); | |||
| @@ -1,8 +1,6 @@ | |||
| import { omit } from 'lodash'; | |||
| import { useEffect } from 'react'; | |||
| import { UseFormReturn, useWatch } from 'react-hook-form'; | |||
| import useGraphStore from '../../store'; | |||
| import { buildCategorizeObjectFromList } from '../../utils'; | |||
| export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { | |||
| let values = useWatch({ control: form?.control }); | |||
| @@ -10,21 +8,10 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn<any>) { | |||
| useEffect(() => { | |||
| // Manually triggered form updates are synchronized to the canvas | |||
| if (id && form?.formState.isDirty) { | |||
| if (id) { | |||
| values = form?.getValues(); | |||
| let nextValues: any = values; | |||
| const categoryDescription = Array.isArray(values.items) | |||
| ? buildCategorizeObjectFromList(values.items) | |||
| : {}; | |||
| if (categoryDescription) { | |||
| nextValues = { | |||
| ...omit(values, 'items'), | |||
| category_description: categoryDescription, | |||
| }; | |||
| } | |||
| updateNodeForm(id, nextValues); | |||
| updateNodeForm(id, { ...values, items: values.items?.slice() || [] }); | |||
| } | |||
| }, [form?.formState.isDirty, id, updateNodeForm, values]); | |||
| }, [id, updateNodeForm, values]); | |||
| } | |||
| @@ -24,7 +24,7 @@ export function Output({ list }: OutputProps) { | |||
| key={idx} | |||
| className="bg-background-highlight text-background-checked rounded-sm px-2 py-1" | |||
| > | |||
| {x.title}: {x.type} | |||
| {x.title}: <span className="text-text-sub-title">{x.type}</span> | |||
| </li> | |||
| ))} | |||
| </ul> | |||
| @@ -49,6 +49,7 @@ import { | |||
| initialRetrievalValues, | |||
| initialRewriteQuestionValues, | |||
| initialSwitchValues, | |||
| initialTavilyExtractValues, | |||
| initialTavilyValues, | |||
| initialTemplateValues, | |||
| initialTuShareValues, | |||
| @@ -135,6 +136,7 @@ export const useInitializeOperatorParams = () => { | |||
| [Operator.WaitingDialogue]: initialWaitingDialogueValues, | |||
| [Operator.Agent]: { ...initialAgentValues, llm_id: llmId }, | |||
| [Operator.TavilySearch]: initialTavilyValues, | |||
| [Operator.TavilyExtract]: initialTavilyExtractValues, | |||
| }; | |||
| }, [llmId]); | |||
| @@ -331,7 +333,7 @@ export const useHandleFormValuesChange = ( | |||
| }; | |||
| export const useValidateConnection = () => { | |||
| const { edges, getOperatorTypeFromId, getParentIdById } = useGraphStore( | |||
| const { getOperatorTypeFromId, getParentIdById } = useGraphStore( | |||
| (state) => state, | |||
| ); | |||
| @@ -354,20 +356,19 @@ export const useValidateConnection = () => { | |||
| const isSelfConnected = connection.target === connection.source; | |||
| // limit the connection between two nodes to only one connection line in one direction | |||
| const hasLine = edges.some( | |||
| (x) => x.source === connection.source && x.target === connection.target, | |||
| ); | |||
| // const hasLine = edges.some( | |||
| // (x) => x.source === connection.source && x.target === connection.target, | |||
| // ); | |||
| const ret = | |||
| !isSelfConnected && | |||
| !hasLine && | |||
| RestrictedUpstreamMap[ | |||
| getOperatorTypeFromId(connection.source) as Operator | |||
| ]?.every((x) => x !== getOperatorTypeFromId(connection.target)) && | |||
| isSameNodeChild(connection); | |||
| return ret; | |||
| }, | |||
| [edges, getOperatorTypeFromId, isSameNodeChild], | |||
| [getOperatorTypeFromId, isSameNodeChild], | |||
| ); | |||
| return isValidConnection; | |||
| @@ -84,6 +84,7 @@ export type RFState = { | |||
| setClickedNodeId: (id?: string) => void; | |||
| setClickedToolId: (id?: string) => void; | |||
| findUpstreamNodeById: (id?: string | null) => RAGFlowNodeType | undefined; | |||
| deleteCategorizeCaseEdges: (source: string, sourceHandle: string) => void; // Deleting a condition of a classification operator will delete the related edge | |||
| }; | |||
| // this is our useStore hook that we can use in our components to get parts of the store and call actions | |||
| @@ -307,14 +308,14 @@ const useGraphStore = create<RFState>()( | |||
| [sourceHandle as string]: undefined, | |||
| }); | |||
| break; | |||
| case Operator.Categorize: | |||
| if (sourceHandle) | |||
| updateNodeForm(source, undefined, [ | |||
| 'category_description', | |||
| sourceHandle, | |||
| 'to', | |||
| ]); | |||
| break; | |||
| // case Operator.Categorize: | |||
| // if (sourceHandle) | |||
| // updateNodeForm(source, undefined, [ | |||
| // 'category_description', | |||
| // sourceHandle, | |||
| // 'to', | |||
| // ]); | |||
| // break; | |||
| case Operator.Switch: { | |||
| updateSwitchFormData(source, sourceHandle, target, false); | |||
| break; | |||
| @@ -508,6 +509,15 @@ const useGraphStore = create<RFState>()( | |||
| const edge = edges.find((x) => x.target === id); | |||
| return getNode(edge?.source); | |||
| }, | |||
| deleteCategorizeCaseEdges: (source, sourceHandle) => { | |||
| const { edges, setEdges } = get(); | |||
| setEdges( | |||
| edges.filter( | |||
| (edge) => | |||
| !(edge.source === source && edge.sourceHandle === sourceHandle), | |||
| ), | |||
| ); | |||
| }, | |||
| })), | |||
| { name: 'graph', trace: true }, | |||
| ), | |||
| @@ -13,13 +13,12 @@ import { | |||
| FormLabel, | |||
| FormMessage, | |||
| } from '@/components/ui/form'; | |||
| import { RAGFlowSelect } from '@/components/ui/select'; | |||
| import { FileMimeType, Platform } from '@/constants/common'; | |||
| import { IModalProps } from '@/interfaces/common'; | |||
| import { TagRenameId } from '@/pages/add-knowledge/constant'; | |||
| import { useTranslation } from 'react-i18next'; | |||
| const options = Object.values(Platform).map((x) => ({ label: x, value: x })); | |||
| // const options = Object.values(Platform).map((x) => ({ label: x, value: x })); | |||
| export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) { | |||
| const { t } = useTranslation(); | |||
| @@ -72,7 +71,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) { | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| <FormField | |||
| {/* <FormField | |||
| control={form.control} | |||
| name="platform" | |||
| render={({ field }) => ( | |||
| @@ -84,7 +83,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps<any>) { | |||
| <FormMessage /> | |||
| </FormItem> | |||
| )} | |||
| /> | |||
| /> */} | |||
| </form> | |||
| </Form> | |||
| ); | |||
| @@ -159,7 +159,7 @@ function buildAgentTools(edges: Edge[], nodes: Node[], nodeId: string) { | |||
| return { | |||
| component_name: Operator.Agent, | |||
| id, | |||
| name, | |||
| name: name as string, // Cast name to string and provide fallback | |||
| params: { ...formData }, | |||
| }; | |||
| }), | |||
| @@ -172,27 +172,29 @@ function filterTargetsBySourceHandleId(edges: Edge[], handleId: string) { | |||
| return edges.filter((x) => x.sourceHandle === handleId).map((x) => x.target); | |||
| } | |||
| function buildCategorizeTos(edges: Edge[], nodes: Node[], nodeId: string) { | |||
| function buildCategorize(edges: Edge[], nodes: Node[], nodeId: string) { | |||
| const node = nodes.find((x) => x.id === nodeId); | |||
| const params = { ...(node?.data.form ?? {}) } as ICategorizeForm; | |||
| if (node && node.data.label === Operator.Categorize) { | |||
| const subEdges = edges.filter((x) => x.source === nodeId); | |||
| const categoryDescription = params.category_description || {}; | |||
| const items = params.items || []; | |||
| const nextCategoryDescription = Object.entries(categoryDescription).reduce< | |||
| const nextCategoryDescription = items.reduce< | |||
| ICategorizeForm['category_description'] | |||
| >((pre, [key, val]) => { | |||
| >((pre, val) => { | |||
| const key = val.name; | |||
| pre[key] = { | |||
| ...val, | |||
| to: filterTargetsBySourceHandleId(subEdges, key), | |||
| ...omit(val, 'name', 'uuid'), | |||
| examples: val.examples?.map((x) => x.value) || [], | |||
| to: filterTargetsBySourceHandleId(subEdges, val.uuid), | |||
| }; | |||
| return pre; | |||
| }, {}); | |||
| params.category_description = nextCategoryDescription; | |||
| } | |||
| return params; | |||
| return omit(params, 'items'); | |||
| } | |||
| const buildOperatorParams = (operatorName: string) => | |||
| @@ -236,7 +238,7 @@ export const buildDslComponentsByGraph = ( | |||
| break; | |||
| } | |||
| case Operator.Categorize: | |||
| params = buildCategorizeTos(edges, nodes, id); | |||
| params = buildCategorize(edges, nodes, id); | |||
| break; | |||
| default: | |||