langgraph-api 0.0.24__py3-none-any.whl → 0.0.26__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.
- langgraph_api/api/__init__.py +81 -7
- langgraph_api/api/openapi.py +246 -1
- langgraph_api/auth/custom.py +2 -0
- langgraph_api/cli.py +9 -2
- langgraph_api/config.py +37 -3
- langgraph_api/js/remote_new.py +1 -0
- langgraph_api/js/remote_old.py +1 -0
- langgraph_api/server.py +99 -24
- langgraph_api/utils.py +29 -0
- {langgraph_api-0.0.24.dist-info → langgraph_api-0.0.26.dist-info}/METADATA +3 -3
- {langgraph_api-0.0.24.dist-info → langgraph_api-0.0.26.dist-info}/RECORD +14 -14
- {langgraph_api-0.0.24.dist-info → langgraph_api-0.0.26.dist-info}/LICENSE +0 -0
- {langgraph_api-0.0.24.dist-info → langgraph_api-0.0.26.dist-info}/WHEEL +0 -0
- {langgraph_api-0.0.24.dist-info → langgraph_api-0.0.26.dist-info}/entry_points.txt +0 -0
langgraph_api/api/__init__.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
2
5
|
|
|
6
|
+
import structlog
|
|
7
|
+
from starlette.applications import Starlette
|
|
3
8
|
from starlette.requests import Request
|
|
4
9
|
from starlette.responses import HTMLResponse, JSONResponse, Response
|
|
5
10
|
from starlette.routing import BaseRoute, Mount, Route
|
|
@@ -11,11 +16,13 @@ from langgraph_api.api.runs import runs_routes
|
|
|
11
16
|
from langgraph_api.api.store import store_routes
|
|
12
17
|
from langgraph_api.api.threads import threads_routes
|
|
13
18
|
from langgraph_api.auth.middleware import auth_middleware
|
|
14
|
-
from langgraph_api.config import MIGRATIONS_PATH
|
|
19
|
+
from langgraph_api.config import HTTP_CONFIG, MIGRATIONS_PATH
|
|
15
20
|
from langgraph_api.graph import js_bg_tasks
|
|
16
21
|
from langgraph_api.validation import DOCS_HTML
|
|
17
22
|
from langgraph_storage.database import connect, healthcheck
|
|
18
23
|
|
|
24
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
async def ok(request: Request):
|
|
21
28
|
check_db = int(request.query_params.get("check_db", "0")) # must be "0" or "1"
|
|
@@ -37,19 +44,86 @@ async def docs(request: Request):
|
|
|
37
44
|
return HTMLResponse(DOCS_HTML)
|
|
38
45
|
|
|
39
46
|
|
|
40
|
-
|
|
47
|
+
meta_routes: list[BaseRoute] = [
|
|
41
48
|
Route("/ok", ok, methods=["GET"]),
|
|
42
49
|
Route("/openapi.json", openapi, methods=["GET"]),
|
|
43
50
|
Route("/docs", docs, methods=["GET"]),
|
|
44
51
|
Route("/info", meta_info, methods=["GET"]),
|
|
45
52
|
Route("/metrics", meta_metrics, methods=["GET"]),
|
|
46
|
-
Mount(
|
|
47
|
-
"",
|
|
48
|
-
middleware=[auth_middleware],
|
|
49
|
-
routes=[*assistants_routes, *runs_routes, *threads_routes, *store_routes],
|
|
50
|
-
),
|
|
51
53
|
]
|
|
52
54
|
|
|
55
|
+
protected_routes: list[BaseRoute] = []
|
|
56
|
+
|
|
57
|
+
if HTTP_CONFIG:
|
|
58
|
+
if not HTTP_CONFIG.get("disable_assistants"):
|
|
59
|
+
protected_routes.extend(assistants_routes)
|
|
60
|
+
if not HTTP_CONFIG.get("disable_runs"):
|
|
61
|
+
protected_routes.extend(runs_routes)
|
|
62
|
+
if not HTTP_CONFIG.get("disable_threads"):
|
|
63
|
+
protected_routes.extend(threads_routes)
|
|
64
|
+
if not HTTP_CONFIG.get("disable_store"):
|
|
65
|
+
protected_routes.extend(store_routes)
|
|
66
|
+
else:
|
|
67
|
+
protected_routes.extend(assistants_routes)
|
|
68
|
+
protected_routes.extend(runs_routes)
|
|
69
|
+
protected_routes.extend(threads_routes)
|
|
70
|
+
protected_routes.extend(store_routes)
|
|
71
|
+
|
|
72
|
+
routes: list[BaseRoute] = []
|
|
73
|
+
user_router = None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def load_custom_app(app_import: str) -> Starlette | None:
|
|
77
|
+
# Expect a string in either "path/to/file.py:my_variable" or "some.module.in:my_variable"
|
|
78
|
+
logger.info(f"Loading custom app from {app_import}")
|
|
79
|
+
path, name = app_import.rsplit(":", 1)
|
|
80
|
+
try:
|
|
81
|
+
os.environ["__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT"] = "true"
|
|
82
|
+
if os.path.isfile(path) or path.endswith(".py"):
|
|
83
|
+
# Import from file path using a unique module name.
|
|
84
|
+
spec = importlib.util.spec_from_file_location("user_router_module", path)
|
|
85
|
+
if spec is None or spec.loader is None:
|
|
86
|
+
raise ImportError(f"Cannot load spec from {path}")
|
|
87
|
+
module = importlib.util.module_from_spec(spec)
|
|
88
|
+
spec.loader.exec_module(module)
|
|
89
|
+
else:
|
|
90
|
+
# Import as a normal module.
|
|
91
|
+
module = importlib.import_module(path)
|
|
92
|
+
user_router = getattr(module, name)
|
|
93
|
+
if not isinstance(user_router, Starlette):
|
|
94
|
+
raise TypeError(
|
|
95
|
+
f"Object '{name}' in module '{path}' is not a Starlette or FastAPI application. "
|
|
96
|
+
"Please initialize your app by importing and using the appropriate class: "
|
|
97
|
+
"\nfrom starlette.applications import Starlette\n\napp = Starlette(...)\n\n"
|
|
98
|
+
"or\n\nfrom fastapi import FastAPI\n\napp = FastAPI(...)\n\n"
|
|
99
|
+
)
|
|
100
|
+
except ImportError as e:
|
|
101
|
+
raise ImportError(f"Failed to import app module '{path}'") from e
|
|
102
|
+
except AttributeError as e:
|
|
103
|
+
raise AttributeError(f"App '{name}' not found in module '{path}'") from e
|
|
104
|
+
finally:
|
|
105
|
+
os.environ.pop("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT", None)
|
|
106
|
+
return user_router
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
if HTTP_CONFIG:
|
|
110
|
+
if router_import := HTTP_CONFIG.get("app"):
|
|
111
|
+
user_router = load_custom_app(router_import)
|
|
112
|
+
if not HTTP_CONFIG.get("disable_meta"):
|
|
113
|
+
routes.extend(meta_routes)
|
|
114
|
+
if protected_routes:
|
|
115
|
+
routes.append(
|
|
116
|
+
Mount(
|
|
117
|
+
"/",
|
|
118
|
+
middleware=[auth_middleware],
|
|
119
|
+
routes=protected_routes,
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
routes.extend(meta_routes)
|
|
125
|
+
routes.append(Mount("/", middleware=[auth_middleware], routes=protected_routes))
|
|
126
|
+
|
|
53
127
|
|
|
54
128
|
if "inmem" in MIGRATIONS_PATH:
|
|
55
129
|
|
langgraph_api/api/openapi.py
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import copy
|
|
1
2
|
import logging
|
|
3
|
+
import typing
|
|
2
4
|
from functools import lru_cache
|
|
3
5
|
|
|
4
6
|
import orjson
|
|
@@ -9,6 +11,13 @@ from langgraph_api.validation import openapi
|
|
|
9
11
|
|
|
10
12
|
logger = logging.getLogger(__name__)
|
|
11
13
|
|
|
14
|
+
CUSTOM_OPENAPI_SPEC = None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def set_custom_spec(spec: dict):
|
|
18
|
+
global CUSTOM_OPENAPI_SPEC
|
|
19
|
+
CUSTOM_OPENAPI_SPEC = spec
|
|
20
|
+
|
|
12
21
|
|
|
13
22
|
@lru_cache(maxsize=1)
|
|
14
23
|
def get_openapi_spec() -> str:
|
|
@@ -66,4 +75,240 @@ def get_openapi_spec() -> str:
|
|
|
66
75
|
"API documentation will not show authentication requirements. "
|
|
67
76
|
"Add 'openapi' section to auth section of your `langgraph.json` file to specify security schemes."
|
|
68
77
|
)
|
|
69
|
-
|
|
78
|
+
final = openapi
|
|
79
|
+
if CUSTOM_OPENAPI_SPEC:
|
|
80
|
+
final = merge_openapi_specs(openapi, CUSTOM_OPENAPI_SPEC)
|
|
81
|
+
return orjson.dumps(final)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def merge_openapi_specs(spec_a: dict, spec_b: dict) -> dict:
|
|
85
|
+
"""
|
|
86
|
+
Merge two OpenAPI specifications with spec_b taking precedence on conflicts.
|
|
87
|
+
|
|
88
|
+
This function handles merging of the following keys:
|
|
89
|
+
- "openapi": Uses spec_b’s version.
|
|
90
|
+
- "info": Merges dictionaries with spec_b taking precedence.
|
|
91
|
+
- "servers": Merges lists with deduplication (by URL and description).
|
|
92
|
+
- "paths": For shared paths, merges HTTP methods:
|
|
93
|
+
- If a method exists in both, spec_b’s definition wins.
|
|
94
|
+
- Otherwise, methods from both are preserved.
|
|
95
|
+
Additionally, merges path-level "parameters" by (name, in).
|
|
96
|
+
- "components": Merges per component type (schemas, responses, etc.).
|
|
97
|
+
- "security" and "tags": Merges lists with deduplication using a key function.
|
|
98
|
+
- "externalDocs" and any additional keys: spec_b wins.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
spec_a (dict): First OpenAPI specification.
|
|
102
|
+
spec_b (dict): Second OpenAPI specification (takes precedence).
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
dict: The merged OpenAPI specification.
|
|
106
|
+
|
|
107
|
+
Raises:
|
|
108
|
+
TypeError: If either input is not a dict.
|
|
109
|
+
ValueError: If a required field (openapi, info, paths) is missing.
|
|
110
|
+
"""
|
|
111
|
+
if not isinstance(spec_a, dict) or not isinstance(spec_b, dict):
|
|
112
|
+
raise TypeError("Both specifications must be dictionaries.")
|
|
113
|
+
|
|
114
|
+
required_fields = {"openapi", "info", "paths"}
|
|
115
|
+
for spec in (spec_a, spec_b):
|
|
116
|
+
missing = required_fields - spec.keys()
|
|
117
|
+
if missing:
|
|
118
|
+
raise ValueError(f"Missing required OpenAPI fields: {missing}")
|
|
119
|
+
|
|
120
|
+
merged = copy.deepcopy(spec_a)
|
|
121
|
+
|
|
122
|
+
if "openapi" in spec_b:
|
|
123
|
+
merged["openapi"] = spec_b["openapi"]
|
|
124
|
+
|
|
125
|
+
# Merge "info": Combine dictionaries with spec_b overriding spec_a.
|
|
126
|
+
merged["info"] = {**merged.get("info", {}), **spec_b.get("info", {})}
|
|
127
|
+
|
|
128
|
+
# Merge "servers": Use deduplication based on (url, description).
|
|
129
|
+
merged["servers"] = _merge_lists(
|
|
130
|
+
merged.get("servers", []),
|
|
131
|
+
spec_b.get("servers", []),
|
|
132
|
+
key_func=lambda x: (x.get("url"), x.get("description")),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
# Merge "paths": Merge individual paths and methods.
|
|
136
|
+
merged["paths"] = _merge_paths(merged.get("paths", {}), spec_b.get("paths", {}))
|
|
137
|
+
|
|
138
|
+
# Merge "components": Merge per component type.
|
|
139
|
+
merged["components"] = _merge_components(
|
|
140
|
+
merged.get("components", {}), spec_b.get("components", {})
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# Merge "security": Merge lists with deduplication.
|
|
144
|
+
merged["security"] = _merge_lists(
|
|
145
|
+
merged.get("security", []),
|
|
146
|
+
spec_b.get("security", []),
|
|
147
|
+
key_func=lambda x: tuple(sorted(x.items())),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Merge "tags": Deduplicate tags by "name".
|
|
151
|
+
merged["tags"] = _merge_lists(
|
|
152
|
+
merged.get("tags", []), spec_b.get("tags", []), key_func=lambda x: x.get("name")
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Merge "externalDocs": Use spec_b if provided.
|
|
156
|
+
if "externalDocs" in spec_b:
|
|
157
|
+
merged["externalDocs"] = spec_b["externalDocs"]
|
|
158
|
+
|
|
159
|
+
# Merge any additional keys not explicitly handled.
|
|
160
|
+
handled_keys = {
|
|
161
|
+
"openapi",
|
|
162
|
+
"info",
|
|
163
|
+
"servers",
|
|
164
|
+
"paths",
|
|
165
|
+
"components",
|
|
166
|
+
"security",
|
|
167
|
+
"tags",
|
|
168
|
+
"externalDocs",
|
|
169
|
+
}
|
|
170
|
+
for key in set(spec_a.keys()).union(spec_b.keys()) - handled_keys:
|
|
171
|
+
merged[key] = spec_b.get(key, spec_a.get(key))
|
|
172
|
+
|
|
173
|
+
return merged
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _merge_lists(list_a: list, list_b: list, key_func) -> list:
|
|
177
|
+
"""
|
|
178
|
+
Merge two lists using a key function for deduplication.
|
|
179
|
+
Items from list_b take precedence over items from list_a.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
list_a (list): First list.
|
|
183
|
+
list_b (list): Second list.
|
|
184
|
+
key_func (callable): Function that returns a key used for deduplication.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
list: Merged list.
|
|
188
|
+
"""
|
|
189
|
+
merged_dict = {}
|
|
190
|
+
for item in list_a:
|
|
191
|
+
key = _ensure_hashable(key_func(item))
|
|
192
|
+
if key not in merged_dict:
|
|
193
|
+
merged_dict[key] = item
|
|
194
|
+
for item in list_b:
|
|
195
|
+
key = _ensure_hashable(key_func(item))
|
|
196
|
+
merged_dict[key] = item # spec_b wins
|
|
197
|
+
return list(merged_dict.values())
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _merge_paths(paths_a: dict, paths_b: dict) -> dict:
|
|
201
|
+
"""
|
|
202
|
+
Merge OpenAPI paths objects.
|
|
203
|
+
|
|
204
|
+
For each path:
|
|
205
|
+
- If the path exists in both specs, merge HTTP methods:
|
|
206
|
+
- If a method exists in both, use spec_b’s definition.
|
|
207
|
+
- Otherwise, preserve both.
|
|
208
|
+
- Additionally, merge path-level "parameters" if present.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
paths_a (dict): Paths from the first spec.
|
|
212
|
+
paths_b (dict): Paths from the second spec.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
dict: Merged paths.
|
|
216
|
+
"""
|
|
217
|
+
merged_paths = {}
|
|
218
|
+
# Start with all paths from paths_a.
|
|
219
|
+
for path, methods in paths_a.items():
|
|
220
|
+
merged_paths[path] = copy.deepcopy(methods)
|
|
221
|
+
|
|
222
|
+
# Merge or add paths from paths_b.
|
|
223
|
+
for path, methods_b in paths_b.items():
|
|
224
|
+
if path not in merged_paths:
|
|
225
|
+
merged_paths[path] = copy.deepcopy(methods_b)
|
|
226
|
+
else:
|
|
227
|
+
methods_a = merged_paths[path]
|
|
228
|
+
for method, details_b in methods_b.items():
|
|
229
|
+
key = method.lower()
|
|
230
|
+
# If the method is "parameters", merge them.
|
|
231
|
+
if key == "parameters":
|
|
232
|
+
params_a = methods_a.get("parameters", [])
|
|
233
|
+
params_b = details_b if isinstance(details_b, list) else []
|
|
234
|
+
methods_a["parameters"] = _merge_lists(
|
|
235
|
+
params_a,
|
|
236
|
+
params_b,
|
|
237
|
+
key_func=lambda x: (x.get("name"), x.get("in")),
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# For HTTP methods, spec_b wins if conflict.
|
|
241
|
+
methods_a[key] = copy.deepcopy(details_b)
|
|
242
|
+
merged_paths[path] = methods_a
|
|
243
|
+
return merged_paths
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _merge_components(components_a: dict, components_b: dict) -> dict:
|
|
247
|
+
"""
|
|
248
|
+
Merge OpenAPI components objects.
|
|
249
|
+
|
|
250
|
+
For each component type (schemas, responses, parameters, examples, requestBodies,
|
|
251
|
+
headers, securitySchemes, links, callbacks), merge dictionaries with spec_b taking precedence.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
components_a (dict): Components from the first spec.
|
|
255
|
+
components_b (dict): Components from the second spec.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
dict: Merged components.
|
|
259
|
+
"""
|
|
260
|
+
merged_components = {}
|
|
261
|
+
# Define the common component types to merge.
|
|
262
|
+
component_types = {
|
|
263
|
+
"schemas",
|
|
264
|
+
"responses",
|
|
265
|
+
"parameters",
|
|
266
|
+
"examples",
|
|
267
|
+
"requestBodies",
|
|
268
|
+
"headers",
|
|
269
|
+
"securitySchemes",
|
|
270
|
+
"links",
|
|
271
|
+
"callbacks",
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
for comp_type in component_types:
|
|
275
|
+
comp_a = components_a.get(comp_type, {})
|
|
276
|
+
comp_b = components_b.get(comp_type, {})
|
|
277
|
+
merged_components[comp_type] = {**comp_a, **comp_b}
|
|
278
|
+
|
|
279
|
+
# Merge any additional keys in components.
|
|
280
|
+
extra_keys = set(components_a.keys()).union(components_b.keys()) - component_types
|
|
281
|
+
for key in extra_keys:
|
|
282
|
+
merged_components[key] = {
|
|
283
|
+
**components_a.get(key, {}),
|
|
284
|
+
**components_b.get(key, {}),
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return merged_components
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _ensure_hashable(obj, depth=0, max_depth=3):
|
|
291
|
+
"""
|
|
292
|
+
Recursively convert a Python object into a hashable representation up to a maximum depth.
|
|
293
|
+
If the depth limit is reached, return str(obj).
|
|
294
|
+
|
|
295
|
+
- Lists are converted to tuples.
|
|
296
|
+
- Dictionaries are converted to tuples of sorted (key, value) pairs.
|
|
297
|
+
- Other types are returned as-is.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
obj: The object to convert.
|
|
301
|
+
depth (int): Current recursion depth.
|
|
302
|
+
max_depth (int): Maximum recursion depth.
|
|
303
|
+
"""
|
|
304
|
+
if depth >= max_depth:
|
|
305
|
+
return str(obj)
|
|
306
|
+
if isinstance(obj, typing.Sequence):
|
|
307
|
+
return tuple(_ensure_hashable(e, depth + 1, max_depth) for e in obj)
|
|
308
|
+
if isinstance(obj, typing.Mapping):
|
|
309
|
+
return tuple(
|
|
310
|
+
sorted(
|
|
311
|
+
(k, _ensure_hashable(v, depth + 1, max_depth)) for k, v in obj.items()
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
return obj
|
langgraph_api/auth/custom.py
CHANGED
langgraph_api/cli.py
CHANGED
|
@@ -4,8 +4,13 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import pathlib
|
|
6
6
|
import threading
|
|
7
|
+
import typing
|
|
7
8
|
from collections.abc import Mapping, Sequence
|
|
8
|
-
|
|
9
|
+
|
|
10
|
+
from typing_extensions import TypedDict
|
|
11
|
+
|
|
12
|
+
if typing.TYPE_CHECKING:
|
|
13
|
+
from langgraph_api.config import HttpConfig
|
|
9
14
|
|
|
10
15
|
logging.basicConfig(level=logging.INFO)
|
|
11
16
|
logger = logging.getLogger(__name__)
|
|
@@ -147,7 +152,8 @@ def run_server(
|
|
|
147
152
|
reload_excludes: Sequence[str] | None = None,
|
|
148
153
|
store: StoreConfig | None = None,
|
|
149
154
|
auth: AuthConfig | None = None,
|
|
150
|
-
|
|
155
|
+
http: typing.Optional["HttpConfig"] = None,
|
|
156
|
+
**kwargs: typing.Any,
|
|
151
157
|
):
|
|
152
158
|
"""Run the LangGraph API server."""
|
|
153
159
|
|
|
@@ -263,6 +269,7 @@ For production use, please use LangGraph Cloud.
|
|
|
263
269
|
LANGSERVE_GRAPHS=json.dumps(graphs) if graphs else None,
|
|
264
270
|
LANGSMITH_LANGGRAPH_API_VARIANT="local_dev",
|
|
265
271
|
LANGGRAPH_AUTH=json.dumps(auth) if auth else None,
|
|
272
|
+
LANGGRAPH_HTTP=json.dumps(http) if http else None,
|
|
266
273
|
# See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
267
274
|
ALLOW_PRIVATE_NETWORK="true",
|
|
268
275
|
**(env_vars or {}),
|
langgraph_api/config.py
CHANGED
|
@@ -1,9 +1,41 @@
|
|
|
1
1
|
from os import environ, getenv
|
|
2
|
+
from typing import TypedDict
|
|
2
3
|
|
|
3
4
|
import orjson
|
|
4
5
|
from starlette.config import Config, undefined
|
|
5
6
|
from starlette.datastructures import CommaSeparatedStrings
|
|
6
7
|
|
|
8
|
+
# types
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CorsConfig(TypedDict, total=False):
|
|
12
|
+
allow_origins: list[str]
|
|
13
|
+
allow_methods: list[str]
|
|
14
|
+
allow_headers: list[str]
|
|
15
|
+
allow_credentials: bool
|
|
16
|
+
allow_origin_regex: str
|
|
17
|
+
expose_headers: list[str]
|
|
18
|
+
max_age: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HttpConfig(TypedDict, total=False):
|
|
22
|
+
app: str
|
|
23
|
+
"""Import path for a custom Starlette/FastAPI app to mount"""
|
|
24
|
+
disable_assistants: bool
|
|
25
|
+
"""Disable /assistants routes"""
|
|
26
|
+
disable_threads: bool
|
|
27
|
+
"""Disable /threads routes"""
|
|
28
|
+
disable_runs: bool
|
|
29
|
+
"""Disable /runs routes"""
|
|
30
|
+
disable_store: bool
|
|
31
|
+
"""Disable /store routes"""
|
|
32
|
+
disable_meta: bool
|
|
33
|
+
"""Disable /ok, /info, /metrics, and /docs routes"""
|
|
34
|
+
cors: CorsConfig | None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# env
|
|
38
|
+
|
|
7
39
|
env = Config()
|
|
8
40
|
|
|
9
41
|
|
|
@@ -17,7 +49,6 @@ def _parse_json(json: str | None) -> dict | None:
|
|
|
17
49
|
|
|
18
50
|
|
|
19
51
|
STATS_INTERVAL_SECS = env("STATS_INTERVAL_SECS", cast=int, default=60)
|
|
20
|
-
HTTP_CONCURRENCY = env("HTTP_CONCURRENCY", cast=int, default=10)
|
|
21
52
|
|
|
22
53
|
# storage
|
|
23
54
|
|
|
@@ -36,9 +67,12 @@ ALLOW_PRIVATE_NETWORK = env("ALLOW_PRIVATE_NETWORK", cast=bool, default=False)
|
|
|
36
67
|
See https://developer.chrome.com/blog/private-network-access-update-2024-03
|
|
37
68
|
"""
|
|
38
69
|
|
|
70
|
+
HTTP_CONFIG: HttpConfig | None = env("LANGGRAPH_HTTP", cast=_parse_json, default=None)
|
|
39
71
|
CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
|
|
40
|
-
|
|
41
|
-
CORS_CONFIG =
|
|
72
|
+
if HTTP_CONFIG and HTTP_CONFIG.get("cors"):
|
|
73
|
+
CORS_CONFIG = HTTP_CONFIG["cors"]
|
|
74
|
+
else:
|
|
75
|
+
CORS_CONFIG: CorsConfig | None = env("CORS_CONFIG", cast=_parse_json, default=None)
|
|
42
76
|
"""
|
|
43
77
|
{
|
|
44
78
|
"type": "object",
|
langgraph_api/js/remote_new.py
CHANGED
|
@@ -336,6 +336,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
336
336
|
env={
|
|
337
337
|
"LANGSERVE_GRAPHS": paths_str,
|
|
338
338
|
"LANGCHAIN_CALLBACKS_BACKGROUND": "true",
|
|
339
|
+
"NODE_ENV": "development" if watch else "production",
|
|
339
340
|
"CHOKIDAR_USEPOLLING": "true",
|
|
340
341
|
**os.environ,
|
|
341
342
|
},
|
langgraph_api/js/remote_old.py
CHANGED
|
@@ -296,6 +296,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
296
296
|
env={
|
|
297
297
|
"LANGSERVE_GRAPHS": paths_str,
|
|
298
298
|
"LANGCHAIN_CALLBACKS_BACKGROUND": "true",
|
|
299
|
+
"NODE_ENV": "development" if watch else "production",
|
|
299
300
|
"CHOKIDAR_USEPOLLING": "true",
|
|
300
301
|
**os.environ,
|
|
301
302
|
},
|
langgraph_api/server.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# MONKEY PATCH: Patch Starlette to fix an error in the library
|
|
2
2
|
import langgraph_api.patch # noqa: F401,I001
|
|
3
|
+
import sys
|
|
3
4
|
|
|
4
5
|
# WARNING: Keep the import above before other code runs as it
|
|
5
6
|
# patches an error in the Starlette library.
|
|
@@ -7,13 +8,15 @@ import logging
|
|
|
7
8
|
|
|
8
9
|
import jsonschema_rs
|
|
9
10
|
import structlog
|
|
11
|
+
from contextlib import asynccontextmanager
|
|
10
12
|
from langgraph.errors import EmptyInputError, InvalidUpdateError
|
|
11
13
|
from starlette.applications import Starlette
|
|
12
14
|
from starlette.middleware import Middleware
|
|
13
15
|
from starlette.middleware.cors import CORSMiddleware
|
|
16
|
+
from langgraph_api.api.openapi import set_custom_spec
|
|
14
17
|
|
|
15
18
|
import langgraph_api.config as config
|
|
16
|
-
from langgraph_api.api import routes
|
|
19
|
+
from langgraph_api.api import routes, user_router
|
|
17
20
|
from langgraph_api.errors import (
|
|
18
21
|
overloaded_error_handler,
|
|
19
22
|
validation_error_handler,
|
|
@@ -22,8 +25,10 @@ from langgraph_api.errors import (
|
|
|
22
25
|
from langgraph_api.lifespan import lifespan
|
|
23
26
|
from langgraph_api.middleware.http_logger import AccessLoggerMiddleware
|
|
24
27
|
from langgraph_api.middleware.private_network import PrivateNetworkMiddleware
|
|
28
|
+
from langgraph_api.utils import SchemaGenerator
|
|
25
29
|
from langgraph_license.middleware import LicenseValidationMiddleware
|
|
26
30
|
from langgraph_storage.retry import OVERLOADED_EXCEPTIONS
|
|
31
|
+
from langgraph_sdk.client import configure_loopback_transports
|
|
27
32
|
|
|
28
33
|
logging.captureWarnings(True)
|
|
29
34
|
logger = structlog.stdlib.get_logger(__name__)
|
|
@@ -35,33 +40,103 @@ if config.ALLOW_PRIVATE_NETWORK:
|
|
|
35
40
|
|
|
36
41
|
middleware.extend(
|
|
37
42
|
[
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
(
|
|
44
|
+
Middleware(
|
|
45
|
+
CORSMiddleware,
|
|
46
|
+
allow_origins=config.CORS_ALLOW_ORIGINS,
|
|
47
|
+
allow_credentials=True,
|
|
48
|
+
allow_methods=["*"],
|
|
49
|
+
allow_headers=["*"],
|
|
50
|
+
)
|
|
51
|
+
if config.CORS_CONFIG is None
|
|
52
|
+
else Middleware(
|
|
53
|
+
CORSMiddleware,
|
|
54
|
+
**config.CORS_CONFIG,
|
|
55
|
+
)
|
|
49
56
|
),
|
|
50
57
|
Middleware(LicenseValidationMiddleware),
|
|
51
58
|
Middleware(AccessLoggerMiddleware, logger=logger),
|
|
52
59
|
]
|
|
53
60
|
)
|
|
61
|
+
exception_handlers = {
|
|
62
|
+
ValueError: value_error_handler,
|
|
63
|
+
InvalidUpdateError: value_error_handler,
|
|
64
|
+
EmptyInputError: value_error_handler,
|
|
65
|
+
jsonschema_rs.ValidationError: validation_error_handler,
|
|
66
|
+
} | {exc: overloaded_error_handler for exc in OVERLOADED_EXCEPTIONS}
|
|
54
67
|
|
|
55
68
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
69
|
+
def update_openapi_spec(app):
|
|
70
|
+
spec = None
|
|
71
|
+
if "fastapi" in sys.modules:
|
|
72
|
+
# It's maybe a fastapi app
|
|
73
|
+
from fastapi import FastAPI
|
|
74
|
+
|
|
75
|
+
if isinstance(user_router, FastAPI):
|
|
76
|
+
spec = app.openapi()
|
|
77
|
+
|
|
78
|
+
if spec is None:
|
|
79
|
+
# How do we add
|
|
80
|
+
schemas = SchemaGenerator(
|
|
81
|
+
{
|
|
82
|
+
"openapi": "3.1.0",
|
|
83
|
+
"info": {"title": "LangGraph Platform", "version": "0.1.0"},
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
spec = schemas.get_schema(routes=app.routes)
|
|
87
|
+
|
|
88
|
+
if spec:
|
|
89
|
+
set_custom_spec(spec)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
if user_router:
|
|
93
|
+
# Merge routes
|
|
94
|
+
app = user_router
|
|
95
|
+
update_openapi_spec(app)
|
|
96
|
+
for route in routes:
|
|
97
|
+
if route.path in ("/docs", "/openapi.json"):
|
|
98
|
+
# Our handlers for these are inclusive of the custom routes and default API ones
|
|
99
|
+
# Don't let these be shadowed
|
|
100
|
+
app.router.routes.insert(0, route)
|
|
101
|
+
else:
|
|
102
|
+
# Everything else could be shadowed.
|
|
103
|
+
app.router.routes.append(route)
|
|
104
|
+
|
|
105
|
+
# Merge lifespans
|
|
106
|
+
original_lifespan = app.router.lifespan_context
|
|
107
|
+
if app.router.on_startup or app.router.on_shutdown:
|
|
108
|
+
raise ValueError(
|
|
109
|
+
f"Cannot merge lifespans with on_startup or on_shutdown: {app.router.on_startup} {app.router.on_shutdown}"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@asynccontextmanager
|
|
113
|
+
async def combined_lifespan(app):
|
|
114
|
+
async with lifespan(app):
|
|
115
|
+
if original_lifespan:
|
|
116
|
+
async with original_lifespan(app):
|
|
117
|
+
yield
|
|
118
|
+
else:
|
|
119
|
+
yield
|
|
120
|
+
|
|
121
|
+
app.router.lifespan_context = combined_lifespan
|
|
122
|
+
|
|
123
|
+
# Merge middleware
|
|
124
|
+
app.user_middleware = (app.user_middleware or []) + middleware
|
|
125
|
+
# Merge exception handlers
|
|
126
|
+
for k, v in exception_handlers.items():
|
|
127
|
+
if k not in app.exception_handlers:
|
|
128
|
+
app.exception_handlers[k] = v
|
|
129
|
+
else:
|
|
130
|
+
logger.debug(f"Overriding exception handler for {k}")
|
|
131
|
+
# If the user creates a loopback client with `get_client() (no url)
|
|
132
|
+
# this will update the http transport to connect to the right app
|
|
133
|
+
configure_loopback_transports(app)
|
|
134
|
+
|
|
135
|
+
else:
|
|
136
|
+
# It's a regular starlette app
|
|
137
|
+
app = Starlette(
|
|
138
|
+
routes=routes,
|
|
139
|
+
lifespan=lifespan,
|
|
140
|
+
middleware=middleware,
|
|
141
|
+
exception_handlers=exception_handlers,
|
|
142
|
+
)
|
langgraph_api/utils.py
CHANGED
|
@@ -5,12 +5,17 @@ from contextlib import asynccontextmanager
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from typing import Any, Protocol, TypeAlias, TypeVar
|
|
7
7
|
|
|
8
|
+
import structlog
|
|
8
9
|
from langgraph_sdk import Auth
|
|
9
10
|
from starlette.authentication import AuthCredentials, BaseUser
|
|
10
11
|
from starlette.exceptions import HTTPException
|
|
12
|
+
from starlette.schemas import BaseSchemaGenerator
|
|
11
13
|
|
|
12
14
|
from langgraph_api.auth.custom import SimpleUser
|
|
13
15
|
|
|
16
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
14
19
|
T = TypeVar("T")
|
|
15
20
|
Row: TypeAlias = dict[str, Any]
|
|
16
21
|
AuthContext = contextvars.ContextVar[Auth.types.BaseAuthContext | None](
|
|
@@ -98,3 +103,27 @@ def next_cron_date(schedule: str, base_time: datetime) -> datetime:
|
|
|
98
103
|
|
|
99
104
|
cron_iter = croniter.croniter(schedule, base_time)
|
|
100
105
|
return cron_iter.get_next(datetime)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SchemaGenerator(BaseSchemaGenerator):
|
|
109
|
+
def __init__(self, base_schema: dict[str, Any]) -> None:
|
|
110
|
+
self.base_schema = base_schema
|
|
111
|
+
|
|
112
|
+
def get_schema(self, routes: list) -> dict[str, Any]:
|
|
113
|
+
schema = dict(self.base_schema)
|
|
114
|
+
schema.setdefault("paths", {})
|
|
115
|
+
endpoints_info = self.get_endpoints(routes)
|
|
116
|
+
|
|
117
|
+
for endpoint in endpoints_info:
|
|
118
|
+
try:
|
|
119
|
+
parsed = self.parse_docstring(endpoint.func)
|
|
120
|
+
except AssertionError:
|
|
121
|
+
logger.warning("Could not parse docstrings for route %s", endpoint.path)
|
|
122
|
+
parsed = {}
|
|
123
|
+
|
|
124
|
+
if endpoint.path not in schema["paths"]:
|
|
125
|
+
schema["paths"][endpoint.path] = {}
|
|
126
|
+
|
|
127
|
+
schema["paths"][endpoint.path][endpoint.http_method] = parsed
|
|
128
|
+
|
|
129
|
+
return schema
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: langgraph-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.26
|
|
4
4
|
Summary:
|
|
5
5
|
License: Elastic-2.0
|
|
6
6
|
Author: Nuno Campos
|
|
@@ -15,8 +15,8 @@ Requires-Dist: httpx (>=0.27.0)
|
|
|
15
15
|
Requires-Dist: jsonschema-rs (>=0.25.0,<0.26.0)
|
|
16
16
|
Requires-Dist: langchain-core (>=0.2.38,<0.4.0)
|
|
17
17
|
Requires-Dist: langgraph (>=0.2.56,<0.3.0)
|
|
18
|
-
Requires-Dist: langgraph-checkpoint (>=2.0.
|
|
19
|
-
Requires-Dist: langgraph-sdk (>=0.1.
|
|
18
|
+
Requires-Dist: langgraph-checkpoint (>=2.0.15,<3.0)
|
|
19
|
+
Requires-Dist: langgraph-sdk (>=0.1.53,<0.2.0)
|
|
20
20
|
Requires-Dist: langsmith (>=0.1.63,<0.4.0)
|
|
21
21
|
Requires-Dist: orjson (>=3.10.1)
|
|
22
22
|
Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
|
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
2
2
|
langgraph_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
langgraph_api/api/__init__.py,sha256=
|
|
3
|
+
langgraph_api/api/__init__.py,sha256=WBw9xUm5JkvgWVbwHs8vhR5jOf3_zhMjktB96zKNI58,4962
|
|
4
4
|
langgraph_api/api/assistants.py,sha256=9wngelDC9vnSs2vYGTHDSCLf5KNds-6mgP1BWnfoY2M,12865
|
|
5
5
|
langgraph_api/api/meta.py,sha256=hueasWpTDQ6xYLo9Bzt2jhNH8XQRzreH8FTeFfnRoxQ,2700
|
|
6
|
-
langgraph_api/api/openapi.py,sha256=
|
|
6
|
+
langgraph_api/api/openapi.py,sha256=f9gfmWN2AMKNUpLCpSgZuw_aeOF9jCXPdOtFT5PaTWM,10960
|
|
7
7
|
langgraph_api/api/runs.py,sha256=wAzPXi_kcYB9BcLBL4FXgkBohWwCPIpe4XERnsnWnsA,16042
|
|
8
8
|
langgraph_api/api/store.py,sha256=VzAJVOwO0IxosBB7km5TTf2rhlWGyPkVz_LpvbxetVY,5437
|
|
9
9
|
langgraph_api/api/threads.py,sha256=taU61XPcCEhBPCYPZcMDsgVDwwWUWJs8p-PrXFXWY48,8661
|
|
10
10
|
langgraph_api/asyncio.py,sha256=2fOlx-cZvuj1gQ867Kw1R_wsBsl9jdHYHcUtK2a-x-U,6264
|
|
11
11
|
langgraph_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
langgraph_api/auth/custom.py,sha256=
|
|
12
|
+
langgraph_api/auth/custom.py,sha256=oyXz7UONY1YeTJ5IBGRAbFUz_K6krkptNxW8zzjb0uk,21088
|
|
13
13
|
langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
langgraph_api/auth/langsmith/backend.py,sha256=InScaL-HYCnxYEauhxU198gRZV9pJn9SzzBoR9Edn7g,2654
|
|
15
15
|
langgraph_api/auth/langsmith/client.py,sha256=eKchvAom7hdkUXauD8vHNceBDDUijrFgdTV8bKd7x4Q,3998
|
|
16
16
|
langgraph_api/auth/middleware.py,sha256=qc7SbaFoeWaqxS1wbjZ2PPQ4iI2p9T0shWL7c6g0ed4,1636
|
|
17
17
|
langgraph_api/auth/noop.py,sha256=Bk6Nf3p8D_iMVy_OyfPlyiJp_aEwzL-sHrbxoXpCbac,586
|
|
18
18
|
langgraph_api/auth/studio_user.py,sha256=FzFQRROKDlA9JjtBuwyZvk6Mbwno5M9RVYjDO6FU3F8,186
|
|
19
|
-
langgraph_api/cli.py,sha256=
|
|
20
|
-
langgraph_api/config.py,sha256=
|
|
19
|
+
langgraph_api/cli.py,sha256=PKZDWjb1NjGXT1qRN0ogsFw2ANlyq7VvXI6QeTPaFhI,12231
|
|
20
|
+
langgraph_api/config.py,sha256=Alb7vm8t13Y1oM9W6JaplApMwTLL1qVZZuXHG08FtgE,5104
|
|
21
21
|
langgraph_api/cron_scheduler.py,sha256=MW41-TSGUe5OuXycFTy7Ax7ypxHVAv-0ImLonRT8h8o,2629
|
|
22
22
|
langgraph_api/errors.py,sha256=Bu_i5drgNTyJcLiyrwVE_6-XrSU50BHf9TDpttki9wQ,1690
|
|
23
23
|
langgraph_api/graph.py,sha256=FombjYQkqj8jrXJFEVkl3m2UyFcq5nSVNswR2HoRsQY,16385
|
|
@@ -31,8 +31,8 @@ langgraph_api/js/errors.py,sha256=Cm1TKWlUCwZReDC5AQ6SgNIVGD27Qov2xcgHyf8-GXo,36
|
|
|
31
31
|
langgraph_api/js/global.d.ts,sha256=zR_zLYfpzyPfxpEFth5RgZoyfGulIXyZYPRf7cU0K0Y,106
|
|
32
32
|
langgraph_api/js/package.json,sha256=AmpkMzr96yF9xZ7bCrSApF-j7PJH6WeALn9HpPGBnmQ,840
|
|
33
33
|
langgraph_api/js/remote.py,sha256=D9cqcEgXau-fm_trpNwCHMra5BXntgUa469lgs_a9JQ,622
|
|
34
|
-
langgraph_api/js/remote_new.py,sha256
|
|
35
|
-
langgraph_api/js/remote_old.py,sha256=
|
|
34
|
+
langgraph_api/js/remote_new.py,sha256=-9gsJeV32cHPXd-EESFVfPoBoPC7pqR7XKhkb_q9cHA,22781
|
|
35
|
+
langgraph_api/js/remote_old.py,sha256=A28NMSvLGPfg044NDTHSv63pcujYZQXZujDxryGGhOw,22652
|
|
36
36
|
langgraph_api/js/schema.py,sha256=7idnv7URlYUdSNMBXQcw7E4SxaPxCq_Oxwnlml8q5ik,408
|
|
37
37
|
langgraph_api/js/server_sent_events.py,sha256=DLgXOHauemt7706vnfDUCG1GI3TidKycSizccdz9KgA,3702
|
|
38
38
|
langgraph_api/js/src/graph.mts,sha256=J-M-vYHj1G5tyonPUym3ePNGqGYtspPCrZOgr92xKb4,3171
|
|
@@ -71,11 +71,11 @@ langgraph_api/queue.py,sha256=2sw9HB2cYVBhYUNA3F7lcJAgRjhQJXhA_HNGhFt2BW8,14508
|
|
|
71
71
|
langgraph_api/route.py,sha256=fM4qYCGbmH0a3_cV8uKocb1sLklehxO6HhdRXqLK6OM,4421
|
|
72
72
|
langgraph_api/schema.py,sha256=mgam5lpuqZnrNWMm_0nQ95683gCnCvQNRKbiuFj7z8Q,5310
|
|
73
73
|
langgraph_api/serde.py,sha256=VoJ7Z1IuqrQGXFzEP1qijAITtWCrmjtVqlCRuScjXJI,3533
|
|
74
|
-
langgraph_api/server.py,sha256=
|
|
74
|
+
langgraph_api/server.py,sha256=CiNK327zTsEpoVGeJK1JOtZHvOBYRoz0CnBTZUmsC7c,4567
|
|
75
75
|
langgraph_api/sse.py,sha256=2wNodCOP2eg7a9mpSu0S3FQ0CHk2BBV_vv0UtIgJIcc,4034
|
|
76
76
|
langgraph_api/state.py,sha256=8jx4IoTCOjTJuwzuXJKKFwo1VseHjNnw_CCq4x1SW14,2284
|
|
77
77
|
langgraph_api/stream.py,sha256=MUYYNgwtLs1Mhq1dm12zda7j8uFYir49umigK6CnuXU,11944
|
|
78
|
-
langgraph_api/utils.py,sha256=
|
|
78
|
+
langgraph_api/utils.py,sha256=92mSti9GfGdMRRWyESKQW5yV-75Z9icGHnIrBYvdypU,3619
|
|
79
79
|
langgraph_api/validation.py,sha256=McizHlz-Ez8Jhdbc79mbPSde7GIuf2Jlbjx2yv_l6dA,4475
|
|
80
80
|
langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
81
|
langgraph_license/middleware.py,sha256=_ODIYzQkymr6W9_Fp9wtf1kAQspnpsmr53xuzyF2GA0,612
|
|
@@ -90,8 +90,8 @@ langgraph_storage/store.py,sha256=D-p3cWc_umamkKp-6Cz3cAriSACpvM5nxUIvND6PuxE,27
|
|
|
90
90
|
langgraph_storage/ttl_dict.py,sha256=FlpEY8EANeXWKo_G5nmIotPquABZGyIJyk6HD9u6vqY,1533
|
|
91
91
|
logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
|
|
92
92
|
openapi.json,sha256=DqXpD6JD4tvSOhUgDN_6f919F8YmOSAK4CsGi1NDoiI,125252
|
|
93
|
-
langgraph_api-0.0.
|
|
94
|
-
langgraph_api-0.0.
|
|
95
|
-
langgraph_api-0.0.
|
|
96
|
-
langgraph_api-0.0.
|
|
97
|
-
langgraph_api-0.0.
|
|
93
|
+
langgraph_api-0.0.26.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
|
|
94
|
+
langgraph_api-0.0.26.dist-info/METADATA,sha256=VFGg6OyMT4eyXHQgQiRPD9aWiHWlAjIKqTqQCX34CSc,4039
|
|
95
|
+
langgraph_api-0.0.26.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
|
96
|
+
langgraph_api-0.0.26.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
|
|
97
|
+
langgraph_api-0.0.26.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|