import { Assistant, AssistantGraph, Config, DefaultValues, GraphSchema, Metadata, Run, Thread, ThreadState, Cron, AssistantVersion, Subgraphs, Checkpoint, } from "./schema.js"; import { AsyncCaller, AsyncCallerParams } from "./utils/async_caller.js"; import { EventSourceParser, createParser, } from "./utils/eventsource-parser/index.js"; import { IterableReadableStream } from "./utils/stream.js"; import { RunsCreatePayload, RunsStreamPayload, RunsWaitPayload, StreamEvent, CronsCreatePayload, OnConflictBehavior, } from "./types.js"; interface ClientConfig { apiUrl?: string; apiKey?: string; callerOptions?: AsyncCallerParams; timeoutMs?: number; defaultHeaders?: Record; } class BaseClient { protected asyncCaller: AsyncCaller; protected timeoutMs: number; protected apiUrl: string; protected defaultHeaders: Record; constructor(config?: ClientConfig) { this.asyncCaller = new AsyncCaller({ maxRetries: 4, maxConcurrency: 4, ...config?.callerOptions, }); this.timeoutMs = config?.timeoutMs || 12_000; this.apiUrl = config?.apiUrl || "http://localhost:8123"; this.defaultHeaders = config?.defaultHeaders || {}; if (config?.apiKey != null) { this.defaultHeaders["X-Api-Key"] = config.apiKey; } } protected prepareFetchOptions( path: string, options?: RequestInit & { json?: unknown; params?: Record; }, ): [url: URL, init: RequestInit] { const mutatedOptions = { ...options, headers: { ...this.defaultHeaders, ...options?.headers }, }; if (mutatedOptions.json) { mutatedOptions.body = JSON.stringify(mutatedOptions.json); mutatedOptions.headers = { ...mutatedOptions.headers, "Content-Type": "application/json", }; delete mutatedOptions.json; } const targetUrl = new URL(`${this.apiUrl}${path}`); if (mutatedOptions.params) { for (const [key, value] of Object.entries(mutatedOptions.params)) { if (value == null) continue; let strValue = typeof value === "string" || typeof value === "number" ? value.toString() : JSON.stringify(value); targetUrl.searchParams.append(key, strValue); } delete mutatedOptions.params; } return [targetUrl, mutatedOptions]; } protected async fetch( path: string, options?: RequestInit & { json?: unknown; params?: Record; }, ): Promise { const response = await this.asyncCaller.fetch( ...this.prepareFetchOptions(path, options), ); if (response.status === 202 || response.status === 204) { return undefined as T; } return response.json() as T; } } export class CronsClient extends BaseClient { /** * * @param threadId The ID of the thread. * @param assistantId Assistant ID to use for this cron job. * @param payload Payload for creating a cron job. * @returns The created background run. */ async createForThread( threadId: string, assistantId: string, payload?: CronsCreatePayload, ): Promise { const json: Record = { schedule: payload?.schedule, input: payload?.input, config: payload?.config, metadata: payload?.metadata, assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/threads/${threadId}/runs/crons`, { method: "POST", json, }); } /** * * @param assistantId Assistant ID to use for this cron job. * @param payload Payload for creating a cron job. * @returns */ async create( assistantId: string, payload?: CronsCreatePayload, ): Promise { const json: Record = { schedule: payload?.schedule, input: payload?.input, config: payload?.config, metadata: payload?.metadata, assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/runs/crons`, { method: "POST", json, }); } /** * * @param cronId Cron ID of Cron job to delete. */ async delete(cronId: string): Promise { await this.fetch(`/runs/crons/${cronId}`, { method: "DELETE", }); } /** * * @param query Query options. * @returns List of crons. */ async search(query?: { assistantId?: string; threadId?: string; limit?: number; offset?: number; }): Promise { return this.fetch("/runs/crons/search", { method: "POST", json: { assistant_id: query?.assistantId ?? undefined, thread_id: query?.threadId ?? undefined, limit: query?.limit ?? 10, offset: query?.offset ?? 0, }, }); } } export class AssistantsClient extends BaseClient { /** * Get an assistant by ID. * * @param assistantId The ID of the assistant. * @returns Assistant */ async get(assistantId: string): Promise { return this.fetch(`/assistants/${assistantId}`); } /** * Get the JSON representation of the graph assigned to a runnable * @param assistantId The ID of the assistant. * @returns Serialized graph */ async getGraph(assistantId: string): Promise { return this.fetch(`/assistants/${assistantId}/graph`); } /** * Get the state and config schema of the graph assigned to a runnable * @param assistantId The ID of the assistant. * @returns Graph schema */ async getSchemas(assistantId: string): Promise { return this.fetch(`/assistants/${assistantId}/schemas`); } /** * Get the schemas of an assistant by ID. * * @param assistantId The ID of the assistant to get the schema of. * @param options Additional options for getting subgraphs, such as namespace or recursion extraction. * @returns The subgraphs of the assistant. */ async getSubgraphs( assistantId: string, options?: { namespace?: string; recurse?: boolean; }, ): Promise { if (options?.namespace) { return this.fetch( `/assistants/${assistantId}/subgraphs/${options.namespace}`, { params: { recurse: options?.recurse } }, ); } return this.fetch(`/assistants/${assistantId}/subgraphs`, { params: { recurse: options?.recurse }, }); } /** * Create a new assistant. * @param payload Payload for creating an assistant. * @returns The created assistant. */ async create(payload: { graphId: string; config?: Config; metadata?: Metadata; assistantId?: string; ifExists?: OnConflictBehavior; name?: string; }): Promise { return this.fetch("/assistants", { method: "POST", json: { graph_id: payload.graphId, config: payload.config, metadata: payload.metadata, assistant_id: payload.assistantId, if_exists: payload.ifExists, name: payload.name, }, }); } /** * Update an assistant. * @param assistantId ID of the assistant. * @param payload Payload for updating the assistant. * @returns The updated assistant. */ async update( assistantId: string, payload: { graphId?: string; config?: Config; metadata?: Metadata; name?: string; }, ): Promise { return this.fetch(`/assistants/${assistantId}`, { method: "PATCH", json: { graph_id: payload.graphId, config: payload.config, metadata: payload.metadata, name: payload.name, }, }); } /** * Delete an assistant. * * @param assistantId ID of the assistant. */ async delete(assistantId: string): Promise { return this.fetch(`/assistants/${assistantId}`, { method: "DELETE", }); } /** * List assistants. * @param query Query options. * @returns List of assistants. */ async search(query?: { graphId?: string; metadata?: Metadata; limit?: number; offset?: number; }): Promise { return this.fetch("/assistants/search", { method: "POST", json: { graph_id: query?.graphId ?? undefined, metadata: query?.metadata ?? undefined, limit: query?.limit ?? 10, offset: query?.offset ?? 0, }, }); } /** * List all versions of an assistant. * * @param assistantId ID of the assistant. * @returns List of assistant versions. */ async getVersions( assistantId: string, payload?: { metadata?: Metadata; limit?: number; offset?: number; }, ): Promise { return this.fetch( `/assistants/${assistantId}/versions`, { method: "POST", json: { metadata: payload?.metadata ?? undefined, limit: payload?.limit ?? 10, offset: payload?.offset ?? 0, }, }, ); } /** * Change the version of an assistant. * * @param assistantId ID of the assistant. * @param version The version to change to. * @returns The updated assistant. */ async setLatest(assistantId: string, version: number): Promise { return this.fetch(`/assistants/${assistantId}/latest`, { method: "POST", json: { version }, }); } } export class ThreadsClient extends BaseClient { /** * Get a thread by ID. * * @param threadId ID of the thread. * @returns The thread. */ async get(threadId: string): Promise { return this.fetch(`/threads/${threadId}`); } /** * Create a new thread. * * @param payload Payload for creating a thread. * @returns The created thread. */ async create(payload?: { /** * Metadata for the thread. */ metadata?: Metadata; threadId?: string; ifExists?: OnConflictBehavior; }): Promise { return this.fetch(`/threads`, { method: "POST", json: { metadata: payload?.metadata, thread_id: payload?.threadId, if_exists: payload?.ifExists, }, }); } /** * Copy an existing thread * @param threadId ID of the thread to be copied * @returns Newly copied thread */ async copy(threadId: string): Promise { return this.fetch(`/threads/${threadId}/copy`, { method: "POST", }); } /** * Update a thread. * * @param threadId ID of the thread. * @param payload Payload for updating the thread. * @returns The updated thread. */ async update( threadId: string, payload?: { /** * Metadata for the thread. */ metadata?: Metadata; }, ): Promise { return this.fetch(`/threads/${threadId}`, { method: "PATCH", json: { metadata: payload?.metadata }, }); } /** * Delete a thread. * * @param threadId ID of the thread. */ async delete(threadId: string): Promise { return this.fetch(`/threads/${threadId}`, { method: "DELETE", }); } /** * List threads * * @param query Query options * @returns List of threads */ async search(query?: { /** * Metadata to filter threads by. */ metadata?: Metadata; /** * Maximum number of threads to return. * Defaults to 10 */ limit?: number; /** * Offset to start from. */ offset?: number; }): Promise { return this.fetch("/threads/search", { method: "POST", json: { metadata: query?.metadata ?? undefined, limit: query?.limit ?? 10, offset: query?.offset ?? 0, }, }); } /** * Get state for a thread. * * @param threadId ID of the thread. * @returns Thread state. */ async getState( threadId: string, checkpoint?: Checkpoint | string, options?: { subgraphs?: boolean }, ): Promise> { if (checkpoint != null) { if (typeof checkpoint !== "string") { return this.fetch>( `/threads/${threadId}/state/checkpoint`, { method: "POST", json: { checkpoint, subgraphs: options?.subgraphs }, }, ); } // deprecated return this.fetch>( `/threads/${threadId}/state/${checkpoint}`, { params: { subgraphs: options?.subgraphs } }, ); } return this.fetch>(`/threads/${threadId}/state`, { params: { subgraphs: options?.subgraphs }, }); } /** * Add state to a thread. * * @param threadId The ID of the thread. * @returns */ async updateState( threadId: string, options: { values: ValuesType; checkpoint?: Checkpoint; checkpointId?: string; asNode?: string; }, ): Promise> { return this.fetch>( `/threads/${threadId}/state`, { method: "POST", json: { values: options.values, checkpoint_id: options.checkpointId, checkpoint: options.checkpoint, as_node: options?.asNode, }, }, ); } /** * Patch the metadata of a thread. * * @param threadIdOrConfig Thread ID or config to patch the state of. * @param metadata Metadata to patch the state with. */ async patchState( threadIdOrConfig: string | Config, metadata: Metadata, ): Promise { let threadId: string; if (typeof threadIdOrConfig !== "string") { if (typeof threadIdOrConfig.configurable.thread_id !== "string") { throw new Error( "Thread ID is required when updating state with a config.", ); } threadId = threadIdOrConfig.configurable.thread_id; } else { threadId = threadIdOrConfig; } return this.fetch(`/threads/${threadId}/state`, { method: "PATCH", json: { metadata: metadata }, }); } /** * Get all past states for a thread. * * @param threadId ID of the thread. * @param options Additional options. * @returns List of thread states. */ async getHistory( threadId: string, options?: { limit?: number; before?: Config; metadata?: Metadata; }, ): Promise[]> { return this.fetch[]>( `/threads/${threadId}/history`, { method: "POST", json: { limit: options?.limit ?? 10, before: options?.before, metadata: options?.metadata, }, }, ); } } export class RunsClient extends BaseClient { stream( threadId: null, assistantId: string, payload?: Omit, ): AsyncGenerator<{ event: StreamEvent; data: any; }>; stream( threadId: string, assistantId: string, payload?: RunsStreamPayload, ): AsyncGenerator<{ event: StreamEvent; data: any; }>; /** * Create a run and stream the results. * * @param threadId The ID of the thread. * @param assistantId Assistant ID to use for this run. * @param payload Payload for creating a run. */ async *stream( threadId: string | null, assistantId: string, payload?: RunsStreamPayload, ): AsyncGenerator<{ event: StreamEvent; data: any; }> { const json: Record = { input: payload?.input, config: payload?.config, metadata: payload?.metadata, stream_mode: payload?.streamMode, stream_subgraphs: payload?.streamSubgraphs, feedback_keys: payload?.feedbackKeys, assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, checkpoint_id: payload?.checkpointId, webhook: payload?.webhook, multitask_strategy: payload?.multitaskStrategy, on_completion: payload?.onCompletion, on_disconnect: payload?.onDisconnect, }; const endpoint = threadId == null ? `/runs/stream` : `/threads/${threadId}/runs/stream`; const response = await this.asyncCaller.fetch( ...this.prepareFetchOptions(endpoint, { method: "POST", json, signal: payload?.signal, }), ); let parser: EventSourceParser; let onEndEvent: () => void; const textDecoder = new TextDecoder(); const stream: ReadableStream<{ event: string; data: any }> = ( response.body || new ReadableStream({ start: (ctrl) => ctrl.close() }) ).pipeThrough( new TransformStream({ async start(ctrl) { parser = createParser((event) => { if ( (payload?.signal && payload.signal.aborted) || (event.type === "event" && event.data === "[DONE]") ) { ctrl.terminate(); return; } if ("data" in event) { ctrl.enqueue({ event: event.event ?? "message", data: JSON.parse(event.data), }); } }); onEndEvent = () => { ctrl.enqueue({ event: "end", data: undefined }); }; }, async transform(chunk) { const payload = textDecoder.decode(chunk); parser.feed(payload); // eventsource-parser will ignore events // that are not terminated by a newline if (payload.trim() === "event: end") onEndEvent(); }, }), ); yield* IterableReadableStream.fromReadableStream(stream); } /** * Create a run. * * @param threadId The ID of the thread. * @param assistantId Assistant ID to use for this run. * @param payload Payload for creating a run. * @returns The created run. */ async create( threadId: string, assistantId: string, payload?: RunsCreatePayload, ): Promise { const json: Record = { input: payload?.input, config: payload?.config, metadata: payload?.metadata, assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, webhook: payload?.webhook, checkpoint_id: payload?.checkpointId, multitask_strategy: payload?.multitaskStrategy, }; return this.fetch(`/threads/${threadId}/runs`, { method: "POST", json, signal: payload?.signal, }); } /** * Create a batch of stateless background runs. * * @param payloads An array of payloads for creating runs. * @returns An array of created runs. */ async createBatch( payloads: (RunsCreatePayload & { assistantId: string })[], ): Promise { const filteredPayloads = payloads .map((payload) => ({ ...payload, assistant_id: payload.assistantId })) .map((payload) => { return Object.fromEntries( Object.entries(payload).filter(([_, v]) => v !== undefined), ); }); return this.fetch("/runs/batch", { method: "POST", json: filteredPayloads, }); } async wait( threadId: null, assistantId: string, payload?: Omit, ): Promise; async wait( threadId: string, assistantId: string, payload?: RunsWaitPayload, ): Promise; /** * Create a run and wait for it to complete. * * @param threadId The ID of the thread. * @param assistantId Assistant ID to use for this run. * @param payload Payload for creating a run. * @returns The last values chunk of the thread. */ async wait( threadId: string | null, assistantId: string, payload?: RunsWaitPayload, ): Promise { const json: Record = { input: payload?.input, config: payload?.config, metadata: payload?.metadata, assistant_id: assistantId, interrupt_before: payload?.interruptBefore, interrupt_after: payload?.interruptAfter, checkpoint_id: payload?.checkpointId, webhook: payload?.webhook, multitask_strategy: payload?.multitaskStrategy, on_completion: payload?.onCompletion, on_disconnect: payload?.onDisconnect, }; const endpoint = threadId == null ? `/runs/wait` : `/threads/${threadId}/runs/wait`; return this.fetch(endpoint, { method: "POST", json, signal: payload?.signal, }); } /** * List all runs for a thread. * * @param threadId The ID of the thread. * @param options Filtering and pagination options. * @returns List of runs. */ async list( threadId: string, options?: { /** * Maximum number of runs to return. * Defaults to 10 */ limit?: number; /** * Offset to start from. * Defaults to 0. */ offset?: number; }, ): Promise { return this.fetch(`/threads/${threadId}/runs`, { params: { limit: options?.limit ?? 10, offset: options?.offset ?? 0, }, }); } /** * Get a run by ID. * * @param threadId The ID of the thread. * @param runId The ID of the run. * @returns The run. */ async get(threadId: string, runId: string): Promise { return this.fetch(`/threads/${threadId}/runs/${runId}`); } /** * Cancel a run. * * @param threadId The ID of the thread. * @param runId The ID of the run. * @param wait Whether to block when canceling * @returns */ async cancel( threadId: string, runId: string, wait: boolean = false, ): Promise { return this.fetch(`/threads/${threadId}/runs/${runId}/cancel`, { method: "POST", params: { wait: wait ? "1" : "0", }, }); } /** * Block until a run is done. * * @param threadId The ID of the thread. * @param runId The ID of the run. * @returns */ async join(threadId: string, runId: string): Promise { return this.fetch(`/threads/${threadId}/runs/${runId}/join`); } /** * Stream output from a run in real-time, until the run is done. * Output is not buffered, so any output produced before this call will * not be received here. * * @param threadId The ID of the thread. * @param runId The ID of the run. * @param signal An optional abort signal. * @returns An async generator yielding stream parts. */ async *joinStream( threadId: string, runId: string, signal?: AbortSignal, ): AsyncGenerator<{ event: StreamEvent; data: any }> { const response = await this.asyncCaller.fetch( ...this.prepareFetchOptions(`/threads/${threadId}/runs/${runId}/stream`, { method: "GET", signal, }), ); let parser: EventSourceParser; let onEndEvent: () => void; const textDecoder = new TextDecoder(); const stream: ReadableStream<{ event: string; data: any }> = ( response.body || new ReadableStream({ start: (ctrl) => ctrl.close() }) ).pipeThrough( new TransformStream({ async start(ctrl) { parser = createParser((event) => { if ( (signal && signal.aborted) || (event.type === "event" && event.data === "[DONE]") ) { ctrl.terminate(); return; } if ("data" in event) { ctrl.enqueue({ event: event.event ?? "message", data: JSON.parse(event.data), }); } }); onEndEvent = () => { ctrl.enqueue({ event: "end", data: undefined }); }; }, async transform(chunk) { const payload = textDecoder.decode(chunk); parser.feed(payload); // eventsource-parser will ignore events // that are not terminated by a newline if (payload.trim() === "event: end") onEndEvent(); }, }), ); yield* IterableReadableStream.fromReadableStream(stream); } /** * Delete a run. * * @param threadId The ID of the thread. * @param runId The ID of the run. * @returns */ async delete(threadId: string, runId: string): Promise { return this.fetch(`/threads/${threadId}/runs/${runId}`, { method: "DELETE", }); } } export class Client { /** * The client for interacting with assistants. */ public assistants: AssistantsClient; /** * The client for interacting with threads. */ public threads: ThreadsClient; /** * The client for interacting with runs. */ public runs: RunsClient; /** * The client for interacting with cron runs. */ public crons: CronsClient; constructor(config?: ClientConfig) { this.assistants = new AssistantsClient(config); this.threads = new ThreadsClient(config); this.runs = new RunsClient(config); this.crons = new CronsClient(config); } }