Feat: Add page rank to ParserForm (#17267)

This commit is contained in:
balibabu
2026-07-23 14:23:07 +08:00
committed by GitHub
parent 4a2564c7da
commit c48a59db96
15 changed files with 282 additions and 175 deletions

View File

@@ -25,6 +25,7 @@ type AsyncTreeSelectProps = {
value?: TreeId;
onChange?(value: TreeId): void;
loadData?(node: TreeNodeType): Promise<any>;
canSelect?(node: TreeNodeType): boolean;
};
function getNodeText(node: ReactNode): string {
@@ -39,6 +40,7 @@ export function AsyncTreeSelect({
value,
loadData,
onChange,
canSelect,
}: AsyncTreeSelectProps) {
const [open, setOpen] = useState(false);
const [searchText, setSearchText] = useState('');
@@ -106,19 +108,8 @@ export function AsyncTreeSelect({
[expandedKeys, searchText, treeData, visibleNodeIds],
);
const handleNodeClick = useCallback(
(id: TreeId) => (e: React.MouseEvent<HTMLLIElement>) => {
e.stopPropagation();
onChange?.(id);
setOpen(false);
setSearchText('');
},
[onChange],
);
const handleArrowClick = useCallback(
(node: TreeNodeType) => async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
const toggleNode = useCallback(
async (node: TreeNodeType) => {
const { id } = node;
if (isExpanded(id)) {
setExpandedKeys((keys) => {
@@ -140,6 +131,28 @@ export function AsyncTreeSelect({
[isExpanded, loadData, treeData],
);
const handleNodeClick = useCallback(
(node: TreeNodeType) => async (e: React.MouseEvent<HTMLLIElement>) => {
e.stopPropagation();
if (canSelect && !canSelect(node)) {
await toggleNode(node);
return;
}
onChange?.(node.id);
setOpen(false);
setSearchText('');
},
[canSelect, onChange, toggleNode],
);
const handleArrowClick = useCallback(
(node: TreeNodeType) => async (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
await toggleNode(node);
},
[toggleNode],
);
const handleOpenChange = useCallback((open: boolean) => {
setOpen(open);
if (!open) {
@@ -166,7 +179,7 @@ export function AsyncTreeSelect({
{currentLevelList.map((x) => (
<li
key={x.id}
onClick={handleNodeClick(x.id)}
onClick={handleNodeClick(x)}
className="cursor-pointer"
>
<div

View File

@@ -631,6 +631,7 @@ Example: A 1 KB message with 1024-dim embedding uses ~9 KB. The 5 MB default lim
autoKeywordsTip: `Automatically extract N keywords for each chunk to increase their ranking for queries containing those keywords. Be aware that extra tokens will be consumed by the indexing model specified in 'Configuration'. You can check or update the added keywords for a chunk from the chunk list. For details, see https://ragflow.io/docs/dev/autokeyword_autoquestion.`,
autoQuestions: 'Auto-question',
autoQuestionsTip: `Automatically extract N questions for each chunk to increase their ranking for queries containing those questions. You can check or update the added questions for a chunk from the chunk list. This feature will not disrupt the chunking process if an error occurs, except that it may add an empty result to the original chunk. Be aware that extra tokens will be consumed by the indexing model specified in 'Configuration'. For details, see https://ragflow.io/docs/dev/autokeyword_autoquestion.`,
autoTags: 'Auto-tags',
redo: 'Do you want to clear the existing {{chunkNum}} chunks?',
setMetaData: 'Set metadata',
pleaseInputJson: 'Please enter JSON',
@@ -3015,6 +3016,7 @@ This delimiter is used to split the input text into several text pieces echo of
systemPrompt: 'System prompt',
userPrompt: 'User prompt',
tocDataSource: 'Data source',
tagFile: 'Tag file',
addCategory: 'Add category',
categoryName: 'Category name',
nextStep: 'Next step',

View File

@@ -574,6 +574,7 @@ export default {
autoKeywordsTip: `自动为每个文本块中提取 N 个关键词,用以提升查询精度。请注意:该功能采用在“配置”中指定的索引模型提取关键词,因此也会产生更多 Token 消耗。另外,你也可以手动更新生成的关键词。详情请见 https://ragflow.io/docs/dev/autokeyword_autoquestion。`,
autoQuestions: '自动问题提取',
autoQuestionsTip: `利用在“配置”中指定的索引模型 对知识库的每个文本块提取 N 个问题以提高其排名得分。请注意,开启后将消耗额外的 token。您可以在块列表中查看、编辑结果。如果自动问题提取发生错误不会妨碍整个分块过程只会将空结果添加到原始文本块。详情请见 https://ragflow.io/docs/dev/autokeyword_autoquestion。`,
autoTags: '自动标签提取',
redo: '是否清空已有 {{chunkNum}}个 chunk',
setMetaData: '设置元数据',
pleaseInputJson: '请输入JSON',
@@ -2682,6 +2683,7 @@ NER使用 spaCy NER 和基于规则的关键词提取来抽取实体和关系
},
systemPrompt: '系统提示词',
userPrompt: '用户提示词',
tagFile: '标签文件',
prompt: '提示词',
promptMessage: '提示词是必填项',
promptTip:

View File

@@ -197,6 +197,7 @@ export const initialParserValues = {
preprocess: PreprocessValue.main_content,
flatten_media_to_text: false,
remove_header_footer: false,
pages: [{ from: 1, to: 100000 }],
},
{
fileFormat: FileType.Spreadsheet,
@@ -353,6 +354,8 @@ export const initialExtractorValues = {
field_name: ContextGeneratorFieldName.Summary,
auto_keywords: 0,
auto_questions: 0,
auto_tags: 1,
tag_file_id: '',
outputs: {
chunks: { type: 'Array<Object>', value: [] },
},

View File

@@ -7,8 +7,11 @@ import { LargeModelFormField } from '@/components/large-model-form-field';
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { SliderInputFormField } from '@/components/slider-input-form-field';
import { AsyncTreeSelect } from '@/components/ui/async-tree-select';
import { Form } from '@/components/ui/form';
import { PromptEditor } from '@/pages/agent/form/components/prompt-editor';
import { isGoBackend } from '@/utils/backend-runtime';
import { buildOptions } from '@/utils/form';
import { zodResolver } from '@hookform/resolvers/zod';
import { memo } from 'react';
@@ -29,6 +32,8 @@ import { buildOutputList } from '../../utils/build-output-list';
import { FormWrapper } from '../components/form-wrapper';
import { Output } from '../components/output';
import { useSwitchPrompt } from './use-switch-prompt';
import { canSelectTagFile, useTagFileTree } from './use-tag-file-tree';
import { FormLayout } from '@/constants/form';
export const FormSchema = z.object({
field_name: z.string(),
@@ -36,6 +41,8 @@ export const FormSchema = z.object({
prompts: z.string().optional(),
auto_keywords: z.number().optional(),
auto_questions: z.number().optional(),
auto_tags: z.number().optional(),
tag_file_id: z.string().optional(),
...LlmSettingSchema,
});
@@ -75,6 +82,8 @@ const ExtractorForm = ({
const ownerTenantId = useOwnerTenantId();
const isToc = form.getValues('field_name') === 'toc';
const { treeData, loadData } = useTagFileTree();
return (
<Form {...form}>
<FormWrapper>
@@ -83,6 +92,31 @@ const ExtractorForm = ({
></LargeModelFormField>
<AutoKeywordsFormField name="auto_keywords"></AutoKeywordsFormField>
<AutoQuestionsFormField name="auto_questions"></AutoQuestionsFormField>
{isGoBackend() && (
<>
<SliderInputFormField
name="auto_tags"
label={t('knowledgeDetails.autoTags')}
min={1}
max={10}
defaultValue={1}
layout={FormLayout.Vertical}
></SliderInputFormField>
<RAGFlowFormItem label={t('flow.tagFile')} name="tag_file_id">
{(field) => (
<AsyncTreeSelect
treeData={treeData}
value={field.value}
onChange={field.onChange}
loadData={loadData}
canSelect={canSelectTagFile}
></AsyncTreeSelect>
)}
</RAGFlowFormItem>
</>
)}
<RAGFlowFormItem label={t('flow.fieldName')} name="field_name">
{(field) => (
<SelectWithSearch

View File

@@ -1,26 +1,25 @@
import { LlmSettingSchema } from '@/components/llm-setting-items/next';
import { useSetModalState } from '@/hooks/common-hooks';
import { useCallback, useRef } from 'react';
import { UseFormReturn } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
export const FormSchema = z.object({
field_name: z.string(),
sys_prompt: z.string(),
prompts: z.string().optional(),
...LlmSettingSchema,
});
type SwitchPromptField = 'field_name' | 'sys_prompt' | 'prompts';
export type ExtractorFormSchemaType = z.infer<typeof FormSchema>;
type SwitchPromptForm = {
getValues(name: 'field_name'): string;
setValue(
name: SwitchPromptField,
value: string,
options?: { shouldDirty?: boolean; shouldValidate?: boolean },
): void;
};
export function useSwitchPrompt(form: UseFormReturn<ExtractorFormSchemaType>) {
export function useSwitchPrompt(form: SwitchPromptForm) {
const { visible, showModal, hideModal } = useSetModalState();
const { t } = useTranslation();
const previousFieldNames = useRef<string[]>([form.getValues('field_name')]);
const setPromptValue = useCallback(
(field: keyof ExtractorFormSchemaType, key: string, value: string) => {
(field: SwitchPromptField, key: string, value: string) => {
form.setValue(field, t(`flow.prompts.${key}.${value}`), {
shouldDirty: true,
shouldValidate: true,

View File

@@ -0,0 +1,52 @@
import { TreeNodeType } from '@/components/ui/async-tree-select';
import { useFetchPureFileList } from '@/hooks/use-file-request';
import { IFile } from '@/interfaces/database/file-manager';
import { isFolderType } from '@/pages/files/util';
import { getExtension } from '@/utils/document-util';
import { uniqBy } from 'lodash';
import { useCallback, useState } from 'react';
const AllowedExtensions = ['xlsx', 'xls', 'csv', 'txt'];
export function canSelectTagFile(node: TreeNodeType) {
return Boolean(node.isLeaf);
}
export function useTagFileTree() {
const { fetchList } = useFetchPureFileList();
const [treeData, setTreeData] = useState<TreeNodeType[]>([]);
const loadData = useCallback(
async ({ id }: TreeNodeType) => {
const ret = await fetchList(id as string);
if (ret.code === 0) {
setTreeData((tree) =>
uniqBy(
tree.concat(
ret.data.files
.filter(
(x: IFile) =>
(isFolderType(x.type) &&
x.name.toLowerCase() !== 'skills') ||
AllowedExtensions.includes(getExtension(x.name)),
)
.map((x: IFile) => ({
id: x.id,
parentId: x.parent_id,
title: x.name,
// has_child_folder only counts child folders, so folders
// must always be expandable to reach files inside them
isLeaf: !isFolderType(x.type),
})),
),
'id',
),
);
}
},
[fetchList],
);
return { treeData, loadData };
}

View File

@@ -0,0 +1,107 @@
import { Button } from '@/components/ui/button';
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { Separator } from '@/components/ui/separator';
import { LucidePlus, LucideTrash2 } from 'lucide-react';
import { useCallback } from 'react';
import { useFieldArray, useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { CommonProps } from './interface';
import { buildFieldNameWithPrefix } from './utils';
export function DynamicPageRange({ prefix }: CommonProps) {
const { t } = useTranslation();
const form = useFormContext();
const pagesName = buildFieldNameWithPrefix('pages', prefix);
const { fields, remove, append } = useFieldArray({
name: pagesName,
control: form.control,
});
const handleAppend = useCallback(() => {
append({ from: 1, to: 100000 });
}, [append]);
return (
<div>
<FormLabel tooltip={t('knowledgeDetails.pageRangesTip')}>
{t('knowledgeDetails.pageRanges')}
</FormLabel>
{fields.map((field, index) => {
return (
<div key={field.id} className="flex items-center gap-2 pt-2">
<FormField
control={form.control}
name={`${pagesName}.${index}.from`}
render={({ field }) => (
<FormItem className="w-2/5">
<FormDescription />
<FormControl>
<Input
type="number"
placeholder={t('common.pleaseInput')}
className="!m-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Separator className="w-3 "></Separator>
<FormField
control={form.control}
name={`${pagesName}.${index}.to`}
render={({ field }) => (
<FormItem className="flex-1">
<FormDescription />
<FormControl>
<Input
type="number"
placeholder={t('common.pleaseInput')}
className="!m-0"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
className="ml-4"
size="icon"
variant="outline"
type="button"
onClick={() => remove(index)}
>
<LucideTrash2 />
</Button>
</div>
);
})}
<Button
onClick={handleAppend}
block
className="mt-4"
variant="dashed"
type="button"
>
<LucidePlus />
{t('knowledgeDetails.addPage')}
</Button>
</div>
);
}

View File

@@ -47,83 +47,6 @@ import { WordFormFields } from './word-form-fields';
const outputList = buildOutputList(initialParserValues.outputs);
// type PreprocessOptionConfig = {
// value: PreprocessValue;
// required?: boolean;
// };
// const DefaultPreprocessOptionConfigs: PreprocessOptionConfig[] = [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ];
// const PreprocessOptionConfigsMap: Partial<
// Record<FileType, PreprocessOptionConfig[]>
// > = {
// [FileType.PDF]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.abstract },
// { value: PreprocessValue.author },
// { value: PreprocessValue.section_title },
// ],
// [FileType.PowerPoint]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ],
// [FileType.Spreadsheet]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// ],
// [FileType.TextMarkdown]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Code]: [{ value: MAIN_CONTENT_PREPROCESS_VALUE, required: true }],
// [FileType.Html]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Doc]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// [FileType.Docx]: [
// { value: MAIN_CONTENT_PREPROCESS_VALUE, required: true },
// { value: PreprocessValue.section_title },
// ],
// };
// function getPreprocessOptionConfigs(fileType?: FileType) {
// if (!fileType) {
// return DefaultPreprocessOptionConfigs;
// }
// return PreprocessOptionConfigsMap[fileType] ?? DefaultPreprocessOptionConfigs;
// }
// function normalizePreprocessValuesByFileType(
// fileType: FileType | undefined,
// values: string[] | undefined,
// ) {
// const optionConfigs = getPreprocessOptionConfigs(fileType);
// const allowedValueSet = new Set(optionConfigs.map((x) => x.value));
// const requiredValues = optionConfigs
// .filter((x) => x.required)
// .map((x) => x.value);
// const normalizedOptionalValues = (Array.isArray(values) ? values : []).filter(
// (value) => allowedValueSet.has(value as PreprocessValue),
// ) as PreprocessValue[];
// return Array.from(
// new Set<PreprocessValue>([...requiredValues, ...normalizedOptionalValues]),
// );
// }
// function isSameStringArray(a: string[] | undefined, b: string[]) {
// if (!a || a.length !== b.length) {
// return false;
// }
// return a.every((item, idx) => item === b[idx]);
// }
const FileFormatWidgetMap = {
[FileType.PDF]: PdfFormFields,
[FileType.Spreadsheet]: SpreadsheetFormFields,
@@ -163,6 +86,9 @@ export const FormSchema = z.object({
enable_multi_column: z.boolean().optional(),
remove_toc: z.boolean().optional(),
remove_header_footer: z.boolean().optional(),
pages: z
.array(z.object({ from: z.coerce.number(), to: z.coerce.number() }))
.optional(),
}),
),
});
@@ -213,57 +139,6 @@ function ParserItem({
[form, index],
);
// const handlePreprocessChange = useCallback(
// (value: PreprocessValue[]) => {
// form.setValue(`setups.${index}.preprocess`, value, {
// shouldDirty: true,
// shouldValidate: true,
// shouldTouch: true,
// });
// },
// [form, index],
// );
// const preprocessOptions = useMemo(() => {
// const optionConfigs = getPreprocessOptionConfigs(fileFormat as FileType);
// return optionConfigs.map((optionConfig) => {
// const labelMap: Record<string, string> = {
// [MAIN_CONTENT_PREPROCESS_VALUE]: t('flow.preprocess.mainContent'),
// [PreprocessValue.section_title]: t('flow.preprocess.sectionTitle'),
// [PreprocessValue.abstract]: t('flow.preprocess.abstract'),
// [PreprocessValue.author]: t('flow.preprocess.author'),
// };
// const label = labelMap[optionConfig.value] || optionConfig.value;
// return {
// value: optionConfig.value,
// disabled: optionConfig.required,
// label: label,
// };
// });
// }, [fileFormat, t]);
// useEffect(() => {
// const currentPreprocessValues = form.getValues(
// `setups.${index}.preprocess`,
// ) as string[] | undefined;
// const normalizedPreprocessValues = normalizePreprocessValuesByFileType(
// fileFormat as FileType,
// currentPreprocessValues,
// );
// if (
// !isSameStringArray(currentPreprocessValues, normalizedPreprocessValues)
// ) {
// form.setValue(`setups.${index}.preprocess`, normalizedPreprocessValues, {
// shouldDirty: false,
// shouldValidate: true,
// });
// }
// }, [fileFormat, form, index]);
return (
<section
className={cn('space-y-5 py-2.5 rounded-md', {
@@ -302,26 +177,7 @@ function ParserItem({
fileType={fileFormat as FileType}
/>
</div>
{/* <RAGFlowFormItem
name={buildFieldNameWithPrefix(`preprocess`, prefix)}
label={t('flow.preprocess.preprocess')}
>
{(field) => (
<MultiSelect
value={field.value || []}
onValueChange={(val) => {
const nextValues = normalizePreprocessValuesByFileType(
fileFormat as FileType,
val,
);
field.onChange(nextValues);
handlePreprocessChange(nextValues);
}}
showSelectAll={false}
options={preprocessOptions}
></MultiSelect>
)}
</RAGFlowFormItem> */}
{index < fieldLength - 1 && <Separator />}
</section>
);

View File

@@ -8,6 +8,7 @@ import {
SelectWithSearchFlagOptionType,
} from '@/components/originui/select-with-search';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import { isGoBackend } from '@/utils/backend-runtime';
import { isEmpty } from 'lodash';
import { useEffect, useMemo } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
@@ -22,6 +23,7 @@ import {
TwoColumnCheckFormField,
} from './common-form-fields';
import { CommonProps } from './interface';
import { DynamicPageRange } from './dynamic-page-range';
import { useSetInitialLanguage } from './use-set-initial-language';
import { buildFieldNameWithPrefix } from './utils';
@@ -107,6 +109,7 @@ export function PdfFormFields({ prefix }: CommonProps) {
<RmdirFormField prefix={prefix} />
<RemoveHeaderFooterFormField prefix={prefix} />
<ParserMethodFormField prefix={prefix}></ParserMethodFormField>
{isGoBackend() && <DynamicPageRange prefix={prefix} />}
<FlattenMediaToTextFormField prefix={prefix} />
{!flattenMediaToText && (
<ModelTreeSelectFormField

View File

@@ -8,7 +8,7 @@ import {
ICategorizeItemResult,
RAGFlowNodeType,
} from '@/interfaces/database/agent';
import { getBackendLanguage } from '@/utils/backend-runtime';
import { getBackendLanguage, isGoBackend } from '@/utils/backend-runtime';
import { buildSelectOptions } from '@/utils/component-util';
import { buildOptions, removeUselessFieldsFromValues } from '@/utils/form';
import { Edge, Node, XYPosition } from '@xyflow/react';
@@ -212,6 +212,7 @@ export function transformParserParams(params: ParserFormSchemaType) {
ParserFormSchemaType['setups'][0] & { suffix: string[] } & {
two_column_check: boolean;
enable_multi_column: boolean;
pages: number[][];
}
> = {
output_format: cur.output_format,
@@ -230,6 +231,11 @@ export function transformParserParams(params: ParserFormSchemaType) {
enable_multi_column: cur.enable_multi_column,
remove_toc: cur.remove_toc,
remove_header_footer: cur.remove_header_footer || false,
...(isGoBackend()
? {
pages: cur.pages?.map((x) => [x.from, x.to]) ?? [],
}
: {}),
};
// Only include TCADP parameters if TCADP Parser is selected
if (cur.parse_method?.toLowerCase() === 'tcadp parser') {

View File

@@ -15,6 +15,7 @@ export const formSchema = z
parser_id: z.string().optional(),
avatar: z.any().nullish(),
permission: z.string().optional(),
language: z.string().optional(),
embedding_model: z.string(),
pagerank: z.number(),
parser_config: z.record(z.string(), z.any()).optional(),

View File

@@ -1,5 +1,7 @@
import { AvatarUpload } from '@/components/avatar-upload';
import { SelectWithSearch } from '@/components/originui/select-with-search';
import PageRankFormField from '@/components/page-rank-form-field';
import { RAGFlowFormItem } from '@/components/ragflow-form';
import {
FormControl,
FormField,
@@ -8,6 +10,9 @@ import {
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { LanguageTranslationMap } from '@/constants/common';
import { isGoBackend } from '@/utils/backend-runtime';
import { useMemo } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useKnowledgeBaseContext } from '../contexts/knowledge-base-context';
@@ -18,6 +23,13 @@ export function GeneralForm() {
const form = useFormContext();
const { t } = useTranslation();
const languageOptions = useMemo(() => {
return Object.keys(LanguageTranslationMap).map((x) => ({
label: x,
value: x,
}));
}, []);
return (
<>
<FormField
@@ -44,6 +56,21 @@ export function GeneralForm() {
</FormItem>
)}
/>
{isGoBackend() && (
<div className="items-center">
<RAGFlowFormItem
name="language"
label={t('common.language')}
horizontal={true}
>
<SelectWithSearch
options={languageOptions}
triggerClassName="w-full"
testId="ds-settings-basic-language-select"
></SelectWithSearch>
</RAGFlowFormItem>
</div>
)}
<FormField
control={form.control}
name="avatar"

View File

@@ -98,6 +98,7 @@ export const useFetchDatasetSettingOnMount = (
'description',
'name',
'permission',
'language',
'connectors',
'pagerank',
'avatar',

View File

@@ -52,6 +52,7 @@ export default function DatasetSetting() {
description: '',
avatar: null,
permission: '',
language: 'English',
embedding_model: '',
pagerank: 0,
connectors: [],