mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-01 05:55:14 +02:00
libs: add cli, sdk-py, sdk-js and move core langgraph
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
import pRetry from "p-retry";
|
||||
import PQueueMod from "p-queue";
|
||||
|
||||
const STATUS_NO_RETRY = [
|
||||
400, // Bad Request
|
||||
401, // Unauthorized
|
||||
403, // Forbidden
|
||||
404, // Not Found
|
||||
405, // Method Not Allowed
|
||||
406, // Not Acceptable
|
||||
407, // Proxy Authentication Required
|
||||
408, // Request Timeout
|
||||
422, // Unprocessable Entity
|
||||
];
|
||||
const STATUS_IGNORE = [
|
||||
409, // Conflict
|
||||
];
|
||||
|
||||
type ResponseCallback = (response?: Response) => Promise<boolean>;
|
||||
|
||||
export interface AsyncCallerParams {
|
||||
/**
|
||||
* The maximum number of concurrent calls that can be made.
|
||||
* Defaults to `Infinity`, which means no limit.
|
||||
*/
|
||||
maxConcurrency?: number;
|
||||
/**
|
||||
* The maximum number of retries that can be made for a single call,
|
||||
* with an exponential backoff between each attempt. Defaults to 6.
|
||||
*/
|
||||
maxRetries?: number;
|
||||
|
||||
onFailedResponseHook?: ResponseCallback;
|
||||
}
|
||||
|
||||
export interface AsyncCallerCallOptions {
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not rely on globalThis.Response, rather just
|
||||
* do duck typing
|
||||
*/
|
||||
function isResponse(x: unknown): x is Response {
|
||||
if (x == null || typeof x !== "object") return false;
|
||||
return "status" in x && "statusText" in x && "text" in x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility error to properly handle failed requests
|
||||
*/
|
||||
class HTTPError extends Error {
|
||||
status: number;
|
||||
text: string;
|
||||
|
||||
response?: Response;
|
||||
|
||||
constructor(status: number, message: string, response?: Response) {
|
||||
super(`HTTP ${status}: ${message}`);
|
||||
this.status = status;
|
||||
this.text = message;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
static async fromResponse(
|
||||
response: Response,
|
||||
options?: { includeResponse?: boolean },
|
||||
): Promise<HTTPError> {
|
||||
try {
|
||||
return new HTTPError(
|
||||
response.status,
|
||||
await response.text(),
|
||||
options?.includeResponse ? response : undefined,
|
||||
);
|
||||
} catch {
|
||||
return new HTTPError(
|
||||
response.status,
|
||||
response.statusText,
|
||||
options?.includeResponse ? response : undefined,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that can be used to make async calls with concurrency and retry logic.
|
||||
*
|
||||
* This is useful for making calls to any kind of "expensive" external resource,
|
||||
* be it because it's rate-limited, subject to network issues, etc.
|
||||
*
|
||||
* Concurrent calls are limited by the `maxConcurrency` parameter, which defaults
|
||||
* to `Infinity`. This means that by default, all calls will be made in parallel.
|
||||
*
|
||||
* Retries are limited by the `maxRetries` parameter, which defaults to 5. This
|
||||
* means that by default, each call will be retried up to 5 times, with an
|
||||
* exponential backoff between each attempt.
|
||||
*/
|
||||
export class AsyncCaller {
|
||||
protected maxConcurrency: AsyncCallerParams["maxConcurrency"];
|
||||
|
||||
protected maxRetries: AsyncCallerParams["maxRetries"];
|
||||
|
||||
private queue: (typeof import("p-queue"))["default"]["prototype"];
|
||||
|
||||
private onFailedResponseHook?: ResponseCallback;
|
||||
|
||||
constructor(params: AsyncCallerParams) {
|
||||
this.maxConcurrency = params.maxConcurrency ?? Infinity;
|
||||
this.maxRetries = params.maxRetries ?? 4;
|
||||
|
||||
if ("default" in PQueueMod) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.queue = new (PQueueMod.default as any)({
|
||||
concurrency: this.maxConcurrency,
|
||||
});
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
this.queue = new (PQueueMod as any)({ concurrency: this.maxConcurrency });
|
||||
}
|
||||
this.onFailedResponseHook = params?.onFailedResponseHook;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
call<A extends any[], T extends (...args: A) => Promise<any>>(
|
||||
callable: T,
|
||||
...args: Parameters<T>
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
const onFailedResponseHook = this.onFailedResponseHook;
|
||||
return this.queue.add(
|
||||
() =>
|
||||
pRetry(
|
||||
() =>
|
||||
callable(...(args as Parameters<T>)).catch(async (error) => {
|
||||
// eslint-disable-next-line no-instanceof/no-instanceof
|
||||
if (error instanceof Error) {
|
||||
throw error;
|
||||
} else if (isResponse(error)) {
|
||||
throw await HTTPError.fromResponse(error, {
|
||||
includeResponse: !!onFailedResponseHook,
|
||||
});
|
||||
} else {
|
||||
throw new Error(error);
|
||||
}
|
||||
}),
|
||||
{
|
||||
async onFailedAttempt(error) {
|
||||
if (
|
||||
error.message.startsWith("Cancel") ||
|
||||
error.message.startsWith("TimeoutError") ||
|
||||
error.message.startsWith("AbortError")
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
if ((error as any)?.code === "ECONNABORTED") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (error instanceof HTTPError) {
|
||||
if (STATUS_NO_RETRY.includes(error.status)) {
|
||||
throw error;
|
||||
} else if (STATUS_IGNORE.includes(error.status)) {
|
||||
return;
|
||||
}
|
||||
if (onFailedResponseHook && error.response) {
|
||||
await onFailedResponseHook(error.response);
|
||||
}
|
||||
}
|
||||
},
|
||||
// If needed we can change some of the defaults here,
|
||||
// but they're quite sensible.
|
||||
retries: this.maxRetries,
|
||||
randomize: true,
|
||||
},
|
||||
),
|
||||
{ throwOnTimeout: true },
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
callWithOptions<A extends any[], T extends (...args: A) => Promise<any>>(
|
||||
options: AsyncCallerCallOptions,
|
||||
callable: T,
|
||||
...args: Parameters<T>
|
||||
): Promise<Awaited<ReturnType<T>>> {
|
||||
// Note this doesn't cancel the underlying request,
|
||||
// when available prefer to use the signal option of the underlying call
|
||||
if (options.signal) {
|
||||
return Promise.race([
|
||||
this.call<A, T>(callable, ...args),
|
||||
new Promise<never>((_, reject) => {
|
||||
options.signal?.addEventListener("abort", () => {
|
||||
reject(new Error("AbortError"));
|
||||
});
|
||||
}),
|
||||
]);
|
||||
}
|
||||
return this.call<A, T>(callable, ...args);
|
||||
}
|
||||
|
||||
fetch(...args: Parameters<typeof fetch>): ReturnType<typeof fetch> {
|
||||
return this.call(() =>
|
||||
fetch(...args).then((res) => (res.ok ? res : Promise.reject(res))),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
// in this case don't quite match.
|
||||
type IterableReadableStreamInterface<T> = ReadableStream<T> & AsyncIterable<T>;
|
||||
|
||||
/*
|
||||
* Support async iterator syntax for ReadableStreams in all environments.
|
||||
* Source: https://github.com/MattiasBuelens/web-streams-polyfill/pull/122#issuecomment-1627354490
|
||||
*/
|
||||
export class IterableReadableStream<T>
|
||||
extends ReadableStream<T>
|
||||
implements IterableReadableStreamInterface<T>
|
||||
{
|
||||
public reader: ReadableStreamDefaultReader<T>;
|
||||
|
||||
ensureReader() {
|
||||
if (!this.reader) {
|
||||
this.reader = this.getReader();
|
||||
}
|
||||
}
|
||||
|
||||
async next(): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
try {
|
||||
const result = await this.reader.read();
|
||||
if (result.done) {
|
||||
this.reader.releaseLock(); // release lock when stream becomes closed
|
||||
return {
|
||||
done: true,
|
||||
value: undefined,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
done: false,
|
||||
value: result.value,
|
||||
};
|
||||
}
|
||||
} catch (e) {
|
||||
this.reader.releaseLock(); // release lock when stream becomes errored
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async return(): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
// If wrapped in a Node stream, cancel is already called.
|
||||
if (this.locked) {
|
||||
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
|
||||
this.reader.releaseLock(); // release lock first
|
||||
await cancelPromise; // now await it
|
||||
}
|
||||
return { done: true, value: undefined };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
async throw(e: any): Promise<IteratorResult<T>> {
|
||||
this.ensureReader();
|
||||
if (this.locked) {
|
||||
const cancelPromise = this.reader.cancel(); // cancel first, but don't await yet
|
||||
this.reader.releaseLock(); // release lock first
|
||||
await cancelPromise; // now await it
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this;
|
||||
}
|
||||
|
||||
static fromReadableStream<T>(stream: ReadableStream<T>) {
|
||||
// From https://developer.mozilla.org/en-US/docs/Web/API/Streams_API/Using_readable_streams#reading_the_stream
|
||||
const reader = stream.getReader();
|
||||
return new IterableReadableStream<T>({
|
||||
start(controller) {
|
||||
return pump();
|
||||
function pump(): Promise<T | undefined> {
|
||||
return reader.read().then(({ done, value }) => {
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Enqueue the next data chunk into our target stream
|
||||
controller.enqueue(value);
|
||||
return pump();
|
||||
});
|
||||
}
|
||||
},
|
||||
cancel() {
|
||||
reader.releaseLock();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static fromAsyncGenerator<T>(generator: AsyncGenerator<T>) {
|
||||
return new IterableReadableStream<T>({
|
||||
async pull(controller) {
|
||||
const { value, done } = await generator.next();
|
||||
// When no more data needs to be consumed, close the stream
|
||||
if (done) {
|
||||
controller.close();
|
||||
}
|
||||
// Fix: `else if (value)` will hang the streaming when nullish value (e.g. empty string) is pulled
|
||||
controller.enqueue(value);
|
||||
},
|
||||
async cancel(reason) {
|
||||
await generator.return(reason);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user