feat: web search

This commit is contained in:
Hk-Gosuto 2025-02-25 12:57:21 +08:00
parent 657e44b501
commit 59dd3213f8
34 changed files with 404 additions and 102 deletions

View File

@ -354,19 +354,28 @@ For ByteDance: use `modelName@bytedance=deploymentName` to customize model name
### `DEEPSEEK_API_KEY` (可选)
DeepSeek Api Key.
DeepSeek Api Key
### `DEEPSEEK_URL` (可选)
DeepSeek Api Url.
DeepSeek Api Url
### `SILICONFLOW_API_KEY` (可选)
硅基流动 API Key.
硅基流动 API Key
### `SILICONFLOW_URL` (可选)
硅基流动 API URL.
硅基流动 API URL
### `TAVILY_API_KEY`
Tavily API Key 用于通用搜索功能
获取地址https://tavily.com
### `TAVILY_MAX_RETURNS` (可选)
通用搜索功能返回的最大结果数,默认为 10
## 部署

View File

@ -19,6 +19,7 @@ const DANGER_CONFIG = {
isUseOpenAIEndpointForAllModels: serverConfig.isUseOpenAIEndpointForAllModels,
disableModelProviderDisplay: serverConfig.disableModelProviderDisplay,
isUseRemoteModels: serverConfig.isUseRemoteModels,
isEnableWebSearch: serverConfig.isEnableWebSearch,
};
declare global {

71
app/api/search/route.ts Normal file
View File

@ -0,0 +1,71 @@
import { getServerSideConfig } from "@/app/config/server";
import { NextRequest, NextResponse } from "next/server";
import { auth } from "../auth";
import { ModelProvider } from "@/app/constant";
import { tavily } from "@tavily/core";
const serverConfig = getServerSideConfig();
async function handle(req: NextRequest) {
const authResult = auth(req, ModelProvider.GPT);
if (authResult.error) {
return NextResponse.json(authResult, {
status: 401,
});
}
try {
const tavilyApiKey = serverConfig.tavilyApiKey;
const maxReturns = serverConfig.tavilyMaxReturns
? parseInt(serverConfig.tavilyMaxReturns, 10)
: 10;
if (!tavilyApiKey) {
return NextResponse.json(
{
error: true,
message: "Tavily API key not configured",
},
{
status: 401,
},
);
}
const body = await req.json();
const { query } = body;
if (!query) {
return NextResponse.json(
{
error: true,
message: "Search query is required",
},
{
status: 400,
},
);
}
const tvly = tavily({ apiKey: tavilyApiKey });
const response = await tvly.search(query, {
maxResults: maxReturns,
});
return NextResponse.json(response);
} catch (error) {
console.error("[Tavily] search error:", error);
return NextResponse.json(
{
error: true,
message: "Failed to process search request",
},
{
status: 500,
},
);
}
}
export const POST = handle;
export const runtime = "edge";

View File

@ -25,6 +25,7 @@ import { DeepSeekApi } from "./platforms/deepseek";
import { XAIApi } from "./platforms/xai";
import { ChatGLMApi } from "./platforms/glm";
import { SiliconflowApi } from "./platforms/siliconflow";
import { TavilySearchResponse } from "@tavily/core";
export const ROLES = ["system", "user", "assistant"] as const;
export type MessageRole = (typeof ROLES)[number];
@ -45,6 +46,7 @@ export interface RequestMessage {
role: MessageRole;
content: string | MultimodalContent[];
fileInfos?: FileInfo[];
webSearchReferences?: TavilySearchResponse;
}
export interface LLMConfig {

View File

@ -25,7 +25,10 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
@ -104,7 +107,7 @@ export class QwenApi implements LLMApi {
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
content: getMessageTextContent(v),
content: getWebReferenceMessageTextContent(v),
}));
const modelConfig = {

View File

@ -22,7 +22,11 @@ import {
} from "@/app/store";
import { getClientConfig } from "@/app/config/client";
import { ANTHROPIC_BASE_URL } from "@/app/constant";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
isVisionModel,
} from "@/app/utils";
import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
@ -318,7 +322,7 @@ export class ClaudeApi implements LLMApi {
if (!visionModel || typeof content === "string") {
return {
role: insideRole,
content: getMessageTextContent(v),
content: getWebReferenceMessageTextContent(v),
};
}
return {

View File

@ -26,7 +26,10 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
@ -97,7 +100,7 @@ export class ErnieApi implements LLMApi {
const messages = options.messages.map((v) => ({
// "error_code": 336006, "error_msg": "the role of message with even index in the messages must be user or function",
role: v.role === "system" ? "user" : v.role,
content: getMessageTextContent(v),
content: getWebReferenceMessageTextContent(v),
}));
// "error_code": 336006, "error_msg": "the length of messages must be an odd number",

View File

@ -25,7 +25,10 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
export interface OpenAIListModelResponse {
@ -98,7 +101,7 @@ export class DoubaoApi implements LLMApi {
async chat(options: ChatOptions) {
const messages = options.messages.map((v) => ({
role: v.role,
content: getMessageTextContent(v),
content: getWebReferenceMessageTextContent(v),
}));
const modelConfig = {

View File

@ -28,6 +28,7 @@ import { getClientConfig } from "@/app/config/client";
import {
getMessageTextContent,
getMessageTextContentWithoutThinking,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -86,7 +87,7 @@ export class DeepSeekApi implements LLMApi {
const content = getMessageTextContentWithoutThinking(v);
messages.push({ role: v.role, content });
} else {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}
}

View File

@ -24,7 +24,10 @@ import {
TranscriptionOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -78,7 +81,7 @@ export class ChatGLMApi implements LLMApi {
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}

View File

@ -25,6 +25,7 @@ import {
getMessageTextContent,
getMessageImages,
isVisionModel,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
@ -91,7 +92,7 @@ export class GeminiProApi implements LLMApi {
_messages.push({ role: v.role, content });
}
const messages = _messages.map((v) => {
let parts: any[] = [{ text: getMessageTextContent(v) }];
let parts: any[] = [{ text: getWebReferenceMessageTextContent(v) }];
if (isVisionModel(options.config.model)) {
const images = getMessageImages(v);
if (images.length > 0) {

View File

@ -24,7 +24,10 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { fetch } from "@/app/utils/stream";
import { RequestPayload } from "./openai";
@ -79,7 +82,7 @@ export class SparkApi implements LLMApi {
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}

View File

@ -25,7 +25,10 @@ import {
TranscriptionOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -79,7 +82,7 @@ export class MoonshotApi implements LLMApi {
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}

View File

@ -44,6 +44,7 @@ import {
getMessageTextContent,
isVisionModel,
isDalle3 as _isDalle3,
getWebReferenceMessageTextContent,
} from "@/app/utils";
export interface OpenAIListModelResponse {
@ -239,7 +240,7 @@ export class ChatGPTApi implements LLMApi {
for (const v of options.messages) {
const content = visionModel
? await preProcessImageContent(v.content)
: getMessageTextContent(v);
: getWebReferenceMessageTextContent(v);
if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content });
}

View File

@ -28,6 +28,7 @@ import { getClientConfig } from "@/app/config/client";
import {
getMessageTextContent,
getMessageTextContentWithoutThinking,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -89,7 +90,7 @@ export class SiliconflowApi implements LLMApi {
const content = getMessageTextContentWithoutThinking(v);
messages.push({ role: v.role, content });
} else {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}
}

View File

@ -20,7 +20,11 @@ import {
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent, isVisionModel } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
isVisionModel,
} from "@/app/utils";
import mapKeys from "lodash-es/mapKeys";
import mapValues from "lodash-es/mapValues";
import isArray from "lodash-es/isArray";
@ -110,7 +114,7 @@ export class HunyuanApi implements LLMApi {
const messages = options.messages.map((v, index) => ({
// "Messages 中 system 角色必须位于列表的最开始"
role: index !== 0 && v.role === "system" ? "user" : v.role,
content: visionModel ? v.content : getMessageTextContent(v),
content: visionModel ? v.content : getWebReferenceMessageTextContent(v),
}));
const modelConfig = {

View File

@ -1,3 +1,4 @@
import { TavilySearchResponse } from "@tavily/core";
import { ClientApi, getClientApi, getHeaders } from "../api";
import { ChatSession } from "@/app/store";
@ -45,3 +46,19 @@ export class FileApi {
return fileInfo;
}
}
export class WebApi {
async search(query: string): Promise<TavilySearchResponse> {
var headers = getHeaders(true);
const api = "/api/search";
var res = await fetch(api, {
method: "POST",
body: JSON.stringify({ query }),
headers: {
...headers,
},
});
const resJson = await res.json();
return resJson;
}
}

View File

@ -20,7 +20,10 @@ import {
TranscriptionOptions,
} from "../api";
import { getClientConfig } from "@/app/config/client";
import { getMessageTextContent } from "@/app/utils";
import {
getMessageTextContent,
getWebReferenceMessageTextContent,
} from "@/app/utils";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream";
@ -74,7 +77,7 @@ export class XAIApi implements LLMApi {
async chat(options: ChatOptions) {
const messages: ChatOptions["messages"] = [];
for (const v of options.messages) {
const content = getMessageTextContent(v);
const content = getWebReferenceMessageTextContent(v);
messages.push({ role: v.role, content });
}

View File

@ -52,6 +52,8 @@ import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import ReloadIcon from "../icons/reload.svg";
import HeadphoneIcon from "../icons/headphone.svg";
import SearchCloseIcon from "../icons/search_close.svg";
import SearchOpenIcon from "../icons/search_open.svg";
import {
ChatMessage,
SubmitKey,
@ -509,6 +511,17 @@ export function ChatActions(props: {
const pluginStore = usePluginStore();
const session = chatStore.currentSession();
// switch web search
const webSearch = chatStore.currentSession().mask.webSearch;
function switchWebSearch() {
chatStore.updateTargetSession(session, (session) => {
session.mask.webSearch =
!session.mask.webSearch &&
!isFunctionCallModel(currentModel) &&
isEnableWebSearch;
});
}
// switch Plugins
const usePlugins = chatStore.currentSession().mask.usePlugins;
function switchUsePlugins() {
@ -593,6 +606,11 @@ export function ChatActions(props: {
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
const isEnableWebSearch = useMemo(
() => accessStore.enableWebSearch(),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);
useEffect(() => {
const show = isVisionModel(currentModel);
@ -723,6 +741,17 @@ export function ChatActions(props: {
text={currentModelName}
icon={<RobotIcon />}
/>
{!isFunctionCallModel(currentModel) && isEnableWebSearch && (
<ChatAction
onClick={switchWebSearch}
text={
webSearch
? Locale.Chat.InputActions.CloseWebSearch
: Locale.Chat.InputActions.OpenWebSearch
}
icon={webSearch ? <SearchOpenIcon /> : <SearchCloseIcon />}
/>
)}
{showModelSelector && (
<SearchSelector
@ -1351,7 +1380,12 @@ function _Chat() {
const textContent = getMessageTextContent(userMessage);
const images = getMessageImages(userMessage);
chatStore
.onUserInput(textContent, images, userMessage.fileInfos)
.onUserInput(
textContent,
images,
userMessage.fileInfos,
userMessage.webSearchReferences,
)
.then(() => setIsLoading(false));
inputRef.current?.focus();
};
@ -1432,34 +1466,36 @@ function _Chat() {
// preview messages
const renderMessages = useMemo(() => {
return context
.concat(session.messages as RenderMessage[])
.concat(
isLoading
? [
{
...createMessage({
role: "assistant",
content: "……",
}),
preview: true,
},
]
: [],
)
.concat(
userInput.length > 0 && config.sendPreviewBubble
? [
{
...createMessage({
role: "user",
content: userInput,
}),
preview: true,
},
]
: [],
);
return (
context
.concat(session.messages as RenderMessage[])
// .concat(
// isLoading
// ? [
// {
// ...createMessage({
// role: "assistant",
// content: "……",
// }),
// preview: true,
// },
// ]
// : [],
// )
.concat(
userInput.length > 0 && config.sendPreviewBubble
? [
{
...createMessage({
role: "user",
content: userInput,
}),
preview: true,
},
]
: [],
)
);
}, [
config.sendPreviewBubble,
context,
@ -2093,6 +2129,7 @@ function _Chat() {
<Markdown
key={message.streaming ? "loading" : "done"}
content={getMessageTextContent(message)}
webSearchReferences={message.webSearchReferences}
loading={
(message.preview || message.streaming) &&
message.content.length === 0 &&
@ -2140,9 +2177,9 @@ function _Chat() {
</div>
)}
</div>
{message?.audio_url && (
{message?.audioUrl && (
<div className={styles["chat-message-audio"]}>
<audio src={message.audio_url} controls />
<audio src={message.audioUrl} controls />
</div>
)}

View File

@ -23,6 +23,7 @@ import { useChatStore } from "../store";
import { IconButton } from "./button";
import { useAppConfig } from "../store/config";
import { TavilySearchResponse } from "@tavily/core";
export function Mermaid(props: { code: string }) {
const ref = useRef<HTMLDivElement>(null);
@ -273,10 +274,20 @@ function tryWrapHtmlCode(text: string) {
);
}
function _MarkDownContent(props: { content: string }) {
function _MarkDownContent(props: {
content: string;
webSearchReferences?: TavilySearchResponse;
}) {
const escapedContent = useMemo(() => {
return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]);
let content = tryWrapHtmlCode(escapeBrackets(props.content));
if (props.webSearchReferences?.results) {
content = content.replace(/\[citation:(\d+)\]/g, (match, index) => {
const result = props.webSearchReferences?.results[parseInt(index) - 1];
return result ? `[\[${index}\]](${result.url})` : match;
});
}
return content;
}, [props.content, props.webSearchReferences]);
return (
<ReactMarkdown
@ -332,6 +343,7 @@ export function Markdown(
fontFamily?: string;
parentRef?: RefObject<HTMLDivElement>;
defaultShow?: boolean;
webSearchReferences?: TavilySearchResponse;
} & React.DOMAttributes<HTMLDivElement>,
) {
const mdRef = useRef<HTMLDivElement>(null);
@ -351,7 +363,10 @@ export function Markdown(
{props.loading ? (
<LoadingIcon />
) : (
<MarkdownContent content={props.content} />
<MarkdownContent
content={props.content}
webSearchReferences={props.webSearchReferences}
/>
)}
</div>
);

View File

@ -191,11 +191,11 @@ export function RealtimeChat({
});
}
if (hasAudio) {
// upload audio get audio_url
// upload audio get audioUrl
const blob = audioHandlerRef.current?.savePlayFile();
uploadImage(blob!).then((audio_url) => {
botMessage.audio_url = audio_url;
// update text and audio_url
uploadImage(blob!).then((audioUrl) => {
botMessage.audioUrl = audioUrl;
// update text and audioUrl
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});
@ -215,15 +215,15 @@ export function RealtimeChat({
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat([userMessage]);
});
// save input audio_url, and update session
// save input audioUrl, and update session
const { audioStartMillis, audioEndMillis } = item;
// upload audio get audio_url
// upload audio get audioUrl
const blob = audioHandlerRef.current?.saveRecordFile(
audioStartMillis,
audioEndMillis,
);
uploadImage(blob!).then((audio_url) => {
userMessage.audio_url = audio_url;
uploadImage(blob!).then((audioUrl) => {
userMessage.audioUrl = audioUrl;
chatStore.updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});

View File

@ -285,5 +285,9 @@ export const getServerSideConfig = () => {
disableModelProviderDisplay: !!process.env.DISABLE_MODEL_PROVIDER_DISPLAY,
isUseRemoteModels: !!process.env.USE_REMOTE_MODELS,
tavilyApiKey: process.env.TAVILY_API_KEY,
tavilyMaxReturns: process.env.TAVILY_MAX_RETURNS ?? "10",
isEnableWebSearch: !!process.env.TAVILY_API_KEY,
};
};

View File

@ -662,30 +662,3 @@ export const internalAllowedWebDavEndpoints = [
];
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const MYFILES_BROWSER_TOOLS_SYSTEM_PROMPT = `
# Tools
## myfiles_browser
You have the tool 'myfiles_browser' with the following functions:
issues queries to search the file(s) uploaded in the current conversation and displays the results.
This tool is for browsing the files uploaded by the user.
Parts of the documents uploaded by users will be automatically included in the conversation. Only use this tool when the relevant parts don't contain the necessary information to fulfill the user's request.
If the user needs to summarize the document, they can summarize it through parts of the document.
Think carefully about how the information you find relates to the user's request. Respond as soon as you find information that clearly answers the request.
Issue multiple queries to the 'myfiles_browser' command only when the user's question needs to be decomposed to find different facts. In other scenarios, prefer providing a single query. Avoid single-word queries that are extremely broad and will return unrelated results.
Here are some examples of how to use the 'myfiles_browser' command:
User: What was the GDP of France and Italy in the 1970s? => myfiles_browser(["france gdp 1970", "italy gdp 1970"])
User: What does the report say about the GPT4 performance on MMLU? => myfiles_browser(["GPT4 MMLU performance"])
User: How can I integrate customer relationship management system with third-party email marketing tools? => myfiles_browser(["customer management system marketing integration"])
User: What are the best practices for data security and privacy for our cloud storage services? => myfiles_browser(["cloud storage security and privacy"])
The user has uploaded the following files:
`;

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683102619308" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7254" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M533.333333 42.666667C262.346667 42.666667 42.666667 262.346667 42.666667 533.333333s219.68 490.666667 490.666666 490.666667 490.666667-219.68 490.666667-490.666667S804.32 42.666667 533.333333 42.666667z m342.086667 760.253333c-29.266667-16-61.566667-29.633333-96.166667-40.853333 18.713333-63.493333 29.413333-134 31.153334-207.4h170.42a445.853333 445.853333 0 0 1-94.413334 254.446666q-5.4-3.113333-10.993333-6.193333z m-695.166667 6.193333A445.853333 445.853333 0 0 1 85.84 554.666667h170.42c1.74 73.373333 12.44 143.906667 31.153333 207.4-34.6 11.22-66.9 24.886667-96.166666 40.853333q-5.593333 3.08-10.993334 6.193333z m10.993334-545.366666c29.266667 15.966667 61.566667 29.633333 96.166666 40.853333-18.713333 63.493333-29.413333 134-31.153333 207.4H85.84a445.866667 445.866667 0 0 1 94.413333-254.446667q5.4 3.113333 10.993334 6.193334zM554.666667 341.073333c64.34-1.526667 126.5-9.94 183.64-24.606666 17.6 59.6 27.706667 126.106667 29.426666 195.533333H554.666667z m0-42.666666V87.226667c52.386667 9.293333 101.793333 52.666667 140.96 124.453333a502.986667 502.986667 0 0 1 29.153333 64.24c-52.853333 13.313333-110.4 21-170.113333 22.48z m-42.666667-211.18V298.4c-59.713333-1.48-117.26-9.166667-170.113333-22.48a502.986667 502.986667 0 0 1 29.153333-64.24C410.206667 139.886667 459.613333 96.52 512 87.226667z m0 253.846666V512H298.933333c1.72-69.426667 11.826667-135.933333 29.426667-195.533333 57.14 14.666667 119.3 23.08 183.64 24.606666zM298.933333 554.666667H512v170.926666c-64.34 1.526667-126.5 9.94-183.64 24.606667-17.6-59.6-27.693333-126.106667-29.426667-195.533333zM512 768.266667v211.173333c-52.386667-9.293333-101.793333-52.666667-140.96-124.453333a502.986667 502.986667 0 0 1-29.153333-64.24c52.853333-13.313333 110.4-21 170.113333-22.48z m42.666667 211.173333V768.266667c59.713333 1.48 117.26 9.166667 170.113333 22.48a502.986667 502.986667 0 0 1-29.153333 64.24c-39.166667 71.793333-88.573333 115.16-140.96 124.453333z m0-253.846667V554.666667h213.066666c-1.72 69.426667-11.826667 135.933333-29.426666 195.533333-57.14-14.666667-119.3-23.08-183.64-24.606667zM810.406667 512c-1.74-73.373333-12.406667-143.906667-31.153334-207.4 34.6-11.22 66.9-24.886667 96.166667-40.853333q5.586667-3.053333 10.993333-6.193334A445.866667 445.866667 0 0 1 980.826667 512zM858 224.626667c-1 0.553333-2 1.113333-3 1.666666-27.053333 14.753333-56.98 27.413333-89.1 37.826667a546.88 546.88 0 0 0-32.806667-72.873333c-18.113333-33.206667-38.526667-61.333333-60.926666-84A448.106667 448.106667 0 0 1 858 224.626667z m-463.473333-117.333334c-22.4 22.666667-42.813333 50.78-60.926667 84a546 546 0 0 0-32.806667 72.873334c-32.126667-10.46-62.06-23.12-89.113333-37.873334-1.013333-0.553333-2-1.113333-3-1.666666a448.106667 448.106667 0 0 1 185.833333-117.366667zM208.666667 842.04c1-0.553333 2-1.113333 3-1.666667 27.053333-14.753333 56.98-27.413333 89.1-37.826666a546 546 0 0 0 32.806666 72.873333c18.113333 33.206667 38.526667 61.333333 60.926667 84A448.106667 448.106667 0 0 1 208.666667 842.04z m463.473333 117.333333c22.4-22.666667 42.813333-50.78 60.926667-84a546.88 546.88 0 0 0 32.806666-72.873333c32.12 10.413333 62.046667 23.073333 89.1 37.826667 1.013333 0.553333 2 1.113333 3 1.666666a448.106667 448.106667 0 0 1-185.82 117.413334z" fill="#353436" p-id="7255"></path></svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1683102597441" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7012" width="16" height="16" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M191.246667 263.746667c28.546667 15.573333 60 28.96 93.606666 40-17.2 64.12-27.02 134.82-28.62 208.226666H43.133333a488.26 488.26 0 0 1 101.233334-277.766666 422.393333 422.393333 0 0 0 46.88 29.54zM512 554.666667H298.913333c1.566667 69.333333 10.78 136 26.853334 196.206666 57.846667-15.08 120.92-23.733333 186.233333-25.28z m-320.753333 248.253333c28.546667-15.573333 60-28.96 93.606666-40-17.2-64.12-27.02-134.82-28.62-208.226667H43.133333a488.26 488.26 0 0 0 101.233334 277.793334 422.393333 422.393333 0 0 1 46.88-29.566667zM554.666667 66v232.4c61.16-1.513333 120.053333-9.54 173.953333-23.453333a549.766667 549.766667 0 0 0-33.18-78.666667C656.246667 121.126667 606.906667 75.74 554.666667 66z m-42.666667 446V341.073333c-65.333333-1.546667-128.386667-10.2-186.233333-25.28C309.693333 376 300.48 442.666667 298.913333 512z m228.9-196.206667C683.053333 330.873333 620 339.526667 554.666667 341.073333V512h213.086666c-1.566667-69.333333-10.78-136-26.853333-196.206667zM512 1000.666667v-232.4c-61.16 1.513333-120.053333 9.54-173.953333 23.453333a549.766667 549.766667 0 0 0 33.18 78.7C410.42 945.54 459.76 990.926667 512 1000.666667zM371.226667 196.246667a549.766667 549.766667 0 0 0-33.18 78.7c53.9 13.913333 112.793333 21.94 173.953333 23.453333V66c-52.24 9.74-101.58 55.126667-140.773333 130.246667z m504.193333 67.5c-28.546667 15.573333-60 28.96-93.606667 40 17.2 64.12 27.02 134.82 28.62 208.226666h213.1a488.26 488.26 0 0 0-101.233333-277.793333 422.393333 422.393333 0 0 1-46.88 29.566667z m-663.74-37.453334c26 14.193333 54.7 26.44 85.433333 36.626667 10.293333-30.866667 22.426667-59.833333 36.286667-86.406667 24.866667-47.666667 54.086667-85.3 86.853333-111.846666Q428.666667 57.833333 437.28 52.073333c-104.086667 20.666667-196.313333 74.233333-265.38 149.426667a387.226667 387.226667 0 0 0 39.78 24.793333zM420.253333 1002c-32.766667-26.56-62-64.193333-86.853333-111.86-13.86-26.566667-26-55.54-36.286667-86.406667-30.733333 10.186667-59.413333 22.433333-85.433333 36.626667a387.226667 387.226667 0 0 0-39.78 24.793333c69.066667 75.193333 161.293333 128.766667 265.38 149.426667q-8.613333-5.746667-17.026667-12.58z m434.733334-161.64c-26-14.193333-54.666667-26.44-85.433334-36.626667-10.293333 30.866667-22.426667 59.84-36.286666 86.406667-24.866667 47.666667-54.086667 85.333333-86.853334 111.86q-8.42 6.82-17.026666 12.58c104.086667-20.666667 196.313333-74.233333 265.38-149.426667a387.226667 387.226667 0 0 0-39.78-24.78z m-159.546667 30.046667a549.766667 549.766667 0 0 0 33.18-78.7c-53.9-13.913333-112.793333-21.94-173.953333-23.453334V1000.666667c52.24-9.74 101.58-55.126667 140.773333-130.246667zM554.666667 554.666667v170.926666c65.333333 1.546667 128.386667 10.2 186.233333 25.28 16.073333-60.233333 25.286667-126.84 26.853333-196.206666zM646.413333 64.666667c32.766667 26.56 62 64.193333 86.853334 111.86 13.86 26.573333 26 55.54 36.286666 86.406666 30.733333-10.186667 59.413333-22.433333 85.433334-36.626666a387.226667 387.226667 0 0 0 39.78-24.793334c-69.066667-75.206667-161.293333-128.78-265.38-149.44Q638 57.833333 646.413333 64.666667z m275.886667 767.806666A488.26 488.26 0 0 0 1023.533333 554.666667h-213.1c-1.6 73.406667-11.42 144.106667-28.62 208.226666 33.633333 11.066667 65.06 24.453333 93.606667 40a422.393333 422.393333 0 0 1 46.88 29.566667z" fill="#353436" p-id="7013"></path></svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -73,9 +73,12 @@ const cn = {
DisablePlugins: "关闭插件",
UploadImage: "上传图片",
UploadFle: "上传文件",
OpenWebSearch: "开启联网",
CloseWebSearch: "关闭联网",
},
Rename: "重命名对话",
Typing: "正在输入…",
Searching: "联网搜索中…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} 发送`;
if (submitKey === String(SubmitKey.Enter)) {

View File

@ -75,9 +75,12 @@ const en: LocaleType = {
DisablePlugins: "Disable Plugins",
UploadImage: "Upload Images",
UploadFle: "Upload Files",
OpenWebSearch: "Enable Web Search",
CloseWebSearch: "Disable Web Search",
},
Rename: "Rename Chat",
Typing: "Typing…",
Searching: "Searching…",
Input: (submitKey: string) => {
var inputHints = `${submitKey} to send`;
if (submitKey === String(SubmitKey.Enter)) {

60
app/prompt.ts Normal file
View File

@ -0,0 +1,60 @@
export const MYFILES_BROWSER_TOOLS_SYSTEM_PROMPT = `
# Tools
## myfiles_browser
You have the tool 'myfiles_browser' with the following functions:
issues queries to search the file(s) uploaded in the current conversation and displays the results.
This tool is for browsing the files uploaded by the user.
Parts of the documents uploaded by users will be automatically included in the conversation. Only use this tool when the relevant parts don't contain the necessary information to fulfill the user's request.
If the user needs to summarize the document, they can summarize it through parts of the document.
Think carefully about how the information you find relates to the user's request. Respond as soon as you find information that clearly answers the request.
Issue multiple queries to the 'myfiles_browser' command only when the user's question needs to be decomposed to find different facts. In other scenarios, prefer providing a single query. Avoid single-word queries that are extremely broad and will return unrelated results.
Here are some examples of how to use the 'myfiles_browser' command:
User: What was the GDP of France and Italy in the 1970s? => myfiles_browser(["france gdp 1970", "italy gdp 1970"])
User: What does the report say about the GPT4 performance on MMLU? => myfiles_browser(["GPT4 MMLU performance"])
User: How can I integrate customer relationship management system with third-party email marketing tools? => myfiles_browser(["customer management system marketing integration"])
User: What are the best practices for data security and privacy for our cloud storage services? => myfiles_browser(["cloud storage security and privacy"])
The user has uploaded the following files:
`;
export const WEB_SEARCH_ANSWER_ZH_PROMPT = `# 以下内容是基于用户发送的消息的搜索结果:
{search_results}
[webpage X begin]...[webpage X end]X代表每篇文章的数字索引[citation:X][citation:3][citation:5]
- {cur_date}
-
- 10
- [citation:3][citation:5]
- 5
-
-
-
-
#
{question}`;
export const WEB_SEARCH_ANSWER_EN_PROMPT = `# The following contents are the search results related to the user's message:
{search_results}
In the search results I provide to you, each result is formatted as [webpage X begin]...[webpage X end], where X represents the numerical index of each article. Please cite the context at the end of the relevant sentence when appropriate. Use the citation format [citation:X] in the corresponding part of your answer. If a sentence is derived from multiple contexts, list all relevant citation numbers, such as [citation:3][citation:5]. Be sure not to cluster all citations at the end; instead, include them in the corresponding parts of the answer.
When responding, please keep the following points in mind:
- Today is {cur_date}.
- Not all content in the search results is closely related to the user's question. You need to evaluate and filter the search results based on the question.
- For listing-type questions (e.g., listing all flight information), try to limit the answer to 10 key points and inform the user that they can refer to the search sources for complete information. Prioritize providing the most complete and relevant items in the list. Avoid mentioning content not provided in the search results unless necessary.
- For creative tasks (e.g., writing an essay), ensure that references are cited within the body of the text, such as [citation:3][citation:5], rather than only at the end of the text. You need to interpret and summarize the user's requirements, choose an appropriate format, fully utilize the search results, extract key information, and generate an answer that is insightful, creative, and professional. Extend the length of your response as much as possible, addressing each point in detail and from multiple perspectives, ensuring the content is rich and thorough.
- If the response is lengthy, structure it well and summarize it in paragraphs. If a point-by-point format is needed, try to limit it to 5 points and merge related content.
- For objective Q&A, if the answer is very brief, you may add one or two related sentences to enrich the content.
- Choose an appropriate and visually appealing format for your response based on the user's requirements and the content of the answer, ensuring strong readability.
- Your answer should synthesize information from multiple relevant webpages and avoid repeatedly citing the same webpage.
- Unless the user requests otherwise, your response should be in the same language as the user's question.
# The user's message is:
{question}`;

View File

@ -142,6 +142,7 @@ const DEFAULT_ACCESS_STATE = {
defaultModel: "",
visionModels: "",
isEnableRAG: false,
isEnableWebSearch: false,
// tts config
edgeTTSVoiceName: "zh-CN-YunxiNeural",
@ -191,6 +192,12 @@ export const useAccessStore = createPersistStore(
return get().isEnableRAG;
},
enableWebSearch() {
this.fetch();
return get().isEnableWebSearch;
},
isValidOpenAI() {
return ensure(get(), ["openaiApiKey"]);
},

View File

@ -33,8 +33,9 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask";
import { FileInfo } from "../client/platforms/utils";
import { FileInfo, WebApi } from "../client/platforms/utils";
import { usePluginStore } from "./plugin";
import { TavilySearchResponse } from "@tavily/core";
export interface ChatToolMessage {
toolName: string;
@ -64,7 +65,7 @@ export type ChatMessage = RequestMessage & {
id: string;
model?: ModelType;
tools?: ChatMessageTool[];
audio_url?: string;
audioUrl?: string;
};
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
@ -383,9 +384,11 @@ export const useChatStore = createPersistStore(
content: string,
attachImages?: string[],
attachFiles?: FileInfo[],
webSearchReference?: TavilySearchResponse,
) {
const session = get().currentSession();
const modelConfig = session.mask.modelConfig;
const accessStore = useAccessStore.getState();
const userContent = fillTemplateWith(content, modelConfig);
console.log("[User Input] after template: ", userContent);
@ -413,6 +416,7 @@ export const useChatStore = createPersistStore(
role: "user",
content: mContent,
fileInfos: attachFiles,
webSearchReferences: webSearchReference,
});
const botMessage: ChatMessage = createMessage({
@ -534,6 +538,16 @@ export const useChatStore = createPersistStore(
};
agentCall();
} else {
if (session.mask.webSearch && accessStore.enableWebSearch()) {
botMessage.content = Locale.Chat.Searching;
get().updateTargetSession(session, (session) => {
session.messages = session.messages.concat();
});
const webApi = new WebApi();
const webSearchReference = await webApi.search(content);
userMessage.webSearchReferences = webSearchReference;
botMessage.webSearchReferences = webSearchReference;
}
// make request
api.llm.chat({
messages: sendMessages,

View File

@ -18,6 +18,7 @@ export type Mask = {
lang: Lang;
builtin: boolean;
usePlugins?: boolean;
webSearch?: boolean;
// 上游插件业务参数
plugin?: string[];
enableArtifacts?: boolean;

View File

@ -1,11 +1,15 @@
import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib";
import Locale from "./locales";
import Locale, { getLang } from "./locales";
import { RequestMessage } from "./client/api";
import { DEFAULT_MODELS } from "./constant";
import { ServiceProvider } from "./constant";
// import { fetch as tauriFetch, ResponseType } from "@tauri-apps/api/http";
import { fetch as tauriStreamFetch } from "./utils/stream";
import {
WEB_SEARCH_ANSWER_EN_PROMPT,
WEB_SEARCH_ANSWER_ZH_PROMPT,
} from "./prompt";
export function trimTopic(topic: string) {
// Fix an issue where double quotes still show in the Indonesian language
@ -227,6 +231,36 @@ export function isMacOS(): boolean {
return false;
}
export function getWebReferenceMessageTextContent(message: RequestMessage) {
let prompt = getMessageTextContent(message);
if (
message.webSearchReferences &&
message.webSearchReferences.results.length > 0
) {
const searchResults = message.webSearchReferences.results
.map((result, index) => {
return `[webpage ${index + 1} begin]
[webpage title]${result.title}
[webpage url]${result.url}
[webpage content begin]
${result.content}
[webpage content end]
[webpage ${index + 1} end]
`;
})
.join("\n");
const isZh = getLang() == "cn";
const promptTemplate = isZh
? WEB_SEARCH_ANSWER_ZH_PROMPT
: WEB_SEARCH_ANSWER_EN_PROMPT;
prompt = promptTemplate
.replace("{cur_date}", new Date().toLocaleString())
.replace("{search_results}", searchResults)
.replace("{question}", prompt);
}
return prompt;
}
export function getMessageTextContent(message: RequestMessage) {
if (typeof message.content === "string") {
return message.content;

View File

@ -34,6 +34,7 @@
"@qdrant/js-client-rest": "^1.8.2",
"@supabase/supabase-js": "^2.44.2",
"@svgr/webpack": "^6.5.1",
"@tavily/core": "^0.3.1",
"@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2",
"axios": "^1.7.5",
@ -109,12 +110,12 @@
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^13.2.2",
"prettier": "^3.0.2",
"raw-loader": "^4.0.2",
"ts-node": "^10.9.2",
"tsx": "^4.16.0",
"typescript": "5.2.2",
"watch": "^1.0.2",
"webpack": "^5.88.1",
"raw-loader": "^4.0.2"
"webpack": "^5.88.1"
},
"resolutions": {
"lint-staged/yaml": "^2.2.2",

View File

@ -3430,6 +3430,14 @@
"@tauri-apps/cli-win32-ia32-msvc" "1.5.11"
"@tauri-apps/cli-win32-x64-msvc" "1.5.11"
"@tavily/core@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@tavily/core/-/core-0.3.1.tgz#bd07c7328be76b061a3dc12e6b978b6ca3a56627"
integrity sha512-7jyvPWG4Zjst0s4v0FMLO1f/dfHqs4FnqvKm86zOGYzXxSfxHu0isbLzlwjJad0csYwF0kifdlECTuNouHfr5A==
dependencies:
axios "^1.7.7"
js-tiktoken "^1.0.14"
"@testing-library/dom@^10.4.0":
version "10.4.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.4.0.tgz#82a9d9462f11d240ecadbf406607c6ceeeff43a8"
@ -4323,7 +4331,7 @@ axe-core@^4.9.1:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.9.1.tgz#fcd0f4496dad09e0c899b44f6c4bb7848da912ae"
integrity sha512-QbUdXJVTpvUTHU7871ppZkdOLBeGUKBQWHkHrvN2V9IQWGMt61zf3B45BtzjxEJzYuj0JBjBZP/hmYS/R9pmAw==
axios@^1.7.5:
axios@^1.7.5, axios@^1.7.7:
version "1.7.9"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.9.tgz#d7d071380c132a24accda1b2cfc1535b79ec650a"
integrity sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==
@ -7876,6 +7884,13 @@ js-tiktoken@^1.0.12:
dependencies:
base64-js "^1.5.1"
js-tiktoken@^1.0.14:
version "1.0.19"
resolved "https://registry.yarnpkg.com/js-tiktoken/-/js-tiktoken-1.0.19.tgz#0298b584382f1d47d4b45cb93d382f11780eab78"
integrity sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==
dependencies:
base64-js "^1.5.1"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"