langgraph-api 0.4.45__py3-none-any.whl → 0.4.47__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/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.4.45"
1
+ __version__ = "0.4.47"
@@ -5,6 +5,8 @@ from starlette.exceptions import HTTPException
5
5
  from starlette.responses import Response
6
6
  from starlette.routing import BaseRoute
7
7
 
8
+ from langgraph_api.feature_flags import FF_USE_CORE_API
9
+ from langgraph_api.grpc_ops.ops import Threads as GrpcThreads
8
10
  from langgraph_api.route import ApiRequest, ApiResponse, ApiRoute
9
11
  from langgraph_api.schema import THREAD_FIELDS, ThreadStreamMode
10
12
  from langgraph_api.sse import EventSourceResponse
@@ -30,6 +32,8 @@ from langgraph_runtime.database import connect
30
32
  from langgraph_runtime.ops import Threads
31
33
  from langgraph_runtime.retry import retry_db
32
34
 
35
+ CrudThreads = GrpcThreads if FF_USE_CORE_API else Threads
36
+
33
37
 
34
38
  @retry_db
35
39
  async def create_thread(
@@ -41,7 +45,7 @@ async def create_thread(
41
45
  validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
42
46
  async with connect() as conn:
43
47
  thread_id = thread_id or str(uuid4())
44
- iter = await Threads.put(
48
+ iter = await CrudThreads.put(
45
49
  conn,
46
50
  thread_id,
47
51
  metadata=payload.get("metadata"),
@@ -78,7 +82,7 @@ async def search_threads(
78
82
  limit = int(payload.get("limit") or 10)
79
83
  offset = int(payload.get("offset") or 0)
80
84
  async with connect() as conn:
81
- threads_iter, next_offset = await Threads.search(
85
+ threads_iter, next_offset = await CrudThreads.search(
82
86
  conn,
83
87
  status=payload.get("status"),
84
88
  values=payload.get("values"),
@@ -103,7 +107,7 @@ async def count_threads(
103
107
  """Count threads."""
104
108
  payload = await request.json(ThreadCountRequest)
105
109
  async with connect() as conn:
106
- count = await Threads.count(
110
+ count = await CrudThreads.count(
107
111
  conn,
108
112
  status=payload.get("status"),
109
113
  values=payload.get("values"),
@@ -277,7 +281,7 @@ async def get_thread(
277
281
  thread_id = request.path_params["thread_id"]
278
282
  validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
279
283
  async with connect() as conn:
280
- thread = await Threads.get(conn, thread_id)
284
+ thread = await CrudThreads.get(conn, thread_id)
281
285
  return ApiResponse(await fetchone(thread))
282
286
 
283
287
 
@@ -290,7 +294,7 @@ async def patch_thread(
290
294
  validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
291
295
  payload = await request.json(ThreadPatch)
292
296
  async with connect() as conn:
293
- thread = await Threads.patch(
297
+ thread = await CrudThreads.patch(
294
298
  conn,
295
299
  thread_id,
296
300
  metadata=payload.get("metadata", {}),
@@ -305,7 +309,7 @@ async def delete_thread(request: ApiRequest):
305
309
  thread_id = request.path_params["thread_id"]
306
310
  validate_uuid(thread_id, "Invalid thread ID: must be a UUID")
307
311
  async with connect() as conn:
308
- tid = await Threads.delete(conn, thread_id)
312
+ tid = await CrudThreads.delete(conn, thread_id)
309
313
  await fetchone(tid)
310
314
  return Response(status_code=204)
311
315
 
@@ -314,7 +318,7 @@ async def delete_thread(request: ApiRequest):
314
318
  async def copy_thread(request: ApiRequest):
315
319
  thread_id = request.path_params["thread_id"]
316
320
  async with connect() as conn:
317
- iter = await Threads.copy(conn, thread_id)
321
+ iter = await CrudThreads.copy(conn, thread_id)
318
322
  return ApiResponse(await fetchone(iter, not_found_code=409))
319
323
 
320
324
 
langgraph_api/cli.py CHANGED
@@ -8,12 +8,10 @@ import typing
8
8
  from collections.abc import Mapping, Sequence
9
9
  from typing import Literal
10
10
 
11
- from typing_extensions import TypedDict
12
-
13
11
  if typing.TYPE_CHECKING:
14
12
  from packaging.version import Version
15
13
 
16
- from langgraph_api.config import HttpConfig, StoreConfig
14
+ from langgraph_api.config import AuthConfig, HttpConfig, StoreConfig
17
15
 
18
16
  logging.basicConfig(level=logging.INFO)
19
17
  logger = logging.getLogger(__name__)
@@ -81,51 +79,6 @@ def patch_environment(**kwargs):
81
79
  os.environ[key] = value
82
80
 
83
81
 
84
- class SecurityConfig(TypedDict, total=False):
85
- securitySchemes: dict
86
- security: list
87
- # path => {method => security}
88
- paths: dict[str, dict[str, list]]
89
-
90
-
91
- class CacheConfig(TypedDict, total=False):
92
- cache_keys: list[str]
93
- ttl_seconds: int
94
- max_size: int
95
-
96
-
97
- class AuthConfig(TypedDict, total=False):
98
- path: str
99
- """Path to the authentication function in a Python file."""
100
- disable_studio_auth: bool
101
- """Whether to disable auth when connecting from the LangSmith Studio."""
102
- openapi: SecurityConfig
103
- """The schema to use for updating the openapi spec.
104
-
105
- Example:
106
- {
107
- "securitySchemes": {
108
- "OAuth2": {
109
- "type": "oauth2",
110
- "flows": {
111
- "password": {
112
- "tokenUrl": "/token",
113
- "scopes": {
114
- "me": "Read information about the current user",
115
- "items": "Access to create and manage items"
116
- }
117
- }
118
- }
119
- }
120
- },
121
- "security": [
122
- {"OAuth2": ["me"]} # Default security requirement for all endpoints
123
- ]
124
- }
125
- """
126
- cache: CacheConfig | None
127
-
128
-
129
82
  def run_server(
130
83
  host: str = "127.0.0.1",
131
84
  port: int = 2024,
@@ -141,7 +94,7 @@ def run_server(
141
94
  reload_includes: Sequence[str] | None = None,
142
95
  reload_excludes: Sequence[str] | None = None,
143
96
  store: typing.Optional["StoreConfig"] = None,
144
- auth: AuthConfig | None = None,
97
+ auth: typing.Optional["AuthConfig"] = None,
145
98
  http: typing.Optional["HttpConfig"] = None,
146
99
  ui: dict | None = None,
147
100
  ui_config: dict | None = None,
langgraph_api/config.py CHANGED
@@ -1,8 +1,10 @@
1
1
  import os
2
+ from collections.abc import Callable
2
3
  from os import environ, getenv
3
- from typing import Literal
4
+ from typing import Literal, TypeVar, cast
4
5
 
5
6
  import orjson
7
+ from pydantic import TypeAdapter
6
8
  from starlette.config import Config, undefined
7
9
  from starlette.datastructures import CommaSeparatedStrings
8
10
  from typing_extensions import TypedDict
@@ -22,11 +24,14 @@ class CorsConfig(TypedDict, total=False):
22
24
  max_age: int
23
25
 
24
26
 
25
- class ConfigurableHeaders(TypedDict):
27
+ class ConfigurableHeaders(TypedDict, total=False):
26
28
  includes: list[str] | None
27
29
  excludes: list[str] | None
28
30
 
29
31
 
32
+ MiddlewareOrders = Literal["auth_first", "middleware_first"]
33
+
34
+
30
35
  class HttpConfig(TypedDict, total=False):
31
36
  app: str
32
37
  """Import path for a custom Starlette/FastAPI app to mount"""
@@ -52,6 +57,8 @@ class HttpConfig(TypedDict, total=False):
52
57
  """Prefix for mounted routes. E.g., "/my-deployment/api"."""
53
58
  configurable_headers: ConfigurableHeaders | None
54
59
  logging_headers: ConfigurableHeaders | None
60
+ enable_custom_route_auth: bool
61
+ middleware_order: MiddlewareOrders | None
55
62
 
56
63
 
57
64
  class ThreadTTLConfig(TypedDict, total=False):
@@ -135,18 +142,72 @@ class CheckpointerConfig(TypedDict, total=False):
135
142
  """
136
143
 
137
144
 
145
+ class SecurityConfig(TypedDict, total=False):
146
+ securitySchemes: dict
147
+ security: list
148
+ # path => {method => security}
149
+ paths: dict[str, dict[str, list]]
150
+
151
+
152
+ class CacheConfig(TypedDict, total=False):
153
+ cache_keys: list[str]
154
+ ttl_seconds: int
155
+ max_size: int
156
+
157
+
158
+ class AuthConfig(TypedDict, total=False):
159
+ path: str
160
+ """Path to the authentication function in a Python file."""
161
+ disable_studio_auth: bool
162
+ """Whether to disable auth when connecting from the LangSmith Studio."""
163
+ openapi: SecurityConfig
164
+ """The schema to use for updating the openapi spec.
165
+
166
+ Example:
167
+ {
168
+ "securitySchemes": {
169
+ "OAuth2": {
170
+ "type": "oauth2",
171
+ "flows": {
172
+ "password": {
173
+ "tokenUrl": "/token",
174
+ "scopes": {
175
+ "me": "Read information about the current user",
176
+ "items": "Access to create and manage items"
177
+ }
178
+ }
179
+ }
180
+ }
181
+ },
182
+ "security": [
183
+ {"OAuth2": ["me"]} # Default security requirement for all endpoints
184
+ ]
185
+ }
186
+ """
187
+ cache: CacheConfig | None
188
+
189
+
138
190
  # env
139
191
 
140
192
  env = Config()
141
193
 
142
194
 
143
- def _parse_json(json: str | None) -> dict | None:
195
+ TD = TypeVar("TD")
196
+
197
+
198
+ def _parse_json(json: str | None, schema: TypeAdapter | None = None) -> dict | None:
144
199
  if not json:
145
200
  return None
146
- parsed = orjson.loads(json)
147
- if not parsed:
148
- return None
149
- return parsed
201
+ parsed = schema.validate_json(json) if schema else orjson.loads(json)
202
+ return parsed or None
203
+
204
+
205
+ def _parse_schema(schema: type[TD]) -> Callable[[str | None], TD | None]:
206
+ def composed(json: str | None) -> TD | None:
207
+ return cast(TD | None, _parse_json(json, schema=TypeAdapter(schema)))
208
+
209
+ composed.__name__ = schema.__name__ # This just gives a nicer error message if the user provides an incompatible value
210
+ return composed
150
211
 
151
212
 
152
213
  STATS_INTERVAL_SECS = env("STATS_INTERVAL_SECS", cast=int, default=60)
@@ -189,17 +250,15 @@ ALLOW_PRIVATE_NETWORK = env("ALLOW_PRIVATE_NETWORK", cast=bool, default=False)
189
250
  See https://developer.chrome.com/blog/private-network-access-update-2024-03
190
251
  """
191
252
 
192
- HTTP_CONFIG: HttpConfig | None = env("LANGGRAPH_HTTP", cast=_parse_json, default=None)
193
- STORE_CONFIG: StoreConfig | None = env(
194
- "LANGGRAPH_STORE", cast=_parse_json, default=None
195
- )
253
+ HTTP_CONFIG = env("LANGGRAPH_HTTP", cast=_parse_schema(HttpConfig), default=None)
254
+ STORE_CONFIG = env("LANGGRAPH_STORE", cast=_parse_schema(StoreConfig), default=None)
196
255
 
197
256
  MOUNT_PREFIX: str | None = env("MOUNT_PREFIX", cast=str, default=None) or (
198
257
  HTTP_CONFIG.get("mount_prefix") if HTTP_CONFIG else None
199
258
  )
200
259
 
201
260
  CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
202
- CORS_CONFIG: CorsConfig | None = env("CORS_CONFIG", cast=_parse_json, default=None) or (
261
+ CORS_CONFIG = env("CORS_CONFIG", cast=_parse_schema(CorsConfig), default=None) or (
203
262
  HTTP_CONFIG.get("cors") if HTTP_CONFIG else None
204
263
  )
205
264
  """
@@ -277,8 +336,8 @@ def _parse_thread_ttl(value: str | None) -> ThreadTTLConfig | None:
277
336
  }
278
337
 
279
338
 
280
- CHECKPOINTER_CONFIG: CheckpointerConfig | None = env(
281
- "LANGGRAPH_CHECKPOINTER", cast=_parse_json, default=None
339
+ CHECKPOINTER_CONFIG = env(
340
+ "LANGGRAPH_CHECKPOINTER", cast=_parse_schema(CheckpointerConfig), default=None
282
341
  )
283
342
  THREAD_TTL: ThreadTTLConfig | None = env(
284
343
  "LANGGRAPH_THREAD_TTL", cast=_parse_thread_ttl, default=None
@@ -292,6 +351,7 @@ BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=3600)
292
351
  FF_CRONS_ENABLED = env("FF_CRONS_ENABLED", cast=bool, default=True)
293
352
  FF_RICH_THREADS = env("FF_RICH_THREADS", cast=bool, default=True)
294
353
  FF_LOG_DROPPED_EVENTS = env("FF_LOG_DROPPED_EVENTS", cast=bool, default=False)
354
+ FF_LOG_QUERY_AND_PARAMS = env("FF_LOG_QUERY_AND_PARAMS", cast=bool, default=False)
295
355
 
296
356
  # auth
297
357
 
@@ -303,7 +363,7 @@ if LANGGRAPH_POSTGRES_EXTENSIONS not in ("standard", "lite"):
303
363
  raise ValueError(
304
364
  f"Unknown LANGGRAPH_POSTGRES_EXTENSIONS value: {LANGGRAPH_POSTGRES_EXTENSIONS}"
305
365
  )
306
- LANGGRAPH_AUTH = env("LANGGRAPH_AUTH", cast=_parse_json, default=None)
366
+ LANGGRAPH_AUTH = env("LANGGRAPH_AUTH", cast=_parse_schema(AuthConfig), default=None)
307
367
  LANGSMITH_TENANT_ID = env("LANGSMITH_TENANT_ID", cast=str, default=None)
308
368
  LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
309
369
  "LANGSMITH_AUTH_VERIFY_TENANT_ID",
@@ -5,7 +5,7 @@ import os
5
5
  import structlog
6
6
  from grpc import aio # type: ignore[import]
7
7
 
8
- from .generated.core_api_pb2_grpc import AdminStub, AssistantsStub
8
+ from .generated.core_api_pb2_grpc import AdminStub, AssistantsStub, ThreadsStub
9
9
 
10
10
  logger = structlog.stdlib.get_logger(__name__)
11
11
 
@@ -27,6 +27,7 @@ class GrpcClient:
27
27
  )
28
28
  self._channel: aio.Channel | None = None
29
29
  self._assistants_stub: AssistantsStub | None = None
30
+ self._threads_stub: ThreadsStub | None = None
30
31
  self._admin_stub: AdminStub | None = None
31
32
 
32
33
  async def __aenter__(self):
@@ -46,6 +47,7 @@ class GrpcClient:
46
47
  self._channel = aio.insecure_channel(self.server_address)
47
48
 
48
49
  self._assistants_stub = AssistantsStub(self._channel)
50
+ self._threads_stub = ThreadsStub(self._channel)
49
51
  self._admin_stub = AdminStub(self._channel)
50
52
 
51
53
  await logger.adebug(
@@ -58,6 +60,7 @@ class GrpcClient:
58
60
  await self._channel.close()
59
61
  self._channel = None
60
62
  self._assistants_stub = None
63
+ self._threads_stub = None
61
64
  self._admin_stub = None
62
65
  await logger.adebug("Closed gRPC connection")
63
66
 
@@ -70,6 +73,15 @@ class GrpcClient:
70
73
  )
71
74
  return self._assistants_stub
72
75
 
76
+ @property
77
+ def threads(self) -> ThreadsStub:
78
+ """Get the threads service stub."""
79
+ if self._threads_stub is None:
80
+ raise RuntimeError(
81
+ "Client not connected. Use async context manager or call connect() first."
82
+ )
83
+ return self._threads_stub
84
+
73
85
  @property
74
86
  def admin(self) -> AdminStub:
75
87
  """Get the admin service stub."""