brave-search
Version:
A fully typed Brave Search API wrapper, providing easy access to web search, local POI search, and automatic polling for web search summary feature.
269 lines (268 loc) • 11.1 kB
JavaScript
"use strict";
// Copyright (C) 2024 Erik Balfe
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.BraveSearchError = exports.BraveSearch = void 0;
const axios_1 = __importDefault(require("axios"));
const DEFAULT_POLLING_INTERVAL = 500;
const DEFAULT_MAX_POLL_ATTEMPTS = 20;
/**
* An error class specific to BraveSearch API interactions.
* It includes additional information about the response data that caused the error.
*/
class BraveSearchError extends Error {
/**
* Initializes a new instance of the BraveSearchError class.
* @param message The error message.
* @param responseData The response data that caused the error.
*/
constructor(message, responseData) {
super(message);
this.name = "BraveSearchError";
this.responseData = responseData;
}
}
exports.BraveSearchError = BraveSearchError;
/**
* The main class for interacting with the Brave Search API, holding API key for all the requests made with it.
* It provides methods for web search, image search, local POI search, and summarization.
*/
class BraveSearch {
/**
* Initializes a new instance of the BraveSearch class.
* @param apiKey The API key for accessing the Brave Search API.
* @param options
*/
constructor(apiKey, options) {
var _a, _b;
this.baseUrl = "https://api.search.brave.com/res/v1";
this.apiKey = apiKey;
this.pollInterval = (_a = options === null || options === void 0 ? void 0 : options.pollInterval) !== null && _a !== void 0 ? _a : DEFAULT_POLLING_INTERVAL;
this.maxPollAttempts = (_b = options === null || options === void 0 ? void 0 : options.maxPollAttempts) !== null && _b !== void 0 ? _b : DEFAULT_MAX_POLL_ATTEMPTS;
}
/**
* Performs a web search using the provided query and options.
* @param query The search query string.
* @param options Optional settings to configure the search behavior.
* @returns A promise that resolves to the search results.
*/
async webSearch(query, options = {}, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/web/search`, {
params: {
q: query,
...this.formatOptions(options),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
const handledError = this.handleApiError(error);
throw handledError;
}
}
/**
* Performs an image search using the provided query and options.
* @param query The search query string.
* @param options Optional settings to configure the search behavior.
* @returns A promise that resolves to the image search results.
*/
async imageSearch(query, options = {}, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/images/search`, {
params: {
q: query,
...this.formatOptions(options),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
const handledError = this.handleApiError(error);
throw handledError;
}
}
async newsSearch(query, options = {}, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/news/search?`, {
params: {
q: query,
...this.formatOptions(options),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
const handledError = this.handleApiError(error);
throw handledError;
}
}
/**
* Executes a web search for the provided query and polls for a summary
* if the query is eligible for a summary and summarizer key is provided in the web search response.
* The summary is usually ready within 2 seconds after the original web search response is received.
* @param query The search query string.
* @param options Optional settings to configure the search behavior.
* @param summarizerOptions Optional settings specific to summarization.
* @returns An object containing promises for the web search results and the summarized answer.
*/
getSummarizedAnswer(query, options = {}, summarizerOptions = {}, signal) {
try {
const webSearchResponse = this.webSearch(query, options, signal);
const summaryPromise = webSearchResponse.then(async (webSearchResponse) => {
var _a;
const summarizerKey = (_a = webSearchResponse.summarizer) === null || _a === void 0 ? void 0 : _a.key;
if (summarizerKey) {
return await this.pollForSummary(summarizerKey, summarizerOptions, signal);
}
return undefined;
});
return { webSearch: webSearchResponse, summary: summaryPromise };
}
catch (error) {
throw this.handleApiError(error);
}
}
/**
* Searches for local points of interest using the provided IDs and options.
* @param ids The IDs of the local points of interest.
* @returns A promise that resolves to the search results.
*/
async localPoiSearch(ids, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/local/pois`, {
params: {
ids: ids.join(","),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
throw this.handleApiError(error);
}
}
/**
* Retrieves descriptions for local points of interest using the provided IDs and options.
* @param ids The IDs of the local points of interest.
* @returns A promise that resolves to the search results.
*/
async localDescriptionsSearch(ids, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/local/descriptions`, {
params: {
ids: ids.join(","),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
throw this.handleApiError(error);
}
}
/**
* Polls for a summary response after a web search request. This method is suggested by the Brave Search API documentation
* as the way to retrieve a summary after initiating a web search.
*
* @param key The key identifying the summary request.
* @param options Optional settings specific to summarization.
* @param signal Optional AbortSignal to cancel the request.
* @returns A promise that resolves to the summary response if available, or undefined if the summary is not ready.
* @throws {BraveSearchError} If the summary generation fails or if the summary is not available after maximum polling attempts.
*
* **Polling Behavior:**
* - The method will make up to 20 attempts to fetch the summary by default.
* - Each attempt is spaced 500ms apart.
* - If the summary is not ready after 20 attempts, a BraveSearchError is thrown.
*
* **Configuration:**
* - The number of attempts and the interval between attempts can be configured through the class constructor options.
*/
async pollForSummary(key, options, signal) {
for (let attempt = 0; attempt < this.maxPollAttempts; attempt++) {
const summaryResponse = await this.summarizerSearch(key, options, signal);
if (summaryResponse.status === "complete" && summaryResponse.summary) {
return summaryResponse;
}
else if (summaryResponse.status === "failed") {
throw new BraveSearchError("Summary generation failed");
}
await new Promise((resolve) => setTimeout(resolve, this.pollInterval));
}
throw new BraveSearchError("Summary not available after maximum polling attempts");
}
async summarizerSearch(key, options, signal) {
try {
const response = await axios_1.default.get(`${this.baseUrl}/summarizer/search`, {
params: {
key,
...this.formatOptions(options),
},
headers: this.getHeaders(),
signal,
});
return response.data;
}
catch (error) {
throw this.handleApiError(error);
}
}
getHeaders() {
return {
Accept: "application/json",
"Accept-Encoding": "gzip",
"X-Subscription-Token": this.apiKey,
};
}
formatOptions(options) {
return Object.entries(options).reduce((acc, [key, value]) => {
if (value !== undefined) {
acc[key] = value.toString();
}
return acc;
}, {});
}
handleApiError(error) {
var _a, _b, _c, _d;
if (axios_1.default.isAxiosError(error)) {
const status = (_a = error.response) === null || _a === void 0 ? void 0 : _a.status;
const message = ((_c = (_b = error.response) === null || _b === void 0 ? void 0 : _b.data) === null || _c === void 0 ? void 0 : _c.message) || error.message;
const responseData = (_d = error.response) === null || _d === void 0 ? void 0 : _d.data;
if (status === 429) {
return new BraveSearchError(`Rate limit exceeded: ${message}`, responseData);
}
else if (status === 401) {
return new BraveSearchError(`Authentication error: ${message}`, responseData);
}
else {
return new BraveSearchError(`API error (${status}): ${message}`, responseData);
}
}
return new BraveSearchError(`Unexpected error: ${error.message}`);
}
}
exports.BraveSearch = BraveSearch;