UNPKG

@gensx/storage

Version:

Cloud storage, blobs, sqlite, and vector database providers/hooks for GenSX.

353 lines (349 loc) 12.6 kB
/** * Check out the docs at https://www.gensx.com/docs * Find us on Github https://github.com/gensx-inc/gensx * Find us on Discord https://discord.gg/F5BSU8Kc */ import { readConfig } from '@gensx/core'; import { parseErrorResponse } from '../utils/parse-error.js'; import { USER_AGENT } from '../utils/user-agent.js'; /* eslint-disable @typescript-eslint/only-throw-error */ /** * Base URL for the GenSX Console API */ const API_BASE_URL = "https://api.gensx.com"; /** * Abstract base error class for search operations */ class SearchError extends Error { code; cause; constructor(code, message, cause) { super(message); this.code = code; this.cause = cause; this.name = "SearchError"; } } /** * Error class for API errors (bad requests, server errors, etc.) */ class SearchApiError extends SearchError { constructor(message, cause) { super("SEARCH_ERROR", message, cause); this.name = "SearchApiError"; } } /** * Error class for network errors */ class SearchNetworkError extends SearchError { constructor(message, cause) { super("NETWORK_ERROR", message, cause); this.name = "SearchNetworkError"; } } /** * Helper to convert API errors to more specific errors */ function handleApiError(err, operation) { if (err instanceof SearchError) { throw err; } if (err instanceof Error) { throw new SearchNetworkError(`Error during ${operation}: ${err.message}`, err); } throw new SearchNetworkError(`Error during ${operation}: ${String(err)}`); } /** * Remote implementation of vector namespace */ class SearchNamespace { namespaceId; apiBaseUrl; apiKey; org; project; environment; constructor(namespaceId, apiBaseUrl, apiKey, org, project, environment) { this.namespaceId = namespaceId; this.apiBaseUrl = apiBaseUrl; this.apiKey = apiKey; this.org = org; this.project = project; this.environment = environment; } async write({ upsertColumns, upsertRows, patchColumns, patchRows, deletes, deleteByFilter, distanceMetric, schema, }) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", "User-Agent": USER_AGENT, }, body: JSON.stringify({ upsertColumns, upsertRows, patchColumns, patchRows, deletes, deleteByFilter, distanceMetric, schema, }), }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to write: ${message}`); } const data = (await response.json()); return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "write"); } throw err; } } async query({ topK, includeAttributes, filters, rankBy, aggregateBy, consistency, }) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/query`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", "User-Agent": USER_AGENT, }, body: JSON.stringify({ rankBy, topK: topK, includeAttributes, filters, aggregateBy, consistency, }), }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to query: ${message}`); } const data = (await response.json()); return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "query"); } throw err; } } async getSchema() { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/schema`, { method: "GET", headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT, }, }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to get schema: ${message}`); } const data = (await response.json()); return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "schema"); } throw err; } } async updateSchema({ schema }) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(this.namespaceId)}/schema`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "Content-Type": "application/json", "User-Agent": USER_AGENT, }, body: JSON.stringify(schema), }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to update schema: ${message}`); } const data = (await response.json()); return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "updateSchema"); } throw err; } } } /** * Remote implementation of search */ class SearchStorage { apiKey; apiBaseUrl; org; project; environment; namespaces = new Map(); constructor(project, environment) { this.project = project; this.environment = environment; const config = readConfig(); this.apiKey = process.env.GENSX_API_KEY ?? config.api?.token ?? ""; if (!this.apiKey) { throw new Error("GENSX_API_KEY environment variable must be set for search"); } this.org = process.env.GENSX_ORG ?? config.api?.org ?? ""; if (!this.org) { throw new Error("Organization ID must be provided via constructor or GENSX_ORG environment variable"); } this.apiBaseUrl = process.env.GENSX_API_BASE_URL ?? config.api?.baseUrl ?? API_BASE_URL; } getNamespace(name) { if (!this.namespaces.has(name)) { this.namespaces.set(name, new SearchNamespace(name, this.apiBaseUrl, this.apiKey, this.org, this.project, this.environment)); } return this.namespaces.get(name); } async ensureNamespace(name) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}/ensure`, { method: "POST", headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT, }, }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to ensure namespace: ${message}`); } const data = (await response.json()); // Make sure the namespace is in our cache if (!this.namespaces.has(name)) { this.getNamespace(name); } return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "ensureNamespace"); } throw err; } } async deleteNamespace(name) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}`, { method: "DELETE", headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT, }, }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to delete namespace: ${message}`); } const data = (await response.json()); // Remove namespace from caches if it was successfully deleted if (data.deleted) { if (this.namespaces.has(name)) { const ns = this.namespaces.get(name); if (ns) { this.namespaces.delete(name); } } } return data; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "deleteNamespace"); } throw err; } } async listNamespaces(options) { try { // Normalize prefix by removing trailing slash const normalizedPrefix = options?.prefix?.replace(/\/$/, ""); const url = new URL(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search`); if (normalizedPrefix) { url.searchParams.append("prefix", normalizedPrefix); } if (options?.limit) { url.searchParams.append("limit", options.limit.toString()); } if (options?.cursor) { url.searchParams.append("cursor", options.cursor); } const response = await fetch(url.toString(), { method: "GET", headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT, }, }); if (!response.ok) { const message = await parseErrorResponse(response); throw new SearchApiError(`Failed to list namespaces: ${message}`); } const data = (await response.json()); return { namespaces: data.namespaces.map((ns) => ({ name: ns.name, createdAt: new Date(ns.createdAt), })), nextCursor: data.nextCursor, }; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "listNamespaces"); } throw err; } } hasEnsuredNamespace(name) { return this.namespaces.has(name); } /** * Check if a namespace exists * @param name The namespace name to check * @returns Promise that resolves to true if the namespace exists */ async namespaceExists(name) { try { const response = await fetch(`${this.apiBaseUrl}/org/${this.org}/projects/${this.project}/environments/${this.environment}/search/${encodeURIComponent(name)}`, { method: "HEAD", headers: { Authorization: `Bearer ${this.apiKey}`, "User-Agent": USER_AGENT, }, }); return response.ok; } catch (err) { if (!(err instanceof SearchError)) { throw handleApiError(err, "namespaceExists"); } throw err; } } } export { SearchApiError, SearchError, SearchNamespace, SearchNetworkError, SearchStorage }; //# sourceMappingURL=remote.js.map