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 +1 -1
- langgraph_api/utils/stream_codec.py +315 -0
- {langgraph_api-0.4.22.dist-info → langgraph_api-0.4.23.dist-info}/METADATA +2 -2
- {langgraph_api-0.4.22.dist-info → langgraph_api-0.4.23.dist-info}/RECORD +7 -6
- {langgraph_api-0.4.22.dist-info → langgraph_api-0.4.23.dist-info}/WHEEL +0 -0
- {langgraph_api-0.4.22.dist-info → langgraph_api-0.4.23.dist-info}/entry_points.txt +0 -0
- {langgraph_api-0.4.22.dist-info → langgraph_api-0.4.23.dist-info}/licenses/LICENSE +0 -0
langgraph_api/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.4.
|
|
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.
|
|
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
|
+
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=
|
|
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.
|
|
102
|
-
langgraph_api-0.4.
|
|
103
|
-
langgraph_api-0.4.
|
|
104
|
-
langgraph_api-0.4.
|
|
105
|
-
langgraph_api-0.4.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|