langgraph-sdk 0.2.6__tar.gz → 0.2.8__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-sdk
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: SDK for interacting with LangGraph API
5
5
  Project-URL: Repository, https://www.github.com/langchain-ai/langgraph
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  from langgraph_sdk.auth import Auth
2
2
  from langgraph_sdk.client import get_client, get_sync_client
3
3
 
4
- __version__ = "0.2.6"
4
+ __version__ = "0.2.8"
5
5
 
6
6
  __all__ = ["Auth", "get_client", "get_sync_client"]
@@ -157,27 +157,38 @@ def get_client(
157
157
  headers: Mapping[str, str] | None = None,
158
158
  timeout: TimeoutTypes | None = None,
159
159
  ) -> LangGraphClient:
160
- """Get a LangGraphClient instance.
160
+ """Create and configure a LangGraphClient.
161
+
162
+ The client provides programmatic access to a LangGraph Platform deployment. It supports
163
+ both remote servers and local in-process connections (when running inside a LangGraph server).
161
164
 
162
165
  Args:
163
- url: The URL of the LangGraph API.
164
- api_key: The API key. If not provided, it will be read from the environment.
165
- Precedence:
166
- 1. explicit argument
167
- 2. LANGGRAPH_API_KEY
168
- 3. LANGSMITH_API_KEY
169
- 4. LANGCHAIN_API_KEY
170
- headers: Optional custom headers
171
- timeout: Optional timeout configuration for the HTTP client.
172
- Accepts an httpx.Timeout instance, a float (seconds), or a tuple of timeouts.
173
- Tuple format is (connect, read, write, pool)
174
- If not provided, defaults to connect=5s, read=300s, write=300s, and pool=5s.
166
+ url:
167
+ Base URL of the LangGraph API.
168
+ – If `None`, the client first attempts an in-process connection via ASGI transport.
169
+ If that fails, it falls back to `http://localhost:8123`.
170
+ api_key:
171
+ API key for authentication. If omitted, the client reads from environment
172
+ variables in the following order:
173
+ 1. Function argument
174
+ 2. `LANGGRAPH_API_KEY`
175
+ 3. `LANGSMITH_API_KEY`
176
+ 4. `LANGCHAIN_API_KEY`
177
+ headers:
178
+ Additional HTTP headers to include in requests. Merged with authentication headers.
179
+ timeout:
180
+ HTTP timeout configuration. May be:
181
+ – `httpx.Timeout` instance
182
+ – float (total seconds)
183
+ – tuple `(connect, read, write, pool)` in seconds
184
+ Defaults: connect=5, read=300, write=300, pool=5.
175
185
 
176
186
  Returns:
177
- LangGraphClient: The top-level client for accessing AssistantsClient,
178
- ThreadsClient, RunsClient, and CronClient.
187
+ LangGraphClient:
188
+ A top-level client exposing sub-clients for assistants, threads,
189
+ runs, and cron operations.
179
190
 
180
- ???+ example "Example"
191
+ ???+ example "Connect to a remote server:"
181
192
 
182
193
  ```python
183
194
  from langgraph_sdk import get_client
@@ -188,6 +199,21 @@ def get_client(
188
199
  # example usage: client.<model>.<method_name>()
189
200
  assistants = await client.assistants.get(assistant_id="some_uuid")
190
201
  ```
