mirror of
https://git.mirrors.martin98.com/https://github.com/bytedance/deer-flow
synced 2025-08-20 15:09:06 +08:00
feat: rag retrieving tool call result display (#263)
* feat: local search tool call result display * chore: add file copyright * fix: miss edit plan interrupt feedback * feat: disable pasting html into input box
This commit is contained in:
parent
7e9fbed918
commit
4ddd659d8d
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from .retriever import Retriever, Document, Resource
|
||||
from .ragflow import RAGFlowProvider
|
||||
from .builder import build_retriever
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from src.config.tools import SELECTED_RAG_PROVIDER, RAGProvider
|
||||
from src.rag.ragflow import RAGFlowProvider
|
||||
from src.rag.retriever import Retriever
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import requests
|
||||
from src.rag.retriever import Chunk, Document, Resource, Retriever
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import abc
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from src.rag.retriever import Resource
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import logging
|
||||
from typing import List, Optional, Type
|
||||
from langchain_core.tools import BaseTool
|
||||
@ -19,9 +22,7 @@ class RetrieverInput(BaseModel):
|
||||
|
||||
class RetrieverTool(BaseTool):
|
||||
name: str = "local_search_tool"
|
||||
description: str = (
|
||||
"Useful for retrieving information from the file with `rag://` uri prefix, it should be higher priority than the web search or writing code. Input should be a search keywords."
|
||||
)
|
||||
description: str = "Useful for retrieving information from the file with `rag://` uri prefix, it should be higher priority than the web search or writing code. Input should be a search keywords."
|
||||
args_schema: Type[BaseModel] = RetrieverInput
|
||||
|
||||
retriever: Retriever = Field(default_factory=Retriever)
|
||||
|
10154
web/public/replay/rag.txt
Normal file
10154
web/public/replay/rag.txt
Normal file
File diff suppressed because one or more lines are too long
@ -49,7 +49,6 @@ export function InputBox({
|
||||
|
||||
const handleSendMessage = useCallback(
|
||||
(message: string, resources: Array<Resource>) => {
|
||||
console.log(message, resources);
|
||||
if (responding) {
|
||||
onCancel?.();
|
||||
} else {
|
||||
|
@ -4,7 +4,7 @@
|
||||
import { PythonOutlined } from "@ant-design/icons";
|
||||
import { motion } from "framer-motion";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import { BookOpenText, PencilRuler, Search } from "lucide-react";
|
||||
import { BookOpenText, FileText, PencilRuler, Search } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
import SyntaxHighlighter from "react-syntax-highlighter";
|
||||
@ -96,6 +96,8 @@ function ActivityListItem({ messageId }: { messageId: string }) {
|
||||
return <CrawlToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else if (toolCall.name === "python_repl_tool") {
|
||||
return <PythonToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else if (toolCall.name === "local_search_tool") {
|
||||
return <RetrieverToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
} else {
|
||||
return <MCPToolCall key={toolCall.id} toolCall={toolCall} />;
|
||||
}
|
||||
@ -118,6 +120,7 @@ type SearchResult =
|
||||
image_url: string;
|
||||
image_description: string;
|
||||
};
|
||||
|
||||
function WebSearchToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const searching = useMemo(() => {
|
||||
return toolCall.result === undefined;
|
||||
@ -275,6 +278,64 @@ function CrawlToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
);
|
||||
}
|
||||
|
||||
function RetrieverToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const searching = useMemo(() => {
|
||||
return toolCall.result === undefined;
|
||||
}, [toolCall.result]);
|
||||
const documents = useMemo<
|
||||
Array<{ id: string; title: string; content: string }>
|
||||
>(() => {
|
||||
return toolCall.result ? parseJSON(toolCall.result, []) : [];
|
||||
}, [toolCall.result]);
|
||||
return (
|
||||
<section className="mt-4 pl-4">
|
||||
<div className="font-medium italic">
|
||||
<RainbowText className="flex items-center" animated={searching}>
|
||||
<Search size={16} className={"mr-2"} />
|
||||
<span>Retrieving documents from RAG </span>
|
||||
<span className="max-w-[500px] overflow-hidden text-ellipsis whitespace-nowrap">
|
||||
{(toolCall.args as { keywords: string }).keywords}
|
||||
</span>
|
||||
</RainbowText>
|
||||
</div>
|
||||
<div className="pr-4">
|
||||
{documents && (
|
||||
<ul className="mt-2 flex flex-wrap gap-4">
|
||||
{searching &&
|
||||
[...Array(2)].map((_, i) => (
|
||||
<li
|
||||
key={`search-result-${i}`}
|
||||
className="flex h-40 w-40 gap-2 rounded-md text-sm"
|
||||
>
|
||||
<Skeleton
|
||||
className="to-accent h-full w-full rounded-md bg-gradient-to-tl from-slate-400"
|
||||
style={{ animationDelay: `${i * 0.2}s` }}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
{documents.map((doc, i) => (
|
||||
<motion.li
|
||||
key={`search-result-${i}`}
|
||||
className="text-muted-foreground bg-accent flex max-w-40 gap-2 rounded-md px-2 py-1 text-sm"
|
||||
initial={{ opacity: 0, y: 10, scale: 0.66 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
transition={{
|
||||
duration: 0.2,
|
||||
delay: i * 0.1,
|
||||
ease: "easeOut",
|
||||
}}
|
||||
>
|
||||
<FileText size={32} />
|
||||
{doc.title}
|
||||
</motion.li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function PythonToolCall({ toolCall }: { toolCall: ToolCallRuntime }) {
|
||||
const code = useMemo<string | undefined>(() => {
|
||||
return (toolCall.args as { code?: string }).code;
|
||||
|
@ -18,7 +18,7 @@ import { useDebouncedCallback } from "use-debounce";
|
||||
|
||||
import "~/styles/prosemirror.css";
|
||||
import { resourceSuggestion } from "./resource-suggestion";
|
||||
import React, { forwardRef, useMemo, useRef } from "react";
|
||||
import React, { forwardRef, useEffect, useMemo, useRef } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { useRAGProvider } from "~/core/api/hooks";
|
||||
import { LoadingOutlined } from "@ant-design/icons";
|
||||
@ -76,6 +76,9 @@ function formatItem(item: JSONContent): {
|
||||
const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
({ className, onChange, onEnter }: MessageInputProps, ref) => {
|
||||
const editorRef = useRef<Editor>(null);
|
||||
const handleEnterRef = useRef<
|
||||
((message: string, resources: Array<Resource>) => void) | undefined
|
||||
>(onEnter);
|
||||
const debouncedUpdates = useDebouncedCallback(
|
||||
async (editor: EditorInstance) => {
|
||||
if (onChange) {
|
||||
@ -100,6 +103,10 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
},
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
handleEnterRef.current = onEnter;
|
||||
}, [onEnter]);
|
||||
|
||||
const { provider, loading } = useRAGProvider();
|
||||
|
||||
const extensions = useMemo(() => {
|
||||
@ -127,11 +134,11 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
addKeyboardShortcuts() {
|
||||
return {
|
||||
Enter: () => {
|
||||
if (onEnter) {
|
||||
if (handleEnterRef.current) {
|
||||
const { text, resources } = formatMessage(
|
||||
this.editor.getJSON() ?? [],
|
||||
);
|
||||
onEnter(text, resources);
|
||||
handleEnterRef.current(text, resources);
|
||||
}
|
||||
return this.editor.commands.clearContent();
|
||||
},
|
||||
@ -150,7 +157,7 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
);
|
||||
}
|
||||
return extensions;
|
||||
}, [onEnter, provider]);
|
||||
}, [provider]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@ -172,6 +179,7 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
class:
|
||||
"prose prose-base dark:prose-invert inline-editor font-default focus:outline-none max-w-full",
|
||||
},
|
||||
transformPastedHTML: transformPastedHTML,
|
||||
}}
|
||||
onCreate={({ editor }) => {
|
||||
editorRef.current = editor;
|
||||
@ -186,4 +194,18 @@ const MessageInput = forwardRef<MessageInputRef, MessageInputProps>(
|
||||
},
|
||||
);
|
||||
|
||||
function transformPastedHTML(html: string) {
|
||||
try {
|
||||
// Strip HTML from user-pasted content
|
||||
const tempEl = document.createElement("div");
|
||||
tempEl.innerHTML = html;
|
||||
|
||||
return tempEl.textContent || tempEl.innerText || "";
|
||||
} catch (error) {
|
||||
console.error("Error transforming pasted HTML", error);
|
||||
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export default MessageInput;
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
||||
import type { Resource } from "~/core/messages";
|
||||
import { cn } from "~/lib/utils";
|
||||
|
@ -1,3 +1,6 @@
|
||||
// Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import type { MentionOptions } from "@tiptap/extension-mention";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import {
|
||||
|
Loading…
x
Reference in New Issue
Block a user