mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-07-26 10:23:28 +08:00
Feat: Support pipeline-related configuration at the document level. (#17212)
### Summary Feat: Support pipeline-related configuration at the document level.
This commit is contained in:
67
web/src/components/builtin-pipeline-form-field.tsx
Normal file
67
web/src/components/builtin-pipeline-form-field.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { SelectWithSearch } from '@/components/originui/select-with-search';
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { useFetchBuiltinPipelines } from '@/hooks/use-agent-request';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function BuiltinPipelineItem({
|
||||
line = 2,
|
||||
name = 'parser_id',
|
||||
}: {
|
||||
line?: 1 | 2;
|
||||
name?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
const { options: builtinPipelineOptions } = useFetchBuiltinPipelines();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div
|
||||
className={cn('flex', {
|
||||
'items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
required
|
||||
className={cn('text-sm whitespace-wrap', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('knowledgeConfiguration.builtIn')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<SelectWithSearch
|
||||
{...field}
|
||||
placeholder={t(
|
||||
'knowledgeConfiguration.chunkMethodPlaceholder',
|
||||
)}
|
||||
options={builtinPipelineOptions}
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
ChunkMethodItem,
|
||||
EnableTocToggle,
|
||||
ImageContextWindow,
|
||||
ParseTypeItem,
|
||||
} from '@/pages/dataset/dataset-setting/configuration/common-item';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import omit from 'lodash/omit';
|
||||
@@ -44,6 +43,7 @@ import { ExcelToHtmlFormField } from '../excel-to-html-form-field';
|
||||
import { LayoutRecognizeFormField } from '../layout-recognize-form-field';
|
||||
import { MaxTokenNumberFormField } from '../max-token-number-from-field';
|
||||
import { MinerUOptionsFormField } from '../mineru-options-form-field';
|
||||
import { ParseTypeItem } from '../parse-type-form-field';
|
||||
import { ButtonLoading } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { DynamicPageRange } from './dynamic-page-range';
|
||||
|
||||
109
web/src/components/document-pipeline-dialog/index.tsx
Normal file
109
web/src/components/document-pipeline-dialog/index.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { BuiltinPipelineItem } from '@/components/builtin-pipeline-form-field';
|
||||
import { DataFlowSelect } from '@/components/data-pipeline-select';
|
||||
import { ParseTypeItem } from '@/components/parse-type-form-field';
|
||||
import PipelineOperatorTabs from '@/components/pipeline-operator-tabs';
|
||||
import { ButtonLoading } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Form } from '@/components/ui/form';
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import { IModalProps } from '@/interfaces/common';
|
||||
import { IChangeParserRequestBody } from '@/interfaces/request/document';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
IDocumentPipelineDialogProps,
|
||||
useDocumentPipelineForm,
|
||||
} from './use-document-pipeline-form';
|
||||
|
||||
const FormId = 'DocumentPipelineDialogForm';
|
||||
|
||||
interface IProps
|
||||
extends IModalProps<IChangeParserRequestBody>, IDocumentPipelineDialogProps {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function DocumentPipelineDialog({
|
||||
hideModal,
|
||||
onOk,
|
||||
parserId,
|
||||
pipelineId,
|
||||
parserConfig,
|
||||
loading,
|
||||
}: IProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const {
|
||||
form,
|
||||
parseType,
|
||||
operatorNodes,
|
||||
operatorNodesLoading,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
handleOperatorValuesChange,
|
||||
showOperatorTabs,
|
||||
buildSubmitData,
|
||||
} = useDocumentPipelineForm({ parserId, pipelineId, parserConfig });
|
||||
|
||||
const onSubmit = useCallback(
|
||||
async (data: Parameters<typeof buildSubmitData>[0]) => {
|
||||
const ret = await onOk?.(buildSubmitData(data));
|
||||
if (ret) {
|
||||
hideModal?.();
|
||||
}
|
||||
},
|
||||
[buildSubmitData, hideModal, onOk],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open onOpenChange={hideModal}>
|
||||
<DialogContent className="max-w-[50vw] text-text-primary">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('knowledgeDetails.chunkMethod')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 max-h-[70vh] overflow-auto -mx-6 px-10 py-5"
|
||||
id={FormId}
|
||||
>
|
||||
<ParseTypeItem />
|
||||
{parseType === ParseType.BuiltIn && <BuiltinPipelineItem />}
|
||||
{parseType === ParseType.Pipeline && (
|
||||
<DataFlowSelect
|
||||
isMult={false}
|
||||
showToDataPipeline={true}
|
||||
formFieldName="pipeline_id"
|
||||
/>
|
||||
)}
|
||||
{showOperatorTabs && (
|
||||
<PipelineOperatorTabs
|
||||
nodes={operatorNodes}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
onOperatorValuesChange={handleOperatorValuesChange}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
<DialogFooter>
|
||||
<ButtonLoading
|
||||
type="submit"
|
||||
form={FormId}
|
||||
loading={
|
||||
loading || (operatorNodesLoading && operatorNodes.length === 0)
|
||||
}
|
||||
>
|
||||
{t('common.save')}
|
||||
</ButtonLoading>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import {
|
||||
useActiveTab,
|
||||
usePipelineOperatorNodes,
|
||||
useResetParserConfigOnPipelineChange,
|
||||
} from '@/hooks/use-pipeline-operator';
|
||||
import { IChangeParserRequestBody } from '@/interfaces/request/document';
|
||||
import {
|
||||
getOperatorType,
|
||||
transformApiConfigToForm,
|
||||
transformFormConfigToApi,
|
||||
} from '@/utils/pipeline-operator';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useForm, useWatch } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { z } from 'zod';
|
||||
|
||||
export interface IDocumentPipelineDialogProps {
|
||||
parserId: string;
|
||||
pipelineId?: string;
|
||||
parserConfig?: Record<string, any>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the saved parser_config (API format, keyed by operator id) to the
|
||||
* form format expected by the operator tabs, mirroring
|
||||
* useFetchDatasetSettingOnMount on the dataset setting page.
|
||||
*/
|
||||
function transformSavedParserConfigToForm(
|
||||
parserConfig?: Record<string, any>,
|
||||
): Record<string, any> {
|
||||
if (
|
||||
!parserConfig ||
|
||||
typeof parserConfig !== 'object' ||
|
||||
Array.isArray(parserConfig)
|
||||
) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const hasPipelineKeys = Object.keys(parserConfig).some((key) =>
|
||||
key.includes(':'),
|
||||
);
|
||||
if (!hasPipelineKeys) {
|
||||
return parserConfig;
|
||||
}
|
||||
|
||||
const formParserConfig: Record<string, any> = {};
|
||||
for (const [operatorId, config] of Object.entries(parserConfig)) {
|
||||
formParserConfig[operatorId] = transformApiConfigToForm(
|
||||
getOperatorType(operatorId),
|
||||
config as Record<string, any>,
|
||||
);
|
||||
}
|
||||
return formParserConfig;
|
||||
}
|
||||
|
||||
export function useDocumentPipelineForm({
|
||||
parserId,
|
||||
pipelineId,
|
||||
parserConfig,
|
||||
}: IDocumentPipelineDialogProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const FormSchema = useMemo(
|
||||
() =>
|
||||
z
|
||||
.object({
|
||||
parseType: z.nativeEnum(ParseType),
|
||||
parser_id: z.string(),
|
||||
pipeline_id: z.string().optional(),
|
||||
parser_config: z.record(z.string(), z.any()).optional(),
|
||||
})
|
||||
.superRefine((data, ctx) => {
|
||||
if (data.parseType === ParseType.BuiltIn && !data.parser_id.trim()) {
|
||||
ctx.addIssue({
|
||||
path: ['parser_id'],
|
||||
message: t('common.pleaseSelect'),
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
if (data.parseType === ParseType.Pipeline && !data.pipeline_id) {
|
||||
ctx.addIssue({
|
||||
path: ['pipeline_id'],
|
||||
message: t('common.pleaseSelect'),
|
||||
code: 'custom',
|
||||
});
|
||||
}
|
||||
}),
|
||||
[t],
|
||||
);
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
parseType: pipelineId ? ParseType.Pipeline : ParseType.BuiltIn,
|
||||
parser_id: parserId || '',
|
||||
pipeline_id: pipelineId || '',
|
||||
parser_config: transformSavedParserConfigToForm(parserConfig),
|
||||
},
|
||||
});
|
||||
|
||||
const parseType = useWatch({
|
||||
control: form.control,
|
||||
name: 'parseType',
|
||||
});
|
||||
const selectedDataFlowId = useWatch({
|
||||
control: form.control,
|
||||
name: 'pipeline_id',
|
||||
});
|
||||
const selectedBuiltinId = useWatch({
|
||||
control: form.control,
|
||||
name: 'parser_id',
|
||||
});
|
||||
|
||||
const isPipelineMode = parseType === ParseType.Pipeline;
|
||||
const selectedPipelineId = isPipelineMode
|
||||
? selectedDataFlowId
|
||||
: selectedBuiltinId;
|
||||
// The saved parser_config only belongs to the pipeline (and parse type) it
|
||||
// was saved with — expose its id only while that exact pipeline is selected,
|
||||
// so that after switching, defaults come purely from the new pipeline's DSL.
|
||||
const savedParseType = pipelineId ? ParseType.Pipeline : ParseType.BuiltIn;
|
||||
const savedPipelineId =
|
||||
parseType === savedParseType
|
||||
? isPipelineMode
|
||||
? pipelineId
|
||||
: parserId
|
||||
: undefined;
|
||||
|
||||
const { operatorNodes, loading: operatorNodesLoading } =
|
||||
usePipelineOperatorNodes(
|
||||
selectedPipelineId,
|
||||
selectedPipelineId && selectedPipelineId === savedPipelineId
|
||||
? parserConfig
|
||||
: undefined,
|
||||
!isPipelineMode,
|
||||
);
|
||||
|
||||
useResetParserConfigOnPipelineChange(
|
||||
form,
|
||||
selectedPipelineId,
|
||||
savedPipelineId,
|
||||
operatorNodes,
|
||||
);
|
||||
|
||||
const { activeTab, setActiveTab } = useActiveTab(operatorNodes);
|
||||
|
||||
useEffect(() => {
|
||||
if (parseType === ParseType.BuiltIn) {
|
||||
form.setValue('pipeline_id', '');
|
||||
}
|
||||
}, [parseType, form]);
|
||||
|
||||
const handleOperatorValuesChange = useCallback(
|
||||
(operatorId: string, values: any) => {
|
||||
const currentParserConfig = form.getValues('parser_config') || {};
|
||||
form.setValue('parser_config', {
|
||||
...currentParserConfig,
|
||||
[operatorId]: values,
|
||||
});
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
const showOperatorTabs =
|
||||
operatorNodes.length > 0 &&
|
||||
((parseType === ParseType.Pipeline && !!selectedDataFlowId) ||
|
||||
(parseType === ParseType.BuiltIn && !!selectedBuiltinId));
|
||||
|
||||
const buildSubmitData = useCallback(
|
||||
(data: z.infer<typeof FormSchema>): IChangeParserRequestBody => {
|
||||
const transformedConfig: Record<string, any> = {};
|
||||
for (const [operatorId, config] of Object.entries(
|
||||
data.parser_config ?? {},
|
||||
)) {
|
||||
transformedConfig[operatorId] = transformFormConfigToApi(
|
||||
getOperatorType(operatorId),
|
||||
config as Record<string, any>,
|
||||
);
|
||||
}
|
||||
|
||||
const isPipeline = data.parseType === ParseType.Pipeline;
|
||||
return {
|
||||
parser_id: isPipeline ? '' : data.parser_id,
|
||||
pipeline_id: isPipeline ? data.pipeline_id : '',
|
||||
parser_config: transformedConfig,
|
||||
};
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return {
|
||||
form,
|
||||
parseType,
|
||||
operatorNodes,
|
||||
operatorNodesLoading,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
handleOperatorValuesChange,
|
||||
showOperatorTabs,
|
||||
buildSubmitData,
|
||||
};
|
||||
}
|
||||
73
web/src/components/parse-type-form-field.tsx
Normal file
73
web/src/components/parse-type-form-field.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
import { Radio } from '@/components/ui/radio';
|
||||
import { ParseType } from '@/constants/knowledge';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export function ParseTypeItem({
|
||||
line = 2,
|
||||
name = 'parseType',
|
||||
}: {
|
||||
line?: number;
|
||||
name?: string;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const form = useFormContext();
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={name}
|
||||
render={({ field }) => (
|
||||
<FormItem className="items-center space-y-0">
|
||||
<div
|
||||
className={cn('flex', {
|
||||
'items-center': line === 1,
|
||||
'flex-col gap-1': line === 2,
|
||||
})}
|
||||
>
|
||||
<FormLabel
|
||||
className={cn('text-sm whitespace-wrap ', {
|
||||
'w-1/4': line === 1,
|
||||
})}
|
||||
>
|
||||
{t('knowledgeConfiguration.parseType')}
|
||||
</FormLabel>
|
||||
<div
|
||||
className={cn('text-muted-foreground', { 'w-3/4': line === 1 })}
|
||||
>
|
||||
<FormControl>
|
||||
<Radio.Group {...field}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex gap-2 justify-between text-muted-foreground',
|
||||
line === 1 ? 'w-1/2' : 'w-3/4',
|
||||
)}
|
||||
>
|
||||
<Radio value={ParseType.BuiltIn}>
|
||||
{t('knowledgeConfiguration.builtIn')}
|
||||
</Radio>
|
||||
<Radio value={ParseType.Pipeline}>
|
||||
{t('knowledgeConfiguration.manualSetup')}
|
||||
</Radio>
|
||||
</div>
|
||||
</Radio.Group>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex pt-1">
|
||||
<div className={line === 1 ? 'w-1/4' : ''}></div>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
66
web/src/components/pipeline-operator-tabs/index.tsx
Normal file
66
web/src/components/pipeline-operator-tabs/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import { memo, useCallback } from 'react';
|
||||
import PipelineOperatorForm from './pipeline-operator-form';
|
||||
|
||||
type PipelineOperatorTabsProps = {
|
||||
nodes: RAGFlowNodeType[];
|
||||
value: string;
|
||||
onValueChange: (value: string) => void;
|
||||
onOperatorValuesChange: (operatorId: string, values: any) => void;
|
||||
};
|
||||
|
||||
const PipelineOperatorTabs = ({
|
||||
nodes,
|
||||
value,
|
||||
onValueChange,
|
||||
onOperatorValuesChange,
|
||||
}: PipelineOperatorTabsProps) => {
|
||||
const getOperatorId = useCallback((node: RAGFlowNodeType) => {
|
||||
return (
|
||||
(node.data as Record<string, any>)?.operatorId || node.data?.label || ''
|
||||
);
|
||||
}, []);
|
||||
|
||||
const getTabValue = useCallback(
|
||||
(node: RAGFlowNodeType, index: number) => {
|
||||
return getOperatorId(node) || String(index);
|
||||
},
|
||||
[getOperatorId],
|
||||
);
|
||||
|
||||
const handleValuesChange = useCallback(
|
||||
(node: RAGFlowNodeType) => (values: any) => {
|
||||
onOperatorValuesChange(getOperatorId(node), values);
|
||||
},
|
||||
[getOperatorId, onOperatorValuesChange],
|
||||
);
|
||||
|
||||
return (
|
||||
<Tabs value={value} onValueChange={onValueChange} className="w-full">
|
||||
<TabsList className="w-full justify-start">
|
||||
{nodes.map((node, index) => {
|
||||
const tabValue = getTabValue(node, index);
|
||||
return (
|
||||
<TabsTrigger key={tabValue} value={tabValue}>
|
||||
{node.data?.name || node.data?.label || tabValue}
|
||||
</TabsTrigger>
|
||||
);
|
||||
})}
|
||||
</TabsList>
|
||||
{nodes.map((node, index) => {
|
||||
const tabValue = getTabValue(node, index);
|
||||
return (
|
||||
<TabsContent key={tabValue} value={tabValue}>
|
||||
<PipelineOperatorForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange(node)}
|
||||
/>
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PipelineOperatorTabs);
|
||||
@@ -0,0 +1,77 @@
|
||||
import { Operator } from '@/constants/agent';
|
||||
import { RAGFlowNodeType } from '@/interfaces/database/agent';
|
||||
import ExtractorForm from '@/pages/agent/form/extractor-form';
|
||||
import ParserForm from '@/pages/agent/form/parser-form';
|
||||
import TitleChunkerForm from '@/pages/agent/form/title-chunker-form';
|
||||
import TokenChunkerForm from '@/pages/agent/form/token-chunker-form';
|
||||
import TokenizerForm from '@/pages/agent/form/tokenizer-form';
|
||||
import { getOperatorType } from '@/utils/pipeline-operator';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
type PipelineOperatorFormProps = {
|
||||
node: RAGFlowNodeType;
|
||||
onValuesChange?: (values: any) => void;
|
||||
};
|
||||
|
||||
const PipelineOperatorForm = ({
|
||||
node,
|
||||
onValuesChange,
|
||||
}: PipelineOperatorFormProps) => {
|
||||
const operatorType = getOperatorType(
|
||||
(node.data as Record<string, any>)?.operatorId || node.data?.label || '',
|
||||
);
|
||||
|
||||
const handleValuesChange = useCallback(
|
||||
(values: any) => {
|
||||
onValuesChange?.(values);
|
||||
},
|
||||
[onValuesChange],
|
||||
);
|
||||
|
||||
switch (operatorType) {
|
||||
case Operator.Parser:
|
||||
return (
|
||||
<ParserForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.TokenChunker:
|
||||
return (
|
||||
<TokenChunkerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.TitleChunker:
|
||||
return (
|
||||
<TitleChunkerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.Extractor:
|
||||
return (
|
||||
<ExtractorForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
case Operator.Tokenizer:
|
||||
return (
|
||||
<TokenizerForm
|
||||
node={node}
|
||||
onValuesChange={handleValuesChange}
|
||||
hideOutputs
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default memo(PipelineOperatorForm);
|
||||
Reference in New Issue
Block a user