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,280 @@
1
+ """Custom encryption loading for LangGraph API.
2
+
3
+ This module provides functions to load and access custom encryption
4
+ instances defined by users in their langgraph.json configuration.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import functools
10
+ import importlib.util
11
+ import sys
12
+ from typing import TYPE_CHECKING, Any, Literal, get_args
13
+
14
+ import structlog
15
+
16
+ from langgraph_api import timing
17
+ from langgraph_api.config import LANGGRAPH_ENCRYPTION
18
+ from langgraph_api.encryption.shared import (
19
+ ENCRYPTION_CONTEXT_KEY,
20
+ strip_encryption_metadata,
21
+ )
22
+ from langgraph_api.timing import profiled_import
23
+
24
+ if TYPE_CHECKING:
25
+ from langgraph_sdk import Encryption
26
+ from langgraph_sdk.encryption.types import JsonDecryptor, JsonEncryptor
27
+
28
+ from langgraph_api.encryption.aes_json import AesEncryptionInstance
29
+
30
+ ModelType = Literal["run", "thread", "assistant", "cron", "checkpoint"]
31
+ SUPPORTED_ENCRYPTION_MODELS: frozenset[str] = frozenset(get_args(ModelType))
32
+
33
+ logger = structlog.stdlib.get_logger(__name__)
34
+
35
+
36
+ @functools.lru_cache(maxsize=1)
37
+ def get_custom_encryption_instance() -> Encryption | None:
38
+ """Get the custom (SDK-injected) encryption instance if configured.
39
+
40
+ Custom encryption is user-defined encryption logic loaded from a Python module
41
+ specified in langgraph.json via LANGGRAPH_ENCRYPTION config.
42
+
43
+ Returns:
44
+ The Encryption instance if configured, or None if no custom encryption is configured.
45
+ """
46
+ if not LANGGRAPH_ENCRYPTION:
47
+ return None
48
+ logger.info(
49
+ f"Getting custom encryption instance: {LANGGRAPH_ENCRYPTION}",
50
+ langgraph_encryption=str(LANGGRAPH_ENCRYPTION),
51
+ )
52
+ path = LANGGRAPH_ENCRYPTION.get("path")
53
+ if path is None:
54
+ return None
55
+ return _load_custom_encryption(path)
56
+
57
+
58
+ def _load_custom_encryption(path: str) -> Encryption:
59
+ """Load a custom encryption instance from a module path.
60
+
61
+ Args:
62
+ path: Module path in format "./path/to/file.py:name" or "module:name"
63
+
64
+ Returns:
65
+ The custom Encryption instance.
66
+
67
+ Raises:
68
+ ValueError: If the path is invalid or the encryption instance is not found.
69
+ """
70
+ encryption_instance = _load_encryption_obj(path)
71
+ logger.info(
72
+ f"Loaded custom encryption instance from path {path}: {encryption_instance}"
73
+ )
74
+ return encryption_instance
75
+
76
+
77
+ @timing.timer(
78
+ message="Loading custom encryption {encryption_path}",
79
+ metadata_fn=lambda encryption_path: {"encryption_path": encryption_path},
80
+ warn_threshold_secs=5,
81
+ warn_message="Loading custom encryption '{encryption_path}' took longer than expected",
82
+ error_threshold_secs=10,
83
+ )
84
+ def _load_encryption_obj(path: str) -> Encryption:
85
+ """Load an Encryption object from a path string.
86
+
87
+ Args:
88
+ path: Module path in format "./path/to/file.py:name" or "module:name"
89
+
90
+ Returns:
91
+ The Encryption instance.
92
+
93
+ Raises:
94
+ ValueError: If the path is invalid or the encryption instance is not found.
95
+ ImportError: If the module cannot be imported.
96
+ FileNotFoundError: If the file cannot be found.
97
+ """
98
+ if ":" not in path:
99
+ raise ValueError(
100
+ f"Invalid encryption path format: {path}. "
101
+ "Must be in format: './path/to/file.py:name' or 'module:name'"
102
+ )
103
+
104
+ module_name, callable_name = path.rsplit(":", 1)
105
+ module_name = module_name.rstrip(":")
106
+
107
+ if module_name.endswith(".js") or module_name.endswith(".mjs"):
108
+ raise ValueError(
109
+ f"JavaScript encryption is not supported. "
110
+ f"Please use a Python module instead: {module_name}"
111
+ )
112
+
113
+ try:
114
+ with profiled_import(path):
115
+ if "/" in module_name or ".py" in module_name:
116
+ modname = f"dynamic_module_{hash(module_name)}"
117
+ modspec = importlib.util.spec_from_file_location(modname, module_name)
118
+ if modspec is None or modspec.loader is None:
119
+ raise ValueError(f"Could not load file: {module_name}")
120
+ module = importlib.util.module_from_spec(modspec)
121
+ sys.modules[modname] = module
122
+ modspec.loader.exec_module(module)
123
+ else:
124
+ module = importlib.import_module(module_name)
125
+
126
+ loaded_encrypt = getattr(module, callable_name, None)
127
+ if loaded_encrypt is None:
128
+ raise ValueError(
129
+ f"Could not find encrypt '{callable_name}' in module: {module_name}"
130
+ )
131
+ # Import Encryption at runtime only when needed (avoids requiring SDK 0.2.14)
132
+ from langgraph_sdk import Encryption as EncryptionClass
133
+
134
+ if not isinstance(loaded_encrypt, EncryptionClass):
135
+ raise ValueError(
136
+ f"Expected an Encryption instance, got {type(loaded_encrypt)}"
137
+ )
138
+
139
+ return loaded_encrypt
140
+
141
+ except ImportError as e:
142
+ e.add_note(f"Could not import module:\n{module_name}\n\n")
143
+ raise
144
+ except FileNotFoundError as e:
145
+ raise ValueError(f"Could not find file: {module_name}") from e
146
+
147
+
148
+ class JsonEncryptionWrapper:
149
+ """Wrapper for JSON encryption that routes between custom and AES encryption.
150
+
151
+ This wrapper handles dual-mode encryption routing:
152
+ - Encrypts using custom (SDK-injected) encryption when configured
153
+ - Decrypts using either custom OR AES based on the encryption context marker
154
+ - Supports migration from AES-only to custom encryption (reads old AES data)
155
+
156
+ Key responsibilities:
157
+ - Key preservation validation (encryptor must not add/remove keys)
158
+ - Encryption context storage (adds __encryption_context__ marker)
159
+ - Migration routing (AES-encrypted data routes to AES decryptor)
160
+ - Defensive checks (AES values in custom path raises error)
161
+ """
162
+
163
+ def __init__(
164
+ self,
165
+ custom_instance: Encryption,
166
+ aes_instance: AesEncryptionInstance | None = None,
167
+ ) -> None:
168
+ """Initialize with custom encryption and optional AES for migration.
169
+
170
+ Args:
171
+ custom_instance: The SDK's Encryption instance (custom/user-defined encryption)
172
+ aes_instance: Optional AES instance for migration (decrypting old AES data)
173
+ """
174
+ self._custom = custom_instance
175
+ self._aes = aes_instance
176
+
177
+ def get_json_encryptor(self, model_type: ModelType) -> JsonEncryptor | None:
178
+ """Return an async encryptor that validates keys and adds context.
179
+
180
+ The encryptor:
181
+ 1. Calls the custom encryptor
182
+ 2. Validates key preservation (no added/removed keys)
183
+ 3. Adds __encryption_context__ with the user's context
184
+
185
+ Args:
186
+ model_type: The type of model being encrypted
187
+
188
+ Returns:
189
+ Async encryptor function, or None if custom has no encryptor
190
+ """
191
+ from langgraph_api.encryption.aes_json import EncryptionKeyError
192
+
193
+ custom_encryptor = self._custom.get_json_encryptor(model_type)
194
+ if custom_encryptor is None:
195
+ return None
196
+
197
+ async def encryptor(ctx: Any, data: dict[str, Any]) -> dict[str, Any]:
198
+ encrypted = await custom_encryptor(ctx, data)
199
+
200
+ # Validate key preservation for SQL JSONB merge compatibility
201
+ if encrypted is not None and isinstance(encrypted, dict):
202
+ input_keys = set(data.keys())
203
+ output_keys = set(encrypted.keys())
204
+ added_keys = output_keys - input_keys
205
+ removed_keys = input_keys - output_keys
206
+ if added_keys or removed_keys:
207
+ raise EncryptionKeyError(
208
+ f"JSON encryptor must preserve key structure for SQL JSONB merge compatibility. "
209
+ f"Added keys: {added_keys or 'none'}, removed keys: {removed_keys or 'none'}. "
210
+ f"Use per-key encryption (transform values, not keys) instead of envelope patterns."
211
+ )
212
+
213
+ # Add encryption context marker with user's context
214
+ from langgraph_api.encryption.context import get_encryption_context
215
+
216
+ encrypted[ENCRYPTION_CONTEXT_KEY] = get_encryption_context()
217
+
218
+ return encrypted
219
+
220
+ return encryptor
221
+
222
+ def get_json_decryptor(self, model_type: ModelType) -> JsonDecryptor:
223
+ """Return an async decryptor that routes based on encryption type.
224
+
225
+ The decryptor routes based on __encryption_context__ marker:
226
+ 1. AES type marker → AES decryptor (migration path)
227
+ 2. Custom marker → custom decryptor (with defensive check)
228
+ 3. No marker → passthrough (plaintext)
229
+
230
+ Args:
231
+ model_type: The type of model being decrypted
232
+
233
+ Returns:
234
+ Async decryptor function (always returns a function for routing)
235
+ """
236
+ from langgraph_api.encryption.aes_json import (
237
+ DecryptorMissingError,
238
+ EncryptionRoutingError,
239
+ has_any_aes_encrypted_values,
240
+ is_aes_encryption_context,
241
+ )
242
+
243
+ custom_decryptor = self._custom.get_json_decryptor(model_type)
244
+
245
+ async def decryptor(ctx: Any, data: dict[str, Any]) -> dict[str, Any]:
246
+ # No marker → plaintext passthrough
247
+ if ENCRYPTION_CONTEXT_KEY not in data:
248
+ return strip_encryption_metadata(data)
249
+
250
+ context_dict = data[ENCRYPTION_CONTEXT_KEY]
251
+
252
+ # AES type marker → route to AES decryptor (migration path)
253
+ if is_aes_encryption_context(context_dict):
254
+ if self._aes is None:
255
+ raise DecryptorMissingError(
256
+ f"Data has AES encryption marker but LANGGRAPH_AES_KEY is not configured "
257
+ f"for {model_type}"
258
+ )
259
+ aes_decryptor = self._aes.get_json_decryptor(model_type)
260
+ return await aes_decryptor(ctx, data)
261
+
262
+ # Custom marker → use custom decryptor
263
+ if custom_decryptor is None:
264
+ raise DecryptorMissingError(
265
+ f"Data contains custom encryption marker but no decryptor is configured for {model_type}"
266
+ )
267
+
268
+ # Defensive check: ensure custom decryptor doesn't receive AES-encrypted values
269
+ if has_any_aes_encrypted_values(data):
270
+ raise EncryptionRoutingError(
271
+ f"Data has AES-encrypted values but is being routed to custom decryptor. "
272
+ f"This indicates a bug in encryption routing for {model_type}."
273
+ )
274
+
275
+ # Strip marker and decrypt
276
+ data = strip_encryption_metadata(data)
277
+ decrypted = await custom_decryptor(ctx, data)
278
+ return strip_encryption_metadata(decrypted)
279
+
280
+ return decryptor