Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docker/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 ############
###########################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,55 @@ export function DuckDuckGoOptions() {
);
}

export function KeenableSearchOptions({ settings }) {
return (
<>
<p className="text-sm text-white/60 my-2">
Keenable works without an API key by default (keyless free tier). To
lift rate limits, add an API key{" "}
<a
href="https://keenable.ai/console"
target="_blank"
rel="noreferrer"
className="text-blue-300 underline"
>
from Keenable.
</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 (optional)
</label>
<input
type="password"
name="env::AgentKeenableApiKey"
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="Keenable API Key"
defaultValue={settings?.AgentKeenableApiKey ? "*".repeat(20) : ""}
autoComplete="off"
spellCheck={false}
/>
</div>
<div className="flex flex-col w-60">
<label className="text-white text-sm font-semibold block mb-3">
Base URL (optional)
</label>
<input
type="url"
name="env::AgentKeenableApiUrl"
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="https://api.keenable.ai"
defaultValue={settings?.AgentKeenableApiUrl}
autoComplete="off"
spellCheck={false}
/>
</div>
</div>
</>
);
}

export function ExaSearchOptions({ settings }) {
return (
<>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions frontend/src/pages/Admin/Agents/WebSearchSelection/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -38,6 +39,7 @@ import {
BraveSearchOptions,
CrwSearchOptions,
YouSearchOptions,
KeenableSearchOptions,
} from "./SearchProviderOptions";

const SEARCH_PROVIDERS = [
Expand All @@ -48,6 +50,14 @@ const SEARCH_PROVIDERS = [
options: () => <DuckDuckGoOptions />,
description: "Free and privacy-focused web search using DuckDuckGo.",
},
{
name: "Keenable",
value: "keenable-search",
logo: KeenableSearchIcon,
options: (settings) => <KeenableSearchOptions settings={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",
Expand Down
4 changes: 4 additions & 0 deletions server/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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 ############
###########################################
Expand Down
3 changes: 3 additions & 0 deletions server/models/systemSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ const SystemSettings = {
"brave-search",
"crw-search",
"you-search",
"keenable-search",
].includes(update)
)
throw new Error("Invalid SERP provider.");
Expand Down Expand Up @@ -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
Expand Down
99 changes: 99 additions & 0 deletions server/utils/agents/aibitat/plugins/web-browsing.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ const webBrowsing = {
case "you-search":
engine = "_youSearch";
break;
case "keenable-search":
engine = "_keenableSearch";
break;
default:
engine = "_duckDuckGoEngine";
}
Expand Down Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions server/utils/helpers/updateENV.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down