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: {