feat: add resource upload support for RAG (#768)

* feat: add resource upload support for RAG

- Backend: Added ingest_file method to Retriever and MilvusRetriever
- Backend: Added /api/rag/upload endpoint
- Frontend: Added RAGTab in settings for uploading resources
- Frontend: Updated translations and settings registration

* Apply suggestions from code review

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Apply suggestions from code review

* Apply suggestions from code review of src/rag/milvus.py

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Willem Jiang
2025-12-19 09:55:34 +08:00
committed by GitHub
parent 3e8f2ce3ad
commit 04296cdf5a
9 changed files with 567 additions and 2 deletions

View File

@@ -32,6 +32,17 @@
"addNewMCPServers": "Add New MCP Servers",
"mcpConfigDescription": "DeerFlow uses the standard JSON MCP config to create a new server.",
"pasteConfigBelow": "Paste your config below and click \"Add\" to add new servers.",
"rag": {
"title": "Resources",
"description": "Manage your knowledge base resources here. Upload markdown or text files to be indexed for retrieval.",
"upload": "Upload",
"uploading": "Uploading...",
"uploadSuccess": "File uploaded successfully",
"uploadFailed": "Failed to upload file",
"emptyFile": "Cannot upload an empty file",
"loading": "Loading resources...",
"noResources": "No resources found. Upload a file to get started."
},
"add": "Add",
"general": {
"title": "General",

View File

@@ -32,6 +32,17 @@
"addNewMCPServers": "添加新的 MCP 服务器",
"mcpConfigDescription": "DeerFlow 使用标准 JSON MCP 配置来创建新服务器。",
"pasteConfigBelow": "将您的配置粘贴到下面,然后点击\"添加\"来添加新服务器。",
"rag": {
"title": "资源",
"description": "在此管理您的知识库资源。上传 Markdown 或文本文件以供检索索引。",
"upload": "上传",
"uploading": "上传中...",
"uploadSuccess": "文件上传成功",
"uploadFailed": "文件上传失败",
"emptyFile": "无法上传空文件",
"loading": "正在加载资源...",
"noResources": "未找到资源。上传文件以开始使用。"
},
"add": "添加",
"general": {
"title": "通用",

View File

@@ -6,8 +6,9 @@ import { Settings, type LucideIcon } from "lucide-react";
import { AboutTab } from "./about-tab";
import { GeneralTab } from "./general-tab";
import { MCPTab } from "./mcp-tab";
import { RAGTab } from "./rag-tab";
export const SETTINGS_TABS = [GeneralTab, MCPTab, AboutTab].map((tab) => {
export const SETTINGS_TABS = [GeneralTab, RAGTab, MCPTab, AboutTab].map((tab) => {
const name = tab.displayName ?? tab.name;
return {
...tab,

View File

@@ -0,0 +1,151 @@
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
// SPDX-License-Identifier: MIT
import { Database, FileText, Upload } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { Button } from "~/components/ui/button";
import { resolveServiceURL } from "~/core/api/resolve-service-url";
import type { Resource } from "~/core/messages";
import { cn } from "~/lib/utils";
import type { Tab } from "./types";
export const RAGTab: Tab = () => {
const t = useTranslations("settings.rag");
const [resources, setResources] = useState<Resource[]>([]);
const [uploading, setUploading] = useState(false);
const [loading, setLoading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const fetchResources = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(resolveServiceURL("rag/resources"), {
method: "GET",
});
if (response.ok) {
const data = await response.json();
setResources(data.resources ?? []);
}
} catch (error) {
console.error("Failed to fetch resources:", error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
void fetchResources();
}, [fetchResources]);
const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) return;
if (file.size === 0) {
toast.error(t("emptyFile"));
event.target.value = "";
return;
}
setUploading(true);
const formData = new FormData();
formData.append("file", file);
try {
const response = await fetch(resolveServiceURL("rag/upload"), {
method: "POST",
body: formData,
});
if (response.ok) {
toast.success(t("uploadSuccess"));
void fetchResources();
} else {
const error = await response.json();
toast.error(error.detail ?? t("uploadFailed"));
}
} catch (error) {
console.error("Upload error:", error);
toast.error(t("uploadFailed"));
} finally {
setUploading(false);
// Reset input value to allow uploading same file again
event.target.value = "";
}
};
return (
<div className="flex flex-col gap-4">
<header>
<div className="flex items-center justify-between gap-2">
<h1 className="text-lg font-medium">{t("title")}</h1>
<div>
<input
ref={fileInputRef}
type="file"
accept=".md,.txt"
className="sr-only"
onChange={handleUpload}
disabled={uploading}
aria-label={t("upload")}
/>
<Button
disabled={uploading}
onClick={() => fileInputRef.current?.click()}
>
<Upload className="mr-2 h-4 w-4" />
{uploading ? t("uploading") : t("upload")}
</Button>
</div>
</div>
<div className={cn("text-muted-foreground text-sm")}>{t("description")}</div>
</header>
<main>
{loading ? (
<div className="flex items-center justify-center p-8 text-sm text-gray-500">
{t("loading")}
</div>
) : resources.length === 0 ? (
<div className="flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed p-8 text-center text-gray-500">
<Database className="h-8 w-8 opacity-50" />
<p>{t("noResources")}</p>
</div>
) : (
<ul className="flex flex-col gap-2">
{resources.map((resource, index) => (
<li
key={resource.uri}
className={cn("bg-card flex items-start gap-3 rounded-lg border p-3")}
>
<div className={cn("bg-primary/10 rounded p-2")}>
<FileText className="text-primary h-5 w-5" />
</div>
<div className="min-w-0 flex-1">
<h3 className="truncate font-medium">{resource.title}</h3>
<div className={cn("text-muted-foreground flex items-center gap-2 text-xs")}>
<span className="truncate max-w-[300px]" title={resource.uri}>
{resource.uri}
</span>
{resource.description && (
<>
<span></span>
<span className="truncate">{resource.description}</span>
</>
)}
</div>
</div>
</li>
))}
</ul>
)}
</main>
</div>
);
};
RAGTab.icon = Database;
RAGTab.displayName = "Resources";

View File

@@ -42,4 +42,5 @@ export interface ToolCallRuntime {
export interface Resource {
uri: string;
title: string;
description?: string;
}