feat(sdk-py): emit id as part of stream events (#6581)

- **Description:** Provide the id of the event for routes that use SSE
streams. This will allow for more custom retry logic when streams
disconnect if needed.
  - **Issue:** N/A
  - **Dependencies:** N/A
  - **Twitter handle:** N/A

---------

Co-authored-by: William FH <13333726+hinthornw@users.noreply.github.com>
This commit is contained in:
Josh Rogers
2025-12-12 17:16:48 -05:00
committed by GitHub
co-authored by William FH
parent df19173191
commit efe78447b3
6 changed files with 21 additions and 18 deletions
+1 -1
View File
@@ -26,7 +26,7 @@ classifiers = [
dependencies = [
"langchain-core>=0.1",
"langgraph-checkpoint>=2.1.0,<4.0.0",
"langgraph-sdk>=0.2.2,<0.3.0",
"langgraph-sdk>=0.3.0,<0.4.0",
"langgraph-prebuilt>=1.0.2,<1.1.0",
"xxhash>=3.5.0",
"pydantic>=2.7.4",
+6 -6
View File
@@ -567,9 +567,9 @@ def test_stream():
stream_parts.append(stream_part)
assert stream_parts == [
("updates", {"chunk": "data3"}),
("updates", {"chunk": "data4"}),
("updates", {"__interrupt__": ()}),
("updates", {"chunk": "data3"}, None),
("updates", {"chunk": "data4"}, None),
("updates", {"__interrupt__": ()}, None),
]
# subgraphs + list modes
@@ -739,9 +739,9 @@ async def test_astream():
stream_parts.append(stream_part)
assert stream_parts == [
("updates", {"chunk": "data3"}),
("updates", {"chunk": "data4"}),
("updates", {"__interrupt__": ()}),
("updates", {"chunk": "data3"}, None),
("updates", {"chunk": "data4"}, None),
("updates", {"__interrupt__": ()}, None),
]
# subgraphs + list modes
+1 -1
View File
@@ -3,6 +3,6 @@ from langgraph_sdk.client import get_client, get_sync_client
from langgraph_sdk.encryption import Encryption
from langgraph_sdk.encryption.types import EncryptionContext
__version__ = "0.2.15"
__version__ = "0.3.0"
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
+5 -3
View File
@@ -167,7 +167,7 @@ class Config(TypedDict, total=False):
"""
Runtime values for attributes previously made configurable on this Runnable,
or sub-Runnables, through .configurable_fields() or .configurable_alternatives().
Check .output_schema() for a description of the attributes that have been made
Check .output_schema() for a description of the attributes that have been made
configurable.
"""
@@ -301,7 +301,7 @@ class ThreadState(TypedDict):
values: list[dict] | dict[str, Any]
"""The state values."""
next: Sequence[str]
"""The next nodes to execute. If empty, the thread is done until new input is
"""The next nodes to execute. If empty, the thread is done until new input is
received."""
checkpoint: Checkpoint
"""The ID of the checkpoint."""
@@ -476,7 +476,7 @@ class Item(TypedDict):
"""The namespace of the item. A namespace is analogous to a document's directory."""
key: str
"""The unique identifier of the item within its namespace.
In general, keys needn't be globally unique.
"""
value: dict[str, Any]
@@ -519,6 +519,8 @@ class StreamPart(NamedTuple):
"""The type of event for this stream part."""
data: dict
"""The data payload associated with the event."""
id: str | None = None
"""The ID of the event."""
class Send(TypedDict):
+1
View File
@@ -103,6 +103,7 @@ class SSEDecoder:
sse = StreamPart(
event=self._event,
data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
id=self.last_event_id,
)
# NOTE: as per the SSE spec, do not reset last_event_id.
+7 -7
View File
@@ -50,7 +50,7 @@ def iter_lines_raw(payload: list[bytes]) -> Iterator[BytesLike]:
yield from decoder.flush()
def test_stream_see():
def test_stream_sse():
for groups in (
[RESPONSE_PAYLOAD],
RESPONSE_PAYLOAD.splitlines(keepends=True),
@@ -150,9 +150,9 @@ def test_sync_http_client_stream_recovers_after_disconnect():
assert call_count == 2
assert parts == [
StreamPart(event="values", data={"step": 1}),
StreamPart(event="values", data={"step": 2}),
StreamPart(event="end", data=None), # ty: ignore
StreamPart(event="values", data={"step": 1}, id="1"),
StreamPart(event="values", data={"step": 2}, id="2"),
StreamPart(event="end", data=None, id="2"),
]
@@ -222,9 +222,9 @@ async def test_http_client_stream_recovers_after_disconnect():
assert call_count == 2
assert parts == [
StreamPart(event="values", data={"step": 1}),
StreamPart(event="values", data={"step": 2}),
StreamPart(event="end", data=None),
StreamPart(event="values", data={"step": 1}, id="1"),
StreamPart(event="values", data={"step": 2}, id="2"),
StreamPart(event="end", data=None, id="2"),
]