langgraph-api 0.4.22__py3-none-any.whl → 0.4.23__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.22"
1
+ __version__ = "0.4.23"
@@ -0,0 +1,315 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ from dataclasses import dataclass
5
+
6
+ import orjson
7
+ import structlog
8
+
9
+ PROTOCOL_VERSION = 1
10
+ """
11
+ ---
12
+ Version 1:
13
+ Byte Offsets
14
+ 0 1 3 5 5+N 5+N+M
15
+ +--------+------------------+----------------+------------------+------------------+--------------------+
16
+ | version| stream_id_len | event_len | stream_id | event | message |
17
+ +--------+------------------+----------------+------------------+------------------+--------------------+
18
+ 1 B 2 B 2 B N B M B variable
19
+
20
+ ---- Old (to be dropped soon / multiple formats)
21
+ Version 0 (old):
22
+ 1) b"$:" + <stream_id> + b"$:" + <event> + b"$:" + <raw_json>
23
+ 2) b"$:" + <stream_id> + b"$:" + <raw_json>
24
+ """
25
+
26
+ BYTE_MASK = 0xFF
27
+ HEADER_LEN = 5
28
+ logger = structlog.stdlib.get_logger(__name__)
29
+
30
+
31
+ class StreamFormatError(ValueError):
32
+ """Raised when a stream frame fails validation."""
33
+
34
+
35
+ @dataclass(slots=True)
36
+ class StreamPacket:
37
+ version: int
38
+ event: memoryview | bytes
39
+ message: memoryview | bytes
40
+ stream_id: memoryview | bytes | None
41
+
42
+ @property
43
+ def event_bytes(self) -> bytes:
44
+ return (
45
+ self.event.tobytes() if isinstance(self.event, memoryview) else self.event
46
+ )
47
+
48
+ @property
49
+ def message_bytes(self) -> bytes:
50
+ return (
51
+ self.message.tobytes()
52
+ if isinstance(self.message, memoryview)
53
+ else self.message
54
+ )
55
+
56
+ @property
57
+ def resumable(self) -> bool:
58
+ return self.stream_id is not None
59
+
60
+ @property
61
+ def stream_id_bytes(self) -> bytes | None:
62
+ if self.stream_id is None:
63
+ return None
64
+ if isinstance(self.stream_id, bytes):
65
+ return self.stream_id
66
+ return self.stream_id.tobytes()
67
+
68
+
69
+ class StreamCodec:
70
+ """Codec for encoding and decoding stream packets."""
71
+
72
+ __slots__ = ("_version",)
73
+
74
+ def __init__(self, *, protocol_version: int = PROTOCOL_VERSION) -> None:
75
+ self._version = protocol_version & BYTE_MASK
76
+
77
+ def encode(
78
+ self,
79
+ event: str,
80
+ message: bytes,
81
+ *,
82
+ stream_id: str | None = None,
83
+ ) -> bytes:
84
+ if not event:
85
+ raise StreamFormatError("event cannot be empty")
86
+ event_bytes = event.encode("utf-8")
87
+ if len(event_bytes) > 0xFFFF:
88
+ raise StreamFormatError("event exceeds 65535 bytes; cannot encode")
89
+ if not event_bytes:
90
+ raise StreamFormatError("event cannot be empty")
91
+
92
+ if stream_id:
93
+ # It's a resumable stream
94
+ stream_id_bytes = stream_id.encode("utf-8")
95
+ if len(stream_id_bytes) > 0xFFFF:
96
+ raise StreamFormatError("stream_id exceeds 65535 bytes; cannot encode")
97
+ else:
98
+ stream_id_bytes = None
99
+ stream_id_len = len(stream_id_bytes) if stream_id_bytes else 0
100
+ event_len = len(event_bytes)
101
+ frame = bytearray(HEADER_LEN + stream_id_len + event_len + len(message))
102
+ frame[0] = self._version
103
+ frame[1:3] = stream_id_len.to_bytes(2, "big")
104
+ frame[3:5] = event_len.to_bytes(2, "big")
105
+
106
+ cursor = HEADER_LEN
107
+ if stream_id_bytes is not None:
108
+ frame[cursor : cursor + stream_id_len] = stream_id_bytes
109
+ cursor += stream_id_len
110
+
111
+ frame[cursor : cursor + event_len] = event_bytes
112
+ cursor += event_len
113
+ frame[cursor:] = message
114
+ return bytes(frame)
115
+
116
+ def decode(self, data: bytes | bytearray | memoryview) -> StreamPacket:
117
+ view = data if isinstance(data, memoryview) else memoryview(data)
118
+ if len(view) < HEADER_LEN:
119
+ raise StreamFormatError("frame too short")
120
+
121
+ version = view[0]
122
+ if version != self._version:
123
+ raise StreamFormatError(f"unsupported protocol version: {version}")
124
+
125
+ stream_id_len = int.from_bytes(view[1:3], "big")
126
+ event_len = int.from_bytes(view[3:5], "big")
127
+ if event_len == 0:
128
+ raise StreamFormatError("event cannot be empty")
129
+ offset = HEADER_LEN
130
+ if stream_id_len > 0:
131
+ stream_id_view = view[offset : offset + stream_id_len]
132
+ offset += stream_id_len
133
+ else:
134
+ # Not resumable
135
+ stream_id_view = None
136
+ if len(view) < offset + event_len:
137
+ raise StreamFormatError("truncated event payload")
138
+ event_view = view[offset : offset + event_len]
139
+ offset += event_len
140
+ message_view = view[offset:]
141
+ return StreamPacket(
142
+ version=version,
143
+ event=event_view,
144
+ message=message_view,
145
+ stream_id=stream_id_view,
146
+ )
147
+
148
+ def decode_safe(self, data: bytes | bytearray | memoryview) -> StreamPacket | None:
149
+ try:
150
+ return self.decode(data)
151
+ except StreamFormatError as e:
152
+ logger.warning(f"Failed to decode as version {self._version}", error=e)
153
+ return None
154
+
155
+
156
+ STREAM_CODEC = StreamCodec()
157
+
158
+
159
+ def decode_stream_message(
160
+ data: bytes | bytearray | memoryview,
161
+ *,
162
+ channel: bytes | str | None = None,
163
+ ) -> StreamPacket:
164
+ if isinstance(data, memoryview):
165
+ view = data
166
+ elif isinstance(data, (bytes, bytearray)):
167
+ view = memoryview(data)
168
+ else:
169
+ logger.warning("Unknown type for stream message", type=type(data))
170
+ view = memoryview(bytes(data))
171
+
172
+ # Current protocol version
173
+ if packet := STREAM_CODEC.decode_safe(view):
174
+ return packet
175
+ logger.debug("Attempting to decode a v0 formatted stream message")
176
+ # Legacy codecs. Yuck. Won't be hit unless you have stale pods running (or for a brief period during upgrade).
177
+ # Schedule for removal in next major release.
178
+ if packet := _decode_v0_resumable_format(view, channel):
179
+ return packet
180
+
181
+ # Non-resumable format.
182
+ if packet := _decode_v0_live_format(view, channel):
183
+ return packet
184
+ raise StreamFormatError("failed to decode stream message")
185
+
186
+
187
+ _STREAMING_DELIMITER = b"$:"
188
+ _STREAMING_DELIMITER_LEN = len(_STREAMING_DELIMITER)
189
+
190
+
191
+ def _decode_v0_resumable_format(
192
+ view: memoryview,
193
+ channel: bytes | str | None = None,
194
+ ) -> StreamPacket | None:
195
+ """
196
+ Legacy v0 resumable format:
197
+ 1) b"$:" + <stream_id> + b"$:" + <event> + b"$:" + <raw_json>
198
+ 2) b"$:" + <stream_id> + b"$:" + <raw_json>
199
+ """
200
+
201
+ # must start with "$:"
202
+ if (
203
+ len(view) < _STREAMING_DELIMITER_LEN
204
+ or view[:_STREAMING_DELIMITER_LEN] != _STREAMING_DELIMITER
205
+ ):
206
+ return None
207
+
208
+ # "$:<stream_id>$:"
209
+ first = _find_delim(view, _STREAMING_DELIMITER_LEN, _STREAMING_DELIMITER)
210
+ if first == -1:
211
+ return None
212
+ stream_view = view[_STREAMING_DELIMITER_LEN:first]
213
+
214
+ # try "$:<event>$:"
215
+ second = _find_delim(view, first + _STREAMING_DELIMITER_LEN, _STREAMING_DELIMITER)
216
+ if second != -1:
217
+ event_view = view[first + _STREAMING_DELIMITER_LEN : second]
218
+ msg_view = view[second + _STREAMING_DELIMITER_LEN :]
219
+ return StreamPacket(
220
+ version=0,
221
+ event=event_view,
222
+ message=msg_view,
223
+ stream_id=stream_view,
224
+ )
225
+
226
+ chan_bytes = channel.encode("utf-8") if isinstance(channel, str) else channel
227
+
228
+ if chan_bytes:
229
+ marker = b":stream:"
230
+ idx = chan_bytes.rfind(marker)
231
+ event_bytes = chan_bytes[idx + len(marker) :] if idx != -1 else chan_bytes
232
+ else:
233
+ event_bytes = b""
234
+
235
+ msg_view = view[first + _STREAMING_DELIMITER_LEN :]
236
+ return StreamPacket(
237
+ version=0,
238
+ event=memoryview(event_bytes),
239
+ message=msg_view,
240
+ stream_id=stream_view,
241
+ )
242
+
243
+
244
+ def _decode_v0_live_format(
245
+ view: memoryview, channel: bytes | str | None = None
246
+ ) -> StreamPacket | None:
247
+ try:
248
+ package = orjson.loads(view)
249
+ except orjson.JSONDecodeError:
250
+ return _decode_v0_flat_format(view, channel)
251
+ if (
252
+ not isinstance(package, dict)
253
+ or "event" not in package
254
+ or "message" not in package
255
+ ):
256
+ return _decode_v0_flat_format(view, channel)
257
+ event_obj = package.get("event")
258
+ message_obj = package.get("message")
259
+ if event_obj is None:
260
+ event_bytes = b""
261
+ elif isinstance(event_obj, str):
262
+ event_bytes = event_obj.encode()
263
+ elif isinstance(event_obj, (bytes, bytearray, memoryview)):
264
+ event_bytes = bytes(event_obj)
265
+ else:
266
+ event_bytes = orjson.dumps(event_obj)
267
+
268
+ if isinstance(message_obj, (bytes, bytearray, memoryview)):
269
+ message_view = memoryview(bytes(message_obj))
270
+ elif isinstance(message_obj, str):
271
+ try:
272
+ message_view = memoryview(base64.b64decode(message_obj))
273
+ except Exception:
274
+ message_view = memoryview(message_obj.encode())
275
+ elif message_obj is None:
276
+ message_view = memoryview(b"")
277
+ else:
278
+ message_view = memoryview(orjson.dumps(message_obj))
279
+
280
+ return StreamPacket(
281
+ event=event_bytes,
282
+ message=message_view,
283
+ stream_id=None,
284
+ version=0,
285
+ )
286
+
287
+
288
+ def _decode_v0_flat_format(
289
+ view: memoryview, channel: bytes | str | None = None
290
+ ) -> StreamPacket | None:
291
+ packet = bytes(view)
292
+ stream_id = None
293
+ if channel is None:
294
+ return
295
+ if packet.startswith(b"$:"):
296
+ _, stream_id, packet = packet.split(b":", 2)
297
+ channel = channel.encode("utf-8") if isinstance(channel, str) else channel
298
+ channel = channel.split(b":")[-1]
299
+ return StreamPacket(
300
+ version=0,
301
+ event=memoryview(channel),
302
+ message=memoryview(packet),
303
+ stream_id=stream_id,
304
+ )
305
+
306
+
307
+ def _find_delim(view: memoryview, start: int, delimiter: bytes) -> int:
308
+ delim_len = len(delimiter)
309
+ end = len(view) - delim_len
310
+ i = start
311
+ while i <= end:
312
+ if view[i : i + delim_len] == delimiter:
313
+ return i
314
+ i += 1
315
+ return -1
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-api
3
- Version: 0.4.22
3
+ Version: 0.4.23
4
4
  Author-email: Nuno Campos <nuno@langchain.dev>, Will Fu-Hinthorn <will@langchain.dev>
