From 996b5fe14ec40ac56deb4021111341f941862581 Mon Sep 17 00:00:00 2001 From: chanx <1243304602@qq.com> Date: Fri, 14 Nov 2025 19:50:01 +0800 Subject: [PATCH 1/5] Fix: Added the ability to download files in the agent message reply function. (#11281) ### What problem does this PR solve? Fix: Added the ability to download files in the agent message reply function. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- .../components/next-message-item/index.tsx | 30 +++++++++++++- web/src/hooks/use-send-message.ts | 7 +++- web/src/interfaces/database/chat.ts | 3 ++ web/src/locales/en.ts | 2 + web/src/locales/zh.ts | 2 + .../agent/chat/use-send-agent-message.ts | 8 +++- web/src/pages/agent/constant/index.tsx | 8 ++++ .../pages/agent/form/message-form/index.tsx | 41 ++++++++++++++++++- .../agent/form/message-form/use-values.ts | 3 +- web/src/services/file-manager-service.ts | 7 ++++ web/src/utils/api.ts | 2 + 11 files changed, 108 insertions(+), 5 deletions(-) diff --git a/web/src/components/next-message-item/index.tsx b/web/src/components/next-message-item/index.tsx index 5dd6cdf60..706553b67 100644 --- a/web/src/components/next-message-item/index.tsx +++ b/web/src/components/next-message-item/index.tsx @@ -18,8 +18,10 @@ import { cn } from '@/lib/utils'; import { AgentChatContext } from '@/pages/agent/context'; import { WorkFlowTimeline } from '@/pages/agent/log-sheet/workflow-timeline'; import { IMessage } from '@/pages/chat/interface'; +import { downloadFile } from '@/services/file-manager-service'; +import { downloadFileFromBlob } from '@/utils/file-util'; import { isEmpty } from 'lodash'; -import { Atom, ChevronDown, ChevronUp } from 'lucide-react'; +import { Atom, ChevronDown, ChevronUp, Download } from 'lucide-react'; import MarkdownContent from '../next-markdown-content'; import { RAGFlowAvatar } from '../ragflow-avatar'; import { useTheme } from '../theme-provider'; @@ -245,6 +247,32 @@ function MessageItem({ {isUser && ( )} + {isAssistant && item.attachment && item.attachment.doc_id && ( +
+ +
+ )} diff --git a/web/src/hooks/use-send-message.ts b/web/src/hooks/use-send-message.ts index 8d602f2e0..e956217f3 100644 --- a/web/src/hooks/use-send-message.ts +++ b/web/src/hooks/use-send-message.ts @@ -44,9 +44,14 @@ export interface IInputData { inputs: Record; tips: string; } - +export interface IAttachment { + doc_id: string; + format: string; + file_name: string; +} export interface IMessageData { content: string; + outputs: any; start_to_think?: boolean; end_to_think?: boolean; } diff --git a/web/src/interfaces/database/chat.ts b/web/src/interfaces/database/chat.ts index 62bcb4696..eb6eebe89 100644 --- a/web/src/interfaces/database/chat.ts +++ b/web/src/interfaces/database/chat.ts @@ -1,4 +1,5 @@ import { MessageType } from '@/constants/chat'; +import { IAttachment } from '@/hooks/use-send-message'; export interface PromptConfig { empty_response: string; @@ -97,6 +98,7 @@ export interface Message { data?: any; files?: File[]; chatBoxId?: string; + attachment?: IAttachment; } export interface IReferenceChunk { @@ -126,6 +128,7 @@ export interface IReferenceObject { export interface IAnswer { answer: string; + attachment?: IAttachment; reference?: IReference; conversationId?: string; prompt?: string; diff --git a/web/src/locales/en.ts b/web/src/locales/en.ts index b9f374f7c..e2035a378 100644 --- a/web/src/locales/en.ts +++ b/web/src/locales/en.ts @@ -1009,6 +1009,8 @@ Example: general/v2/`, pleaseUploadAtLeastOneFile: 'Please upload at least one file', }, flow: { + downloadFileTypeTip: 'The file type to download', + downloadFileType: 'Download file type', formatTypeError: 'Format or type error', variableNameMessage: 'Variable name can only contain letters and underscores', diff --git a/web/src/locales/zh.ts b/web/src/locales/zh.ts index ce21c5a30..301719117 100644 --- a/web/src/locales/zh.ts +++ b/web/src/locales/zh.ts @@ -956,6 +956,8 @@ General:实体和关系提取提示来自 GitHub - microsoft/graphrag:基于 pleaseUploadAtLeastOneFile: '请上传至少一个文件', }, flow: { + downloadFileTypeTip: '文件下载的类型', + downloadFileType: '文件类型', formatTypeError: '格式或类型错误', variableNameMessage: '名称只能包含字母和下划线', variableDescription: '变量的描述', diff --git a/web/src/pages/agent/chat/use-send-agent-message.ts b/web/src/pages/agent/chat/use-send-agent-message.ts index a0460fd71..5fc49d4ce 100644 --- a/web/src/pages/agent/chat/use-send-agent-message.ts +++ b/web/src/pages/agent/chat/use-send-agent-message.ts @@ -5,6 +5,7 @@ import { useSelectDerivedMessages, } from '@/hooks/logic-hooks'; import { + IAttachment, IEventList, IInputEvent, IMessageEndData, @@ -75,9 +76,13 @@ export function findMessageFromList(eventList: IEventList) { nextContent += ''; } + const workflowFinished = eventList.find( + (x) => x.event === MessageEventType.WorkflowFinished, + ) as IMessageEvent; return { id: eventList[0]?.message_id, content: nextContent, + attachment: workflowFinished?.data?.outputs?.attachment || {}, }; } @@ -388,12 +393,13 @@ export const useSendAgentMessage = ({ }, [sendMessageInTaskMode]); useEffect(() => { - const { content, id } = findMessageFromList(answerList); + const { content, id, attachment } = findMessageFromList(answerList); const inputAnswer = findInputFromList(answerList); const answer = content || getLatestError(answerList); if (answerList.length > 0) { addNewestOneAnswer({ answer: answer ?? '', + attachment: attachment as IAttachment, id: id, ...inputAnswer, }); diff --git a/web/src/pages/agent/constant/index.tsx b/web/src/pages/agent/constant/index.tsx index 7aad5e4a3..3a161d87d 100644 --- a/web/src/pages/agent/constant/index.tsx +++ b/web/src/pages/agent/constant/index.tsx @@ -417,6 +417,7 @@ export const initialIterationValues = { items_ref: '', outputs: {}, }; + export const initialIterationStartValues = { outputs: { item: { @@ -845,3 +846,10 @@ export enum JsonSchemaDataType { Array = 'array', Object = 'object', } + +export enum ExportFileType { + PDF = 'pdf', + HTML = 'html', + Markdown = 'md', + DOCX = 'docx', +} diff --git a/web/src/pages/agent/form/message-form/index.tsx b/web/src/pages/agent/form/message-form/index.tsx index e93735ee7..31b52659e 100644 --- a/web/src/pages/agent/form/message-form/index.tsx +++ b/web/src/pages/agent/form/message-form/index.tsx @@ -8,12 +8,14 @@ import { FormLabel, FormMessage, } from '@/components/ui/form'; +import { RAGFlowSelect } from '@/components/ui/select'; import { zodResolver } from '@hookform/resolvers/zod'; import { X } from 'lucide-react'; import { memo } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { z } from 'zod'; +import { ExportFileType } from '../../constant'; import { INextOperatorForm } from '../../interface'; import { FormWrapper } from '../components/form-wrapper'; import { PromptEditor } from '../components/prompt-editor'; @@ -33,10 +35,14 @@ function MessageForm({ node }: INextOperatorForm) { }), ) .optional(), + output_format: z.string().optional(), }); const form = useForm({ - defaultValues: values, + defaultValues: { + ...values, + output_format: values.output_format, + }, resolver: zodResolver(FormSchema), }); @@ -50,6 +56,39 @@ function MessageForm({ node }: INextOperatorForm) { return (
+ + + + {t('flow.downloadFileType')} + + ( + + + { + return { + value: + ExportFileType[ + key as keyof typeof ExportFileType + ], + label: key, + }; + }, + )} + {...field} + onValueChange={field.onChange} + placeholder={t('flow.messagePlaceholder')} + > + + + )} + /> + + {t('flow.msg')} diff --git a/web/src/pages/agent/form/message-form/use-values.ts b/web/src/pages/agent/form/message-form/use-values.ts index 6a90881be..0cece91fc 100644 --- a/web/src/pages/agent/form/message-form/use-values.ts +++ b/web/src/pages/agent/form/message-form/use-values.ts @@ -1,7 +1,7 @@ import { RAGFlowNodeType } from '@/interfaces/database/flow'; import { isEmpty } from 'lodash'; import { useMemo } from 'react'; -import { initialMessageValues } from '../../constant'; +import { ExportFileType, initialMessageValues } from '../../constant'; import { convertToObjectArray } from '../../utils'; export function useValues(node?: RAGFlowNodeType) { @@ -15,6 +15,7 @@ export function useValues(node?: RAGFlowNodeType) { return { ...formData, content: convertToObjectArray(formData.content), + output_format: formData.output_format || ExportFileType.PDF, }; }, [node]); diff --git a/web/src/services/file-manager-service.ts b/web/src/services/file-manager-service.ts index 8342117c9..8c5eb6c4e 100644 --- a/web/src/services/file-manager-service.ts +++ b/web/src/services/file-manager-service.ts @@ -13,6 +13,7 @@ const { get_document_file, getFile, moveFile, + get_document_file_download, } = api; const methods = { @@ -65,4 +66,10 @@ const fileManagerService = registerServer( request, ); +export const downloadFile = (data: { docId: string; ext: string }) => { + return request.get(get_document_file_download(data.docId), { + params: { ext: data.ext }, + responseType: 'blob', + }); +}; export default fileManagerService; diff --git a/web/src/utils/api.ts b/web/src/utils/api.ts index e0afdbeb3..c4ce8205f 100644 --- a/web/src/utils/api.ts +++ b/web/src/utils/api.ts @@ -100,6 +100,8 @@ export default { document_change_parser: `${api_host}/document/change_parser`, document_thumbnails: `${api_host}/document/thumbnails`, get_document_file: `${api_host}/document/get`, + get_document_file_download: (docId: string) => + `${api_host}/document/download/${docId}`, document_upload: `${api_host}/document/upload`, web_crawl: `${api_host}/document/web_crawl`, document_infos: `${api_host}/document/infos`, From cd55f6c1b822d84e23a2199ae5f71eac5671d736 Mon Sep 17 00:00:00 2001 From: buua436 <66937541+buua436@users.noreply.github.com> Date: Fri, 14 Nov 2025 19:50:29 +0800 Subject: [PATCH 2/5] Fix:ListOperations does not support sorting arrays of objects. (#11278) ### What problem does this PR solve? pr: #11276 change: ListOperations does not support sorting arrays of objects. ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) --- agent/component/list_operations.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/agent/component/list_operations.py b/agent/component/list_operations.py index c29d79ea6..9ae8c2e04 100644 --- a/agent/component/list_operations.py +++ b/agent/component/list_operations.py @@ -121,10 +121,26 @@ class ListOperations(ComponentBase,ABC): return False def _sort(self): - if self._param.sort_method == "asc": - self._set_outputs(sorted(self.inputs)) - elif self._param.sort_method == "desc": - self._set_outputs(sorted(self.inputs, reverse=True)) + items = self.inputs or [] + method = getattr(self._param, "sort_method", "asc") or "asc" + reverse = method == "desc" + + if not items: + self._set_outputs([]) + return + + first = items[0] + + if isinstance(first, dict): + outputs = sorted( + items, + key=lambda x: self._hashable(x), + reverse=reverse, + ) + else: + outputs = sorted(items, reverse=reverse) + + self._set_outputs(outputs) def _drop_duplicates(self): seen = set() @@ -145,5 +161,6 @@ class ListOperations(ComponentBase,ABC): if isinstance(x, set): return tuple(sorted(self._hashable(v) for v in x)) return x + def thoughts(self) -> str: return "ListOperation in progress" From 68e3b33ae4b8043620d2ea901174ec75054b1a15 Mon Sep 17 00:00:00 2001 From: Billy Bao Date: Fri, 14 Nov 2025 19:52:11 +0800 Subject: [PATCH 3/5] Feat: extract message output to file (#11251) ### What problem does this PR solve? Feat: extract message output to file ### Type of change - [x] New Feature (non-breaking change which adds functionality) --- Dockerfile | 4 ++- agent/canvas.py | 4 +++ agent/component/message.py | 70 +++++++++++++++++++++++++++++++++++++- api/apps/document_app.py | 17 +++++++++ pyproject.toml | 1 + uv.lock | 10 ++++++ 6 files changed, 104 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b16a0d7d5..239330183 100644 --- a/Dockerfile +++ b/Dockerfile @@ -51,7 +51,9 @@ RUN --mount=type=cache,id=ragflow_apt,target=/var/cache/apt,sharing=locked \ apt install -y libpython3-dev libgtk-4-1 libnss3 xdg-utils libgbm-dev && \ apt install -y libjemalloc-dev && \ apt install -y python3-pip pipx nginx unzip curl wget git vim less && \ - apt install -y ghostscript + apt install -y ghostscript && \ + apt install -y pandoc && \ + apt install -y texlive RUN if [ "$NEED_MIRROR" == "1" ]; then \ pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple && \ diff --git a/agent/canvas.py b/agent/canvas.py index bc7a45e3e..f262cd597 100644 --- a/agent/canvas.py +++ b/agent/canvas.py @@ -408,6 +408,10 @@ class Canvas(Graph): else: yield decorate("message", {"content": cpn_obj.output("content")}) cite = re.search(r"\[ID:[ 0-9]+\]", cpn_obj.output("content")) + + if isinstance(cpn_obj.output("attachment"), tuple): + yield decorate("message", {"attachment": cpn_obj.output("attachment")}) + yield decorate("message_end", {"reference": self.get_reference() if cite else None}) while partials: diff --git a/agent/component/message.py b/agent/component/message.py index 641198083..555534610 100644 --- a/agent/component/message.py +++ b/agent/component/message.py @@ -17,6 +17,9 @@ import json import os import random import re +import pypandoc +import logging +import tempfile from functools import partial from typing import Any @@ -24,7 +27,8 @@ from agent.component.base import ComponentBase, ComponentParamBase from jinja2 import Template as Jinja2Template from common.connection_utils import timeout - +from common.misc_utils import get_uuid +from common import settings class MessageParam(ComponentParamBase): """ @@ -34,6 +38,7 @@ class MessageParam(ComponentParamBase): super().__init__() self.content = [] self.stream = True + self.output_format = None # default output format self.outputs = { "content": { "type": "str" @@ -133,6 +138,7 @@ class Message(ComponentBase): yield rand_cnt[s: ] self.set_output("content", all_content) + self._convert_content(all_content) def _is_jinjia2(self, content:str) -> bool: patt = [ @@ -164,6 +170,68 @@ class Message(ComponentBase): content = re.sub(n, v, content) self.set_output("content", content) + self._convert_content(content) def thoughts(self) -> str: return "" + + def _convert_content(self, content): + doc_id = get_uuid() + + if self._param.output_format.lower() not in {"markdown", "html", "pdf", "docx"}: + self._param.output_format = "markdown" + + try: + if self._param.output_format in {"markdown", "html"}: + if isinstance(content, str): + converted = pypandoc.convert_text( + content, + to=self._param.output_format, + format="markdown", + ) + else: + converted = pypandoc.convert_file( + content, + to=self._param.output_format, + format="markdown", + ) + + binary_content = converted.encode("utf-8") + + else: # pdf, docx + with tempfile.NamedTemporaryFile(suffix=f".{self._param.output_format}", delete=False) as tmp: + tmp_name = tmp.name + + try: + if isinstance(content, str): + pypandoc.convert_text( + content, + to=self._param.output_format, + format="markdown", + outputfile=tmp_name, + ) + else: + pypandoc.convert_file( + content, + to=self._param.output_format, + format="markdown", + outputfile=tmp_name, + ) + + with open(tmp_name, "rb") as f: + binary_content = f.read() + + finally: + if os.path.exists(tmp_name): + os.remove(tmp_name) + + settings.STORAGE_IMPL.put(self._canvas._tenant_id, doc_id, binary_content) + self.set_output("attachment", { + "doc_id":doc_id, + "format":self._param.output_format, + "file_name":f"{doc_id[:8]}.{self._param.output_format}"}) + + logging.info(f"Converted content uploaded as {doc_id} (format={self._param.output_format})") + + except Exception as e: + logging.error(f"Error converting content to {self._param.output_format}: {e}") \ No newline at end of file diff --git a/api/apps/document_app.py b/api/apps/document_app.py index 12c19f978..8cea336de 100644 --- a/api/apps/document_app.py +++ b/api/apps/document_app.py @@ -508,6 +508,7 @@ def get(doc_id): ext = ext.group(1) if ext else None if ext: if doc.type == FileType.VISUAL.value: + content_type = CONTENT_TYPE_MAP.get(ext, f"image/{ext}") else: content_type = CONTENT_TYPE_MAP.get(ext, f"application/{ext}") @@ -517,6 +518,22 @@ def get(doc_id): return server_error_response(e) +@manager.route("/download/", methods=["GET"]) # noqa: F821 +@login_required +def download_attachment(attachment_id): + try: + ext = request.args.get("ext", "markdown") + data = settings.STORAGE_IMPL.get(current_user.id, attachment_id) + # data = settings.STORAGE_IMPL.get("eb500d50bb0411f0907561d2782adda5", attachment_id) + response = flask.make_response(data) + response.headers.set("Content-Type", CONTENT_TYPE_MAP.get(ext, f"application/{ext}")) + + return response + + except Exception as e: + return server_error_response(e) + + @manager.route("/change_parser", methods=["POST"]) # noqa: F821 @login_required @validate_request("doc_id") diff --git a/pyproject.toml b/pyproject.toml index 2ec792b90..c1210dfb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -145,6 +145,7 @@ dependencies = [ "markdownify>=1.2.0", "captcha>=0.7.1", "pip>=25.2", + "pypandoc>=1.16", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 166b34ce4..474ca510b 100644 --- a/uv.lock +++ b/uv.lock @@ -4892,6 +4892,14 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/28/2659c02301b9500751f8d42f9a6632e1508aa5120de5e43042b8b30f8d5d/pyopenssl-25.1.0-py3-none-any.whl", hash = "sha256:2b11f239acc47ac2e5aca04fd7fa829800aeee22a2eb30d744572a157bd8a1ab", size = 56771, upload-time = "2025-05-17T16:28:29.197Z" }, ] +[[package]] +name = "pypandoc" +version = "1.16" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/77/af1fc54740a0712988f9518e629d38edc7b8ffccd7549203f19c3d8a2db6/pypandoc-1.16-py3-none-any.whl", hash = "sha256:868f390d48388743e7a5885915cbbaa005dea36a825ecdfd571f8c523416c822", size = 19425, upload-time = "2025-11-08T15:44:38.429Z" }, +] + [[package]] name = "pyparsing" version = "3.2.3" @@ -5292,6 +5300,7 @@ dependencies = [ { name = "pyicu" }, { name = "pymysql" }, { name = "pyodbc" }, + { name = "pypandoc" }, { name = "pypdf" }, { name = "pypdf2" }, { name = "python-calamine" }, @@ -5447,6 +5456,7 @@ requires-dist = [ { name = "pyicu", specifier = ">=2.15.3,<3.0.0" }, { name = "pymysql", specifier = ">=1.1.1,<2.0.0" }, { name = "pyodbc", specifier = ">=5.2.0,<6.0.0" }, + { name = "pypandoc", specifier = ">=1.16" }, { name = "pypdf", specifier = "==6.0.0" }, { name = "pypdf2", specifier = ">=3.0.1,<4.0.0" }, { name = "python-calamine", specifier = ">=0.4.0" }, From b1a1eedf5382512ec9ec737abb17174006209026 Mon Sep 17 00:00:00 2001 From: Billy Bao Date: Fri, 14 Nov 2025 19:52:58 +0800 Subject: [PATCH 4/5] Doc: add default username & pwd (#11283) ### What problem does this PR solve? Doc: add default username & pwd ### Type of change - [x] Documentation Update --------- Co-authored-by: writinwaters <93570324+writinwaters@users.noreply.github.com> --- docs/guides/accessing_admin_ui.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/guides/accessing_admin_ui.md b/docs/guides/accessing_admin_ui.md index 52ff4d6c7..23521244b 100644 --- a/docs/guides/accessing_admin_ui.md +++ b/docs/guides/accessing_admin_ui.md @@ -12,6 +12,10 @@ The RAGFlow Admin UI is a web-based interface that provides comprehensive system To access the RAGFlow admin UI, append `/admin` to the web UI's address, e.g. `http://[RAGFLOW_WEB_UI_ADDR]/admin`, replace `[RAGFLOW_WEB_UI_ADDR]` with real RAGFlow web UI address. +### Default Credentials +| Username | Password | +|----------|----------| +| admin@ragflow.io | admin | ## Admin UI Overview From e841b09d631f2426067a2e45f25721e8d9ca9285 Mon Sep 17 00:00:00 2001 From: Jin Hai Date: Fri, 14 Nov 2025 20:39:54 +0800 Subject: [PATCH 5/5] Remove unused code and fix performance issue (#11284) ### What problem does this PR solve? 1. remove redundant code 2. fix miner performance issue ### Type of change - [x] Bug Fix (non-breaking change which fixes an issue) - [x] Refactoring Signed-off-by: Jin Hai --- agent/canvas.py | 2 -- agent/component/base.py | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/agent/canvas.py b/agent/canvas.py index f262cd597..e18cb8d26 100644 --- a/agent/canvas.py +++ b/agent/canvas.py @@ -298,8 +298,6 @@ class Canvas(Graph): for kk, vv in kwargs["webhook_payload"].items(): self.components[k]["obj"].set_output(kk, vv) - self.components[k]["obj"].reset(True) - for k in kwargs.keys(): if k in ["query", "user_id", "files"] and kwargs[k]: if k == "files": diff --git a/agent/component/base.py b/agent/component/base.py index 31ad46820..0864ccb9e 100644 --- a/agent/component/base.py +++ b/agent/component/base.py @@ -463,12 +463,15 @@ class ComponentBase(ABC): return self._param.outputs.get("_ERROR", {}).get("value") def reset(self, only_output=False): - for k in self._param.outputs.keys(): - self._param.outputs[k]["value"] = None + outputs: dict = self._param.outputs # for better performance + for k in outputs.keys(): + outputs[k]["value"] = None if only_output: return - for k in self._param.inputs.keys(): - self._param.inputs[k]["value"] = None + + inputs: dict = self._param.inputs # for better performance + for k in inputs.keys(): + inputs[k]["value"] = None self._param.debug_inputs = {} def get_input(self, key: str=None) -> Union[Any, dict[str, Any]]: