mirror of
https://github.com/infiniflow/ragflow.git
synced 2025-12-08 20:42:30 +08:00
### 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:
@ -48,13 +48,3 @@ export type HandleContextType = {
|
|||||||
export const HandleContext = createContext<HandleContextType>(
|
export const HandleContext = createContext<HandleContextType>(
|
||||||
{} as 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,
|
|
||||||
);
|
|
||||||
|
|||||||
55
web/src/pages/agent/hooks/use-run-dataflow.ts
Normal file
55
web/src/pages/agent/hooks/use-run-dataflow.ts
Normal 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>;
|
||||||
@ -32,25 +32,26 @@ import {
|
|||||||
Settings,
|
Settings,
|
||||||
Upload,
|
Upload,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { ComponentPropsWithoutRef, useCallback, useState } from 'react';
|
import { ComponentPropsWithoutRef, useCallback } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useParams } from 'umi';
|
import { useParams } from 'umi';
|
||||||
import AgentCanvas from './canvas';
|
import AgentCanvas from './canvas';
|
||||||
import { DropdownProvider } from './canvas/context';
|
import { DropdownProvider } from './canvas/context';
|
||||||
import { Operator } from './constant';
|
import { Operator } from './constant';
|
||||||
import { PipelineLogContext } from './context';
|
|
||||||
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
|
import { useCancelCurrentDataflow } from './hooks/use-cancel-dataflow';
|
||||||
import { useHandleExportJsonFile } from './hooks/use-export-json';
|
import { useHandleExportJsonFile } from './hooks/use-export-json';
|
||||||
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
import { useFetchDataOnMount } from './hooks/use-fetch-data';
|
||||||
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
|
import { useFetchPipelineLog } from './hooks/use-fetch-pipeline-log';
|
||||||
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
|
import { useGetBeginNodeDataInputs } from './hooks/use-get-begin-query';
|
||||||
import { useIsPipeline } from './hooks/use-is-pipeline';
|
import { useIsPipeline } from './hooks/use-is-pipeline';
|
||||||
|
import { useRunDataflow } from './hooks/use-run-dataflow';
|
||||||
import {
|
import {
|
||||||
useSaveGraph,
|
useSaveGraph,
|
||||||
useSaveGraphBeforeOpeningDebugDrawer,
|
useSaveGraphBeforeOpeningDebugDrawer,
|
||||||
useWatchAgentChange,
|
useWatchAgentChange,
|
||||||
} from './hooks/use-save-graph';
|
} from './hooks/use-save-graph';
|
||||||
import { PipelineLogSheet } from './pipeline-log-sheet';
|
import { PipelineLogSheet } from './pipeline-log-sheet';
|
||||||
|
import PipelineRunSheet from './pipeline-run-sheet';
|
||||||
import { SettingDialog } from './setting-dialog';
|
import { SettingDialog } from './setting-dialog';
|
||||||
import useGraphStore from './store';
|
import useGraphStore from './store';
|
||||||
import { useAgentHistoryManager } from './use-agent-history-manager';
|
import { useAgentHistoryManager } from './use-agent-history-manager';
|
||||||
@ -110,6 +111,12 @@ export default function Agent() {
|
|||||||
|
|
||||||
// pipeline
|
// pipeline
|
||||||
|
|
||||||
|
const {
|
||||||
|
visible: pipelineRunSheetVisible,
|
||||||
|
hideModal: hidePipelineRunSheet,
|
||||||
|
showModal: showPipelineRunSheet,
|
||||||
|
} = useSetModalState();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
visible: pipelineLogSheetVisible,
|
visible: pipelineLogSheetVisible,
|
||||||
showModal: showPipelineLogSheet,
|
showModal: showPipelineLogSheet,
|
||||||
@ -126,8 +133,6 @@ export default function Agent() {
|
|||||||
isLogEmpty,
|
isLogEmpty,
|
||||||
} = useFetchPipelineLog(pipelineLogSheetVisible);
|
} = useFetchPipelineLog(pipelineLogSheetVisible);
|
||||||
|
|
||||||
const [uploadedFileData, setUploadedFileData] =
|
|
||||||
useState<Record<string, any>>();
|
|
||||||
const findNodeByName = useGraphStore((state) => state.findNodeByName);
|
const findNodeByName = useGraphStore((state) => state.findNodeByName);
|
||||||
|
|
||||||
const handleRunPipeline = useCallback(() => {
|
const handleRunPipeline = useCallback(() => {
|
||||||
@ -141,14 +146,15 @@ export default function Agent() {
|
|||||||
showPipelineLogSheet();
|
showPipelineLogSheet();
|
||||||
} else {
|
} else {
|
||||||
hidePipelineLogSheet();
|
hidePipelineLogSheet();
|
||||||
handleRun();
|
// handleRun();
|
||||||
|
showPipelineRunSheet();
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
findNodeByName,
|
findNodeByName,
|
||||||
handleRun,
|
|
||||||
hidePipelineLogSheet,
|
hidePipelineLogSheet,
|
||||||
isParsing,
|
isParsing,
|
||||||
showPipelineLogSheet,
|
showPipelineLogSheet,
|
||||||
|
showPipelineRunSheet,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -157,7 +163,7 @@ export default function Agent() {
|
|||||||
stopFetchTrace,
|
stopFetchTrace,
|
||||||
});
|
});
|
||||||
|
|
||||||
const run = useCallback(() => {
|
const handleButtonRunClick = useCallback(() => {
|
||||||
if (isPipeline) {
|
if (isPipeline) {
|
||||||
handleRunPipeline();
|
handleRunPipeline();
|
||||||
} else {
|
} else {
|
||||||
@ -165,6 +171,12 @@ export default function Agent() {
|
|||||||
}
|
}
|
||||||
}, [handleRunAgent, handleRunPipeline, isPipeline]);
|
}, [handleRunAgent, handleRunPipeline, isPipeline]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
run: runPipeline,
|
||||||
|
loading: pipelineRunning,
|
||||||
|
uploadedFileData,
|
||||||
|
} = useRunDataflow({ showLogSheet: showPipelineLogSheet, setMessageId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="h-full">
|
<section className="h-full">
|
||||||
<PageHeader>
|
<PageHeader>
|
||||||
@ -194,7 +206,7 @@ export default function Agent() {
|
|||||||
>
|
>
|
||||||
<LaptopMinimalCheck /> {t('flow.save')}
|
<LaptopMinimalCheck /> {t('flow.save')}
|
||||||
</ButtonLoading>
|
</ButtonLoading>
|
||||||
<Button variant={'secondary'} onClick={run}>
|
<Button variant={'secondary'} onClick={handleButtonRunClick}>
|
||||||
<CirclePlay />
|
<CirclePlay />
|
||||||
{t('flow.run')}
|
{t('flow.run')}
|
||||||
</Button>
|
</Button>
|
||||||
@ -241,9 +253,6 @@ export default function Agent() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<PipelineLogContext.Provider
|
|
||||||
value={{ messageId, setMessageId, setUploadedFileData }}
|
|
||||||
>
|
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<DropdownProvider>
|
<DropdownProvider>
|
||||||
<AgentCanvas
|
<AgentCanvas
|
||||||
@ -252,7 +261,6 @@ export default function Agent() {
|
|||||||
></AgentCanvas>
|
></AgentCanvas>
|
||||||
</DropdownProvider>
|
</DropdownProvider>
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
</PipelineLogContext.Provider>
|
|
||||||
{embedVisible && (
|
{embedVisible && (
|
||||||
<EmbedDialog
|
<EmbedDialog
|
||||||
visible={embedVisible}
|
visible={embedVisible}
|
||||||
@ -284,6 +292,13 @@ export default function Agent() {
|
|||||||
uploadedFileData={uploadedFileData}
|
uploadedFileData={uploadedFileData}
|
||||||
></PipelineLogSheet>
|
></PipelineLogSheet>
|
||||||
)}
|
)}
|
||||||
|
{pipelineRunSheetVisible && (
|
||||||
|
<PipelineRunSheet
|
||||||
|
hideModal={hidePipelineRunSheet}
|
||||||
|
run={runPipeline}
|
||||||
|
loading={pipelineRunning}
|
||||||
|
></PipelineRunSheet>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
31
web/src/pages/agent/pipeline-run-sheet/index.tsx
Normal file
31
web/src/pages/agent/pipeline-run-sheet/index.tsx
Normal 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;
|
||||||
57
web/src/pages/agent/pipeline-run-sheet/uploader.tsx
Normal file
57
web/src/pages/agent/pipeline-run-sheet/uploader.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -9,16 +9,30 @@ import { removeUselessFieldsFromValues } from '@/utils/form';
|
|||||||
import { Edge, Node, XYPosition } from '@xyflow/react';
|
import { Edge, Node, XYPosition } from '@xyflow/react';
|
||||||
import { FormInstance, FormListFieldData } from 'antd';
|
import { FormInstance, FormListFieldData } from 'antd';
|
||||||
import { humanId } from 'human-id';
|
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 pipe from 'lodash/fp/pipe';
|
||||||
import isObject from 'lodash/isObject';
|
import isObject from 'lodash/isObject';
|
||||||
import {
|
import {
|
||||||
CategorizeAnchorPointPositions,
|
CategorizeAnchorPointPositions,
|
||||||
|
FileType,
|
||||||
|
FileTypeSuffixMap,
|
||||||
NoCopyOperatorsList,
|
NoCopyOperatorsList,
|
||||||
NoDebugOperatorsList,
|
NoDebugOperatorsList,
|
||||||
NodeHandleId,
|
NodeHandleId,
|
||||||
Operator,
|
Operator,
|
||||||
} from './constant';
|
} 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';
|
import { BeginQuery, IPosition } from './interface';
|
||||||
|
|
||||||
function buildAgentExceptionGoto(edges: Edge[], nodeId: string) {
|
function buildAgentExceptionGoto(edges: Edge[], nodeId: string) {
|
||||||
@ -170,6 +184,92 @@ export function hasSubAgent(edges: Edge[], nodeId?: string) {
|
|||||||
return !!edge;
|
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
|
// construct a dsl based on the node information of the graph
|
||||||
export const buildDslComponentsByGraph = (
|
export const buildDslComponentsByGraph = (
|
||||||
nodes: RAGFlowNodeType[],
|
nodes: RAGFlowNodeType[],
|
||||||
@ -202,6 +302,21 @@ export const buildDslComponentsByGraph = (
|
|||||||
params = buildCategorize(edges, nodes, id);
|
params = buildCategorize(edges, nodes, id);
|
||||||
break;
|
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:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user