From 7c734a7e4f8b7a730487cca7386fd81a026b64c3 Mon Sep 17 00:00:00 2001 From: Sebastjan Prachovskij Date: Thu, 12 Sep 2024 17:23:30 +0300 Subject: [PATCH] Add SearchApi search provider --- core/src/blocks/search.rs | 24 +- front/components/app/blocks/Search.tsx | 2 +- front/components/providers/SearchApiSetup.tsx | 206 ++++++++++++++++++ front/lib/providers.ts | 6 + .../api/w/[wId]/providers/[pId]/check.ts | 20 ++ .../w/[wId]/vaults/[vaultId]/apps/index.tsx | 12 + types/src/front/lib/api/credentials.ts | 3 + 7 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 front/components/providers/SearchApiSetup.tsx diff --git a/core/src/blocks/search.rs b/core/src/blocks/search.rs index 63efc3ff7344..38b9f7ab7025 100644 --- a/core/src/blocks/search.rs +++ b/core/src/blocks/search.rs @@ -21,6 +21,7 @@ pub struct Error { #[derive(Debug, Clone, Copy, Serialize, PartialEq, Deserialize)] #[serde(rename_all = "lowercase")] pub enum SearchProviderID { + SearchApi, SerpAPI, Serper, } @@ -28,6 +29,7 @@ pub enum SearchProviderID { impl ToString for SearchProviderID { fn to_string(&self) -> String { match self { + SearchProviderID::SearchApi => String::from("searchapi"), SearchProviderID::SerpAPI => String::from("serpapi"), SearchProviderID::Serper => String::from("serper"), } @@ -39,10 +41,11 @@ impl FromStr for SearchProviderID { fn from_str(s: &str) -> std::result::Result { match s { + "searchapi" => Ok(SearchProviderID::SearchApi), "serpapi" => Ok(SearchProviderID::SerpAPI), "serper" => Ok(SearchProviderID::Serper), _ => Err(ParseError::with_message( - "Unknown search provider ID (possible values: serpapi, serper)", + "Unknown search provider ID (possible values: searchapi, serpapi, serper)", )), } } @@ -200,6 +203,7 @@ impl Block for Search { let query = replace_variables_in_string(&self.query, "query", env)?; let credential_key = match provider_id { + SearchProviderID::SearchApi => "SEARCH_API_KEY", SearchProviderID::SerpAPI => "SERP_API_KEY", SearchProviderID::Serper => "SERPER_API_KEY", }; @@ -221,6 +225,24 @@ impl Block for Search { }; let request = match provider_id { + SearchProviderID::SearchApi => { + let url = format!( + "https://www.searchapi.io/api/v1/search?q={}&engine={}&num={}", + encode(&query), + self.engine, + num.unwrap_or(10) + ); + + let headers = json!({ + "Authorization": format!("Bearer {}", provider_api_key), + "Content-Type": "application/json", + "X-SearchApi-Source": "dust" + }); + + let request = HttpRequest::new("GET", url.as_str(), headers, Value::Null)?; + + request + } SearchProviderID::SerpAPI => { let url = match num { None => format!( diff --git a/front/components/app/blocks/Search.tsx b/front/components/app/blocks/Search.tsx index 94fc3a6039c7..1f7a008a9b0d 100644 --- a/front/components/app/blocks/Search.tsx +++ b/front/components/app/blocks/Search.tsx @@ -50,7 +50,7 @@ export default function Search({ }); const serviceProviders = filterServiceProviders(providers); const searchProviders = serviceProviders?.filter?.( - (p) => p.providerId === "serpapi" || p.providerId === "serper" + (p) => p.providerId === "searchapi" || p.providerId === "serpapi" || p.providerId === "serper" ); const currentProvider = searchProviders?.find?.( diff --git a/front/components/providers/SearchApiSetup.tsx b/front/components/providers/SearchApiSetup.tsx new file mode 100644 index 000000000000..0abb1bb5af94 --- /dev/null +++ b/front/components/providers/SearchApiSetup.tsx @@ -0,0 +1,206 @@ +import { Button } from "@dust-tt/sparkle"; +import type { WorkspaceType } from "@dust-tt/types"; +import { Dialog, Transition } from "@headlessui/react"; +import { Fragment, useEffect, useState } from "react"; +import { useSWRConfig } from "swr"; + +import { checkProvider } from "@app/lib/providers"; + +export default function SearchApiSetup({ + owner, + open, + setOpen, + config, + enabled, +}: { + owner: WorkspaceType; + open: boolean; + setOpen: (open: boolean) => void; + config: { [key: string]: string }; + enabled: boolean; +}) { + const { mutate } = useSWRConfig(); + + const [apiKey, setApiKey] = useState(config ? config.api_key : ""); + const [testSuccessful, setTestSuccessful] = useState(false); + const [testRunning, setTestRunning] = useState(false); + const [testError, setTestError] = useState(""); + const [enableRunning, setEnableRunning] = useState(false); + + useEffect(() => { + if (config && config.api_key.length > 0 && apiKey.length == 0) { + setApiKey(config.api_key); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [config]); + + const runTest = async () => { + setTestRunning(true); + setTestError(""); + const check = await checkProvider(owner, "searchapi", { api_key: apiKey }); + + if (!check.ok) { + setTestError(check.error); + setTestSuccessful(false); + setTestRunning(false); + } else { + setTestError(""); + setTestSuccessful(true); + setTestRunning(false); + } + }; + + const handleEnable = async () => { + setEnableRunning(true); + const res = await fetch(`/api/w/${owner.sId}/providers/searchapi`, { + headers: { + "Content-Type": "application/json", + }, + method: "POST", + body: JSON.stringify({ + config: JSON.stringify({ + api_key: apiKey, + }), + }), + }); + await res.json(); + setEnableRunning(false); + setOpen(false); + await mutate(`/api/w/${owner.sId}/providers`); + }; + + const handleDisable = async () => { + const res = await fetch(`/api/w/${owner.sId}/providers/searchapi`, { + method: "DELETE", + }); + await res.json(); + setOpen(false); + await mutate(`/api/w/${owner.sId}/providers`); + }; + + return ( + + setOpen(false)}> + +
+ + +
+
+ + +
+
+ + Setup SearchApi Search + +
+

+ SearchApi lets you search Google, Bing, Baidu,and other search + engines. To use SearchApi you must provide your API key. + It can be found{" "} + + here + +

+

+ We'll never use your API key for anything other than to + run your apps. +

+
+
+ { + setApiKey(e.target.value); + setTestSuccessful(false); + }} + /> +
+
+
+
+ {testError.length > 0 ? ( + Error: {testError} + ) : testSuccessful ? ( + + Test succeeded! You can enable SearchApi Search. + + ) : ( +   + )} +
+
+ {enabled ? ( +
handleDisable()} + > + Disable +
+ ) : ( + <> + )} +
+
+
+
+ {testSuccessful ? ( +
+
+
+
+
+
+
+
+ ); +} diff --git a/front/lib/providers.ts b/front/lib/providers.ts index 2e16c0e0bb67..b910808fe7dd 100644 --- a/front/lib/providers.ts +++ b/front/lib/providers.ts @@ -72,6 +72,12 @@ type ServiceProvider = { }; export const serviceProviders: ServiceProvider[] = [ + { + providerId: "searchapi", + name: "SearchApi", + built: true, + enabled: false, + }, { providerId: "serpapi", name: "SerpApi (Google Search)", diff --git a/front/pages/api/w/[wId]/providers/[pId]/check.ts b/front/pages/api/w/[wId]/providers/[pId]/check.ts index c5ec99df2ca4..b333878963f8 100644 --- a/front/pages/api/w/[wId]/providers/[pId]/check.ts +++ b/front/pages/api/w/[wId]/providers/[pId]/check.ts @@ -146,6 +146,26 @@ async function handler( } return; + case "searchapi": + const testSearch = await fetch( + `https://www.searchapi.io/api/v1/search?engine=google&q=dust.tt`, + { + method: "GET", + headers: { + "Authorization": `Bearer ${config.api_key}`, + "Content-Type": "application/json", + "X-SearchApi-Source": "dust" + }, + } + ); + if (!testSearch.ok) { + const err = await testSearch.json(); + res.status(400).json({ ok: false, error: err.error }); + } else { + await testSearch.json(); + res.status(200).json({ ok: true }); + } + return; case "serpapi": const testSearch = await fetch( `https://serpapi.com/search?engine=google&q=Coffee&api_key=${config.api_key}`, diff --git a/front/pages/w/[wId]/vaults/[vaultId]/apps/index.tsx b/front/pages/w/[wId]/vaults/[vaultId]/apps/index.tsx index 21679f100741..525b8b23aee7 100644 --- a/front/pages/w/[wId]/vaults/[vaultId]/apps/index.tsx +++ b/front/pages/w/[wId]/vaults/[vaultId]/apps/index.tsx @@ -33,6 +33,7 @@ import BrowserlessAPISetup from "@app/components/providers/BrowserlessAPISetup"; import GoogleAiStudioSetup from "@app/components/providers/GoogleAiStudioSetup"; import MistralAISetup from "@app/components/providers/MistralAISetup"; import OpenAISetup from "@app/components/providers/OpenAISetup"; +import SearchApiSetup from "@app/components/providers/SearchApiSetup"; import SerpAPISetup from "@app/components/providers/SerpAPISetup"; import SerperSetup from "@app/components/providers/SerperSetup"; import AppLayout from "@app/components/sparkle/AppLayout"; @@ -437,6 +438,7 @@ export function Providers({ owner }: { owner: WorkspaceType }) { const [anthropicOpen, setAnthropicOpen] = useState(false); const [mistalAIOpen, setMistralAiOpen] = useState(false); const [googleAiStudioOpen, setGoogleAiStudioOpen] = useState(false); + const [searchapiOpen, setSearchapiOpen] = useState(false); const [serpapiOpen, setSerpapiOpen] = useState(false); const [serperOpen, setSerperOpen] = useState(false); const [browserlessapiOpen, setBrowserlessapiOpen] = useState(false); @@ -514,6 +516,13 @@ export function Providers({ owner }: { owner: WorkspaceType }) { enabled={!!configs["google_ai_studio"]} config={configs["google_ai_studio"] ?? null} /> + { switch (provider.providerId) { + case "searchapi": + setSearchapiOpen(true); + break; case "serpapi": setSerpapiOpen(true); break; diff --git a/types/src/front/lib/api/credentials.ts b/types/src/front/lib/api/credentials.ts index 7076dad5d5db..5169a3ccbc5e 100644 --- a/types/src/front/lib/api/credentials.ts +++ b/types/src/front/lib/api/credentials.ts @@ -46,6 +46,9 @@ export const credentialsFromProviders = ( case "textsynth": credentials["TEXTSYNTH_API_KEY"] = config.api_key; break; + case "searchapi": + credentials["SEARCHAPI_API_KEY"] = config.api_key; + break; case "serpapi": credentials["SERP_API_KEY"] = config.api_key; break;