From 1767039be343096b003a2a53ab102a6af1c42809 Mon Sep 17 00:00:00 2001 From: balibabu Date: Tue, 21 Oct 2025 12:59:30 +0800 Subject: [PATCH] Feat: Display the pipeline operation sheet on the agent page #9869 (#10690) ### What problem does this PR solve? Feat: Display the pipeline operation sheet on the agent page #9869 ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- web/src/pages/agent/context.ts | 10 -- web/src/pages/agent/hooks/use-run-dataflow.ts | 55 ++++++++ web/src/pages/agent/index.tsx | 55 +++++--- .../pages/agent/pipeline-run-sheet/index.tsx | 31 +++++ .../agent/pipeline-run-sheet/uploader.tsx | 57 +++++++++ web/src/pages/agent/utils.ts | 117 +++++++++++++++++- 6 files changed, 294 insertions(+), 31 deletions(-) create mode 100644 web/src/pages/agent/hooks/use-run-dataflow.ts create mode 100644 web/src/pages/agent/pipeline-run-sheet/index.tsx create mode 100644 web/src/pages/agent/pipeline-run-sheet/uploader.tsx diff --git a/web/src/pages/agent/context.ts b/web/src/pages/agent/context.ts index 9d575cc3c..6839554d3 100644 --- a/web/src/pages/agent/context.ts +++ b/web/src/pages/agent/context.ts @@ -48,13 +48,3 @@ export type HandleContextType = { export const HandleContext = createContext( {} as HandleContextType, ); - -export type PipelineLogContextType = { - messageId: string; - setMessageId: (messageId: string) => void; - setUploadedFileData: (data: Record) => void; -}; - -export const PipelineLogContext = createContext( - {} as PipelineLogContextType, -); diff --git a/web/src/pages/agent/hooks/use-run-dataflow.ts b/web/src/pages/agent/hooks/use-run-dataflow.ts new file mode 100644 index 000000000..806a38096 --- /dev/null +++ b/web/src/pages/agent/hooks/use-run-dataflow.ts @@ -0,0 +1,55 @@ +import message from '@/components/ui/message'; +import { useSendMessageBySSE } from '@/hooks/use-send-message'; +import api from '@/utils/api'; +import { get } from 'lodash'; +import { useCallback, useState } from 'react'; +import { useParams } from 'umi'; +import { UseFetchLogReturnType } from './use-fetch-pipeline-log'; +import { useSaveGraph } from './use-save-graph'; + +export function useRunDataflow({ + showLogSheet, + setMessageId, +}: { + showLogSheet: () => void; +} & Pick) { + const { send } = useSendMessageBySSE(api.runCanvas); + const { id } = useParams(); + const { saveGraph, loading } = useSaveGraph(); + const [uploadedFileData, setUploadedFileData] = + useState>(); + + const run = useCallback( + async (fileResponseData: Record) => { + const saveRet = await saveGraph(); + const success = saveRet?.code === 0; + if (!success) return; + + showLogSheet(); + const res = await send({ + id, + query: '', + session_id: null, + files: [fileResponseData.file], + }); + + if (res && res?.response.status === 200 && get(res, 'data.code') === 0) { + // fetch canvas + setUploadedFileData(fileResponseData.file); + const msgId = get(res, 'data.data.message_id'); + if (msgId) { + setMessageId(msgId); + } + + return msgId; + } else { + message.error(get(res, 'data.message', '')); + } + }, + [id, saveGraph, send, setMessageId, setUploadedFileData, showLogSheet], + ); + + return { run, loading: loading, uploadedFileData }; +} + +export type RunDataflowType = ReturnType; diff --git a/web/src/pages/agent/index.tsx b/web/src/pages/agent/index.tsx index 8b265b3bc..e65f5d224 100644 --- a/web/src/pages/agent/index.tsx +++ b/web/src/pages/agent/index.tsx @@ -32,25 +32,26 @@ import { Settings, Upload, } from 'lucide-react'; -import { ComponentPropsWithoutRef, useCallback, useState } from 'react'; +import { ComponentPropsWithoutRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useParams } from 'umi'; import AgentCanvas from './canvas'; import { DropdownProvider } from './canvas/context'; import { Operator } from './constant'; -import { PipelineLogContext } from './context'; import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow'; import { useHandleExportJsonFile } from './hooks/use-export-json'; import { useFetchDataOnMount } from './hooks/use-fetch-data'; import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log'; import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query'; import { useIsPipeline } from './hooks/use-is-pipeline'; +import { useRunDataflow } from './hooks/use-run-dataflow'; import { useSaveGraph, useSaveGraphBeforeOpeningDebugDrawer, useWatchAgentChange, } from './hooks/use-save-graph'; import { PipelineLogSheet } from './pipeline-log-sheet'; +import PipelineRunSheet from './pipeline-run-sheet'; import { SettingDialog } from './setting-dialog'; import useGraphStore from './store'; import { useAgentHistoryManager } from './use-agent-history-manager'; @@ -110,6 +111,12 @@ export default function Agent() { // pipeline + const { + visible: pipelineRunSheetVisible, + hideModal: hidePipelineRunSheet, + showModal: showPipelineRunSheet, + } = useSetModalState(); + const { visible: pipelineLogSheetVisible, showModal: showPipelineLogSheet, @@ -126,8 +133,6 @@ export default function Agent() { isLogEmpty, } = useFetchPipelineLog(pipelineLogSheetVisible); - const [uploadedFileData, setUploadedFileData] = - useState>(); const findNodeByName = useGraphStore((state) => state.findNodeByName); const handleRunPipeline = useCallback(() => { @@ -141,14 +146,15 @@ export default function Agent() { showPipelineLogSheet(); } else { hidePipelineLogSheet(); - handleRun(); + // handleRun(); + showPipelineRunSheet(); } }, [ findNodeByName, - handleRun, hidePipelineLogSheet, isParsing, showPipelineLogSheet, + showPipelineRunSheet, t, ]); @@ -157,7 +163,7 @@ export default function Agent() { stopFetchTrace, }); - const run = useCallback(() => { + const handleButtonRunClick = useCallback(() => { if (isPipeline) { handleRunPipeline(); } else { @@ -165,6 +171,12 @@ export default function Agent() { } }, [handleRunAgent, handleRunPipeline, isPipeline]); + const { + run: runPipeline, + loading: pipelineRunning, + uploadedFileData, + } = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId }); + return (
@@ -194,7 +206,7 @@ export default function Agent() { > {t('flow.save')} - @@ -241,18 +253,14 @@ export default function Agent() { - - - - - - - + + + + + {embedVisible && ( )} + {pipelineRunSheetVisible && ( + + )}
); } diff --git a/web/src/pages/agent/pipeline-run-sheet/index.tsx b/web/src/pages/agent/pipeline-run-sheet/index.tsx new file mode 100644 index 000000000..8c082b91a --- /dev/null +++ b/web/src/pages/agent/pipeline-run-sheet/index.tsx @@ -0,0 +1,31 @@ +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, +} from '@/components/ui/sheet'; +import { IModalProps } from '@/interfaces/common'; +import { cn } from '@/lib/utils'; +import { useTranslation } from 'react-i18next'; +import { RunDataflowType } from '../hooks/use-run-dataflow'; +import { UploaderForm } from './uploader'; + +type RunSheetProps = IModalProps & + Pick; + +const PipelineRunSheet = ({ hideModal, run, loading }: RunSheetProps) => { + const { t } = useTranslation(); + + return ( + + + + {t('flow.testRun')} + + + + + ); +}; + +export default PipelineRunSheet; diff --git a/web/src/pages/agent/pipeline-run-sheet/uploader.tsx b/web/src/pages/agent/pipeline-run-sheet/uploader.tsx new file mode 100644 index 000000000..db4a977dd --- /dev/null +++ b/web/src/pages/agent/pipeline-run-sheet/uploader.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { z } from 'zod'; + +import { RAGFlowFormItem } from '@/components/ragflow-form'; +import { ButtonLoading } from '@/components/ui/button'; +import { Form } from '@/components/ui/form'; +import { FileUploadDirectUpload } from '@/pages/agent/debug-content/uploader'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; + +const formSchema = z.object({ + file: z.record(z.any()), +}); + +export type FormSchemaType = z.infer; + +type UploaderFormProps = { + ok: (values: FormSchemaType) => void; + loading: boolean; +}; + +export function UploaderForm({ ok, loading }: UploaderFormProps) { + const { t } = useTranslation(); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: {}, + }); + + return ( +
+ + + {(field) => { + return ( + + ); + }} + + +
+ + {t('flow.run')} + +
+
+ + ); +} diff --git a/web/src/pages/agent/utils.ts b/web/src/pages/agent/utils.ts index c073fc578..a22e2fedc 100644 --- a/web/src/pages/agent/utils.ts +++ b/web/src/pages/agent/utils.ts @@ -9,16 +9,30 @@ import { removeUselessFieldsFromValues } from '@/utils/form'; import { Edge, Node, XYPosition } from '@xyflow/react'; import { FormInstance, FormListFieldData } from 'antd'; import { humanId } from 'human-id'; -import { curry, get, intersectionWith, isEqual, omit, sample } from 'lodash'; +import { + curry, + get, + intersectionWith, + isEmpty, + isEqual, + omit, + sample, +} from 'lodash'; import pipe from 'lodash/fp/pipe'; import isObject from 'lodash/isObject'; import { CategorizeAnchorPointPositions, + FileType, + FileTypeSuffixMap, NoCopyOperatorsList, NoDebugOperatorsList, NodeHandleId, Operator, } from './constant'; +import { ExtractorFormSchemaType } from './form/extractor-form'; +import { HierarchicalMergerFormSchemaType } from './form/hierarchical-merger-form'; +import { ParserFormSchemaType } from './form/parser-form'; +import { SplitterFormSchemaType } from './form/splitter-form'; import { BeginQuery, IPosition } from './interface'; function buildAgentExceptionGoto(edges: Edge[], nodeId: string) { @@ -170,6 +184,92 @@ export function hasSubAgent(edges: Edge[], nodeId?: string) { return !!edge; } +// Because the array of react-hook-form must be object data, +// it needs to be converted into a simple data type array required by the backend +function transformObjectArrayToPureArray( + list: Array>, + field: string, +) { + return Array.isArray(list) + ? list.filter((x) => !isEmpty(x[field])).map((y) => y[field]) + : []; +} + +function transformParserParams(params: ParserFormSchemaType) { + const setups = params.setups.reduce< + Record + >((pre, cur) => { + if (cur.fileFormat) { + let filteredSetup: Partial< + ParserFormSchemaType['setups'][0] & { suffix: string[] } + > = { + output_format: cur.output_format, + suffix: FileTypeSuffixMap[cur.fileFormat as FileType], + }; + + switch (cur.fileFormat) { + case FileType.PDF: + filteredSetup = { + ...filteredSetup, + parse_method: cur.parse_method, + lang: cur.lang, + }; + break; + case FileType.Image: + filteredSetup = { + ...filteredSetup, + parse_method: cur.parse_method, + lang: cur.lang, + system_prompt: cur.system_prompt, + }; + break; + case FileType.Email: + filteredSetup = { + ...filteredSetup, + fields: cur.fields, + }; + break; + case FileType.Video: + case FileType.Audio: + filteredSetup = { + ...filteredSetup, + llm_id: cur.llm_id, + }; + break; + default: + break; + } + + pre[cur.fileFormat] = filteredSetup; + } + return pre; + }, {}); + + return { ...params, setups }; +} + +function transformSplitterParams(params: SplitterFormSchemaType) { + return { + ...params, + overlapped_percent: Number(params.overlapped_percent) / 100, + delimiters: transformObjectArrayToPureArray(params.delimiters, 'value'), + }; +} + +function transformHierarchicalMergerParams( + params: HierarchicalMergerFormSchemaType, +) { + const levels = params.levels.map((x) => + transformObjectArrayToPureArray(x.expressions, 'expression'), + ); + + return { ...params, hierarchy: Number(params.hierarchy), levels }; +} + +function transformExtractorParams(params: ExtractorFormSchemaType) { + return { ...params, prompts: [{ content: params.prompts, role: 'user' }] }; +} + // construct a dsl based on the node information of the graph export const buildDslComponentsByGraph = ( nodes: RAGFlowNodeType[], @@ -202,6 +302,21 @@ export const buildDslComponentsByGraph = ( params = buildCategorize(edges, nodes, id); break; + case Operator.Parser: + params = transformParserParams(params); + break; + + case Operator.Splitter: + params = transformSplitterParams(params); + break; + + case Operator.HierarchicalMerger: + params = transformHierarchicalMergerParams(params); + break; + case Operator.Extractor: + params = transformExtractorParams(params); + break; + default: break; }