@langchain/core
Version:
Core LangChain.js abstractions and schemas
1,226 lines • 93.8 kB
JavaScript
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