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:
JeffJiang 2025-05-29 19:52:34 +08:00 committed by GitHub
parent 7e9fbed918
commit 4ddd659d8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 10267 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -1,3 +1,6 @@
# Copyright (c) 2025 Bytedance Ltd. and/or its affiliates
# SPDX-License-Identifier: MIT
import abc
from pydantic import BaseModel, Field

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

@ -49,7 +49,6 @@ export function InputBox({
const handleSendMessage = useCallback(
(message: string, resources: Array<Resource>) => {
console.log(message, resources);
if (responding) {
onCancel?.();
} else {

View File

@ -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&nbsp;</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;

View File

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

View File

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

View File

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