langgraph-api 0.0.11__py3-none-any.whl → 0.0.13__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.
@@ -121,28 +121,27 @@ async def get_assistant_graph(
121
121
  assistant_ = await Assistants.get(conn, assistant_id)
122
122
  assistant = await fetchone(assistant_)
123
123
  config = await ajson_loads(assistant["config"])
124
- graph = await get_graph(assistant["graph_id"], config)
125
-
126
- xray: bool | int = False
127
- xray_query = request.query_params.get("xray")
128
- if xray_query:
129
- if xray_query in ("true", "True"):
130
- xray = True
131
- elif xray_query in ("false", "False"):
132
- xray = False
133
- else:
134
- try:
135
- xray = int(xray_query)
136
- except ValueError:
137
- raise HTTPException(422, detail="Invalid xray value") from None
138
-
139
- if xray <= 0:
140
- raise HTTPException(422, detail="Invalid xray value") from None
141
-
142
- if isinstance(graph, RemotePregel):
143
- drawable_graph = await graph.fetch_graph(xray=xray)
144
- return ApiResponse(drawable_graph.to_json())
145
- return ApiResponse(graph.get_graph(xray=xray).to_json())
124
+ async with get_graph(assistant["graph_id"], config) as graph:
125
+ xray: bool | int = False
126
+ xray_query = request.query_params.get("xray")
127
+ if xray_query:
128
+ if xray_query in ("true", "True"):
129
+ xray = True
130
+ elif xray_query in ("false", "False"):
131
+ xray = False
132
+ else:
133
+ try:
134
+ xray = int(xray_query)
135
+ except ValueError:
136
+ raise HTTPException(422, detail="Invalid xray value") from None
137
+
138
+ if xray <= 0:
139
+ raise HTTPException(422, detail="Invalid xray value") from None
140
+
141
+ if isinstance(graph, RemotePregel):
142
+ drawable_graph = await graph.fetch_graph(xray=xray)
143
+ return ApiResponse(drawable_graph.to_json())
144
+ return ApiResponse(graph.get_graph(xray=xray).to_json())
146
145
 
147
146
 
148
147
  @retry_db
@@ -156,29 +155,29 @@ async def get_assistant_subgraphs(
156
155
  assistant_ = await Assistants.get(conn, assistant_id)
157
156
  assistant = await fetchone(assistant_)
158
157
  config = await ajson_loads(assistant["config"])
159
- graph = await get_graph(assistant["graph_id"], config)
160
- namespace = request.path_params.get("namespace")
158
+ async with get_graph(assistant["graph_id"], config) as graph:
159
+ namespace = request.path_params.get("namespace")
160
+
161
+ if isinstance(graph, RemotePregel):
162
+ return ApiResponse(
163
+ await graph.fetch_subgraphs(
164
+ namespace=namespace,
165
+ recurse=request.query_params.get("recurse", "False")
166
+ in ("true", "True"),
167
+ )
168
+ )
161
169
 
162
- if isinstance(graph, RemotePregel):
163
170
  return ApiResponse(
164
- await graph.fetch_subgraphs(
165
- namespace=namespace,
166
- recurse=request.query_params.get("recurse", "False")
167
- in ("true", "True"),
168
- )
171
+ {
172
+ ns: _graph_schemas(subgraph)
173
+ async for ns, subgraph in graph.aget_subgraphs(
174
+ namespace=namespace,
175
+ recurse=request.query_params.get("recurse", "False")
176
+ in ("true", "True"),
177
+ )
178
+ }
169
179
  )
170
180
 
171
- return ApiResponse(
172
- {
173
- ns: _graph_schemas(subgraph)
174
- async for ns, subgraph in graph.aget_subgraphs(
175
- namespace=namespace,
176
- recurse=request.query_params.get("recurse", "False")
177
- in ("true", "True"),
178
- )
179
- }
180
- )
181
-
182
181
 
183
182
  @retry_db
184
183
  async def get_assistant_schemas(
@@ -191,48 +190,47 @@ async def get_assistant_schemas(
191
190
  assistant_ = await Assistants.get(conn, assistant_id)
192
191
  assistant = await fetchone(assistant_)
193
192
  config = await ajson_loads(assistant["config"])
194
- graph = await get_graph(assistant["graph_id"], config)
193
+ async with get_graph(assistant["graph_id"], config) as graph:
194
+ if isinstance(graph, RemotePregel):
195
+ schemas = await graph.fetch_state_schema()
196
+ return ApiResponse(
197
+ {
198
+ "graph_id": assistant["graph_id"],
199
+ "input_schema": schemas.get("input"),
200
+ "output_schema": schemas.get("output"),
201
+ "state_schema": schemas.get("state"),
202
+ "config_schema": schemas.get("config"),
203
+ }
204
+ )
205
+
206
+ try:
207
+ input_schema = graph.get_input_schema().schema()
208
+ except Exception:
209
+ input_schema = None
210
+ try:
211
+ output_schema = graph.get_output_schema().schema()
212
+ except Exception:
213
+ output_schema = None
195
214
 
196
- if isinstance(graph, RemotePregel):
197
- schemas = await graph.fetch_state_schema()
215
+ state_schema = _state_jsonschema(graph)
216
+ try:
217
+ config_schema = (
218
+ graph.config_schema().__fields__["configurable"].annotation.schema()
219
+ if "configurable" in graph.config_schema().__fields__
220
+ else {}
221
+ )
222
+ except Exception:
223
+ config_schema = None
198
224
  return ApiResponse(
199
225
  {
200
226
  "graph_id": assistant["graph_id"],
201
- "input_schema": schemas.get("input"),
202
- "output_schema": schemas.get("output"),
203
- "state_schema": schemas.get("state"),
204
- "config_schema": schemas.get("config"),
227
+ "input_schema": input_schema,
228
+ "output_schema": output_schema,
229
+ "state_schema": state_schema,
230
+ "config_schema": config_schema,
205
231
  }
206
232
  )
207
233
 
208
- try:
209
- input_schema = graph.get_input_schema().schema()
210
- except Exception:
211
- input_schema = None
212
- try:
213
- output_schema = graph.get_output_schema().schema()
214
- except Exception:
215
- output_schema = None
216
-
217
- state_schema = _state_jsonschema(graph)
218
- try:
219
- config_schema = (
220
- graph.config_schema().__fields__["configurable"].annotation.schema()
221
- if "configurable" in graph.config_schema().__fields__
222
- else {}
223
- )
224
- except Exception:
225
- config_schema = None
226
- return ApiResponse(
227
- {
228
- "graph_id": assistant["graph_id"],
229
- "input_schema": input_schema,
230
- "output_schema": output_schema,
231
- "state_schema": state_schema,
232
- "config_schema": config_schema,
233
- }
234
- )
235
-
236
234
 
237
235
  @retry_db
238
236
  async def patch_assistant(
@@ -32,7 +32,7 @@ def get_openapi_spec() -> str:
32
32
  openapi["components"]["securitySchemes"] = {
33
33
  "x-api-key": {"type": "apiKey", "in": "header", "name": "x-api-key"}
34
34
  }
35
- elif LANGGRAPH_AUTH_TYPE == "custom":
35
+ if LANGGRAPH_AUTH:
36
36
  # Allow user to specify OpenAPI security configuration
37
37
  if isinstance(LANGGRAPH_AUTH, dict) and "openapi" in LANGGRAPH_AUTH:
38
38
  openapi_config = LANGGRAPH_AUTH["openapi"]
@@ -24,7 +24,8 @@ from starlette.requests import HTTPConnection, Request
24
24
  from starlette.responses import Response
25
25
 
26
26
  from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
27
- from langgraph_api.config import LANGGRAPH_AUTH
27
+ from langgraph_api.auth.studio_user import StudioUser
28
+ from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
28
29
 
29
30
  logger = structlog.stdlib.get_logger(__name__)
30
31
 
@@ -91,12 +92,23 @@ async def handle_event(
91
92
  handler = _get_handler(auth, ctx)
92
93
  if not handler:
93
94
  return
94
-
95
- result = await handler(ctx=ctx, value=value)
95
+ try:
96
+ result = await handler(ctx=ctx, value=value)
97
+ except Auth.exceptions.HTTPException as e:
98
+ raise HTTPException(
99
+ status_code=e.status_code,
100
+ detail=e.detail,
101
+ headers=dict(e.headers) if e.headers else None,
102
+ ) from e
103
+ except AssertionError as e:
104
+ raise HTTPException(
105
+ status_code=403,
106
+ detail=str(e),
107
+ ) from e
96
108
 
97
109
  if result in (None, True):
98
110
  return
99
- if result is True:
111
+ if result is False:
100
112
  raise HTTPException(403, "Forbidden")
101
113
 
102
114
  if not isinstance(result, dict):
@@ -111,30 +123,31 @@ async def handle_event(
111
123
  class CustomAuthBackend(AuthenticationBackend):
112
124
  def __init__(
113
125
  self,
114
- fn: (
115
- Callable[
116
- [Request],
117
- Awaitable[tuple[list[str], Any]],
118
- ]
119
- | None
120
- ) = None,
126
+ fn: Callable[
127
+ [Request],
128
+ Awaitable[tuple[list[str], Any]],
129
+ ],
121
130
  disable_studio_auth: bool = False,
122
131
  ):
123
- if fn is None:
124
- self.fn = None
125
- elif not inspect.iscoroutinefunction(fn):
132
+ if not inspect.iscoroutinefunction(fn):
126
133
  self.fn = functools.partial(run_in_threadpool, fn)
127
134
  else:
128
135
  self.fn = fn
129
136
  self._param_names = (
130
- get_named_arguments(fn, supported_params=SUPPORTED_PARAMETERS)
137
+ _get_named_arguments(fn, supported_params=SUPPORTED_PARAMETERS)
131
138
  if fn
132
139
  else None
133
140
  )
141
+ self.ls_auth = None
134
142
  if not disable_studio_auth:
135
- self.ls_auth = LangsmithAuthBackend()
136
- else:
137
- self.ls_auth = None
143
+ if LANGGRAPH_AUTH_TYPE == "langsmith":
144
+ self.ls_auth = LangsmithAuthBackend()
145
+ elif (
146
+ LANGGRAPH_AUTH_TYPE == "noop"
147
+ and (auth_type := os.environ.get("LANGSMITH_LANGGRAPH_API_VARIANT"))
148
+ and auth_type == "local_dev"
149
+ ):
150
+ self.ls_auth = StudioNoopAuthBackend()
138
151
 
139
152
  def __str__(self):
140
153
  return (
@@ -160,8 +173,24 @@ class CustomAuthBackend(AuthenticationBackend):
160
173
  )
161
174
  response = await self.fn(**args)
162
175
  return _normalize_auth_response(response)
176
+ except (AuthenticationError, HTTPException):
177
+ raise
178
+ except Auth.exceptions.HTTPException as e:
179
+ raise HTTPException(
180
+ status_code=e.status_code,
181
+ detail=e.detail,
182
+ headers=dict(e.headers) if e.headers else None,
183
+ ) from None
163
184
  except AssertionError as e:
164
185
  raise AuthenticationError(str(e)) from None
186
+ except Exception as e:
187
+ await logger.aerror("Error authenticating request", exc_info=e)
188
+ status_code = getattr(e, "status_code", 401)
189
+ detail = getattr(e, "detail", "Unauthorized")
190
+ headers = getattr(e, "headers", None)
191
+ raise HTTPException(
192
+ status_code=status_code, detail=detail, headers=headers
193
+ ) from None
165
194
 
166
195
 
167
196
  def _get_custom_auth_middleware(
@@ -174,8 +203,26 @@ def _get_custom_auth_middleware(
174
203
  path = config.get("path")
175
204
  disable_studio_auth = config.get("disable_studio_auth", disable_studio_auth)
176
205
  auth_instance = _get_auth_instance(path)
206
+ if auth_instance is None:
207
+ raise ValueError(
208
+ f"Custom Auth object not found at path: {path}. "
209
+ "Check that the path is correct and the file is available."
210
+ "Auth objects are created like:\n"
211
+ "from langgraph_sdk import Auth\n"
212
+ "auth = Auth()"
213
+ )
214
+ if auth_instance._authenticate_handler is None:
215
+ raise ValueError(
216
+ f"Custom Auth object at path: {path} does not have an authenticate handler."
217
+ "Please define one like:\n"
218
+ "from langgraph_sdk import Auth\n"
219
+ "auth = Auth()\n"
220
+ "@auth.authenticate\n"
221
+ "async def authenticate(request):\n"
222
+ ' return "my-user-id"'
223
+ )
177
224
  result = CustomAuthBackend(
178
- auth_instance._authenticate_handler if auth_instance else None,
225
+ auth_instance._authenticate_handler,
179
226
  disable_studio_auth,
180
227
  )
181
228
  logger.info(f"Loaded custom auth middleware: {str(result)}")
@@ -288,7 +335,7 @@ def _solve_fastapi_dependencies(
288
335
 
289
336
  _param_names = {
290
337
  k
291
- for k in get_named_arguments(
338
+ for k in _get_named_arguments(
292
339
  fn, supported_params=SUPPORTED_PARAMETERS | dict(deps)
293
340
  )
294
341
  if k not in dependents
@@ -535,7 +582,14 @@ def _get_handler(auth: Auth, ctx: Auth.types.AuthContext) -> Auth.types.Handler
535
582
  return None
536
583
 
537
584
 
538
- def get_named_arguments(fn: Callable, supported_params: dict) -> set[str]:
585
+ class StudioNoopAuthBackend(AuthenticationBackend):
586
+ async def authenticate(
587
+ self, conn: HTTPConnection
588
+ ) -> tuple[AuthCredentials, BaseUser] | None:
589
+ return AuthCredentials(), StudioUser("langgraph-studio-user")
590
+
591
+
592
+ def _get_named_arguments(fn: Callable, supported_params: dict) -> set[str]:
539
593
  """Get the named arguments that a function accepts, ensuring they're supported."""
540
594
  sig = inspect.signature(fn)
541
595
  # Check for unsupported required parameters
@@ -5,11 +5,11 @@ from starlette.authentication import (
5
5
  AuthenticationBackend,
6
6
  AuthenticationError,
7
7
  BaseUser,
8
- SimpleUser,
9
8
  )
10
9
  from starlette.requests import HTTPConnection
11
10
 
12
11
  from langgraph_api.auth.langsmith.client import auth_client
12
+ from langgraph_api.auth.studio_user import StudioUser
13
13
  from langgraph_api.config import (
14
14
  LANGSMITH_AUTH_VERIFY_TENANT_ID,
15
15
  LANGSMITH_TENANT_ID,
@@ -64,4 +64,6 @@ class LangsmithAuthBackend(AuthenticationBackend):
64
64
  if auth_dict["tenant_id"] != LANGSMITH_TENANT_ID:
65
65
  raise AuthenticationError("Invalid tenant ID")
66
66
 
67
- return AuthCredentials(["authenticated"]), SimpleUser(auth_dict.get("user_id"))
67
+ return AuthCredentials(["authenticated"]), StudioUser(
68
+ auth_dict.get("user_id"), is_authenticated=True
69
+ )
@@ -8,23 +8,23 @@ from starlette.requests import HTTPConnection
8
8
  from starlette.responses import JSONResponse
9
9
  from starlette.types import Receive, Scope, Send
10
10
 
11
- from langgraph_api.config import LANGGRAPH_AUTH_TYPE
11
+ from langgraph_api.config import LANGGRAPH_AUTH, LANGGRAPH_AUTH_TYPE
12
12
 
13
13
  logger = structlog.stdlib.get_logger(__name__)
14
14
 
15
15
 
16
16
  def get_auth_backend():
17
17
  logger.info(f"Using auth of type={LANGGRAPH_AUTH_TYPE}")
18
+ if LANGGRAPH_AUTH:
19
+ from langgraph_api.auth.custom import get_custom_auth_middleware
20
+
21
+ return get_custom_auth_middleware()
22
+
18
23
  if LANGGRAPH_AUTH_TYPE == "langsmith":
19
24
  from langgraph_api.auth.langsmith.backend import LangsmithAuthBackend
20
25
 
21
26
  return LangsmithAuthBackend()
22
27
 
23
- if LANGGRAPH_AUTH_TYPE == "custom":
24
- from langgraph_api.auth.custom import get_custom_auth_middleware
25
-
26
- return get_custom_auth_middleware()
27
-
28
28
  from langgraph_api.auth.noop import NoopAuthBackend
29
29
 
30
30
  return NoopAuthBackend()
@@ -0,0 +1,6 @@
1
+ from langgraph_sdk.auth.types import StudioUser as StudioUserBase
2
+ from starlette.authentication import BaseUser
3
+
4
+
5
+ class StudioUser(StudioUserBase, BaseUser):
6
+ """StudioUser class."""
langgraph_api/config.py CHANGED
@@ -51,9 +51,6 @@ LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
51
51
  default=LANGSMITH_TENANT_ID is not None,
52
52
  )
53
53
 
54
- if LANGGRAPH_AUTH:
55
- LANGGRAPH_AUTH_TYPE = "custom"
56
-
57
54
 
58
55
  if LANGGRAPH_AUTH_TYPE == "langsmith":
59
56
  LANGSMITH_AUTH_ENDPOINT = env("LANGSMITH_AUTH_ENDPOINT", cast=str)
langgraph_api/graph.py CHANGED
@@ -6,10 +6,11 @@ import inspect
6
6
  import json
7
7
  import os
8
8
  import sys
9
- from collections.abc import Callable
9
+ from collections.abc import AsyncIterator, Callable
10
+ from contextlib import asynccontextmanager
10
11
  from itertools import filterfalse
11
12
  from random import choice
12
- from typing import TYPE_CHECKING, NamedTuple
13
+ from typing import TYPE_CHECKING, Any, NamedTuple
13
14
  from uuid import UUID, uuid5
14
15
 
15
16
  import structlog
@@ -59,39 +60,58 @@ async def register_graph(graph_id: str, graph: GraphValue, config: dict | None)
59
60
  )
60
61
 
61
62
 
63
+ @asynccontextmanager
64
+ async def _generate_graph(value: Any) -> AsyncIterator[Any]:
65
+ """Yield a graph object regardless of its type."""
66
+ if isinstance(value, Pregel | RemotePregel):
67
+ yield value
68
+ elif hasattr(value, "__aenter__") and hasattr(value, "__aexit__"):
69
+ async with value as ctx_value:
70
+ yield ctx_value
71
+ elif hasattr(value, "__enter__") and hasattr(value, "__exit__"):
72
+ with value as ctx_value:
73
+ yield ctx_value
74
+ elif asyncio.iscoroutine(value):
75
+ yield await value
76
+ else:
77
+ yield value
78
+
79
+
80
+ @asynccontextmanager
62
81
  async def get_graph(
63
82
  graph_id: str,
64
83
  config: Config,
65
84
  *,
66
85
  checkpointer: BaseCheckpointSaver | None = None,
67
86
  store: BaseStore | None = None,
68
- ) -> Pregel:
87
+ ) -> AsyncIterator[Pregel]:
69
88
  """Return the runnable."""
70
89
  assert_graph_exists(graph_id)
71
90
  value = GRAPHS[graph_id]
72
91
  if graph_id in FACTORY_ACCEPTS_CONFIG:
73
92
  value = value(config) if FACTORY_ACCEPTS_CONFIG[graph_id] else value()
74
- if asyncio.iscoroutine(value):
75
- value = await value
76
- if isinstance(value, Graph):
77
- value = value.compile()
78
- if not isinstance(value, Pregel) and not isinstance(value, RemotePregel):
79
- raise HTTPException(
80
- status_code=424,
81
- detail=f"Graph '{graph_id}' is not valid. Review graph registration.",
82
- )
83
- if isinstance(value, RemotePregel):
84
- value.checkpointer = checkpointer
85
- value.name = graph_id
86
- return value
87
-
88
- update = {
89
- "checkpointer": checkpointer,
90
- "store": store,
91
- }
92
- if value.name == "LangGraph":
93
- update["name"] = graph_id
94
- return value.copy(update=update)
93
+
94
+ async with _generate_graph(value) as graph_obj:
95
+ if isinstance(graph_obj, Graph):
96
+ graph_obj = graph_obj.compile()
97
+ if not isinstance(graph_obj, Pregel | RemotePregel):
98
+ raise HTTPException(
99
+ status_code=424,
100
+ detail=f"Graph '{graph_id}' is not valid. Review graph registration.",
101
+ )
102
+ if isinstance(graph_obj, RemotePregel):
103
+ graph_obj.checkpointer = checkpointer
104
+ graph_obj.name = graph_id
105
+ yield graph_obj
106
+ return
107
+
108
+ update = {
109
+ "checkpointer": checkpointer,
110
+ "store": store,
111
+ }
112
+ if graph_obj.name == "LangGraph":
113
+ update["name"] = graph_id
114
+ yield graph_obj.copy(update=update)
95
115
 
96
116
 
97
117
  def graph_exists(graph_id: str) -> bool:
langgraph_api/stream.py CHANGED
@@ -77,11 +77,14 @@ def _map_cmd(cmd: RunCommand) -> Command:
77
77
 
78
78
  return Command(
79
79
  update=cmd.get("update"),
80
- goto=[
81
- it if isinstance(it, str) else Send(it["node"], it["input"]) for it in goto
82
- ]
83
- if goto
84
- else None,
80
+ goto=(
81
+ [
82
+ it if isinstance(it, str) else Send(it["node"], it["input"])
83
+ for it in goto
84
+ ]
85
+ if goto
86
+ else None
87
+ ),
85
88
  resume=cmd.get("resume"),
86
89
  )
87
90
 
@@ -104,11 +107,13 @@ async def astream_state(
104
107
  subgraphs = kwargs.get("subgraphs", False)
105
108
  temporary = kwargs.pop("temporary", False)
106
109
  config = kwargs.pop("config")
107
- graph = await get_graph(
108
- config["configurable"]["graph_id"],
109
- config,
110
- store=Store(),
111
- checkpointer=None if temporary else Checkpointer(conn),
110
+ graph = await stack.enter_async_context(
111
+ get_graph(
112
+ config["configurable"]["graph_id"],
113
+ config,
114
+ store=Store(),
115
+ checkpointer=None if temporary else Checkpointer(conn),
116
+ )
112
117
  )
113
118
  input = kwargs.pop("input")
114
119
  if cmd := kwargs.pop("command"):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: langgraph-api
3
- Version: 0.0.11
3
+ Version: 0.0.13
4
4
  Summary:
5
5
  License: Elastic-2.0
6
6
  Author: Nuno Campos
@@ -16,7 +16,7 @@ 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
18
  Requires-Dist: langgraph-checkpoint (>=2.0.7,<3.0)
19
- Requires-Dist: langgraph-sdk (>=0.1.47,<0.2.0)
19
+ Requires-Dist: langgraph-sdk (>=0.1.48,<0.2.0)
20
20
  Requires-Dist: langsmith (>=0.1.63,<0.3.0)
21
21
  Requires-Dist: orjson (>=3.10.1)
22
22
  Requires-Dist: pyjwt (>=2.9.0,<3.0.0)
@@ -1,25 +1,26 @@
1
1
  LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
2
2
  langgraph_api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  langgraph_api/api/__init__.py,sha256=tlMXuqnyJt99aSlUXwR-dS3w5X6sDDczJu4hbm2LP30,2057
4
- langgraph_api/api/assistants.py,sha256=3v4v7kLmlb2meSAnNt73toijnaSikbJgH9Jhb6IL04g,11270
4
+ langgraph_api/api/assistants.py,sha256=Cxryr4K4qFeRoT0gQTu-gld_jSPzMDRZRHoazlSBZVo,11585
5
5
  langgraph_api/api/meta.py,sha256=hueasWpTDQ6xYLo9Bzt2jhNH8XQRzreH8FTeFfnRoxQ,2700
6
- langgraph_api/api/openapi.py,sha256=3eRo3V6Pni8DjqHI_MNLn8F5CEOOGVBMXPEffft8Ubo,2781
6
+ langgraph_api/api/openapi.py,sha256=AUxfnD5hlRp7s-0g2hBC5dNSNk3HTwOLeJiF489DT44,2762
7
7
  langgraph_api/api/runs.py,sha256=wAzPXi_kcYB9BcLBL4FXgkBohWwCPIpe4XERnsnWnsA,16042
8
8
  langgraph_api/api/store.py,sha256=y7VIejpsE7rpPF-tiMGBqqBwWPZ1wb3o48th6NUvb5I,3849
9
9
  langgraph_api/api/threads.py,sha256=taU61XPcCEhBPCYPZcMDsgVDwwWUWJs8p-PrXFXWY48,8661
10
10
  langgraph_api/asyncio.py,sha256=XiFEllu-Kg4zAO084npHPYOPnLQRire3V75XrVQYMxE,6023
11
11
  langgraph_api/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- langgraph_api/auth/custom.py,sha256=8jyZz-HOC4NwpDASVAD2fP8priQSw8uEjZ8VPJNC5EE,18225
12
+ langgraph_api/auth/custom.py,sha256=_BlII18X8ji_t8qA9FIUq_ip99dMQGkNscjsuEiLHus,20527
13
13
  langgraph_api/auth/langsmith/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- langgraph_api/auth/langsmith/backend.py,sha256=uHeb5-h13NIjrX_LDAvfWYr3zpbJvlvbdUffch48hbM,2571
14
+ langgraph_api/auth/langsmith/backend.py,sha256=InScaL-HYCnxYEauhxU198gRZV9pJn9SzzBoR9Edn7g,2654
15
15
  langgraph_api/auth/langsmith/client.py,sha256=eKchvAom7hdkUXauD8vHNceBDDUijrFgdTV8bKd7x4Q,3998
16
- langgraph_api/auth/middleware.py,sha256=eHj4D2p2ASpMKzoTgMkEUXMhuubQdg9Jnq0gQS9qbvE,1637
16
+ langgraph_api/auth/middleware.py,sha256=qc7SbaFoeWaqxS1wbjZ2PPQ4iI2p9T0shWL7c6g0ed4,1636
17
17
  langgraph_api/auth/noop.py,sha256=vDJmzG2vArJxVzdHePvrJWahEa0dvGnhc2LEMMeiFz0,391
18
+ langgraph_api/auth/studio_user.py,sha256=FzFQRROKDlA9JjtBuwyZvk6Mbwno5M9RVYjDO6FU3F8,186
18
19
  langgraph_api/cli.py,sha256=7vQQiD3F50r-8KkbuFjwIz8LLbdKUTd4xZGUJPiO3yQ,11688
19
- langgraph_api/config.py,sha256=m3cf13AS9HVF84deJ3-9mmNTJ5GSVqcukES-FNbDbvo,2794
20
+ langgraph_api/config.py,sha256=JueNW95UDn7uId3atctyC9BPZTzmUqwF2Gtr2i-MZ-g,2739
20
21
  langgraph_api/cron_scheduler.py,sha256=CybK-9Jwopi_scObTHRyB7gyb0KjC4gqaT2GLe-WOFg,2587
21
22
  langgraph_api/errors.py,sha256=Bu_i5drgNTyJcLiyrwVE_6-XrSU50BHf9TDpttki9wQ,1690
22
- langgraph_api/graph.py,sha256=LXTo2lGnku3KPBiC9ssh4NcSvieT4Ks2vJuhPaqBCpY,15807
23
+ langgraph_api/graph.py,sha256=zjcjgtlvfPIEJws8RksC6wKvt3g-8IGaJj79ScSs6GE,16561
23
24
  langgraph_api/http.py,sha256=XrbyxpjtfSvnaWWh5ZLGpgZmY83WoDCrP_1GPguNiXI,4712
24
25
  langgraph_api/http_logger.py,sha256=Sxo_q-65tElauRvkzVLt9lJojgNdgtcHGBYD0IRyX7M,3146
25
26
  langgraph_api/js/.gitignore,sha256=qAah3Fq0HWAlfRj5ktZyC6QRQIsAolGLRGcRukA1XJI,33
@@ -64,7 +65,7 @@ langgraph_api/serde.py,sha256=VoJ7Z1IuqrQGXFzEP1qijAITtWCrmjtVqlCRuScjXJI,3533
64
65
  langgraph_api/server.py,sha256=afHDnL6b_fAIu_q4icnK60a74lHTTZOMIe1egdhRXIk,1522
65
66
  langgraph_api/sse.py,sha256=2wNodCOP2eg7a9mpSu0S3FQ0CHk2BBV_vv0UtIgJIcc,4034
66
67
  langgraph_api/state.py,sha256=8jx4IoTCOjTJuwzuXJKKFwo1VseHjNnw_CCq4x1SW14,2284
67
- langgraph_api/stream.py,sha256=u0gjbCrmYvVw8Ux6DgsYTojLCHSwM4Pi-0LhSLGY4HM,11546
68
+ langgraph_api/stream.py,sha256=uK1MFr3hp08o3yE-W5V36CljaPmo97VfpDtexa5eqOQ,11663
68
69
  langgraph_api/utils.py,sha256=o7TFlY25IjujeKdXgtyE2mMLPETIlrbOc3w6giYBq2Y,2509
69
70
  langgraph_api/validation.py,sha256=McizHlz-Ez8Jhdbc79mbPSde7GIuf2Jlbjx2yv_l6dA,4475
70
71
  langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -73,15 +74,15 @@ langgraph_license/validation.py,sha256=Uu_G8UGO_WTlLsBEY0gTVWjRR4czYGfw5YAD3HLZo
73
74
  langgraph_storage/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
75
  langgraph_storage/checkpoint.py,sha256=V4t2GwYEJdPCHbhq_4Udhlv0TWKDzlMu_rlNPdTDc50,3589
75
76
  langgraph_storage/database.py,sha256=Nr5zE9Fur3-tESkqe7xNXMf2QlBuw3H0CUie7jVa6Q4,6003
76
- langgraph_storage/ops.py,sha256=W3NcQbY1Q8EpsIs4HjGY5Yx2In0TYc9_D4SKdZYPaDQ,67679
77
+ langgraph_storage/ops.py,sha256=vinc095b_eZYSWAfK_trZbmb_IIGcp55lWJEzqRwTPU,67967
77
78
  langgraph_storage/queue.py,sha256=6cTZ0ubHu3S1T43yxHMVOwsQsDaJupByiU0sTUFFls8,3261
78
79
  langgraph_storage/retry.py,sha256=uvYFuXJ-T6S1QY1ZwkZHyZQbsvS-Ab68LSbzbUUSI2E,696
79
80
  langgraph_storage/store.py,sha256=D-p3cWc_umamkKp-6Cz3cAriSACpvM5nxUIvND6PuxE,2710
80
81
  langgraph_storage/ttl_dict.py,sha256=FlpEY8EANeXWKo_G5nmIotPquABZGyIJyk6HD9u6vqY,1533
81
82
  logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
82
- openapi.json,sha256=UxAGHZYM4PgNd48TSZt7f2lVuyPUkDadxBBhRy5jcmk,124512
83
- langgraph_api-0.0.11.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
84
- langgraph_api-0.0.11.dist-info/METADATA,sha256=dIfDLkeyakteByG5V-curloWZyMGWy77SUf1J3gtBok,4041
85
- langgraph_api-0.0.11.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
86
- langgraph_api-0.0.11.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
87
- langgraph_api-0.0.11.dist-info/RECORD,,
83
+ openapi.json,sha256=gh6FxpyQqspAuQQH3O22qqGW5owtFj45gyR15QAcS9k,124729
84
+ langgraph_api-0.0.13.dist-info/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
85
+ langgraph_api-0.0.13.dist-info/METADATA,sha256=F_D67LIP0IP8jFVJ5wUwieyIqFuAP_mtrDNe8uZS8Rs,4041
86
+ langgraph_api-0.0.13.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
87
+ langgraph_api-0.0.13.dist-info/entry_points.txt,sha256=3EYLgj89DfzqJHHYGxPH4A_fEtClvlRbWRUHaXO7hj4,77
88
+ langgraph_api-0.0.13.dist-info/RECORD,,
langgraph_storage/ops.py CHANGED
@@ -976,17 +976,17 @@ class Threads(Authenticated):
976
976
  if graph_id := metadata.get("graph_id"):
977
977
  # format latest checkpoint for response
978
978
  checkpointer.latest_iter = checkpoint
979
- graph = await get_graph(
979
+ async with get_graph(
980
980
  graph_id, thread_config, checkpointer=checkpointer
981
- )
982
- result = await graph.aget_state(config, subgraphs=subgraphs)
983
- if (
984
- result.metadata is not None
985
- and "checkpoint_ns" in result.metadata
986
- and result.metadata["checkpoint_ns"] == ""
987
- ):
988
- result.metadata.pop("checkpoint_ns")
989
- return result
981
+ ) as graph:
982
+ result = await graph.aget_state(config, subgraphs=subgraphs)
983
+ if (
984
+ result.metadata is not None
985
+ and "checkpoint_ns" in result.metadata
986
+ and result.metadata["checkpoint_ns"] == ""
987
+ ):
988
+ result.metadata.pop("checkpoint_ns")
989
+ return result
990
990
  else:
991
991
  return StateSnapshot(
992
992
  values={},
@@ -1034,32 +1034,36 @@ class Threads(Authenticated):
1034
1034
  config["configurable"].setdefault("graph_id", graph_id)
1035
1035
 
1036
1036
  checkpointer.latest_iter = checkpoint
1037
- graph = await get_graph(
1037
+ async with get_graph(
1038
1038
  graph_id, thread_config, checkpointer=checkpointer
1039
- )
1040
- update_config = config.copy()
1041
- update_config["configurable"] = {
1042
- **config["configurable"],
1043
- "checkpoint_ns": config["configurable"].get("checkpoint_ns", ""),
1044
- }
1045
- next_config = await graph.aupdate_state(
1046
- update_config, values, as_node=as_node
1047
- )
1039
+ ) as graph:
1040
+ update_config = config.copy()
1041
+ update_config["configurable"] = {
1042
+ **config["configurable"],
1043
+ "checkpoint_ns": config["configurable"].get(
1044
+ "checkpoint_ns", ""
1045
+ ),
1046
+ }
1047
+ next_config = await graph.aupdate_state(
1048
+ update_config, values, as_node=as_node
1049
+ )
1048
1050
 
1049
- # Get current state
1050
- state = await Threads.State.get(conn, config, subgraphs=False, ctx=ctx)
1051
- # Update thread values
1052
- for thread in conn.store["threads"]:
1053
- if thread["thread_id"] == thread_id:
1054
- thread["values"] = state.values
1055
- break
1056
-
1057
- return ThreadUpdateResponse(
1058
- checkpoint=next_config["configurable"],
1059
- # Including deprecated fields
1060
- configurable=next_config["configurable"],
1061
- checkpoint_id=next_config["configurable"]["checkpoint_id"],
1062
- )
1051
+ # Get current state
1052
+ state = await Threads.State.get(
1053
+ conn, config, subgraphs=False, ctx=ctx
1054
+ )
1055
+ # Update thread values
1056
+ for thread in conn.store["threads"]:
1057
+ if thread["thread_id"] == thread_id:
1058
+ thread["values"] = state.values
1059
+ break
1060
+
1061
+ return ThreadUpdateResponse(
1062
+ checkpoint=next_config["configurable"],
1063
+ # Including deprecated fields
1064
+ configurable=next_config["configurable"],
1065
+ checkpoint_id=next_config["configurable"]["checkpoint_id"],
1066
+ )
1063
1067
  else:
1064
1068
  raise HTTPException(status_code=400, detail="Thread has no graph ID.")
1065
1069
 
@@ -1094,25 +1098,24 @@ class Threads(Authenticated):
1094
1098
  thread_config = thread["config"]
1095
1099
  # If graph_id exists, get state history
1096
1100
  if graph_id := thread_metadata.get("graph_id"):
1097
- graph = await get_graph(
1101
+ async with get_graph(
1098
1102
  graph_id, thread_config, checkpointer=Checkpointer(conn)
1099
- )
1100
-
1101
- # Convert before parameter if it's a string
1102
- before_param = (
1103
- {"configurable": {"checkpoint_id": before}}
1104
- if isinstance(before, str)
1105
- else before
1106
- )
1107
-
1108
- states = [
1109
- state
1110
- async for state in graph.aget_state_history(
1111
- config, limit=limit, filter=metadata, before=before_param
1103
+ ) as graph:
1104
+ # Convert before parameter if it's a string
1105
+ before_param = (
1106
+ {"configurable": {"checkpoint_id": before}}
1107
+ if isinstance(before, str)
1108
+ else before
1112
1109
  )
1113
- ]
1114
1110
 
1115
- return states
1111
+ states = [
1112
+ state
1113
+ async for state in graph.aget_state_history(
1114
+ config, limit=limit, filter=metadata, before=before_param
1115
+ )
1116
+ ]
1117
+
1118
+ return states
1116
1119
 
1117
1120
  return []
1118
1121
 
openapi.json CHANGED
@@ -3242,7 +3242,11 @@
3242
3242
  "description": "The command to run.",
3243
3243
  "properties": {
3244
3244
  "update": {
3245
- "type": "object",
3245
+ "type": [
3246
+ "object",
3247
+ "array",
3248
+ "null"
3249
+ ],
3246
3250
  "title": "Update",
3247
3251
  "description": "An update to the state."
3248
3252
  },
@@ -3268,13 +3272,22 @@
3268
3272
  "$ref": "#/components/schemas/Send"
3269
3273
  }
3270
3274
  },
3271
- { "type": "string" },
3272
- { "type": "array", "items": { "type": "string" } },
3273
- { "type": "null" }
3275
+ {
3276
+ "type": "string"
3277
+ },
3278
+ {
3279
+ "type": "array",
3280
+ "items": {
3281
+ "type": "string"
3282
+ }
3283
+ },
3284
+ {
3285
+ "type": "null"
3286
+ }
3274
3287
  ],
3275
3288
  "title": "Goto",
3276
3289
  "description": "Name of the node(s) to navigate to next or node(s) to be executed with a provided input."
3277
- }
3290
+ }
3278
3291
  }
3279
3292
  },
3280
3293
  "RunCreateStateful": {