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.
- langgraph_api/__init__.py +1 -1
- langgraph_api/api/__init__.py +111 -51
- langgraph_api/api/a2a.py +1610 -0
- langgraph_api/api/assistants.py +212 -89
- langgraph_api/api/mcp.py +3 -3
- langgraph_api/api/meta.py +52 -28
- langgraph_api/api/openapi.py +27 -17
- langgraph_api/api/profile.py +108 -0
- langgraph_api/api/runs.py +342 -195
- langgraph_api/api/store.py +19 -2
- langgraph_api/api/threads.py +209 -27
- langgraph_api/asgi_transport.py +14 -9
- langgraph_api/asyncio.py +14 -4
- langgraph_api/auth/custom.py +52 -37
- langgraph_api/auth/langsmith/backend.py +4 -3
- langgraph_api/auth/langsmith/client.py +13 -8
- langgraph_api/cli.py +230 -133
- langgraph_api/command.py +5 -3
- langgraph_api/config/__init__.py +532 -0
- langgraph_api/config/_parse.py +58 -0
- langgraph_api/config/schemas.py +431 -0
- langgraph_api/cron_scheduler.py +17 -1
- langgraph_api/encryption/__init__.py +15 -0
- langgraph_api/encryption/aes_json.py +158 -0
- langgraph_api/encryption/context.py +35 -0
- langgraph_api/encryption/custom.py +280 -0
- langgraph_api/encryption/middleware.py +632 -0
- langgraph_api/encryption/shared.py +63 -0
- langgraph_api/errors.py +12 -1
- langgraph_api/executor_entrypoint.py +11 -6
- langgraph_api/feature_flags.py +29 -0
- langgraph_api/graph.py +176 -76
- langgraph_api/grpc/client.py +313 -0
- langgraph_api/grpc/config_conversion.py +231 -0
- langgraph_api/grpc/generated/__init__.py +29 -0
- langgraph_api/grpc/generated/checkpointer_pb2.py +63 -0
- langgraph_api/grpc/generated/checkpointer_pb2.pyi +99 -0
- langgraph_api/grpc/generated/checkpointer_pb2_grpc.py +329 -0
- langgraph_api/grpc/generated/core_api_pb2.py +216 -0
- langgraph_api/grpc/generated/core_api_pb2.pyi +905 -0
- langgraph_api/grpc/generated/core_api_pb2_grpc.py +1621 -0
- langgraph_api/grpc/generated/engine_common_pb2.py +219 -0
- langgraph_api/grpc/generated/engine_common_pb2.pyi +722 -0
- langgraph_api/grpc/generated/engine_common_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2.pyi +12 -0
- langgraph_api/grpc/generated/enum_cancel_run_action_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_control_signal_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_durability_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_durability_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_durability_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_multitask_strategy_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_run_status_pb2.pyi +22 -0
- langgraph_api/grpc/generated/enum_run_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2.pyi +28 -0
- langgraph_api/grpc/generated/enum_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_status_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.py +37 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2.pyi +16 -0
- langgraph_api/grpc/generated/enum_thread_stream_mode_pb2_grpc.py +24 -0
- langgraph_api/grpc/generated/errors_pb2.py +39 -0
- langgraph_api/grpc/generated/errors_pb2.pyi +21 -0
- langgraph_api/grpc/generated/errors_pb2_grpc.py +24 -0
- langgraph_api/grpc/ops/__init__.py +370 -0
- langgraph_api/grpc/ops/assistants.py +424 -0
- langgraph_api/grpc/ops/runs.py +792 -0
- langgraph_api/grpc/ops/threads.py +1013 -0
- langgraph_api/http.py +16 -5
- langgraph_api/http_metrics.py +15 -35
- langgraph_api/http_metrics_utils.py +38 -0
- langgraph_api/js/build.mts +1 -1
- langgraph_api/js/client.http.mts +13 -7
- langgraph_api/js/client.mts +2 -5
- langgraph_api/js/package.json +29 -28
- langgraph_api/js/remote.py +56 -30
- langgraph_api/js/src/graph.mts +20 -0
- langgraph_api/js/sse.py +2 -2
- langgraph_api/js/ui.py +1 -1
- langgraph_api/js/yarn.lock +1204 -1006
- langgraph_api/logging.py +29 -2
- langgraph_api/metadata.py +99 -28
- langgraph_api/middleware/http_logger.py +7 -2
- langgraph_api/middleware/private_network.py +7 -7
- langgraph_api/models/run.py +54 -93
- langgraph_api/otel_context.py +205 -0
- langgraph_api/patch.py +5 -3
- langgraph_api/queue_entrypoint.py +154 -65
- langgraph_api/route.py +47 -5
- langgraph_api/schema.py +88 -10
- langgraph_api/self_hosted_logs.py +124 -0
- langgraph_api/self_hosted_metrics.py +450 -0
- langgraph_api/serde.py +79 -37
- langgraph_api/server.py +138 -60
- langgraph_api/state.py +4 -3
- langgraph_api/store.py +25 -16
- langgraph_api/stream.py +80 -29
- langgraph_api/thread_ttl.py +31 -13
- langgraph_api/timing/__init__.py +25 -0
- langgraph_api/timing/profiler.py +200 -0
- langgraph_api/timing/timer.py +318 -0
- langgraph_api/utils/__init__.py +53 -8
- langgraph_api/utils/cache.py +47 -10
- langgraph_api/utils/config.py +2 -1
- langgraph_api/utils/errors.py +77 -0
- langgraph_api/utils/future.py +10 -6
- langgraph_api/utils/headers.py +76 -2
- langgraph_api/utils/retriable_client.py +74 -0
- langgraph_api/utils/stream_codec.py +315 -0
- langgraph_api/utils/uuids.py +29 -62
- langgraph_api/validation.py +9 -0
- langgraph_api/webhook.py +120 -6
- langgraph_api/worker.py +55 -24
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/METADATA +16 -8
- langgraph_api-0.7.3.dist-info/RECORD +168 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/WHEEL +1 -1
- langgraph_runtime/__init__.py +1 -0
- langgraph_runtime/routes.py +11 -0
- logging.json +1 -3
- openapi.json +839 -478
- langgraph_api/config.py +0 -387
- langgraph_api/js/isolate-0x130008000-46649-46649-v8.log +0 -4430
- langgraph_api/js/isolate-0x138008000-44681-44681-v8.log +0 -4430
- langgraph_api/js/package-lock.json +0 -3308
- langgraph_api-0.4.1.dist-info/RECORD +0 -107
- /langgraph_api/{utils.py → grpc/__init__.py} +0 -0
- {langgraph_api-0.4.1.dist-info → langgraph_api-0.7.3.dist-info}/entry_points.txt +0 -0
- {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
|
+
}
|