Add SerpApi web search (#4623)

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Alex Barron
2025-11-20 23:12:15 +00:00
committed by GitHub
parent 7a0c149d2e
commit 2eb5384e27
9 changed files with 356 additions and 0 deletions

View File

@@ -46,6 +46,7 @@
"royalblue",
"SearchApi",
"searxng",
"SerpApi",
"Serper",
"Serply",
"streamable",

View File

@@ -331,6 +331,10 @@ GID='1000'
# AGENT_SEARCHAPI_API_KEY=
# AGENT_SEARCHAPI_ENGINE=google
#------ SerpApi ----------- https://serpapi.com/
# AGENT_SERPAPI_API_KEY=
# AGENT_SERPAPI_ENGINE=google
#------ Serper.dev ----------- https://serper.dev/
# AGENT_SERPER_DEV_KEY=

View File

@@ -50,6 +50,80 @@ export function GoogleSearchOptions({ settings }) {
);
}
const SerpApiEngines = [
{ name: "Google Search", value: "google" },
{ name: "Google Images", value: "google_images_light" },
{ name: "Google Jobs", value: "google_jobs" },
{ name: "Google Maps", value: "google_maps" },
{ name: "Google News", value: "google_news_light" },
{ name: "Google Patents", value: "google_patents" },
{ name: "Google Scholar", value: "google_scholar" },
{ name: "Google Shopping", value: "google_shopping_light" },
{ name: "Amazon", value: "amazon" },
{ name: "Baidu", value: "baidu" },
];
export function SerpApiOptions({ settings }) {
return (
<>
<p className="text-sm text-white/60 my-2">
Get a free API key{" "}
<a
href="https://serpapi.com/"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from SerpApi.
</a>
</p>
<div className="flex gap-x-4">
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
API Key
</label>
<input
type="password"
name="env::AgentSerpApiKey"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="SerpApi API Key"
defaultValue={settings?.AgentSerpApiKey ? "*".repeat(20) : ""}
required={true}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Engine
</label>
<select
name="env::AgentSerpApiEngine"
required={true}
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
defaultValue={settings?.AgentSerpApiEngine || "google"}
>
{SerpApiEngines.map(({ name, value }) => (
<option key={name} value={value}>
{name}
</option>
))}
</select>
{/* <input
type="text"
name="env::AgentSerpApiEngine"
className="border-none bg-theme-settings-input-bg text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
placeholder="SerpApi engine (Google, Amazon...)"
defaultValue={settings?.AgentSerpApiEngine || "google"}
required={true}
autoComplete="off"
spellCheck={false}
/> */}
</div>
</div>
</>
);
}
const SearchApiEngines = [
{ name: "Google Search", value: "google" },
{ name: "Google Maps", value: "google_maps" },

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useRef, useState } from "react";
import Admin from "@/models/admin";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import GoogleSearchIcon from "./icons/google.png";
import SerpApiIcon from "./icons/serpapi.png";
import SearchApiIcon from "./icons/searchapi.png";
import SerperDotDevIcon from "./icons/serper.png";
import BingSearchIcon from "./icons/bing.png";
@@ -19,6 +20,7 @@ import {
import SearchProviderItem from "./SearchProviderItem";
import WebSearchImage from "@/media/agents/scrape-websites.png";
import {
SerpApiOptions,
SearchApiOptions,
SerperDotDevOptions,
GoogleSearchOptions,
@@ -54,6 +56,14 @@ const SEARCH_PROVIDERS = [
description:
"Web search powered by a custom Google Search Engine. Free for 100 queries per day.",
},
{
name: "SerpApi",
value: "serpapi",
logo: SerpApiIcon,
options: (settings) => <SerpApiOptions settings={settings} />,
description:
"Scrape Google and several other search engines with SerpApi. 250 free searches every month, and then paid.",
},
{
name: "SearchApi",
value: "searchapi",

View File

@@ -325,6 +325,10 @@ TTS_PROVIDER="native"
# AGENT_GSE_KEY=
# AGENT_GSE_CTX=
#------ SerpApi ----------- https://serpapi.com/
# AGENT_SERPAPI_API_KEY=
# AGENT_SERPAPI_ENGINE=google
#------ SearchApi.io ----------- https://www.searchapi.io/
# AGENT_SEARCHAPI_API_KEY=
# AGENT_SEARCHAPI_ENGINE=google

View File

@@ -106,6 +106,7 @@ const SystemSettings = {
if (
![
"google-search-engine",
"serpapi",
"searchapi",
"serper-dot-dev",
"bing-search",
@@ -276,6 +277,8 @@ const SystemSettings = {
// --------------------------------------------------------
AgentGoogleSearchEngineId: process.env.AGENT_GSE_CTX || null,
AgentGoogleSearchEngineKey: !!process.env.AGENT_GSE_KEY || null,
AgentSerpApiKey: !!process.env.AGENT_SERPAPI_API_KEY || null,
AgentSerpApiEngine: process.env.AGENT_SERPAPI_ENGINE || "google",
AgentSearchApiKey: !!process.env.AGENT_SEARCHAPI_API_KEY || null,
AgentSearchApiEngine: process.env.AGENT_SEARCHAPI_ENGINE || "google",
AgentSerperApiKey: !!process.env.AGENT_SERPER_DEV_KEY || null,

View File

@@ -69,6 +69,9 @@ const webBrowsing = {
case "google-search-engine":
engine = "_googleSearchEngine";
break;
case "serpapi":
engine = "_serpApi";
break;
case "searchapi":
engine = "_searchApi";
break;
@@ -170,6 +173,255 @@ const webBrowsing = {
return result;
},
/**
* Use SerpApi
* SerpApi supports dozens of search engines across the major platforms including Google, DuckDuckGo, Bing, eBay, Amazon, Baidu, Yandex, and more.
* https://serpapi.com/
*/
_serpApi: async function (query) {
if (!process.env.AGENT_SERPAPI_API_KEY) {
this.super.introspect(
`${this.caller}: I can't use SerpApi searching because the user has not defined the required API key.\nVisit: https://serpapi.com/ to create the API key for free.`
);
return `Search is disabled and no content was found. This functionality is disabled because the user has not set it up yet.`;
}
this.super.introspect(
`${this.caller}: Using SerpApi to search for "${
query.length > 100 ? `${query.slice(0, 100)}...` : query
}"`
);
const engine = process.env.AGENT_SERPAPI_ENGINE;
const queryParamKey = engine === "amazon" ? "k" : "q";
const params = new URLSearchParams({
engine: engine,
[queryParamKey]: query,
api_key: process.env.AGENT_SERPAPI_API_KEY,
});
const url = `https://serpapi.com/search.json?${params.toString()}`;
const { response, error } = await fetch(url, {
method: "GET",
headers: {},
})
.then((res) => {
if (res.ok) return res.json();
throw new Error(
`${res.status} - ${res.statusText}. params: ${JSON.stringify({ auth: this.middleTruncate(process.env.AGENT_SERPAPI_API_KEY, 5), q: query })}`
);
})
.then((data) => {
return { response: data, error: null };
})
.catch((e) => {
this.super.handlerProps.log(`SerpApi Error: ${e.message}`);
return { response: null, error: e.message };
});
if (error)
return `There was an error searching for content. ${error}`;
const data = [];
switch (engine) {
case "google":
if (response.hasOwnProperty("knowledge_graph"))
data.push(response.knowledge_graph);
if (response.hasOwnProperty("answer_box"))
data.push(response.answer_box);
response.organic_results?.forEach((searchResult) => {
const { title, link, snippet } = searchResult;
data.push({
title,
link,
snippet,
});
});
response.local_results?.forEach((searchResult) => {
const {
title,
rating,
reviews,
description,
address,
website,
extensions,
} = searchResult;
data.push({
title,
rating,
reviews,
description,
address,
website,
extensions,
});
});
case "google_maps":
response.local_results?.slice(0, 10).forEach((searchResult) => {
const {
title,
rating,
reviews,
description,
address,
website,
extensions,
} = searchResult;
data.push({
title,
rating,
reviews,
description,
address,
website,
extensions,
});
});
case "google_images_light":
response.images_results
?.slice(0, 10)
.forEach((searchResult) => {
const { title, source, link, thumbnail } = searchResult;
data.push({
title,
source,
link,
thumbnail,
});
});
case "google_shopping_light":
response.shopping_results
?.slice(0, 10)
.forEach((searchResult) => {
const {
title,
source,
price,
rating,
reviews,
snippet,
thumbnail,
product_link,
} = searchResult;
data.push({
title,
source,
price,
rating,
reviews,
snippet,
thumbnail,
product_link,
});
});
case "google_news_light":
response.news_results?.slice(0, 10).forEach((searchResult) => {
const { title, link, source, thumbnail, snippet, date } =
searchResult;
data.push({
title,
link,
source,
thumbnail,
snippet,
date,
});
});
case "google_jobs":
response.jobs_results?.forEach((searchResult) => {
const {
title,
company_name,
location,
description,
apply_options,
extensions,
} = searchResult;
data.push({
title,
company_name,
location,
description,
apply_options,
extensions,
});
});
case "google_patents":
response.organic_results?.forEach((searchResult) => {
const {
title,
patent_link,
snippet,
inventor,
assignee,
publication_number,
} = searchResult;
data.push({
title,
patent_link,
snippet,
inventor,
assignee,
publication_number,
});
});
case "google_scholar":
response.organic_results?.forEach((searchResult) => {
const { title, link, snippet, publication_info } =
searchResult;
data.push({
title,
link,
snippet,
publication_info,
});
});
case "baidu":
if (response.hasOwnProperty("answer_box"))
data.push(response.answer_box);
response.organic_results?.forEach((searchResult) => {
const { title, link, snippet } = searchResult;
data.push({
title,
link,
snippet,
});
});
case "amazon":
response.organic_results
?.slice(0, 10)
.forEach((searchResult) => {
const {
title,
rating,
reviews,
price,
link_clean,
thumbnail,
} = searchResult;
data.push({
title,
rating,
reviews,
price,
link_clean,
thumbnail,
});
});
}
if (data.length === 0)
return `No information was found online for the search query.`;
const result = JSON.stringify(data);
this.super.introspect(
`${this.caller}: I found ${data.length} results - reviewing the results now. (~${this.countTokens(result)} tokens)`
);
return result;
},
/**
* Use SearchApi
* SearchApi supports multiple search engines like Google Search, Bing Search, Baidu Search, Google News, YouTube, and many more.

View File

@@ -549,6 +549,14 @@ const KEY_MAPPING = {
envKey: "AGENT_GSE_KEY",
checks: [],
},
AgentSerpApiKey: {
envKey: "AGENT_SERPAPI_API_KEY",
checks: [],
},
AgentSerpApiEngine: {
envKey: "AGENT_SERPAPI_ENGINE",
checks: [],
},
AgentSearchApiKey: {
envKey: "AGENT_SEARCHAPI_API_KEY",
checks: [],