5
5
  License: Elastic-2.0
6
6
  License-File: LICENSE
@@ -11,7 +11,7 @@ Requires-Dist: httpx>=0.25.0
11
11
  Requires-Dist: jsonschema-rs<0.30,>=0.20.0
12
12
  Requires-Dist: langchain-core>=0.3.64
13
13
  Requires-Dist: langgraph-checkpoint>=2.0.23
14
- Requires-Dist: langgraph-runtime-inmem<0.14.0,>=0.13.0
14
+ Requires-Dist: langgraph-runtime-inmem<0.15.0,>=0.14.0
15
15
  Requires-Dist: langgraph-sdk>=0.2.0
16
16
  Requires-Dist: langgraph>=0.4.0
17
17
  Requires-Dist: langsmith>=0.3.45
@@ -1,4 +1,4 @@
1
- langgraph_api/__init__.py,sha256=dTG-jzL6gT1bOj-aXPys00hnioYMNGLuF9EnbsEF7HI,23
1
+ langgraph_api/__init__.py,sha256=bUxoIOr-G9-PoGmh7zAW9CCJTt17Q0QuRmIjl2A39Sw,23
2
2
  langgraph_api/asgi_transport.py,sha256=XtiLOu4WWsd-xizagBLzT5xUkxc9ZG9YqwvETBPjBFE,5161
3
3
  langgraph_api/asyncio.py,sha256=FEEkLm_N-15cbElo4vQ309MkDKBZuRqAYV8VJ1DocNw,9860
4
4
  langgraph_api/cli.py,sha256=DrTkO5JSX6jpv-aFXZfRP5Fa9j121nvnrjDgQQzqlHs,19576
@@ -83,6 +83,7 @@ langgraph_api/utils/config.py,sha256=Tbp4tKDSLKXQJ44EKr885wAQupY-9VWNJ6rgUU2oLOY
83
83
  langgraph_api/utils/future.py,sha256=lXsRQPhJwY7JUbFFZrK-94JjgsToLu-EWU896hvbUxE,7289
84
84
  langgraph_api/utils/headers.py,sha256=NDBmKSSVOOYeYN0HfK1a3xbYtAg35M_JO1G9yklpZsA,5682
85
85
  langgraph_api/utils/retriable_client.py,sha256=a50ZxfXV48C97rOCiVWAEmfOPJELwPnvUyEqo3vEixI,2379
86
+ langgraph_api/utils/stream_codec.py,sha256=bwxCm9bIsfSQ742oPKmZs__O0qVR4ylahEKtFMF4ygM,9941
86
87
  langgraph_api/utils/uuids.py,sha256=AW_9-1iFqK2K5hljmi-jtaNzUIoBshk5QPt8LbpbD2g,2975
87
88
  langgraph_license/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
89
  langgraph_license/validation.py,sha256=CU38RUZ5xhP1S8F_y8TNeV6OmtO-tIGjCXbXTwJjJO4,612
@@ -98,8 +99,8 @@ langgraph_runtime/store.py,sha256=7mowndlsIroGHv3NpTSOZDJR0lCuaYMBoTnTrewjslw,11
98
99
  LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
99
100
  logging.json,sha256=3RNjSADZmDq38eHePMm1CbP6qZ71AmpBtLwCmKU9Zgo,379
100
101
  openapi.json,sha256=21wu-NxdxyTQwZctNcEfRkLMnSBi0QhGAfwq5kg8XNU,172618
101
- langgraph_api-0.4.22.dist-info/METADATA,sha256=sfURjIfcLWtfec-SjnaQIIE_1ZjLzposwZ3DhXZ4XwI,3893
102
- langgraph_api-0.4.22.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
103
- langgraph_api-0.4.22.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
104
- langgraph_api-0.4.22.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
105
- langgraph_api-0.4.22.dist-info/RECORD,,
102
+ langgraph_api-0.4.23.dist-info/METADATA,sha256=ui831WZVVxEY2Uz8JdOAfMfmPLrCJaJPiLJfd8M2FMg,3893
103
+ langgraph_api-0.4.23.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
104
+ langgraph_api-0.4.23.dist-info/entry_points.txt,sha256=hGedv8n7cgi41PypMfinwS_HfCwA7xJIfS0jAp8htV8,78
105
+ langgraph_api-0.4.23.dist-info/licenses/LICENSE,sha256=ZPwVR73Biwm3sK6vR54djCrhaRiM4cAD2zvOQZV8Xis,3859
106
+ langgraph_api-0.4.23.dist-info/RECORD,,