substarte 120240617.1.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
1
+ /**
2
+ * @private
3
+ *
4
+ * Returns properties of the current environment.
5
+ */
6
+
7
+ declare const Deno: any;
8
+ declare const EdgeRuntime: any;
9
+
10
+ type Arch = "x32" | "x64" | "arm" | "arm64" | `other:${string}` | "unknown";
11
+
12
+ type PlatformName =
13
+ | "MacOS"
14
+ | "Linux"
15
+ | "Windows"
16
+ | "FreeBSD"
17
+ | "OpenBSD"
18
+ | "iOS"
19
+ | "Android"
20
+ | `Other:${string}`
21
+ | "Unknown";
22
+
23
+ type Browser = "ie" | "edge" | "chrome" | "firefox" | "safari";
24
+
25
+ type PlatformProperties = {
26
+ os: PlatformName;
27
+ arch: Arch;
28
+ runtime:
29
+ | "node"
30
+ | "deno"
31
+ | "edge"
32
+ | "workerd"
33
+ | `browser:${Browser}`
34
+ | "unknown";
35
+ runtimeVersion: string;
36
+ };
37
+
38
+ export const getPlatformProperties = (): PlatformProperties => {
39
+ if (typeof Deno !== "undefined" && Deno.build != null) {
40
+ return {
41
+ os: normalizePlatform(Deno.build.os),
42
+ arch: normalizeArch(Deno.build.arch),
43
+ runtime: "deno",
44
+ runtimeVersion:
45
+ typeof Deno.version === "string"
46
+ ? Deno.version
47
+ : (Deno.version?.deno ?? "unknown"),
48
+ };
49
+ }
50
+ if (typeof EdgeRuntime !== "undefined") {
51
+ return {
52
+ os: "Unknown",
53
+ arch: `other:${EdgeRuntime}`,
54
+ runtime: "edge",
55
+ runtimeVersion: process.version,
56
+ };
57
+ }
58
+ // Check if Node.js
59
+ if (
60
+ Object.prototype.toString.call(
61
+ typeof process !== "undefined" ? process : 0,
62
+ ) === "[object process]"
63
+ ) {
64
+ return {
65
+ os: normalizePlatform(process.platform),
66
+ arch: normalizeArch(process.arch),
67
+ runtime: "node",
68
+ runtimeVersion: process.version,
69
+ };
70
+ }
71
+
72
+ // https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent
73
+ if (
74
+ typeof navigator !== undefined &&
75
+ navigator.userAgent === "Cloudflare-Workers"
76
+ ) {
77
+ return {
78
+ os: "Unknown",
79
+ arch: "unknown",
80
+ runtime: "workerd",
81
+ runtimeVersion: "unknown",
82
+ };
83
+ }
84
+
85
+ const browserInfo = getBrowserInfo();
86
+ if (browserInfo) {
87
+ return {
88
+ os: "Unknown",
89
+ arch: "unknown",
90
+ runtime: `browser:${browserInfo.browser}`,
91
+ runtimeVersion: browserInfo.version,
92
+ };
93
+ }
94
+
95
+ return {
96
+ os: "Unknown",
97
+ arch: "unknown",
98
+ runtime: "unknown",
99
+ runtimeVersion: "unknown",
100
+ };
101
+ };
102
+
103
+ type BrowserInfo = {
104
+ browser: Browser;
105
+ version: string;
106
+ };
107
+
108
+ // Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
109
+ function getBrowserInfo(): BrowserInfo | null {
110
+ if (typeof navigator === "undefined" || !navigator) {
111
+ return null;
112
+ }
113
+
114
+ // NOTE: The order matters here!
115
+ const browserPatterns = [
116
+ { key: "edge" as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
117
+ { key: "ie" as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
118
+ {
119
+ key: "ie" as const,
120
+ pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/,
121
+ },
122
+ {
123
+ key: "chrome" as const,
124
+ pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/,
125
+ },
126
+ {
127
+ key: "firefox" as const,
128
+ pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/,
129
+ },
130
+ {
131
+ key: "safari" as const,
132
+ pattern:
133
+ /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/,
134
+ },
135
+ ];
136
+
137
+ // Find the FIRST matching browser
138
+ for (const { key, pattern } of browserPatterns) {
139
+ const match = pattern.exec(navigator.userAgent);
140
+ if (match) {
141
+ const major = match[1] || 0;
142
+ const minor = match[2] || 0;
143
+ const patch = match[3] || 0;
144
+
145
+ return { browser: key, version: `${major}.${minor}.${patch}` };
146
+ }
147
+ }
148
+
149
+ return null;
150
+ }
151
+
152
+ const normalizeArch = (arch: string): Arch => {
153
+ // Node docs:
154
+ // - https://nodejs.org/api/process.html#processarch
155
+ // Deno docs:
156
+ // - https://doc.deno.land/deno/stable/~/Deno.build
157
+ if (arch === "x32") return "x32";
158
+ if (arch === "x86_64" || arch === "x64") return "x64";
159
+ if (arch === "arm") return "arm";
160
+ if (arch === "aarch64" || arch === "arm64") return "arm64";
161
+ if (arch) return `other:${arch}`;
162
+ return "unknown";
163
+ };
164
+
165
+ const normalizePlatform = (platform: string): PlatformName => {
166
+ // Node platforms:
167
+ // - https://nodejs.org/api/process.html#processplatform
168
+ // Deno platforms:
169
+ // - https://doc.deno.land/deno/stable/~/Deno.build
170
+ // - https://github.com/denoland/deno/issues/14799
171
+
172
+ platform = platform.toLowerCase();
173
+
174
+ // NOTE: this iOS check is untested and may not work
175
+ // Node does not work natively on IOS, there is a fork at
176
+ // https://github.com/nodejs-mobile/nodejs-mobile
177
+ // however it is unknown at the time of writing how to detect if it is running
178
+ if (platform.includes("ios")) return "iOS";
179
+ if (platform === "android") return "Android";
180
+ if (platform === "darwin") return "MacOS";
181
+ if (platform === "win32") return "Windows";
182
+ if (platform === "freebsd") return "FreeBSD";
183
+ if (platform === "openbsd") return "OpenBSD";
184
+ if (platform === "linux") return "Linux";
185
+ if (platform) return `Other:${platform}`;
186
+ return "Unknown";
187
+ };
@@ -0,0 +1,55 @@
1
+ /** Represents an array item within a `Node` output chunk, specifies the field is an array containing this `item` at the `index`. **/
2
+ type ChunkArrayItem<T = Object> = {
3
+ object: "array.item";
4
+ index: number;
5
+ item: T;
6
+ };
7
+
8
+ /** Helper types for producing the "Chunk" types used in the `NodeDelta` messages */
9
+ type ChunkizeObject<T> = T extends object
10
+ ? { [P in keyof T]: ChunkizeAny<T[P]> }
11
+ : T;
12
+
13
+ type ChunkizeArray<T> = T extends (infer U)[]
14
+ ? ChunkArrayItem<ChunkizeAny<U>>
15
+ : ChunkArrayItem<T>;
16
+
17
+ type ChunkizeAny<T> = T extends (infer U)[]
18
+ ? ChunkizeArray<U>
19
+ : T extends object
20
+ ? ChunkizeObject<T>
21
+ : T;
22
+
23
+ /** Stream message that contains the completed `Node` output */
24
+ type NodeResult<T = Object> = {
25
+ object: "node.result";
26
+ nodeId: string;
27
+ data: T;
28
+ };
29
+
30
+ /** Stream message that contains a chunk of the `Node` output */
31
+ type NodeDelta<T = Object> = {
32
+ object: "node.delta";
33
+ nodeId: string;
34
+ data: ChunkizeAny<T>;
35
+ };
36
+
37
+ /** Stream message when an error happened during a `Node` run. */
38
+ export type NodeError = {
39
+ object: "node.error";
40
+ nodeId: string;
41
+ data: {
42
+ type: string;
43
+ message: string;
44
+ };
45
+ };
46
+
47
+ /** Stream message that contains the completed "Graph" output */
48
+ export type GraphResult<T = Object> = {
49
+ object: "graph.result";
50
+ data: T;
51
+ };
52
+
53
+ export type NodeMessage<T = Object> = NodeResult<T> | NodeDelta<T> | NodeError;
54
+
55
+ export type SSEMessage<T = Object> = NodeMessage | GraphResult<T>;
@@ -0,0 +1,314 @@
1
+ import { SubstrateError, RequestTimeoutError } from "substrate/Error";
2
+ import { VERSION } from "substrate/version";
3
+ import OpenAPIjson from "substrate/openapi.json";
4
+ import { SubstrateResponse } from "substrate/SubstrateResponse";
5
+ import { SubstrateStreamingResponse } from "substrate/SubstrateStreamingResponse";
6
+ import { Node } from "substrate/Node";
7
+ import { Future } from "substrate/Future";
8
+ import { getPlatformProperties } from "substrate/Platform";
9
+ import { deflate } from "pako";
10
+ import { randomString } from "substrate/idGenerator";
11
+
12
+ type Configuration = {
13
+ /**
14
+ * [docs/authentication](https://docs.substrate.run/#authentication)
15
+ */
16
+ apiKey: string | undefined;
17
+
18
+ /**
19
+ * [docs/versioning](https://docs.substrate.run/versioning)
20
+ */
21
+ apiVersion?: string | undefined;
22
+
23
+ baseUrl?: string;
24
+
25
+ /**
26
+ * Request timeout in milliseconds. Default: 5m
27
+ */
28
+ timeout?: number;
29
+
30
+ /**
31
+ * Secrets for third party services.
32
+ */
33
+ secrets?: Secrets;
34
+
35
+ /**
36
+ * Add additional headers to each request. These may override headers set by the Substrate client.
37
+ */
38
+ additionalHeaders?: Record<string, string>;
39
+ };
40
+
41
+ export type Secrets = {
42
+ openai?: string;
43
+ anthropic?: string;
44
+ };
45
+
46
+ /**
47
+ * [docs/introduction](https://docs.substrate.run)
48
+ */
49
+ export class Substrate {
50
+ apiKey: Configuration["apiKey"];
51
+ baseUrl: NonNullable<Configuration["baseUrl"]>;
52
+ apiVersion: NonNullable<Configuration["apiVersion"]>;
53
+ timeout: NonNullable<Configuration["timeout"]>;
54
+ additionalHeaders: NonNullable<Configuration["additionalHeaders"]>;
55
+
56
+ /**
57
+ * Initialize the Substrate SDK.
58
+ */
59
+ constructor({
60
+ apiKey,
61
+ baseUrl,
62
+ apiVersion,
63
+ timeout,
64
+ secrets,
65
+ additionalHeaders,
66
+ }: Configuration) {
67
+ if (!apiKey) {
68
+ console.warn(
69
+ "[Substrate] An API Key is required. Specify it when constructing the client: `new Substrate({ apiKey: 'API_KEY' })`",
70
+ );
71
+ }
72
+ this.apiKey = apiKey;
73
+ this.baseUrl = baseUrl ?? "https://api.substrate.run";
74
+ this.apiVersion = apiVersion ?? OpenAPIjson["info"]["version"];
75
+ this.timeout = timeout ?? 300_000;
76
+ this.additionalHeaders = additionalHeaders ?? {};
77
+ if (secrets) {
78
+ if (secrets.openai) {
79
+ this.additionalHeaders["x-substrate-openai-api-key"] = secrets.openai;
80
+ }
81
+ if (secrets.anthropic) {
82
+ this.additionalHeaders["x-substrate-anthropic-api-key"] =
83
+ secrets.anthropic;
84
+ }
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Run the given nodes.
90
+ *
91
+ * @throws {SubstrateError} when the server response is an error.
92
+ * @throws {RequestTimeoutError} when the client has timed out (Configured by `Substrate.timeout`).
93
+ * @throws {Error} when the client encounters an error making the request.
94
+ */
95
+ async run(...nodes: Node[]): Promise<SubstrateResponse> {
96
+ return this.runSerialized(nodes);
97
+ }
98
+
99
+ /**
100
+ * Stream the given nodes.
101
+ */
102
+ async stream(...nodes: Node[]): Promise<SubstrateStreamingResponse> {
103
+ const serialized = Substrate.serialize(...nodes);
104
+ return this.streamSerialized(serialized);
105
+ }
106
+
107
+ /**
108
+ * Run the given nodes, serialized using `Substrate.serialize`.
109
+ *
110
+ * @throws {SubstrateError} when the server response is an error.
111
+ * @throws {RequestTimeoutError} when the client has timed out (Configured by `Substrate.timeout`).
112
+ * @throws {Error} when the client encounters an error making the request.
113
+ */
114
+ async runSerialized(
115
+ nodes: Node[],
116
+ endpoint: string = "/compose",
117
+ ): Promise<SubstrateResponse> {
118
+ const serialized = Substrate.serialize(...nodes);
119
+ const url = this.baseUrl + endpoint;
120
+ const req = { dag: serialized };
121
+ // NOTE: we're creating the signal this way instead of AbortController.timeout because it is only very
122
+ // recently available on some environments, so this is a bit more supported.
123
+ const abortController = new AbortController();
124
+ const { signal } = abortController;
125
+ const timeout = setTimeout(() => abortController.abort(), this.timeout);
126
+
127
+ const request = new Request(url, this.requestOptions(req, signal));
128
+ const requestId = request.headers.get("x-substrate-request-id");
129
+ try {
130
+ const apiResponse = await fetch(request);
131
+
132
+ if (apiResponse.ok) {
133
+ const json = await apiResponse.json();
134
+ const res = new SubstrateResponse(request, apiResponse, json);
135
+ /** TODO stop setting output on node */
136
+
137
+ // @ts-expect-error (accessing protected)
138
+ for (let node of Substrate.findAllNodes(nodes)) node.response = res;
139
+
140
+ return res;
141
+ } else {
142
+ throw new SubstrateError(
143
+ `[Request failed] status=${apiResponse.status} statusText=${apiResponse.statusText} requestId=${requestId}`,
144
+ );
145
+ }
146
+ } catch (err: unknown) {
147
+ if (err instanceof Error) {
148
+ if (err.name === "AbortError") {
149
+ throw new RequestTimeoutError(
150
+ `Request timed out after ${this.timeout}ms requestId=${requestId}`,
151
+ );
152
+ // TODO: We could propagate timeout errors to nodes too, but I'm
153
+ // not sure yet what might be easier for the user to manage.
154
+ }
155
+ }
156
+ throw err;
157
+ } finally {
158
+ clearTimeout(timeout);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Stream the given nodes, serialized using `Substrate.serialize`.
164
+ */
165
+ async streamSerialized(serialized: any, endpoint: string = "/compose") {
166
+ const url = this.baseUrl + endpoint;
167
+ const req = { dag: serialized };
168
+ const abortController = new AbortController();
169
+ const { signal } = abortController;
170
+ const timeout = setTimeout(() => abortController.abort(), this.timeout);
171
+ const requestOptions = this.requestOptions(req, signal);
172
+
173
+ // Add Streaming Headers
174
+ requestOptions.headers.set("Accept", "text/event-stream");
175
+ requestOptions.headers.set("X-Substrate-Streaming", "1");
176
+
177
+ const request = new Request(url, requestOptions);
178
+ const requestId = request.headers.get("x-substrate-request-id");
179
+ try {
180
+ const response = await fetch(request);
181
+ return await SubstrateStreamingResponse.fromRequestReponse(
182
+ request,
183
+ response,
184
+ );
185
+ } catch (err: unknown) {
186
+ if (err instanceof Error) {
187
+ if (err.name === "AbortError") {
188
+ throw new RequestTimeoutError(
189
+ `Request timed out after ${this.timeout}ms requestId=${requestId}`,
190
+ );
191
+ // TODO: We could propagate timeout errors to nodes too, but I'm
192
+ // not sure yet what might be easier for the user to manage.
193
+ }
194
+ }
195
+ throw err;
196
+ } finally {
197
+ clearTimeout(timeout);
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Return a set of all nodes and their dependent nodes.
203
+ */
204
+ static findAllNodes(fromNodes: Node[]): Set<Node> {
205
+ const allNodes = new Set<Node>();
206
+ for (let node of fromNodes) {
207
+ // @ts-ignore: .references() is protected
208
+ const refs = node.references();
209
+ for (let n of refs.nodes) {
210
+ allNodes.add(n);
211
+ }
212
+ }
213
+ return allNodes;
214
+ }
215
+
216
+ /**
217
+ * Return a set of all futures and their dependent futures.
218
+ */
219
+ static findAllFutures(fromNodes: Node[]): Set<Future<any>> {
220
+ const allFutures = new Set<Future<any>>();
221
+ for (let node of fromNodes) {
222
+ // @ts-ignore: .references() is protected
223
+ const refs = node.references();
224
+ for (let f of refs.futures) {
225
+ allFutures.add(f);
226
+ }
227
+ }
228
+ return allFutures;
229
+ }
230
+
231
+ /**
232
+ * Transform an array of nodes into JSON for the Substrate API
233
+ */
234
+ static serialize(...nodes: Node[]): any {
235
+ const allFutures = this.findAllFutures(nodes);
236
+ const allNodes = this.findAllNodes(nodes);
237
+ const allEdges: Record<string, Set<string>> = {};
238
+ for (let n of allNodes) {
239
+ allEdges[n.id] = new Set<string>();
240
+ for (let d of n.depends) {
241
+ allEdges[n.id]!.add(d.id);
242
+ }
243
+ }
244
+ return {
245
+ nodes: Array.from(allNodes).map((node) => node.toJSON()),
246
+ futures: Array.from(allFutures).map((future) => future.toJSON()),
247
+ edges: Object.keys(allEdges).flatMap((toId: string) => {
248
+ let fromIds: string[] = Array.from(allEdges[toId] as Set<string>);
249
+ return fromIds.map((fromId: string) => [fromId, toId, {}]);
250
+ }),
251
+ initial_args: {}, // @deprecated
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Returns a url to visualize the given nodes.
257
+ */
258
+ static visualize(...nodes: Node[]): string {
259
+ const serialized = this.serialize(...nodes);
260
+ const compressed = deflate(JSON.stringify(serialized), {
261
+ level: 9,
262
+ });
263
+ const numArray = Array.from(compressed);
264
+ const base64 = btoa(String.fromCharCode.apply(null, numArray));
265
+ const urlEncoded = base64
266
+ .replace(/\+/g, "-")
267
+ .replace(/\//g, "_")
268
+ .replace(/=+$/, "");
269
+ const baseURL = "https://explore.substrate.run/s/";
270
+ return baseURL + urlEncoded;
271
+ }
272
+
273
+ protected requestOptions(body: any, signal: AbortSignal) {
274
+ return {
275
+ method: "POST",
276
+ headers: this.headers(),
277
+ body: JSON.stringify(body),
278
+ signal,
279
+ };
280
+ }
281
+
282
+ protected headers() {
283
+ const headers = new Headers();
284
+
285
+ // API
286
+ headers.append("Accept", "application/json");
287
+ headers.append("Content-Type", "application/json");
288
+ headers.append("User-Agent", `APIClient/JS ${VERSION}`);
289
+ headers.append("X-Substrate-Version", this.apiVersion);
290
+ headers.append("X-Substrate-Request-Id", randomString(32));
291
+ headers.append("X-Substrate-Backend", "v1");
292
+
293
+ // Auth
294
+ headers.append("Authorization", `Bearer ${this.apiKey}`);
295
+
296
+ // SDK
297
+ headers.append("X-Substrate-Lang", "js");
298
+ headers.append("X-Substrate-Package-Version", VERSION);
299
+
300
+ // Platform, Runtime
301
+ const props = getPlatformProperties();
302
+ headers.append("X-Substrate-OS", props.os);
303
+ headers.append("X-Substrate-Arch", props.arch);
304
+ headers.append("X-Substrate-Runtime", props.runtime);
305
+ headers.append("X-Substrate-Runtime-Version", props.runtimeVersion);
306
+
307
+ // User-Provided
308
+ for (const [name, value] of Object.entries(this.additionalHeaders)) {
309
+ headers.set(name, value);
310
+ }
311
+
312
+ return headers;
313
+ }
314
+ }
@@ -0,0 +1,41 @@
1
+ import { AnyNode, NodeOutput } from "substrate/Nodes";
2
+ import { NodeError } from "substrate/Error";
3
+
4
+ /**
5
+ * Response to a run request.
6
+ */
7
+ export class SubstrateResponse {
8
+ public apiRequest: Request;
9
+ public apiResponse: Response;
10
+ public json: any;
11
+
12
+ constructor(request: Request, response: Response, json: any = null) {
13
+ this.apiRequest = request;
14
+ this.apiResponse = response;
15
+ this.json = json;
16
+ }
17
+
18
+ get requestId() {
19
+ return this.apiRequest.headers.get("x-substrate-request-id");
20
+ }
21
+
22
+ /**
23
+ * Returns an error from the `Node` if there was one.
24
+ */
25
+ getError<T extends AnyNode>(node: T): NodeError | undefined {
26
+ // @ts-expect-error
27
+ return node.output() instanceof NodeError ? node.output() : undefined;
28
+ }
29
+
30
+ /**
31
+ * Returns the result for given `Node`.
32
+ *
33
+ * @throws {NodeError} when there was an error running the node.
34
+ */
35
+ get<T extends AnyNode>(node: T): NodeOutput<T> {
36
+ const err = this.getError(node);
37
+ if (err) throw err;
38
+ // @ts-expect-error
39
+ return node.output() as NodeOutput<T>;
40
+ }
41
+ }