Compare commits

...

5 Commits

Author SHA1 Message Date
e841b09d63 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 <haijin.chn@gmail.com>
2025-11-14 20:39:54 +08:00
b1a1eedf53 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>
2025-11-14 19:52:58 +08:00
68e3b33ae4 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)
2025-11-14 19:52:11 +08:00
cd55f6c1b8 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)
2025-11-14 19:50:29 +08:00
996b5fe14e 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)
2025-11-14 19:50:01 +08:00
20 changed files with 244 additions and 17 deletions

View File

@ -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 && \

View File

@ -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":
@ -408,6 +406,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:

View File

@ -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]]:

View File

@ -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"

View File

@ -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}")

View File

@ -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/<attachment_id>", 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")

View File

@ -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

View File

@ -145,6 +145,7 @@ dependencies = [
"markdownify>=1.2.0",
"captcha>=0.7.1",
"pip>=25.2",
"pypandoc>=1.16",
]
[dependency-groups]

10
uv.lock generated
View File

@ -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" },

View File

@ -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 && (
<UploadedMessageFiles files={item.files}></UploadedMessageFiles>
)}
{isAssistant && item.attachment && item.attachment.doc_id && (
<div className="w-full flex items-center justify-end">
<Button
variant="link"
className="p-1 m-0 h-auto text-text-sub-title-invert"
onClick={async () => {
if (item.attachment?.doc_id) {
try {
const response = await downloadFile({
docId: item.attachment.doc_id,
ext: item.attachment.format,
});
const blob = new Blob([response.data], {
type: response.data.type,
});
downloadFileFromBlob(blob, item.attachment.file_name);
} catch (error) {
console.error('Download failed:', error);
}
}
}}
>
<Download size={16} />
</Button>
</div>
)}
</section>
</div>
</section>

View File

@ -44,9 +44,14 @@ export interface IInputData {
inputs: Record<string, BeginQuery>;
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;
}

View File

@ -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;

View File

@ -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',

View File

@ -956,6 +956,8 @@ General实体和关系提取提示来自 GitHub - microsoft/graphrag基于
pleaseUploadAtLeastOneFile: '请上传至少一个文件',
},
flow: {
downloadFileTypeTip: '文件下载的类型',
downloadFileType: '文件类型',
formatTypeError: '格式或类型错误',
variableNameMessage: '名称只能包含字母和下划线',
variableDescription: '变量的描述',

View File

@ -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 += '</think>';
}
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,
});

View File

@ -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',
}

View File

@ -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 (
<Form {...form}>
<FormWrapper>
<FormContainer>
<FormItem>
<FormLabel tooltip={t('flow.downloadFileTypeTip')}>
{t('flow.downloadFileType')}
</FormLabel>
<FormField
control={form.control}
name={`output_format`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<RAGFlowSelect
options={Object.keys(ExportFileType).map(
(key: string) => {
return {
value:
ExportFileType[
key as keyof typeof ExportFileType
],
label: key,
};
},
)}
{...field}
onValueChange={field.onChange}
placeholder={t('flow.messagePlaceholder')}
></RAGFlowSelect>
</FormControl>
</FormItem>
)}
/>
</FormItem>
</FormContainer>
<FormContainer>
<FormItem>
<FormLabel tooltip={t('flow.msgTip')}>{t('flow.msg')}</FormLabel>

View File

@ -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]);

View File

@ -13,6 +13,7 @@ const {
get_document_file,
getFile,
moveFile,
get_document_file_download,
} = api;
const methods = {
@ -65,4 +66,10 @@ const fileManagerService = registerServer<keyof typeof methods>(
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;

View File

@ -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`,