Compare commits

...

12 Commits

Author SHA1 Message Date
af6eabad0e Docs: Added v0.21.1 release notes (#10757)
### What problem does this PR solve?

### Type of change

- [x] Documentation Update
2025-10-23 17:25:29 +08:00
5fb5a51b2e Fix: create KB initial embedding. (#10751)
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 16:17:43 +08:00
37004ecfb3 Fix: Clicking "Stop receiving messages" in Firefox will cause the page to crash. #10752 (#10754)
### What problem does this PR solve?

Fix: Clicking "Stop receiving messages" in Firefox will cause the page
to crash. #10752
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 16:17:28 +08:00
6d333ec4bc Fix: Add video preview #9869 (#10748)
### What problem does this PR solve?

Fix: Add video preview

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 14:25:05 +08:00
ac188b0486 Feat: The default value of the parser operator's Video output format is set to text #9869 (#10745)
### What problem does this PR solve?
Feat: The default value of the parser operator's Video output format is
set to text #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-23 14:18:51 +08:00
adeb9d87e2 Bump infinity to 0.6.1 (#10749)
### What problem does this PR solve?

Bump infinity to 0.6.1

#10727 missed `docker/docker-compose-base.yml`.

### Type of change

- [x] Other (please describe):
2025-10-23 13:36:43 +08:00
d121033208 Fix: Resolved the issue where the Generate button must be refreshed after generating chunk to take effect #9869 (#10742)
### What problem does this PR solve?

Fix: Resolved the issue where the Generate button must be refreshed
after generating chunk to take effect

### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 11:54:45 +08:00
494f84cd69 Feat: Add suffix to the parser operator's video configuration #9869 (#10741)
### What problem does this PR solve?

Feat: Add suffix to the parser operator's video configuration #9869

### Type of change


- [x] New Feature (non-breaking change which adds functionality)
2025-10-23 11:13:21 +08:00
f24d464a53 Fix: video file suffix (#10740)
### What problem does this PR solve?


### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 11:13:09 +08:00
484c536f2e Fix typo (#10737)
### What problem does this PR solve?

Chunkder to Chunker

### Type of change

- [x] Documentation Update
2025-10-23 09:25:15 +08:00
f7112acd97 Feat: pipeline supports MinerU PDF parser (#10736)
### What problem does this PR solve?

Pipeline supports MinerU PDF parser.

### Type of change

- [x] New Feature (non-breaking change which adds functionality)
2025-10-23 09:24:31 +08:00
de4f75dcd8 Fix: add video parser (#10735)
### Type of change

- [x] Bug Fix (non-breaking change which fixes an issue)
2025-10-23 09:24:16 +08:00
22 changed files with 253 additions and 66 deletions

View File

@ -70,6 +70,7 @@ def create():
e, t = TenantService.get_by_id(current_user.id)
if not e:
return get_data_error_result(message="Tenant not found.")
req["parser_config"] = {
"layout_recognize": "DeepDOC",
"chunk_token_num": 512,

View File

@ -173,7 +173,7 @@ def filename_type(filename):
if re.match(r".*\.(wav|flac|ape|alac|wavpack|wv|mp3|aac|ogg|vorbis|opus)$", filename):
return FileType.AURAL.value
if re.match(r".*\.(jpg|jpeg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp|avif|apng|icon|ico|mpg|mpeg|avi|rm|rmvb|mov|wmv|asf|dat|asx|wvx|mpe|mpa|mp4)$", filename):
if re.match(r".*\.(jpg|jpeg|png|tif|gif|pcx|tga|exif|fpx|svg|psd|cdr|pcd|dxf|ufo|eps|ai|raw|WMF|webp|avif|apng|icon|ico|mpg|mpeg|avi|rm|rmvb|mov|wmv|asf|dat|asx|wvx|mpe|mpa|mp4|avi|mkv)$", filename):
return FileType.VISUAL.value
return FileType.OTHER.value

View File

@ -77,7 +77,7 @@ services:
container_name: ragflow-infinity
profiles:
- infinity
image: infiniflow/infinity:v0.6.0
image: infiniflow/infinity:v0.6.1
volumes:
- infinity_data:/var/infinity
- ./infinity_conf.toml:/infinity_conf.toml

View File

@ -34,7 +34,7 @@ Click **+ Add** to add heading levels here or update the corresponding **Regular
### Output
The global variable name for the output of the **Title chunkder** component, which can be referenced by subsequent components in the ingestion pipeline.
The global variable name for the output of the **Title chunker** component, which can be referenced by subsequent components in the ingestion pipeline.
- Default: `chunks`
- Type: `Array<Object>`

View File

@ -37,7 +37,7 @@ Defaults to `\n`. Click the right-hand **Recycle bin** button to remove it, or c
### Output
The global variable name for the output of the **Token chunkder** component, which can be referenced by subsequent components in the ingestion pipeline.
The global variable name for the output of the **Token chunker** component, which can be referenced by subsequent components in the ingestion pipeline.
- Default: `chunks`
- Type: `Array<Object>`

View File

@ -22,6 +22,23 @@ The embedding models included in a full edition are:
These two embedding models are optimized specifically for English and Chinese, so performance may be compromised if you use them to embed documents in other languages.
:::
## v0.21.1
Released on October 23, 2025.
### New features
- Experimental: Adds support for PDF document parsing using MinerU.
### Improvements
- Enhances UI/UX for the dataset and personal center pages.
- Upgrades RAGFlow's document engine, Infinity, to v0.6.1.
### Fixed issues
- An issue with video parsing.
## v0.21.0
Released on October 15, 2025.

View File

@ -29,7 +29,7 @@ from rag.utils import clean_markdown_block
ocr = OCR()
# Gemini supported MIME types
VIDEO_EXTS = [".mp4", ".mov", ".avi", ".flv", ".mpeg", ".mpg", ".webm", ".wmv", ".3gp", ".3gpp"]
VIDEO_EXTS = [".mp4", ".mov", ".avi", ".flv", ".mpeg", ".mpg", ".webm", ".wmv", ".3gp", ".3gpp", ".mkv"]
def chunk(filename, binary, tenant_id, lang, callback=None, **kwargs):

View File

@ -29,6 +29,7 @@ from api.db.services.llm_service import LLMBundle
from api.utils import get_uuid
from api.utils.base64_image import image2id
from deepdoc.parser import ExcelParser
from deepdoc.parser.mineru_parser import MinerUParser
from deepdoc.parser.pdf_parser import PlainParser, RAGFlowPdfParser, VisionParser
from rag.app.naive import Docx
from rag.flow.base import ProcessBase, ProcessParamBase
@ -138,9 +139,16 @@ class ParserParam(ProcessParamBase):
"oggvorbis",
"ape"
],
"output_format": "json",
"output_format": "text",
},
"video": {
"suffix":[
"mp4",
"avi",
"mkv"
],
"output_format": "text",
},
"video": {},
}
def check(self):
@ -149,7 +157,7 @@ class ParserParam(ProcessParamBase):
pdf_parse_method = pdf_config.get("parse_method", "")
self.check_empty(pdf_parse_method, "Parse method abnormal.")
if pdf_parse_method.lower() not in ["deepdoc", "plain_text"]:
if pdf_parse_method.lower() not in ["deepdoc", "plain_text", "mineru"]:
self.check_empty(pdf_config.get("lang", ""), "PDF VLM language")
pdf_output_format = pdf_config.get("output_format", "")
@ -185,6 +193,10 @@ class ParserParam(ProcessParamBase):
if audio_config:
self.check_empty(audio_config.get("llm_id"), "Audio VLM")
video_config = self.setups.get("video", "")
if video_config:
self.check_empty(video_config.get("llm_id"), "Video VLM")
email_config = self.setups.get("email", "")
if email_config:
email_output_format = email_config.get("output_format", "")
@ -207,13 +219,34 @@ class Parser(ProcessBase):
elif conf.get("parse_method").lower() == "plain_text":
lines, _ = PlainParser()(blob)
bboxes = [{"text": t} for t, _ in lines]
elif conf.get("parse_method").lower() == "mineru":
mineru_executable = os.environ.get("MINERU_EXECUTABLE", "mineru")
pdf_parser = MinerUParser(mineru_path=mineru_executable)
if not pdf_parser.check_installation():
raise RuntimeError("MinerU not found. Please install it via: pip install -U 'mineru[core]'.")
lines, _ = pdf_parser.parse_pdf(
filepath=name,
binary=blob,
callback=self.callback,
output_dir=os.environ.get("MINERU_OUTPUT_DIR", ""),
delete_output=bool(int(os.environ.get("MINERU_DELETE_OUTPUT", 1))),
)
bboxes = []
for t, poss in lines:
box = {
"image": pdf_parser.crop(poss, 1),
"positions": [[pos[0][-1], *pos[1:]] for pos in pdf_parser.extract_positions(poss)],
"text": t,
}
bboxes.append(box)
else:
vision_model = LLMBundle(self._canvas._tenant_id, LLMType.IMAGE2TEXT, llm_name=conf.get("parse_method"), lang=self._param.setups["pdf"].get("lang"))
lines, _ = VisionParser(vision_model=vision_model)(blob, callback=self.callback)
bboxes = []
for t, poss in lines:
pn, x0, x1, top, bott = poss.split(" ")
bboxes.append({"page_number": int(pn), "x0": float(x0), "x1": float(x1), "top": float(top), "bottom": float(bott), "text": t})
for pn, x0, x1, top, bott in RAGFlowPdfParser.extract_positions(poss):
bboxes.append({"page_number": int(pn[0]), "x0": float(x0), "x1": float(x1), "top": float(top), "bottom": float(bott), "text": t})
if conf.get("output_format") == "json":
self.set_output("json", bboxes)
@ -357,6 +390,17 @@ class Parser(ProcessBase):
self.set_output("text", txt)
def _video(self, name, blob):
self.callback(random.randint(1, 5) / 100.0, "Start to work on an video.")
conf = self._param.setups["video"]
self.set_output("output_format", conf["output_format"])
cv_mdl = LLMBundle(self._canvas.get_tenant_id(), LLMType.IMAGE2TEXT)
txt = cv_mdl.chat(system="", history=[], gen_conf={}, video_bytes=blob, filename=name)
self.set_output("text", txt)
def _email(self, name, blob):
self.callback(random.randint(1, 5) / 100.0, "Start to work on an email.")
@ -483,6 +527,7 @@ class Parser(ProcessBase):
"word": self._word,
"image": self._image,
"audio": self._audio,
"video": self._video,
"email": self._email,
}
try:

View File

@ -257,25 +257,32 @@ export const useSendMessageWithSse = (
.getReader();
while (true) {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
resetAnswer();
break;
}
try {
const val = JSON.parse(value?.data || '');
const d = val?.data;
if (typeof d !== 'boolean') {
setAnswer({
...d,
conversationId: body?.conversation_id,
chatBoxId: body.chatBoxId,
});
try {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
resetAnswer();
break;
}
} catch (e) {
// Swallow parse errors silently
try {
const val = JSON.parse(value?.data || '');
const d = val?.data;
if (typeof d !== 'boolean') {
setAnswer({
...d,
conversationId: body?.conversation_id,
chatBoxId: body.chatBoxId,
});
}
} catch (e) {
// Swallow parse errors silently
}
}
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('Request was aborted by user or logic.');
break;
}
}
}

View File

@ -126,29 +126,36 @@ export const useSendMessageBySSE = (url: string = api.completeConversation) => {
.getReader();
while (true) {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
console.info('done');
resetAnswerList();
break;
}
try {
const val = JSON.parse(value?.data || '');
console.info('data:', val);
if (val.code === 500) {
message.error(val.message);
try {
const x = await reader?.read();
if (x) {
const { done, value } = x;
if (done) {
console.info('done');
resetAnswerList();
break;
}
try {
const val = JSON.parse(value?.data || '');
setAnswerList((list) => {
const nextList = [...list];
nextList.push(val);
return nextList;
});
} catch (e) {
console.warn(e);
console.info('data:', val);
if (val.code === 500) {
message.error(val.message);
}
setAnswerList((list) => {
const nextList = [...list];
nextList.push(val);
return nextList;
});
} catch (e) {
console.warn(e);
}
}
} catch (e) {
if (e instanceof DOMException && e.name === 'AbortError') {
console.log('Request was aborted by user or logic.');
break;
}
}
}

View File

@ -430,7 +430,7 @@ export default {
`,
useRaptor: 'RAPTOR',
useRaptorTip:
'Enable RAPTOR for multi-hop question-answering tasks. See https://ragflow.io/docs/dev/enable_raptor for details.',
'RAPTOR can be used for multi-hop question-answering tasks. Navigate to the Files page, click Generate > RAPTOR to enable it. See https://ragflow.io/docs/dev/enable_raptor for details.',
prompt: 'Prompt',
promptTip:
'Use the system prompt to describe the task for the LLM, specify how it should respond, and outline other miscellaneous requirements. The system prompt is often used in conjunction with keys (variables), which serve as various data inputs for the LLM. Use a forward slash `/` or the (x) button to show the keys to use.',

View File

@ -425,7 +425,7 @@ export default {
`,
useRaptor: '使用召回增强 RAPTOR 策略',
useRaptorTip:
'为多跳问答任务启用 RAPTOR,详情请见 : https://ragflow.io/docs/dev/enable_raptor。',
'RAPTOR 常应用于复杂的多跳问答任务。如需打开,请跳转至知识库的文件页面,点击生成 > RAPTOR 开启。详见: https://ragflow.io/docs/dev/enable_raptor。',
prompt: '提示词',
promptMessage: '提示词是必填项',
promptText: `请总结以下段落。 小心数字,不要编造。 段落如下:

View File

@ -2,10 +2,13 @@ import { Sheet, SheetContent, SheetTitle } from '@/components/ui/sheet';
import { IModalProps } from '@/interfaces/common';
import { cn } from '@/lib/utils';
import { useTranslation } from 'react-i18next';
import { useIsTaskMode } from '../hooks/use-get-begin-query';
import AgentChatBox from './box';
export function ChatSheet({ hideModal }: IModalProps<any>) {
const { t } = useTranslation();
const isTaskMode = useIsTaskMode();
return (
<Sheet open modal={false} onOpenChange={hideModal}>
<SheetContent
@ -13,7 +16,9 @@ export function ChatSheet({ hideModal }: IModalProps<any>) {
onInteractOutside={(e) => e.preventDefault()}
>
<SheetTitle className="hidden"></SheetTitle>
<div className="pl-5 pt-2">{t('chat.chat')}</div>
<div className="pl-5 pt-2">
{t(isTaskMode ? 'flow.task' : 'chat.chat')}
</div>
<AgentChatBox></AgentChatBox>
</SheetContent>
</Sheet>

View File

@ -382,9 +382,9 @@ export const useSendAgentMessage = ({
const { content, id } = findMessageFromList(answerList);
const inputAnswer = findInputFromList(answerList);
const answer = content || getLatestError(answerList);
if (answerList.length > 0 && answer) {
if (answerList.length > 0) {
addNewestOneAnswer({
answer: answer,
answer: answer ?? '',
id: id,
...inputAnswer,
});

View File

@ -49,7 +49,7 @@ export enum PptOutputFormat {
}
export enum VideoOutputFormat {
Json = 'json',
Text = 'text',
}
export enum AudioOutputFormat {
@ -76,7 +76,7 @@ export const InitialOutputFormatMap = {
[FileType.TextMarkdown]: TextMarkdownOutputFormat.Text,
[FileType.Docx]: DocxOutputFormat.Json,
[FileType.PowerPoint]: PptOutputFormat.Json,
[FileType.Video]: VideoOutputFormat.Json,
[FileType.Video]: VideoOutputFormat.Text,
[FileType.Audio]: AudioOutputFormat.Text,
};
@ -244,7 +244,7 @@ export const FileTypeSuffixMap = {
[FileType.TextMarkdown]: ['md', 'markdown', 'mdx', 'txt'],
[FileType.Docx]: ['doc', 'docx'],
[FileType.PowerPoint]: ['pptx'],
[FileType.Video]: [],
[FileType.Video]: ['mp4', 'avi', 'mkv'],
[FileType.Audio]: [
'da',
'wave',

View File

@ -2,7 +2,7 @@ import message from '@/components/ui/message';
import { Spin } from '@/components/ui/spin';
import request from '@/utils/request';
import classNames from 'classnames';
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
interface ImagePreviewerProps {
className?: string;
@ -17,7 +17,7 @@ export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
const [imageSrc, setImageSrc] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const fetchImage = async () => {
const fetchImage = useCallback(async () => {
setIsLoading(true);
const res = await request(url, {
method: 'GET',
@ -30,12 +30,13 @@ export const ImagePreviewer: React.FC<ImagePreviewerProps> = ({
const objectUrl = URL.createObjectURL(res.data);
setImageSrc(objectUrl);
setIsLoading(false);
};
}, [url]);
useEffect(() => {
if (url) {
fetchImage();
}
}, [url]);
}, [url, fetchImage]);
useEffect(() => {
return () => {

View File

@ -8,6 +8,7 @@ import styles from './index.less';
import PdfPreviewer, { IProps } from './pdf-preview';
import { PptPreviewer } from './ppt-preview';
import { TxtPreviewer } from './txt-preview';
import { VideoPreviewer } from './video-preview';
type PreviewProps = {
fileType: string;
@ -42,11 +43,30 @@ const Preview = ({
<TxtPreviewer className={className} url={url} />
</section>
)}
{['visual'].indexOf(fileType) > -1 && (
{['jpg', 'png', 'gif', 'jpeg', 'svg', 'bmp', 'ico', 'tif'].indexOf(
fileType,
) > -1 && (
<section>
<ImagePreviewer className={className} url={url} />
</section>
)}
{[
'mp4',
'avi',
'mov',
'mkv',
'wmv',
'flv',
'mpeg',
'mpg',
'asf',
'rm',
'rmvb',
].indexOf(fileType) > -1 && (
<section>
<VideoPreviewer className={className} url={url} />
</section>
)}
{['pptx'].indexOf(fileType) > -1 && (
<section>
<PptPreviewer className={className} url={url} />

View File

@ -0,0 +1,74 @@
import message from '@/components/ui/message';
import { Spin } from '@/components/ui/spin';
import request from '@/utils/request';
import classNames from 'classnames';
import { useCallback, useEffect, useState } from 'react';
interface VideoPreviewerProps {
className?: string;
url: string;
}
export const VideoPreviewer: React.FC<VideoPreviewerProps> = ({
className,
url,
}) => {
// const url = useGetDocumentUrl();
const [videoSrc, setVideoSrc] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(true);
const fetchVideo = useCallback(async () => {
setIsLoading(true);
const res = await request(url, {
method: 'GET',
responseType: 'blob',
onError: () => {
message.error('Failed to load video');
setIsLoading(false);
},
});
const objectUrl = URL.createObjectURL(res.data);
setVideoSrc(objectUrl);
setIsLoading(false);
}, [url]);
useEffect(() => {
if (url) {
fetchVideo();
}
}, [url, fetchVideo]);
useEffect(() => {
return () => {
if (videoSrc) {
URL.revokeObjectURL(videoSrc);
}
};
}, [videoSrc]);
return (
<div
className={classNames(
'relative w-full h-full p-4 bg-background-paper border border-border-normal rounded-md video-previewer',
className,
)}
>
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center">
<Spin />
</div>
)}
{!isLoading && videoSrc && (
<div className="max-h-[80vh] overflow-auto p-2">
<video
src={videoSrc}
controls
className="w-full h-auto max-w-full object-contain"
onLoadedData={() => URL.revokeObjectURL(videoSrc!)}
/>
</div>
)}
</div>
);
};

View File

@ -166,6 +166,7 @@ const Chunk = () => {
case 'doc':
return documentInfo?.name.split('.').pop() || 'doc';
case 'visual':
return documentInfo?.name.split('.').pop() || 'visual';
case 'docx':
case 'txt':
case 'md':

View File

@ -28,6 +28,7 @@ import {
} from '@/components/ui/breadcrumb';
import { Button } from '@/components/ui/button';
import { Modal } from '@/components/ui/modal/modal';
import { AgentCategory } from '@/constants/agent';
import { Images } from '@/constants/common';
import { useNavigatePage } from '@/hooks/logic-hooks/navigate-hooks';
import { useGetKnowledgeSearchParams } from '@/hooks/route-hook';
@ -178,8 +179,8 @@ const Chunk = () => {
if (knowledgeId) {
navigateToDatasetOverview(knowledgeId)();
}
if (agentId) {
navigateToAgent(agentId)();
if (isAgent) {
navigateToAgent(agentId, AgentCategory.DataflowCanvas)();
}
}}
>

View File

@ -14,6 +14,7 @@ import { useRowSelection } from '@/hooks/logic-hooks/use-row-selection';
import { useFetchDocumentList } from '@/hooks/use-document-request';
import { useFetchKnowledgeBaseConfiguration } from '@/hooks/use-knowledge-request';
import { Upload } from 'lucide-react';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { DatasetTable } from './dataset-table';
import Generate from './generate-button/generate';
@ -31,7 +32,6 @@ export default function Dataset() {
onDocumentUploadOk,
documentUploadLoading,
} = useHandleUploadDocument();
const { data: dataSetData } = useFetchKnowledgeBaseConfiguration();
const {
searchString,
@ -43,6 +43,14 @@ export default function Dataset() {
handleFilterSubmit,
loading,
} = useFetchDocumentList();
const refreshCount = useMemo(() => {
return documents.findIndex((doc) => doc.run === '1') + documents.length;
}, [documents]);
const { data: dataSetData } = useFetchKnowledgeBaseConfiguration({
refreshCount,
});
const { filters, onOpenChange } = useSelectDatasetFilters();
const {

View File

@ -20,7 +20,7 @@ export function useUploadFile() {
if (Array.isArray(files) && files.length) {
const file = files[0];
const ret = await uploadAndParseFile({ file, options, conversationId });
if (ret.code === 0 && Array.isArray(ret.data)) {
if (ret?.code === 0 && Array.isArray(ret?.data)) {
setFileIds((list) => [...list, ...ret.data]);
setFileMap((map) => {
map.set(files[0], ret.data[0]);