From cc0227cf6e451d2d9e6115b970a34c65fae799b7 Mon Sep 17 00:00:00 2001 From: balibabu Date: Mon, 28 Jul 2025 14:16:20 +0800 Subject: [PATCH] Fix: Fixed the issue that the condition of deleting the classification operator cannot be connected anymore #3221 (#9068) ### 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) --- web/src/interfaces/database/agent.ts | 4 +- web/src/pages/agent/canvas/edge/index.tsx | 8 +++- .../agent/canvas/node/categorize-handle.tsx | 40 ------------------- .../agent/canvas/node/categorize-node.tsx | 12 +++--- .../use-build-categorize-handle-positions.ts | 35 ++++++++-------- web/src/pages/agent/constant.tsx | 3 +- .../categorize-form/dynamic-categorize.tsx | 35 ++++++++++++++-- .../agent/form/categorize-form/index.tsx | 28 +------------ .../form/categorize-form/use-form-schema.ts | 32 +++++++++++++++ .../agent/form/categorize-form/use-values.ts | 12 +++--- .../form/categorize-form/use-watch-change.ts | 19 ++------- .../pages/agent/form/components/output.tsx | 2 +- web/src/pages/agent/hooks.tsx | 13 +++--- web/src/pages/agent/store.ts | 26 ++++++++---- .../upload-agent-dialog/upload-agent-form.tsx | 7 ++-- web/src/pages/agent/utils.ts | 20 +++++----- 16 files changed, 150 insertions(+), 146 deletions(-) delete mode 100644 web/src/pages/agent/canvas/node/categorize-handle.tsx create mode 100644 web/src/pages/agent/form/categorize-form/use-form-schema.ts diff --git a/web/src/interfaces/database/agent.ts b/web/src/interfaces/database/agent.ts index 0c1b9b29b..eabd2cbd0 100644 --- a/web/src/interfaces/database/agent.ts +++ b/web/src/interfaces/database/agent.ts @@ -4,11 +4,12 @@ export interface ICategorizeItem { examples?: { value: string }[]; index: number; to: string[]; + uuid: string; } export type ICategorizeItemResult = Record< string, - Omit & { examples: string[] } + Omit & { 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 { diff --git a/web/src/pages/agent/canvas/edge/index.tsx b/web/src/pages/agent/canvas/edge/index.tsx index 6358fdc9d..d762d7413 100644 --- a/web/src/pages/agent/canvas/edge/index.tsx +++ b/web/src/pages/agent/canvas/edge/index.tsx @@ -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); diff --git a/web/src/pages/agent/canvas/node/categorize-handle.tsx b/web/src/pages/agent/canvas/node/categorize-handle.tsx deleted file mode 100644 index f1eeff765..000000000 --- a/web/src/pages/agent/canvas/node/categorize-handle.tsx +++ /dev/null @@ -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 ( - - {children || id} - - ); -}; - -export default memo(CategorizeHandle); diff --git a/web/src/pages/agent/canvas/node/categorize-node.tsx b/web/src/pages/agent/canvas/node/categorize-node.tsx index 047135194..4747b71b2 100644 --- a/web/src/pages/agent/canvas/node/categorize-node.tsx +++ b/web/src/pages/agent/canvas/node/categorize-node.tsx @@ -34,15 +34,15 @@ export function InnerCategorizeNode({
- {positions.map((position, idx) => { + {positions.map((position) => { return ( -
-
- {position.text} +
+
+ {position.name}
{ const updateNodeInternals = useUpdateNodeInternals(); - const categoryData: ICategorizeItemResult = useMemo(() => { - return get(data, `form.category_description`, {}); + const FormSchema = useCreateCategorizeFormSchema(); + + type FormSchemaType = z.infer; + + const items: Required = useMemo(() => { + return get(data, `form.items`, []); }, [data]); const positions = useMemo(() => { const list: Array<{ - text: string; top: number; - idx: number; - }> = []; + name: string; + uuid: string; + }> & + Required = []; - 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, - }); + 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 }; }; diff --git a/web/src/pages/agent/constant.tsx b/web/src/pages/agent/constant.tsx index b75bf99d3..4d26074f5 100644 --- a/web/src/pages/agent/constant.tsx +++ b/web/src/pages/agent/constant.tsx @@ -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 = { diff --git a/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx b/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx index 02354223d..51979853e 100644 --- a/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx +++ b/web/src/pages/agent/form/categorize-form/dynamic-categorize.tsx @@ -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 }) => { )} /> + {/* Create a hidden field to make Form instance record this */} +
} + /> ); @@ -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>(); 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 (
@@ -194,7 +221,7 @@ const DynamicCategorize = ({ nodeId }: IProps) => { variant="ghost" size="sm" className="w-9 p-0" - onClick={() => remove(index)} + onClick={handleRemove(index)} > diff --git a/web/src/pages/agent/form/categorize-form/index.tsx b/web/src/pages/agent/form/categorize-form/index.tsx index 03d569466..4ba5c73b6 100644 --- a/web/src/pages/agent/form/categorize-form/index.tsx +++ b/web/src/pages/agent/form/categorize-form/index.tsx @@ -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, diff --git a/web/src/pages/agent/form/categorize-form/use-form-schema.ts b/web/src/pages/agent/form/categorize-form/use-form-schema.ts new file mode 100644 index 000000000..9e56bb18b --- /dev/null +++ b/web/src/pages/agent/form/categorize-form/use-form-schema.ts @@ -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; +} diff --git a/web/src/pages/agent/form/categorize-form/use-values.ts b/web/src/pages/agent/form/categorize-form/use-values.ts index ef75575dc..a4c0e80aa 100644 --- a/web/src/pages/agent/form/categorize-form/use-values.ts +++ b/web/src/pages/agent/form/categorize-form/use-values.ts @@ -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]); diff --git a/web/src/pages/agent/form/categorize-form/use-watch-change.ts b/web/src/pages/agent/form/categorize-form/use-watch-change.ts index 91ae84e80..a97b80a77 100644 --- a/web/src/pages/agent/form/categorize-form/use-watch-change.ts +++ b/web/src/pages/agent/form/categorize-form/use-watch-change.ts @@ -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) { let values = useWatch({ control: form?.control }); @@ -10,21 +8,10 @@ export function useWatchFormChange(id?: string, form?: UseFormReturn) { 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]); } diff --git a/web/src/pages/agent/form/components/output.tsx b/web/src/pages/agent/form/components/output.tsx index eefc3cef5..22e09a752 100644 --- a/web/src/pages/agent/form/components/output.tsx +++ b/web/src/pages/agent/form/components/output.tsx @@ -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}: {x.type} ))} diff --git a/web/src/pages/agent/hooks.tsx b/web/src/pages/agent/hooks.tsx index c1b44b605..99e10e52a 100644 --- a/web/src/pages/agent/hooks.tsx +++ b/web/src/pages/agent/hooks.tsx @@ -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; diff --git a/web/src/pages/agent/store.ts b/web/src/pages/agent/store.ts index e8cc2b241..d52bff641 100644 --- a/web/src/pages/agent/store.ts +++ b/web/src/pages/agent/store.ts @@ -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()( [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()( 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 }, ), diff --git a/web/src/pages/agent/upload-agent-dialog/upload-agent-form.tsx b/web/src/pages/agent/upload-agent-dialog/upload-agent-form.tsx index ddd6778ae..fe93b9bdd 100644 --- a/web/src/pages/agent/upload-agent-dialog/upload-agent-form.tsx +++ b/web/src/pages/agent/upload-agent-dialog/upload-agent-form.tsx @@ -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) { const { t } = useTranslation(); @@ -72,7 +71,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps) { )} /> - ( @@ -84,7 +83,7 @@ export function UploadAgentForm({ hideModal, onOk }: IModalProps) { )} - /> + /> */} ); diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index a90eafc3d..184cfdc83 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -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: