UNPKG

@langchain/core

Version:
1,226 lines 93.8 kB
import { z } from "zod"; import pRetry from "p-retry"; import { v4 as uuidv4 } from "uuid"; import { isTraceableFunction, } from "langsmith/singletons/traceable"; import { LogStreamCallbackHandler, RunLog, RunLogPatch, isLogStreamHandler, } from "../tracers/log_stream.js"; import { EventStreamCallbackHandler, isStreamEventsHandler, } from "../tracers/event_stream.js"; import { Serializable } from "../load/serializable.js"; import { IterableReadableStream, concat, atee, pipeGeneratorWithSetup, AsyncGeneratorWithSetup, } from "../utils/stream.js"; import { raceWithSignal } from "../utils/signal.js"; import { DEFAULT_RECURSION_LIMIT, ensureConfig, getCallbackManagerForConfig, mergeConfigs, patchConfig, pickRunnableConfigKeys, } from "./config.js"; import { AsyncCaller } from "../utils/async_caller.js"; import { RootListenersTracer } from "../tracers/root_listener.js"; import { _RootEventFilter, isRunnableInterface } from "./utils.js"; import { AsyncLocalStorageProviderSingleton } from "../singletons/index.js"; import { Graph } from "./graph.js"; import { convertToHttpEventStream } from "./wrappers.js"; import { consumeAsyncIterableInContext, consumeIteratorInContext, isAsyncIterable, isIterableIterator, isIterator, } from "./iter.js"; import { _isToolCall, ToolInputParsingException } from "../tools/utils.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function _coerceToDict(value, defaultKey) { return value && !Array.isArray(value) && // eslint-disable-next-line no-instanceof/no-instanceof !(value instanceof Date) && typeof value === "object" ? value : { [defaultKey]: value }; } /** * A Runnable is a generic unit of work that can be invoked, batched, streamed, and/or * transformed. */ export class Runnable extends Serializable { constructor() { super(...arguments); Object.defineProperty(this, "lc_runnable", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "name", { enumerable: true, configurable: true, writable: true, value: void 0 }); } getName(suffix) { const name = // eslint-disable-next-line @typescript-eslint/no-explicit-any this.name ?? this.constructor.lc_name() ?? this.constructor.name; return suffix ? `${name}${suffix}` : name; } /** * Bind arguments to a Runnable, returning a new Runnable. * @param kwargs * @returns A new RunnableBinding that, when invoked, will apply the bound args. */ bind(kwargs) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableBinding({ bound: this, kwargs, config: {} }); } /** * Return a new Runnable that maps a list of inputs to a list of outputs, * by calling invoke() with each input. */ map() { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableEach({ bound: this }); } /** * Add retry logic to an existing runnable. * @param kwargs * @returns A new RunnableRetry that, when invoked, will retry according to the parameters. */ withRetry(fields) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableRetry({ bound: this, kwargs: {}, config: {}, maxAttemptNumber: fields?.stopAfterAttempt, ...fields, }); } /** * Bind config to a Runnable, returning a new Runnable. * @param config New configuration parameters to attach to the new runnable. * @returns A new RunnableBinding with a config matching what's passed. */ withConfig(config) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableBinding({ bound: this, config, kwargs: {}, }); } /** * Create a new runnable from the current one that will try invoking * other passed fallback runnables if the initial invocation fails. * @param fields.fallbacks Other runnables to call if the runnable errors. * @returns A new RunnableWithFallbacks. */ withFallbacks(fields) { const fallbacks = Array.isArray(fields) ? fields : fields.fallbacks; // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableWithFallbacks({ runnable: this, fallbacks, }); } _getOptionsList(options, length = 0) { if (Array.isArray(options) && options.length !== length) { throw new Error(`Passed "options" must be an array with the same length as the inputs, but got ${options.length} options for ${length} inputs`); } if (Array.isArray(options)) { return options.map(ensureConfig); } if (length > 1 && !Array.isArray(options) && options.runId) { console.warn("Provided runId will be used only for the first element of the batch."); const subsequent = Object.fromEntries(Object.entries(options).filter(([key]) => key !== "runId")); return Array.from({ length }, (_, i) => ensureConfig(i === 0 ? options : subsequent)); } return Array.from({ length }, () => ensureConfig(options)); } async batch(inputs, options, batchOptions) { const configList = this._getOptionsList(options ?? {}, inputs.length); const maxConcurrency = configList[0]?.maxConcurrency ?? batchOptions?.maxConcurrency; const caller = new AsyncCaller({ maxConcurrency, onFailedAttempt: (e) => { throw e; }, }); const batchCalls = inputs.map((input, i) => caller.call(async () => { try { const result = await this.invoke(input, configList[i]); return result; } catch (e) { if (batchOptions?.returnExceptions) { return e; } throw e; } })); return Promise.all(batchCalls); } /** * Default streaming implementation. * Subclasses should override this method if they support streaming output. * @param input * @param options */ async *_streamIterator(input, options) { yield this.invoke(input, options); } /** * Stream output in chunks. * @param input * @param options * @returns A readable stream that is also an iterable. */ async stream(input, options) { // Buffer the first streamed chunk to allow for initial errors // to surface immediately. const config = ensureConfig(options); const wrappedGenerator = new AsyncGeneratorWithSetup({ generator: this._streamIterator(input, config), config, }); await wrappedGenerator.setup; return IterableReadableStream.fromAsyncGenerator(wrappedGenerator); } _separateRunnableConfigFromCallOptions(options) { let runnableConfig; if (options === undefined) { runnableConfig = ensureConfig(options); } else { runnableConfig = ensureConfig({ callbacks: options.callbacks, tags: options.tags, metadata: options.metadata, runName: options.runName, configurable: options.configurable, recursionLimit: options.recursionLimit, maxConcurrency: options.maxConcurrency, runId: options.runId, timeout: options.timeout, signal: options.signal, }); } const callOptions = { ...options }; delete callOptions.callbacks; delete callOptions.tags; delete callOptions.metadata; delete callOptions.runName; delete callOptions.configurable; delete callOptions.recursionLimit; delete callOptions.maxConcurrency; delete callOptions.runId; delete callOptions.timeout; delete callOptions.signal; return [runnableConfig, callOptions]; } async _callWithConfig(func, input, options) { const config = ensureConfig(options); const callbackManager_ = await getCallbackManagerForConfig(config); const runManager = await callbackManager_?.handleChainStart(this.toJSON(), _coerceToDict(input, "input"), config.runId, config?.runType, undefined, undefined, config?.runName ?? this.getName()); delete config.runId; let output; try { const promise = func.call(this, input, config, runManager); output = await raceWithSignal(promise, options?.signal); } catch (e) { await runManager?.handleChainError(e); throw e; } await runManager?.handleChainEnd(_coerceToDict(output, "output")); return output; } /** * Internal method that handles batching and configuration for a runnable * It takes a function, input values, and optional configuration, and * returns a promise that resolves to the output values. * @param func The function to be executed for each input value. * @param input The input values to be processed. * @param config Optional configuration for the function execution. * @returns A promise that resolves to the output values. */ async _batchWithConfig(func, inputs, options, batchOptions) { const optionsList = this._getOptionsList(options ?? {}, inputs.length); const callbackManagers = await Promise.all(optionsList.map(getCallbackManagerForConfig)); const runManagers = await Promise.all(callbackManagers.map(async (callbackManager, i) => { const handleStartRes = await callbackManager?.handleChainStart(this.toJSON(), _coerceToDict(inputs[i], "input"), optionsList[i].runId, optionsList[i].runType, undefined, undefined, optionsList[i].runName ?? this.getName()); delete optionsList[i].runId; return handleStartRes; })); let outputs; try { const promise = func.call(this, inputs, optionsList, runManagers, batchOptions); outputs = await raceWithSignal(promise, optionsList?.[0]?.signal); } catch (e) { await Promise.all(runManagers.map((runManager) => runManager?.handleChainError(e))); throw e; } await Promise.all(runManagers.map((runManager) => runManager?.handleChainEnd(_coerceToDict(outputs, "output")))); return outputs; } /** * Helper method to transform an Iterator of Input values into an Iterator of * Output values, with callbacks. * Use this to implement `stream()` or `transform()` in Runnable subclasses. */ async *_transformStreamWithConfig(inputGenerator, transformer, options) { let finalInput; let finalInputSupported = true; let finalOutput; let finalOutputSupported = true; const config = ensureConfig(options); const callbackManager_ = await getCallbackManagerForConfig(config); async function* wrapInputForTracing() { for await (const chunk of inputGenerator) { if (finalInputSupported) { if (finalInput === undefined) { finalInput = chunk; } else { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any finalInput = concat(finalInput, chunk); } catch { finalInput = undefined; finalInputSupported = false; } } } yield chunk; } } let runManager; try { const pipe = await pipeGeneratorWithSetup(transformer.bind(this), wrapInputForTracing(), async () => callbackManager_?.handleChainStart(this.toJSON(), { input: "" }, config.runId, config.runType, undefined, undefined, config.runName ?? this.getName()), options?.signal, config); delete config.runId; runManager = pipe.setup; const streamEventsHandler = runManager?.handlers.find(isStreamEventsHandler); let iterator = pipe.output; if (streamEventsHandler !== undefined && runManager !== undefined) { iterator = streamEventsHandler.tapOutputIterable(runManager.runId, iterator); } const streamLogHandler = runManager?.handlers.find(isLogStreamHandler); if (streamLogHandler !== undefined && runManager !== undefined) { iterator = streamLogHandler.tapOutputIterable(runManager.runId, iterator); } for await (const chunk of iterator) { yield chunk; if (finalOutputSupported) { if (finalOutput === undefined) { finalOutput = chunk; } else { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any finalOutput = concat(finalOutput, chunk); } catch { finalOutput = undefined; finalOutputSupported = false; } } } } } catch (e) { await runManager?.handleChainError(e, undefined, undefined, undefined, { inputs: _coerceToDict(finalInput, "input"), }); throw e; } await runManager?.handleChainEnd(finalOutput ?? {}, undefined, undefined, undefined, { inputs: _coerceToDict(finalInput, "input") }); } getGraph(_) { const graph = new Graph(); // TODO: Add input schema for runnables const inputNode = graph.addNode({ name: `${this.getName()}Input`, schema: z.any(), }); const runnableNode = graph.addNode(this); // TODO: Add output schemas for runnables const outputNode = graph.addNode({ name: `${this.getName()}Output`, schema: z.any(), }); graph.addEdge(inputNode, runnableNode); graph.addEdge(runnableNode, outputNode); return graph; } /** * Create a new runnable sequence that runs each individual runnable in series, * piping the output of one runnable into another runnable or runnable-like. * @param coerceable A runnable, function, or object whose values are functions or runnables. * @returns A new runnable sequence. */ pipe(coerceable) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableSequence({ first: this, last: _coerceToRunnable(coerceable), }); } /** * Pick keys from the dict output of this runnable. Returns a new runnable. */ pick(keys) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return this.pipe(new RunnablePick(keys)); } /** * Assigns new fields to the dict output of this runnable. Returns a new runnable. */ assign(mapping) { return this.pipe( // eslint-disable-next-line @typescript-eslint/no-use-before-define new RunnableAssign( // eslint-disable-next-line @typescript-eslint/no-use-before-define new RunnableMap({ steps: mapping }))); } /** * Default implementation of transform, which buffers input and then calls stream. * Subclasses should override this method if they can start producing output while * input is still being generated. * @param generator * @param options */ async *transform(generator, options) { let finalChunk; for await (const chunk of generator) { if (finalChunk === undefined) { finalChunk = chunk; } else { // Make a best effort to gather, for any type that supports concat. // This method should throw an error if gathering fails. // eslint-disable-next-line @typescript-eslint/no-explicit-any finalChunk = concat(finalChunk, chunk); } } yield* this._streamIterator(finalChunk, ensureConfig(options)); } /** * Stream all output from a runnable, as reported to the callback system. * This includes all inner runs of LLMs, Retrievers, Tools, etc. * Output is streamed as Log objects, which include a list of * jsonpatch ops that describe how the state of the run has changed in each * step, and the final state of the run. * The jsonpatch ops can be applied in order to construct state. * @param input * @param options * @param streamOptions */ async *streamLog(input, options, streamOptions) { const logStreamCallbackHandler = new LogStreamCallbackHandler({ ...streamOptions, autoClose: false, _schemaFormat: "original", }); const config = ensureConfig(options); yield* this._streamLog(input, logStreamCallbackHandler, config); } async *_streamLog(input, logStreamCallbackHandler, config) { const { callbacks } = config; if (callbacks === undefined) { // eslint-disable-next-line no-param-reassign config.callbacks = [logStreamCallbackHandler]; } else if (Array.isArray(callbacks)) { // eslint-disable-next-line no-param-reassign config.callbacks = callbacks.concat([logStreamCallbackHandler]); } else { const copiedCallbacks = callbacks.copy(); copiedCallbacks.addHandler(logStreamCallbackHandler, true); // eslint-disable-next-line no-param-reassign config.callbacks = copiedCallbacks; } const runnableStreamPromise = this.stream(input, config); async function consumeRunnableStream() { try { const runnableStream = await runnableStreamPromise; for await (const chunk of runnableStream) { const patch = new RunLogPatch({ ops: [ { op: "add", path: "/streamed_output/-", value: chunk, }, ], }); await logStreamCallbackHandler.writer.write(patch); } } finally { await logStreamCallbackHandler.writer.close(); } } const runnableStreamConsumePromise = consumeRunnableStream(); try { for await (const log of logStreamCallbackHandler) { yield log; } } finally { await runnableStreamConsumePromise; } } streamEvents(input, options, streamOptions) { let stream; if (options.version === "v1") { stream = this._streamEventsV1(input, options, streamOptions); } else if (options.version === "v2") { stream = this._streamEventsV2(input, options, streamOptions); } else { throw new Error(`Only versions "v1" and "v2" of the schema are currently supported.`); } if (options.encoding === "text/event-stream") { return convertToHttpEventStream(stream); } else { return IterableReadableStream.fromAsyncGenerator(stream); } } async *_streamEventsV2(input, options, streamOptions) { const eventStreamer = new EventStreamCallbackHandler({ ...streamOptions, autoClose: false, }); const config = ensureConfig(options); const runId = config.runId ?? uuidv4(); config.runId = runId; const callbacks = config.callbacks; if (callbacks === undefined) { config.callbacks = [eventStreamer]; } else if (Array.isArray(callbacks)) { config.callbacks = callbacks.concat(eventStreamer); } else { const copiedCallbacks = callbacks.copy(); copiedCallbacks.addHandler(eventStreamer, true); // eslint-disable-next-line no-param-reassign config.callbacks = copiedCallbacks; } const abortController = new AbortController(); // Call the runnable in streaming mode, // add each chunk to the output stream const outerThis = this; async function consumeRunnableStream() { try { let signal; if (options?.signal) { if ("any" in AbortSignal) { // Use native AbortSignal.any() if available (Node 19+) // eslint-disable-next-line @typescript-eslint/no-explicit-any signal = AbortSignal.any([ abortController.signal, options.signal, ]); } else { // Fallback for Node 18 and below - just use the provided signal signal = options.signal; // Ensure we still abort our controller when the parent signal aborts options.signal.addEventListener("abort", () => { abortController.abort(); }, { once: true }); } } else { signal = abortController.signal; } const runnableStream = await outerThis.stream(input, { ...config, signal, }); const tappedStream = eventStreamer.tapOutputIterable(runId, runnableStream); // eslint-disable-next-line @typescript-eslint/no-unused-vars for await (const _ of tappedStream) { // Just iterate so that the callback handler picks up events if (abortController.signal.aborted) break; } } finally { await eventStreamer.finish(); } } const runnableStreamConsumePromise = consumeRunnableStream(); let firstEventSent = false; let firstEventRunId; try { for await (const event of eventStreamer) { // This is a work-around an issue where the inputs into the // chain are not available until the entire input is consumed. // As a temporary solution, we'll modify the input to be the input // that was passed into the chain. if (!firstEventSent) { event.data.input = input; firstEventSent = true; firstEventRunId = event.run_id; yield event; continue; } if (event.run_id === firstEventRunId && event.event.endsWith("_end")) { // If it's the end event corresponding to the root runnable // we dont include the input in the event since it's guaranteed // to be included in the first event. if (event.data?.input) { delete event.data.input; } } yield event; } } finally { abortController.abort(); await runnableStreamConsumePromise; } } async *_streamEventsV1(input, options, streamOptions) { let runLog; let hasEncounteredStartEvent = false; const config = ensureConfig(options); const rootTags = config.tags ?? []; const rootMetadata = config.metadata ?? {}; const rootName = config.runName ?? this.getName(); const logStreamCallbackHandler = new LogStreamCallbackHandler({ ...streamOptions, autoClose: false, _schemaFormat: "streaming_events", }); const rootEventFilter = new _RootEventFilter({ ...streamOptions, }); const logStream = this._streamLog(input, logStreamCallbackHandler, config); for await (const log of logStream) { if (!runLog) { runLog = RunLog.fromRunLogPatch(log); } else { runLog = runLog.concat(log); } if (runLog.state === undefined) { throw new Error(`Internal error: "streamEvents" state is missing. Please open a bug report.`); } // Yield the start event for the root runnable if it hasn't been seen. // The root run is never filtered out if (!hasEncounteredStartEvent) { hasEncounteredStartEvent = true; const state = { ...runLog.state }; const event = { run_id: state.id, event: `on_${state.type}_start`, name: rootName, tags: rootTags, metadata: rootMetadata, data: { input, }, }; if (rootEventFilter.includeEvent(event, state.type)) { yield event; } } const paths = log.ops .filter((op) => op.path.startsWith("/logs/")) .map((op) => op.path.split("/")[2]); const dedupedPaths = [...new Set(paths)]; for (const path of dedupedPaths) { let eventType; let data = {}; const logEntry = runLog.state.logs[path]; if (logEntry.end_time === undefined) { if (logEntry.streamed_output.length > 0) { eventType = "stream"; } else { eventType = "start"; } } else { eventType = "end"; } if (eventType === "start") { // Include the inputs with the start event if they are available. // Usually they will NOT be available for components that operate // on streams, since those components stream the input and // don't know its final value until the end of the stream. if (logEntry.inputs !== undefined) { data.input = logEntry.inputs; } } else if (eventType === "end") { if (logEntry.inputs !== undefined) { data.input = logEntry.inputs; } data.output = logEntry.final_output; } else if (eventType === "stream") { const chunkCount = logEntry.streamed_output.length; if (chunkCount !== 1) { throw new Error(`Expected exactly one chunk of streamed output, got ${chunkCount} instead. Encountered in: "${logEntry.name}"`); } data = { chunk: logEntry.streamed_output[0] }; // Clean up the stream, we don't need it anymore. // And this avoids duplicates as well! logEntry.streamed_output = []; } yield { event: `on_${logEntry.type}_${eventType}`, name: logEntry.name, run_id: logEntry.id, tags: logEntry.tags, metadata: logEntry.metadata, data, }; } // Finally, we take care of the streaming output from the root chain // if there is any. const { state } = runLog; if (state.streamed_output.length > 0) { const chunkCount = state.streamed_output.length; if (chunkCount !== 1) { throw new Error(`Expected exactly one chunk of streamed output, got ${chunkCount} instead. Encountered in: "${state.name}"`); } const data = { chunk: state.streamed_output[0] }; // Clean up the stream, we don't need it anymore. state.streamed_output = []; const event = { event: `on_${state.type}_stream`, run_id: state.id, tags: rootTags, metadata: rootMetadata, name: rootName, data, }; if (rootEventFilter.includeEvent(event, state.type)) { yield event; } } } const state = runLog?.state; if (state !== undefined) { // Finally, yield the end event for the root runnable. const event = { event: `on_${state.type}_end`, name: rootName, run_id: state.id, tags: rootTags, metadata: rootMetadata, data: { output: state.final_output, }, }; if (rootEventFilter.includeEvent(event, state.type)) yield event; } } // eslint-disable-next-line @typescript-eslint/no-explicit-any static isRunnable(thing) { return isRunnableInterface(thing); } /** * Bind lifecycle listeners to a Runnable, returning a new Runnable. * The Run object contains information about the run, including its id, * type, input, output, error, startTime, endTime, and any tags or metadata * added to the run. * * @param {Object} params - The object containing the callback functions. * @param {(run: Run) => void} params.onStart - Called before the runnable starts running, with the Run object. * @param {(run: Run) => void} params.onEnd - Called after the runnable finishes running, with the Run object. * @param {(run: Run) => void} params.onError - Called if the runnable throws an error, with the Run object. */ withListeners({ onStart, onEnd, onError, }) { // eslint-disable-next-line @typescript-eslint/no-use-before-define return new RunnableBinding({ bound: this, config: {}, configFactories: [ (config) => ({ callbacks: [ new RootListenersTracer({ config, onStart, onEnd, onError, }), ], }), ], }); } /** * Convert a runnable to a tool. Return a new instance of `RunnableToolLike` * which contains the runnable, name, description and schema. * * @template {T extends RunInput = RunInput} RunInput - The input type of the runnable. Should be the same as the `RunInput` type of the runnable. * * @param fields * @param {string | undefined} [fields.name] The name of the tool. If not provided, it will default to the name of the runnable. * @param {string | undefined} [fields.description] The description of the tool. Falls back to the description on the Zod schema if not provided, or undefined if neither are provided. * @param {z.ZodType<T>} [fields.schema] The Zod schema for the input of the tool. Infers the Zod type from the input type of the runnable. * @returns {RunnableToolLike<z.ZodType<T>, RunOutput>} An instance of `RunnableToolLike` which is a runnable that can be used as a tool. */ asTool(fields) { return convertRunnableToTool(this, fields); } } /** * A runnable that delegates calls to another runnable with a set of kwargs. * @example * ```typescript * import { * type RunnableConfig, * RunnableLambda, * } from "@langchain/core/runnables"; * * const enhanceProfile = ( * profile: Record<string, any>, * config?: RunnableConfig * ) => { * if (config?.configurable?.role) { * return { ...profile, role: config.configurable.role }; * } * return profile; * }; * * const runnable = RunnableLambda.from(enhanceProfile); * * // Bind configuration to the runnable to set the user's role dynamically * const adminRunnable = runnable.bind({ configurable: { role: "Admin" } }); * const userRunnable = runnable.bind({ configurable: { role: "User" } }); * * const result1 = await adminRunnable.invoke({ * name: "Alice", * email: "alice@example.com" * }); * * // { name: "Alice", email: "alice@example.com", role: "Admin" } * * const result2 = await userRunnable.invoke({ * name: "Bob", * email: "bob@example.com" * }); * * // { name: "Bob", email: "bob@example.com", role: "User" } * ``` */ export class RunnableBinding extends Runnable { static lc_name() { return "RunnableBinding"; } constructor(fields) { super(fields); Object.defineProperty(this, "lc_namespace", { enumerable: true, configurable: true, writable: true, value: ["langchain_core", "runnables"] }); Object.defineProperty(this, "lc_serializable", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "bound", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "config", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "kwargs", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "configFactories", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.bound = fields.bound; this.kwargs = fields.kwargs; this.config = fields.config; this.configFactories = fields.configFactories; } getName(suffix) { return this.bound.getName(suffix); } async _mergeConfig(...options) { const config = mergeConfigs(this.config, ...options); return mergeConfigs(config, ...(this.configFactories ? await Promise.all(this.configFactories.map(async (configFactory) => await configFactory(config))) : [])); } bind(kwargs) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new this.constructor({ bound: this.bound, kwargs: { ...this.kwargs, ...kwargs }, config: this.config, }); } withConfig(config) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new this.constructor({ bound: this.bound, kwargs: this.kwargs, config: { ...this.config, ...config }, }); } withRetry(fields) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return new this.constructor({ bound: this.bound.withRetry(fields), kwargs: this.kwargs, config: this.config, }); } async invoke(input, options) { return this.bound.invoke(input, await this._mergeConfig(ensureConfig(options), this.kwargs)); } async batch(inputs, options, batchOptions) { const mergedOptions = Array.isArray(options) ? await Promise.all(options.map(async (individualOption) => this._mergeConfig(ensureConfig(individualOption), this.kwargs))) : await this._mergeConfig(ensureConfig(options), this.kwargs); return this.bound.batch(inputs, mergedOptions, batchOptions); } async *_streamIterator(input, options) { yield* this.bound._streamIterator(input, await this._mergeConfig(ensureConfig(options), this.kwargs)); } async stream(input, options) { return this.bound.stream(input, await this._mergeConfig(ensureConfig(options), this.kwargs)); } async *transform(generator, options) { yield* this.bound.transform(generator, await this._mergeConfig(ensureConfig(options), this.kwargs)); } streamEvents(input, options, streamOptions) { // eslint-disable-next-line @typescript-eslint/no-this-alias const outerThis = this; const generator = async function* () { yield* outerThis.bound.streamEvents(input, { ...(await outerThis._mergeConfig(ensureConfig(options), outerThis.kwargs)), version: options.version, }, streamOptions); }; return IterableReadableStream.fromAsyncGenerator(generator()); } static isRunnableBinding( // eslint-disable-next-line @typescript-eslint/no-explicit-any thing // eslint-disable-next-line @typescript-eslint/no-explicit-any ) { return thing.bound && Runnable.isRunnable(thing.bound); } /** * Bind lifecycle listeners to a Runnable, returning a new Runnable. * The Run object contains information about the run, including its id, * type, input, output, error, startTime, endTime, and any tags or metadata * added to the run. * * @param {Object} params - The object containing the callback functions. * @param {(run: Run) => void} params.onStart - Called before the runnable starts running, with the Run object. * @param {(run: Run) => void} params.onEnd - Called after the runnable finishes running, with the Run object. * @param {(run: Run) => void} params.onError - Called if the runnable throws an error, with the Run object. */ withListeners({ onStart, onEnd, onError, }) { return new RunnableBinding({ bound: this.bound, kwargs: this.kwargs, config: this.config, configFactories: [ (config) => ({ callbacks: [ new RootListenersTracer({ config, onStart, onEnd, onError, }), ], }), ], }); } } /** * A runnable that delegates calls to another runnable * with each element of the input sequence. * @example * ```typescript * import { RunnableEach, RunnableLambda } from "@langchain/core/runnables"; * * const toUpperCase = (input: string): string => input.toUpperCase(); * const addGreeting = (input: string): string => `Hello, ${input}!`; * * const upperCaseLambda = RunnableLambda.from(toUpperCase); * const greetingLambda = RunnableLambda.from(addGreeting); * * const chain = new RunnableEach({ * bound: upperCaseLambda.pipe(greetingLambda), * }); * * const result = await chain.invoke(["alice", "bob", "carol"]) * * // ["Hello, ALICE!", "Hello, BOB!", "Hello, CAROL!"] * ``` */ export class RunnableEach extends Runnable { static lc_name() { return "RunnableEach"; } constructor(fields) { super(fields); Object.defineProperty(this, "lc_serializable", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "lc_namespace", { enumerable: true, configurable: true, writable: true, value: ["langchain_core", "runnables"] }); Object.defineProperty(this, "bound", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.bound = fields.bound; } /** * Binds the runnable with the specified arguments. * @param kwargs The arguments to bind the runnable with. * @returns A new instance of the `RunnableEach` class that is bound with the specified arguments. */ bind(kwargs) { return new RunnableEach({ bound: this.bound.bind(kwargs), }); } /** * Invokes the runnable with the specified input and configuration. * @param input The input to invoke the runnable with. * @param config The configuration to invoke the runnable with. * @returns A promise that resolves to the output of the runnable. */ async invoke(inputs, config) { return this._callWithConfig(this._invoke.bind(this), inputs, config); } /** * A helper method that is used to invoke the runnable with the specified input and configuration. * @param input The input to invoke the runnable with. * @param config The configuration to invoke the runnable with. * @returns A promise that resolves to the output of the runnable. */ async _invoke(inputs, config, runManager) { return this.bound.batch(inputs, patchConfig(config, { callbacks: runManager?.getChild() })); } /** * Bind lifecycle listeners to a Runnable, returning a new Runnable. * The Run object contains information about the run, including its id, * type, input, output, error, startTime, endTime, and any tags or metadata * added to the run. * * @param {Object} params - The object containing the callback functions. * @param {(run: Run) => void} params.onStart - Called before the runnable starts running, with the Run object. * @param {(run: Run) => void} params.onEnd - Called after the runnable finishes running, with the Run object. * @param {(run: Run) => void} params.onError - Called if the runnable throws an error, with the Run object. */ withListeners({ onStart, onEnd, onError, }) { return new RunnableEach({ bound: this.bound.withListeners({ onStart, onEnd, onError }), }); } } /** * Base class for runnables that can be retried a * specified number of times. * @example * ```typescript * import { * RunnableLambda, * RunnableRetry, * } from "@langchain/core/runnables"; * * // Simulate an API call that fails * const simulateApiCall = (input: string): string => { * console.log(`Attempting API call with input: ${input}`); * throw new Error("API call failed due to network issue"); * }; * * const apiCallLambda = RunnableLambda.from(simulateApiCall); * * // Apply retry logic using the .withRetry() method * const apiCallWithRetry = apiCallLambda.withRetry({ stopAfterAttempt: 3 }); * * // Alternatively, create a RunnableRetry instance manually * const manualRetry = new RunnableRetry({ * bound: apiCallLambda, * maxAttemptNumber: 3, * config: {}, * }); * * // Example invocation using the .withRetry() method * const res = await apiCallWithRetry * .invoke("Request 1") * .catch((error) => { * console.error("Failed after multiple retries:", error.message); * }); * * // Example invocation using the manual retry instance * const res2 = await manualRetry * .invoke("Request 2") * .catch((error) => { * console.error("Failed after multiple retries:", error.message); * }); * ``` */ export class RunnableRetry extends RunnableBinding { static lc_name() { return "RunnableRetry"; } constructor(fields) { super(fields); Object.defineProperty(this, "lc_namespace", { enumerable: true, configurable: true, writable: true, value: ["langchain_core", "runnables"] }); Object.defineProperty(this, "maxAttemptNumber", { enumerable: true, configurable: true, writable: true, value: 3 }); // eslint-disable-next-line @typescript-eslint/no-explicit-any Object.defineProperty(this, "onFailedAttempt", { enumerable: true, configurable: true, writable: true, value: () => { } }); this.maxAttemptNumber = fields.maxAttemptNumber ?? this.maxAttemptNumber; this.onFailedAttempt = fields.onFailedAttempt ?? this.onFailedAttempt; } _patchConfigForRetry(attempt, config, runManager) { const tag = attempt > 1 ? `retry:attempt:${attempt}` : undefined; return patchConfig(config, { callbacks: runManager?.getChild(tag) }); } async _invoke(input, config, runManager) { return pRetry((attemptNumber) => super.invoke(input, this._patchConfigForRetry(attemptNumber, config, runManager)), { // eslint-disable-next-line @typescript-eslint/no-explicit-any onFailedAttempt: (error) => this.onFailedAttempt(error, input), retries: Math.max(this.maxAttemptNumber - 1, 0), randomize: true, }); } /** * Method that invokes the runnable with the specified input, run manager, * and config. It handles the retry logic by catching any errors and * recursively invoking itself with the updated config for the next retry * attempt. * @param input The input for the runnable. * @param runManager The run manager for the runnable. * @param config The config for the runnable. * @returns A promise that resolves to the output of the runnable. */ async invoke(input, config) { return this._callWithConfig(this._invoke.bind(this), input, config); } async _batch(inputs, configs, runManagers, batchOptions) { const resultsMap = {}; try { await pRetry(async (attemptNumber) => { const remainingIndexes = inputs .map((_, i) => i) .filter((i) => resultsMap[i.toString()] === undefined || // eslint-disable-next-line no-instanceof/no-instanceof resultsMap[i.toString()] instanceof Error); const remainingInputs = remainingIndexes.map((i) => inputs[i]); const patchedConfigs = remainingIndexes.map((i) => this._patchConfigForRetry(attemptNumber, configs?.[i], runManagers?.[i])); const results = await super.batch(remainingInputs, patchedConfigs, { ...batchOptions, returnExceptions: true, }); let firstException; for (let i = 0; i < results.length; i += 1) { const result = results[i]; const resultMapIndex = remainingIndexes[i]; // eslint-disable-next-line no-instanceof/no-instanceof if (result instanceof Error) { if (firstException === undefined) { firstException = result; // eslint-disable-next-line @typescript-eslint/no-explicit-any firstException.input = remainingInputs[i]; } } resultsMap[resultMapIndex.toString()] = result; } if (firstException) { throw firstException; } return results; }, { // eslint-disable-next-line @typescript-eslint/no-explicit-any onFailedAttempt: (error) => this.onFailedAttempt(error, error.input), retries: Math.max(this.maxAttemptNumber - 1, 0), randomize: true, }); } catch (e) { if (batchOptions?.returnExceptions !== true) { throw e; } } return Object.keys(resultsMap) .sort((a, b) => parseInt(a, 10) - parseInt(b, 10)) .map((key) => resultsMap[parseInt(key, 10)]); } async batch(inputs, options, batchOptions) { return this._batchWithConfig(this._batch.bind(this), inputs, options, batchOptions); } } /** * A sequence of runnables, where the output of each is the input of the next. * @example * ```typescript * const promptTemplate = PromptTemplate.fromTemplate( * "Tell me a joke about {topic}", * ); * const chain = RunnableSequence.from([promptTemplate, new ChatOpenAI({})]); * const result = await chain.invoke({ topic: "bears" }); * ``` */ export class RunnableSequence extends Runnable { static lc_name() { return "RunnableSequence"; } constructor(fields) { super(fields); Object.defineProperty(this, "first", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "middle", { enumerable: true, configurable: true, writable: true, value: [] }); // eslin