langgraph-api 0.4.1__py3-none-any.whl → 0.7.3__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.
Files changed (135) hide show
  1. langgraph_api/__init__.py +1 -1
  2. langgraph_api/api/__init__.py +111 -51
  3. langgraph_api/api/a2a.py +1610 -0
  4. langgraph_api/api/assistants.py +212 -89
  5. langgraph_api/api/mcp.py +3 -3
  6. langgraph_api/api/meta.py +52 -28
  7. langgraph_api/api/openapi.py +27 -17
  8. langgraph_api/api/profile.py +108 -0
  9. langgraph_api/api/runs.py +342 -195
  10. langgraph_api/api/store.py +19 -2
  11. langgraph_api/api/threads.py +209 -27
  12. langgraph_api/asgi_transport.py +14 -9
  13. langgraph_api/asyncio.py +14 -4
  14. langgraph_api/auth/custom.py +52 -37
  15. langgraph_api/auth/langsmith/backend.py +4 -3
  16. langgraph_api/auth/langsmith/client.py +13 -8
  17. langgraph_api/cli.py +230 -133
  18. langgraph_api/command.py +5 -3
  19. langgraph_api/config/__init__.py +532 -0
  20. langgraph_api/config/_parse.py +58 -0
  21. langgraph_api/config/schemas.py +431 -0
  22. langgraph_api/cron_scheduler.py +17 -1
  23. langgraph_api/encryption/__init__.py +15 -0
  24. langgraph_api/encryption/aes_json.py +158 -0
  25. langgraph_api/encryption/context.py +35 -0
  26. langgraph_api/encryption/custom.py +280 -0
  27. langgraph_api/encryption/middleware.py +632 -0
  28. langgraph_api/encryption/shared.py +63 -0
  29. langgraph_api/errors.py +12 -1
  30. langgraph_api/executor_entrypoint.py +11 -6
  31. langgraph_api/feature_flags.py +29 -0
  32. langgraph_api/graph.py +176 -76
  33. langgraph_api/grpc/client.py +313 -0
  34. langgraph_api/grpc/config_conversion.py +231 -0
  35. langgraph_api/grpc/generated/__init__.py +29 -0
  36. langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
  37. langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
  38. langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
  39. langgraph_api/grpc/generated/core_api_pb2.py +216 -0
  40. langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
  41. langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
  42. langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
  43. langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
  44. langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
  45. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
  46. langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
  47. langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
  48. langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
  49. langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
  50. langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
  51. langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
  52. langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
  53. langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
  54. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
  55. langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
  56. langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
  57. langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
  58. langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
  59. langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
  60. langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
  61. langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
  62. langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
  63. langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
  64. langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
  65. langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
  66. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
  67. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
  68. langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
  69. langgraph_api/grpc/generated/errors_pb2.py +39 -0
  70. langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
  71. langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
  72. langgraph_api/grpc/ops/__init__.py +370 -0
  73. langgraph_api/grpc/ops/assistants.py +424 -0
  74. langgraph_api/grpc/ops/runs.py +792 -0
  75. langgraph_api/grpc/ops/threads.py +1013 -0
  76. langgraph_api/http.py +16 -5
  77. langgraph_api/http_metrics.py +15 -35
  78. langgraph_api/http_metrics_utils.py +38 -0
  79. langgraph_api/js/build.mts +1 -1
  80. langgraph_api/js/client.http.mts +13 -7
  81. langgraph_api/js/client.mts +2 -5
  82. langgraph_api/js/package.json +29 -28
  83. langgraph_api/js/remote.py +56 -30
  84. langgraph_api/js/src/graph.mts +20 -0
  85. langgraph_api/js/sse.py +2 -2
  86. langgraph_api/js/ui.py +1 -1
  87. langgraph_api/js/yarn.lock +1204 -1006
  88. langgraph_api/logging.py +29 -2
  89. langgraph_api/metadata.py +99 -28
  90. langgraph_api/middleware/http_logger.py +7 -2
  91. langgraph_api/middleware/private_network.py +7 -7
  92. langgraph_api/models/run.py +54 -93
  93. langgraph_api/otel_context.py +205 -0
  94. langgraph_api/patch.py +5 -3
  95. langgraph_api/queue_entrypoint.py +154 -65
  96. langgraph_api/route.py +47 -5
  97. langgraph_api/schema.py +88 -10
  98. langgraph_api/self_hosted_logs.py +124 -0
  99. langgraph_api/self_hosted_metrics.py +450 -0
  100. langgraph_api/serde.py +79 -37
  101. langgraph_api/server.py +138 -60
  102. langgraph_api/state.py +4 -3
  103. langgraph_api/store.py +25 -16
  104. langgraph_api/stream.py +80 -29
  105. langgraph_api/thread_ttl.py +31 -13
  106. langgraph_api/timing/__init__.py +25 -0
  107. langgraph_api/timing/profiler.py +200 -0
  108. langgraph_api/timing/timer.py +318 -0
  109. langgraph_api/utils/__init__.py +53 -8
  110. langgraph_api/utils/cache.py +47 -10
  111. langgraph_api/utils/config.py +2 -1
  112. langgraph_api/utils/errors.py +77 -0
  113. langgraph_api/utils/future.py +10 -6
  114. langgraph_api/utils/headers.py +76 -2
  115. langgraph_api/utils/retriable_client.py +74 -0
  116. langgraph_api/utils/stream_codec.py +315 -0
  117. langgraph_api/utils/uuids.py +29 -62
  118. langgraph_api/validation.py +9 -0
  119. langgraph_api/webhook.py +120 -6
  120. langgraph_api/worker.py +55 -24
  121. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
  122. langgraph_api-0.7.3.dist-info/RECORD +168 -0
  123. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
  124. langgraph_runtime/__init__.py +1 -0
  125. langgraph_runtime/routes.py +11 -0
  126. logging.json +1 -3
  127. openapi.json +839 -478
  128. langgraph_api/config.py +0 -387
  129. langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
  130. langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
  131. langgraph_api/js/package-lock.json +0 -3308
  132. langgraph_api-0.4.1.dist-info/RECORD +0 -107
  133. /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
  134. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
  135. {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,532 @@
1
+ import os
2
+ from os import environ, getenv
3
+ from typing import TYPE_CHECKING, Annotated, Literal, TypeVar, cast
4
+
5
+ import structlog
6
+ from pydantic.functional_validators import AfterValidator
7
+ from starlette.config import Config, undefined
8
+ from starlette.datastructures import CommaSeparatedStrings
9
+
10
+ from langgraph_api import traceblock
11
+ from langgraph_api.config import _parse
12
+ from langgraph_api.config.schemas import (
13
+ AuthConfig,
14
+ CheckpointerConfig,
15
+ CorsConfig,
16
+ EncryptionConfig,
17
+ HttpConfig,
18
+ SerdeConfig,
19
+ StoreConfig,
20
+ ThreadTTLConfig,
21
+ TTLConfig,
22
+ WebhooksConfig,
23
+ webhooks_validator,
24
+ )
25
+
26
+ if TYPE_CHECKING:
27
+ from collections.abc import Callable
28
+
29
+ # env
30
+
31
+ env = Config()
32
+
33
+ logger = structlog.stdlib.get_logger(__name__)
34
+
35
+
36
+ TD = TypeVar("TD")
37
+
38
+
39
+ STATS_INTERVAL_SECS = env("STATS_INTERVAL_SECS", cast=int, default=60)
40
+
41
+ # storage
42
+
43
+ DATABASE_URI = env("DATABASE_URI", cast=str, default=getenv("POSTGRES_URI", undefined))
44
+ MIGRATIONS_PATH = env("MIGRATIONS_PATH", cast=str, default="/storage/migrations")
45
+ POSTGRES_POOL_MAX_SIZE = env("LANGGRAPH_POSTGRES_POOL_MAX_SIZE", cast=int, default=150)
46
+ RESUMABLE_STREAM_TTL_SECONDS = env(
47
+ "RESUMABLE_STREAM_TTL_SECONDS",
48
+ cast=int,
49
+ default=120, # 2 minutes
50
+ )
51
+
52
+
53
+ def _parse_aes_key(key_str: str | None) -> bytes | None:
54
+ """Parse and validate the AES encryption key from string.
55
+
56
+ Args:
57
+ key_str: The key string from LANGGRAPH_AES_KEY env var
58
+
59
+ Returns:
60
+ The key as bytes, or None if not set
61
+
62
+ Raises:
63
+ ValueError: If key is not 16, 24, or 32 bytes (AES-128/192/256)
64
+ """
65
+ if not key_str:
66
+ return None
67
+ key = key_str.encode(encoding="utf-8")
68
+ if len(key) not in (16, 24, 32):
69
+ raise ValueError("LANGGRAPH_AES_KEY must be 16, 24, or 32 bytes long.")
70
+ return key
71
+
72
+
73
+ LANGGRAPH_AES_KEY = env("LANGGRAPH_AES_KEY", default=None, cast=_parse_aes_key)
74
+
75
+ # System-populated fields that cannot be encrypted (would break functionality)
76
+ AES_JSON_DISALLOWED_KEYS = frozenset(
77
+ {
78
+ "langgraph_version",
79
+ "langgraph_api_version",
80
+ "langgraph_plan",
81
+ "langgraph_host",
82
+ "langgraph_api_url",
83
+ "langgraph_request_id",
84
+ "langgraph_auth_user_id",
85
+ "langgraph_auth_permissions",
86
+ }
87
+ )
88
+
89
+
90
+ def _get_aes_json_keys(keys_str: str | None) -> frozenset[str] | None:
91
+ """Parse LANGGRAPH_AES_JSON_KEYS comma-separated list.
92
+
93
+ Validates:
94
+ - No disallowed system keys
95
+ - LANGGRAPH_AES_KEY must be set
96
+ """
97
+ if not keys_str:
98
+ return None
99
+ keys = frozenset(k.strip() for k in keys_str.split(",") if k.strip())
100
+ if not keys:
101
+ return None
102
+
103
+ # Check for disallowed keys
104
+ disallowed = keys & AES_JSON_DISALLOWED_KEYS
105
+ if disallowed:
106
+ raise ValueError(
107
+ f"LANGGRAPH_AES_JSON_KEYS contains disallowed system keys: {sorted(disallowed)}. "
108
+ f"These keys are used internally and cannot be encrypted. Remove them from LANGGRAPH_AES_JSON_KEYS"
109
+ )
110
+
111
+ # Require AES key to be set
112
+ if LANGGRAPH_AES_KEY is None:
113
+ raise ValueError(
114
+ "LANGGRAPH_AES_JSON_KEYS requires LANGGRAPH_AES_KEY to be set."
115
+ )
116
+
117
+ return keys
118
+
119
+
120
+ LANGGRAPH_AES_JSON_KEYS: frozenset[str] | None = env(
121
+ "LANGGRAPH_AES_JSON_KEYS", default=None, cast=_get_aes_json_keys
122
+ )
123
+
124
+ # redis
125
+ REDIS_URI = env("REDIS_URI", cast=str)
126
+ REDIS_CLUSTER = env("REDIS_CLUSTER", cast=bool, default=False)
127
+ REDIS_MAX_CONNECTIONS = env("REDIS_MAX_CONNECTIONS", cast=int, default=2000)
128
+ REDIS_CONNECT_TIMEOUT = env("REDIS_CONNECT_TIMEOUT", cast=float, default=10.0)
129
+ REDIS_HEALTH_CHECK_INTERVAL = env(
130
+ "REDIS_HEALTH_CHECK_INTERVAL", cast=float, default=10.0
131
+ )
132
+ REDIS_KEY_PREFIX = env("REDIS_KEY_PREFIX", cast=str, default="")
133
+ RUN_STATS_CACHE_SECONDS = env("RUN_STATS_CACHE_SECONDS", cast=int, default=60)
134
+
135
+ # server
136
+ ALLOW_PRIVATE_NETWORK = env("ALLOW_PRIVATE_NETWORK", cast=bool, default=False)
137
+ """Only enable for langgraph dev when server is running on loopback address.
138
+
139
+ See https://developer.chrome.com/blog/private-network-access-update-2024-03
140
+ """
141
+
142
+ # gRPC client pool size for persistence server.
143
+ GRPC_CLIENT_POOL_SIZE = env("GRPC_CLIENT_POOL_SIZE", cast=int, default=5)
144
+
145
+ # gRPC message size limits (100MB default)
146
+ GRPC_SERVER_MAX_RECV_MSG_BYTES = env(
147
+ "GRPC_SERVER_MAX_RECV_MSG_BYTES", cast=int, default=300 * 1024 * 1024
148
+ )
149
+ GRPC_SERVER_MAX_SEND_MSG_BYTES = env(
150
+ "GRPC_SERVER_MAX_SEND_MSG_BYTES", cast=int, default=300 * 1024 * 1024
151
+ )
152
+ GRPC_CLIENT_MAX_RECV_MSG_BYTES = env(
153
+ "GRPC_CLIENT_MAX_RECV_MSG_BYTES", cast=int, default=300 * 1024 * 1024
154
+ )
155
+ GRPC_CLIENT_MAX_SEND_MSG_BYTES = env(
156
+ "GRPC_CLIENT_MAX_SEND_MSG_BYTES", cast=int, default=300 * 1024 * 1024
157
+ )
158
+ GRPC_SERVER_ADDRESS = env("GRPC_SERVER_ADDRESS", cast=str, default="localhost:50051")
159
+
160
+ # Minimum payload size to use the dedicated thread pool for JSON parsing.
161
+ # (Otherwise, the payload is parsed directly in the event loop.)
162
+ JSON_THREAD_POOL_MINIMUM_SIZE_BYTES = 100 * 1024 # 100 KB
163
+
164
+ HTTP_CONFIG = env("LANGGRAPH_HTTP", cast=_parse.parse_schema(HttpConfig), default=None)
165
+ MCP_ENABLED = HTTP_CONFIG is None or not HTTP_CONFIG.get("disable_mcp")
166
+ A2A_ENABLED = HTTP_CONFIG is None or not HTTP_CONFIG.get("disable_a2a")
167
+ WEBHOOKS_ENABLED = HTTP_CONFIG and HTTP_CONFIG.get("disable_webhooks")
168
+ STORE_CONFIG = env(
169
+ "LANGGRAPH_STORE", cast=_parse.parse_schema(StoreConfig), default=None
170
+ )
171
+
172
+ MOUNT_PREFIX: str | None = env("MOUNT_PREFIX", cast=str, default=None) or (
173
+ HTTP_CONFIG.get("mount_prefix") if HTTP_CONFIG else None
174
+ )
175
+ if MOUNT_PREFIX:
176
+ if not MOUNT_PREFIX.startswith("/"):
177
+ raise ValueError(
178
+ f"Invalid mount_prefix '{MOUNT_PREFIX}': Must start with '/'. "
179
+ f"Valid examples: '/my-api', '/v1', '/api/v1'.\nInvalid examples: 'api/', '/api/'"
180
+ )
181
+ if MOUNT_PREFIX.endswith("/"):
182
+ MOUNT_PREFIX = MOUNT_PREFIX[:-1]
183
+
184
+ CORS_ALLOW_ORIGINS = env("CORS_ALLOW_ORIGINS", cast=CommaSeparatedStrings, default="*")
185
+ CORS_CONFIG = env(
186
+ "CORS_CONFIG", cast=_parse.parse_schema(CorsConfig), default=None
187
+ ) or (HTTP_CONFIG.get("cors") if HTTP_CONFIG else None)
188
+ """
189
+ {
190
+ "type": "object",
191
+ "properties": {
192
+ "allow_origins": {
193
+ "type": "array",
194
+ "items": {"type": "string"},
195
+ "default": []
196
+ },
197
+ "allow_methods": {
198
+ "type": "array",
199
+ "items": {"type": "string"},
200
+ "default": ["GET"]
201
+ },
202
+ "allow_headers": {
203
+ "type": "array",
204
+ "items": {"type": "string"},
205
+ "default": []
206
+ },
207
+ "allow_credentials": {
208
+ "type": "boolean",
209
+ "default": false
210
+ },
211
+ "allow_origin_regex": {
212
+ "type": ["string", "null"],
213
+ "default": null
214
+ },
215
+ "expose_headers": {
216
+ "type": "array",
217
+ "items": {"type": "string"},
218
+ "default": []
219
+ },
220
+ "max_age": {
221
+ "type": "integer",
222
+ "default": 600
223
+ }
224
+ }
225
+ }
226
+ """
227
+ if (
228
+ CORS_CONFIG is not None
229
+ and CORS_ALLOW_ORIGINS != "*"
230
+ and CORS_CONFIG.get("allow_origins") is None
231
+ ):
232
+ CORS_CONFIG["allow_origins"] = CORS_ALLOW_ORIGINS
233
+
234
+ # queue
235
+
236
+ BG_JOB_HEARTBEAT = 120 # seconds
237
+ BG_JOB_INTERVAL = 30 # seconds
238
+ BG_JOB_MAX_RETRIES = env("BG_JOB_MAX_RETRIES", cast=int, default=3)
239
+ BG_JOB_ISOLATED_LOOPS = env("BG_JOB_ISOLATED_LOOPS", cast=bool, default=False)
240
+ BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS = env(
241
+ "BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS",
242
+ cast=int,
243
+ default=180, # 3 minutes
244
+ )
245
+ # We set the default termination grace period to 60 minutes for hosts so that's the max we could allow here
246
+ if BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS > 3600:
247
+ logger.warning(
248
+ f"BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS was set to greater than the default termination grace period of 3600 seconds. If you are running on cloud, this may cause the pod to be terminated before the workers finish. If you are running on self-hosted, make sure to set the termination grace period to a value greater than {BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS} seconds",
249
+ BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS=BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS,
250
+ )
251
+
252
+ MAX_STREAM_CHUNK_SIZE_BYTES = env(
253
+ "MAX_STREAM_CHUNK_SIZE_BYTES", cast=int, default=1024 * 1024 * 128
254
+ )
255
+
256
+
257
+ CHECKPOINTER_CONFIG = env(
258
+ "LANGGRAPH_CHECKPOINTER", cast=_parse.parse_schema(CheckpointerConfig), default=None
259
+ )
260
+ SERDE: SerdeConfig | None = (
261
+ CHECKPOINTER_CONFIG["serde"]
262
+ if CHECKPOINTER_CONFIG and "serde" in CHECKPOINTER_CONFIG
263
+ else None
264
+ )
265
+ THREAD_TTL: ThreadTTLConfig | None = env(
266
+ "LANGGRAPH_THREAD_TTL", cast=_parse.parse_thread_ttl, default=None
267
+ )
268
+ if THREAD_TTL is None and CHECKPOINTER_CONFIG is not None:
269
+ THREAD_TTL = CHECKPOINTER_CONFIG.get("ttl")
270
+
271
+ N_JOBS_PER_WORKER = env("N_JOBS_PER_WORKER", cast=int, default=10)
272
+ BG_JOB_TIMEOUT_SECS = env("BG_JOB_TIMEOUT_SECS", cast=float, default=3600)
273
+
274
+ FF_CRONS_ENABLED = env("FF_CRONS_ENABLED", cast=bool, default=True)
275
+ FF_LOG_DROPPED_EVENTS = env("FF_LOG_DROPPED_EVENTS", cast=bool, default=False)
276
+ FF_LOG_QUERY_AND_PARAMS = env("FF_LOG_QUERY_AND_PARAMS", cast=bool, default=False)
277
+
278
+ # Internal flag intended for testing only
279
+ CRON_SCHEDULER_SLEEP_TIME = env("CRON_SCHEDULER_SLEEP_TIME", cast=int, default=5)
280
+
281
+
282
+ # auth
283
+
284
+ LANGGRAPH_AUTH_TYPE = env("LANGGRAPH_AUTH_TYPE", cast=str, default="noop")
285
+ LANGGRAPH_POSTGRES_EXTENSIONS: Literal["standard", "lite"] = env(
286
+ "LANGGRAPH_POSTGRES_EXTENSIONS", cast=str, default="standard"
287
+ )
288
+ if LANGGRAPH_POSTGRES_EXTENSIONS not in ("standard", "lite"):
289
+ raise ValueError(
290
+ f"Unknown LANGGRAPH_POSTGRES_EXTENSIONS value: {LANGGRAPH_POSTGRES_EXTENSIONS}"
291
+ )
292
+ LANGGRAPH_AUTH = env(
293
+ "LANGGRAPH_AUTH", cast=_parse.parse_schema(AuthConfig), default=None
294
+ )
295
+ LANGGRAPH_ENCRYPTION = env(
296
+ "LANGGRAPH_ENCRYPTION", cast=_parse.parse_schema(EncryptionConfig), default=None
297
+ )
298
+ LANGSMITH_TENANT_ID = env("LANGSMITH_TENANT_ID", cast=str, default=None)
299
+ LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
300
+ "LANGSMITH_AUTH_VERIFY_TENANT_ID",
301
+ cast=bool,
302
+ default=LANGSMITH_TENANT_ID is not None,
303
+ )
304
+
305
+ if LANGGRAPH_AUTH_TYPE == "langsmith":
306
+ LANGSMITH_AUTH_ENDPOINT = env("LANGSMITH_AUTH_ENDPOINT", cast=str)
307
+ LANGSMITH_TENANT_ID = env("LANGSMITH_TENANT_ID", cast=str)
308
+ LANGSMITH_AUTH_VERIFY_TENANT_ID = env(
309
+ "LANGSMITH_AUTH_VERIFY_TENANT_ID", cast=bool, default=True
310
+ )
311
+
312
+ else:
313
+ LANGSMITH_AUTH_ENDPOINT = env(
314
+ "LANGSMITH_AUTH_ENDPOINT",
315
+ cast=str,
316
+ default=getenv(
317
+ "LANGCHAIN_ENDPOINT",
318
+ getenv("LANGSMITH_ENDPOINT", "https://api.smith.langchain.com"),
319
+ ),
320
+ )
321
+
322
+ # Webhooks
323
+
324
+
325
+ WEBHOOKS_CONFIG = env(
326
+ "LANGGRAPH_WEBHOOKS",
327
+ cast=cast(
328
+ "Callable[[str | None], WebhooksConfig | None]",
329
+ _parse.parse_schema(
330
+ Annotated[WebhooksConfig, AfterValidator(webhooks_validator)]
331
+ ),
332
+ ),
333
+ default=None,
334
+ )
335
+
336
+ # license
337
+
338
+ LANGGRAPH_CLOUD_LICENSE_KEY = env("LANGGRAPH_CLOUD_LICENSE_KEY", cast=str, default="")
339
+
340
+ # Products that are built on top of langgraph-api can be configured
341
+ # to check for additional claims in the LangSmith license key. By default,
342
+ # no additional claims are checked. The `lgp_enabled` claim is always
343
+ # checked.
344
+ LANGSMITH_LICENSE_REQUIRED_CLAIMS = env(
345
+ "LANGSMITH_LICENSE_REQUIRED_CLAIMS", cast=CommaSeparatedStrings, default=[]
346
+ )
347
+
348
+ LANGSMITH_API_KEY = env(
349
+ "LANGSMITH_API_KEY", cast=str, default=getenv("LANGCHAIN_API_KEY", "")
350
+ )
351
+ # LANGSMITH_CONTROL_PLANE_API_KEY is used for license verification and
352
+ # submitting usage metadata to LangSmith SaaS.
353
+ #
354
+ # Use case: A self-hosted deployment can configure LANGSMITH_API_KEY
355
+ # from a self-hosted LangSmith instance (i.e. trace to self-hosted
356
+ # LangSmith) and configure LANGSMITH_CONTROL_PLANE_API_KEY from LangSmith SaaS
357
+ # to facilitate license key verification and metadata submission.
358
+ LANGSMITH_CONTROL_PLANE_API_KEY = env(
359
+ "LANGSMITH_CONTROL_PLANE_API_KEY", cast=str, default=LANGSMITH_API_KEY
360
+ )
361
+
362
+ # if langsmith api key is set, enable tracing unless explicitly disabled
363
+
364
+ if (
365
+ LANGSMITH_CONTROL_PLANE_API_KEY
366
+ and not getenv("LANGCHAIN_TRACING_V2")
367
+ and not getenv("LANGCHAIN_TRACING")
368
+ and not getenv("LANGSMITH_TRACING_V2")
369
+ and not getenv("LANGSMITH_TRACING")
370
+ ):
371
+ environ["LANGCHAIN_TRACING_V2"] = "true"
372
+
373
+ TRACING = (
374
+ env("LANGCHAIN_TRACING_V2", cast=bool, default=None)
375
+ or env("LANGCHAIN_TRACING", cast=bool, default=None)
376
+ or env("LANGSMITH_TRACING_V2", cast=bool, default=None)
377
+ or env("LANGSMITH_TRACING", cast=bool, default=None)
378
+ )
379
+
380
+ # OpenTelemetry
381
+ # Centralized enablement flag so app code does not read raw env vars.
382
+ # If OTEL_ENABLED is unset, auto-enable when a standard OTLP endpoint var is present.
383
+ OTEL_ENABLED = env("OTEL_ENABLED", cast=bool, default=None)
384
+ if OTEL_ENABLED is None:
385
+ OTEL_ENABLED = bool(
386
+ getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT")
387
+ or getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
388
+ )
389
+
390
+ # if variant is "licensed", update to "local" if using LANGSMITH_CONTROL_PLANE_API_KEY instead
391
+
392
+ if (
393
+ getenv("LANGSMITH_LANGGRAPH_API_VARIANT") == "licensed"
394
+ and LANGSMITH_CONTROL_PLANE_API_KEY
395
+ ):
396
+ environ["LANGSMITH_LANGGRAPH_API_VARIANT"] = "local"
397
+
398
+
399
+ # Metrics.
400
+ USES_INDEXING = (
401
+ STORE_CONFIG
402
+ and STORE_CONFIG.get("index")
403
+ and STORE_CONFIG.get("index").get("embed")
404
+ )
405
+ USES_CUSTOM_APP = HTTP_CONFIG and HTTP_CONFIG.get("app")
406
+ USES_CUSTOM_AUTH = bool(LANGGRAPH_AUTH)
407
+ USES_THREAD_TTL = bool(THREAD_TTL)
408
+ USES_STORE_TTL = bool(STORE_CONFIG and STORE_CONFIG.get("ttl"))
409
+
410
+ API_VARIANT = env("LANGSMITH_LANGGRAPH_API_VARIANT", cast=str, default="")
411
+
412
+ # UI
413
+ UI_USE_BUNDLER = env("LANGGRAPH_UI_BUNDLER", cast=bool, default=False)
414
+
415
+ LANGGRAPH_METRICS_ENABLED = env("LANGGRAPH_METRICS_ENABLED", cast=bool, default=False)
416
+ LANGGRAPH_METRICS_ENDPOINT = env("LANGGRAPH_METRICS_ENDPOINT", cast=str, default=None)
417
+ LANGGRAPH_METRICS_EXPORT_INTERVAL_MS = env(
418
+ "LANGGRAPH_METRICS_EXPORT_INTERVAL_MS", cast=int, default=60000
419
+ )
420
+ LANGGRAPH_LOGS_ENDPOINT = env("LANGGRAPH_LOGS_ENDPOINT", cast=str, default=None)
421
+ LANGGRAPH_LOGS_ENABLED = env("LANGGRAPH_LOGS_ENABLED", cast=bool, default=False)
422
+
423
+ FF_PYSPY_PROFILING_ENABLED = env("FF_PYSPY_PROFILING_ENABLED", cast=bool, default=False)
424
+ if FF_PYSPY_PROFILING_ENABLED:
425
+ import shutil
426
+
427
+ pyspy = shutil.which("py-spy")
428
+ if not pyspy:
429
+ raise ValueError(
430
+ "py-spy not found on PATH. Please re-deploy with py-spy installed."
431
+ )
432
+ FF_PYSPY_PROFILING_MAX_DURATION_SECS = env(
433
+ "FF_PYSPY_PROFILING_MAX_DURATION_SECS", cast=int, default=240
434
+ )
435
+ FF_PROFILE_IMPORTS = env("FF_PROFILE_IMPORTS", cast=bool, default=False)
436
+
437
+ SELF_HOSTED_OBSERVABILITY_SERVICE_NAME = "LGP_Self_Hosted"
438
+
439
+ IS_QUEUE_ENTRYPOINT = False
440
+ IS_EXECUTOR_ENTRYPOINT = False
441
+ ref_sha = None
442
+ if not os.getenv("LANGCHAIN_REVISION_ID") and (
443
+ ref_sha := os.getenv("LANGSMITH_LANGGRAPH_GIT_REF_SHA")
444
+ ):
445
+ # This is respected by the langsmith SDK env inference
446
+ # https://github.com/langchain-ai/langsmith-sdk/blob/1b93e4c13b8369d92db891ae3babc3e2254f0e56/python/langsmith/env/_runtime_env.py#L190
447
+ os.environ["LANGCHAIN_REVISION_ID"] = ref_sha
448
+
449
+ traceblock.patch_requests()
450
+
451
+ __all__ = [
452
+ "AES_JSON_DISALLOWED_KEYS",
453
+ "ALLOW_PRIVATE_NETWORK",
454
+ "API_VARIANT",
455
+ "BG_JOB_HEARTBEAT",
456
+ "BG_JOB_INTERVAL",
457
+ "BG_JOB_ISOLATED_LOOPS",
458
+ "BG_JOB_MAX_RETRIES",
459
+ "BG_JOB_SHUTDOWN_GRACE_PERIOD_SECS",
460
+ "BG_JOB_TIMEOUT_SECS",
461
+ "CHECKPOINTER_CONFIG",
462
+ "CORS_ALLOW_ORIGINS",
463
+ "CORS_CONFIG",
464
+ "CRON_SCHEDULER_SLEEP_TIME",
465
+ "DATABASE_URI",
466
+ "FF_CRONS_ENABLED",
467
+ "FF_LOG_DROPPED_EVENTS",
468
+ "FF_LOG_QUERY_AND_PARAMS",
469
+ "FF_PYSPY_PROFILING_ENABLED",
470
+ "FF_PYSPY_PROFILING_MAX_DURATION_SECS",
471
+ "GRPC_CLIENT_MAX_RECV_MSG_BYTES",
472
+ "GRPC_CLIENT_MAX_SEND_MSG_BYTES",
473
+ "GRPC_CLIENT_POOL_SIZE",
474
+ "GRPC_SERVER_ADDRESS",
475
+ "GRPC_SERVER_MAX_RECV_MSG_BYTES",
476
+ "GRPC_SERVER_MAX_SEND_MSG_BYTES",
477
+ "HTTP_CONFIG",
478
+ "IS_EXECUTOR_ENTRYPOINT",
479
+ "IS_QUEUE_ENTRYPOINT",
480
+ "JSON_THREAD_POOL_MINIMUM_SIZE_BYTES",
481
+ "LANGGRAPH_AES_JSON_KEYS",
482
+ "LANGGRAPH_AES_KEY",
483
+ "LANGGRAPH_AUTH",
484
+ "LANGGRAPH_AUTH_TYPE",
485
+ "LANGGRAPH_CLOUD_LICENSE_KEY",
486
+ "LANGGRAPH_LOGS_ENABLED",
487
+ "LANGGRAPH_LOGS_ENDPOINT",
488
+ "LANGGRAPH_METRICS_ENABLED",
489
+ "LANGGRAPH_METRICS_ENDPOINT",
490
+ "LANGGRAPH_METRICS_EXPORT_INTERVAL_MS",
491
+ "LANGGRAPH_POSTGRES_EXTENSIONS",
492
+ "LANGSMITH_API_KEY",
493
+ "LANGSMITH_AUTH_ENDPOINT",
494
+ "LANGSMITH_AUTH_VERIFY_TENANT_ID",
495
+ "LANGSMITH_CONTROL_PLANE_API_KEY",
496
+ "LANGSMITH_TENANT_ID",
497
+ "MAX_STREAM_CHUNK_SIZE_BYTES",
498
+ "MIGRATIONS_PATH",
499
+ "MOUNT_PREFIX",
500
+ "N_JOBS_PER_WORKER",
501
+ "OTEL_ENABLED",
502
+ "POSTGRES_POOL_MAX_SIZE",
503
+ "REDIS_CLUSTER",
504
+ "REDIS_CONNECT_TIMEOUT",
505
+ "REDIS_HEALTH_CHECK_INTERVAL",
506
+ "REDIS_KEY_PREFIX",
507
+ "REDIS_MAX_CONNECTIONS",
508
+ "REDIS_URI",
509
+ "RESUMABLE_STREAM_TTL_SECONDS",
510
+ "RUN_STATS_CACHE_SECONDS",
511
+ "SELF_HOSTED_OBSERVABILITY_SERVICE_NAME",
512
+ "SERDE",
513
+ "STATS_INTERVAL_SECS",
514
+ "STORE_CONFIG",
515
+ "THREAD_TTL",
516
+ "TRACING",
517
+ "UI_USE_BUNDLER",
518
+ "USES_CUSTOM_APP",
519
+ "USES_CUSTOM_AUTH",
520
+ "USES_INDEXING",
521
+ "USES_STORE_TTL",
522
+ "USES_THREAD_TTL",
523
+ "AuthConfig",
524
+ "CheckpointerConfig",
525
+ "CorsConfig",
526
+ "HttpConfig",
527
+ "SerdeConfig",
528
+ "StoreConfig",
529
+ "TTLConfig",
530
+ "ThreadTTLConfig",
531
+ "WebhooksConfig",
532
+ ]
@@ -0,0 +1,58 @@
1
+ from collections.abc import Callable
2
+ from typing import Annotated, TypeVar, cast, get_args, get_origin
3
+
4
+ import orjson
5
+ from pydantic import TypeAdapter
6
+ from typing_extensions import TypeForm
7
+
8
+ from langgraph_api.config.schemas import (
9
+ ThreadTTLConfig,
10
+ )
11
+
12
+ TD = TypeVar("TD")
13
+
14
+
15
+ def parse_json(json: str | None, schema: TypeAdapter | None = None) -> dict | None:
16
+ if not json:
17
+ return None
18
+ parsed = schema.validate_json(json) if schema else orjson.loads(json)
19
+ return parsed or None
20
+
21
+
22
+ def parse_schema(
23
+ schema: TypeForm[TD],
24
+ ) -> Callable[[str | None], TD | None]:
25
+ def composed(json: str | None) -> TD | None:
26
+ return cast("TD | None", parse_json(json, schema=TypeAdapter(schema)))
27
+
28
+ # This just gives a nicer error message if the user provides an incompatible value
29
+ if get_origin(schema) is Annotated:
30
+ schema_type = get_args(schema)[0]
31
+ composed.__name__ = schema_type.__name__
32
+ else:
33
+ composed.__name__ = schema.__name__ # type: ignore
34
+ return composed
35
+
36
+
37
+ def parse_thread_ttl(value: str | None) -> ThreadTTLConfig | None:
38
+ """Parse LANGGRAPH_THREAD_TTL environment variable.
39
+
40
+ Accepts either:
41
+ - A simple number (TTL in minutes): "60"
42
+ - A JSON object: '{"strategy": "keep_latest", "default_ttl": 60, "sweep_limit": 500}'
43
+
44
+ Supported strategies:
45
+ - "delete": Remove the thread and all its data entirely
46
+ - "keep_latest": Prune old checkpoints but keep the thread and latest state
47
+ """
48
+ if not value:
49
+ return None
50
+ if str(value).strip().startswith("{"):
51
+ return parse_json(value.strip())
52
+ return {
53
+ "strategy": "delete",
54
+ # We permit float values mainly for testing purposes
55
+ "default_ttl": float(value),
56
+ "sweep_interval_minutes": 5.1,
57
+ "sweep_limit": 1000, # Default max threads per sweep iteration
58
+ }