UNPKG

langsmith

Version:

Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.

1,245 lines (1,244 loc) 152 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Client = exports.DEFAULT_BATCH_SIZE_LIMIT_BYTES = exports.AutoBatchQueue = void 0; exports.mergeRuntimeEnvIntoRun = mergeRuntimeEnvIntoRun; const uuid = __importStar(require("uuid")); const translator_js_1 = require("./experimental/otel/translator.cjs"); const otel_js_1 = require("./singletons/otel.cjs"); const async_caller_js_1 = require("./utils/async_caller.cjs"); const messages_js_1 = require("./utils/messages.cjs"); const env_js_1 = require("./utils/env.cjs"); const index_js_1 = require("./index.cjs"); const _uuid_js_1 = require("./utils/_uuid.cjs"); const warn_js_1 = require("./utils/warn.cjs"); const prompts_js_1 = require("./utils/prompts.cjs"); const error_js_1 = require("./utils/error.cjs"); const fetch_js_1 = require("./singletons/fetch.cjs"); const index_js_2 = require("./utils/fast-safe-stringify/index.cjs"); function mergeRuntimeEnvIntoRun(run) { const runtimeEnv = (0, env_js_1.getRuntimeEnvironment)(); const envVars = (0, env_js_1.getLangChainEnvVarsMetadata)(); const extra = run.extra ?? {}; const metadata = extra.metadata; run.extra = { ...extra, runtime: { ...runtimeEnv, ...extra?.runtime, }, metadata: { ...envVars, ...(envVars.revision_id || ("revision_id" in run && run.revision_id) ? { revision_id: ("revision_id" in run ? run.revision_id : undefined) ?? envVars.revision_id, } : {}), ...metadata, }, }; return run; } const getTracingSamplingRate = (configRate) => { const samplingRateStr = configRate?.toString() ?? (0, env_js_1.getLangSmithEnvironmentVariable)("TRACING_SAMPLING_RATE"); if (samplingRateStr === undefined) { return undefined; } const samplingRate = parseFloat(samplingRateStr); if (samplingRate < 0 || samplingRate > 1) { throw new Error(`LANGSMITH_TRACING_SAMPLING_RATE must be between 0 and 1 if set. Got: ${samplingRate}`); } return samplingRate; }; // utility functions const isLocalhost = (url) => { const strippedUrl = url.replace("http://", "").replace("https://", ""); const hostname = strippedUrl.split("/")[0].split(":")[0]; return (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1"); }; async function toArray(iterable) { const result = []; for await (const item of iterable) { result.push(item); } return result; } function trimQuotes(str) { if (str === undefined) { return undefined; } return str .trim() .replace(/^"(.*)"$/, "$1") .replace(/^'(.*)'$/, "$1"); } const handle429 = async (response) => { if (response?.status === 429) { const retryAfter = parseInt(response.headers.get("retry-after") ?? "30", 10) * 1000; if (retryAfter > 0) { await new Promise((resolve) => setTimeout(resolve, retryAfter)); // Return directly after calling this check return true; } } // Fall back to existing status checks return false; }; function _formatFeedbackScore(score) { if (typeof score === "number") { // Truncate at 4 decimal places return Number(score.toFixed(4)); } return score; } class AutoBatchQueue { constructor() { Object.defineProperty(this, "items", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(this, "sizeBytes", { enumerable: true, configurable: true, writable: true, value: 0 }); } peek() { return this.items[0]; } push(item) { let itemPromiseResolve; const itemPromise = new Promise((resolve) => { // Setting itemPromiseResolve is synchronous with promise creation: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise itemPromiseResolve = resolve; }); const size = (0, index_js_2.serialize)(item.item, `Serializing run with id: ${item.item.id}`).length; this.items.push({ action: item.action, payload: item.item, otelContext: item.otelContext, apiKey: item.apiKey, apiUrl: item.apiUrl, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion itemPromiseResolve: itemPromiseResolve, itemPromise, size, }); this.sizeBytes += size; return itemPromise; } pop(upToSizeBytes) { if (upToSizeBytes < 1) { throw new Error("Number of bytes to pop off may not be less than 1."); } const popped = []; let poppedSizeBytes = 0; // Pop items until we reach or exceed the size limit while (poppedSizeBytes + (this.peek()?.size ?? 0) < upToSizeBytes && this.items.length > 0) { const item = this.items.shift(); if (item) { popped.push(item); poppedSizeBytes += item.size; this.sizeBytes -= item.size; } } // If there is an item on the queue we were unable to pop, // just return it as a single batch. if (popped.length === 0 && this.items.length > 0) { const item = this.items.shift(); popped.push(item); poppedSizeBytes += item.size; this.sizeBytes -= item.size; } return [ popped.map((it) => ({ action: it.action, item: it.payload, otelContext: it.otelContext, apiKey: it.apiKey, apiUrl: it.apiUrl, })), () => popped.forEach((it) => it.itemPromiseResolve()), ]; } } exports.AutoBatchQueue = AutoBatchQueue; // 20 MB exports.DEFAULT_BATCH_SIZE_LIMIT_BYTES = 20_971_520; const SERVER_INFO_REQUEST_TIMEOUT = 2500; const DEFAULT_API_URL = "https://api.smith.langchain.com"; class Client { constructor(config = {}) { Object.defineProperty(this, "apiKey", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "apiUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "webUrl", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "caller", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "batchIngestCaller", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "timeout_ms", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_tenantId", { enumerable: true, configurable: true, writable: true, value: null }); Object.defineProperty(this, "hideInputs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "hideOutputs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "tracingSampleRate", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "filteredPostUuids", { enumerable: true, configurable: true, writable: true, value: new Set() }); Object.defineProperty(this, "autoBatchTracing", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "autoBatchQueue", { enumerable: true, configurable: true, writable: true, value: new AutoBatchQueue() }); Object.defineProperty(this, "autoBatchTimeout", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "autoBatchAggregationDelayMs", { enumerable: true, configurable: true, writable: true, value: 250 }); Object.defineProperty(this, "batchSizeBytesLimit", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "fetchOptions", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "settings", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "blockOnRootRunFinalization", { enumerable: true, configurable: true, writable: true, value: (0, env_js_1.getEnvironmentVariable)("LANGSMITH_TRACING_BACKGROUND") === "false" }); Object.defineProperty(this, "traceBatchConcurrency", { enumerable: true, configurable: true, writable: true, value: 5 }); Object.defineProperty(this, "_serverInfo", { enumerable: true, configurable: true, writable: true, value: void 0 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "_getServerInfoPromise", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "manualFlushMode", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "langSmithToOTELTranslator", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "multipartStreamingDisabled", { enumerable: true, configurable: true, writable: true, value: false }); Object.defineProperty(this, "debug", { enumerable: true, configurable: true, writable: true, value: (0, env_js_1.getEnvironmentVariable)("LANGSMITH_DEBUG") === "true" }); const defaultConfig = Client.getDefaultClientConfig(); this.tracingSampleRate = getTracingSamplingRate(config.tracingSamplingRate); this.apiUrl = trimQuotes(config.apiUrl ?? defaultConfig.apiUrl) ?? ""; if (this.apiUrl.endsWith("/")) { this.apiUrl = this.apiUrl.slice(0, -1); } this.apiKey = trimQuotes(config.apiKey ?? defaultConfig.apiKey); this.webUrl = trimQuotes(config.webUrl ?? defaultConfig.webUrl); if (this.webUrl?.endsWith("/")) { this.webUrl = this.webUrl.slice(0, -1); } this.timeout_ms = config.timeout_ms ?? 90_000; this.caller = new async_caller_js_1.AsyncCaller({ ...(config.callerOptions ?? {}), debug: config.debug ?? this.debug, }); this.traceBatchConcurrency = config.traceBatchConcurrency ?? this.traceBatchConcurrency; if (this.traceBatchConcurrency < 1) { throw new Error("Trace batch concurrency must be positive."); } this.debug = config.debug ?? this.debug; this.batchIngestCaller = new async_caller_js_1.AsyncCaller({ maxRetries: 2, maxConcurrency: this.traceBatchConcurrency, ...(config.callerOptions ?? {}), onFailedResponseHook: handle429, debug: config.debug ?? this.debug, }); this.hideInputs = config.hideInputs ?? config.anonymizer ?? defaultConfig.hideInputs; this.hideOutputs = config.hideOutputs ?? config.anonymizer ?? defaultConfig.hideOutputs; this.autoBatchTracing = config.autoBatchTracing ?? this.autoBatchTracing; this.blockOnRootRunFinalization = config.blockOnRootRunFinalization ?? this.blockOnRootRunFinalization; this.batchSizeBytesLimit = config.batchSizeBytesLimit; this.fetchOptions = config.fetchOptions || {}; this.manualFlushMode = config.manualFlushMode ?? this.manualFlushMode; if ((0, env_js_1.getOtelEnabled)()) { this.langSmithToOTELTranslator = new translator_js_1.LangSmithToOTELTranslator(); } } static getDefaultClientConfig() { const apiKey = (0, env_js_1.getLangSmithEnvironmentVariable)("API_KEY"); const apiUrl = (0, env_js_1.getLangSmithEnvironmentVariable)("ENDPOINT") ?? DEFAULT_API_URL; const hideInputs = (0, env_js_1.getLangSmithEnvironmentVariable)("HIDE_INPUTS") === "true"; const hideOutputs = (0, env_js_1.getLangSmithEnvironmentVariable)("HIDE_OUTPUTS") === "true"; return { apiUrl: apiUrl, apiKey: apiKey, webUrl: undefined, hideInputs: hideInputs, hideOutputs: hideOutputs, }; } getHostUrl() { if (this.webUrl) { return this.webUrl; } else if (isLocalhost(this.apiUrl)) { this.webUrl = "http://localhost:3000"; return this.webUrl; } else if (this.apiUrl.endsWith("/api/v1")) { this.webUrl = this.apiUrl.replace("/api/v1", ""); return this.webUrl; } else if (this.apiUrl.includes("/api") && !this.apiUrl.split(".", 1)[0].endsWith("api")) { this.webUrl = this.apiUrl.replace("/api", ""); return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("dev")) { this.webUrl = "https://dev.smith.langchain.com"; return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("eu")) { this.webUrl = "https://eu.smith.langchain.com"; return this.webUrl; } else if (this.apiUrl.split(".", 1)[0].includes("beta")) { this.webUrl = "https://beta.smith.langchain.com"; return this.webUrl; } else { this.webUrl = "https://smith.langchain.com"; return this.webUrl; } } get headers() { const headers = { "User-Agent": `langsmith-js/${index_js_1.__version__}`, }; if (this.apiKey) { headers["x-api-key"] = `${this.apiKey}`; } return headers; } _getPlatformEndpointPath(path) { // Check if apiUrl already ends with /v1 or /v1/ to avoid double /v1/v1/ paths const needsV1Prefix = this.apiUrl.slice(-3) !== "/v1" && this.apiUrl.slice(-4) !== "/v1/"; return needsV1Prefix ? `/v1/platform/${path}` : `/platform/${path}`; } async processInputs(inputs) { if (this.hideInputs === false) { return inputs; } if (this.hideInputs === true) { return {}; } if (typeof this.hideInputs === "function") { return this.hideInputs(inputs); } return inputs; } async processOutputs(outputs) { if (this.hideOutputs === false) { return outputs; } if (this.hideOutputs === true) { return {}; } if (typeof this.hideOutputs === "function") { return this.hideOutputs(outputs); } return outputs; } async prepareRunCreateOrUpdateInputs(run) { const runParams = { ...run }; if (runParams.inputs !== undefined) { runParams.inputs = await this.processInputs(runParams.inputs); } if (runParams.outputs !== undefined) { runParams.outputs = await this.processOutputs(runParams.outputs); } return runParams; } async _getResponse(path, queryParams) { const paramsString = queryParams?.toString() ?? ""; const url = `${this.apiUrl}${path}?${paramsString}`; const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), url, { method: "GET", headers: this.headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, `Failed to fetch ${path}`); return response; } async _get(path, queryParams) { const response = await this._getResponse(path, queryParams); return response.json(); } async *_getPaginated(path, queryParams = new URLSearchParams(), transform) { let offset = Number(queryParams.get("offset")) || 0; const limit = Number(queryParams.get("limit")) || 100; while (true) { queryParams.set("offset", String(offset)); queryParams.set("limit", String(limit)); const url = `${this.apiUrl}${path}?${queryParams}`; const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), url, { method: "GET", headers: this.headers, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, `Failed to fetch ${path}`); const items = transform ? transform(await response.json()) : await response.json(); if (items.length === 0) { break; } yield items; if (items.length < limit) { break; } offset += items.length; } } async *_getCursorPaginatedList(path, body = null, requestMethod = "POST", dataKey = "runs") { const bodyParams = body ? { ...body } : {}; while (true) { const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${this.apiUrl}${path}`, { method: requestMethod, headers: { ...this.headers, "Content-Type": "application/json" }, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, body: JSON.stringify(bodyParams), }); const responseBody = await response.json(); if (!responseBody) { break; } if (!responseBody[dataKey]) { break; } yield responseBody[dataKey]; const cursors = responseBody.cursors; if (!cursors) { break; } if (!cursors.next) { break; } bodyParams.cursor = cursors.next; } } // Allows mocking for tests _shouldSample() { if (this.tracingSampleRate === undefined) { return true; } return Math.random() < this.tracingSampleRate; } _filterForSampling(runs, patch = false) { if (this.tracingSampleRate === undefined) { return runs; } if (patch) { const sampled = []; for (const run of runs) { if (!this.filteredPostUuids.has(run.trace_id)) { sampled.push(run); } else if (run.id === run.trace_id) { this.filteredPostUuids.delete(run.trace_id); } } return sampled; } else { // For new runs, sample at trace level to maintain consistency const sampled = []; for (const run of runs) { const traceId = run.trace_id ?? run.id; // If we've already made a decision about this trace, follow it if (this.filteredPostUuids.has(traceId)) { continue; } // For new traces, apply sampling if (run.id === traceId) { if (this._shouldSample()) { sampled.push(run); } else { this.filteredPostUuids.add(traceId); } } else { // Child runs follow their trace's sampling decision sampled.push(run); } } return sampled; } } async _getBatchSizeLimitBytes() { const serverInfo = await this._ensureServerInfo(); return (this.batchSizeBytesLimit ?? serverInfo.batch_ingest_config?.size_limit_bytes ?? exports.DEFAULT_BATCH_SIZE_LIMIT_BYTES); } async _getMultiPartSupport() { const serverInfo = await this._ensureServerInfo(); return (serverInfo.instance_flags?.dataset_examples_multipart_enabled ?? false); } drainAutoBatchQueue(batchSizeLimit) { const promises = []; while (this.autoBatchQueue.items.length > 0) { const [batch, done] = this.autoBatchQueue.pop(batchSizeLimit); if (!batch.length) { done(); break; } const batchesByDestination = batch.reduce((acc, item) => { const apiUrl = item.apiUrl ?? this.apiUrl; const apiKey = item.apiKey ?? this.apiKey; const isDefault = item.apiKey === this.apiKey && item.apiUrl === this.apiUrl; const batchKey = isDefault ? "default" : `${apiUrl}|${apiKey}`; if (!acc[batchKey]) { acc[batchKey] = []; } acc[batchKey].push(item); return acc; }, {}); const batchPromises = []; for (const [batchKey, batch] of Object.entries(batchesByDestination)) { const batchPromise = this._processBatch(batch, { apiUrl: batchKey === "default" ? undefined : batchKey.split("|")[0], apiKey: batchKey === "default" ? undefined : batchKey.split("|")[1], }); batchPromises.push(batchPromise); } // Wait for all batches to complete, then call the overall done callback const allBatchesPromise = Promise.all(batchPromises).finally(done); promises.push(allBatchesPromise); } return Promise.all(promises); } async _processBatch(batch, options) { if (!batch.length) { return; } try { if (this.langSmithToOTELTranslator !== undefined) { this._sendBatchToOTELTranslator(batch); } else { const ingestParams = { runCreates: batch .filter((item) => item.action === "create") .map((item) => item.item), runUpdates: batch .filter((item) => item.action === "update") .map((item) => item.item), }; const serverInfo = await this._ensureServerInfo(); if (serverInfo?.batch_ingest_config?.use_multipart_endpoint) { await this.multipartIngestRuns(ingestParams, options); } else { await this.batchIngestRuns(ingestParams, options); } } } catch (e) { console.error("Error exporting batch:", e); } } _sendBatchToOTELTranslator(batch) { if (this.langSmithToOTELTranslator !== undefined) { const otelContextMap = new Map(); const operations = []; for (const item of batch) { if (item.item.id && item.otelContext) { otelContextMap.set(item.item.id, item.otelContext); if (item.action === "create") { operations.push({ operation: "post", id: item.item.id, trace_id: item.item.trace_id ?? item.item.id, run: item.item, }); } else { operations.push({ operation: "patch", id: item.item.id, trace_id: item.item.trace_id ?? item.item.id, run: item.item, }); } } } this.langSmithToOTELTranslator.exportBatch(operations, otelContextMap); } } async processRunOperation(item) { clearTimeout(this.autoBatchTimeout); this.autoBatchTimeout = undefined; item.item = mergeRuntimeEnvIntoRun(item.item); const itemPromise = this.autoBatchQueue.push(item); if (this.manualFlushMode) { // Rely on manual flushing in serverless environments return itemPromise; } const sizeLimitBytes = await this._getBatchSizeLimitBytes(); if (this.autoBatchQueue.sizeBytes > sizeLimitBytes) { void this.drainAutoBatchQueue(sizeLimitBytes); } if (this.autoBatchQueue.items.length > 0) { this.autoBatchTimeout = setTimeout(() => { this.autoBatchTimeout = undefined; void this.drainAutoBatchQueue(sizeLimitBytes); }, this.autoBatchAggregationDelayMs); } return itemPromise; } async _getServerInfo() { const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${this.apiUrl}/info`, { method: "GET", headers: { Accept: "application/json" }, signal: AbortSignal.timeout(SERVER_INFO_REQUEST_TIMEOUT), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, "get server info"); const json = await response.json(); if (this.debug) { console.log("\n=== LangSmith Server Configuration ===\n" + JSON.stringify(json, null, 2) + "\n"); } return json; } async _ensureServerInfo() { if (this._getServerInfoPromise === undefined) { this._getServerInfoPromise = (async () => { if (this._serverInfo === undefined) { try { this._serverInfo = await this._getServerInfo(); } catch (e) { console.warn(`[WARNING]: LangSmith failed to fetch info on supported operations with status code ${e.status}. Falling back to batch operations and default limits.`); } } return this._serverInfo ?? {}; })(); } return this._getServerInfoPromise.then((serverInfo) => { if (this._serverInfo === undefined) { this._getServerInfoPromise = undefined; } return serverInfo; }); } async _getSettings() { if (!this.settings) { this.settings = this._get("/settings"); } return await this.settings; } /** * Flushes current queued traces. */ async flush() { const sizeLimitBytes = await this._getBatchSizeLimitBytes(); await this.drainAutoBatchQueue(sizeLimitBytes); } _cloneCurrentOTELContext() { const otel_trace = (0, otel_js_1.getOTELTrace)(); const otel_context = (0, otel_js_1.getOTELContext)(); if (this.langSmithToOTELTranslator !== undefined) { const currentSpan = otel_trace.getActiveSpan(); if (currentSpan) { return otel_trace.setSpan(otel_context.active(), currentSpan); } } return undefined; } async createRun(run, options) { if (!this._filterForSampling([run]).length) { return; } const headers = { ...this.headers, "Content-Type": "application/json", }; const session_name = run.project_name; delete run.project_name; const runCreate = await this.prepareRunCreateOrUpdateInputs({ session_name, ...run, start_time: run.start_time ?? Date.now(), }); if (this.autoBatchTracing && runCreate.trace_id !== undefined && runCreate.dotted_order !== undefined) { const otelContext = this._cloneCurrentOTELContext(); void this.processRunOperation({ action: "create", item: runCreate, otelContext, apiKey: options?.apiKey, apiUrl: options?.apiUrl, }).catch(console.error); return; } const mergedRunCreateParam = mergeRuntimeEnvIntoRun(runCreate); if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${options?.apiUrl ?? this.apiUrl}/runs`, { method: "POST", headers, body: (0, index_js_2.serialize)(mergedRunCreateParam, `Creating run with id: ${mergedRunCreateParam.id}`), signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, "create run", true); } /** * Batch ingest/upsert multiple runs in the Langsmith system. * @param runs */ async batchIngestRuns({ runCreates, runUpdates, }, options) { if (runCreates === undefined && runUpdates === undefined) { return; } let preparedCreateParams = await Promise.all(runCreates?.map((create) => this.prepareRunCreateOrUpdateInputs(create)) ?? []); let preparedUpdateParams = await Promise.all(runUpdates?.map((update) => this.prepareRunCreateOrUpdateInputs(update)) ?? []); if (preparedCreateParams.length > 0 && preparedUpdateParams.length > 0) { const createById = preparedCreateParams.reduce((params, run) => { if (!run.id) { return params; } params[run.id] = run; return params; }, {}); const standaloneUpdates = []; for (const updateParam of preparedUpdateParams) { if (updateParam.id !== undefined && createById[updateParam.id]) { createById[updateParam.id] = { ...createById[updateParam.id], ...updateParam, }; } else { standaloneUpdates.push(updateParam); } } preparedCreateParams = Object.values(createById); preparedUpdateParams = standaloneUpdates; } const rawBatch = { post: preparedCreateParams, patch: preparedUpdateParams, }; if (!rawBatch.post.length && !rawBatch.patch.length) { return; } const batchChunks = { post: [], patch: [], }; for (const k of ["post", "patch"]) { const key = k; const batchItems = rawBatch[key].reverse(); let batchItem = batchItems.pop(); while (batchItem !== undefined) { // Type is wrong but this is a deprecated code path anyway batchChunks[key].push(batchItem); batchItem = batchItems.pop(); } } if (batchChunks.post.length > 0 || batchChunks.patch.length > 0) { const runIds = batchChunks.post .map((item) => item.id) .concat(batchChunks.patch.map((item) => item.id)) .join(","); await this._postBatchIngestRuns((0, index_js_2.serialize)(batchChunks, `Ingesting runs with ids: ${runIds}`), options); } } async _postBatchIngestRuns(body, options) { const headers = { ...this.headers, "Content-Type": "application/json", Accept: "application/json", }; if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } const response = await this.batchIngestCaller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${options?.apiUrl ?? this.apiUrl}/runs/batch`, { method: "POST", headers, body: body, signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, "batch create run", true); } /** * Batch ingest/upsert multiple runs in the Langsmith system. * @param runs */ async multipartIngestRuns({ runCreates, runUpdates, }, options) { if (runCreates === undefined && runUpdates === undefined) { return; } // transform and convert to dicts const allAttachments = {}; let preparedCreateParams = []; for (const create of runCreates ?? []) { const preparedCreate = await this.prepareRunCreateOrUpdateInputs(create); if (preparedCreate.id !== undefined && preparedCreate.attachments !== undefined) { allAttachments[preparedCreate.id] = preparedCreate.attachments; } delete preparedCreate.attachments; preparedCreateParams.push(preparedCreate); } let preparedUpdateParams = []; for (const update of runUpdates ?? []) { preparedUpdateParams.push(await this.prepareRunCreateOrUpdateInputs(update)); } // require trace_id and dotted_order const invalidRunCreate = preparedCreateParams.find((runCreate) => { return (runCreate.trace_id === undefined || runCreate.dotted_order === undefined); }); if (invalidRunCreate !== undefined) { throw new Error(`Multipart ingest requires "trace_id" and "dotted_order" to be set when creating a run`); } const invalidRunUpdate = preparedUpdateParams.find((runUpdate) => { return (runUpdate.trace_id === undefined || runUpdate.dotted_order === undefined); }); if (invalidRunUpdate !== undefined) { throw new Error(`Multipart ingest requires "trace_id" and "dotted_order" to be set when updating a run`); } // combine post and patch dicts where possible if (preparedCreateParams.length > 0 && preparedUpdateParams.length > 0) { const createById = preparedCreateParams.reduce((params, run) => { if (!run.id) { return params; } params[run.id] = run; return params; }, {}); const standaloneUpdates = []; for (const updateParam of preparedUpdateParams) { if (updateParam.id !== undefined && createById[updateParam.id]) { createById[updateParam.id] = { ...createById[updateParam.id], ...updateParam, }; } else { standaloneUpdates.push(updateParam); } } preparedCreateParams = Object.values(createById); preparedUpdateParams = standaloneUpdates; } if (preparedCreateParams.length === 0 && preparedUpdateParams.length === 0) { return; } // send the runs in multipart requests const accumulatedContext = []; const accumulatedParts = []; for (const [method, payloads] of [ ["post", preparedCreateParams], ["patch", preparedUpdateParams], ]) { for (const originalPayload of payloads) { // collect fields to be sent as separate parts const { inputs, outputs, events, extra, error, serialized, attachments, ...payload } = originalPayload; const fields = { inputs, outputs, events, extra, error, serialized }; // encode the main run payload const stringifiedPayload = (0, index_js_2.serialize)(payload, `Serializing for multipart ingestion of run with id: ${payload.id}`); accumulatedParts.push({ name: `${method}.${payload.id}`, payload: new Blob([stringifiedPayload], { type: `application/json; length=${stringifiedPayload.length}`, // encoding=gzip }), }); // encode the fields we collected for (const [key, value] of Object.entries(fields)) { if (value === undefined) { continue; } const stringifiedValue = (0, index_js_2.serialize)(value, `Serializing ${key} for multipart ingestion of run with id: ${payload.id}`); accumulatedParts.push({ name: `${method}.${payload.id}.${key}`, payload: new Blob([stringifiedValue], { type: `application/json; length=${stringifiedValue.length}`, }), }); } // encode the attachments if (payload.id !== undefined) { const attachments = allAttachments[payload.id]; if (attachments) { delete allAttachments[payload.id]; for (const [name, attachment] of Object.entries(attachments)) { let contentType; let content; if (Array.isArray(attachment)) { [contentType, content] = attachment; } else { contentType = attachment.mimeType; content = attachment.data; } // Validate that the attachment name doesn't contain a '.' if (name.includes(".")) { console.warn(`Skipping attachment '${name}' for run ${payload.id}: Invalid attachment name. ` + `Attachment names must not contain periods ('.'). Please rename the attachment and try again.`); continue; } accumulatedParts.push({ name: `attachment.${payload.id}.${name}`, payload: new Blob([content], { type: `${contentType}; length=${content.byteLength}`, }), }); } } } // compute context accumulatedContext.push(`trace=${payload.trace_id},id=${payload.id}`); } } await this._sendMultipartRequest(accumulatedParts, accumulatedContext.join("; "), options); } async _createNodeFetchBody(parts, boundary) { // Create multipart form data manually using Blobs const chunks = []; for (const part of parts) { // Add field boundary chunks.push(new Blob([`--${boundary}\r\n`])); chunks.push(new Blob([ `Content-Disposition: form-data; name="${part.name}"\r\n`, `Content-Type: ${part.payload.type}\r\n\r\n`, ])); chunks.push(part.payload); chunks.push(new Blob(["\r\n"])); } // Add final boundary chunks.push(new Blob([`--${boundary}--\r\n`])); // Combine all chunks into a single Blob const body = new Blob(chunks); // Convert Blob to ArrayBuffer for compatibility const arrayBuffer = await body.arrayBuffer(); return arrayBuffer; } async _createMultipartStream(parts, boundary) { const encoder = new TextEncoder(); // Create a ReadableStream for streaming the multipart data // Only do special handling if we're using node-fetch const stream = new ReadableStream({ async start(controller) { // Helper function to write a chunk to the stream const writeChunk = async (chunk) => { if (typeof chunk === "string") { controller.enqueue(encoder.encode(chunk)); } else { controller.enqueue(chunk); } }; // Write each part to the stream for (const part of parts) { // Write boundary and headers await writeChunk(`--${boundary}\r\n`); await writeChunk(`Content-Disposition: form-data; name="${part.name}"\r\n`); await writeChunk(`Content-Type: ${part.payload.type}\r\n\r\n`); // Write the payload const payloadStream = part.payload.stream(); const reader = payloadStream.getReader(); try { let result; while (!(result = await reader.read()).done) { controller.enqueue(result.value); } } finally { reader.releaseLock(); } await writeChunk("\r\n"); } // Write final boundary await writeChunk(`--${boundary}--\r\n`); controller.close(); }, }); return stream; } async _sendMultipartRequest(parts, context, options) { // Create multipart form data boundary const boundary = "----LangSmithFormBoundary" + Math.random().toString(36).slice(2); const isNodeFetch = (0, fetch_js_1._globalFetchImplementationIsNodeFetch)(); const buildBuffered = () => this._createNodeFetchBody(parts, boundary); const buildStream = () => this._createMultipartStream(parts, boundary); const send = async (body) => { const headers = { ...this.headers, "Content-Type": `multipart/form-data; boundary=${boundary}`, }; if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } return this.batchIngestCaller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${options?.apiUrl ?? this.apiUrl}/runs/multipart`, { method: "POST", headers, body, duplex: "half", signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); }; try { let res; let streamedAttempt = false; // attempt stream only if not disabled and not using node-fetch if (!isNodeFetch && !this.multipartStreamingDisabled) { streamedAttempt = true; res = await send(await buildStream()); } else { res = await send(await buildBuffered()); } // if stream fails, fallback to buffered body if ((!this.multipartStreamingDisabled || streamedAttempt) && res.status === 422 && (options?.apiUrl ?? this.apiUrl) !== DEFAULT_API_URL) { console.warn(`Streaming multipart upload to ${options?.apiUrl ?? this.apiUrl}/runs/multipart failed. ` + `This usually means the host does not support chunked uploads. ` + `Retrying with a buffered upload for operation "${context}".`); // Disable streaming for future requests this.multipartStreamingDisabled = true; // retry with fully-buffered body res = await send(await buildBuffered()); } // raise if still failing await (0, error_js_1.raiseForStatus)(res, "ingest multipart runs", true); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (e) { console.warn(`${e.message.trim()}\n\nContext: ${context}`); } } async updateRun(runId, run, options) { (0, _uuid_js_1.assertUuid)(runId); if (run.inputs) { run.inputs = await this.processInputs(run.inputs); } if (run.outputs) { run.outputs = await this.processOutputs(run.outputs); } // TODO: Untangle types const data = { ...run, id: runId }; if (!this._filterForSampling([data], true).length) { return; } if (this.autoBatchTracing && data.trace_id !== undefined && data.dotted_order !== undefined) { const otelContext = this._cloneCurrentOTELContext(); if (run.end_time !== undefined && data.parent_run_id === undefined && this.blockOnRootRunFinalization && !this.manualFlushMode) { // Trigger batches as soon as a root trace ends and wait to ensure trace finishes // in serverless environments. await this.processRunOperation({ action: "update", item: data, otelContext, apiKey: options?.apiKey, apiUrl: options?.apiUrl, }).catch(console.error); return; } else { void this.processRunOperation({ action: "update", item: data, otelContext, apiKey: options?.apiKey, apiUrl: options?.apiUrl, }).catch(console.error); } return; } const headers = { ...this.headers, "Content-Type": "application/json", }; if (options?.apiKey !== undefined) { headers["x-api-key"] = options.apiKey; } const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${options?.apiUrl ?? this.apiUrl}/runs/${runId}`, { method: "PATCH", headers, body: (0, index_js_2.serialize)(run, `Serializing payload to update run with id: ${runId}`), signal: AbortSignal.timeout(this.timeout_ms), ...this.fetchOptions, }); await (0, error_js_1.raiseForStatus)(response, "update run", true); } async readRun(runId, { loadChildRuns } = { loadChildRuns: false }) { (0, _uuid_js_1.assertUuid)(runId); let run = await this._get(`/runs/${runId}`); if (loadChildRuns) { run = await this._loadChildRuns(run); }