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:
balibabu
2026-07-22 22:24:50 +08:00
committed by GitHub
parent dccc0a4bee
commit b3d394954d
19 changed files with 702 additions and 263 deletions

View File

@@ -4,6 +4,20 @@
* and merge unknown fields into the `ext` field for flexible configuration.
*/
/**
* Pipeline parser configs are keyed by operator id (e.g. "Parser:xxx"), so a
* top-level key containing ":" marks the pipeline structure, which must be
* sent as-is instead of being reshaped by extractParserConfigExt.
*/
export const isPipelineParserConfig = (
parserConfig: Record<string, any> | undefined,
): boolean => {
if (!parserConfig || typeof parserConfig !== 'object') {
return false;
}
return Object.keys(parserConfig).some((key) => key.includes(':'));
};
/**
* Extracts Raptor configuration with extra fields merged into ext.
* @param raptorConfig - The raptor configuration object

View File

@@ -39,7 +39,10 @@ import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { extractParserConfigExt } from './parser-config-utils';
import {
extractParserConfigExt,
isPipelineParserConfig,
} from './parser-config-utils';
import {
useGetKnowledgeSearchParams,
useSetPaginationParams,
@@ -502,6 +505,66 @@ export const useSetDocumentParser = () => {
return { setDocumentParser: mutateAsync, data, loading };
};
/**
* Go-backend variant of useSetDocumentParser. The Go document endpoint takes
* `parser_id` (instead of the legacy `chunk_method`) and expects the
* pipeline-shaped parser_config (keyed by operator id) to be sent as-is.
* Keep it parallel to the Python version — the original hook stays untouched
* and can be dropped once the Python backend is retired.
*/
export const useSetDocumentPipelineParser = () => {
const queryClient = useQueryClient();
const {
data,
isPending: loading,
mutateAsync,
} = useMutation({
mutationKey: [DocumentApiAction.SetDocumentParser, 'pipeline'],
mutationFn: async ({
parserId,
pipelineId,
documentId,
datasetId,
parserConfig,
}: {
parserId: string;
pipelineId: string;
documentId: string;
datasetId: string;
parserConfig?: IChangeParserConfigRequestBody;
}) => {
const updateData: Record<string, unknown> = {
parser_id: parserId,
pipeline_id: pipelineId,
};
if (parserConfig) {
updateData.parser_config = isPipelineParserConfig(parserConfig)
? parserConfig
: extractParserConfigExt(parserConfig);
}
const { data } = await changeDocumentParser(
datasetId,
documentId,
updateData,
);
if (data.code === 0) {
queryClient.invalidateQueries({
queryKey: [DocumentApiAction.FetchDocumentList],
});
message.success(i18n.t('message.modified'));
}
return data.code;
},
});
return { setDocumentPipelineParser: mutateAsync, data, loading };
};
export const useSetDocumentMeta = () => {
const queryClient = useQueryClient();

View File

@@ -57,7 +57,10 @@ import {
useGetPaginationWithRouter,
useHandleSearchChange,
} from './logic-hooks';
import { extractParserConfigExt } from './parser-config-utils';
import {
extractParserConfigExt,
isPipelineParserConfig,
} from './parser-config-utils';
import { useSetPaginationParams } from './route-hook';
export const enum KnowledgeApiAction {
@@ -263,15 +266,6 @@ export const useDeleteKnowledge = () => {
return { data, loading, deleteKnowledge: mutateAsync };
};
function isPipelineParserConfig(
parserConfig: Record<string, any> | undefined,
): boolean {
if (!parserConfig || typeof parserConfig !== 'object') {
return false;
}
return Object.keys(parserConfig).some((key) => key.includes(':'));
}
export const useUpdateKnowledge = (shouldFetchList = false) => {
const knowledgeBaseId = useKnowledgeBaseId();
const queryClient = useQueryClient();

View File

@@ -0,0 +1,98 @@
import { useFetchPipelineDslByPipelineId } from '@/hooks/use-agent-request';
import { RAGFlowNodeType } from '@/interfaces/database/agent';
import {
buildParserConfigFromNodes,
buildPipelineOperatorNodes,
} from '@/utils/pipeline-operator';
import { useEffect, useMemo, useRef, useState } from 'react';
import { UseFormReturn } from 'react-hook-form';
export const usePipelineOperatorNodes = (
pipelineId?: string,
pipelineParserConfig?: Record<string, any>,
isBuiltin = false,
) => {
const { dsl, loading } = useFetchPipelineDslByPipelineId(
pipelineId,
isBuiltin,
);
const operatorNodes = useMemo(() => {
return buildPipelineOperatorNodes(dsl, pipelineParserConfig);
}, [dsl, pipelineParserConfig]);
return { operatorNodes, loading };
};
/**
* Resets parser_config when the selected pipeline changes, so stale configs
* from the previous pipeline are never submitted. Once the new pipeline's
* DSL has loaded, parser_config is seeded with the DSL defaults (i.e. what
* the operator tabs display).
*
* The very first pipeline seen is treated as the initial load: the form was
* already reset with the saved parser_config on mount, so it is left
* untouched.
*/
export const useResetParserConfigOnPipelineChange = (
form: UseFormReturn<any>,
pipelineId: string | undefined,
savedPipelineId: string | undefined,
operatorNodes: RAGFlowNodeType[],
) => {
const previousPipelineIdRef = useRef<string>();
useEffect(() => {
if (!pipelineId) {
// Selection cleared (e.g. parse type switched): drop configs that
// belonged to the previously selected pipeline.
if (previousPipelineIdRef.current) {
previousPipelineIdRef.current = '';
form.setValue('parser_config', {});
}
return;
}
const isInitialLoad =
previousPipelineIdRef.current === undefined &&
pipelineId === savedPipelineId;
if (isInitialLoad || previousPipelineIdRef.current === pipelineId) {
previousPipelineIdRef.current = pipelineId;
return;
}
if (operatorNodes.length === 0) {
// The new pipeline's DSL is still loading — clear the previous
// pipeline's configs right away; defaults are seeded once it arrives.
form.setValue('parser_config', {});
return;
}
previousPipelineIdRef.current = pipelineId;
form.setValue('parser_config', buildParserConfigFromNodes(operatorNodes));
}, [form, pipelineId, savedPipelineId, operatorNodes]);
};
export const useActiveTab = (operatorNodes: RAGFlowNodeType[]) => {
const [activeTab, setActiveTab] = useState('');
useEffect(() => {
if (operatorNodes.length > 0) {
const firstTab =
(operatorNodes[0].data as Record<string, any>)?.operatorId ||
operatorNodes[0].data?.label ||
'';
const validTabs = operatorNodes.map(
(node) =>
(node.data as Record<string, any>)?.operatorId ||
node.data?.label ||
'',
);
setActiveTab((prev) => (validTabs.includes(prev) ? prev : firstTab));
} else {
setActiveTab('');
}
}, [operatorNodes]);
return { activeTab, setActiveTab };
};