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)
This commit is contained in:
balibabu
2025-10-21 12:59:30 +08:00
committed by GitHub
parent cd75fa02b1
commit 1767039be3
6 changed files with 294 additions and 31 deletions

View File

@ -48,13 +48,3 @@ export type HandleContextType = {
export const HandleContext = createContext<HandleContextType>(
{} as HandleContextType,
);
export type PipelineLogContextType = {
messageId: string;
setMessageId: (messageId: string) => void;
setUploadedFileData: (data: Record<string, any>) => void;
};
export const PipelineLogContext = createContext<PipelineLogContextType>(
{} as PipelineLogContextType,
);

View File

@ -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<UseFetchLogReturnType, 'setMessageId'>) {
const { send } = useSendMessageBySSE(api.runCanvas);
const { id } = useParams();
const { saveGraph, loading } = useSaveGraph();
const [uploadedFileData, setUploadedFileData] =
useState<Record<string, any>>();
const run = useCallback(
async (fileResponseData: Record<string, any>) => {
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<typeof useRunDataflow>;

View File

@ -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<Record<string, any>>();
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 (
<section className="h-full">
<PageHeader>
@ -194,7 +206,7 @@ export default function Agent() {
>
<LaptopMinimalCheck /> {t('flow.save')}
</ButtonLoading>
<Button variant={'secondary'} onClick={run}>
<Button variant={'secondary'} onClick={handleButtonRunClick}>
<CirclePlay />
{t('flow.run')}
</Button>
@ -241,18 +253,14 @@ export default function Agent() {
</DropdownMenu>
</div>
</PageHeader>
<PipelineLogContext.Provider
value={{ messageId, setMessageId, setUploadedFileData }}
>
<ReactFlowProvider>
<DropdownProvider>
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></AgentCanvas>
</DropdownProvider>
</ReactFlowProvider>
</PipelineLogContext.Provider>
<ReactFlowProvider>
<DropdownProvider>
<AgentCanvas
drawerVisible={chatDrawerVisible}
hideDrawer={hideChatDrawer}
></AgentCanvas>
</DropdownProvider>
</ReactFlowProvider>
{embedVisible && (
<EmbedDialog
visible={embedVisible}
@ -284,6 +292,13 @@ export default function Agent() {
uploadedFileData={uploadedFileData}
></PipelineLogSheet>
)}
{pipelineRunSheetVisible && (
<PipelineRunSheet
hideModal={hidePipelineRunSheet}
run={runPipeline}
loading={pipelineRunning}
></PipelineRunSheet>
)}
</section>
);
}

View File

@ -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<any> &
Pick<RunDataflowType, 'run' | 'loading'>;
const PipelineRunSheet = ({ hideModal, run, loading }: RunSheetProps) => {
const { t } = useTranslation();
return (
<Sheet onOpenChange={hideModal} open modal={false}>
<SheetContent className={cn('top-20 p-2')}>
<SheetHeader>
<SheetTitle>{t('flow.testRun')}</SheetTitle>
<UploaderForm ok={run} loading={loading}></UploaderForm>
</SheetHeader>
</SheetContent>
</Sheet>
);
};
export default PipelineRunSheet;

View File

@ -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<typeof formSchema>;
type UploaderFormProps = {
ok: (values: FormSchemaType) => void;
loading: boolean;
};
export function UploaderForm({ ok, loading }: UploaderFormProps) {
const { t } = useTranslation();
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
defaultValues: {},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(ok)} className="space-y-8">
<RAGFlowFormItem name="file">
{(field) => {
return (
<FileUploadDirectUpload
value={field.value}
onChange={field.onChange}
></FileUploadDirectUpload>
);
}}
</RAGFlowFormItem>
<div>
<ButtonLoading
type="submit"
loading={loading}
className="w-full mt-1"
>
{t('flow.run')}
</ButtonLoading>
</div>
</form>
</Form>
);
}

View File

@ -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<Record<string, any>>,
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<string, ParserFormSchemaType['setups'][0]>
>((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;
}