mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
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:
co-authored by
William FH
parent
df19173191
commit
efe78447b3
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user