From 04d3c1f315f10f8699b7795320c95995e7550229 Mon Sep 17 00:00:00 2001 From: Hk-Gosuto Date: Thu, 6 Jul 2023 00:19:05 +0800 Subject: [PATCH] sync upstream code --- app/api/tools/ddg/route.ts | 49 ++++++++++++++++++++++++++++++++++ app/client/api.ts | 10 +++++++ app/client/platforms/openai.ts | 2 +- app/client/tools/ddg_search.ts | 15 +++++++++++ app/components/chat.tsx | 20 ++++++++++++++ app/icons/search_close.svg | 1 + app/icons/search_open.svg | 1 + app/locales/cn.ts | 2 ++ app/locales/en.ts | 2 ++ app/store/chat.ts | 41 +++++++++++++++++++++++++--- package.json | 1 + yarn.lock | 33 ++++++++++++++++++++--- 12 files changed, 169 insertions(+), 8 deletions(-) create mode 100644 app/api/tools/ddg/route.ts create mode 100644 app/client/tools/ddg_search.ts create mode 100644 app/icons/search_close.svg create mode 100644 app/icons/search_open.svg diff --git a/app/api/tools/ddg/route.ts b/app/api/tools/ddg/route.ts new file mode 100644 index 000000000..19504a7c0 --- /dev/null +++ b/app/api/tools/ddg/route.ts @@ -0,0 +1,49 @@ +import { prettyObject } from "@/app/utils/format"; +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "../../auth"; + +import { search, SafeSearchType } from "duck-duck-scrape"; + +async function handle(req: NextRequest) { + if (req.method === "OPTIONS") { + return NextResponse.json({ body: "OK" }, { status: 200 }); + } + + let query = req.nextUrl.searchParams.get("query") ?? ""; + let maxResults = req.nextUrl.searchParams.get( + "max_results", + ) as unknown as number; + if (!maxResults) maxResults = 3; + console.log("[Tools Route] query ", query); + + const authResult = auth(req); + if (authResult.error) { + return NextResponse.json(authResult, { + status: 401, + }); + } + + try { + const searchResults = await search(query, { + safeSearch: SafeSearchType.OFF, + }); + const result = searchResults.results + .slice(0, maxResults) + .map(({ title, description, url }) => ({ + title, + content: description, + url, + })); + const res = new NextResponse(JSON.stringify(result)); + res.headers.set("Content-Type", "application/json"); + res.headers.set("Cache-Control", "no-cache"); + return res; + } catch (e) { + console.error("[Tools] ", e); + return NextResponse.json(prettyObject(e)); + } +} + +export const GET = handle; + +export const runtime = "nodejs"; diff --git a/app/client/api.ts b/app/client/api.ts index 08c4bb92a..8686d52a7 100644 --- a/app/client/api.ts +++ b/app/client/api.ts @@ -2,6 +2,7 @@ import { getClientConfig } from "../config/client"; import { ACCESS_CODE_PREFIX } from "../constant"; import { ChatMessage, ModelType, useAccessStore } from "../store"; import { ChatGPTApi } from "./platforms/openai"; +import { DuckDuckGoSearch } from "./tools/ddg_search"; export const ROLES = ["system", "user", "assistant"] as const; export type MessageRole = (typeof ROLES)[number]; @@ -12,6 +13,7 @@ export type ChatModel = ModelType; export interface RequestMessage { role: MessageRole; content: string; + toolPrompt?: string; } export interface LLMConfig { @@ -70,11 +72,19 @@ interface ChatProvider { usage: () => void; } +export abstract class ToolApi { + abstract call(input: string): Promise; + abstract name: string; + abstract description: string; +} + export class ClientApi { public llm: LLMApi; + public searchTool: ToolApi; constructor() { this.llm = new ChatGPTApi(); + this.searchTool = new DuckDuckGoSearch(); } config() {} diff --git a/app/client/platforms/openai.ts b/app/client/platforms/openai.ts index 3384aeefb..124689e02 100644 --- a/app/client/platforms/openai.ts +++ b/app/client/platforms/openai.ts @@ -44,7 +44,7 @@ export class ChatGPTApi implements LLMApi { async chat(options: ChatOptions) { const messages = options.messages.map((v) => ({ role: v.role, - content: v.content, + content: v.toolPrompt ?? v.content, })); const modelConfig = { diff --git a/app/client/tools/ddg_search.ts b/app/client/tools/ddg_search.ts new file mode 100644 index 000000000..c091dd124 --- /dev/null +++ b/app/client/tools/ddg_search.ts @@ -0,0 +1,15 @@ +import { ToolApi, getHeaders } from "../api"; + +export class DuckDuckGoSearch implements ToolApi { + name = "duckduckgo_search"; + description = + "A wrapper around DuckDuckGo Search.Useful for when you need to answer questions about current events.Input should be a search query."; + + async call(input: string): Promise { + const res = await fetch(`/api/tools/ddg?query=${input}`, { + method: "GET", + headers: getHeaders(), + }); + return await res.json(); + } +} diff --git a/app/components/chat.tsx b/app/components/chat.tsx index 267161506..99bd6ac3f 100644 --- a/app/components/chat.tsx +++ b/app/components/chat.tsx @@ -31,6 +31,8 @@ import AutoIcon from "../icons/auto.svg"; import BottomIcon from "../icons/bottom.svg"; import StopIcon from "../icons/pause.svg"; import RobotIcon from "../icons/robot.svg"; +import SearchCloseIcon from "../icons/search_close.svg"; +import SearchOpenIcon from "../icons/search_open.svg"; import { ChatMessage, @@ -388,6 +390,14 @@ export function ChatActions(props: { const navigate = useNavigate(); const chatStore = useChatStore(); + // switch web search + const webSearch = chatStore.currentSession().webSearch; + function switchWebSearch() { + chatStore.updateCurrentSession((session) => { + session.webSearch = !session.webSearch; + }); + } + // switch themes const theme = config.theme; function nextTheme() { @@ -489,6 +499,16 @@ export function ChatActions(props: { text={currentModel} icon={} /> + + : } + /> ); } diff --git a/app/icons/search_close.svg b/app/icons/search_close.svg new file mode 100644 index 000000000..bd819285d --- /dev/null +++ b/app/icons/search_close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/icons/search_open.svg b/app/icons/search_open.svg new file mode 100644 index 000000000..7dce1700e --- /dev/null +++ b/app/icons/search_open.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/locales/cn.ts b/app/locales/cn.ts index cb0cbbb17..d9fbb97d2 100644 --- a/app/locales/cn.ts +++ b/app/locales/cn.ts @@ -51,6 +51,8 @@ const cn = { Masks: "所有面具", Clear: "清除聊天", Settings: "对话设置", + OpenWebSearch: "开启联网", + CloseWebSearch: "关闭联网", }, Rename: "重命名对话", Typing: "正在输入…", diff --git a/app/locales/en.ts b/app/locales/en.ts index 11b8b1572..c1cce118a 100644 --- a/app/locales/en.ts +++ b/app/locales/en.ts @@ -52,6 +52,8 @@ const en: LocaleType = { Masks: "Masks", Clear: "Clear Context", Settings: "Settings", + OpenWebSearch: "Enable Web Search", + CloseWebSearch: "Disable Web Search", }, Rename: "Rename Chat", Typing: "Typing…", diff --git a/app/store/chat.ts b/app/store/chat.ts index 222b29c94..e2e990964 100644 --- a/app/store/chat.ts +++ b/app/store/chat.ts @@ -23,6 +23,7 @@ export type ChatMessage = RequestMessage & { isError?: boolean; id?: number; model?: ModelType; + toolPrompt?: string; }; export function createMessage(override: Partial): ChatMessage { @@ -31,6 +32,7 @@ export function createMessage(override: Partial): ChatMessage { date: new Date().toLocaleString(), role: "user", content: "", + toolPrompt: undefined, ...override, }; } @@ -53,6 +55,7 @@ export interface ChatSession { clearContextIndex?: number; mask: Mask; + webSearch: boolean; } export const DEFAULT_TOPIC = Locale.Store.DefaultTopic; @@ -76,6 +79,7 @@ function createEmptySession(): ChatSession { lastSummarizeIndex: 0, mask: createEmptyMask(), + webSearch: false, }; } @@ -309,10 +313,39 @@ export const useChatStore = create()( ...userMessage, content, }; - session.messages = session.messages.concat([ - savedUserMessage, - botMessage, - ]); + session.messages.push(savedUserMessage); + }); + + if (session.webSearch) { + const query = encodeURIComponent(content); + let searchResult = await api.searchTool.call(query); + console.log("[Tools] ", searchResult); + const webSearchPrompt = ` +Using the provided web search results, write a comprehensive reply to the given query. +If the provided search results refer to multiple subjects with the same name, write separate answers for each subject. +Make sure to cite results using \`[[number](URL)]\` notation after the reference. + +Web search json results: +""" +${JSON.stringify(searchResult)} +""" + +Current date: +""" +${new Date().toISOString()} +""" + +Query: +""" +${content} +""" + +Reply in ${getLang()} and markdown.`; + userMessage.toolPrompt = webSearchPrompt; + } + // save user's and bot's message + get().updateCurrentSession((session) => { + session.messages.push(botMessage); }); // make request diff --git a/package.json b/package.json index cec288f43..7dca59fb8 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@hello-pangea/dnd": "^16.3.0", "@svgr/webpack": "^6.5.1", "@vercel/analytics": "^0.1.11", + "duck-duck-scrape": "^2.2.4", "emoji-picker-react": "^4.4.7", "fuse.js": "^6.6.2", "html-to-image": "^1.11.11", diff --git a/yarn.lock b/yarn.lock index 4e86fd7c9..2e7bd1212 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2551,7 +2551,7 @@ dayjs@^1.11.7: resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.7.tgz#4b296922642f70999544d1144a2c25730fce63e2" integrity sha512-+Yw9U6YO5TQohxLcIkrXBeY73WP3ejHWVvx8XCk3gxvQDCTEmS48ZrSZCKciI7Bhl/uCMyxYtE9UqRILmFphkQ== -debug@^3.2.7: +debug@^3.2.6, debug@^3.2.7: version "3.2.7" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== @@ -2691,6 +2691,14 @@ domutils@^2.8.0: domelementtype "^2.2.0" domhandler "^4.2.0" +duck-duck-scrape@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/duck-duck-scrape/-/duck-duck-scrape-2.2.4.tgz#4281311ddd51997af6dab6a7f0e7b953a34c78c0" + integrity sha512-5hbMNxKYFQZrykT2heDuLW4smBWDp1jHrTz2ZqaLQMAz/6fgdolgSWpDKWZCd7ZcYHgfvQrmc/cnU7ICuEqQ9Q== + dependencies: + html-entities "^2.3.3" + needle "^3.2.0" + eastasianwidth@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" @@ -3553,6 +3561,11 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: dependencies: react-is "^16.7.0" +html-entities@^2.3.3: + version "2.4.0" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.4.0.tgz#edd0cee70402584c8c76cc2c0556db09d1f45061" + integrity sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ== + html-to-image@^1.11.11: version "1.11.11" resolved "https://registry.npmmirror.com/html-to-image/-/html-to-image-1.11.11.tgz#c0f8a34dc9e4b97b93ff7ea286eb8562642ebbea" @@ -3568,9 +3581,9 @@ husky@^8.0.0: resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" integrity sha512-+dQSyqPh4x1hlO1swXBiNb2HzTDN1I2IGLQx1GrBuiqFJfoMrnZWwVmatvSiO+Iz8fBUnf+lekwNo4c2LlXItg== -iconv-lite@0.6: +iconv-lite@0.6, iconv-lite@^0.6.3: version "0.6.3" - resolved "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== dependencies: safer-buffer ">= 2.1.2 < 3.0.0" @@ -4644,6 +4657,15 @@ natural-compare@^1.4.0: resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +needle@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/needle/-/needle-3.2.0.tgz#07d240ebcabfd65c76c03afae7f6defe6469df44" + integrity sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ== + dependencies: + debug "^3.2.6" + iconv-lite "^0.6.3" + sax "^1.2.4" + neo-async@^2.6.2: version "2.6.2" resolved "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" @@ -5300,6 +5322,11 @@ sass@^1.59.2: immutable "^4.0.0" source-map-js ">=0.6.2 <2.0.0" +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw== + scheduler@^0.23.0: version "0.23.0" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"