langgraph-api 0.2.130__py3-none-any.whl → 0.2.134__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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/assistants.py +32 -6
- langgraph_api/api/meta.py +3 -1
- langgraph_api/api/openapi.py +1 -1
- langgraph_api/api/runs.py +50 -10
- langgraph_api/api/threads.py +27 -1
- langgraph_api/api/ui.py +2 -0
- langgraph_api/asgi_transport.py +2 -2
- langgraph_api/asyncio.py +10 -8
- langgraph_api/auth/custom.py +9 -4
- langgraph_api/auth/langsmith/client.py +1 -1
- langgraph_api/cli.py +5 -4
- langgraph_api/config.py +1 -1
- langgraph_api/executor_entrypoint.py +23 -0
- langgraph_api/graph.py +25 -9
- langgraph_api/http.py +10 -7
- langgraph_api/http_metrics.py +4 -1
- langgraph_api/js/build.mts +11 -2
- langgraph_api/js/client.http.mts +2 -0
- langgraph_api/js/client.mts +13 -3
- langgraph_api/js/package.json +2 -2
- langgraph_api/js/remote.py +17 -12
- langgraph_api/js/src/preload.mjs +9 -1
- langgraph_api/js/src/utils/files.mts +5 -2
- langgraph_api/js/sse.py +1 -1
- langgraph_api/js/yarn.lock +9 -9
- langgraph_api/logging.py +3 -3
- langgraph_api/middleware/http_logger.py +2 -1
- langgraph_api/models/run.py +19 -14
- langgraph_api/patch.py +2 -2
- langgraph_api/queue_entrypoint.py +33 -18
- langgraph_api/schema.py +88 -4
- langgraph_api/serde.py +32 -5
- langgraph_api/server.py +5 -3
- langgraph_api/state.py +8 -8
- langgraph_api/store.py +1 -1
- langgraph_api/stream.py +33 -20
- langgraph_api/traceblock.py +1 -1
- langgraph_api/utils/__init__.py +40 -5
- langgraph_api/utils/config.py +13 -4
- langgraph_api/utils/future.py +1 -1
- langgraph_api/utils/uuids.py +87 -0
- langgraph_api/validation.py +9 -0
- langgraph_api/webhook.py +20 -20
- langgraph_api/worker.py +8 -5
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/METADATA +2 -2
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/RECORD +51 -49
- openapi.json +331 -1
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/WHEEL +0 -0
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.2.130.dist-info → langgraph_api-0.2.134.dist-info}/licenses/LICENSE +0 -0
langgraph_api/http.py
CHANGED
|
@@ -72,7 +72,7 @@ class JsonHttpClient:
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
_http_client: JsonHttpClient
|
|
75
|
-
_loopback_client: JsonHttpClient = None
|
|
75
|
+
_loopback_client: JsonHttpClient | None = None
|
|
76
76
|
|
|
77
77
|
|
|
78
78
|
async def start_http_client() -> None:
|
|
@@ -113,16 +113,16 @@ def get_loopback_client() -> JsonHttpClient:
|
|
|
113
113
|
return _loopback_client
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
def is_retriable_error(exception:
|
|
116
|
+
def is_retriable_error(exception: BaseException) -> bool:
|
|
117
117
|
# httpx error hierarchy: https://www.python-httpx.org/exceptions/
|
|
118
118
|
# Retry all timeout related errors
|
|
119
119
|
if isinstance(exception, httpx.TimeoutException | httpx.NetworkError):
|
|
120
120
|
return True
|
|
121
121
|
# Seems to just apply to HttpStatusError but doesn't hurt to check all
|
|
122
122
|
if isinstance(exception, httpx.HTTPError):
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
or
|
|
123
|
+
response = getattr(exception, "response", None)
|
|
124
|
+
return response is not None and (
|
|
125
|
+
response.status_code >= 500 or response.status_code == 429
|
|
126
126
|
)
|
|
127
127
|
return False
|
|
128
128
|
|
|
@@ -149,7 +149,7 @@ async def http_request(
|
|
|
149
149
|
request_timeout: float | None = 30,
|
|
150
150
|
raise_error: bool = True,
|
|
151
151
|
client: JsonHttpClient | None = None,
|
|
152
|
-
) ->
|
|
152
|
+
) -> None:
|
|
153
153
|
"""Make an HTTP request with retries.
|
|
154
154
|
|
|
155
155
|
Args:
|
|
@@ -173,7 +173,10 @@ async def http_request(
|
|
|
173
173
|
|
|
174
174
|
content = None
|
|
175
175
|
if body is not None:
|
|
176
|
-
|
|
176
|
+
if isinstance(body, str):
|
|
177
|
+
content = body.encode("utf-8")
|
|
178
|
+
else:
|
|
179
|
+
content = body
|
|
177
180
|
elif json is not None:
|
|
178
181
|
content = json_dumpb(json)
|
|
179
182
|
|
langgraph_api/http_metrics.py
CHANGED
|
@@ -98,7 +98,10 @@ class HTTPMetricsCollector:
|
|
|
98
98
|
hist_data["count"] += 1
|
|
99
99
|
|
|
100
100
|
def get_metrics(
|
|
101
|
-
self,
|
|
101
|
+
self,
|
|
102
|
+
project_id: str | None,
|
|
103
|
+
revision_id: str | None,
|
|
104
|
+
format: str = "prometheus",
|
|
102
105
|
) -> dict | list[str]:
|
|
103
106
|
if format == "json":
|
|
104
107
|
return {
|
langgraph_api/js/build.mts
CHANGED
|
@@ -17,7 +17,14 @@ const __dirname = new URL(".", import.meta.url).pathname;
|
|
|
17
17
|
|
|
18
18
|
async function main() {
|
|
19
19
|
const specs = Object.entries(
|
|
20
|
-
z
|
|
20
|
+
z
|
|
21
|
+
.record(
|
|
22
|
+
z.union([
|
|
23
|
+
z.string(),
|
|
24
|
+
z.object({ path: z.string(), description: z.string().nullish() }),
|
|
25
|
+
]),
|
|
26
|
+
)
|
|
27
|
+
.parse(JSON.parse(process.env.LANGSERVE_GRAPHS)),
|
|
21
28
|
).filter(([_, spec]) => filterValidExportPath(spec));
|
|
22
29
|
|
|
23
30
|
let GRAPH_SCHEMAS: Record<string, Record<string, GraphSchema> | false> = {};
|
|
@@ -49,7 +56,9 @@ async function main() {
|
|
|
49
56
|
await Promise.all(
|
|
50
57
|
specs.map(async ([graphId, rawSpec]) => {
|
|
51
58
|
console.info(`[${graphId}]: Checking for source file existence`);
|
|
52
|
-
const
|
|
59
|
+
const importPath =
|
|
60
|
+
typeof rawSpec === "string" ? rawSpec : rawSpec.path;
|
|
61
|
+
const { resolved, ...spec } = await resolveGraph(importPath, {
|
|
53
62
|
onlyFilePresence: true,
|
|
54
63
|
});
|
|
55
64
|
|
langgraph_api/js/client.http.mts
CHANGED
langgraph_api/js/client.mts
CHANGED
|
@@ -950,6 +950,8 @@ async function* getStateHistoryRequest(
|
|
|
950
950
|
const __dirname = new URL(".", import.meta.url).pathname;
|
|
951
951
|
|
|
952
952
|
async function main() {
|
|
953
|
+
logger.info("Starting graph loop", { pid: process.pid });
|
|
954
|
+
|
|
953
955
|
const app = new Hono();
|
|
954
956
|
|
|
955
957
|
GRAPH_OPTIONS = {
|
|
@@ -959,7 +961,12 @@ async function main() {
|
|
|
959
961
|
|
|
960
962
|
const specs = Object.entries(
|
|
961
963
|
z
|
|
962
|
-
.record(
|
|
964
|
+
.record(
|
|
965
|
+
z.union([
|
|
966
|
+
z.string(),
|
|
967
|
+
z.object({ path: z.string(), description: z.string().nullish() }),
|
|
968
|
+
]),
|
|
969
|
+
)
|
|
963
970
|
.parse(JSON.parse(process.env.LANGSERVE_GRAPHS ?? "{}")),
|
|
964
971
|
).filter(([_, spec]) => filterValidExportPath(spec));
|
|
965
972
|
|
|
@@ -978,7 +985,8 @@ async function main() {
|
|
|
978
985
|
await Promise.all(
|
|
979
986
|
specs.map(async ([graphId, rawSpec]) => {
|
|
980
987
|
logger.info(`Resolving graph ${graphId}`);
|
|
981
|
-
const
|
|
988
|
+
const importPath = typeof rawSpec === "string" ? rawSpec : rawSpec.path;
|
|
989
|
+
const { resolved, ...spec } = await resolveGraph(importPath);
|
|
982
990
|
|
|
983
991
|
GRAPH_RESOLVED[graphId] = resolved;
|
|
984
992
|
GRAPH_SPEC[graphId] = spec;
|
|
@@ -1140,7 +1148,9 @@ async function getNodesExecutedRequest(
|
|
|
1140
1148
|
) {
|
|
1141
1149
|
const value = nodesExecuted;
|
|
1142
1150
|
nodesExecuted = 0;
|
|
1143
|
-
logger.debug(
|
|
1151
|
+
logger.debug(
|
|
1152
|
+
`Returning ${value} nodes executed. Reset nodes executed to ${nodesExecuted}.`,
|
|
1153
|
+
);
|
|
1144
1154
|
return { nodesExecuted: value };
|
|
1145
1155
|
}
|
|
1146
1156
|
patchFetch();
|
langgraph_api/js/package.json
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"@hono/zod-validator": "^0.2.2",
|
|
12
12
|
"@langchain/core": "^0.3.59",
|
|
13
13
|
"@langchain/langgraph": "^0.2.65",
|
|
14
|
-
"@langchain/langgraph-api": "~0.0.
|
|
15
|
-
"@langchain/langgraph-ui": "~0.0.
|
|
14
|
+
"@langchain/langgraph-api": "~0.0.59",
|
|
15
|
+
"@langchain/langgraph-ui": "~0.0.59",
|
|
16
16
|
"@langchain/langgraph-checkpoint": "~0.0.18",
|
|
17
17
|
"@types/json-schema": "^7.0.15",
|
|
18
18
|
"@typescript/vfs": "^1.6.0",
|
langgraph_api/js/remote.py
CHANGED
|
@@ -106,6 +106,10 @@ async def _client_stream(method: str, data: dict[str, Any]):
|
|
|
106
106
|
yield sse.data
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
class NoopModel(BaseModel):
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
109
113
|
async def _client_invoke(method: str, data: dict[str, Any]):
|
|
110
114
|
graph_id = data.get("graph_id")
|
|
111
115
|
res = await _client.post(
|
|
@@ -177,13 +181,10 @@ class RemotePregel(BaseRemotePregel):
|
|
|
177
181
|
nodes: list[Any] = response.pop("nodes")
|
|
178
182
|
edges: list[Any] = response.pop("edges")
|
|
179
183
|
|
|
180
|
-
class NoopModel(BaseModel):
|
|
181
|
-
pass
|
|
182
|
-
|
|
183
184
|
return DrawableGraph(
|
|
184
185
|
{
|
|
185
186
|
data["id"]: Node(
|
|
186
|
-
data["id"], data["id"], NoopModel
|
|
187
|
+
data["id"], data["id"], NoopModel, data.get("metadata")
|
|
187
188
|
)
|
|
188
189
|
for data in nodes
|
|
189
190
|
},
|
|
@@ -244,9 +245,9 @@ class RemotePregel(BaseRemotePregel):
|
|
|
244
245
|
)
|
|
245
246
|
return tuple(result)
|
|
246
247
|
|
|
247
|
-
return StateSnapshot(
|
|
248
|
+
return StateSnapshot( # type: ignore[missing-argument]
|
|
248
249
|
item.get("values"),
|
|
249
|
-
item.get("next"),
|
|
250
|
+
cast(tuple, item.get("next", ())),
|
|
250
251
|
item.get("config"),
|
|
251
252
|
item.get("metadata"),
|
|
252
253
|
item.get("createdAt"),
|
|
@@ -352,7 +353,7 @@ class RemotePregel(BaseRemotePregel):
|
|
|
352
353
|
return result["nodesExecuted"]
|
|
353
354
|
|
|
354
355
|
|
|
355
|
-
async def run_js_process(paths_str: str, watch: bool = False):
|
|
356
|
+
async def run_js_process(paths_str: str | None, watch: bool = False):
|
|
356
357
|
# check if tsx is available
|
|
357
358
|
tsx_path = shutil.which("tsx")
|
|
358
359
|
if tsx_path is None:
|
|
@@ -360,7 +361,7 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
360
361
|
"tsx not found in PATH. Please upgrade to latest LangGraph CLI to support running JS graphs."
|
|
361
362
|
)
|
|
362
363
|
attempt = 0
|
|
363
|
-
while
|
|
364
|
+
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
364
365
|
client_file = os.path.join(os.path.dirname(__file__), "client.mts")
|
|
365
366
|
client_preload_file = os.path.join(
|
|
366
367
|
os.path.dirname(__file__), "src", "preload.mjs"
|
|
@@ -408,7 +409,9 @@ async def run_js_process(paths_str: str, watch: bool = False):
|
|
|
408
409
|
attempt += 1
|
|
409
410
|
|
|
410
411
|
|
|
411
|
-
async def run_js_http_process(
|
|
412
|
+
async def run_js_http_process(
|
|
413
|
+
paths_str: str | None, http_config: dict, watch: bool = False
|
|
414
|
+
):
|
|
412
415
|
# check if tsx is available
|
|
413
416
|
tsx_path = shutil.which("tsx")
|
|
414
417
|
if tsx_path is None:
|
|
@@ -417,7 +420,7 @@ async def run_js_http_process(paths_str: str, http_config: dict, watch: bool = F
|
|
|
417
420
|
)
|
|
418
421
|
|
|
419
422
|
attempt = 0
|
|
420
|
-
while
|
|
423
|
+
while (current_task := asyncio.current_task()) and not current_task.cancelled():
|
|
421
424
|
client_file = os.path.join(os.path.dirname(__file__), "client.http.mts")
|
|
422
425
|
args = ("tsx", "watch", client_file) if watch else ("tsx", client_file)
|
|
423
426
|
pid = None
|
|
@@ -795,7 +798,9 @@ async def wait_until_js_ready():
|
|
|
795
798
|
) as checkpointer_client,
|
|
796
799
|
):
|
|
797
800
|
attempt = 0
|
|
798
|
-
while
|
|
801
|
+
while (
|
|
802
|
+
current_task := asyncio.current_task()
|
|
803
|
+
) and not current_task.cancelled():
|
|
799
804
|
try:
|
|
800
805
|
res = await graph_client.get("/ok")
|
|
801
806
|
res.raise_for_status()
|
|
@@ -909,7 +914,7 @@ class CustomJsAuthBackend(AuthenticationBackend):
|
|
|
909
914
|
|
|
910
915
|
|
|
911
916
|
async def handle_js_auth_event(
|
|
912
|
-
ctx: Auth.types.AuthContext
|
|
917
|
+
ctx: Auth.types.AuthContext,
|
|
913
918
|
value: dict,
|
|
914
919
|
) -> Auth.types.FilterType | None:
|
|
915
920
|
if hasattr(ctx.user, "dict") and callable(ctx.user.dict):
|
langgraph_api/js/src/preload.mjs
CHANGED
|
@@ -11,7 +11,15 @@ const cwd = process.cwd();
|
|
|
11
11
|
// be working fine as well.
|
|
12
12
|
const firstGraphFile =
|
|
13
13
|
Object.values(graphs)
|
|
14
|
-
.
|
|
14
|
+
.map((i) => {
|
|
15
|
+
if (typeof i === "string") {
|
|
16
|
+
return i.split(":").at(0);
|
|
17
|
+
} else if (i && typeof i === "object" && i.path) {
|
|
18
|
+
return i.path.split(":").at(0);
|
|
19
|
+
}
|
|
20
|
+
return null;
|
|
21
|
+
})
|
|
22
|
+
.filter(Boolean)
|
|
15
23
|
.at(0) || "index.mts";
|
|
16
24
|
|
|
17
25
|
// enforce API @langchain/langgraph resolution
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export function filterValidExportPath(
|
|
1
|
+
export function filterValidExportPath(
|
|
2
|
+
path: string | { path: string } | undefined,
|
|
3
|
+
) {
|
|
2
4
|
if (!path) return false;
|
|
3
|
-
|
|
5
|
+
const p = typeof path === "string" ? path : path.path;
|
|
6
|
+
return !p.split(":")[0].endsWith(".py");
|
|
4
7
|
}
|
langgraph_api/js/sse.py
CHANGED
|
@@ -91,7 +91,7 @@ class SSEDecoder:
|
|
|
91
91
|
|
|
92
92
|
sse = StreamPart(
|
|
93
93
|
event=self._event,
|
|
94
|
-
data=orjson.loads(self._data) if self._data else None,
|
|
94
|
+
data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
|
|
95
95
|
)
|
|
96
96
|
|
|
97
97
|
# NOTE: as per the SSE spec, do not reset last_event_id.
|
langgraph_api/js/yarn.lock
CHANGED
|
@@ -203,15 +203,15 @@
|
|
|
203
203
|
zod "^3.25.32"
|
|
204
204
|
zod-to-json-schema "^3.22.3"
|
|
205
205
|
|
|
206
|
-
"@langchain/langgraph-api@~0.0.
|
|
207
|
-
version "0.0.
|
|
208
|
-
resolved "https://registry.yarnpkg.com/@langchain/langgraph-api/-/langgraph-api-0.0.
|
|
209
|
-
integrity sha512-
|
|
206
|
+
"@langchain/langgraph-api@~0.0.59":
|
|
207
|
+
version "0.0.59"
|
|
208
|
+
resolved "https://registry.yarnpkg.com/@langchain/langgraph-api/-/langgraph-api-0.0.59.tgz#a3d69b8cc68ceebd8a8d86b77abd03924ddcd02c"
|
|
209
|
+
integrity sha512-pUt3yKB2z1nsXdhqpRQgVefYg5MdJVsqH64gTNzSb/JYJBPIUj2h8XttRx9+3mHtVqCzvL2bb+HVIterNFOKtw==
|
|
210
210
|
dependencies:
|
|
211
211
|
"@babel/code-frame" "^7.26.2"
|
|
212
212
|
"@hono/node-server" "^1.12.0"
|
|
213
213
|
"@hono/zod-validator" "^0.2.2"
|
|
214
|
-
"@langchain/langgraph-ui" "0.0.
|
|
214
|
+
"@langchain/langgraph-ui" "0.0.59"
|
|
215
215
|
"@types/json-schema" "^7.0.15"
|
|
216
216
|
"@typescript/vfs" "^1.6.0"
|
|
217
217
|
dedent "^1.5.3"
|
|
@@ -256,10 +256,10 @@
|
|
|
256
256
|
p-retry "4"
|
|
257
257
|
uuid "^9.0.0"
|
|
258
258
|
|
|
259
|
-
"@langchain/langgraph-ui@0.0.
|
|
260
|
-
version "0.0.
|
|
261
|
-
resolved "https://registry.yarnpkg.com/@langchain/langgraph-ui/-/langgraph-ui-0.0.
|
|
262
|
-
integrity sha512-
|
|
259
|
+
"@langchain/langgraph-ui@0.0.59", "@langchain/langgraph-ui@~0.0.59":
|
|
260
|
+
version "0.0.59"
|
|
261
|
+
resolved "https://registry.yarnpkg.com/@langchain/langgraph-ui/-/langgraph-ui-0.0.59.tgz#41988eae48c7520c5ebfa9bcdda65ddadfe1aab9"
|
|
262
|
+
integrity sha512-x6Jqt7TZRfHGU0MyxVbz7ugucT/oJQm9kM+DlGdmfAEm2JrNqgy1W85E589xJCJjpuRiDLpP7NiqSwnp8lCEqg==
|
|
263
263
|
dependencies:
|
|
264
264
|
"@commander-js/extra-typings" "^13.0.0"
|
|
265
265
|
commander "^13.0.0"
|
langgraph_api/logging.py
CHANGED
|
@@ -19,8 +19,8 @@ LOG_LEVEL = log_env("LOG_LEVEL", cast=str, default="INFO")
|
|
|
19
19
|
logging.getLogger().setLevel(LOG_LEVEL.upper())
|
|
20
20
|
logging.getLogger("psycopg").setLevel(logging.WARNING)
|
|
21
21
|
|
|
22
|
-
worker_config
|
|
23
|
-
|
|
22
|
+
worker_config = contextvars.ContextVar[dict[str, typing.Any] | None](
|
|
23
|
+
"worker_config", default=None
|
|
24
24
|
)
|
|
25
25
|
|
|
26
26
|
# custom processors
|
|
@@ -165,6 +165,6 @@ if not structlog.is_configured():
|
|
|
165
165
|
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
166
166
|
],
|
|
167
167
|
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
168
|
-
wrapper_class=structlog.stdlib.BoundLogger,
|
|
168
|
+
wrapper_class=structlog.stdlib.BoundLogger, # type: ignore[invalid-argument-type]
|
|
169
169
|
cache_logger_on_first_use=True,
|
|
170
170
|
)
|
|
@@ -84,6 +84,7 @@ class AccessLoggerMiddleware:
|
|
|
84
84
|
|
|
85
85
|
if method and route and status:
|
|
86
86
|
HTTP_METRICS_COLLECTOR.record_request(method, route, status, latency)
|
|
87
|
+
qs = scope.get("query_string")
|
|
87
88
|
self.logger.log(
|
|
88
89
|
_get_level(status),
|
|
89
90
|
f"{method} {path} {status} {latency}ms",
|
|
@@ -93,7 +94,7 @@ class AccessLoggerMiddleware:
|
|
|
93
94
|
latency_ms=latency,
|
|
94
95
|
route=route,
|
|
95
96
|
path_params=scope.get("path_params"),
|
|
96
|
-
query_string=
|
|
97
|
+
query_string=qs.decode() if qs else "",
|
|
97
98
|
proto=scope.get("http_version"),
|
|
98
99
|
req_header=_headers_to_dict(scope.get("headers")),
|
|
99
100
|
res_header=_headers_to_dict(info["response"].get("headers")),
|
langgraph_api/models/run.py
CHANGED
|
@@ -3,11 +3,11 @@ import time
|
|
|
3
3
|
import urllib.parse
|
|
4
4
|
import uuid
|
|
5
5
|
from collections.abc import Mapping, Sequence
|
|
6
|
-
from typing import Any, NamedTuple
|
|
6
|
+
from typing import Any, NamedTuple, cast
|
|
7
7
|
from uuid import UUID
|
|
8
8
|
|
|
9
9
|
import orjson
|
|
10
|
-
|
|
10
|
+
import structlog
|
|
11
11
|
from starlette.authentication import BaseUser
|
|
12
12
|
from starlette.exceptions import HTTPException
|
|
13
13
|
from typing_extensions import TypedDict
|
|
@@ -27,7 +27,10 @@ from langgraph_api.schema import (
|
|
|
27
27
|
)
|
|
28
28
|
from langgraph_api.utils import AsyncConnectionProto, get_auth_ctx
|
|
29
29
|
from langgraph_api.utils.headers import should_include_header
|
|
30
|
-
from
|
|
30
|
+
from langgraph_api.utils.uuids import uuid7
|
|
31
|
+
from langgraph_runtime.ops import Runs
|
|
32
|
+
|
|
33
|
+
logger = structlog.stdlib.get_logger(__name__)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
class LangSmithTracer(TypedDict, total=False):
|
|
@@ -183,7 +186,7 @@ LANGSMITH_PROJECT = "langsmith-project"
|
|
|
183
186
|
DEFAULT_RUN_HEADERS_EXCLUDE = {"x-api-key", "x-tenant-id", "x-service-key"}
|
|
184
187
|
|
|
185
188
|
|
|
186
|
-
def get_configurable_headers(headers:
|
|
189
|
+
def get_configurable_headers(headers: Mapping[str, str]) -> dict[str, str]:
|
|
187
190
|
"""Extract headers that should be added to run configuration.
|
|
188
191
|
|
|
189
192
|
This function handles special cases like langsmith-trace and baggage headers,
|
|
@@ -249,7 +252,7 @@ async def create_valid_run(
|
|
|
249
252
|
request_id = headers.get("x-request-id") # Will be null in the crons scheduler.
|
|
250
253
|
(
|
|
251
254
|
assistant_id,
|
|
252
|
-
|
|
255
|
+
thread_id_,
|
|
253
256
|
checkpoint_id,
|
|
254
257
|
run_id,
|
|
255
258
|
) = _get_ids(
|
|
@@ -258,7 +261,7 @@ async def create_valid_run(
|
|
|
258
261
|
run_id=run_id,
|
|
259
262
|
)
|
|
260
263
|
if (
|
|
261
|
-
|
|
264
|
+
thread_id_ is None
|
|
262
265
|
and (command := payload.get("command"))
|
|
263
266
|
and command.get("resume")
|
|
264
267
|
):
|
|
@@ -266,7 +269,9 @@ async def create_valid_run(
|
|
|
266
269
|
status_code=400,
|
|
267
270
|
detail="You must provide a thread_id when resuming.",
|
|
268
271
|
)
|
|
269
|
-
temporary =
|
|
272
|
+
temporary = (
|
|
273
|
+
thread_id_ is None and payload.get("on_completion", "delete") == "delete"
|
|
274
|
+
)
|
|
270
275
|
stream_resumable = payload.get("stream_resumable", False)
|
|
271
276
|
stream_mode, multitask_strategy, prevent_insert_if_inflight = assign_defaults(
|
|
272
277
|
payload
|
|
@@ -296,7 +301,7 @@ async def create_valid_run(
|
|
|
296
301
|
configurable.update(get_configurable_headers(headers))
|
|
297
302
|
ctx = get_auth_ctx()
|
|
298
303
|
if ctx:
|
|
299
|
-
user = ctx.user
|
|
304
|
+
user = cast(BaseUser | None, ctx.user)
|
|
300
305
|
user_id = get_user_id(user)
|
|
301
306
|
configurable["langgraph_auth_user"] = user
|
|
302
307
|
configurable["langgraph_auth_user_id"] = user_id
|
|
@@ -336,7 +341,7 @@ async def create_valid_run(
|
|
|
336
341
|
metadata=payload.get("metadata"),
|
|
337
342
|
status="pending",
|
|
338
343
|
user_id=user_id,
|
|
339
|
-
thread_id=
|
|
344
|
+
thread_id=thread_id_,
|
|
340
345
|
run_id=run_id,
|
|
341
346
|
multitask_strategy=multitask_strategy,
|
|
342
347
|
prevent_insert_if_inflight=prevent_insert_if_inflight,
|
|
@@ -362,7 +367,7 @@ async def create_valid_run(
|
|
|
362
367
|
logger.info(
|
|
363
368
|
"Created run",
|
|
364
369
|
run_id=str(run_id),
|
|
365
|
-
thread_id=str(
|
|
370
|
+
thread_id=str(thread_id_),
|
|
366
371
|
assistant_id=str(assistant_id),
|
|
367
372
|
multitask_strategy=multitask_strategy,
|
|
368
373
|
stream_mode=stream_mode,
|
|
@@ -383,7 +388,7 @@ async def create_valid_run(
|
|
|
383
388
|
await Runs.cancel(
|
|
384
389
|
conn,
|
|
385
390
|
[run["run_id"] for run in inflight_runs],
|
|
386
|
-
thread_id=
|
|
391
|
+
thread_id=thread_id_,
|
|
387
392
|
action=multitask_strategy,
|
|
388
393
|
)
|
|
389
394
|
except HTTPException:
|
|
@@ -415,15 +420,15 @@ def _get_ids(
|
|
|
415
420
|
assistant_id = get_assistant_id(payload["assistant_id"])
|
|
416
421
|
|
|
417
422
|
# ensure UUID validity defaults
|
|
418
|
-
assistant_id,
|
|
423
|
+
assistant_id, thread_id_, checkpoint_id = ensure_ids(
|
|
419
424
|
assistant_id, thread_id, payload
|
|
420
425
|
)
|
|
421
426
|
|
|
422
|
-
run_id = run_id or
|
|
427
|
+
run_id = run_id or uuid7()
|
|
423
428
|
|
|
424
429
|
return _Ids(
|
|
425
430
|
assistant_id,
|
|
426
|
-
|
|
431
|
+
thread_id_,
|
|
427
432
|
checkpoint_id,
|
|
428
433
|
run_id,
|
|
429
434
|
)
|
langgraph_api/patch.py
CHANGED
|
@@ -41,8 +41,8 @@ async def StreamingResponse_stream_response(self, send: Send) -> None:
|
|
|
41
41
|
|
|
42
42
|
# patch StreamingResponse.stream_response
|
|
43
43
|
|
|
44
|
-
StreamingResponse.stream_response = StreamingResponse_stream_response
|
|
44
|
+
StreamingResponse.stream_response = StreamingResponse_stream_response # type: ignore[invalid-assignment]
|
|
45
45
|
|
|
46
46
|
# patch Response.render
|
|
47
47
|
|
|
48
|
-
Response.render = Response_render
|
|
48
|
+
Response.render = Response_render # type: ignore[invalid-assignment]
|
|
@@ -18,9 +18,9 @@ import os
|
|
|
18
18
|
import pathlib
|
|
19
19
|
import signal
|
|
20
20
|
from contextlib import asynccontextmanager
|
|
21
|
+
from typing import cast
|
|
21
22
|
|
|
22
23
|
import structlog
|
|
23
|
-
import uvloop
|
|
24
24
|
|
|
25
25
|
from langgraph_runtime.lifespan import lifespan
|
|
26
26
|
from langgraph_runtime.metrics import get_metrics
|
|
@@ -36,21 +36,22 @@ async def health_and_metrics_server():
|
|
|
36
36
|
class HealthAndMetricsHandler(http.server.SimpleHTTPRequestHandler):
|
|
37
37
|
def log_message(self, format, *args):
|
|
38
38
|
# Skip logging for /ok and /metrics endpoints
|
|
39
|
-
if self
|
|
39
|
+
if getattr(self, "path", None) in ["/ok", "/metrics"]:
|
|
40
40
|
return
|
|
41
41
|
# Log other requests normally
|
|
42
42
|
super().log_message(format, *args)
|
|
43
43
|
|
|
44
44
|
def do_GET(self):
|
|
45
|
-
|
|
45
|
+
path = getattr(self, "path", None)
|
|
46
|
+
if path == "/ok":
|
|
46
47
|
self.send_response(200)
|
|
47
48
|
self.send_header("Content-Type", "application/json")
|
|
48
49
|
self.send_header("Content-Length", ok_len)
|
|
49
50
|
self.end_headers()
|
|
50
51
|
self.wfile.write(ok)
|
|
51
|
-
elif
|
|
52
|
+
elif path == "/metrics":
|
|
52
53
|
metrics = get_metrics()
|
|
53
|
-
worker_metrics = metrics["workers"]
|
|
54
|
+
worker_metrics = cast(dict[str, int], metrics["workers"])
|
|
54
55
|
workers_max = worker_metrics["max"]
|
|
55
56
|
workers_active = worker_metrics["active"]
|
|
56
57
|
workers_available = worker_metrics["available"]
|
|
@@ -93,20 +94,27 @@ async def health_and_metrics_server():
|
|
|
93
94
|
httpd.shutdown()
|
|
94
95
|
|
|
95
96
|
|
|
96
|
-
async def entrypoint(
|
|
97
|
+
async def entrypoint(
|
|
98
|
+
grpc_port: int | None = None, entrypoint_name: str = "python-queue"
|
|
99
|
+
):
|
|
97
100
|
from langgraph_api import logging as lg_logging
|
|
98
101
|
from langgraph_api.api import user_router
|
|
99
102
|
|
|
100
|
-
lg_logging.set_logging_context({"entrypoint":
|
|
103
|
+
lg_logging.set_logging_context({"entrypoint": entrypoint_name})
|
|
101
104
|
tasks: set[asyncio.Task] = set()
|
|
102
105
|
tasks.add(asyncio.create_task(health_and_metrics_server()))
|
|
103
106
|
|
|
104
107
|
original_lifespan = user_router.router.lifespan_context if user_router else None
|
|
105
108
|
|
|
106
109
|
@asynccontextmanager
|
|
107
|
-
async def combined_lifespan(
|
|
110
|
+
async def combined_lifespan(
|
|
111
|
+
app, with_cron_scheduler=False, grpc_port=None, taskset=None
|
|
112
|
+
):
|
|
108
113
|
async with lifespan(
|
|
109
|
-
app,
|
|
114
|
+
app,
|
|
115
|
+
with_cron_scheduler=with_cron_scheduler,
|
|
116
|
+
grpc_port=grpc_port,
|
|
117
|
+
taskset=taskset,
|
|
110
118
|
):
|
|
111
119
|
if original_lifespan:
|
|
112
120
|
async with original_lifespan(app):
|
|
@@ -114,11 +122,13 @@ async def entrypoint():
|
|
|
114
122
|
else:
|
|
115
123
|
yield
|
|
116
124
|
|
|
117
|
-
async with combined_lifespan(
|
|
125
|
+
async with combined_lifespan(
|
|
126
|
+
None, with_cron_scheduler=False, grpc_port=grpc_port, taskset=tasks
|
|
127
|
+
):
|
|
118
128
|
await asyncio.gather(*tasks)
|
|
119
129
|
|
|
120
130
|
|
|
121
|
-
async def main():
|
|
131
|
+
async def main(grpc_port: int | None = None, entrypoint_name: str = "python-queue"):
|
|
122
132
|
"""Run the queue entrypoint and shut down gracefully on SIGTERM/SIGINT."""
|
|
123
133
|
loop = asyncio.get_running_loop()
|
|
124
134
|
stop_event = asyncio.Event()
|
|
@@ -132,11 +142,9 @@ async def main():
|
|
|
132
142
|
except (NotImplementedError, RuntimeError):
|
|
133
143
|
signal.signal(signal.SIGTERM, lambda *_: _handle_signal())
|
|
134
144
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
entry_task = asyncio.create_task(entrypoint())
|
|
145
|
+
entry_task = asyncio.create_task(
|
|
146
|
+
entrypoint(grpc_port=grpc_port, entrypoint_name=entrypoint_name)
|
|
147
|
+
)
|
|
140
148
|
await stop_event.wait()
|
|
141
149
|
|
|
142
150
|
logger.warning("Cancelling queue entrypoint task")
|
|
@@ -146,10 +154,17 @@ async def main():
|
|
|
146
154
|
|
|
147
155
|
|
|
148
156
|
if __name__ == "__main__":
|
|
157
|
+
from langgraph_api import config
|
|
158
|
+
|
|
159
|
+
config.IS_QUEUE_ENTRYPOINT = True
|
|
149
160
|
with open(pathlib.Path(__file__).parent.parent / "logging.json") as file:
|
|
150
161
|
loaded_config = json.load(file)
|
|
151
162
|
logging.config.dictConfig(loaded_config)
|
|
163
|
+
try:
|
|
164
|
+
import uvloop # type: ignore[unresolved-import]
|
|
152
165
|
|
|
153
|
-
|
|
154
|
-
|
|
166
|
+
uvloop.install()
|
|
167
|
+
except ImportError:
|
|
168
|
+
pass
|
|
169
|
+
# run the entrypoint
|
|
155
170
|
asyncio.run(main())
|