diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx index 2dae3f7db4d3ba94d7f1ea57b995def972c21fae..940f6b474d9b88bbe2bb4702e401619cd1347e5e 100644 --- a/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx @@ -316,3 +316,13 @@ export function TavilySearchOptions({ settings }) { </> ); } + +export function DuckDuckGoOptions() { + return ( + <> + <p className="text-sm text-white/60 my-2"> + DuckDuckGo is ready to use without any additional configuration. + </p> + </> + ); +} diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/duckduckgo.png b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/duckduckgo.png new file mode 100644 index 0000000000000000000000000000000000000000..ce8762767974d421265f39255125221838a26b26 Binary files /dev/null and b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/duckduckgo.png differ diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx index cba4397c7990191eab55d6921a0af2a12abf3d98..c4e9907eb1e49b347b134a2278527b14bf511f10 100644 --- a/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx @@ -8,6 +8,7 @@ import BingSearchIcon from "./icons/bing.png"; import SerplySearchIcon from "./icons/serply.png"; import SearXNGSearchIcon from "./icons/searxng.png"; import TavilySearchIcon from "./icons/tavily.svg"; +import DuckDuckGoIcon from "./icons/duckduckgo.png"; import { CaretUpDown, MagnifyingGlass, @@ -24,6 +25,7 @@ import { SerplySearchOptions, SearXNGOptions, TavilySearchOptions, + DuckDuckGoOptions, } from "./SearchProviderOptions"; const SEARCH_PROVIDERS = [ @@ -35,6 +37,14 @@ const SEARCH_PROVIDERS = [ description: "Web search will be disabled until a provider and keys are provided.", }, + { + name: "DuckDuckGo", + value: "duckduckgo-engine", + logo: DuckDuckGoIcon, + options: () => <DuckDuckGoOptions />, + description: + "Free and privacy-focused web search using DuckDuckGo's HTML interface.", + }, { name: "Google Search Engine", value: "google-search-engine", diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index f43118c62626b85a95da9d529df06693d86fc8f8..9bd4a7bf1e0b48fbcae5d83189150a958582ebea 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -100,6 +100,7 @@ const SystemSettings = { "serply-engine", "searxng-engine", "tavily-search", + "duckduckgo-engine", ].includes(update) ) throw new Error("Invalid SERP provider."); diff --git a/server/utils/agents/aibitat/plugins/web-browsing.js b/server/utils/agents/aibitat/plugins/web-browsing.js index 31e06cab00600925424cb6d2f21f9989fd3c6a9a..0c139515f044440d590149dc6671f183367c69a5 100644 --- a/server/utils/agents/aibitat/plugins/web-browsing.js +++ b/server/utils/agents/aibitat/plugins/web-browsing.js @@ -80,6 +80,9 @@ const webBrowsing = { case "tavily-search": engine = "_tavilySearch"; break; + case "duckduckgo-engine": + engine = "_duckDuckGoEngine"; + break; default: engine = "_googleSearchEngine"; } @@ -499,6 +502,66 @@ const webBrowsing = { ); return JSON.stringify(data); }, + _duckDuckGoEngine: async function (query) { + this.super.introspect( + `${this.caller}: Using DuckDuckGo to search for "${ + query.length > 100 ? `${query.slice(0, 100)}...` : query + }"` + ); + + const searchURL = new URL("https://html.duckduckgo.com/html"); + searchURL.searchParams.append("q", query); + + const response = await fetch(searchURL.toString()); + + if (!response.ok) { + return `There was an error searching DuckDuckGo. Status: ${response.status}`; + } + + const html = await response.text(); + const data = []; + + const results = html.split('<div class="result results_links'); + + // Skip first element since it's before the first result + for (let i = 1; i < results.length; i++) { + const result = results[i]; + + // Extract title + const titleMatch = result.match( + /<a[^>]*class="result__a"[^>]*>(.*?)<\/a>/ + ); + const title = titleMatch ? titleMatch[1].trim() : ""; + + // Extract URL + const urlMatch = result.match( + /<a[^>]*class="result__a"[^>]*href="([^"]*)">/ + ); + const link = urlMatch ? urlMatch[1] : ""; + + // Extract snippet + const snippetMatch = result.match( + /<a[^>]*class="result__snippet"[^>]*>(.*?)<\/a>/ + ); + const snippet = snippetMatch + ? snippetMatch[1].replace(/<\/?b>/g, "").trim() + : ""; + + if (title && link && snippet) { + data.push({ title, link, snippet }); + } + } + + if (data.length === 0) { + return `No information was found online for the search query.`; + } + + this.super.introspect( + `${this.caller}: I found ${data.length} results - looking over them now.` + ); + + return JSON.stringify(data); + }, }); }, };