UNPKG

@datastax/langflow-client

Version:
189 lines 7.43 kB
// Copyright DataStax, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { fetch, Headers } from "undici"; import pkg from "../package.json" with { type: "json" }; import { LangflowError, LangflowRequestError } from "./errors.js"; import { Flow } from "./flow.js"; import { Logs } from "./logs.js"; import { Files } from "./files.js"; import { DATASTAX_LANGFLOW_BASE_URL } from "./consts.js"; import { platform, arch } from "node:os"; import { NDJSONStream } from "./ndjson.js"; export class LangflowClient { baseUrl; basePath; langflowId; apiKey; fetch; defaultHeaders; logs; files; constructor(opts) { this.baseUrl = this.#resolveBaseUrl(opts); this.basePath = "/api"; this.langflowId = opts.langflowId; this.apiKey = opts.apiKey; this.fetch = opts.fetch ?? fetch; this.defaultHeaders = opts.defaultHeaders ?? new Headers(); if (!this.defaultHeaders.has("User-Agent")) { this.defaultHeaders.set("User-Agent", this.#getUserAgent()); } if (this.#isDataStax()) { const errors = []; if (!this.langflowId) { errors.push("langflowId is required"); } if (!this.apiKey) { errors.push("apiKey is required"); } if (errors.length > 0) { throw new TypeError(errors.join(", ")); } this.basePath = `/lf/${this.langflowId}/api`; } if (!this.#isDataStax() && this.langflowId) { throw new TypeError("langflowId is not supported"); } this.logs = new Logs(this); this.files = new Files(this); } #resolveBaseUrl(opts) { // If baseUrl is provided as an property on the opts, but is it undefined or // null, this is likely not the intention. // Previously the baseUrl would get set to the DATASTAX_LANGFLOW_BASE_URL // and the instantiation would fail due to a missing langflowId. Now it will // throw an more useful error, unless langflowId and apiKey are present. if (Object.hasOwn(opts, "baseUrl")) { const isBaseUrlEffectivelyEmpty = typeof opts.baseUrl === "undefined" || opts.baseUrl === null || (typeof opts.baseUrl === "string" && opts.baseUrl.trim() === ""); if (isBaseUrlEffectivelyEmpty) { // BaseUrl is explicitly provided but is empty/undefined/null. // If langflowId and apiKey are also provided, assume DataStax // connection and use the default DataStax URL. if (opts.langflowId && opts.apiKey) { return DATASTAX_LANGFLOW_BASE_URL; } // Otherwise, this is an invalid configuration. throw new TypeError(`You are trying to set baseUrl, but the value is '${String(opts.baseUrl)}'.`); } // If opts.baseUrl is explicitly set and not effectively empty, it will be used by the return statement below. } return opts.baseUrl ?? DATASTAX_LANGFLOW_BASE_URL; } #isDataStax() { return this.baseUrl === DATASTAX_LANGFLOW_BASE_URL; } #getUserAgent() { return `@datastax/langflow-client/${pkg.version} (${platform()} ${arch()}) node/${process.version}`; } #setApiKey(apiKey, headers) { if (this.#isDataStax()) { headers.set("Authorization", `Bearer ${apiKey}`); } else { headers.set("x-api-key", apiKey); } } #setHeaders(headers) { const newHeaders = headers instanceof Headers ? headers : new Headers(headers); for (const [header, value] of this.defaultHeaders.entries()) { if (!newHeaders.has(header)) { newHeaders.set(header, value); } } if (this.apiKey) { this.#setApiKey(this.apiKey, newHeaders); } } #setUrl(path) { if (["/logs", "/logs-stream"].includes(path)) { return new URL(`${this.baseUrl}${path}`); } return new URL(`${this.baseUrl}${this.basePath}${path}`); } flow(flowId, tweaks) { return new Flow(this, flowId, tweaks); } async request(options) { const { path, method, query, body, headers, signal } = options; const url = this.#setUrl(path); if (query) { for (const [key, value] of Object.entries(query)) { url.searchParams.append(key, value); } } this.#setHeaders(headers); try { signal?.throwIfAborted(); const response = await this.fetch(url, { method, body, headers, signal }); if (!response.ok) { throw new LangflowError(`${response.status} - ${response.statusText}`, response); } signal?.throwIfAborted(); return await response.json(); } catch (error) { // If it is a LangflowError or the result of an aborted signal, rethrow it if (error instanceof LangflowError || (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError"))) { throw error; } if (error instanceof Error) { throw new LangflowRequestError(error.message, error); } throw error; } } async stream(options) { const { path, method, body, headers, signal } = options; const url = this.#setUrl(path); url.searchParams.set("stream", "true"); this.#setHeaders(headers); try { signal?.throwIfAborted(); const response = await this.fetch(url, { method, body, headers, signal, }); if (!response.ok) { throw new LangflowError(`${response.status} - ${response.statusText}`, response); } if (response.body) { const ndjsonStream = NDJSONStream(); return response.body .pipeThrough(new TextDecoderStream(), { signal }) .pipeThrough(ndjsonStream, { signal }); } else { throw new LangflowError("No body in the response", response); } } catch (error) { if (error instanceof LangflowError || (error instanceof DOMException && (error.name === "AbortError" || error.name === "TimeoutError"))) { throw error; } if (error instanceof Error) { throw new LangflowRequestError(error.message, error); } throw error; } } } //# sourceMappingURL=index.js.map