langgraph-api 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of langgraph-api might be problematic. Click here for more details.
- LICENSE +93 -0
- langgraph_api/__init__.py +0 -0
- langgraph_api/api/__init__.py +63 -0
- langgraph_api/api/assistants.py +326 -0
- langgraph_api/api/meta.py +71 -0
- langgraph_api/api/openapi.py +32 -0
- langgraph_api/api/runs.py +463 -0
- langgraph_api/api/store.py +116 -0
- langgraph_api/api/threads.py +263 -0
- langgraph_api/asyncio.py +201 -0
- langgraph_api/auth/__init__.py +0 -0
- langgraph_api/auth/langsmith/__init__.py +0 -0
- langgraph_api/auth/langsmith/backend.py +67 -0
- langgraph_api/auth/langsmith/client.py +145 -0
- langgraph_api/auth/middleware.py +41 -0
- langgraph_api/auth/noop.py +14 -0
- langgraph_api/cli.py +209 -0
- langgraph_api/config.py +70 -0
- langgraph_api/cron_scheduler.py +60 -0
- langgraph_api/errors.py +52 -0
- langgraph_api/graph.py +314 -0
- langgraph_api/http.py +168 -0
- langgraph_api/http_logger.py +89 -0
- langgraph_api/js/.gitignore +2 -0
- langgraph_api/js/build.mts +49 -0
- langgraph_api/js/client.mts +849 -0
- langgraph_api/js/global.d.ts +6 -0
- langgraph_api/js/package.json +33 -0
- langgraph_api/js/remote.py +673 -0
- langgraph_api/js/server_sent_events.py +126 -0
- langgraph_api/js/src/graph.mts +88 -0
- langgraph_api/js/src/hooks.mjs +12 -0
- langgraph_api/js/src/parser/parser.mts +443 -0
- langgraph_api/js/src/parser/parser.worker.mjs +12 -0
- langgraph_api/js/src/schema/types.mts +2136 -0
- langgraph_api/js/src/schema/types.template.mts +74 -0
- langgraph_api/js/src/utils/importMap.mts +85 -0
- langgraph_api/js/src/utils/pythonSchemas.mts +28 -0
- langgraph_api/js/src/utils/serde.mts +21 -0
- langgraph_api/js/tests/api.test.mts +1566 -0
- langgraph_api/js/tests/compose-postgres.yml +56 -0
- langgraph_api/js/tests/graphs/.gitignore +1 -0
- langgraph_api/js/tests/graphs/agent.mts +127 -0
- langgraph_api/js/tests/graphs/error.mts +17 -0
- langgraph_api/js/tests/graphs/langgraph.json +8 -0
- langgraph_api/js/tests/graphs/nested.mts +44 -0
- langgraph_api/js/tests/graphs/package.json +7 -0
- langgraph_api/js/tests/graphs/weather.mts +57 -0
- langgraph_api/js/tests/graphs/yarn.lock +159 -0
- langgraph_api/js/tests/parser.test.mts +870 -0
- langgraph_api/js/tests/utils.mts +17 -0
- langgraph_api/js/yarn.lock +1340 -0
- langgraph_api/lifespan.py +41 -0
- langgraph_api/logging.py +121 -0
- langgraph_api/metadata.py +101 -0
- langgraph_api/models/__init__.py +0 -0
- langgraph_api/models/run.py +229 -0
- langgraph_api/patch.py +42 -0
- langgraph_api/queue.py +245 -0
- langgraph_api/route.py +118 -0
- langgraph_api/schema.py +190 -0
- langgraph_api/serde.py +124 -0
- langgraph_api/server.py +48 -0
- langgraph_api/sse.py +118 -0
- langgraph_api/state.py +67 -0
- langgraph_api/stream.py +289 -0
- langgraph_api/utils.py +60 -0
- langgraph_api/validation.py +141 -0
- langgraph_api-0.0.1.dist-info/LICENSE +93 -0
- langgraph_api-0.0.1.dist-info/METADATA +26 -0
- langgraph_api-0.0.1.dist-info/RECORD +86 -0
- langgraph_api-0.0.1.dist-info/WHEEL +4 -0
- langgraph_api-0.0.1.dist-info/entry_points.txt +3 -0
- langgraph_license/__init__.py +0 -0
- langgraph_license/middleware.py +21 -0
- langgraph_license/validation.py +11 -0
- langgraph_storage/__init__.py +0 -0
- langgraph_storage/checkpoint.py +94 -0
- langgraph_storage/database.py +190 -0
- langgraph_storage/ops.py +1523 -0
- langgraph_storage/queue.py +108 -0
- langgraph_storage/retry.py +27 -0
- langgraph_storage/store.py +28 -0
- langgraph_storage/ttl_dict.py +54 -0
- logging.json +22 -0
- openapi.json +4304 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Adapted from https://github.com/florimondmanca/httpx-sse"""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator, Iterator
|
|
4
|
+
from contextlib import asynccontextmanager
|
|
5
|
+
from typing import Any, TypedDict
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ServerSentEvent(TypedDict):
|
|
11
|
+
event: str | None
|
|
12
|
+
data: str | None
|
|
13
|
+
id: str | None
|
|
14
|
+
retry: int | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SSEDecoder:
|
|
18
|
+
def __init__(self) -> None:
|
|
19
|
+
self._event = ""
|
|
20
|
+
self._data: list[str] = []
|
|
21
|
+
self._last_event_id = ""
|
|
22
|
+
self._retry: int | None = None
|
|
23
|
+
|
|
24
|
+
def decode(self, line: str) -> ServerSentEvent | None:
|
|
25
|
+
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
|
|
26
|
+
if not line:
|
|
27
|
+
if (
|
|
28
|
+
not self._event
|
|
29
|
+
and not self._data
|
|
30
|
+
and not self._last_event_id
|
|
31
|
+
and self._retry is None
|
|
32
|
+
):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
sse = {
|
|
36
|
+
"event": self._event,
|
|
37
|
+
"data": "\n".join(self._data),
|
|
38
|
+
"id": self._last_event_id,
|
|
39
|
+
"retry": self._retry,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# NOTE: as per the SSE spec, do not reset last_event_id.
|
|
43
|
+
self._event = ""
|
|
44
|
+
self._data = []
|
|
45
|
+
self._retry = None
|
|
46
|
+
|
|
47
|
+
return sse
|
|
48
|
+
|
|
49
|
+
if line.startswith(":"):
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
fieldname, _, value = line.partition(":")
|
|
53
|
+
|
|
54
|
+
if value.startswith(" "):
|
|
55
|
+
value = value[1:]
|
|
56
|
+
|
|
57
|
+
if fieldname == "event":
|
|
58
|
+
self._event = value
|
|
59
|
+
elif fieldname == "data":
|
|
60
|
+
self._data.append(value)
|
|
61
|
+
elif fieldname == "id":
|
|
62
|
+
if "\0" in value:
|
|
63
|
+
pass
|
|
64
|
+
else:
|
|
65
|
+
self._last_event_id = value
|
|
66
|
+
elif fieldname == "retry":
|
|
67
|
+
try:
|
|
68
|
+
self._retry = int(value)
|
|
69
|
+
except (TypeError, ValueError):
|
|
70
|
+
pass
|
|
71
|
+
else:
|
|
72
|
+
pass # Field is ignored.
|
|
73
|
+
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class EventSource:
|
|
78
|
+
def __init__(self, response: httpx.Response) -> None:
|
|
79
|
+
self._response = response
|
|
80
|
+
|
|
81
|
+
def _check_content_type(self) -> None:
|
|
82
|
+
"""Check that the response content type is 'text/event-stream'."""
|
|
83
|
+
self._response.raise_for_status()
|
|
84
|
+
content_type = self._response.headers.get("content-type", "").partition(";")[0]
|
|
85
|
+
if "text/event-stream" not in content_type:
|
|
86
|
+
raise AssertionError(
|
|
87
|
+
"Expected response header Content-Type to contain 'text/event-stream', "
|
|
88
|
+
f"got {content_type!r}"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def response(self) -> httpx.Response:
|
|
93
|
+
return self._response
|
|
94
|
+
|
|
95
|
+
def iter_sse(self) -> Iterator[ServerSentEvent]:
|
|
96
|
+
self._check_content_type()
|
|
97
|
+
decoder = SSEDecoder()
|
|
98
|
+
for line in self._response.iter_lines():
|
|
99
|
+
line = line.rstrip("\n")
|
|
100
|
+
sse = decoder.decode(line)
|
|
101
|
+
if sse is not None:
|
|
102
|
+
yield sse
|
|
103
|
+
|
|
104
|
+
async def aiter_sse(self) -> AsyncIterator[ServerSentEvent]:
|
|
105
|
+
self._check_content_type()
|
|
106
|
+
decoder = SSEDecoder()
|
|
107
|
+
async for line in self._response.aiter_lines():
|
|
108
|
+
line = line.rstrip("\n")
|
|
109
|
+
sse = decoder.decode(line)
|
|
110
|
+
if sse is not None:
|
|
111
|
+
yield sse
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@asynccontextmanager
|
|
115
|
+
async def aconnect_sse(
|
|
116
|
+
client: httpx.AsyncClient,
|
|
117
|
+
method: str,
|
|
118
|
+
url: str,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> AsyncIterator[EventSource]:
|
|
121
|
+
headers = kwargs.pop("headers", {})
|
|
122
|
+
headers["Accept"] = "text/event-stream"
|
|
123
|
+
headers["Cache-Control"] = "no-store"
|
|
124
|
+
|
|
125
|
+
async with client.stream(method, url, headers=headers, **kwargs) as response:
|
|
126
|
+
yield EventSource(response)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
import { register } from "node:module";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import type { CompiledGraph, Graph } from "@langchain/langgraph";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import type { JSONSchema7 } from "json-schema";
|
|
7
|
+
|
|
8
|
+
// enforce API @langchain/langgraph precedence
|
|
9
|
+
register("./hooks.mjs", import.meta.url);
|
|
10
|
+
|
|
11
|
+
export interface GraphSchema {
|
|
12
|
+
state: JSONSchema7 | undefined;
|
|
13
|
+
input: JSONSchema7 | undefined;
|
|
14
|
+
output: JSONSchema7 | undefined;
|
|
15
|
+
config: JSONSchema7 | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GraphSpec {
|
|
19
|
+
sourceFile: string;
|
|
20
|
+
exportSymbol: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveGraph(
|
|
24
|
+
spec: string,
|
|
25
|
+
options?: { onlyFilePresence?: boolean }
|
|
26
|
+
) {
|
|
27
|
+
const [userFile, exportSymbol] = spec.split(":", 2);
|
|
28
|
+
|
|
29
|
+
// validate file exists
|
|
30
|
+
await fs.stat(userFile);
|
|
31
|
+
if (options?.onlyFilePresence) {
|
|
32
|
+
return { sourceFile: userFile, exportSymbol, resolved: undefined };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sourceFile = path.resolve(process.cwd(), userFile);
|
|
36
|
+
type GraphLike = CompiledGraph<string> | Graph<string>;
|
|
37
|
+
|
|
38
|
+
type GraphUnknown =
|
|
39
|
+
| GraphLike
|
|
40
|
+
| Promise<GraphLike>
|
|
41
|
+
| (() => GraphLike | Promise<GraphLike>)
|
|
42
|
+
| undefined;
|
|
43
|
+
|
|
44
|
+
const isGraph = (graph: GraphLike): graph is Graph<string> => {
|
|
45
|
+
if (typeof graph !== "object" || graph == null) return false;
|
|
46
|
+
return "compile" in graph && typeof graph.compile === "function";
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const graph: GraphUnknown = await import(sourceFile).then(
|
|
50
|
+
(module) => module[exportSymbol || "default"]
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// obtain the graph, and if not compiled, compile it
|
|
54
|
+
const resolved: CompiledGraph<string> = await (async () => {
|
|
55
|
+
if (!graph) throw new Error("Failed to load graph: graph is nullush");
|
|
56
|
+
const graphLike = typeof graph === "function" ? await graph() : await graph;
|
|
57
|
+
|
|
58
|
+
if (isGraph(graphLike)) return graphLike.compile();
|
|
59
|
+
return graphLike;
|
|
60
|
+
})();
|
|
61
|
+
|
|
62
|
+
return { sourceFile, exportSymbol, resolved };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function runGraphSchemaWorker(spec: GraphSpec) {
|
|
66
|
+
const SCHEMA_RESOLVE_TIMEOUT_MS = 30_000;
|
|
67
|
+
|
|
68
|
+
return await new Promise<Record<string, GraphSchema>>((resolve, reject) => {
|
|
69
|
+
const worker = new Worker(
|
|
70
|
+
new URL("./parser/parser.worker.mjs", import.meta.url).pathname
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// Set a timeout to reject if the worker takes too long
|
|
74
|
+
const timeoutId = setTimeout(() => {
|
|
75
|
+
worker.terminate();
|
|
76
|
+
reject(new Error("Schema extract worker timed out"));
|
|
77
|
+
}, SCHEMA_RESOLVE_TIMEOUT_MS);
|
|
78
|
+
|
|
79
|
+
worker.on("message", (result) => {
|
|
80
|
+
worker.terminate();
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
resolve(result);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
worker.on("error", reject);
|
|
86
|
+
worker.postMessage(spec);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// This hook is to ensure that @langchain/langgraph package
|
|
2
|
+
// found in /api folder has precendence compared to user-provided package
|
|
3
|
+
// found in /deps. Does not attempt to semver check for too old packages.
|
|
4
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
5
|
+
const parentURL = new URL("./graph.mts", import.meta.url).toString();
|
|
6
|
+
|
|
7
|
+
if (specifier.startsWith("@langchain/langgraph")) {
|
|
8
|
+
return nextResolve(specifier, { ...context, parentURL });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return nextResolve(specifier, context);
|
|
12
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
import * as ts from "typescript";
|
|
2
|
+
import * as vfs from "@typescript/vfs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import dedent from "dedent";
|
|
5
|
+
import { buildGenerator } from "../schema/types.mts";
|
|
6
|
+
|
|
7
|
+
const __dirname = new URL(".", import.meta.url).pathname;
|
|
8
|
+
|
|
9
|
+
const compilerOptions = {
|
|
10
|
+
noEmit: true,
|
|
11
|
+
strict: true,
|
|
12
|
+
allowUnusedLabels: true,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class SubgraphExtractor {
|
|
16
|
+
protected program: ts.Program;
|
|
17
|
+
protected checker: ts.TypeChecker;
|
|
18
|
+
protected sourceFile: ts.SourceFile;
|
|
19
|
+
protected inferFile: ts.SourceFile;
|
|
20
|
+
|
|
21
|
+
protected anyPregelType: ts.Type;
|
|
22
|
+
protected anyGraphType: ts.Type;
|
|
23
|
+
|
|
24
|
+
protected strict: boolean;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
program: ts.Program,
|
|
28
|
+
sourceFile: ts.SourceFile,
|
|
29
|
+
inferFile: ts.SourceFile,
|
|
30
|
+
options?: { strict?: boolean }
|
|
31
|
+
) {
|
|
32
|
+
this.program = program;
|
|
33
|
+
this.sourceFile = sourceFile;
|
|
34
|
+
this.inferFile = inferFile;
|
|
35
|
+
|
|
36
|
+
this.checker = program.getTypeChecker();
|
|
37
|
+
this.strict = options?.strict ?? false;
|
|
38
|
+
|
|
39
|
+
this.anyPregelType = this.findTypeByName("AnyPregel");
|
|
40
|
+
this.anyGraphType = this.findTypeByName("AnyGraph");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private findTypeByName = (needle: string) => {
|
|
44
|
+
let result: ts.Type | undefined = undefined;
|
|
45
|
+
|
|
46
|
+
const visit = (node: ts.Node) => {
|
|
47
|
+
if (ts.isTypeAliasDeclaration(node)) {
|
|
48
|
+
const symbol = (node as any).symbol;
|
|
49
|
+
|
|
50
|
+
if (symbol != null) {
|
|
51
|
+
const name = this.checker
|
|
52
|
+
.getFullyQualifiedName(symbol)
|
|
53
|
+
.replace(/".*"\./, "");
|
|
54
|
+
if (name === needle) result = this.checker.getTypeAtLocation(node);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (result == null) ts.forEachChild(node, visit);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
ts.forEachChild(this.inferFile, visit);
|
|
61
|
+
if (!result) throw new Error(`Failed to find "${needle}" type`);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
private find = (
|
|
66
|
+
root: ts.Node,
|
|
67
|
+
predicate: (node: ts.Node) => boolean
|
|
68
|
+
): ts.Node | undefined => {
|
|
69
|
+
let result: ts.Node | undefined = undefined;
|
|
70
|
+
|
|
71
|
+
const visit = (node: ts.Node) => {
|
|
72
|
+
if (predicate(node)) {
|
|
73
|
+
result = node;
|
|
74
|
+
} else {
|
|
75
|
+
ts.forEachChild(node, visit);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
if (predicate(root)) return root;
|
|
80
|
+
ts.forEachChild(root, visit);
|
|
81
|
+
return result;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
protected findSubgraphs = (
|
|
85
|
+
node: ts.Node,
|
|
86
|
+
namespace: string[] = []
|
|
87
|
+
): {
|
|
88
|
+
node: string;
|
|
89
|
+
namespace: string[];
|
|
90
|
+
subgraph: { name: string; node: ts.Node };
|
|
91
|
+
}[] => {
|
|
92
|
+
const findAllAddNodeCalls = (
|
|
93
|
+
acc: {
|
|
94
|
+
node: string;
|
|
95
|
+
namespace: string[];
|
|
96
|
+
subgraph: { name: string; node: ts.Node };
|
|
97
|
+
}[],
|
|
98
|
+
node: ts.Node
|
|
99
|
+
) => {
|
|
100
|
+
if (ts.isCallExpression(node)) {
|
|
101
|
+
const firstChild = node.getChildAt(0);
|
|
102
|
+
|
|
103
|
+
if (
|
|
104
|
+
ts.isPropertyAccessExpression(firstChild) &&
|
|
105
|
+
this.getText(firstChild.name) === "addNode"
|
|
106
|
+
) {
|
|
107
|
+
let nodeName: string = "unknown";
|
|
108
|
+
let variables: { node: ts.Node; name: string }[] = [];
|
|
109
|
+
|
|
110
|
+
const [subgraphNode, callArg] = node.arguments;
|
|
111
|
+
|
|
112
|
+
if (subgraphNode && ts.isStringLiteralLike(subgraphNode)) {
|
|
113
|
+
nodeName = this.getText(subgraphNode);
|
|
114
|
+
if (
|
|
115
|
+
(nodeName.startsWith(`"`) && nodeName.endsWith(`"`)) ||
|
|
116
|
+
(nodeName.startsWith(`'`) && nodeName.endsWith(`'`))
|
|
117
|
+
) {
|
|
118
|
+
nodeName = nodeName.slice(1, -1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (callArg) {
|
|
123
|
+
if (
|
|
124
|
+
ts.isFunctionLike(callArg) ||
|
|
125
|
+
ts.isCallLikeExpression(callArg)
|
|
126
|
+
) {
|
|
127
|
+
variables = this.reduceChildren(
|
|
128
|
+
callArg,
|
|
129
|
+
this.findSubgraphIdentifiers,
|
|
130
|
+
[]
|
|
131
|
+
);
|
|
132
|
+
} else if (ts.isIdentifier(callArg)) {
|
|
133
|
+
variables = this.findSubgraphIdentifiers([], callArg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (variables.length > 0) {
|
|
138
|
+
if (variables.length > 1) {
|
|
139
|
+
const targetName = [...namespace, nodeName].join("|");
|
|
140
|
+
const errMsg = `Multiple unique subgraph invocations found for "${targetName}"`;
|
|
141
|
+
if (this.strict) throw new Error(errMsg);
|
|
142
|
+
console.warn(errMsg);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
acc.push({
|
|
146
|
+
namespace: namespace,
|
|
147
|
+
node: nodeName,
|
|
148
|
+
subgraph: variables[0],
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return acc;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
let subgraphs = this.reduceChildren(node, findAllAddNodeCalls, []);
|
|
158
|
+
|
|
159
|
+
// TODO: make this more strict, only traverse the flow graph only
|
|
160
|
+
// if no `addNode` calls were found
|
|
161
|
+
if (!subgraphs.length) {
|
|
162
|
+
// internal property, however relied upon by ts-ast-viewer et all
|
|
163
|
+
// so that we don't need to traverse the control flow ourselves
|
|
164
|
+
// https://github.com/microsoft/TypeScript/pull/58036
|
|
165
|
+
type InternalFlowNode = ts.Node & { flowNode?: { node: ts.Node } };
|
|
166
|
+
const candidate = this.find(
|
|
167
|
+
node,
|
|
168
|
+
(node: any) => node && "flowNode" in node && node.flowNode
|
|
169
|
+
) as InternalFlowNode | undefined;
|
|
170
|
+
|
|
171
|
+
if (
|
|
172
|
+
candidate?.flowNode &&
|
|
173
|
+
this.isGraphOrPregelType(
|
|
174
|
+
this.checker.getTypeAtLocation(candidate.flowNode.node)
|
|
175
|
+
)
|
|
176
|
+
) {
|
|
177
|
+
subgraphs = this.findSubgraphs(candidate.flowNode.node, namespace);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// handle recursive behaviour
|
|
182
|
+
if (subgraphs.length > 0) {
|
|
183
|
+
return [
|
|
184
|
+
...subgraphs,
|
|
185
|
+
...subgraphs.map(({ subgraph, node }) =>
|
|
186
|
+
this.findSubgraphs(subgraph.node, [...namespace, node])
|
|
187
|
+
),
|
|
188
|
+
].flat();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return subgraphs;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
protected getSubgraphsVariables = (name: string) => {
|
|
195
|
+
const sourceSymbol = this.checker.getSymbolAtLocation(this.sourceFile)!;
|
|
196
|
+
const exports = this.checker.getExportsOfModule(sourceSymbol);
|
|
197
|
+
|
|
198
|
+
const targetExport = exports.find((item) => item.name === name);
|
|
199
|
+
if (!targetExport) throw new Error(`Failed to find export "${name}"`);
|
|
200
|
+
const varDecls = (targetExport.declarations ?? []).filter(
|
|
201
|
+
ts.isVariableDeclaration
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return varDecls.flatMap((varDecl) => {
|
|
205
|
+
if (!varDecl.initializer) return [];
|
|
206
|
+
return this.findSubgraphs(varDecl.initializer, [name]);
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
public getAugmentedSourceFile = (
|
|
211
|
+
name: string
|
|
212
|
+
): {
|
|
213
|
+
files: [filePath: string, contents: string][];
|
|
214
|
+
exports: { typeName: string; valueName: string; graphName: string }[];
|
|
215
|
+
} => {
|
|
216
|
+
const vars = this.getSubgraphsVariables(name);
|
|
217
|
+
type TypeExport = {
|
|
218
|
+
typeName: `__langgraph__${string}`;
|
|
219
|
+
valueName: string;
|
|
220
|
+
graphName: string;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const typeExports: TypeExport[] = [
|
|
224
|
+
{ typeName: `__langgraph__${name}`, valueName: name, graphName: name },
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
for (const { subgraph, node, namespace } of vars) {
|
|
228
|
+
typeExports.push({
|
|
229
|
+
typeName: `__langgraph__${namespace.join("_")}_${node}`,
|
|
230
|
+
valueName: subgraph.name,
|
|
231
|
+
graphName: [...namespace, node].join("|"),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const sourceFilePath = "__langgraph__source.mts";
|
|
236
|
+
const sourceContents = [
|
|
237
|
+
this.getText(this.sourceFile),
|
|
238
|
+
...typeExports.map(
|
|
239
|
+
({ typeName, valueName }) =>
|
|
240
|
+
`export type ${typeName} = typeof ${valueName}`
|
|
241
|
+
),
|
|
242
|
+
].join("\n\n");
|
|
243
|
+
|
|
244
|
+
const inferFilePath = "__langraph__infer.mts";
|
|
245
|
+
const inferContents = [
|
|
246
|
+
...typeExports.map(
|
|
247
|
+
({ typeName }) =>
|
|
248
|
+
`import type { ${typeName}} from "./__langgraph__source.mts"`
|
|
249
|
+
),
|
|
250
|
+
this.inferFile.getText(this.inferFile),
|
|
251
|
+
|
|
252
|
+
...typeExports.flatMap(({ typeName }) => {
|
|
253
|
+
return [
|
|
254
|
+
dedent`
|
|
255
|
+
type ${typeName}__reflect = Reflect<${typeName}>;
|
|
256
|
+
export type ${typeName}__state = Inspect<${typeName}__reflect["state"]>;
|
|
257
|
+
export type ${typeName}__update = Inspect<${typeName}__reflect["update"]>;
|
|
258
|
+
|
|
259
|
+
type ${typeName}__builder = BuilderReflect<${typeName}>;
|
|
260
|
+
export type ${typeName}__input = Inspect<FilterAny<${typeName}__builder["input"]>>;
|
|
261
|
+
export type ${typeName}__output = Inspect<FilterAny<${typeName}__builder["output"]>>;
|
|
262
|
+
export type ${typeName}__config = Inspect<FilterAny<${typeName}__builder["config"]>>;
|
|
263
|
+
`,
|
|
264
|
+
];
|
|
265
|
+
}),
|
|
266
|
+
].join("\n\n");
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
files: [
|
|
270
|
+
[sourceFilePath, sourceContents],
|
|
271
|
+
[inferFilePath, inferContents],
|
|
272
|
+
],
|
|
273
|
+
exports: typeExports,
|
|
274
|
+
};
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
protected findSubgraphIdentifiers = (
|
|
278
|
+
acc: { node: ts.Node; name: string }[],
|
|
279
|
+
node: ts.Node
|
|
280
|
+
) => {
|
|
281
|
+
if (ts.isIdentifier(node)) {
|
|
282
|
+
const smb = this.checker.getSymbolAtLocation(node);
|
|
283
|
+
|
|
284
|
+
if (
|
|
285
|
+
smb?.valueDeclaration &&
|
|
286
|
+
ts.isVariableDeclaration(smb.valueDeclaration)
|
|
287
|
+
) {
|
|
288
|
+
const target = smb.valueDeclaration;
|
|
289
|
+
const targetType = this.checker.getTypeAtLocation(target);
|
|
290
|
+
|
|
291
|
+
if (this.isGraphOrPregelType(targetType)) {
|
|
292
|
+
acc.push({ name: this.getText(target.name), node: target });
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (smb?.declarations) {
|
|
297
|
+
const target = smb.declarations.find(ts.isImportSpecifier);
|
|
298
|
+
if (target) {
|
|
299
|
+
const targetType = this.checker.getTypeAtLocation(target);
|
|
300
|
+
if (this.isGraphOrPregelType(targetType)) {
|
|
301
|
+
acc.push({ name: this.getText(target.name), node: target });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return acc;
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
protected isGraphOrPregelType = (type: ts.Type) => {
|
|
311
|
+
return (
|
|
312
|
+
this.checker.isTypeAssignableTo(type, this.anyPregelType) ||
|
|
313
|
+
this.checker.isTypeAssignableTo(type, this.anyGraphType)
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
protected getText(node: ts.Node) {
|
|
318
|
+
return node.getText(this.sourceFile);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
protected reduceChildren<Acc>(
|
|
322
|
+
node: ts.Node,
|
|
323
|
+
fn: (acc: Acc, node: ts.Node) => Acc,
|
|
324
|
+
initial: Acc
|
|
325
|
+
): Acc {
|
|
326
|
+
let acc = initial;
|
|
327
|
+
function it(node: ts.Node) {
|
|
328
|
+
acc = fn(acc, node);
|
|
329
|
+
ts.forEachChild(node, it.bind(this));
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
ts.forEachChild(node, it.bind(this));
|
|
333
|
+
return acc;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
static extractSchemas(
|
|
337
|
+
target:
|
|
338
|
+
| string
|
|
339
|
+
| {
|
|
340
|
+
contents: string;
|
|
341
|
+
files?: [fileName: string, contents: string][];
|
|
342
|
+
},
|
|
343
|
+
name: string,
|
|
344
|
+
options?: { strict?: boolean }
|
|
345
|
+
) {
|
|
346
|
+
const dirname =
|
|
347
|
+
typeof target === "string" ? path.dirname(target) : __dirname;
|
|
348
|
+
|
|
349
|
+
const fsMap = new Map<string, string>();
|
|
350
|
+
const system = vfs.createFSBackedSystem(fsMap, dirname, ts);
|
|
351
|
+
const host = vfs.createVirtualCompilerHost(system, compilerOptions, ts);
|
|
352
|
+
|
|
353
|
+
const targetPath =
|
|
354
|
+
typeof target === "string"
|
|
355
|
+
? target
|
|
356
|
+
: path.resolve(dirname, "./__langgraph__target.mts");
|
|
357
|
+
|
|
358
|
+
const inferTemplatePath = path.resolve(
|
|
359
|
+
__dirname,
|
|
360
|
+
"../schema/types.template.mts"
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
if (typeof target !== "string") {
|
|
364
|
+
fsMap.set(targetPath, target.contents);
|
|
365
|
+
for (const [name, contents] of target.files ?? []) {
|
|
366
|
+
fsMap.set(path.resolve(dirname, name), contents);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
host.compilerHost.resolveModuleNames = (moduleNames, containingFile) => {
|
|
371
|
+
const resolvedModules: (ts.ResolvedModule | undefined)[] = [];
|
|
372
|
+
for (const moduleName of moduleNames) {
|
|
373
|
+
let target = containingFile;
|
|
374
|
+
const relative = path.relative(dirname, containingFile);
|
|
375
|
+
if (
|
|
376
|
+
moduleName.startsWith("@langchain/langgraph") &&
|
|
377
|
+
relative &&
|
|
378
|
+
!relative.startsWith("..") &&
|
|
379
|
+
!path.isAbsolute(relative)
|
|
380
|
+
) {
|
|
381
|
+
target = path.resolve(__dirname, relative);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
resolvedModules.push(
|
|
385
|
+
ts.resolveModuleName(
|
|
386
|
+
moduleName,
|
|
387
|
+
target,
|
|
388
|
+
compilerOptions,
|
|
389
|
+
host.compilerHost
|
|
390
|
+
).resolvedModule
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return resolvedModules;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const research = ts.createProgram({
|
|
398
|
+
rootNames: [inferTemplatePath, targetPath],
|
|
399
|
+
options: compilerOptions,
|
|
400
|
+
host: host.compilerHost,
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const extractor = new SubgraphExtractor(
|
|
404
|
+
research,
|
|
405
|
+
research.getSourceFile(targetPath)!,
|
|
406
|
+
research.getSourceFile(inferTemplatePath)!,
|
|
407
|
+
options
|
|
408
|
+
);
|
|
409
|
+
|
|
410
|
+
const { files, exports } = extractor.getAugmentedSourceFile(name);
|
|
411
|
+
for (const [name, source] of files) {
|
|
412
|
+
system.writeFile(path.resolve(dirname, name), source);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const extract = ts.createProgram({
|
|
416
|
+
rootNames: [path.resolve(dirname, "./__langraph__infer.mts")],
|
|
417
|
+
options: compilerOptions,
|
|
418
|
+
host: host.compilerHost,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const schemaGenerator = buildGenerator(extract);
|
|
422
|
+
const trySymbol = (schema: typeof schemaGenerator, symbol: string) => {
|
|
423
|
+
try {
|
|
424
|
+
return schema?.getSchemaForSymbol(symbol) ?? undefined;
|
|
425
|
+
} catch (e) {
|
|
426
|
+
console.warn(`Failed to obtain symbol "${symbol}":`, e?.message);
|
|
427
|
+
}
|
|
428
|
+
return undefined;
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
return Object.fromEntries(
|
|
432
|
+
exports.map(({ typeName, graphName }) => [
|
|
433
|
+
graphName,
|
|
434
|
+
{
|
|
435
|
+
input: trySymbol(schemaGenerator, `${typeName}__input`),
|
|
436
|
+
output: trySymbol(schemaGenerator, `${typeName}__output`),
|
|
437
|
+
state: trySymbol(schemaGenerator, `${typeName}__update`),
|
|
438
|
+
config: trySymbol(schemaGenerator, `${typeName}__config`),
|
|
439
|
+
},
|
|
440
|
+
])
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { tsImport } from "tsx/esm/api";
|
|
2
|
+
import { parentPort } from "node:worker_threads";
|
|
3
|
+
|
|
4
|
+
parentPort?.on("message", async (payload) => {
|
|
5
|
+
const { SubgraphExtractor } = await tsImport("./parser.mts", import.meta.url);
|
|
6
|
+
const result = SubgraphExtractor.extractSchemas(
|
|
7
|
+
payload.sourceFile,
|
|
8
|
+
payload.exportSymbol,
|
|
9
|
+
{ strict: false }
|
|
10
|
+
);
|
|
11
|
+
parentPort?.postMessage(result);
|
|
12
|
+
});
|