diff --git a/docker/.env.example b/docker/.env.example index 9992f5c9029..baef4372287 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -444,6 +444,10 @@ GID='1000' #------ You.com Search ----------- https://you.com/docs # AGENT_YOU_API_KEY= # Optional. Keyless free tier works by default; set for higher rate limits. +#------ Keenable ----------- https://keenable.ai +# AGENT_KEENABLE_API_KEY= # Optional. Keyless free tier by default; set a key to lift rate limits. +# AGENT_KEENABLE_API_URL= # Optional. Defaults to https://api.keenable.ai. + ########################################### ######## Other Configurations ############ ########################################### diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx index 5513617d7f0..5da10a9858e 100644 --- a/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/SearchProviderOptions/index.jsx @@ -445,6 +445,55 @@ export function DuckDuckGoOptions() { ); } +export function KeenableSearchOptions({ settings }) { + return ( + <> +

+ Keenable works without an API key by default (keyless free tier). To + lift rate limits, add an API key{" "} + + from Keenable. + +

+
+
+ + +
+
+ + +
+
+ + ); +} + export function ExaSearchOptions({ settings }) { return ( <> diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/keenable.svg b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/keenable.svg new file mode 100644 index 00000000000..be1d489ec4b --- /dev/null +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/icons/keenable.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx index 2208df15db2..9a6c774f86b 100644 --- a/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx +++ b/frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx @@ -14,6 +14,7 @@ import PerplexitySearchIcon from "./icons/perplexity.png"; import BraveSearchIcon from "./icons/brave.png"; import CrwSearchIcon from "./icons/crw.png"; import YouSearchIcon from "./icons/you.png"; +import KeenableSearchIcon from "./icons/keenable.svg"; import { CaretUpDown, MagnifyingGlass, @@ -38,6 +39,7 @@ import { BraveSearchOptions, CrwSearchOptions, YouSearchOptions, + KeenableSearchOptions, } from "./SearchProviderOptions"; const SEARCH_PROVIDERS = [ @@ -48,6 +50,14 @@ const SEARCH_PROVIDERS = [ options: () => , description: "Free and privacy-focused web search using DuckDuckGo.", }, + { + name: "Keenable", + value: "keenable-search", + logo: KeenableSearchIcon, + options: (settings) => , + description: + "Web search built for AI agents. Works without an API key (keyless free tier); add a key to lift rate limits.", + }, { name: "Brave Search", value: "brave-search", diff --git a/server/.env.example b/server/.env.example index f63cffd41d7..1f50aaed8dc 100644 --- a/server/.env.example +++ b/server/.env.example @@ -450,6 +450,10 @@ STT_PROVIDER="native" #------ You.com Search ----------- https://you.com/docs # AGENT_YOU_API_KEY= # Optional. Keyless free tier works by default; set for higher rate limits. +#------ Keenable ----------- https://keenable.ai +# AGENT_KEENABLE_API_KEY= # Optional. Keyless free tier by default; set a key to lift rate limits. +# AGENT_KEENABLE_API_URL= # Optional. Defaults to https://api.keenable.ai. + ########################################### ######## Other Configurations ############ ########################################### diff --git a/server/models/systemSettings.js b/server/models/systemSettings.js index 6b1bfaa0854..0e3890208a9 100644 --- a/server/models/systemSettings.js +++ b/server/models/systemSettings.js @@ -167,6 +167,7 @@ const SystemSettings = { "brave-search", "crw-search", "you-search", + "keenable-search", ].includes(update) ) throw new Error("Invalid SERP provider."); @@ -591,6 +592,8 @@ const SystemSettings = { AgentCrwApiKey: !!process.env.AGENT_CRW_API_KEY || null, AgentCrwApiUrl: process.env.AGENT_CRW_API_URL || null, AgentYouApiKey: !!process.env.AGENT_YOU_API_KEY || null, + AgentKeenableApiKey: !!process.env.AGENT_KEENABLE_API_KEY || null, + AgentKeenableApiUrl: process.env.AGENT_KEENABLE_API_URL || null, // -------------------------------------------------------- // Compliance Settings diff --git a/server/utils/agents/aibitat/plugins/web-browsing.js b/server/utils/agents/aibitat/plugins/web-browsing.js index 703cd58eaf5..d66876ab6a7 100644 --- a/server/utils/agents/aibitat/plugins/web-browsing.js +++ b/server/utils/agents/aibitat/plugins/web-browsing.js @@ -110,6 +110,9 @@ const webBrowsing = { case "you-search": engine = "_youSearch"; break; + case "keenable-search": + engine = "_keenableSearch"; + break; default: engine = "_duckDuckGoEngine"; } @@ -1336,6 +1339,102 @@ const webBrowsing = { return result; }, + /** + * Use Keenable + * A web search API built for AI agents. Keyless by default (free + * tier); set AGENT_KEENABLE_API_KEY to lift rate limits. + * https://keenable.ai + */ + _keenableSearch: async function (query) { + // Unlike the other providers, a key is optional here: with none we + // use the keyless public endpoint, so this works out of the box. + const apiKey = (process.env.AGENT_KEENABLE_API_KEY || "").trim(); + + let baseUrl = "https://api.keenable.ai"; + if (process.env.AGENT_KEENABLE_API_URL) { + try { + const parsed = new URL(process.env.AGENT_KEENABLE_API_URL); + const isLoopback = ["localhost", "127.0.0.1", "::1"].includes( + parsed.hostname + ); + // HTTPS-only, except loopback for local development. + if (parsed.protocol === "https:" || isLoopback) + baseUrl = parsed.origin; + else throw new Error("KEENABLE base URL must be https://"); + } catch (e) { + this.super.handlerProps.log( + `invalid Keenable Search URL: ${e.message}` + ); + } + } + + this.super.introspect( + `${this.caller}: Using Keenable to search for "${ + query.length > 100 ? `${query.slice(0, 100)}...` : query + }"` + ); + + const headers = { + "Content-Type": "application/json", + "User-Agent": "keenable-anythingllm", + // Attribution header the Keenable backend segments traffic by. + "X-Keenable-Title": "AnythingLLM", + }; + // Keyless public endpoint by default; keyed endpoint + X-API-Key + // when a key is configured. + const path = apiKey ? "/v1/search" : "/v1/search/public"; + if (apiKey) headers["X-API-Key"] = apiKey; + + const { response, error } = await fetch(`${baseUrl}${path}`, { + method: "POST", + headers, + body: JSON.stringify({ query, mode: "pro" }), + }) + .then((res) => { + if (res.ok) return res.json(); + throw new Error(`${res.status} - ${res.statusText}`); + }) + .then((data) => { + return { response: data, error: null }; + }) + .catch((e) => { + this.super.handlerProps.log( + `Keenable Search Error: ${e.message}` + ); + return { response: null, error: e.message }; + }); + if (error) + return `There was an error searching for content. ${error}`; + + const data = []; + response.results?.forEach((searchResult) => { + const { title, url, description, snippet } = searchResult; + // Keenable returns both fields: `snippet` carries the page text and + // `description` is the page's meta description, which is empty for + // most pages. It returns whole pages rather than an excerpt, so the + // text is collapsed and capped to snippet length for the agent. + const text = String(snippet || description || "") + .replace(/\s+/g, " ") + .trim() + .slice(0, 500); + data.push({ + title, + link: url, + snippet: text, + }); + }); + + if (data.length === 0) + return `No information was found online for the search query.`; + + this.reportSearchResultsCitations(data); + 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; + }, + /** * You.com Search — keyless free tier by default, optional API key for higher limits. * Keyless: GET https://api.you.com/v1/agents/search diff --git a/server/utils/helpers/updateENV.js b/server/utils/helpers/updateENV.js index e1464cd4601..ee79ae61c7f 100644 --- a/server/utils/helpers/updateENV.js +++ b/server/utils/helpers/updateENV.js @@ -600,6 +600,14 @@ const KEY_MAPPING = { envKey: "AGENT_YOU_API_KEY", checks: [], }, + AgentKeenableApiKey: { + envKey: "AGENT_KEENABLE_API_KEY", + checks: [], + }, + AgentKeenableApiUrl: { + envKey: "AGENT_KEENABLE_API_URL", + checks: [], + }, // TTS/STT Integration ENVS TextToSpeechProvider: {