202
+
203
+ ???+ example "Connect in-process to a running LangGraph server:"
204
+
205
+ ```python
206
+ from langgraph_sdk import get_client
207
+
208
+ client = get_client(url=None)
209
+
210
+ async def my_node(...):
211
+ subagent_result = await client.runs.wait(
212
+ thread_id=None,
213
+ assistant_id="agent",
214
+ input={"messages": [{"role": "user", "content": "Foo"}]},
215
+ )
216
+ ```
191
217
  """
192
218
 
193
219
  transport: httpx.AsyncBaseTransport | None = None
@@ -429,34 +455,94 @@ class HttpClient:
429
455
  if headers:
430
456
  request_headers.update(headers)
431
457
 
432
- async with self.client.stream(
433
- method, path, headers=request_headers, content=content, params=params
434
- ) as res:
435
- if on_response:
436
- on_response(res)
437
- # check status
438
- try:
439
- res.raise_for_status()
440
- except httpx.HTTPStatusError as e:
441
- body = (await res.aread()).decode()
442
- if sys.version_info >= (3, 11):
443
- e.add_note(body)
458
+ reconnect_headers = {
459
+ key: value
460
+ for key, value in request_headers.items()
461
+ if key.lower() not in {"content-length", "content-type"}
462
+ }
463
+
464
+ last_event_id: str | None = None
465
+ reconnect_path: str | None = None
466
+ reconnect_attempts = 0
467
+ max_reconnect_attempts = 5
468
+
469
+ while True:
470
+ current_headers = dict(
471
+ request_headers if reconnect_path is None else reconnect_headers
472
+ )
473
+ if last_event_id is not None:
474
+ current_headers["Last-Event-ID"] = last_event_id
475
+
476
+ current_method = method if reconnect_path is None else "GET"
477
+ current_content = content if reconnect_path is None else None
478
+ current_params = params if reconnect_path is None else None
479
+
480
+ retry = False
481
+ async with self.client.stream(
482
+ current_method,
483
+ reconnect_path or path,
484
+ headers=current_headers,
485
+ content=current_content,
486
+ params=current_params,
487
+ ) as res:
488
+ if reconnect_path is None and on_response:
489
+ on_response(res)
490
+ # check status
491
+ try:
492
+ res.raise_for_status()
493
+ except httpx.HTTPStatusError as e:
494
+ body = (await res.aread()).decode()
495
+ if sys.version_info >= (3, 11):
496
+ e.add_note(body)
497
+ else:
498
+ logger.error(f"Error from langgraph-api: {body}", exc_info=e)
499
+ raise e
500
+ # check content type
501
+ content_type = res.headers.get("content-type", "").partition(";")[0]
502
+ if "text/event-stream" not in content_type:
503
+ raise httpx.TransportError(
504
+ "Expected response header Content-Type to contain 'text/event-stream', "
505
+ f"got {content_type!r}"
506
+ )
507
+
508
+ reconnect_location = res.headers.get("location")
509
+ if reconnect_location:
510
+ reconnect_path = reconnect_location
511
+
512
+ # parse SSE
513
+ decoder = SSEDecoder()
514
+ try:
515
+ async for line in aiter_lines_raw(res):
516
+ sse = decoder.decode(line=line.rstrip(b"\n"))
517
+ if sse is not None:
518
+ if decoder.last_event_id is not None:
519
+ last_event_id = decoder.last_event_id
520
+ if sse.event or sse.data is not None:
521
+ yield sse
522
+ except httpx.HTTPError:
523
+ # httpx.TransportError inherits from HTTPError, so transient
524
+ # disconnects during streaming land here.
525
+ if reconnect_path is None:
526
+ raise
527
+ retry = True
444
528
  else:
445
- logger.error(f"Error from langgraph-api: {body}", exc_info=e)
446
- raise e
447
- # check content type
448
- content_type = res.headers.get("content-type", "").partition(";")[0]
449
- if "text/event-stream" not in content_type:
450
- raise httpx.TransportError(
451
- "Expected response header Content-Type to contain 'text/event-stream', "
452
- f"got {content_type!r}"
453
- )
454
- # parse SSE
455
- decoder = SSEDecoder()
456
- async for line in aiter_lines_raw(res):
457
- sse = decoder.decode(line=line.rstrip(b"\n"))
458
- if sse is not None:
459
- yield sse
529
+ if sse := decoder.decode(b""):
530
+ if decoder.last_event_id is not None:
531
+ last_event_id = decoder.last_event_id
532
+ if sse.event or sse.data is not None:
533
+ # decoder.decode(b"") flushes the in-flight event and may
534
+ # return an empty placeholder when there is no pending
535
+ # message. Skip these no-op events so the stream doesn't
536
+ # emit a trailing blank item after reconnects.
537
+ yield sse
538
+ if retry:
539
+ reconnect_attempts += 1
540
+ if reconnect_attempts > max_reconnect_attempts:
541
+ raise httpx.TransportError(
542
+ "Exceeded maximum SSE reconnection attempts"
543
+ )
544
+ continue
545
+ break
460
546
 
461
547
 
462
548
  async def _aencode_json(json: Any) -> tuple[dict[str, str], bytes | None]:
@@ -3614,39 +3700,100 @@ class SyncHttpClient:
3614
3700
  on_response: Callable[[httpx.Response], None] | None = None,
3615
3701
  ) -> Iterator[StreamPart]:
3616
3702
  """Stream the results of a request using SSE."""
3617
- request_headers, content = _encode_json(json)
3703
+ if json is not None:
3704
+ request_headers, content = _encode_json(json)
3705
+ else:
3706
+ request_headers, content = {}, None
3618
3707
  request_headers["Accept"] = "text/event-stream"
3619
3708
  request_headers["Cache-Control"] = "no-store"
3620
3709
  if headers:
3621
3710
  request_headers.update(headers)
3622
- with self.client.stream(
3623
- method, path, headers=request_headers, content=content, params=params
3624
- ) as res:
3625
- if on_response:
3626
- on_response(res)
3627
- # check status
3628
- try:
3629
- res.raise_for_status()
3630
- except httpx.HTTPStatusError as e:
3631
- body = (res.read()).decode()
3632
- if sys.version_info >= (3, 11):
3633
- e.add_note(body)
3711
+
3712
+ reconnect_headers = {
3713
+ key: value
3714
+ for key, value in request_headers.items()
3715
+ if key.lower() not in {"content-length", "content-type"}
3716
+ }
3717
+
3718
+ last_event_id: str | None = None
3719
+ reconnect_path: str | None = None
3720
+ reconnect_attempts = 0
3721
+ max_reconnect_attempts = 5
3722
+
3723
+ while True:
3724
+ current_headers = dict(
3725
+ request_headers if reconnect_path is None else reconnect_headers
3726
+ )
3727
+ if last_event_id is not None:
3728
+ current_headers["Last-Event-ID"] = last_event_id
3729
+
3730
+ current_method = method if reconnect_path is None else "GET"
3731
+ current_content = content if reconnect_path is None else None
3732
+ current_params = params if reconnect_path is None else None
3733
+
3734
+ retry = False
3735
+ with self.client.stream(
3736
+ current_method,
3737
+ reconnect_path or path,
3738
+ headers=current_headers,
3739
+ content=current_content,
3740
+ params=current_params,
3741
+ ) as res:
3742
+ if reconnect_path is None and on_response:
3743
+ on_response(res)
3744
+ # check status
3745
+ try:
3746
+ res.raise_for_status()
3747
+ except httpx.HTTPStatusError as e:
3748
+ body = (res.read()).decode()
3749
+ if sys.version_info >= (3, 11):
3750
+ e.add_note(body)
3751
+ else:
3752
+ logger.error(f"Error from langgraph-api: {body}", exc_info=e)
3753
+ raise e
3754
+ # check content type
3755
+ content_type = res.headers.get("content-type", "").partition(";")[0]
3756
+ if "text/event-stream" not in content_type:
3757
+ raise httpx.TransportError(
3758
+ "Expected response header Content-Type to contain 'text/event-stream', "
3759
+ f"got {content_type!r}"
3760
+ )
3761
+
3762
+ reconnect_location = res.headers.get("location")
3763
+ if reconnect_location:
3764
+ reconnect_path = reconnect_location
3765
+
3766
+ decoder = SSEDecoder()
3767
+ try:
3768
+ for line in iter_lines_raw(res):
3769
+ sse = decoder.decode(line.rstrip(b"\n"))
3770
+ if sse is not None:
3771
+ if decoder.last_event_id is not None:
3772
+ last_event_id = decoder.last_event_id
3773
+ if sse.event or sse.data is not None:
3774
+ yield sse
3775
+ except httpx.HTTPError:
3776
+ # httpx.TransportError inherits from HTTPError, so transient
3777
+ # disconnects during streaming land here.
3778
+ if reconnect_path is None:
3779
+ raise
3780
+ retry = True
3634
3781
  else:
3635
- logger.error(f"Error from langgraph-api: {body}", exc_info=e)
3636
- raise e
3637
- # check content type
3638
- content_type = res.headers.get("content-type", "").partition(";")[0]
3639
- if "text/event-stream" not in content_type:
3640
- raise httpx.TransportError(
3641
- "Expected response header Content-Type to contain 'text/event-stream', "
3642
- f"got {content_type!r}"
3643
- )
3644
- # parse SSE
3645
- decoder = SSEDecoder()
3646
- for line in iter_lines_raw(res):
3647
- sse = decoder.decode(line.rstrip(b"\n"))
3648
- if sse is not None:
3649
- yield sse
3782
+ if sse := decoder.decode(b""):
3783
+ if decoder.last_event_id is not None:
3784
+ last_event_id = decoder.last_event_id
3785
+ if sse.event or sse.data is not None:
3786
+ # See async stream implementation for rationale on
3787
+ # skipping empty flush events.
3788
+ yield sse
3789
+ if retry:
3790
+ reconnect_attempts += 1
3791
+ if reconnect_attempts > max_reconnect_attempts:
3792
+ raise httpx.TransportError(
3793
+ "Exceeded maximum SSE reconnection attempts"
3794
+ )
3795
+ continue
3796
+ break
3650
3797
 
3651
3798
 
3652
3799
  def _encode_json(json: Any) -> tuple[dict[str, str], bytes]:
@@ -81,6 +81,12 @@ class SSEDecoder:
81
81
  self._last_event_id = ""
82
82
  self._retry: int | None = None
83
83
 
84
+ @property
85
+ def last_event_id(self) -> str | None:
86
+ """Return the last event identifier that was seen."""
87
+
88
+ return self._last_event_id or None
89
+
84
90
  def decode(self, line: bytes) -> StreamPart | None:
85
91
  # See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation # noqa: E501
86
92