diff --git a/libs/sdk-py/langgraph_sdk/_async/__init__.py b/libs/sdk-py/langgraph_sdk/_async/__init__.py new file mode 100644 index 000000000..16bf0e4ff --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/__init__.py @@ -0,0 +1,20 @@ +"""Async client exports.""" + +from langgraph_sdk._async.assistants import AssistantsClient +from langgraph_sdk._async.client import LangGraphClient, get_client +from langgraph_sdk._async.cron import CronClient +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk._async.runs import RunsClient +from langgraph_sdk._async.store import StoreClient +from langgraph_sdk._async.threads import ThreadsClient + +__all__ = [ + "AssistantsClient", + "CronClient", + "HttpClient", + "LangGraphClient", + "RunsClient", + "StoreClient", + "ThreadsClient", + "get_client", +] diff --git a/libs/sdk-py/langgraph_sdk/_async/assistants.py b/libs/sdk-py/langgraph_sdk/_async/assistants.py new file mode 100644 index 000000000..447535252 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/assistants.py @@ -0,0 +1,722 @@ +"""Async client for managing assistants in LangGraph.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, cast, overload + +import httpx + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk.schema import ( + Assistant, + AssistantSelectField, + AssistantSortBy, + AssistantsSearchResponse, + AssistantVersion, + Config, + Context, + GraphSchema, + Json, + OnConflictBehavior, + QueryParamTypes, + SortOrder, + Subgraphs, +) + + +class AssistantsClient: + """Client for managing assistants in LangGraph. + + This class provides methods to interact with assistants, + which are versioned configurations of your graph. + + ???+ example "Example" + + ```python + client = get_client(url="http://localhost:2024") + assistant = await client.assistants.get("assistant_id_123") + ``` + """ + + def __init__(self, http: HttpClient) -> None: + self.http = http + + async def get( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Get an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Assistant: Assistant Object. + + ???+ example "Example Usage" + + ```python + assistant = await client.assistants.get( + assistant_id="my_assistant_id" + ) + print(assistant) + ``` + + ```shell + ---------------------------------------------------- + + { + 'assistant_id': 'my_assistant_id', + 'graph_id': 'agent', + 'created_at': '2024-06-25T17:10:33.109781+00:00', + 'updated_at': '2024-06-25T17:10:33.109781+00:00', + 'config': {}, + 'metadata': {'created_by': 'system'}, + 'version': 1, + 'name': 'my_assistant' + } + ``` + """ + return await self.http.get( + f"/assistants/{assistant_id}", headers=headers, params=params + ) + + async def get_graph( + self, + assistant_id: str, + *, + xray: int | bool = False, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> dict[str, list[dict[str, Any]]]: + """Get the graph of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the graph of. + xray: Include graph representation of subgraphs. If an integer value is provided, only subgraphs with a depth less than or equal to the value will be included. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Graph: The graph information for the assistant in JSON format. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + graph_info = await client.assistants.get_graph( + assistant_id="my_assistant_id" + ) + print(graph_info) + ``` + + ```shell + + -------------------------------------------------------------------------------------------------------------------------- + + { + 'nodes': + [ + {'id': '__start__', 'type': 'schema', 'data': '__start__'}, + {'id': '__end__', 'type': 'schema', 'data': '__end__'}, + {'id': 'agent','type': 'runnable','data': {'id': ['langgraph', 'utils', 'RunnableCallable'],'name': 'agent'}}, + ], + 'edges': + [ + {'source': '__start__', 'target': 'agent'}, + {'source': 'agent','target': '__end__'} + ] + } + ``` + + + """ + query_params = {"xray": xray} + if params: + query_params.update(params) + + return await self.http.get( + f"/assistants/{assistant_id}/graph", params=query_params, headers=headers + ) + + async def get_schemas( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> GraphSchema: + """Get the schemas of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the schema of. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + GraphSchema: The graph schema for the assistant. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + schema = await client.assistants.get_schemas( + assistant_id="my_assistant_id" + ) + print(schema) + ``` + + ```shell + + ---------------------------------------------------------------------------------------------------------------------------- + + { + 'graph_id': 'agent', + 'state_schema': + { + 'title': 'LangGraphInput', + '$ref': '#/definitions/AgentState', + 'definitions': + { + 'BaseMessage': + { + 'title': 'BaseMessage', + 'description': 'Base abstract Message class. Messages are the inputs and outputs of ChatModels.', + 'type': 'object', + 'properties': + { + 'content': + { + 'title': 'Content', + 'anyOf': [ + {'type': 'string'}, + {'type': 'array','items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}} + ] + }, + 'additional_kwargs': + { + 'title': 'Additional Kwargs', + 'type': 'object' + }, + 'response_metadata': + { + 'title': 'Response Metadata', + 'type': 'object' + }, + 'type': + { + 'title': 'Type', + 'type': 'string' + }, + 'name': + { + 'title': 'Name', + 'type': 'string' + }, + 'id': + { + 'title': 'Id', + 'type': 'string' + } + }, + 'required': ['content', 'type'] + }, + 'AgentState': + { + 'title': 'AgentState', + 'type': 'object', + 'properties': + { + 'messages': + { + 'title': 'Messages', + 'type': 'array', + 'items': {'$ref': '#/definitions/BaseMessage'} + } + }, + 'required': ['messages'] + } + } + }, + 'context_schema': + { + 'title': 'Context', + 'type': 'object', + 'properties': + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } + } + } + ``` + + """ + return await self.http.get( + f"/assistants/{assistant_id}/schemas", headers=headers, params=params + ) + + async def get_subgraphs( + self, + assistant_id: str, + namespace: str | None = None, + recurse: bool = False, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Subgraphs: + """Get the schemas of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the schema of. + namespace: Optional namespace to filter by. + recurse: Whether to recursively get subgraphs. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Subgraphs: The graph schema for the assistant. + + """ + get_params = {"recurse": recurse} + if params: + get_params = {**get_params, **params} + if namespace is not None: + return await self.http.get( + f"/assistants/{assistant_id}/subgraphs/{namespace}", + params=get_params, + headers=headers, + ) + else: + return await self.http.get( + f"/assistants/{assistant_id}/subgraphs", + params=get_params, + headers=headers, + ) + + async def create( + self, + graph_id: str | None, + config: Config | None = None, + *, + context: Context | None = None, + metadata: Json = None, + assistant_id: str | None = None, + if_exists: OnConflictBehavior | None = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + description: str | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Create a new assistant. + + Useful when graph is configurable and you want to create different assistants based on different configurations. + + Args: + graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. + config: Configuration to use for the graph. + metadata: Metadata to add to assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + assistant_id: Assistant ID to use, will default to a random UUID if not provided. + if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. + Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing assistant). + name: The name of the assistant. Defaults to 'Untitled' under the hood. + headers: Optional custom headers to include with the request. + description: Optional description of the assistant. + The description field is available for langgraph-api server version>=0.0.45 + params: Optional query parameters to include with the request. + + Returns: + Assistant: The created assistant. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + assistant = await client.assistants.create( + graph_id="agent", + context={"model_name": "openai"}, + metadata={"number":1}, + assistant_id="my-assistant-id", + if_exists="do_nothing", + name="my_name" + ) + ``` + """ + payload: dict[str, Any] = { + "graph_id": graph_id, + } + if config: + payload["config"] = config + if context: + payload["context"] = context + if metadata: + payload["metadata"] = metadata + if assistant_id: + payload["assistant_id"] = assistant_id + if if_exists: + payload["if_exists"] = if_exists + if name: + payload["name"] = name + if description: + payload["description"] = description + return await self.http.post( + "/assistants", json=payload, headers=headers, params=params + ) + + async def update( + self, + assistant_id: str, + *, + graph_id: str | None = None, + config: Config | None = None, + context: Context | None = None, + metadata: Json = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + description: str | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Update an assistant. + + Use this to point to a different graph, update the configuration, or change the metadata of an assistant. + + Args: + assistant_id: Assistant to update. + graph_id: The ID of the graph the assistant should use. + The graph ID is normally set in your langgraph.json configuration. If `None`, assistant will keep pointing to same graph. + config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + metadata: Metadata to merge with existing assistant metadata. + name: The new name for the assistant. + headers: Optional custom headers to include with the request. + description: Optional description of the assistant. + The description field is available for langgraph-api server version>=0.0.45 + params: Optional query parameters to include with the request. + + Returns: + The updated assistant. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + assistant = await client.assistants.update( + assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', + graph_id="other-graph", + context={"model_name": "anthropic"}, + metadata={"number":2} + ) + ``` + + """ + payload: dict[str, Any] = {} + if graph_id: + payload["graph_id"] = graph_id + if config: + payload["config"] = config + if context: + payload["context"] = context + if metadata: + payload["metadata"] = metadata + if name: + payload["name"] = name + if description: + payload["description"] = description + return await self.http.patch( + f"/assistants/{assistant_id}", + json=payload, + headers=headers, + params=params, + ) + + async def delete( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete an assistant. + + Args: + assistant_id: The assistant ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.assistants.delete( + assistant_id="my_assistant_id" + ) + ``` + + """ + await self.http.delete( + f"/assistants/{assistant_id}", headers=headers, params=params + ) + + @overload + async def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["object"], + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> AssistantsSearchResponse: ... + + @overload + async def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["array"] = "array", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Assistant]: ... + + async def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["array", "object"] = "array", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> AssistantsSearchResponse | list[Assistant]: + """Search for assistants. + + Args: + metadata: Metadata to filter by. Exact match filter for each KV pair. + graph_id: The ID of the graph to filter by. + The graph ID is normally set in your langgraph.json configuration. + name: The name of the assistant to filter by. + The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. + limit: The maximum number of results to return. + offset: The number of results to skip. + sort_by: The field to sort by. + sort_order: The order to sort by. + select: Specific assistant fields to include in the response. + response_format: Controls the response shape. Use `"array"` (default) + to return a bare list of assistants, or `"object"` to return + a mapping containing assistants plus pagination metadata. + Defaults to "array", though this default will be changed to "object" in a future release. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + A list of assistants (when `response_format="array"`) or a mapping + with the assistants and the next pagination cursor (when + `response_format="object"`). + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + response = await client.assistants.search( + metadata = {"name":"my_name"}, + graph_id="my_graph_id", + limit=5, + offset=5, + response_format="object" + ) + next_cursor = response["next"] + assistants = response["assistants"] + ``` + """ + if response_format not in ("array", "object"): + raise ValueError( + f"response_format must be 'array' or 'object', got {response_format!r}" + ) + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + if graph_id: + payload["graph_id"] = graph_id + if name: + payload["name"] = name + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + next_cursor: str | None = None + + def capture_pagination(response: httpx.Response) -> None: + nonlocal next_cursor + next_cursor = response.headers.get("X-Pagination-Next") + + assistants = cast( + list[Assistant], + await self.http.post( + "/assistants/search", + json=payload, + headers=headers, + params=params, + on_response=capture_pagination if response_format == "object" else None, + ), + ) + if response_format == "object": + return {"assistants": assistants, "next": next_cursor} + return assistants + + async def count( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count assistants matching filters. + + Args: + metadata: Metadata to filter by. Exact match for each key/value. + graph_id: Optional graph id to filter by. + name: Optional name to filter by. + The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of assistants matching the criteria. + """ + payload: dict[str, Any] = {} + if metadata: + payload["metadata"] = metadata + if graph_id: + payload["graph_id"] = graph_id + if name: + payload["name"] = name + return await self.http.post( + "/assistants/count", json=payload, headers=headers, params=params + ) + + async def get_versions( + self, + assistant_id: str, + metadata: Json = None, + limit: int = 10, + offset: int = 0, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[AssistantVersion]: + """List all versions of an assistant. + + Args: + assistant_id: The assistant ID to get versions for. + metadata: Metadata to filter versions by. Exact match filter for each KV pair. + limit: The maximum number of versions to return. + offset: The number of versions to skip. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + A list of assistant versions. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + assistant_versions = await client.assistants.get_versions( + assistant_id="my_assistant_id" + ) + ``` + """ + + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + return await self.http.post( + f"/assistants/{assistant_id}/versions", + json=payload, + headers=headers, + params=params, + ) + + async def set_latest( + self, + assistant_id: str, + version: int, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Change the version of an assistant. + + Args: + assistant_id: The assistant ID to delete. + version: The version to change to. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Assistant Object. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + new_version_assistant = await client.assistants.set_latest( + assistant_id="my_assistant_id", + version=3 + ) + ``` + + """ + + payload: dict[str, Any] = {"version": version} + + return await self.http.post( + f"/assistants/{assistant_id}/latest", + json=payload, + headers=headers, + params=params, + ) diff --git a/libs/sdk-py/langgraph_sdk/_async/client.py b/libs/sdk-py/langgraph_sdk/_async/client.py new file mode 100644 index 000000000..5d7e494da --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/client.py @@ -0,0 +1,178 @@ +"""Async LangGraph client.""" + +from __future__ import annotations + +import logging +import os +from collections.abc import Mapping +from types import TracebackType + +import httpx + +from langgraph_sdk._async.assistants import AssistantsClient +from langgraph_sdk._async.cron import CronClient +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk._async.runs import RunsClient +from langgraph_sdk._async.store import StoreClient +from langgraph_sdk._async.threads import ThreadsClient +from langgraph_sdk._shared.types import TimeoutTypes +from langgraph_sdk._shared.utilities import ( + NOT_PROVIDED, + _get_headers, + _registered_transports, + get_asgi_transport, +) + +logger = logging.getLogger(__name__) + + +def get_client( + *, + url: str | None = None, + api_key: str | None = NOT_PROVIDED, + headers: Mapping[str, str] | None = None, + timeout: TimeoutTypes | None = None, +) -> LangGraphClient: + """Create and configure a LangGraphClient. + + The client provides programmatic access to LangSmith Deployment. It supports + both remote servers and local in-process connections (when running inside a LangGraph server). + + Args: + url: + Base URL of the LangGraph API. + - If `None`, the client first attempts an in-process connection via ASGI transport. + If that fails, it defers registration until after app initialization. This + only works if the client is used from within the Agent server. + api_key: + API key for authentication. Can be: + - A string: use this exact API key + - `None`: explicitly skip loading from environment variables + - Not provided (default): auto-load from environment in this order: + 1. `LANGGRAPH_API_KEY` + 2. `LANGSMITH_API_KEY` + 3. `LANGCHAIN_API_KEY` + headers: + Additional HTTP headers to include in requests. Merged with authentication headers. + timeout: + HTTP timeout configuration. May be: + - `httpx.Timeout` instance + - float (total seconds) + - tuple `(connect, read, write, pool)` in seconds + Defaults: connect=5, read=300, write=300, pool=5. + + Returns: + LangGraphClient: + A top-level client exposing sub-clients for assistants, threads, + runs, and cron operations. + + ???+ example "Connect to a remote server:" + + ```python + from langgraph_sdk import get_client + + # get top-level LangGraphClient + client = get_client(url="http://localhost:8123") + + # example usage: client..() + assistants = await client.assistants.get(assistant_id="some_uuid") + ``` + + ???+ example "Connect in-process to a running LangGraph server:" + + ```python + from langgraph_sdk import get_client + + client = get_client(url=None) + + async def my_node(...): + subagent_result = await client.runs.wait( + thread_id=None, + assistant_id="agent", + input={"messages": [{"role": "user", "content": "Foo"}]}, + ) + ``` + + ???+ example "Skip auto-loading API key from environment:" + + ```python + from langgraph_sdk import get_client + + # Don't load API key from environment variables + client = get_client( + url="http://localhost:8123", + api_key=None + ) + ``` + """ + + transport: httpx.AsyncBaseTransport | None = None + if url is None: + url = "http://api" + if os.environ.get("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT") == "true": + transport = get_asgi_transport()(app=None, root_path="/noauth") + _registered_transports.append(transport) + else: + try: + from langgraph_api.server import app # type: ignore + + transport = get_asgi_transport()(app, root_path="/noauth") + except Exception: + logger.debug( + "Failed to connect to in-process LangGraph server. Deferring configuration.", + exc_info=True, + ) + transport = get_asgi_transport()(app=None, root_path="/noauth") + _registered_transports.append(transport) + + if transport is None: + transport = httpx.AsyncHTTPTransport(retries=5) + client = httpx.AsyncClient( + base_url=url, + transport=transport, + timeout=( + httpx.Timeout(timeout) # type: ignore[arg-type] + if timeout is not None + else httpx.Timeout(connect=5, read=300, write=300, pool=5) + ), + headers=_get_headers(api_key, headers), + ) + return LangGraphClient(client) + + +class LangGraphClient: + """Top-level client for LangGraph API. + + Attributes: + assistants: Manages versioned configuration for your graphs. + threads: Handles (potentially) multi-turn interactions, such as conversational threads. + runs: Controls individual invocations of the graph. + crons: Manages scheduled operations. + store: Interfaces with persistent, shared data storage. + """ + + def __init__(self, client: httpx.AsyncClient) -> None: + self.http = HttpClient(client) + self.assistants = AssistantsClient(self.http) + self.threads = ThreadsClient(self.http) + self.runs = RunsClient(self.http) + self.crons = CronClient(self.http) + self.store = StoreClient(self.http) + + async def __aenter__(self) -> LangGraphClient: + """Enter the async context manager.""" + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the async context manager.""" + await self.aclose() + + async def aclose(self) -> None: + """Close the underlying HTTP client.""" + if hasattr(self, "http"): + await self.http.client.aclose() diff --git a/libs/sdk-py/langgraph_sdk/_async/cron.py b/libs/sdk-py/langgraph_sdk/_async/cron.py new file mode 100644 index 000000000..808980366 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/cron.py @@ -0,0 +1,452 @@ +"""Async client for managing recurrent runs (cron jobs) in LangGraph.""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import datetime +from typing import Any + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk.schema import ( + All, + Config, + Context, + Cron, + CronSelectField, + CronSortBy, + Input, + OnCompletionBehavior, + QueryParamTypes, + Run, + SortOrder, +) + + +class CronClient: + """Client for managing recurrent runs (cron jobs) in LangGraph. + + A run is a single invocation of an assistant with optional input, config, and context. + This client allows scheduling recurring runs to occur automatically. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024")) + cron_job = await client.crons.create_for_thread( + thread_id="thread_123", + assistant_id="asst_456", + schedule="0 9 * * *", + input={"message": "Daily update"} + ) + ``` + + !!! note "Feature Availability" + + The crons client functionality is not supported on all licenses. + Please check the relevant license documentation for the most up-to-date + details on feature availability. + """ + + def __init__(self, http_client: HttpClient) -> None: + self.http = http_client + + async def create_for_thread( + self, + thread_id: str, + assistant_id: str, + *, + schedule: str, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + webhook: str | None = None, + multitask_strategy: str | None = None, + end_time: datetime | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Create a cron job for a thread. + + Args: + thread_id: the thread ID to run the cron job on. + assistant_id: The assistant ID or graph name to use for the cron job. + If using graph name, will default to first assistant created from that graph. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. + enabled: Whether the cron job is enabled or not. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The cron run. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + cron_run = await client.crons.create_for_thread( + thread_id="my-thread-id", + assistant_id="agent", + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt", + enabled=True, + ) + ``` + """ + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "context": context, + "assistant_id": assistant_id, + "checkpoint_during": checkpoint_during, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "end_time": end_time.isoformat() if end_time else None, + "enabled": enabled, + } + if multitask_strategy: + payload["multitask_strategy"] = multitask_strategy + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.post( + f"/threads/{thread_id}/runs/crons", + json=payload, + headers=headers, + params=params, + ) + + async def create( + self, + assistant_id: str, + *, + schedule: str, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + webhook: str | None = None, + on_run_completed: OnCompletionBehavior | None = None, + multitask_strategy: str | None = None, + end_time: datetime | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Create a cron run. + + Args: + assistant_id: The assistant ID or graph name to use for the cron job. + If using graph name, will default to first assistant created from that graph. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + on_run_completed: What to do with the thread after the run completes. + Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread + after execution. 'keep' creates a new thread for each execution but does not + clean them up. Clients are responsible for cleaning up kept threads. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. + enabled: Whether the cron job is enabled or not. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The cron run. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + cron_run = client.crons.create( + assistant_id="agent", + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt", + enabled=True, + ) + ``` + + """ + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "context": context, + "assistant_id": assistant_id, + "checkpoint_during": checkpoint_during, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "on_run_completed": on_run_completed, + "end_time": end_time.isoformat() if end_time else None, + "enabled": enabled, + } + if multitask_strategy: + payload["multitask_strategy"] = multitask_strategy + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.post( + "/runs/crons", json=payload, headers=headers, params=params + ) + + async def delete( + self, + cron_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a cron. + + Args: + cron_id: The cron ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.crons.delete( + cron_id="cron_to_delete" + ) + ``` + + """ + await self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params) + + async def update( + self, + cron_id: str, + *, + schedule: str | None = None, + end_time: datetime | None = None, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + webhook: str | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + on_run_completed: OnCompletionBehavior | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Cron: + """Update a cron job by ID. + + Args: + cron_id: The cron ID to update. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + end_time: The end date to stop running the cron. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context added to the assistant. + webhook: Webhook to call after LangGraph API call is done. + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to interrupt immediately after they get executed. + on_run_completed: What to do with the thread after the run completes. + Must be one of 'delete' or 'keep'. 'delete' removes the thread + after execution. 'keep' creates a new thread for each execution but does not + clean them up. + enabled: Enable or disable the cron job. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The updated cron job. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + updated_cron = await client.crons.update( + cron_id="1ef3cefa-4c09-6926-96d0-3dc97fd5e39b", + schedule="0 10 * * *", + enabled=False, + ) + ``` + + """ + payload = { + "schedule": schedule, + "end_time": end_time.isoformat() if end_time else None, + "input": input, + "metadata": metadata, + "config": config, + "context": context, + "webhook": webhook, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "on_run_completed": on_run_completed, + "enabled": enabled, + } + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.patch( + f"/runs/crons/{cron_id}", + json=payload, + headers=headers, + params=params, + ) + + async def search( + self, + *, + assistant_id: str | None = None, + thread_id: str | None = None, + enabled: bool | None = None, + limit: int = 10, + offset: int = 0, + sort_by: CronSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[CronSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Cron]: + """Get a list of cron jobs. + + Args: + assistant_id: The assistant ID or graph name to search for. + thread_id: the thread ID to search for. + enabled: The enabled status to search for. + limit: The maximum number of results to return. + offset: The number of results to skip. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The list of cron jobs returned by the search, + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + cron_jobs = await client.crons.search( + assistant_id="my_assistant_id", + thread_id="my_thread_id", + enabled=True, + limit=5, + offset=5, + ) + print(cron_jobs) + ``` + ```shell + + ---------------------------------------------------------- + + [ + { + 'cron_id': '1ef3cefa-4c09-6926-96d0-3dc97fd5e39b', + 'assistant_id': 'my_assistant_id', + 'thread_id': 'my_thread_id', + 'user_id': None, + 'payload': + { + 'input': {'start_time': ''}, + 'schedule': '4 * * * *', + 'assistant_id': 'my_assistant_id' + }, + 'schedule': '4 * * * *', + 'next_run_date': '2024-07-25T17:04:00+00:00', + 'end_time': None, + 'created_at': '2024-07-08T06:02:23.073257+00:00', + 'updated_at': '2024-07-08T06:02:23.073257+00:00' + } + ] + ``` + + """ + payload = { + "assistant_id": assistant_id, + "thread_id": thread_id, + "enabled": enabled, + "limit": limit, + "offset": offset, + } + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + payload = {k: v for k, v in payload.items() if v is not None} + return await self.http.post( + "/runs/crons/search", json=payload, headers=headers, params=params + ) + + async def count( + self, + *, + assistant_id: str | None = None, + thread_id: str | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count cron jobs matching filters. + + Args: + assistant_id: Assistant ID to filter by. + thread_id: Thread ID to filter by. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of crons matching the criteria. + """ + payload: dict[str, Any] = {} + if assistant_id: + payload["assistant_id"] = assistant_id + if thread_id: + payload["thread_id"] = thread_id + return await self.http.post( + "/runs/crons/count", json=payload, headers=headers, params=params + ) diff --git a/libs/sdk-py/langgraph_sdk/_async/http.py b/libs/sdk-py/langgraph_sdk/_async/http.py new file mode 100644 index 000000000..48e224be0 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/http.py @@ -0,0 +1,305 @@ +"""HTTP client for async operations.""" + +from __future__ import annotations + +import asyncio +import logging +import sys +import warnings +from collections.abc import AsyncIterator, Callable, Mapping +from typing import Any, cast + +import httpx +import orjson + +from langgraph_sdk._shared.utilities import _orjson_default +from langgraph_sdk.errors import _araise_for_status_typed +from langgraph_sdk.schema import QueryParamTypes, StreamPart +from langgraph_sdk.sse import SSEDecoder, aiter_lines_raw + +logger = logging.getLogger(__name__) + + +class HttpClient: + """Handle async requests to the LangGraph API. + + Adds additional error messaging & content handling above the + provided httpx client. + + Attributes: + client (httpx.AsyncClient): Underlying HTTPX async client. + """ + + def __init__(self, client: httpx.AsyncClient) -> None: + self.client = client + + async def get( + self, + path: str, + *, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `GET` request.""" + r = await self.client.get(path, params=params, headers=headers) + if on_response: + on_response(r) + await _araise_for_status_typed(r) + return await _adecode_json(r) + + async def post( + self, + path: str, + *, + json: dict[str, Any] | list | None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `POST` request.""" + if json is not None: + request_headers, content = await _aencode_json(json) + else: + request_headers, content = {}, b"" + # Merge headers, with runtime headers taking precedence + if headers: + request_headers.update(headers) + r = await self.client.post( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + await _araise_for_status_typed(r) + return await _adecode_json(r) + + async def put( + self, + path: str, + *, + json: dict, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `PUT` request.""" + request_headers, content = await _aencode_json(json) + if headers: + request_headers.update(headers) + r = await self.client.put( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + await _araise_for_status_typed(r) + return await _adecode_json(r) + + async def patch( + self, + path: str, + *, + json: dict, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `PATCH` request.""" + request_headers, content = await _aencode_json(json) + if headers: + request_headers.update(headers) + r = await self.client.patch( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + await _araise_for_status_typed(r) + return await _adecode_json(r) + + async def delete( + self, + path: str, + *, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> None: + """Send a `DELETE` request.""" + r = await self.client.request( + "DELETE", path, json=json, params=params, headers=headers + ) + if on_response: + on_response(r) + await _araise_for_status_typed(r) + + async def request_reconnect( + self, + path: str, + method: str, + *, + json: dict[str, Any] | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + reconnect_limit: int = 5, + ) -> Any: + """Send a request that automatically reconnects to Location header.""" + request_headers, content = await _aencode_json(json) + if headers: + request_headers.update(headers) + async with self.client.stream( + method, path, headers=request_headers, content=content, params=params + ) as r: + if on_response: + on_response(r) + try: + r.raise_for_status() + except httpx.HTTPStatusError as e: + body = (await r.aread()).decode() + if sys.version_info >= (3, 11): + e.add_note(body) + else: + logger.error(f"Error from langgraph-api: {body}", exc_info=e) + raise e + loc = r.headers.get("location") + if reconnect_limit <= 0 or not loc: + return await _adecode_json(r) + try: + return await _adecode_json(r) + except httpx.HTTPError: + warnings.warn( + f"Request failed, attempting reconnect to Location: {loc}", + stacklevel=2, + ) + await r.aclose() + return await self.request_reconnect( + loc, + "GET", + headers=request_headers, + # don't pass on_response so it's only called once + reconnect_limit=reconnect_limit - 1, + ) + + async def stream( + self, + path: str, + method: str, + *, + json: dict[str, Any] | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> AsyncIterator[StreamPart]: + """Stream results using SSE.""" + request_headers, content = await _aencode_json(json) + request_headers["Accept"] = "text/event-stream" + request_headers["Cache-Control"] = "no-store" + # Add runtime headers with precedence + if headers: + request_headers.update(headers) + + reconnect_headers = { + key: value + for key, value in request_headers.items() + if key.lower() not in {"content-length", "content-type"} + } + + last_event_id: str | None = None + reconnect_path: str | None = None + reconnect_attempts = 0 + max_reconnect_attempts = 5 + + while True: + current_headers = dict( + request_headers if reconnect_path is None else reconnect_headers + ) + if last_event_id is not None: + current_headers["Last-Event-ID"] = last_event_id + + current_method = method if reconnect_path is None else "GET" + current_content = content if reconnect_path is None else None + current_params = params if reconnect_path is None else None + + retry = False + async with self.client.stream( + current_method, + reconnect_path or path, + headers=current_headers, + content=current_content, + params=current_params, + ) as res: + if reconnect_path is None and on_response: + on_response(res) + # check status + await _araise_for_status_typed(res) + # check content type + content_type = res.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise httpx.TransportError( + "Expected response header Content-Type to contain 'text/event-stream', " + f"got {content_type!r}" + ) + + reconnect_location = res.headers.get("location") + if reconnect_location: + reconnect_path = reconnect_location + + # parse SSE + decoder = SSEDecoder() + try: + async for line in aiter_lines_raw(res): + sse = decoder.decode(line=cast("bytes", line).rstrip(b"\n")) + if sse is not None: + if decoder.last_event_id is not None: + last_event_id = decoder.last_event_id + if sse.event or sse.data is not None: + yield sse + except httpx.HTTPError: + # httpx.TransportError inherits from HTTPError, so transient + # disconnects during streaming land here. + if reconnect_path is None: + raise + retry = True + else: + if sse := decoder.decode(b""): + if decoder.last_event_id is not None: + last_event_id = decoder.last_event_id + if sse.event or sse.data is not None: + # decoder.decode(b"") flushes the in-flight event and may + # return an empty placeholder when there is no pending + # message. Skip these no-op events so the stream doesn't + # emit a trailing blank item after reconnects. + yield sse + if retry: + reconnect_attempts += 1 + if reconnect_attempts > max_reconnect_attempts: + raise httpx.TransportError( + "Exceeded maximum SSE reconnection attempts" + ) + continue + break + + +async def _aencode_json(json: Any) -> tuple[dict[str, str], bytes | None]: + if json is None: + return {}, None + body = await asyncio.get_running_loop().run_in_executor( + None, + orjson.dumps, + json, + _orjson_default, + orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, + ) + content_length = str(len(body)) + content_type = "application/json" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, body + + +async def _adecode_json(r: httpx.Response) -> Any: + body = await r.aread() + return ( + await asyncio.get_running_loop().run_in_executor(None, orjson.loads, body) + if body + else None + ) diff --git a/libs/sdk-py/langgraph_sdk/_async/runs.py b/libs/sdk-py/langgraph_sdk/_async/runs.py new file mode 100644 index 000000000..1b02c2555 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/runs.py @@ -0,0 +1,1017 @@ +"""Async client for managing runs in LangGraph.""" + +from __future__ import annotations + +import warnings +from collections.abc import AsyncIterator, Callable, Mapping, Sequence +from typing import Any, overload + +import httpx + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk._shared.utilities import _get_run_metadata_from_response +from langgraph_sdk.schema import ( + All, + CancelAction, + Checkpoint, + Command, + Config, + Context, + DisconnectMode, + Durability, + IfNotExists, + Input, + MultitaskStrategy, + OnCompletionBehavior, + QueryParamTypes, + Run, + RunCreate, + RunCreateMetadata, + RunSelectField, + RunStatus, + StreamMode, + StreamPart, +) + + +class RunsClient: + """Client for managing runs in LangGraph. + + A run is a single assistant invocation with optional input, config, context, and metadata. + This client manages runs, which can be stateful (on threads) or stateless. + + ???+ example "Example" + + ```python + client = get_client(url="http://localhost:2024") + run = await client.runs.create(assistant_id="asst_123", thread_id="thread_456", input={"query": "Hello"}) + ``` + """ + + def __init__(self, http: HttpClient) -> None: + self.http = http + + @overload + def stream( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> AsyncIterator[StreamPart]: ... + + @overload + def stream( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + webhook: str | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> AsyncIterator[StreamPart]: ... + + def stream( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, # deprecated + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> AsyncIterator[StreamPart]: + """Create a run and stream the results. + + Args: + thread_id: the thread ID to assign to the thread. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to stream from. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: A command to execute. Cannot be combined with input. + stream_mode: The stream mode(s) to use. + stream_subgraphs: Whether to stream output from subgraphs. + stream_resumable: Whether the stream is considered resumable. + If true, the stream can be resumed and replayed in its entirety even after disconnection. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + feedback_keys: Feedback keys to assign to run. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + on_run_created: Callback when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + Returns: + Asynchronous iterator of stream results. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024) + async for chunk in client.runs.stream( + thread_id=None, + assistant_id="agent", + input={"messages": [{"role": "user", "content": "how are you?"}]}, + stream_mode=["values","debug"], + metadata={"name":"my_run"}, + context={"model_name": "anthropic"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + feedback_keys=["my_feedback_key_1","my_feedback_key_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ): + print(chunk) + ``` + + ```shell + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + + StreamPart(event='metadata', data={'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2'}) + StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}]}) + StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}, {'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}) + StreamPart(event='end', data=None) + ``` + + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "config": config, + "context": context, + "metadata": metadata, + "stream_mode": stream_mode, + "stream_subgraphs": stream_subgraphs, + "stream_resumable": stream_resumable, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "feedback_keys": feedback_keys, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "checkpoint_during": checkpoint_during, + "multitask_strategy": multitask_strategy, + "if_not_exists": if_not_exists, + "on_disconnect": on_disconnect, + "on_completion": on_completion, + "after_seconds": after_seconds, + "durability": durability, + } + endpoint = ( + f"/threads/{thread_id}/runs/stream" + if thread_id is not None + else "/runs/stream" + ) + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + return self.http.stream( + endpoint, + "POST", + json={k: v for k, v in payload.items() if v is not None}, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + + @overload + async def create( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + checkpoint_during: bool | None = None, + config: Config | None = None, + context: Context | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Run: ... + + @overload + async def create( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Run: ... + + async def create( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, # deprecated + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + on_completion: OnCompletionBehavior | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> Run: + """Create a background run. + + Args: + thread_id: the thread ID to assign to the thread. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to stream from. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: A command to execute. Cannot be combined with input. + stream_mode: The stream mode(s) to use. + stream_subgraphs: Whether to stream output from subgraphs. + stream_resumable: Whether the stream is considered resumable. + If true, the stream can be resumed and replayed in its entirety even after disconnection. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + headers: Optional custom headers to include with the request. + on_run_created: Optional callback to call when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + Returns: + The created background run. + + ???+ example "Example Usage" + + ```python + + background_run = await client.runs.create( + thread_id="my_thread_id", + assistant_id="my_assistant_id", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ) + print(background_run) + ``` + + ```shell + -------------------------------------------------------------------------------- + + { + 'run_id': 'my_run_id', + 'thread_id': 'my_thread_id', + 'assistant_id': 'my_assistant_id', + 'created_at': '2024-07-25T15:35:42.598503+00:00', + 'updated_at': '2024-07-25T15:35:42.598503+00:00', + 'metadata': {}, + 'status': 'pending', + 'kwargs': + { + 'input': + { + 'messages': [ + { + 'role': 'user', + 'content': 'how are you?' + } + ] + }, + 'config': + { + 'metadata': + { + 'created_by': 'system' + }, + 'configurable': + { + 'run_id': 'my_run_id', + 'user_id': None, + 'graph_id': 'agent', + 'thread_id': 'my_thread_id', + 'checkpoint_id': None, + 'assistant_id': 'my_assistant_id' + }, + }, + 'context': + { + 'model_name': 'openai' + } + 'webhook': "https://my.fake.webhook.com", + 'temporary': False, + 'stream_mode': ['values'], + 'feedback_keys': None, + 'interrupt_after': ["node_to_stop_after_1","node_to_stop_after_2"], + 'interrupt_before': ["node_to_stop_before_1","node_to_stop_before_2"] + }, + 'multitask_strategy': 'interrupt' + } + ``` + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "stream_mode": stream_mode, + "stream_subgraphs": stream_subgraphs, + "stream_resumable": stream_resumable, + "config": config, + "context": context, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "checkpoint_during": checkpoint_during, + "multitask_strategy": multitask_strategy, + "if_not_exists": if_not_exists, + "on_completion": on_completion, + "after_seconds": after_seconds, + "durability": durability, + } + payload = {k: v for k, v in payload.items() if v is not None} + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + return await self.http.post( + f"/threads/{thread_id}/runs" if thread_id else "/runs", + json=payload, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + + async def create_batch( + self, + payloads: list[RunCreate], + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Run]: + """Create a batch of stateless background runs.""" + + def filter_payload(payload: RunCreate): + return {k: v for k, v in payload.items() if v is not None} + + filtered = [filter_payload(payload) for payload in payloads] + return await self.http.post( + "/runs/batch", json=filtered, headers=headers, params=params + ) + + @overload + async def wait( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> list[dict] | dict[str, Any]: ... + + @overload + async def wait( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> list[dict] | dict[str, Any]: ... + + async def wait( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, # deprecated + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> list[dict] | dict[str, Any]: + """Create a run, wait until it finishes and return the final state. + + Args: + thread_id: the thread ID to create the run on. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to run. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: A command to execute. Cannot be combined with input. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + headers: Optional custom headers to include with the request. + on_run_created: Optional callback to call when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + Returns: + The output of the run. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + final_state_of_run = await client.runs.wait( + thread_id=None, + assistant_id="agent", + input={"messages": [{"role": "user", "content": "how are you?"}]}, + metadata={"name":"my_run"}, + context={"model_name": "anthropic"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ) + print(final_state_of_run) + ``` + + ```shell + ------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'messages': [ + { + 'content': 'how are you?', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': 'f51a862c-62fe-4866-863b-b0863e8ad78a', + 'example': False + }, + { + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-bf1cd3c6-768f-4c16-b62d-ba6f17ad8b36', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + } + ``` + + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "config": config, + "context": context, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "multitask_strategy": multitask_strategy, + "checkpoint_during": checkpoint_during, + "if_not_exists": if_not_exists, + "on_disconnect": on_disconnect, + "on_completion": on_completion, + "after_seconds": after_seconds, + "durability": durability, + } + endpoint = ( + f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" + ) + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + response = await self.http.request_reconnect( + endpoint, + "POST", + json={k: v for k, v in payload.items() if v is not None}, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + if ( + raise_error + and isinstance(response, dict) + and "__error__" in response + and isinstance(response["__error__"], dict) + ): + raise Exception( + f"{response['__error__'].get('error')}: {response['__error__'].get('message')}" + ) + return response + + async def list( + self, + thread_id: str, + *, + limit: int = 10, + offset: int = 0, + status: RunStatus | None = None, + select: list[RunSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Run]: + """List runs. + + Args: + thread_id: The thread ID to list runs for. + limit: The maximum number of results to return. + offset: The number of results to skip. + status: The status of the run to filter by. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The runs for the thread. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.runs.list( + thread_id="thread_id", + limit=5, + offset=5, + ) + ``` + + """ + query_params: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if status is not None: + query_params["status"] = status + if select: + query_params["select"] = select + if params: + query_params.update(params) + return await self.http.get( + f"/threads/{thread_id}/runs", params=query_params, headers=headers + ) + + async def get( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Get a run. + + Args: + thread_id: The thread ID to get. + run_id: The run ID to get. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `Run` object. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + run = await client.runs.get( + thread_id="thread_id_to_delete", + run_id="run_id_to_delete", + ) + ``` + + """ + + return await self.http.get( + f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params + ) + + async def cancel( + self, + thread_id: str, + run_id: str, + *, + wait: bool = False, + action: CancelAction = "interrupt", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Get a run. + + Args: + thread_id: The thread ID to cancel. + run_id: The run ID to cancel. + wait: Whether to wait until run has completed. + action: Action to take when cancelling the run. Possible values + are `interrupt` or `rollback`. Default is `interrupt`. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.runs.cancel( + thread_id="thread_id_to_cancel", + run_id="run_id_to_cancel", + wait=True, + action="interrupt" + ) + ``` + + """ + query_params = { + "wait": 1 if wait else 0, + "action": action, + } + if params: + query_params.update(params) + if wait: + return await self.http.request_reconnect( + f"/threads/{thread_id}/runs/{run_id}/cancel", + "POST", + params=query_params, + headers=headers, + ) + else: + return await self.http.post( + f"/threads/{thread_id}/runs/{run_id}/cancel", + json=None, + params=query_params, + headers=headers, + ) + + async def join( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> dict: + """Block until a run is done. Returns the final state of the thread. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + result =await client.runs.join( + thread_id="thread_id_to_join", + run_id="run_id_to_join" + ) + ``` + + """ + return await self.http.request_reconnect( + f"/threads/{thread_id}/runs/{run_id}/join", + "GET", + headers=headers, + params=params, + ) + + def join_stream( + self, + thread_id: str, + run_id: str, + *, + cancel_on_disconnect: bool = False, + stream_mode: StreamMode | Sequence[StreamMode] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + last_event_id: str | None = None, + ) -> AsyncIterator[StreamPart]: + """Stream output from a run in real-time, until the run is done. + Output is not buffered, so any output produced before this call will + not be received here. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + cancel_on_disconnect: Whether to cancel the run when the stream is disconnected. + stream_mode: The stream mode(s) to use. Must be a subset of the stream modes passed + when creating the run. Background runs default to having the union of all + stream modes. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + last_event_id: The last event ID to use for the stream. + + Returns: + The stream of parts. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + async for part in client.runs.join_stream( + thread_id="thread_id_to_join", + run_id="run_id_to_join", + stream_mode=["values", "debug"] + ): + print(part) + ``` + + """ + query_params = { + "cancel_on_disconnect": cancel_on_disconnect, + "stream_mode": stream_mode, + } + if params: + query_params.update(params) + return self.http.stream( + f"/threads/{thread_id}/runs/{run_id}/stream", + "GET", + params=query_params, + headers={ + **({"Last-Event-ID": last_event_id} if last_event_id else {}), + **(headers or {}), + } + or None, + ) + + async def delete( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a run. + + Args: + thread_id: The thread ID to delete. + run_id: The run ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.runs.delete( + thread_id="thread_id_to_delete", + run_id="run_id_to_delete" + ) + ``` + + """ + await self.http.delete( + f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params + ) diff --git a/libs/sdk-py/langgraph_sdk/_async/store.py b/libs/sdk-py/langgraph_sdk/_async/store.py new file mode 100644 index 000000000..e24967047 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/store.py @@ -0,0 +1,313 @@ +"""Async Store client for LangGraph SDK.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any, Literal + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk._shared.utilities import _provided_vals +from langgraph_sdk.schema import ( + Item, + ListNamespaceResponse, + QueryParamTypes, + SearchItemsResponse, +) + + +class StoreClient: + """Client for interacting with the graph's shared storage. + + The Store provides a key-value storage system for persisting data across graph executions, + allowing for stateful operations and data sharing across threads. + + ???+ example "Example" + + ```python + client = get_client(url="http://localhost:2024") + await client.store.put_item(["users", "user123"], "mem-123451342", {"name": "Alice", "score": 100}) + ``` + """ + + def __init__(self, http: HttpClient) -> None: + self.http = http + + async def put_item( + self, + namespace: Sequence[str], + /, + key: str, + value: Mapping[str, Any], + index: Literal[False] | list[str] | None = None, + ttl: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Store or update an item. + + Args: + namespace: A list of strings representing the namespace path. + key: The unique identifier for the item within the namespace. + value: A dictionary containing the item's data. + index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index. + ttl: Optional time-to-live in minutes for the item, or None for no expiration. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.store.put_item( + ["documents", "user123"], + key="item456", + value={"title": "My Document", "content": "Hello World"} + ) + ``` + """ + for label in namespace: + if "." in label: + raise ValueError( + f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." + ) + payload = { + "namespace": namespace, + "key": key, + "value": value, + "index": index, + "ttl": ttl, + } + await self.http.put( + "/store/items", json=_provided_vals(payload), headers=headers, params=params + ) + + async def get_item( + self, + namespace: Sequence[str], + /, + key: str, + *, + refresh_ttl: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Item: + """Retrieve a single item. + + Args: + key: The unique identifier for the item. + namespace: Optional list of strings representing the namespace path. + refresh_ttl: Whether to refresh the TTL on this read operation. If `None`, uses the store's default behavior. + + Returns: + Item: The retrieved item. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + item = await client.store.get_item( + ["documents", "user123"], + key="item456", + ) + print(item) + ``` + ```shell + + ---------------------------------------------------------------- + + { + 'namespace': ['documents', 'user123'], + 'key': 'item456', + 'value': {'title': 'My Document', 'content': 'Hello World'}, + 'created_at': '2024-07-30T12:00:00Z', + 'updated_at': '2024-07-30T12:00:00Z' + } + ``` + """ + for label in namespace: + if "." in label: + raise ValueError( + f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." + ) + get_params = {"namespace": ".".join(namespace), "key": key} + if refresh_ttl is not None: + get_params["refresh_ttl"] = refresh_ttl + if params: + get_params = {**get_params, **params} + return await self.http.get("/store/items", params=get_params, headers=headers) + + async def delete_item( + self, + namespace: Sequence[str], + /, + key: str, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete an item. + + Args: + key: The unique identifier for the item. + namespace: Optional list of strings representing the namespace path. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + await client.store.delete_item( + ["documents", "user123"], + key="item456", + ) + ``` + """ + await self.http.delete( + "/store/items", + json={"namespace": namespace, "key": key}, + headers=headers, + params=params, + ) + + async def search_items( + self, + namespace_prefix: Sequence[str], + /, + filter: Mapping[str, Any] | None = None, + limit: int = 10, + offset: int = 0, + query: str | None = None, + refresh_ttl: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> SearchItemsResponse: + """Search for items within a namespace prefix. + + Args: + namespace_prefix: List of strings representing the namespace prefix. + filter: Optional dictionary of key-value pairs to filter results. + limit: Maximum number of items to return (default is 10). + offset: Number of items to skip before returning results (default is 0). + query: Optional query for natural language search. + refresh_ttl: Whether to refresh the TTL on items returned by this search. If `None`, uses the store's default behavior. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + A list of items matching the search criteria. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + items = await client.store.search_items( + ["documents"], + filter={"author": "John Doe"}, + limit=5, + offset=0 + ) + print(items) + ``` + ```shell + + ---------------------------------------------------------------- + + { + "items": [ + { + "namespace": ["documents", "user123"], + "key": "item789", + "value": { + "title": "Another Document", + "author": "John Doe" + }, + "created_at": "2024-07-30T12:00:00Z", + "updated_at": "2024-07-30T12:00:00Z" + }, + # ... additional items ... + ] + } + ``` + """ + payload = { + "namespace_prefix": namespace_prefix, + "filter": filter, + "limit": limit, + "offset": offset, + "query": query, + "refresh_ttl": refresh_ttl, + } + + return await self.http.post( + "/store/items/search", + json=_provided_vals(payload), + headers=headers, + params=params, + ) + + async def list_namespaces( + self, + prefix: list[str] | None = None, + suffix: list[str] | None = None, + max_depth: int | None = None, + limit: int = 100, + offset: int = 0, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ListNamespaceResponse: + """List namespaces with optional match conditions. + + Args: + prefix: Optional list of strings representing the prefix to filter namespaces. + suffix: Optional list of strings representing the suffix to filter namespaces. + max_depth: Optional integer specifying the maximum depth of namespaces to return. + limit: Maximum number of namespaces to return (default is 100). + offset: Number of namespaces to skip before returning results (default is 0). + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + A list of namespaces matching the criteria. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + namespaces = await client.store.list_namespaces( + prefix=["documents"], + max_depth=3, + limit=10, + offset=0 + ) + print(namespaces) + + ---------------------------------------------------------------- + + [ + ["documents", "user123", "reports"], + ["documents", "user456", "invoices"], + ... + ] + ``` + """ + payload = { + "prefix": prefix, + "suffix": suffix, + "max_depth": max_depth, + "limit": limit, + "offset": offset, + } + return await self.http.post( + "/store/namespaces", + json=_provided_vals(payload), + headers=headers, + params=params, + ) diff --git a/libs/sdk-py/langgraph_sdk/_async/threads.py b/libs/sdk-py/langgraph_sdk/_async/threads.py new file mode 100644 index 000000000..d7081cd83 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_async/threads.py @@ -0,0 +1,671 @@ +"""Async client for managing threads in LangGraph.""" + +from __future__ import annotations + +from collections.abc import AsyncIterator, Mapping, Sequence +from typing import Any + +from langgraph_sdk._async.http import HttpClient +from langgraph_sdk.schema import ( + Checkpoint, + Json, + OnConflictBehavior, + QueryParamTypes, + SortOrder, + StreamPart, + Thread, + ThreadSelectField, + ThreadSortBy, + ThreadState, + ThreadStatus, + ThreadStreamMode, + ThreadUpdateStateResponse, +) + + +class ThreadsClient: + """Client for managing threads in LangGraph. + + A thread maintains the state of a graph across multiple interactions/invocations (aka runs). + It accumulates and persists the graph's state, allowing for continuity between separate + invocations of the graph. + + ???+ example "Example" + + ```python + client = get_client(url="http://localhost:2024")) + new_thread = await client.threads.create(metadata={"user_id": "123"}) + ``` + """ + + def __init__(self, http: HttpClient) -> None: + self.http = http + + async def get( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Get a thread by ID. + + Args: + thread_id: The ID of the thread to get. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Thread object. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + thread = await client.threads.get( + thread_id="my_thread_id" + ) + print(thread) + ``` + + ```shell + ----------------------------------------------------- + + { + 'thread_id': 'my_thread_id', + 'created_at': '2024-07-18T18:35:15.540834+00:00', + 'updated_at': '2024-07-18T18:35:15.540834+00:00', + 'metadata': {'graph_id': 'agent'} + } + ``` + + """ + + return await self.http.get( + f"/threads/{thread_id}", headers=headers, params=params + ) + + async def create( + self, + *, + metadata: Json = None, + thread_id: str | None = None, + if_exists: OnConflictBehavior | None = None, + supersteps: Sequence[dict[str, Sequence[dict[str, Any]]]] | None = None, + graph_id: str | None = None, + ttl: int | Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Create a new thread. + + Args: + metadata: Metadata to add to thread. + thread_id: ID of thread. + If `None`, ID will be a randomly generated UUID. + if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. + Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread). + supersteps: Apply a list of supersteps when creating a thread, each containing a sequence of updates. + Each update has `values` or `command` and `as_node`. Used for copying a thread between deployments. + graph_id: Optional graph ID to associate with the thread. + ttl: Optional time-to-live in minutes for the thread. You can pass an + integer (minutes) or a mapping with keys `ttl` and optional + `strategy` (defaults to "delete"). + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The created thread. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + thread = await client.threads.create( + metadata={"number":1}, + thread_id="my-thread-id", + if_exists="raise" + ) + ``` + """ + payload: dict[str, Any] = {} + if thread_id: + payload["thread_id"] = thread_id + if metadata or graph_id: + payload["metadata"] = { + **(metadata or {}), + **({"graph_id": graph_id} if graph_id else {}), + } + if if_exists: + payload["if_exists"] = if_exists + if supersteps: + payload["supersteps"] = [ + { + "updates": [ + { + "values": u["values"], + "command": u.get("command"), + "as_node": u["as_node"], + } + for u in s["updates"] + ] + } + for s in supersteps + ] + if ttl is not None: + if isinstance(ttl, (int, float)): + payload["ttl"] = {"ttl": ttl, "strategy": "delete"} + else: + payload["ttl"] = ttl + + return await self.http.post( + "/threads", json=payload, headers=headers, params=params + ) + + async def update( + self, + thread_id: str, + *, + metadata: Mapping[str, Any], + ttl: int | Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Update a thread. + + Args: + thread_id: ID of thread to update. + metadata: Metadata to merge with existing thread metadata. + ttl: Optional time-to-live in minutes for the thread. You can pass an + integer (minutes) or a mapping with keys `ttl` and optional + `strategy` (defaults to "delete"). + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The created thread. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + thread = await client.threads.update( + thread_id="my-thread-id", + metadata={"number":1}, + ttl=43_200, + ) + ``` + """ + payload: dict[str, Any] = {"metadata": metadata} + if ttl is not None: + if isinstance(ttl, (int, float)): + payload["ttl"] = {"ttl": ttl, "strategy": "delete"} + else: + payload["ttl"] = ttl + return await self.http.patch( + f"/threads/{thread_id}", + json=payload, + headers=headers, + params=params, + ) + + async def delete( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a thread. + + Args: + thread_id: The ID of the thread to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost2024) + await client.threads.delete( + thread_id="my_thread_id" + ) + ``` + + """ + await self.http.delete(f"/threads/{thread_id}", headers=headers, params=params) + + async def search( + self, + *, + metadata: Json = None, + values: Json = None, + ids: Sequence[str] | None = None, + status: ThreadStatus | None = None, + limit: int = 10, + offset: int = 0, + sort_by: ThreadSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[ThreadSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Thread]: + """Search for threads. + + Args: + metadata: Thread metadata to filter on. + values: State values to filter on. + ids: List of thread IDs to filter by. + status: Thread status to filter on. + Must be one of 'idle', 'busy', 'interrupted' or 'error'. + limit: Limit on number of threads to return. + offset: Offset in threads table to start search from. + sort_by: Sort by field. + sort_order: Sort order. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + List of the threads matching the search parameters. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024") + threads = await client.threads.search( + metadata={"number":1}, + status="interrupted", + limit=15, + offset=5 + ) + ``` + + """ + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + if values: + payload["values"] = values + if ids: + payload["ids"] = ids + if status: + payload["status"] = status + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + return await self.http.post( + "/threads/search", + json=payload, + headers=headers, + params=params, + ) + + async def count( + self, + *, + metadata: Json = None, + values: Json = None, + status: ThreadStatus | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count threads matching filters. + + Args: + metadata: Thread metadata to filter on. + values: State values to filter on. + status: Thread status to filter on. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of threads matching the criteria. + """ + payload: dict[str, Any] = {} + if metadata: + payload["metadata"] = metadata + if values: + payload["values"] = values + if status: + payload["status"] = status + return await self.http.post( + "/threads/count", json=payload, headers=headers, params=params + ) + + async def copy( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Copy a thread. + + Args: + thread_id: The ID of the thread to copy. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024) + await client.threads.copy( + thread_id="my_thread_id" + ) + ``` + + """ + return await self.http.post( + f"/threads/{thread_id}/copy", json=None, headers=headers, params=params + ) + + async def get_state( + self, + thread_id: str, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, # deprecated + *, + subgraphs: bool = False, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ThreadState: + """Get the state of a thread. + + Args: + thread_id: The ID of the thread to get the state of. + checkpoint: The checkpoint to get the state of. + checkpoint_id: (deprecated) The checkpoint ID to get the state of. + subgraphs: Include subgraphs states. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The thread of the state. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024) + thread_state = await client.threads.get_state( + thread_id="my_thread_id", + checkpoint_id="my_checkpoint_id" + ) + print(thread_state) + ``` + + ```shell + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'values': { + 'messages': [ + { + 'content': 'how are you?', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', + 'example': False + }, + { + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + }, + 'next': [], + 'checkpoint': + { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1' + } + 'metadata': + { + 'step': 1, + 'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2', + 'source': 'loop', + 'writes': + { + 'agent': + { + 'messages': [ + { + 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', + 'name': None, + 'type': 'ai', + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'example': False, + 'tool_calls': [], + 'usage_metadata': None, + 'additional_kwargs': {}, + 'response_metadata': {}, + 'invalid_tool_calls': [] + } + ] + } + }, + 'user_id': None, + 'graph_id': 'agent', + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'created_by': 'system', + 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, + 'created_at': '2024-07-25T15:35:44.184703+00:00', + 'parent_config': + { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f' + } + } + ``` + """ + if checkpoint: + return await self.http.post( + f"/threads/{thread_id}/state/checkpoint", + json={"checkpoint": checkpoint, "subgraphs": subgraphs}, + headers=headers, + params=params, + ) + elif checkpoint_id: + get_params = {"subgraphs": subgraphs} + if params: + get_params = {**get_params, **params} + return await self.http.get( + f"/threads/{thread_id}/state/{checkpoint_id}", + params=get_params, + headers=headers, + ) + else: + get_params = {"subgraphs": subgraphs} + if params: + get_params = {**get_params, **params} + return await self.http.get( + f"/threads/{thread_id}/state", + params=get_params, + headers=headers, + ) + + async def update_state( + self, + thread_id: str, + values: dict[str, Any] | Sequence[dict] | None, + *, + as_node: str | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, # deprecated + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ThreadUpdateStateResponse: + """Update the state of a thread. + + Args: + thread_id: The ID of the thread to update. + values: The values to update the state with. + as_node: Update the state as if this node had just executed. + checkpoint: The checkpoint to update the state of. + checkpoint_id: (deprecated) The checkpoint ID to update the state of. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Response after updating a thread's state. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024) + response = await client.threads.update_state( + thread_id="my_thread_id", + values={"messages":[{"role": "user", "content": "hello!"}]}, + as_node="my_node", + ) + print(response) + ``` + ```shell + + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'checkpoint': { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1', + 'checkpoint_map': {} + } + } + ``` + """ + payload: dict[str, Any] = { + "values": values, + } + if checkpoint_id: + payload["checkpoint_id"] = checkpoint_id + if checkpoint: + payload["checkpoint"] = checkpoint + if as_node: + payload["as_node"] = as_node + return await self.http.post( + f"/threads/{thread_id}/state", json=payload, headers=headers, params=params + ) + + async def get_history( + self, + thread_id: str, + *, + limit: int = 10, + before: str | Checkpoint | None = None, + metadata: Mapping[str, Any] | None = None, + checkpoint: Checkpoint | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[ThreadState]: + """Get the state history of a thread. + + Args: + thread_id: The ID of the thread to get the state history for. + checkpoint: Return states for this subgraph. If empty defaults to root. + limit: The maximum number of states to return. + before: Return states before this checkpoint. + metadata: Filter states by metadata key-value pairs. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The state history of the thread. + + ???+ example "Example Usage" + + ```python + client = get_client(url="http://localhost:2024) + thread_state = await client.threads.get_history( + thread_id="my_thread_id", + limit=5, + ) + ``` + + """ + payload: dict[str, Any] = { + "limit": limit, + } + if before: + payload["before"] = before + if metadata: + payload["metadata"] = metadata + if checkpoint: + payload["checkpoint"] = checkpoint + return await self.http.post( + f"/threads/{thread_id}/history", + json=payload, + headers=headers, + params=params, + ) + + async def join_stream( + self, + thread_id: str, + *, + last_event_id: str | None = None, + stream_mode: ThreadStreamMode | Sequence[ThreadStreamMode] = "run_modes", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> AsyncIterator[StreamPart]: + """Get a stream of events for a thread. + + Args: + thread_id: The ID of the thread to get the stream for. + last_event_id: The ID of the last event to get. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + An iterator of stream parts. + + ???+ example "Example Usage" + + ```python + + for chunk in client.threads.join_stream( + thread_id="my_thread_id", + last_event_id="my_event_id", + ): + print(chunk) + ``` + + """ + query_params = { + "stream_mode": stream_mode, + } + if params: + query_params.update(params) + return self.http.stream( + f"/threads/{thread_id}/stream", + "GET", + headers={ + **({"Last-Event-ID": last_event_id} if last_event_id else {}), + **(headers or {}), + }, + params=query_params, + ) diff --git a/libs/sdk-py/langgraph_sdk/_shared/__init__.py b/libs/sdk-py/langgraph_sdk/_shared/__init__.py new file mode 100644 index 000000000..5783ce0c2 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_shared/__init__.py @@ -0,0 +1 @@ +"""Shared utilities for async and sync clients.""" diff --git a/libs/sdk-py/langgraph_sdk/_shared/types.py b/libs/sdk-py/langgraph_sdk/_shared/types.py new file mode 100644 index 000000000..073b1380a --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_shared/types.py @@ -0,0 +1,10 @@ +"""Type aliases and constants.""" + +from __future__ import annotations + +TimeoutTypes = ( + None + | float + | tuple[float | None, float | None] + | tuple[float | None, float | None, float | None, float | None] +) diff --git a/libs/sdk-py/langgraph_sdk/_shared/utilities.py b/libs/sdk-py/langgraph_sdk/_shared/utilities.py new file mode 100644 index 000000000..02f28fd9a --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_shared/utilities.py @@ -0,0 +1,131 @@ +"""Shared utility functions for async and sync clients.""" + +from __future__ import annotations + +import functools +import os +import re +from collections.abc import Mapping +from typing import Any, cast + +import httpx + +import langgraph_sdk +from langgraph_sdk.schema import RunCreateMetadata + +RESERVED_HEADERS = ("x-api-key",) + +NOT_PROVIDED = cast(None, object()) + + +def _get_api_key(api_key: str | None = NOT_PROVIDED) -> str | None: + """Get the API key from the environment. + Precedence: + 1. explicit string argument + 2. LANGGRAPH_API_KEY (if api_key not provided) + 3. LANGSMITH_API_KEY (if api_key not provided) + 4. LANGCHAIN_API_KEY (if api_key not provided) + + Args: + api_key: The API key to use. Can be: + - A string: use this exact API key + - None: explicitly skip loading from environment + - NOT_PROVIDED (default): auto-load from environment variables + """ + if isinstance(api_key, str): + return api_key + if api_key is NOT_PROVIDED: + # api_key is not explicitly provided, try to load from environment + for prefix in ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"]: + if env := os.getenv(f"{prefix}_API_KEY"): + return env.strip().strip('"').strip("'") + # api_key is explicitly None, don't load from environment + return None + + +def _get_headers( + api_key: str | None, + custom_headers: Mapping[str, str] | None, +) -> dict[str, str]: + """Combine api_key and custom user-provided headers.""" + custom_headers = custom_headers or {} + for header in RESERVED_HEADERS: + if header in custom_headers: + raise ValueError(f"Cannot set reserved header '{header}'") + + headers = { + "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", + **custom_headers, + } + resolved_api_key = _get_api_key(api_key) + if resolved_api_key: + headers["x-api-key"] = resolved_api_key + + return headers + + +def _orjson_default(obj: Any) -> Any: + is_class = isinstance(obj, type) + if hasattr(obj, "model_dump") and callable(obj.model_dump): + if is_class: + raise TypeError( + f"Cannot JSON-serialize type object: {obj!r}. Did you mean to pass an instance of the object instead?" + f"\nReceived type: {obj!r}" + ) + return obj.model_dump() + elif hasattr(obj, "dict") and callable(obj.dict): + if is_class: + raise TypeError( + f"Cannot JSON-serialize type object: {obj!r}. Did you mean to pass an instance of the object instead?" + f"\nReceived type: {obj!r}" + ) + return obj.dict() + elif isinstance(obj, (set, frozenset)): + return list(obj) + else: + raise TypeError(f"Object of type {type(obj)} is not JSON serializable") + + +# Compiled regex pattern for extracting run metadata from Content-Location header +_RUN_METADATA_PATTERN = re.compile( + r"(\/threads\/(?P.+))?\/runs\/(?P.+)" +) + + +def _get_run_metadata_from_response( + response: httpx.Response, +) -> RunCreateMetadata | None: + """Extract run metadata from the response headers.""" + if (content_location := response.headers.get("Content-Location")) and ( + match := _RUN_METADATA_PATTERN.search(content_location) + ): + return RunCreateMetadata( + run_id=match.group("run_id"), + thread_id=match.group("thread_id") or None, + ) + + return None + + +def _provided_vals(d: Mapping[str, Any]) -> dict[str, Any]: + return {k: v for k, v in d.items() if v is not None} + + +_registered_transports: list[httpx.ASGITransport] = [] + + +# Do not move; this is used in the server. +def configure_loopback_transports(app: Any) -> None: + for transport in _registered_transports: + transport.app = app + + +@functools.lru_cache(maxsize=1) +def get_asgi_transport() -> type[httpx.ASGITransport]: + try: + from langgraph_api import asgi_transport # type: ignore[unresolved-import] + + return asgi_transport.ASGITransport + except ImportError: + # Older versions of the server + return httpx.ASGITransport diff --git a/libs/sdk-py/langgraph_sdk/_sync/__init__.py b/libs/sdk-py/langgraph_sdk/_sync/__init__.py new file mode 100644 index 000000000..1ab36bf80 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/__init__.py @@ -0,0 +1,20 @@ +"""Sync client exports.""" + +from langgraph_sdk._sync.assistants import SyncAssistantsClient +from langgraph_sdk._sync.client import SyncLangGraphClient, get_sync_client +from langgraph_sdk._sync.cron import SyncCronClient +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk._sync.runs import SyncRunsClient +from langgraph_sdk._sync.store import SyncStoreClient +from langgraph_sdk._sync.threads import SyncThreadsClient + +__all__ = [ + "SyncAssistantsClient", + "SyncCronClient", + "SyncHttpClient", + "SyncLangGraphClient", + "SyncRunsClient", + "SyncStoreClient", + "SyncThreadsClient", + "get_sync_client", +] diff --git a/libs/sdk-py/langgraph_sdk/_sync/assistants.py b/libs/sdk-py/langgraph_sdk/_sync/assistants.py new file mode 100644 index 000000000..0f5dec393 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/assistants.py @@ -0,0 +1,718 @@ +"""Synchronous client for managing assistants in LangGraph.""" + +from __future__ import annotations + +from collections.abc import Mapping +from typing import Any, Literal, cast, overload + +import httpx + +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk.schema import ( + Assistant, + AssistantSelectField, + AssistantSortBy, + AssistantsSearchResponse, + AssistantVersion, + Config, + Context, + GraphSchema, + Json, + OnConflictBehavior, + QueryParamTypes, + SortOrder, + Subgraphs, +) + + +class SyncAssistantsClient: + """Client for managing assistants in LangGraph synchronously. + + This class provides methods to interact with assistants, which are versioned configurations of your graph. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:2024") + assistant = client.assistants.get("assistant_id_123") + ``` + """ + + def __init__(self, http: SyncHttpClient) -> None: + self.http = http + + def get( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Get an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get OR the name of the graph (to use the default assistant). + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `Assistant` Object. + + ???+ example "Example Usage" + + ```python + assistant = client.assistants.get( + assistant_id="my_assistant_id" + ) + print(assistant) + ``` + + ```shell + ---------------------------------------------------- + + { + 'assistant_id': 'my_assistant_id', + 'graph_id': 'agent', + 'created_at': '2024-06-25T17:10:33.109781+00:00', + 'updated_at': '2024-06-25T17:10:33.109781+00:00', + 'config': {}, + 'context': {}, + 'metadata': {'created_by': 'system'} + } + ``` + + """ + return self.http.get( + f"/assistants/{assistant_id}", headers=headers, params=params + ) + + def get_graph( + self, + assistant_id: str, + *, + xray: int | bool = False, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> dict[str, list[dict[str, Any]]]: + """Get the graph of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the graph of. + xray: Include graph representation of subgraphs. If an integer value is provided, only subgraphs with a depth less than or equal to the value will be included. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The graph information for the assistant in JSON format. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + graph_info = client.assistants.get_graph( + assistant_id="my_assistant_id" + ) + print(graph_info) + + -------------------------------------------------------------------------------------------------------------------------- + + { + 'nodes': + [ + {'id': '__start__', 'type': 'schema', 'data': '__start__'}, + {'id': '__end__', 'type': 'schema', 'data': '__end__'}, + {'id': 'agent','type': 'runnable','data': {'id': ['langgraph', 'utils', 'RunnableCallable'],'name': 'agent'}}, + ], + 'edges': + [ + {'source': '__start__', 'target': 'agent'}, + {'source': 'agent','target': '__end__'} + ] + } + ``` + + """ + query_params = {"xray": xray} + if params: + query_params.update(params) + return self.http.get( + f"/assistants/{assistant_id}/graph", params=query_params, headers=headers + ) + + def get_schemas( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> GraphSchema: + """Get the schemas of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the schema of. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + GraphSchema: The graph schema for the assistant. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + schema = client.assistants.get_schemas( + assistant_id="my_assistant_id" + ) + print(schema) + ``` + ```shell + ---------------------------------------------------------------------------------------------------------------------------- + + { + 'graph_id': 'agent', + 'state_schema': + { + 'title': 'LangGraphInput', + '$ref': '#/definitions/AgentState', + 'definitions': + { + 'BaseMessage': + { + 'title': 'BaseMessage', + 'description': 'Base abstract Message class. Messages are the inputs and outputs of ChatModels.', + 'type': 'object', + 'properties': + { + 'content': + { + 'title': 'Content', + 'anyOf': [ + {'type': 'string'}, + {'type': 'array','items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}} + ] + }, + 'additional_kwargs': + { + 'title': 'Additional Kwargs', + 'type': 'object' + }, + 'response_metadata': + { + 'title': 'Response Metadata', + 'type': 'object' + }, + 'type': + { + 'title': 'Type', + 'type': 'string' + }, + 'name': + { + 'title': 'Name', + 'type': 'string' + }, + 'id': + { + 'title': 'Id', + 'type': 'string' + } + }, + 'required': ['content', 'type'] + }, + 'AgentState': + { + 'title': 'AgentState', + 'type': 'object', + 'properties': + { + 'messages': + { + 'title': 'Messages', + 'type': 'array', + 'items': {'$ref': '#/definitions/BaseMessage'} + } + }, + 'required': ['messages'] + } + } + }, + 'config_schema': + { + 'title': 'Configurable', + 'type': 'object', + 'properties': + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } + }, + 'context_schema': + { + 'title': 'Context', + 'type': 'object', + 'properties': + { + 'model_name': + { + 'title': 'Model Name', + 'enum': ['anthropic', 'openai'], + 'type': 'string' + } + } + } + } + ``` + + """ + return self.http.get( + f"/assistants/{assistant_id}/schemas", headers=headers, params=params + ) + + def get_subgraphs( + self, + assistant_id: str, + namespace: str | None = None, + recurse: bool = False, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Subgraphs: + """Get the schemas of an assistant by ID. + + Args: + assistant_id: The ID of the assistant to get the schema of. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + Subgraphs: The graph schema for the assistant. + + """ + get_params = {"recurse": recurse} + if params: + get_params = {**get_params, **params} + if namespace is not None: + return self.http.get( + f"/assistants/{assistant_id}/subgraphs/{namespace}", + params=get_params, + headers=headers, + ) + else: + return self.http.get( + f"/assistants/{assistant_id}/subgraphs", + params=get_params, + headers=headers, + ) + + def create( + self, + graph_id: str | None, + config: Config | None = None, + *, + context: Context | None = None, + metadata: Json = None, + assistant_id: str | None = None, + if_exists: OnConflictBehavior | None = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + description: str | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Create a new assistant. + + Useful when graph is configurable and you want to create different assistants based on different configurations. + + Args: + graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. + config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + metadata: Metadata to add to assistant. + assistant_id: Assistant ID to use, will default to a random UUID if not provided. + if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. + Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing assistant). + name: The name of the assistant. Defaults to 'Untitled' under the hood. + headers: Optional custom headers to include with the request. + description: Optional description of the assistant. + The description field is available for langgraph-api server version>=0.0.45 + params: Optional query parameters to include with the request. + + Returns: + The created assistant. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + assistant = client.assistants.create( + graph_id="agent", + context={"model_name": "openai"}, + metadata={"number":1}, + assistant_id="my-assistant-id", + if_exists="do_nothing", + name="my_name" + ) + ``` + """ + payload: dict[str, Any] = { + "graph_id": graph_id, + } + if config: + payload["config"] = config + if context: + payload["context"] = context + if metadata: + payload["metadata"] = metadata + if assistant_id: + payload["assistant_id"] = assistant_id + if if_exists: + payload["if_exists"] = if_exists + if name: + payload["name"] = name + if description: + payload["description"] = description + return self.http.post( + "/assistants", json=payload, headers=headers, params=params + ) + + def update( + self, + assistant_id: str, + *, + graph_id: str | None = None, + config: Config | None = None, + context: Context | None = None, + metadata: Json = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + description: str | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Update an assistant. + + Use this to point to a different graph, update the configuration, or change the metadata of an assistant. + + Args: + assistant_id: Assistant to update. + graph_id: The ID of the graph the assistant should use. + The graph ID is normally set in your langgraph.json configuration. If `None`, assistant will keep pointing to same graph. + config: Configuration to use for the graph. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + metadata: Metadata to merge with existing assistant metadata. + name: The new name for the assistant. + headers: Optional custom headers to include with the request. + description: Optional description of the assistant. + The description field is available for langgraph-api server version>=0.0.45 + + Returns: + The updated assistant. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + assistant = client.assistants.update( + assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', + graph_id="other-graph", + context={"model_name": "anthropic"}, + metadata={"number":2} + ) + ``` + """ + payload: dict[str, Any] = {} + if graph_id: + payload["graph_id"] = graph_id + if config: + payload["config"] = config + if context: + payload["context"] = context + if metadata: + payload["metadata"] = metadata + if name: + payload["name"] = name + if description: + payload["description"] = description + return self.http.patch( + f"/assistants/{assistant_id}", + json=payload, + headers=headers, + params=params, + ) + + def delete( + self, + assistant_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete an assistant. + + Args: + assistant_id: The assistant ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.assistants.delete( + assistant_id="my_assistant_id" + ) + ``` + + """ + self.http.delete(f"/assistants/{assistant_id}", headers=headers, params=params) + + @overload + def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["object"], + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> AssistantsSearchResponse: ... + + @overload + def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["array"] = "array", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Assistant]: ... + + def search( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + limit: int = 10, + offset: int = 0, + sort_by: AssistantSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[AssistantSelectField] | None = None, + response_format: Literal["array", "object"] = "array", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> AssistantsSearchResponse | list[Assistant]: + """Search for assistants. + + Args: + metadata: Metadata to filter by. Exact match filter for each KV pair. + graph_id: The ID of the graph to filter by. + The graph ID is normally set in your langgraph.json configuration. + name: The name of the assistant to filter by. + The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. + limit: The maximum number of results to return. + offset: The number of results to skip. + sort_by: The field to sort by. + sort_order: The order to sort by. + select: Specific assistant fields to include in the response. + response_format: Controls the response shape. Use `"array"` (default) + to return a bare list of assistants, or `"object"` to return + a mapping containing assistants plus pagination metadata. + Defaults to "array", though this default will be changed to "object" in a future release. + headers: Optional custom headers to include with the request. + + Returns: + A list of assistants (when `response_format="array"`) or a mapping + with the assistants and the next pagination cursor (when + `response_format="object"`). + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + response = client.assistants.search( + metadata = {"name":"my_name"}, + graph_id="my_graph_id", + limit=5, + offset=5, + response_format="object", + ) + assistants = response["assistants"] + next_cursor = response["next"] + ``` + """ + if response_format not in ("array", "object"): + raise ValueError("response_format must be 'array' or 'object'") + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + if graph_id: + payload["graph_id"] = graph_id + if name: + payload["name"] = name + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + next_cursor: str | None = None + + def capture_pagination(response: httpx.Response) -> None: + nonlocal next_cursor + next_cursor = response.headers.get("X-Pagination-Next") + + assistants = cast( + list[Assistant], + self.http.post( + "/assistants/search", + json=payload, + headers=headers, + params=params, + on_response=capture_pagination if response_format == "object" else None, + ), + ) + if response_format == "object": + return {"assistants": assistants, "next": next_cursor} + return assistants + + def count( + self, + *, + metadata: Json = None, + graph_id: str | None = None, + name: str | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count assistants matching filters. + + Args: + metadata: Metadata to filter by. Exact match for each key/value. + graph_id: Optional graph id to filter by. + name: Optional name to filter by. + The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of assistants matching the criteria. + """ + payload: dict[str, Any] = {} + if metadata: + payload["metadata"] = metadata + if graph_id: + payload["graph_id"] = graph_id + if name: + payload["name"] = name + return self.http.post( + "/assistants/count", json=payload, headers=headers, params=params + ) + + def get_versions( + self, + assistant_id: str, + metadata: Json = None, + limit: int = 10, + offset: int = 0, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[AssistantVersion]: + """List all versions of an assistant. + + Args: + assistant_id: The assistant ID to get versions for. + metadata: Metadata to filter versions by. Exact match filter for each KV pair. + limit: The maximum number of versions to return. + offset: The number of versions to skip. + headers: Optional custom headers to include with the request. + + Returns: + A list of assistants. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + assistant_versions = client.assistants.get_versions( + assistant_id="my_assistant_id" + ) + ``` + + """ + + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + return self.http.post( + f"/assistants/{assistant_id}/versions", + json=payload, + headers=headers, + params=params, + ) + + def set_latest( + self, + assistant_id: str, + version: int, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Assistant: + """Change the version of an assistant. + + Args: + assistant_id: The assistant ID to delete. + version: The version to change to. + headers: Optional custom headers to include with the request. + + Returns: + `Assistant` Object. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + new_version_assistant = client.assistants.set_latest( + assistant_id="my_assistant_id", + version=3 + ) + ``` + + """ + + payload: dict[str, Any] = {"version": version} + + return self.http.post( + f"/assistants/{assistant_id}/latest", + json=payload, + headers=headers, + params=params, + ) diff --git a/libs/sdk-py/langgraph_sdk/_sync/client.py b/libs/sdk-py/langgraph_sdk/_sync/client.py new file mode 100644 index 000000000..1a0d641e2 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/client.py @@ -0,0 +1,127 @@ +"""Sync LangGraph client.""" + +from __future__ import annotations + +from collections.abc import Mapping +from types import TracebackType + +import httpx + +from langgraph_sdk._shared.types import TimeoutTypes +from langgraph_sdk._shared.utilities import NOT_PROVIDED, _get_headers +from langgraph_sdk._sync.assistants import SyncAssistantsClient +from langgraph_sdk._sync.cron import SyncCronClient +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk._sync.runs import SyncRunsClient +from langgraph_sdk._sync.store import SyncStoreClient +from langgraph_sdk._sync.threads import SyncThreadsClient + + +def get_sync_client( + *, + url: str | None = None, + api_key: str | None = NOT_PROVIDED, + headers: Mapping[str, str] | None = None, + timeout: TimeoutTypes | None = None, +) -> SyncLangGraphClient: + """Get a synchronous LangGraphClient instance. + + Args: + url: The URL of the LangGraph API. + api_key: API key for authentication. Can be: + - A string: use this exact API key + - `None`: explicitly skip loading from environment variables + - Not provided (default): auto-load from environment in this order: + 1. `LANGGRAPH_API_KEY` + 2. `LANGSMITH_API_KEY` + 3. `LANGCHAIN_API_KEY` + headers: Optional custom headers + timeout: Optional timeout configuration for the HTTP client. + Accepts an httpx.Timeout instance, a float (seconds), or a tuple of timeouts. + Tuple format is (connect, read, write, pool) + If not provided, defaults to connect=5s, read=300s, write=300s, and pool=5s. + Returns: + SyncLangGraphClient: The top-level synchronous client for accessing AssistantsClient, + ThreadsClient, RunsClient, and CronClient. + + ???+ example "Example" + + ```python + from langgraph_sdk import get_sync_client + + # get top-level synchronous LangGraphClient + client = get_sync_client(url="http://localhost:8123") + + # example usage: client..() + assistant = client.assistants.get(assistant_id="some_uuid") + ``` + + ???+ example "Skip auto-loading API key from environment:" + + ```python + from langgraph_sdk import get_sync_client + + # Don't load API key from environment variables + client = get_sync_client( + url="http://localhost:8123", + api_key=None + ) + ``` + """ + + if url is None: + url = "http://localhost:8123" + + transport = httpx.HTTPTransport(retries=5) + client = httpx.Client( + base_url=url, + transport=transport, + timeout=( + httpx.Timeout(timeout) # type: ignore[arg-type] + if timeout is not None + else httpx.Timeout(connect=5, read=300, write=300, pool=5) + ), + headers=_get_headers(api_key, headers), + ) + return SyncLangGraphClient(client) + + +class SyncLangGraphClient: + """Synchronous client for interacting with the LangGraph API. + + This class provides synchronous access to LangGraph API endpoints for managing + assistants, threads, runs, cron jobs, and data storage. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:2024") + assistant = client.assistants.get("asst_123") + ``` + """ + + def __init__(self, client: httpx.Client) -> None: + self.http = SyncHttpClient(client) + self.assistants = SyncAssistantsClient(self.http) + self.threads = SyncThreadsClient(self.http) + self.runs = SyncRunsClient(self.http) + self.crons = SyncCronClient(self.http) + self.store = SyncStoreClient(self.http) + + def __enter__(self) -> SyncLangGraphClient: + """Enter the sync context manager.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + """Exit the sync context manager.""" + self.close() + + def close(self) -> None: + """Close the underlying HTTP client.""" + if hasattr(self, "http"): + self.http.client.close() diff --git a/libs/sdk-py/langgraph_sdk/_sync/cron.py b/libs/sdk-py/langgraph_sdk/_sync/cron.py new file mode 100644 index 000000000..b335a15ef --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/cron.py @@ -0,0 +1,439 @@ +"""Synchronous cron client for LangGraph SDK.""" + +from __future__ import annotations + +from collections.abc import Mapping +from datetime import datetime +from typing import Any + +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk.schema import ( + All, + Config, + Context, + Cron, + CronSelectField, + CronSortBy, + Input, + OnCompletionBehavior, + QueryParamTypes, + Run, + SortOrder, +) + + +class SyncCronClient: + """Synchronous client for managing cron jobs in LangGraph. + + This class provides methods to create and manage scheduled tasks (cron jobs) for automated graph executions. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:8123") + cron_job = client.crons.create_for_thread(thread_id="thread_123", assistant_id="asst_456", schedule="0 * * * *") + ``` + + !!! note "Feature Availability" + + The crons client functionality is not supported on all licenses. + Please check the relevant license documentation for the most up-to-date + details on feature availability. + """ + + def __init__(self, http_client: SyncHttpClient) -> None: + self.http = http_client + + def create_for_thread( + self, + thread_id: str, + assistant_id: str, + *, + schedule: str, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + webhook: str | None = None, + multitask_strategy: str | None = None, + end_time: datetime | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Create a cron job for a thread. + + Args: + thread_id: the thread ID to run the cron job on. + assistant_id: The assistant ID or graph name to use for the cron job. + If using graph name, will default to first assistant created from that graph. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. + enabled: Whether the cron job is enabled. By default, it is considered enabled. + headers: Optional custom headers to include with the request. + + Returns: + The cron `Run`. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + cron_run = client.crons.create_for_thread( + thread_id="my-thread-id", + assistant_id="agent", + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt", + enabled=True + ) + ``` + """ + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "context": context, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "checkpoint_during": checkpoint_during, + "webhook": webhook, + "multitask_strategy": multitask_strategy, + "end_time": end_time.isoformat() if end_time else None, + "enabled": enabled, + } + payload = {k: v for k, v in payload.items() if v is not None} + return self.http.post( + f"/threads/{thread_id}/runs/crons", + json=payload, + headers=headers, + params=params, + ) + + def create( + self, + assistant_id: str, + *, + schedule: str, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + webhook: str | None = None, + on_run_completed: OnCompletionBehavior | None = None, + multitask_strategy: str | None = None, + end_time: datetime | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Create a cron run. + + Args: + assistant_id: The assistant ID or graph name to use for the cron job. + If using graph name, will default to first assistant created from that graph. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + on_run_completed: What to do with the thread after the run completes. + Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread + after execution. 'keep' creates a new thread for each execution but does not + clean them up. Clients are responsible for cleaning up kept threads. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. + enabled: Whether the cron job is enabled. By default, it is considered enabled. + headers: Optional custom headers to include with the request. + + Returns: + The cron `Run`. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + cron_run = client.crons.create( + assistant_id="agent", + schedule="27 15 * * *", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + checkpoint_during=True, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt", + enabled=True + ) + ``` + + """ + payload = { + "schedule": schedule, + "input": input, + "config": config, + "metadata": metadata, + "context": context, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "checkpoint_during": checkpoint_during, + "on_run_completed": on_run_completed, + "multitask_strategy": multitask_strategy, + "end_time": end_time.isoformat() if end_time else None, + "enabled": enabled, + } + payload = {k: v for k, v in payload.items() if v is not None} + return self.http.post( + "/runs/crons", json=payload, headers=headers, params=params + ) + + def delete( + self, + cron_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a cron. + + Args: + cron_id: The cron ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + client.crons.delete( + cron_id="cron_to_delete" + ) + ``` + + """ + self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params) + + def update( + self, + cron_id: str, + *, + schedule: str | None = None, + end_time: datetime | None = None, + input: Input | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + webhook: str | None = None, + interrupt_before: All | list[str] | None = None, + interrupt_after: All | list[str] | None = None, + on_run_completed: OnCompletionBehavior | None = None, + enabled: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Cron: + """Update a cron job by ID. + + Args: + cron_id: The cron ID to update. + schedule: The cron schedule to execute this job on. + Schedules are interpreted in UTC. + end_time: The end date to stop running the cron. + input: The input to the graph. + metadata: Metadata to assign to the cron job runs. + config: The configuration for the assistant. + context: Static context added to the assistant. + webhook: Webhook to call after LangGraph API call is done. + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to interrupt immediately after they get executed. + on_run_completed: What to do with the thread after the run completes. + Must be one of 'delete' or 'keep'. 'delete' removes the thread + after execution. 'keep' creates a new thread for each execution but does not + clean them up. + enabled: Enable or disable the cron job. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The updated cron job. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + updated_cron = client.crons.update( + cron_id="1ef3cefa-4c09-6926-96d0-3dc97fd5e39b", + schedule="0 10 * * *", + enabled=False, + ) + ``` + + """ + payload = { + "schedule": schedule, + "end_time": end_time.isoformat() if end_time else None, + "input": input, + "metadata": metadata, + "config": config, + "context": context, + "webhook": webhook, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "on_run_completed": on_run_completed, + "enabled": enabled, + } + payload = {k: v for k, v in payload.items() if v is not None} + return self.http.patch( + f"/runs/crons/{cron_id}", + json=payload, + headers=headers, + params=params, + ) + + def search( + self, + *, + assistant_id: str | None = None, + thread_id: str | None = None, + enabled: bool | None = None, + limit: int = 10, + offset: int = 0, + sort_by: CronSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[CronSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Cron]: + """Get a list of cron jobs. + + Args: + assistant_id: The assistant ID or graph name to search for. + thread_id: the thread ID to search for. + enabled: Whether the cron job is enabled. + limit: The maximum number of results to return. + offset: The number of results to skip. + headers: Optional custom headers to include with the request. + + Returns: + The list of cron jobs returned by the search, + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + cron_jobs = client.crons.search( + assistant_id="my_assistant_id", + thread_id="my_thread_id", + enabled=True, + limit=5, + offset=5, + ) + print(cron_jobs) + ``` + + ```shell + ---------------------------------------------------------- + + [ + { + 'cron_id': '1ef3cefa-4c09-6926-96d0-3dc97fd5e39b', + 'assistant_id': 'my_assistant_id', + 'thread_id': 'my_thread_id', + 'user_id': None, + 'payload': + { + 'input': {'start_time': ''}, + 'schedule': '4 * * * *', + 'assistant_id': 'my_assistant_id' + }, + 'schedule': '4 * * * *', + 'next_run_date': '2024-07-25T17:04:00+00:00', + 'end_time': None, + 'created_at': '2024-07-08T06:02:23.073257+00:00', + 'updated_at': '2024-07-08T06:02:23.073257+00:00' + } + ] + ``` + """ + payload = { + "assistant_id": assistant_id, + "thread_id": thread_id, + "enabled": enabled, + "limit": limit, + "offset": offset, + } + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + payload = {k: v for k, v in payload.items() if v is not None} + return self.http.post( + "/runs/crons/search", json=payload, headers=headers, params=params + ) + + def count( + self, + *, + assistant_id: str | None = None, + thread_id: str | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count cron jobs matching filters. + + Args: + assistant_id: Assistant ID to filter by. + thread_id: Thread ID to filter by. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of crons matching the criteria. + """ + payload: dict[str, Any] = {} + if assistant_id: + payload["assistant_id"] = assistant_id + if thread_id: + payload["thread_id"] = thread_id + return self.http.post( + "/runs/crons/count", json=payload, headers=headers, params=params + ) diff --git a/libs/sdk-py/langgraph_sdk/_sync/http.py b/libs/sdk-py/langgraph_sdk/_sync/http.py new file mode 100644 index 000000000..dccba518f --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/http.py @@ -0,0 +1,296 @@ +"""Synchronous HTTP client for LangGraph API.""" + +from __future__ import annotations + +import logging +import sys +import warnings +from collections.abc import Callable, Iterator, Mapping +from typing import Any, cast + +import httpx +import orjson + +from langgraph_sdk._shared.utilities import _orjson_default +from langgraph_sdk.errors import _raise_for_status_typed +from langgraph_sdk.schema import QueryParamTypes, StreamPart +from langgraph_sdk.sse import SSEDecoder, iter_lines_raw + +logger = logging.getLogger(__name__) + + +class SyncHttpClient: + """Handle synchronous requests to the LangGraph API. + + Provides error messaging and content handling enhancements above the + underlying httpx client, mirroring the interface of [HttpClient](#HttpClient) + but for sync usage. + + Attributes: + client (httpx.Client): Underlying HTTPX sync client. + """ + + def __init__(self, client: httpx.Client) -> None: + self.client = client + + def get( + self, + path: str, + *, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `GET` request.""" + r = self.client.get(path, params=params, headers=headers) + if on_response: + on_response(r) + _raise_for_status_typed(r) + return _decode_json(r) + + def post( + self, + path: str, + *, + json: dict[str, Any] | list | None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `POST` request.""" + if json is not None: + request_headers, content = _encode_json(json) + else: + request_headers, content = {}, b"" + if headers: + request_headers.update(headers) + r = self.client.post( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + _raise_for_status_typed(r) + return _decode_json(r) + + def put( + self, + path: str, + *, + json: dict, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `PUT` request.""" + request_headers, content = _encode_json(json) + if headers: + request_headers.update(headers) + + r = self.client.put( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + _raise_for_status_typed(r) + return _decode_json(r) + + def patch( + self, + path: str, + *, + json: dict, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Any: + """Send a `PATCH` request.""" + request_headers, content = _encode_json(json) + if headers: + request_headers.update(headers) + r = self.client.patch( + path, headers=request_headers, content=content, params=params + ) + if on_response: + on_response(r) + _raise_for_status_typed(r) + return _decode_json(r) + + def delete( + self, + path: str, + *, + json: Any | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> None: + """Send a `DELETE` request.""" + r = self.client.request( + "DELETE", path, json=json, params=params, headers=headers + ) + if on_response: + on_response(r) + _raise_for_status_typed(r) + + def request_reconnect( + self, + path: str, + method: str, + *, + json: dict[str, Any] | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + reconnect_limit: int = 5, + ) -> Any: + """Send a request that automatically reconnects to Location header.""" + request_headers, content = _encode_json(json) + if headers: + request_headers.update(headers) + with self.client.stream( + method, path, headers=request_headers, content=content, params=params + ) as r: + if on_response: + on_response(r) + try: + r.raise_for_status() + except httpx.HTTPStatusError as e: + body = r.read().decode() + if sys.version_info >= (3, 11): + e.add_note(body) + else: + logger.error(f"Error from langgraph-api: {body}", exc_info=e) + raise e + loc = r.headers.get("location") + if reconnect_limit <= 0 or not loc: + return _decode_json(r) + try: + return _decode_json(r) + except httpx.HTTPError: + warnings.warn( + f"Request failed, attempting reconnect to Location: {loc}", + stacklevel=2, + ) + r.close() + return self.request_reconnect( + loc, + "GET", + headers=request_headers, + # don't pass on_response so it's only called once + reconnect_limit=reconnect_limit - 1, + ) + + def stream( + self, + path: str, + method: str, + *, + json: dict[str, Any] | None = None, + params: QueryParamTypes | None = None, + headers: Mapping[str, str] | None = None, + on_response: Callable[[httpx.Response], None] | None = None, + ) -> Iterator[StreamPart]: + """Stream the results of a request using SSE.""" + if json is not None: + request_headers, content = _encode_json(json) + else: + request_headers, content = {}, None + request_headers["Accept"] = "text/event-stream" + request_headers["Cache-Control"] = "no-store" + if headers: + request_headers.update(headers) + + reconnect_headers = { + key: value + for key, value in request_headers.items() + if key.lower() not in {"content-length", "content-type"} + } + + last_event_id: str | None = None + reconnect_path: str | None = None + reconnect_attempts = 0 + max_reconnect_attempts = 5 + + while True: + current_headers = dict( + request_headers if reconnect_path is None else reconnect_headers + ) + if last_event_id is not None: + current_headers["Last-Event-ID"] = last_event_id + + current_method = method if reconnect_path is None else "GET" + current_content = content if reconnect_path is None else None + current_params = params if reconnect_path is None else None + + retry = False + with self.client.stream( + current_method, + reconnect_path or path, + headers=current_headers, + content=current_content, + params=current_params, + ) as res: + if reconnect_path is None and on_response: + on_response(res) + # check status + _raise_for_status_typed(res) + # check content type + content_type = res.headers.get("content-type", "").partition(";")[0] + if "text/event-stream" not in content_type: + raise httpx.TransportError( + "Expected response header Content-Type to contain 'text/event-stream', " + f"got {content_type!r}" + ) + + reconnect_location = res.headers.get("location") + if reconnect_location: + reconnect_path = reconnect_location + + decoder = SSEDecoder() + try: + for line in iter_lines_raw(res): + sse = decoder.decode(cast(bytes, line).rstrip(b"\n")) + if sse is not None: + if decoder.last_event_id is not None: + last_event_id = decoder.last_event_id + if sse.event or sse.data is not None: + yield sse + except httpx.HTTPError: + # httpx.TransportError inherits from HTTPError, so transient + # disconnects during streaming land here. + if reconnect_path is None: + raise + retry = True + else: + if sse := decoder.decode(b""): + if decoder.last_event_id is not None: + last_event_id = decoder.last_event_id + if sse.event or sse.data is not None: + # See async stream implementation for rationale on + # skipping empty flush events. + yield sse + if retry: + reconnect_attempts += 1 + if reconnect_attempts > max_reconnect_attempts: + raise httpx.TransportError( + "Exceeded maximum SSE reconnection attempts" + ) + continue + break + + +def _encode_json(json: Any) -> tuple[dict[str, str], bytes]: + body = orjson.dumps( + json, + _orjson_default, + orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, + ) + content_length = str(len(body)) + content_type = "application/json" + headers = {"Content-Length": content_length, "Content-Type": content_type} + return headers, body + + +def _decode_json(r: httpx.Response) -> Any: + body = r.read() + return orjson.loads(body) if body else None diff --git a/libs/sdk-py/langgraph_sdk/_sync/runs.py b/libs/sdk-py/langgraph_sdk/_sync/runs.py new file mode 100644 index 000000000..ab6e1943f --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/runs.py @@ -0,0 +1,999 @@ +"""Synchronous client for managing runs in LangGraph.""" + +from __future__ import annotations + +import warnings +from collections.abc import Callable, Iterator, Mapping, Sequence +from typing import Any, overload + +import httpx + +from langgraph_sdk._shared.utilities import _get_run_metadata_from_response +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk.schema import ( + All, + CancelAction, + Checkpoint, + Command, + Config, + Context, + DisconnectMode, + Durability, + IfNotExists, + Input, + MultitaskStrategy, + OnCompletionBehavior, + QueryParamTypes, + Run, + RunCreate, + RunCreateMetadata, + RunSelectField, + RunStatus, + StreamMode, + StreamPart, +) + + +class SyncRunsClient: + """Synchronous client for managing runs in LangGraph. + + This class provides methods to create, retrieve, and manage runs, which represent + individual executions of graphs. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:2024") + run = client.runs.create(thread_id="thread_123", assistant_id="asst_456") + ``` + """ + + def __init__(self, http: SyncHttpClient) -> None: + self.http = http + + @overload + def stream( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Iterator[StreamPart]: ... + + @overload + def stream( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + webhook: str | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Iterator[StreamPart]: ... + + def stream( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, # deprecated + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + feedback_keys: Sequence[str] | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> Iterator[StreamPart]: + """Create a run and stream the results. + + Args: + thread_id: the thread ID to assign to the thread. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to stream from. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: The command to execute. + stream_mode: The stream mode(s) to use. + stream_subgraphs: Whether to stream output from subgraphs. + stream_resumable: Whether the stream is considered resumable. + If true, the stream can be resumed and replayed in its entirety even after disconnection. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + feedback_keys: Feedback keys to assign to run. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + headers: Optional custom headers to include with the request. + on_run_created: Optional callback to call when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + + Returns: + Iterator of stream results. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + async for chunk in client.runs.stream( + thread_id=None, + assistant_id="agent", + input={"messages": [{"role": "user", "content": "how are you?"}]}, + stream_mode=["values","debug"], + metadata={"name":"my_run"}, + context={"model_name": "anthropic"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + feedback_keys=["my_feedback_key_1","my_feedback_key_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ): + print(chunk) + ``` + ```shell + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ + + StreamPart(event='metadata', data={'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2'}) + StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}]}) + StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}, {'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}) + StreamPart(event='end', data=None) + ``` + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "config": config, + "context": context, + "metadata": metadata, + "stream_mode": stream_mode, + "stream_subgraphs": stream_subgraphs, + "stream_resumable": stream_resumable, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "feedback_keys": feedback_keys, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "checkpoint_during": checkpoint_during, + "multitask_strategy": multitask_strategy, + "if_not_exists": if_not_exists, + "on_disconnect": on_disconnect, + "on_completion": on_completion, + "after_seconds": after_seconds, + "durability": durability, + } + endpoint = ( + f"/threads/{thread_id}/runs/stream" + if thread_id is not None + else "/runs/stream" + ) + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + return self.http.stream( + endpoint, + "POST", + json={k: v for k, v in payload.items() if v is not None}, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + + @overload + def create( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Run: ... + + @overload + def create( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> Run: ... + + def create( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + stream_mode: StreamMode | Sequence[StreamMode] = "values", + stream_subgraphs: bool = False, + stream_resumable: bool = False, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, # deprecated + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + on_completion: OnCompletionBehavior | None = None, + after_seconds: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> Run: + """Create a background run. + + Args: + thread_id: the thread ID to assign to the thread. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to stream from. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: The command to execute. + stream_mode: The stream mode(s) to use. + stream_subgraphs: Whether to stream output from subgraphs. + stream_resumable: Whether the stream is considered resumable. + If true, the stream can be resumed and replayed in its entirety even after disconnection. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + headers: Optional custom headers to include with the request. + on_run_created: Optional callback to call when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + Returns: + The created background `Run`. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + background_run = client.runs.create( + thread_id="my_thread_id", + assistant_id="my_assistant_id", + input={"messages": [{"role": "user", "content": "hello!"}]}, + metadata={"name":"my_run"}, + context={"model_name": "openai"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ) + print(background_run) + ``` + + ```shell + -------------------------------------------------------------------------------- + + { + 'run_id': 'my_run_id', + 'thread_id': 'my_thread_id', + 'assistant_id': 'my_assistant_id', + 'created_at': '2024-07-25T15:35:42.598503+00:00', + 'updated_at': '2024-07-25T15:35:42.598503+00:00', + 'metadata': {}, + 'status': 'pending', + 'kwargs': + { + 'input': + { + 'messages': [ + { + 'role': 'user', + 'content': 'how are you?' + } + ] + }, + 'config': + { + 'metadata': + { + 'created_by': 'system' + }, + 'configurable': + { + 'run_id': 'my_run_id', + 'user_id': None, + 'graph_id': 'agent', + 'thread_id': 'my_thread_id', + 'checkpoint_id': None, + 'assistant_id': 'my_assistant_id' + } + }, + 'context': + { + 'model_name': 'openai' + }, + 'webhook': "https://my.fake.webhook.com", + 'temporary': False, + 'stream_mode': ['values'], + 'feedback_keys': None, + 'interrupt_after': ["node_to_stop_after_1","node_to_stop_after_2"], + 'interrupt_before': ["node_to_stop_before_1","node_to_stop_before_2"] + }, + 'multitask_strategy': 'interrupt' + } + ``` + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "stream_mode": stream_mode, + "stream_subgraphs": stream_subgraphs, + "stream_resumable": stream_resumable, + "config": config, + "context": context, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "checkpoint_during": checkpoint_during, + "multitask_strategy": multitask_strategy, + "if_not_exists": if_not_exists, + "on_completion": on_completion, + "after_seconds": after_seconds, + "durability": durability, + } + payload = {k: v for k, v in payload.items() if v is not None} + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + return self.http.post( + f"/threads/{thread_id}/runs" if thread_id else "/runs", + json=payload, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + + def create_batch( + self, + payloads: list[RunCreate], + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Run]: + """Create a batch of stateless background runs.""" + + def filter_payload(payload: RunCreate): + return {k: v for k, v in payload.items() if v is not None} + + filtered = [filter_payload(payload) for payload in payloads] + return self.http.post( + "/runs/batch", json=filtered, headers=headers, params=params + ) + + @overload + def wait( + self, + thread_id: str, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> list[dict] | dict[str, Any]: ... + + @overload + def wait( + self, + thread_id: None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + ) -> list[dict] | dict[str, Any]: ... + + def wait( + self, + thread_id: str | None, + assistant_id: str, + *, + input: Input | None = None, + command: Command | None = None, + metadata: Mapping[str, Any] | None = None, + config: Config | None = None, + context: Context | None = None, + checkpoint_during: bool | None = None, # deprecated + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, + interrupt_before: All | Sequence[str] | None = None, + interrupt_after: All | Sequence[str] | None = None, + webhook: str | None = None, + on_disconnect: DisconnectMode | None = None, + on_completion: OnCompletionBehavior | None = None, + multitask_strategy: MultitaskStrategy | None = None, + if_not_exists: IfNotExists | None = None, + after_seconds: int | None = None, + raise_error: bool = True, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + on_run_created: Callable[[RunCreateMetadata], None] | None = None, + durability: Durability | None = None, + ) -> list[dict] | dict[str, Any]: + """Create a run, wait until it finishes and return the final state. + + Args: + thread_id: the thread ID to create the run on. + If `None` will create a stateless run. + assistant_id: The assistant ID or graph name to run. + If using graph name, will default to first assistant created from that graph. + input: The input to the graph. + command: The command to execute. + metadata: Metadata to assign to the run. + config: The configuration for the assistant. + context: Static context to add to the assistant. + !!! version-added "Added in version 0.6.0" + checkpoint: The checkpoint to resume from. + checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). + interrupt_before: Nodes to interrupt immediately before they get executed. + interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. + webhook: Webhook to call after LangGraph API call is done. + on_disconnect: The disconnect mode to use. + Must be one of 'cancel' or 'continue'. + on_completion: Whether to delete or keep the thread created for a stateless run. + Must be one of 'delete' or 'keep'. + multitask_strategy: Multitask strategy to use. + Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. + if_not_exists: How to handle missing thread. Defaults to 'reject'. + Must be either 'reject' (raise error if missing), or 'create' (create new thread). + after_seconds: The number of seconds to wait before starting the run. + Use to schedule future runs. + raise_error: Whether to raise an error if the run fails. + headers: Optional custom headers to include with the request. + on_run_created: Optional callback to call when a run is created. + durability: The durability to use for the run. Values are "sync", "async", or "exit". + "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True + "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False + "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps + + Returns: + The output of the `Run`. + + ???+ example "Example Usage" + + ```python + + final_state_of_run = client.runs.wait( + thread_id=None, + assistant_id="agent", + input={"messages": [{"role": "user", "content": "how are you?"}]}, + metadata={"name":"my_run"}, + context={"model_name": "anthropic"}, + interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], + interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], + webhook="https://my.fake.webhook.com", + multitask_strategy="interrupt" + ) + print(final_state_of_run) + ``` + + ```shell + + ------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'messages': [ + { + 'content': 'how are you?', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': 'f51a862c-62fe-4866-863b-b0863e8ad78a', + 'example': False + }, + { + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-bf1cd3c6-768f-4c16-b62d-ba6f17ad8b36', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + } + ``` + + """ + if checkpoint_during is not None: + warnings.warn( + "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", + DeprecationWarning, + stacklevel=2, + ) + payload = { + "input": input, + "command": ( + {k: v for k, v in command.items() if v is not None} if command else None + ), + "config": config, + "context": context, + "metadata": metadata, + "assistant_id": assistant_id, + "interrupt_before": interrupt_before, + "interrupt_after": interrupt_after, + "webhook": webhook, + "checkpoint": checkpoint, + "checkpoint_id": checkpoint_id, + "multitask_strategy": multitask_strategy, + "if_not_exists": if_not_exists, + "on_disconnect": on_disconnect, + "checkpoint_during": checkpoint_during, + "on_completion": on_completion, + "after_seconds": after_seconds, + "raise_error": raise_error, + "durability": durability, + } + + def on_response(res: httpx.Response): + """Callback function to handle the response.""" + if on_run_created and (metadata := _get_run_metadata_from_response(res)): + on_run_created(metadata) + + endpoint = ( + f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" + ) + return self.http.request_reconnect( + endpoint, + "POST", + json={k: v for k, v in payload.items() if v is not None}, + params=params, + headers=headers, + on_response=on_response if on_run_created else None, + ) + + def list( + self, + thread_id: str, + *, + limit: int = 10, + offset: int = 0, + status: RunStatus | None = None, + select: list[RunSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Run]: + """List runs. + + Args: + thread_id: The thread ID to list runs for. + limit: The maximum number of results to return. + offset: The number of results to skip. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The runs for the thread. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.runs.list( + thread_id="thread_id", + limit=5, + offset=5, + ) + ``` + + """ + query_params: dict[str, Any] = {"limit": limit, "offset": offset} + if status is not None: + query_params["status"] = status + if select: + query_params["select"] = select + if params: + query_params.update(params) + return self.http.get( + f"/threads/{thread_id}/runs", params=query_params, headers=headers + ) + + def get( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Run: + """Get a run. + + Args: + thread_id: The thread ID to get. + run_id: The run ID to get. + headers: Optional custom headers to include with the request. + + Returns: + `Run` object. + + ???+ example "Example Usage" + + ```python + + run = client.runs.get( + thread_id="thread_id_to_delete", + run_id="run_id_to_delete", + ) + ``` + """ + + return self.http.get( + f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params + ) + + def cancel( + self, + thread_id: str, + run_id: str, + *, + wait: bool = False, + action: CancelAction = "interrupt", + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Get a run. + + Args: + thread_id: The thread ID to cancel. + run_id: The run ID to cancel. + wait: Whether to wait until run has completed. + action: Action to take when cancelling the run. Possible values + are `interrupt` or `rollback`. Default is `interrupt`. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.runs.cancel( + thread_id="thread_id_to_cancel", + run_id="run_id_to_cancel", + wait=True, + action="interrupt" + ) + ``` + + """ + query_params = { + "wait": 1 if wait else 0, + "action": action, + } + if params: + query_params.update(params) + if wait: + return self.http.request_reconnect( + f"/threads/{thread_id}/runs/{run_id}/cancel", + "POST", + json=None, + params=query_params, + headers=headers, + ) + return self.http.post( + f"/threads/{thread_id}/runs/{run_id}/cancel", + json=None, + params=query_params, + headers=headers, + ) + + def join( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> dict: + """Block until a run is done. Returns the final state of the thread. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.runs.join( + thread_id="thread_id_to_join", + run_id="run_id_to_join" + ) + ``` + + """ + return self.http.request_reconnect( + f"/threads/{thread_id}/runs/{run_id}/join", + "GET", + headers=headers, + params=params, + ) + + def join_stream( + self, + thread_id: str, + run_id: str, + *, + cancel_on_disconnect: bool = False, + stream_mode: StreamMode | Sequence[StreamMode] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + last_event_id: str | None = None, + ) -> Iterator[StreamPart]: + """Stream output from a run in real-time, until the run is done. + Output is not buffered, so any output produced before this call will + not be received here. + + Args: + thread_id: The thread ID to join. + run_id: The run ID to join. + stream_mode: The stream mode(s) to use. Must be a subset of the stream modes passed + when creating the run. Background runs default to having the union of all + stream modes. + cancel_on_disconnect: Whether to cancel the run when the stream is disconnected. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + last_event_id: The last event ID to use for the stream. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.runs.join_stream( + thread_id="thread_id_to_join", + run_id="run_id_to_join", + stream_mode=["values", "debug"] + ) + ``` + + """ + query_params = { + "stream_mode": stream_mode, + "cancel_on_disconnect": cancel_on_disconnect, + } + if params: + query_params.update(params) + return self.http.stream( + f"/threads/{thread_id}/runs/{run_id}/stream", + "GET", + params=query_params, + headers={ + **({"Last-Event-ID": last_event_id} if last_event_id else {}), + **(headers or {}), + } + or None, + ) + + def delete( + self, + thread_id: str, + run_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a run. + + Args: + thread_id: The thread ID to delete. + run_id: The run ID to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.runs.delete( + thread_id="thread_id_to_delete", + run_id="run_id_to_delete" + ) + ``` + + """ + self.http.delete( + f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params + ) diff --git a/libs/sdk-py/langgraph_sdk/_sync/store.py b/libs/sdk-py/langgraph_sdk/_sync/store.py new file mode 100644 index 000000000..ae9bf4134 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/store.py @@ -0,0 +1,313 @@ +"""Synchronous store client for LangGraph SDK.""" + +from __future__ import annotations + +from collections.abc import Mapping, Sequence +from typing import Any, Literal + +from langgraph_sdk._shared.utilities import _provided_vals +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk.schema import ( + Item, + ListNamespaceResponse, + QueryParamTypes, + SearchItemsResponse, +) + + +class SyncStoreClient: + """A client for synchronous operations on a key-value store. + + Provides methods to interact with a remote key-value store, allowing + storage and retrieval of items within namespaced hierarchies. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:2024")) + client.store.put_item(["users", "profiles"], "user123", {"name": "Alice", "age": 30}) + ``` + """ + + def __init__(self, http: SyncHttpClient) -> None: + self.http = http + + def put_item( + self, + namespace: Sequence[str], + /, + key: str, + value: Mapping[str, Any], + index: Literal[False] | list[str] | None = None, + ttl: int | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Store or update an item. + + Args: + namespace: A list of strings representing the namespace path. + key: The unique identifier for the item within the namespace. + value: A dictionary containing the item's data. + index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index. + ttl: Optional time-to-live in minutes for the item, or None for no expiration. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + client.store.put_item( + ["documents", "user123"], + key="item456", + value={"title": "My Document", "content": "Hello World"} + ) + ``` + """ + for label in namespace: + if "." in label: + raise ValueError( + f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." + ) + payload = { + "namespace": namespace, + "key": key, + "value": value, + "index": index, + "ttl": ttl, + } + self.http.put( + "/store/items", json=_provided_vals(payload), headers=headers, params=params + ) + + def get_item( + self, + namespace: Sequence[str], + /, + key: str, + *, + refresh_ttl: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Item: + """Retrieve a single item. + + Args: + key: The unique identifier for the item. + namespace: Optional list of strings representing the namespace path. + refresh_ttl: Whether to refresh the TTL on this read operation. If `None`, uses the store's default behavior. + headers: Optional custom headers to include with the request. + + Returns: + The retrieved item. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + item = client.store.get_item( + ["documents", "user123"], + key="item456", + ) + print(item) + ``` + + ```shell + ---------------------------------------------------------------- + + { + 'namespace': ['documents', 'user123'], + 'key': 'item456', + 'value': {'title': 'My Document', 'content': 'Hello World'}, + 'created_at': '2024-07-30T12:00:00Z', + 'updated_at': '2024-07-30T12:00:00Z' + } + ``` + """ + for label in namespace: + if "." in label: + raise ValueError( + f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." + ) + + query_params = {"key": key, "namespace": ".".join(namespace)} + if refresh_ttl is not None: + query_params["refresh_ttl"] = refresh_ttl + if params: + query_params.update(params) + return self.http.get("/store/items", params=query_params, headers=headers) + + def delete_item( + self, + namespace: Sequence[str], + /, + key: str, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete an item. + + Args: + key: The unique identifier for the item. + namespace: Optional list of strings representing the namespace path. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + client.store.delete_item( + ["documents", "user123"], + key="item456", + ) + ``` + """ + self.http.delete( + "/store/items", + json={"key": key, "namespace": namespace}, + headers=headers, + params=params, + ) + + def search_items( + self, + namespace_prefix: Sequence[str], + /, + filter: Mapping[str, Any] | None = None, + limit: int = 10, + offset: int = 0, + query: str | None = None, + refresh_ttl: bool | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> SearchItemsResponse: + """Search for items within a namespace prefix. + + Args: + namespace_prefix: List of strings representing the namespace prefix. + filter: Optional dictionary of key-value pairs to filter results. + limit: Maximum number of items to return (default is 10). + offset: Number of items to skip before returning results (default is 0). + query: Optional query for natural language search. + refresh_ttl: Whether to refresh the TTL on items returned by this search. If `None`, uses the store's default behavior. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + A list of items matching the search criteria. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + items = client.store.search_items( + ["documents"], + filter={"author": "John Doe"}, + limit=5, + offset=0 + ) + print(items) + ``` + ```shell + ---------------------------------------------------------------- + + { + "items": [ + { + "namespace": ["documents", "user123"], + "key": "item789", + "value": { + "title": "Another Document", + "author": "John Doe" + }, + "created_at": "2024-07-30T12:00:00Z", + "updated_at": "2024-07-30T12:00:00Z" + }, + # ... additional items ... + ] + } + ``` + """ + payload = { + "namespace_prefix": namespace_prefix, + "filter": filter, + "limit": limit, + "offset": offset, + "query": query, + "refresh_ttl": refresh_ttl, + } + return self.http.post( + "/store/items/search", + json=_provided_vals(payload), + headers=headers, + params=params, + ) + + def list_namespaces( + self, + prefix: list[str] | None = None, + suffix: list[str] | None = None, + max_depth: int | None = None, + limit: int = 100, + offset: int = 0, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ListNamespaceResponse: + """List namespaces with optional match conditions. + + Args: + prefix: Optional list of strings representing the prefix to filter namespaces. + suffix: Optional list of strings representing the suffix to filter namespaces. + max_depth: Optional integer specifying the maximum depth of namespaces to return. + limit: Maximum number of namespaces to return (default is 100). + offset: Number of namespaces to skip before returning results (default is 0). + headers: Optional custom headers to include with the request. + + Returns: + A list of namespaces matching the criteria. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:8123") + namespaces = client.store.list_namespaces( + prefix=["documents"], + max_depth=3, + limit=10, + offset=0 + ) + print(namespaces) + ``` + + ```shell + ---------------------------------------------------------------- + + [ + ["documents", "user123", "reports"], + ["documents", "user456", "invoices"], + ... + ] + ``` + """ + payload = { + "prefix": prefix, + "suffix": suffix, + "max_depth": max_depth, + "limit": limit, + "offset": offset, + } + return self.http.post( + "/store/namespaces", + json=_provided_vals(payload), + headers=headers, + params=params, + ) diff --git a/libs/sdk-py/langgraph_sdk/_sync/threads.py b/libs/sdk-py/langgraph_sdk/_sync/threads.py new file mode 100644 index 000000000..1086da193 --- /dev/null +++ b/libs/sdk-py/langgraph_sdk/_sync/threads.py @@ -0,0 +1,654 @@ +"""Synchronous client for managing threads in LangGraph.""" + +from __future__ import annotations + +from collections.abc import Iterator, Mapping, Sequence +from typing import Any + +from langgraph_sdk._sync.http import SyncHttpClient +from langgraph_sdk.schema import ( + Checkpoint, + Json, + OnConflictBehavior, + QueryParamTypes, + SortOrder, + StreamPart, + Thread, + ThreadSelectField, + ThreadSortBy, + ThreadState, + ThreadStatus, + ThreadStreamMode, + ThreadUpdateStateResponse, +) + + +class SyncThreadsClient: + """Synchronous client for managing threads in LangGraph. + + This class provides methods to create, retrieve, and manage threads, + which represent conversations or stateful interactions. + + ???+ example "Example" + + ```python + client = get_sync_client(url="http://localhost:2024") + thread = client.threads.create(metadata={"user_id": "123"}) + ``` + """ + + def __init__(self, http: SyncHttpClient) -> None: + self.http = http + + def get( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Get a thread by ID. + + Args: + thread_id: The ID of the thread to get. + headers: Optional custom headers to include with the request. + + Returns: + `Thread` object. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + thread = client.threads.get( + thread_id="my_thread_id" + ) + print(thread) + ``` + ```shell + ----------------------------------------------------- + + { + 'thread_id': 'my_thread_id', + 'created_at': '2024-07-18T18:35:15.540834+00:00', + 'updated_at': '2024-07-18T18:35:15.540834+00:00', + 'metadata': {'graph_id': 'agent'} + } + ``` + + """ + + return self.http.get(f"/threads/{thread_id}", headers=headers, params=params) + + def create( + self, + *, + metadata: Json = None, + thread_id: str | None = None, + if_exists: OnConflictBehavior | None = None, + supersteps: Sequence[dict[str, Sequence[dict[str, Any]]]] | None = None, + graph_id: str | None = None, + ttl: int | Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Create a new thread. + + Args: + metadata: Metadata to add to thread. + thread_id: ID of thread. + If `None`, ID will be a randomly generated UUID. + if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. + Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread). + supersteps: Apply a list of supersteps when creating a thread, each containing a sequence of updates. + Each update has `values` or `command` and `as_node`. Used for copying a thread between deployments. + graph_id: Optional graph ID to associate with the thread. + ttl: Optional time-to-live in minutes for the thread. You can pass an + integer (minutes) or a mapping with keys `ttl` and optional + `strategy` (defaults to "delete"). + headers: Optional custom headers to include with the request. + + Returns: + The created `Thread`. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + thread = client.threads.create( + metadata={"number":1}, + thread_id="my-thread-id", + if_exists="raise" + ) + ``` + ) + """ + payload: dict[str, Any] = {} + if thread_id: + payload["thread_id"] = thread_id + if metadata or graph_id: + payload["metadata"] = { + **(metadata or {}), + **({"graph_id": graph_id} if graph_id else {}), + } + if if_exists: + payload["if_exists"] = if_exists + if supersteps: + payload["supersteps"] = [ + { + "updates": [ + { + "values": u["values"], + "command": u.get("command"), + "as_node": u["as_node"], + } + for u in s["updates"] + ] + } + for s in supersteps + ] + if ttl is not None: + if isinstance(ttl, (int, float)): + payload["ttl"] = {"ttl": ttl, "strategy": "delete"} + else: + payload["ttl"] = ttl + + return self.http.post("/threads", json=payload, headers=headers, params=params) + + def update( + self, + thread_id: str, + *, + metadata: Mapping[str, Any], + ttl: int | Mapping[str, Any] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Thread: + """Update a thread. + + Args: + thread_id: ID of thread to update. + metadata: Metadata to merge with existing thread metadata. + ttl: Optional time-to-live in minutes for the thread. You can pass an + integer (minutes) or a mapping with keys `ttl` and optional + `strategy` (defaults to "delete"). + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + The created `Thread`. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + thread = client.threads.update( + thread_id="my-thread-id", + metadata={"number":1}, + ttl=43_200, + ) + ``` + """ + payload: dict[str, Any] = {"metadata": metadata} + if ttl is not None: + if isinstance(ttl, (int, float)): + payload["ttl"] = {"ttl": ttl, "strategy": "delete"} + else: + payload["ttl"] = ttl + return self.http.patch( + f"/threads/{thread_id}", + json=payload, + headers=headers, + params=params, + ) + + def delete( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Delete a thread. + + Args: + thread_id: The ID of the thread to delete. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client.threads.delete( + thread_id="my_thread_id" + ) + ``` + + """ + self.http.delete(f"/threads/{thread_id}", headers=headers, params=params) + + def search( + self, + *, + metadata: Json = None, + values: Json = None, + ids: Sequence[str] | None = None, + status: ThreadStatus | None = None, + limit: int = 10, + offset: int = 0, + sort_by: ThreadSortBy | None = None, + sort_order: SortOrder | None = None, + select: list[ThreadSelectField] | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[Thread]: + """Search for threads. + + Args: + metadata: Thread metadata to filter on. + values: State values to filter on. + ids: List of thread IDs to filter by. + status: Thread status to filter on. + Must be one of 'idle', 'busy', 'interrupted' or 'error'. + limit: Limit on number of threads to return. + offset: Offset in threads table to start search from. + headers: Optional custom headers to include with the request. + + Returns: + List of the threads matching the search parameters. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + threads = client.threads.search( + metadata={"number":1}, + status="interrupted", + limit=15, + offset=5 + ) + ``` + """ + payload: dict[str, Any] = { + "limit": limit, + "offset": offset, + } + if metadata: + payload["metadata"] = metadata + if values: + payload["values"] = values + if ids: + payload["ids"] = ids + if status: + payload["status"] = status + if sort_by: + payload["sort_by"] = sort_by + if sort_order: + payload["sort_order"] = sort_order + if select: + payload["select"] = select + return self.http.post( + "/threads/search", json=payload, headers=headers, params=params + ) + + def count( + self, + *, + metadata: Json = None, + values: Json = None, + status: ThreadStatus | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> int: + """Count threads matching filters. + + Args: + metadata: Thread metadata to filter on. + values: State values to filter on. + status: Thread status to filter on. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + int: Number of threads matching the criteria. + """ + payload: dict[str, Any] = {} + if metadata: + payload["metadata"] = metadata + if values: + payload["values"] = values + if status: + payload["status"] = status + return self.http.post( + "/threads/count", json=payload, headers=headers, params=params + ) + + def copy( + self, + thread_id: str, + *, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> None: + """Copy a thread. + + Args: + thread_id: The ID of the thread to copy. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + `None` + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + client.threads.copy( + thread_id="my_thread_id" + ) + ``` + + """ + return self.http.post( + f"/threads/{thread_id}/copy", json=None, headers=headers, params=params + ) + + def get_state( + self, + thread_id: str, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, # deprecated + *, + subgraphs: bool = False, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ThreadState: + """Get the state of a thread. + + Args: + thread_id: The ID of the thread to get the state of. + checkpoint: The checkpoint to get the state of. + subgraphs: Include subgraphs states. + headers: Optional custom headers to include with the request. + + Returns: + The thread of the state. + + ???+ example "Example Usage" + + ```python + client = get_sync_client(url="http://localhost:2024") + thread_state = client.threads.get_state( + thread_id="my_thread_id", + checkpoint_id="my_checkpoint_id" + ) + print(thread_state) + ``` + + ```shell + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'values': { + 'messages': [ + { + 'content': 'how are you?', + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'human', + 'name': None, + 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', + 'example': False + }, + { + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'additional_kwargs': {}, + 'response_metadata': {}, + 'type': 'ai', + 'name': None, + 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', + 'example': False, + 'tool_calls': [], + 'invalid_tool_calls': [], + 'usage_metadata': None + } + ] + }, + 'next': [], + 'checkpoint': + { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1' + } + 'metadata': + { + 'step': 1, + 'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2', + 'source': 'loop', + 'writes': + { + 'agent': + { + 'messages': [ + { + 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', + 'name': None, + 'type': 'ai', + 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", + 'example': False, + 'tool_calls': [], + 'usage_metadata': None, + 'additional_kwargs': {}, + 'response_metadata': {}, + 'invalid_tool_calls': [] + } + ] + } + }, + 'user_id': None, + 'graph_id': 'agent', + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'created_by': 'system', + 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, + 'created_at': '2024-07-25T15:35:44.184703+00:00', + 'parent_config': + { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f' + } + } + ``` + + """ + if checkpoint: + return self.http.post( + f"/threads/{thread_id}/state/checkpoint", + json={"checkpoint": checkpoint, "subgraphs": subgraphs}, + headers=headers, + params=params, + ) + elif checkpoint_id: + get_params = {"subgraphs": subgraphs} + if params: + get_params = {**get_params, **params} + return self.http.get( + f"/threads/{thread_id}/state/{checkpoint_id}", + params=get_params, + headers=headers, + ) + else: + get_params = {"subgraphs": subgraphs} + if params: + get_params = {**get_params, **params} + return self.http.get( + f"/threads/{thread_id}/state", + params=get_params, + headers=headers, + ) + + def update_state( + self, + thread_id: str, + values: dict[str, Any] | Sequence[dict] | None, + *, + as_node: str | None = None, + checkpoint: Checkpoint | None = None, + checkpoint_id: str | None = None, # deprecated + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> ThreadUpdateStateResponse: + """Update the state of a thread. + + Args: + thread_id: The ID of the thread to update. + values: The values to update the state with. + as_node: Update the state as if this node had just executed. + checkpoint: The checkpoint to update the state of. + headers: Optional custom headers to include with the request. + + Returns: + Response after updating a thread's state. + + ???+ example "Example Usage" + + ```python + + response = await client.threads.update_state( + thread_id="my_thread_id", + values={"messages":[{"role": "user", "content": "hello!"}]}, + as_node="my_node", + ) + print(response) + + ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- + + { + 'checkpoint': { + 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', + 'checkpoint_ns': '', + 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1', + 'checkpoint_map': {} + } + } + ``` + + """ + payload: dict[str, Any] = { + "values": values, + } + if checkpoint_id: + payload["checkpoint_id"] = checkpoint_id + if checkpoint: + payload["checkpoint"] = checkpoint + if as_node: + payload["as_node"] = as_node + return self.http.post( + f"/threads/{thread_id}/state", json=payload, headers=headers, params=params + ) + + def get_history( + self, + thread_id: str, + *, + limit: int = 10, + before: str | Checkpoint | None = None, + metadata: Mapping[str, Any] | None = None, + checkpoint: Checkpoint | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> list[ThreadState]: + """Get the state history of a thread. + + Args: + thread_id: The ID of the thread to get the state history for. + checkpoint: Return states for this subgraph. If empty defaults to root. + limit: The maximum number of states to return. + before: Return states before this checkpoint. + metadata: Filter states by metadata key-value pairs. + headers: Optional custom headers to include with the request. + + Returns: + The state history of the `Thread`. + + ???+ example "Example Usage" + + ```python + + thread_state = client.threads.get_history( + thread_id="my_thread_id", + limit=5, + before="my_timestamp", + metadata={"name":"my_name"} + ) + ``` + + """ + payload: dict[str, Any] = { + "limit": limit, + } + if before: + payload["before"] = before + if metadata: + payload["metadata"] = metadata + if checkpoint: + payload["checkpoint"] = checkpoint + return self.http.post( + f"/threads/{thread_id}/history", + json=payload, + headers=headers, + params=params, + ) + + def join_stream( + self, + thread_id: str, + *, + stream_mode: ThreadStreamMode | Sequence[ThreadStreamMode] = "run_modes", + last_event_id: str | None = None, + headers: Mapping[str, str] | None = None, + params: QueryParamTypes | None = None, + ) -> Iterator[StreamPart]: + """Get a stream of events for a thread. + + Args: + thread_id: The ID of the thread to get the stream for. + last_event_id: The ID of the last event to get. + headers: Optional custom headers to include with the request. + params: Optional query parameters to include with the request. + + Returns: + An iterator of stream parts. + + ???+ example "Example Usage" + + ```python + + for chunk in client.threads.join_stream( + thread_id="my_thread_id", + last_event_id="my_event_id", + stream_mode="run_modes", + ): + print(chunk) + ``` + + """ + query_params = { + "stream_mode": stream_mode, + } + if params: + query_params.update(params) + return self.http.stream( + f"/threads/{thread_id}/stream", + "GET", + headers={ + **({"Last-Event-ID": last_event_id} if last_event_id else {}), + **(headers or {}), + }, + params=query_params, + ) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index a0b0a3263..f9ce571e8 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -9,7084 +9,47 @@ Store. from __future__ import annotations -import asyncio -import functools -import logging -import os -import re -import sys -import warnings -from collections.abc import AsyncIterator, Callable, Iterator, Mapping, Sequence -from datetime import datetime -from types import TracebackType -from typing import ( - Any, - Literal, - cast, - overload, -) - -import httpx -import orjson - -import langgraph_sdk -from langgraph_sdk.errors import _araise_for_status_typed, _raise_for_status_typed -from langgraph_sdk.schema import ( - All, - Assistant, - AssistantSelectField, - AssistantSortBy, - AssistantsSearchResponse, - AssistantVersion, - CancelAction, - Checkpoint, - Command, - Config, - Context, - Cron, - CronSelectField, - CronSortBy, - DisconnectMode, - Durability, - GraphSchema, - IfNotExists, - Input, - Item, - Json, - ListNamespaceResponse, - MultitaskStrategy, - OnCompletionBehavior, - OnConflictBehavior, - QueryParamTypes, - Run, - RunCreate, - RunCreateMetadata, - RunSelectField, - RunStatus, - SearchItemsResponse, - SortOrder, - StreamMode, - StreamPart, - Subgraphs, - Thread, - ThreadSelectField, - ThreadSortBy, - ThreadState, - ThreadStatus, - ThreadStreamMode, - ThreadUpdateStateResponse, -) -from langgraph_sdk.sse import SSEDecoder, aiter_lines_raw, iter_lines_raw - -logger = logging.getLogger(__name__) - - -RESERVED_HEADERS = ("x-api-key",) - -NOT_PROVIDED = cast(None, object()) - - -def _get_api_key(api_key: str | None = NOT_PROVIDED) -> str | None: - """Get the API key from the environment. - Precedence: - 1. explicit string argument - 2. LANGGRAPH_API_KEY (if api_key not provided) - 3. LANGSMITH_API_KEY (if api_key not provided) - 4. LANGCHAIN_API_KEY (if api_key not provided) - - Args: - api_key: The API key to use. Can be: - - A string: use this exact API key - - None: explicitly skip loading from environment - - NOT_PROVIDED (default): auto-load from environment variables - """ - if isinstance(api_key, str): - return api_key - if api_key is NOT_PROVIDED: - # api_key is not explicitly provided, try to load from environment - for prefix in ["LANGGRAPH", "LANGSMITH", "LANGCHAIN"]: - if env := os.getenv(f"{prefix}_API_KEY"): - return env.strip().strip('"').strip("'") - # api_key is explicitly None, don't load from environment - return None - - -def _get_headers( - api_key: str | None, - custom_headers: Mapping[str, str] | None, -) -> dict[str, str]: - """Combine api_key and custom user-provided headers.""" - custom_headers = custom_headers or {} - for header in RESERVED_HEADERS: - if header in custom_headers: - raise ValueError(f"Cannot set reserved header '{header}'") - - headers = { - "User-Agent": f"langgraph-sdk-py/{langgraph_sdk.__version__}", - **custom_headers, - } - resolved_api_key = _get_api_key(api_key) - if resolved_api_key: - headers["x-api-key"] = resolved_api_key - - return headers - - -def _orjson_default(obj: Any) -> Any: - is_class = isinstance(obj, type) - if hasattr(obj, "model_dump") and callable(obj.model_dump): - if is_class: - raise TypeError( - f"Cannot JSON-serialize type object: {obj!r}. Did you mean to pass an instance of the object instead?" - f"\nReceived type: {obj!r}" - ) - return obj.model_dump() - elif hasattr(obj, "dict") and callable(obj.dict): - if is_class: - raise TypeError( - f"Cannot JSON-serialize type object: {obj!r}. Did you mean to pass an instance of the object instead?" - f"\nReceived type: {obj!r}" - ) - return obj.dict() - elif isinstance(obj, (set, frozenset)): - return list(obj) - else: - raise TypeError(f"Object of type {type(obj)} is not JSON serializable") - - -# Compiled regex pattern for extracting run metadata from Content-Location header -_RUN_METADATA_PATTERN = re.compile( - r"(\/threads\/(?P.+))?\/runs\/(?P.+)" -) - - -def _get_run_metadata_from_response( - response: httpx.Response, -) -> RunCreateMetadata | None: - """Extract run metadata from the response headers.""" - if (content_location := response.headers.get("Content-Location")) and ( - match := _RUN_METADATA_PATTERN.search(content_location) - ): - return RunCreateMetadata( - run_id=match.group("run_id"), - thread_id=match.group("thread_id") or None, - ) - - return None - - -def get_client( - *, - url: str | None = None, - api_key: str | None = NOT_PROVIDED, - headers: Mapping[str, str] | None = None, - timeout: TimeoutTypes | None = None, -) -> LangGraphClient: - """Create and configure a LangGraphClient. - - The client provides programmatic access to LangSmith Deployment. It supports - both remote servers and local in-process connections (when running inside a LangGraph server). - - Args: - url: - Base URL of the LangGraph API. - - If `None`, the client first attempts an in-process connection via ASGI transport. - If that fails, it defers registration until after app initialization. This - only works if the client is used from within the Agent server. - api_key: - API key for authentication. Can be: - - A string: use this exact API key - - `None`: explicitly skip loading from environment variables - - Not provided (default): auto-load from environment in this order: - 1. `LANGGRAPH_API_KEY` - 2. `LANGSMITH_API_KEY` - 3. `LANGCHAIN_API_KEY` - headers: - Additional HTTP headers to include in requests. Merged with authentication headers. - timeout: - HTTP timeout configuration. May be: - - `httpx.Timeout` instance - - float (total seconds) - - tuple `(connect, read, write, pool)` in seconds - Defaults: connect=5, read=300, write=300, pool=5. - - Returns: - LangGraphClient: - A top-level client exposing sub-clients for assistants, threads, - runs, and cron operations. - - ???+ example "Connect to a remote server:" - - ```python - from langgraph_sdk import get_client - - # get top-level LangGraphClient - client = get_client(url="http://localhost:8123") - - # example usage: client..() - assistants = await client.assistants.get(assistant_id="some_uuid") - ``` - - ???+ example "Connect in-process to a running LangGraph server:" - - ```python - from langgraph_sdk import get_client - - client = get_client(url=None) - - async def my_node(...): - subagent_result = await client.runs.wait( - thread_id=None, - assistant_id="agent", - input={"messages": [{"role": "user", "content": "Foo"}]}, - ) - ``` - - ???+ example "Skip auto-loading API key from environment:" - - ```python - from langgraph_sdk import get_client - - # Don't load API key from environment variables - client = get_client( - url="http://localhost:8123", - api_key=None - ) - ``` - """ - - transport: httpx.AsyncBaseTransport | None = None - if url is None: - url = "http://api" - if os.environ.get("__LANGGRAPH_DEFER_LOOPBACK_TRANSPORT") == "true": - transport = get_asgi_transport()(app=None, root_path="/noauth") - _registered_transports.append(transport) - else: - try: - from langgraph_api.server import app # type: ignore - - transport = get_asgi_transport()(app, root_path="/noauth") - except Exception: - logger.debug( - "Failed to connect to in-process LangGraph server. Deferring configuration.", - exc_info=True, - ) - transport = get_asgi_transport()(app=None, root_path="/noauth") - _registered_transports.append(transport) - - if transport is None: - transport = httpx.AsyncHTTPTransport(retries=5) - client = httpx.AsyncClient( - base_url=url, - transport=transport, - timeout=( - httpx.Timeout(timeout) # ty: ignore[invalid-argument-type] - if timeout is not None - else httpx.Timeout(connect=5, read=300, write=300, pool=5) - ), - headers=_get_headers(api_key, headers), - ) - return LangGraphClient(client) - - -class LangGraphClient: - """Top-level client for LangGraph API. - - Attributes: - assistants: Manages versioned configuration for your graphs. - threads: Handles (potentially) multi-turn interactions, such as conversational threads. - runs: Controls individual invocations of the graph. - crons: Manages scheduled operations. - store: Interfaces with persistent, shared data storage. - """ - - def __init__(self, client: httpx.AsyncClient) -> None: - self.http = HttpClient(client) - self.assistants = AssistantsClient(self.http) - self.threads = ThreadsClient(self.http) - self.runs = RunsClient(self.http) - self.crons = CronClient(self.http) - self.store = StoreClient(self.http) - - async def __aenter__(self) -> LangGraphClient: - """Enter the async context manager.""" - return self - - async def __aexit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Exit the async context manager.""" - await self.aclose() - - async def aclose(self) -> None: - """Close the underlying HTTP client.""" - if hasattr(self, "http"): - await self.http.client.aclose() - - -class HttpClient: - """Handle async requests to the LangGraph API. - - Adds additional error messaging & content handling above the - provided httpx client. - - Attributes: - client (httpx.AsyncClient): Underlying HTTPX async client. - """ - - def __init__(self, client: httpx.AsyncClient) -> None: - self.client = client - - async def get( - self, - path: str, - *, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `GET` request.""" - r = await self.client.get(path, params=params, headers=headers) - if on_response: - on_response(r) - await _araise_for_status_typed(r) - return await _adecode_json(r) - - async def post( - self, - path: str, - *, - json: dict[str, Any] | list | None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `POST` request.""" - if json is not None: - request_headers, content = await _aencode_json(json) - else: - request_headers, content = {}, b"" - # Merge headers, with runtime headers taking precedence - if headers: - request_headers.update(headers) - r = await self.client.post( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - await _araise_for_status_typed(r) - return await _adecode_json(r) - - async def put( - self, - path: str, - *, - json: dict, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `PUT` request.""" - request_headers, content = await _aencode_json(json) - if headers: - request_headers.update(headers) - r = await self.client.put( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - await _araise_for_status_typed(r) - return await _adecode_json(r) - - async def patch( - self, - path: str, - *, - json: dict, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `PATCH` request.""" - request_headers, content = await _aencode_json(json) - if headers: - request_headers.update(headers) - r = await self.client.patch( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - await _araise_for_status_typed(r) - return await _adecode_json(r) - - async def delete( - self, - path: str, - *, - json: Any | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> None: - """Send a `DELETE` request.""" - r = await self.client.request( - "DELETE", path, json=json, params=params, headers=headers - ) - if on_response: - on_response(r) - await _araise_for_status_typed(r) - - async def request_reconnect( - self, - path: str, - method: str, - *, - json: dict[str, Any] | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - reconnect_limit: int = 5, - ) -> Any: - """Send a request that automatically reconnects to Location header.""" - request_headers, content = await _aencode_json(json) - if headers: - request_headers.update(headers) - async with self.client.stream( - method, path, headers=request_headers, content=content, params=params - ) as r: - if on_response: - on_response(r) - try: - r.raise_for_status() - except httpx.HTTPStatusError as e: - body = (await r.aread()).decode() - if sys.version_info >= (3, 11): - e.add_note(body) - else: - logger.error(f"Error from langgraph-api: {body}", exc_info=e) - raise e - loc = r.headers.get("location") - if reconnect_limit <= 0 or not loc: - return await _adecode_json(r) - try: - return await _adecode_json(r) - except httpx.HTTPError: - warnings.warn( - f"Request failed, attempting reconnect to Location: {loc}", - stacklevel=2, - ) - await r.aclose() - return await self.request_reconnect( - loc, - "GET", - headers=request_headers, - # don't pass on_response so it's only called once - reconnect_limit=reconnect_limit - 1, - ) - - async def stream( - self, - path: str, - method: str, - *, - json: dict[str, Any] | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> AsyncIterator[StreamPart]: - """Stream results using SSE.""" - request_headers, content = await _aencode_json(json) - request_headers["Accept"] = "text/event-stream" - request_headers["Cache-Control"] = "no-store" - # Add runtime headers with precedence - if headers: - request_headers.update(headers) - - reconnect_headers = { - key: value - for key, value in request_headers.items() - if key.lower() not in {"content-length", "content-type"} - } - - last_event_id: str | None = None - reconnect_path: str | None = None - reconnect_attempts = 0 - max_reconnect_attempts = 5 - - while True: - current_headers = dict( - request_headers if reconnect_path is None else reconnect_headers - ) - if last_event_id is not None: - current_headers["Last-Event-ID"] = last_event_id - - current_method = method if reconnect_path is None else "GET" - current_content = content if reconnect_path is None else None - current_params = params if reconnect_path is None else None - - retry = False - async with self.client.stream( - current_method, - reconnect_path or path, - headers=current_headers, - content=current_content, - params=current_params, - ) as res: - if reconnect_path is None and on_response: - on_response(res) - # check status - await _araise_for_status_typed(res) - # check content type - content_type = res.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise httpx.TransportError( - "Expected response header Content-Type to contain 'text/event-stream', " - f"got {content_type!r}" - ) - - reconnect_location = res.headers.get("location") - if reconnect_location: - reconnect_path = reconnect_location - - # parse SSE - decoder = SSEDecoder() - try: - async for line in aiter_lines_raw(res): - sse = decoder.decode(line=cast("bytes", line).rstrip(b"\n")) - if sse is not None: - if decoder.last_event_id is not None: - last_event_id = decoder.last_event_id - if sse.event or sse.data is not None: - yield sse - except httpx.HTTPError: - # httpx.TransportError inherits from HTTPError, so transient - # disconnects during streaming land here. - if reconnect_path is None: - raise - retry = True - else: - if sse := decoder.decode(b""): - if decoder.last_event_id is not None: - last_event_id = decoder.last_event_id - if sse.event or sse.data is not None: - # decoder.decode(b"") flushes the in-flight event and may - # return an empty placeholder when there is no pending - # message. Skip these no-op events so the stream doesn't - # emit a trailing blank item after reconnects. - yield sse - if retry: - reconnect_attempts += 1 - if reconnect_attempts > max_reconnect_attempts: - raise httpx.TransportError( - "Exceeded maximum SSE reconnection attempts" - ) - continue - break - - -async def _aencode_json(json: Any) -> tuple[dict[str, str], bytes | None]: - if json is None: - return {}, None - body = await asyncio.get_running_loop().run_in_executor( - None, - orjson.dumps, - json, - _orjson_default, - orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, - ) - content_length = str(len(body)) - content_type = "application/json" - headers = {"Content-Length": content_length, "Content-Type": content_type} - return headers, body - - -async def _adecode_json(r: httpx.Response) -> Any: - body = await r.aread() - return ( - await asyncio.get_running_loop().run_in_executor(None, orjson.loads, body) - if body - else None - ) - - -class AssistantsClient: - """Client for managing assistants in LangGraph. - - This class provides methods to interact with assistants, - which are versioned configurations of your graph. - - ???+ example "Example" - - ```python - client = get_client(url="http://localhost:2024") - assistant = await client.assistants.get("assistant_id_123") - ``` - """ - - def __init__(self, http: HttpClient) -> None: - self.http = http - - async def get( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Get an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Assistant: Assistant Object. - - ???+ example "Example Usage" - - ```python - assistant = await client.assistants.get( - assistant_id="my_assistant_id" - ) - print(assistant) - ``` - - ```shell - ---------------------------------------------------- - - { - 'assistant_id': 'my_assistant_id', - 'graph_id': 'agent', - 'created_at': '2024-06-25T17:10:33.109781+00:00', - 'updated_at': '2024-06-25T17:10:33.109781+00:00', - 'config': {}, - 'metadata': {'created_by': 'system'}, - 'version': 1, - 'name': 'my_assistant' - } - ``` - """ - return await self.http.get( - f"/assistants/{assistant_id}", headers=headers, params=params - ) - - async def get_graph( - self, - assistant_id: str, - *, - xray: int | bool = False, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> dict[str, list[dict[str, Any]]]: - """Get the graph of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the graph of. - xray: Include graph representation of subgraphs. If an integer value is provided, only subgraphs with a depth less than or equal to the value will be included. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Graph: The graph information for the assistant in JSON format. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - graph_info = await client.assistants.get_graph( - assistant_id="my_assistant_id" - ) - print(graph_info) - ``` - - ```shell - - -------------------------------------------------------------------------------------------------------------------------- - - { - 'nodes': - [ - {'id': '__start__', 'type': 'schema', 'data': '__start__'}, - {'id': '__end__', 'type': 'schema', 'data': '__end__'}, - {'id': 'agent','type': 'runnable','data': {'id': ['langgraph', 'utils', 'RunnableCallable'],'name': 'agent'}}, - ], - 'edges': - [ - {'source': '__start__', 'target': 'agent'}, - {'source': 'agent','target': '__end__'} - ] - } - ``` - - - """ - query_params = {"xray": xray} - if params: - query_params.update(params) - - return await self.http.get( - f"/assistants/{assistant_id}/graph", params=query_params, headers=headers - ) - - async def get_schemas( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> GraphSchema: - """Get the schemas of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the schema of. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - GraphSchema: The graph schema for the assistant. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - schema = await client.assistants.get_schemas( - assistant_id="my_assistant_id" - ) - print(schema) - ``` - - ```shell - - ---------------------------------------------------------------------------------------------------------------------------- - - { - 'graph_id': 'agent', - 'state_schema': - { - 'title': 'LangGraphInput', - '$ref': '#/definitions/AgentState', - 'definitions': - { - 'BaseMessage': - { - 'title': 'BaseMessage', - 'description': 'Base abstract Message class. Messages are the inputs and outputs of ChatModels.', - 'type': 'object', - 'properties': - { - 'content': - { - 'title': 'Content', - 'anyOf': [ - {'type': 'string'}, - {'type': 'array','items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}} - ] - }, - 'additional_kwargs': - { - 'title': 'Additional Kwargs', - 'type': 'object' - }, - 'response_metadata': - { - 'title': 'Response Metadata', - 'type': 'object' - }, - 'type': - { - 'title': 'Type', - 'type': 'string' - }, - 'name': - { - 'title': 'Name', - 'type': 'string' - }, - 'id': - { - 'title': 'Id', - 'type': 'string' - } - }, - 'required': ['content', 'type'] - }, - 'AgentState': - { - 'title': 'AgentState', - 'type': 'object', - 'properties': - { - 'messages': - { - 'title': 'Messages', - 'type': 'array', - 'items': {'$ref': '#/definitions/BaseMessage'} - } - }, - 'required': ['messages'] - } - } - }, - 'context_schema': - { - 'title': 'Context', - 'type': 'object', - 'properties': - { - 'model_name': - { - 'title': 'Model Name', - 'enum': ['anthropic', 'openai'], - 'type': 'string' - } - } - } - } - ``` - - """ - return await self.http.get( - f"/assistants/{assistant_id}/schemas", headers=headers, params=params - ) - - async def get_subgraphs( - self, - assistant_id: str, - namespace: str | None = None, - recurse: bool = False, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Subgraphs: - """Get the schemas of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the schema of. - namespace: Optional namespace to filter by. - recurse: Whether to recursively get subgraphs. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Subgraphs: The graph schema for the assistant. - - """ - get_params = {"recurse": recurse} - if params: - get_params = {**get_params, **params} - if namespace is not None: - return await self.http.get( - f"/assistants/{assistant_id}/subgraphs/{namespace}", - params=get_params, - headers=headers, - ) - else: - return await self.http.get( - f"/assistants/{assistant_id}/subgraphs", - params=get_params, - headers=headers, - ) - - async def create( - self, - graph_id: str | None, - config: Config | None = None, - *, - context: Context | None = None, - metadata: Json = None, - assistant_id: str | None = None, - if_exists: OnConflictBehavior | None = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - description: str | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Create a new assistant. - - Useful when graph is configurable and you want to create different assistants based on different configurations. - - Args: - graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. - config: Configuration to use for the graph. - metadata: Metadata to add to assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - assistant_id: Assistant ID to use, will default to a random UUID if not provided. - if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. - Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing assistant). - name: The name of the assistant. Defaults to 'Untitled' under the hood. - headers: Optional custom headers to include with the request. - description: Optional description of the assistant. - The description field is available for langgraph-api server version>=0.0.45 - params: Optional query parameters to include with the request. - - Returns: - Assistant: The created assistant. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - assistant = await client.assistants.create( - graph_id="agent", - context={"model_name": "openai"}, - metadata={"number":1}, - assistant_id="my-assistant-id", - if_exists="do_nothing", - name="my_name" - ) - ``` - """ - payload: dict[str, Any] = { - "graph_id": graph_id, - } - if config: - payload["config"] = config - if context: - payload["context"] = context - if metadata: - payload["metadata"] = metadata - if assistant_id: - payload["assistant_id"] = assistant_id - if if_exists: - payload["if_exists"] = if_exists - if name: - payload["name"] = name - if description: - payload["description"] = description - return await self.http.post( - "/assistants", json=payload, headers=headers, params=params - ) - - async def update( - self, - assistant_id: str, - *, - graph_id: str | None = None, - config: Config | None = None, - context: Context | None = None, - metadata: Json = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - description: str | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Update an assistant. - - Use this to point to a different graph, update the configuration, or change the metadata of an assistant. - - Args: - assistant_id: Assistant to update. - graph_id: The ID of the graph the assistant should use. - The graph ID is normally set in your langgraph.json configuration. If `None`, assistant will keep pointing to same graph. - config: Configuration to use for the graph. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - metadata: Metadata to merge with existing assistant metadata. - name: The new name for the assistant. - headers: Optional custom headers to include with the request. - description: Optional description of the assistant. - The description field is available for langgraph-api server version>=0.0.45 - params: Optional query parameters to include with the request. - - Returns: - The updated assistant. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - assistant = await client.assistants.update( - assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', - graph_id="other-graph", - context={"model_name": "anthropic"}, - metadata={"number":2} - ) - ``` - - """ - payload: dict[str, Any] = {} - if graph_id: - payload["graph_id"] = graph_id - if config: - payload["config"] = config - if context: - payload["context"] = context - if metadata: - payload["metadata"] = metadata - if name: - payload["name"] = name - if description: - payload["description"] = description - return await self.http.patch( - f"/assistants/{assistant_id}", - json=payload, - headers=headers, - params=params, - ) - - async def delete( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete an assistant. - - Args: - assistant_id: The assistant ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.assistants.delete( - assistant_id="my_assistant_id" - ) - ``` - - """ - await self.http.delete( - f"/assistants/{assistant_id}", headers=headers, params=params - ) - - @overload - async def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["object"], - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> AssistantsSearchResponse: ... - - @overload - async def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["array"] = "array", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Assistant]: ... - - async def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["array", "object"] = "array", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> AssistantsSearchResponse | list[Assistant]: - """Search for assistants. - - Args: - metadata: Metadata to filter by. Exact match filter for each KV pair. - graph_id: The ID of the graph to filter by. - The graph ID is normally set in your langgraph.json configuration. - name: The name of the assistant to filter by. - The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. - limit: The maximum number of results to return. - offset: The number of results to skip. - sort_by: The field to sort by. - sort_order: The order to sort by. - select: Specific assistant fields to include in the response. - response_format: Controls the response shape. Use ``"array"`` (default) - to return a bare list of assistants, or ``"object"`` to return - a mapping containing assistants plus pagination metadata. - Defaults to "array", though this default will be changed to "object" in a future release. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - A list of assistants (when ``response_format=\"array\"``) or a mapping - with the assistants and the next pagination cursor (when - ``response_format=\"object\"``). - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - response = await client.assistants.search( - metadata = {"name":"my_name"}, - graph_id="my_graph_id", - limit=5, - offset=5, - response_format="object" - ) - next_cursor = response["next"] - assistants = response["assistants"] - ``` - """ - if response_format not in ("array", "object"): - raise ValueError( - f"response_format must be 'array' or 'object', got {response_format!r}" - ) - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - if graph_id: - payload["graph_id"] = graph_id - if name: - payload["name"] = name - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - next_cursor: str | None = None - - def capture_pagination(response: httpx.Response) -> None: - nonlocal next_cursor - next_cursor = response.headers.get("X-Pagination-Next") - - assistants = cast( - list[Assistant], - await self.http.post( - "/assistants/search", - json=payload, - headers=headers, - params=params, - on_response=capture_pagination if response_format == "object" else None, - ), - ) - if response_format == "object": - return {"assistants": assistants, "next": next_cursor} - return assistants - - async def count( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count assistants matching filters. - - Args: - metadata: Metadata to filter by. Exact match for each key/value. - graph_id: Optional graph id to filter by. - name: Optional name to filter by. - The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of assistants matching the criteria. - """ - payload: dict[str, Any] = {} - if metadata: - payload["metadata"] = metadata - if graph_id: - payload["graph_id"] = graph_id - if name: - payload["name"] = name - return await self.http.post( - "/assistants/count", json=payload, headers=headers, params=params - ) - - async def get_versions( - self, - assistant_id: str, - metadata: Json = None, - limit: int = 10, - offset: int = 0, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[AssistantVersion]: - """List all versions of an assistant. - - Args: - assistant_id: The assistant ID to get versions for. - metadata: Metadata to filter versions by. Exact match filter for each KV pair. - limit: The maximum number of versions to return. - offset: The number of versions to skip. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - A list of assistant versions. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - assistant_versions = await client.assistants.get_versions( - assistant_id="my_assistant_id" - ) - ``` - """ - - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - return await self.http.post( - f"/assistants/{assistant_id}/versions", - json=payload, - headers=headers, - params=params, - ) - - async def set_latest( - self, - assistant_id: str, - version: int, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Change the version of an assistant. - - Args: - assistant_id: The assistant ID to delete. - version: The version to change to. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Assistant Object. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - new_version_assistant = await client.assistants.set_latest( - assistant_id="my_assistant_id", - version=3 - ) - ``` - - """ - - payload: dict[str, Any] = {"version": version} - - return await self.http.post( - f"/assistants/{assistant_id}/latest", - json=payload, - headers=headers, - params=params, - ) - - -class ThreadsClient: - """Client for managing threads in LangGraph. - - A thread maintains the state of a graph across multiple interactions/invocations (aka runs). - It accumulates and persists the graph's state, allowing for continuity between separate - invocations of the graph. - - ???+ example "Example" - - ```python - client = get_client(url="http://localhost:2024")) - new_thread = await client.threads.create(metadata={"user_id": "123"}) - ``` - """ - - def __init__(self, http: HttpClient) -> None: - self.http = http - - async def get( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Get a thread by ID. - - Args: - thread_id: The ID of the thread to get. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Thread object. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - thread = await client.threads.get( - thread_id="my_thread_id" - ) - print(thread) - ``` - - ```shell - ----------------------------------------------------- - - { - 'thread_id': 'my_thread_id', - 'created_at': '2024-07-18T18:35:15.540834+00:00', - 'updated_at': '2024-07-18T18:35:15.540834+00:00', - 'metadata': {'graph_id': 'agent'} - } - ``` - - """ - - return await self.http.get( - f"/threads/{thread_id}", headers=headers, params=params - ) - - async def create( - self, - *, - metadata: Json = None, - thread_id: str | None = None, - if_exists: OnConflictBehavior | None = None, - supersteps: Sequence[dict[str, Sequence[dict[str, Any]]]] | None = None, - graph_id: str | None = None, - ttl: int | Mapping[str, Any] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Create a new thread. - - Args: - metadata: Metadata to add to thread. - thread_id: ID of thread. - If `None`, ID will be a randomly generated UUID. - if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. - Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread). - supersteps: Apply a list of supersteps when creating a thread, each containing a sequence of updates. - Each update has `values` or `command` and `as_node`. Used for copying a thread between deployments. - graph_id: Optional graph ID to associate with the thread. - ttl: Optional time-to-live in minutes for the thread. You can pass an - integer (minutes) or a mapping with keys `ttl` and optional - `strategy` (defaults to "delete"). - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The created thread. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - thread = await client.threads.create( - metadata={"number":1}, - thread_id="my-thread-id", - if_exists="raise" - ) - ``` - """ - payload: dict[str, Any] = {} - if thread_id: - payload["thread_id"] = thread_id - if metadata or graph_id: - payload["metadata"] = { - **(metadata or {}), - **({"graph_id": graph_id} if graph_id else {}), - } - if if_exists: - payload["if_exists"] = if_exists - if supersteps: - payload["supersteps"] = [ - { - "updates": [ - { - "values": u["values"], - "command": u.get("command"), - "as_node": u["as_node"], - } - for u in s["updates"] - ] - } - for s in supersteps - ] - if ttl is not None: - if isinstance(ttl, (int, float)): - payload["ttl"] = {"ttl": ttl, "strategy": "delete"} - else: - payload["ttl"] = ttl - - return await self.http.post( - "/threads", json=payload, headers=headers, params=params - ) - - async def update( - self, - thread_id: str, - *, - metadata: Mapping[str, Any], - ttl: int | Mapping[str, Any] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Update a thread. - - Args: - thread_id: ID of thread to update. - metadata: Metadata to merge with existing thread metadata. - ttl: Optional time-to-live in minutes for the thread. You can pass an - integer (minutes) or a mapping with keys `ttl` and optional - `strategy` (defaults to "delete"). - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The created thread. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - thread = await client.threads.update( - thread_id="my-thread-id", - metadata={"number":1}, - ttl=43_200, - ) - ``` - """ - payload: dict[str, Any] = {"metadata": metadata} - if ttl is not None: - if isinstance(ttl, (int, float)): - payload["ttl"] = {"ttl": ttl, "strategy": "delete"} - else: - payload["ttl"] = ttl - return await self.http.patch( - f"/threads/{thread_id}", - json=payload, - headers=headers, - params=params, - ) - - async def delete( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a thread. - - Args: - thread_id: The ID of the thread to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost2024) - await client.threads.delete( - thread_id="my_thread_id" - ) - ``` - - """ - await self.http.delete(f"/threads/{thread_id}", headers=headers, params=params) - - async def search( - self, - *, - metadata: Json = None, - values: Json = None, - ids: Sequence[str] | None = None, - status: ThreadStatus | None = None, - limit: int = 10, - offset: int = 0, - sort_by: ThreadSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[ThreadSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Thread]: - """Search for threads. - - Args: - metadata: Thread metadata to filter on. - values: State values to filter on. - ids: List of thread IDs to filter by. - status: Thread status to filter on. - Must be one of 'idle', 'busy', 'interrupted' or 'error'. - limit: Limit on number of threads to return. - offset: Offset in threads table to start search from. - sort_by: Sort by field. - sort_order: Sort order. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - List of the threads matching the search parameters. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - threads = await client.threads.search( - metadata={"number":1}, - status="interrupted", - limit=15, - offset=5 - ) - ``` - - """ - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - if values: - payload["values"] = values - if ids: - payload["ids"] = ids - if status: - payload["status"] = status - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - return await self.http.post( - "/threads/search", - json=payload, - headers=headers, - params=params, - ) - - async def count( - self, - *, - metadata: Json = None, - values: Json = None, - status: ThreadStatus | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count threads matching filters. - - Args: - metadata: Thread metadata to filter on. - values: State values to filter on. - status: Thread status to filter on. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of threads matching the criteria. - """ - payload: dict[str, Any] = {} - if metadata: - payload["metadata"] = metadata - if values: - payload["values"] = values - if status: - payload["status"] = status - return await self.http.post( - "/threads/count", json=payload, headers=headers, params=params - ) - - async def copy( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Copy a thread. - - Args: - thread_id: The ID of the thread to copy. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024) - await client.threads.copy( - thread_id="my_thread_id" - ) - ``` - - """ - return await self.http.post( - f"/threads/{thread_id}/copy", json=None, headers=headers, params=params - ) - - async def get_state( - self, - thread_id: str, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, # deprecated - *, - subgraphs: bool = False, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ThreadState: - """Get the state of a thread. - - Args: - thread_id: The ID of the thread to get the state of. - checkpoint: The checkpoint to get the state of. - checkpoint_id: (deprecated) The checkpoint ID to get the state of. - subgraphs: Include subgraphs states. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The thread of the state. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024) - thread_state = await client.threads.get_state( - thread_id="my_thread_id", - checkpoint_id="my_checkpoint_id" - ) - print(thread_state) - ``` - - ```shell - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'values': { - 'messages': [ - { - 'content': 'how are you?', - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'human', - 'name': None, - 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', - 'example': False - }, - { - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'ai', - 'name': None, - 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', - 'example': False, - 'tool_calls': [], - 'invalid_tool_calls': [], - 'usage_metadata': None - } - ] - }, - 'next': [], - 'checkpoint': - { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1' - } - 'metadata': - { - 'step': 1, - 'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2', - 'source': 'loop', - 'writes': - { - 'agent': - { - 'messages': [ - { - 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', - 'name': None, - 'type': 'ai', - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'example': False, - 'tool_calls': [], - 'usage_metadata': None, - 'additional_kwargs': {}, - 'response_metadata': {}, - 'invalid_tool_calls': [] - } - ] - } - }, - 'user_id': None, - 'graph_id': 'agent', - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'created_by': 'system', - 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, - 'created_at': '2024-07-25T15:35:44.184703+00:00', - 'parent_config': - { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f' - } - } - ``` - """ - if checkpoint: - return await self.http.post( - f"/threads/{thread_id}/state/checkpoint", - json={"checkpoint": checkpoint, "subgraphs": subgraphs}, - headers=headers, - params=params, - ) - elif checkpoint_id: - get_params = {"subgraphs": subgraphs} - if params: - get_params = {**get_params, **params} - return await self.http.get( - f"/threads/{thread_id}/state/{checkpoint_id}", - params=get_params, - headers=headers, - ) - else: - get_params = {"subgraphs": subgraphs} - if params: - get_params = {**get_params, **params} - return await self.http.get( - f"/threads/{thread_id}/state", - params=get_params, - headers=headers, - ) - - async def update_state( - self, - thread_id: str, - values: dict[str, Any] | Sequence[dict] | None, - *, - as_node: str | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, # deprecated - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ThreadUpdateStateResponse: - """Update the state of a thread. - - Args: - thread_id: The ID of the thread to update. - values: The values to update the state with. - as_node: Update the state as if this node had just executed. - checkpoint: The checkpoint to update the state of. - checkpoint_id: (deprecated) The checkpoint ID to update the state of. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Response after updating a thread's state. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024) - response = await client.threads.update_state( - thread_id="my_thread_id", - values={"messages":[{"role": "user", "content": "hello!"}]}, - as_node="my_node", - ) - print(response) - ``` - ```shell - - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'checkpoint': { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1', - 'checkpoint_map': {} - } - } - ``` - """ - payload: dict[str, Any] = { - "values": values, - } - if checkpoint_id: - payload["checkpoint_id"] = checkpoint_id - if checkpoint: - payload["checkpoint"] = checkpoint - if as_node: - payload["as_node"] = as_node - return await self.http.post( - f"/threads/{thread_id}/state", json=payload, headers=headers, params=params - ) - - async def get_history( - self, - thread_id: str, - *, - limit: int = 10, - before: str | Checkpoint | None = None, - metadata: Mapping[str, Any] | None = None, - checkpoint: Checkpoint | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[ThreadState]: - """Get the state history of a thread. - - Args: - thread_id: The ID of the thread to get the state history for. - checkpoint: Return states for this subgraph. If empty defaults to root. - limit: The maximum number of states to return. - before: Return states before this checkpoint. - metadata: Filter states by metadata key-value pairs. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The state history of the thread. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024) - thread_state = await client.threads.get_history( - thread_id="my_thread_id", - limit=5, - ) - ``` - - """ - payload: dict[str, Any] = { - "limit": limit, - } - if before: - payload["before"] = before - if metadata: - payload["metadata"] = metadata - if checkpoint: - payload["checkpoint"] = checkpoint - return await self.http.post( - f"/threads/{thread_id}/history", - json=payload, - headers=headers, - params=params, - ) - - async def join_stream( - self, - thread_id: str, - *, - last_event_id: str | None = None, - stream_mode: ThreadStreamMode | Sequence[ThreadStreamMode] = "run_modes", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> AsyncIterator[StreamPart]: - """Get a stream of events for a thread. - - Args: - thread_id: The ID of the thread to get the stream for. - last_event_id: The ID of the last event to get. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - An iterator of stream parts. - - ???+ example "Example Usage" - - ```python - - for chunk in client.threads.join_stream( - thread_id="my_thread_id", - last_event_id="my_event_id", - ): - print(chunk) - ``` - - """ - query_params = { - "stream_mode": stream_mode, - } - if params: - query_params.update(params) - return self.http.stream( - f"/threads/{thread_id}/stream", - "GET", - headers={ - **({"Last-Event-ID": last_event_id} if last_event_id else {}), - **(headers or {}), - }, - params=query_params, - ) - - -class RunsClient: - """Client for managing runs in LangGraph. - - A run is a single assistant invocation with optional input, config, context, and metadata. - This client manages runs, which can be stateful (on threads) or stateless. - - ???+ example "Example" - - ```python - client = get_client(url="http://localhost:2024") - run = await client.runs.create(assistant_id="asst_123", thread_id="thread_456", input={"query": "Hello"}) - ``` - """ - - def __init__(self, http: HttpClient) -> None: - self.http = http - - @overload - def stream( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> AsyncIterator[StreamPart]: ... - - @overload - def stream( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - webhook: str | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> AsyncIterator[StreamPart]: ... - - def stream( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, # deprecated - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> AsyncIterator[StreamPart]: - """Create a run and stream the results. - - Args: - thread_id: the thread ID to assign to the thread. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to stream from. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: A command to execute. Cannot be combined with input. - stream_mode: The stream mode(s) to use. - stream_subgraphs: Whether to stream output from subgraphs. - stream_resumable: Whether the stream is considered resumable. - If true, the stream can be resumed and replayed in its entirety even after disconnection. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - feedback_keys: Feedback keys to assign to run. - on_disconnect: The disconnect mode to use. - Must be one of 'cancel' or 'continue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - on_run_created: Callback when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - Returns: - Asynchronous iterator of stream results. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024) - async for chunk in client.runs.stream( - thread_id=None, - assistant_id="agent", - input={"messages": [{"role": "user", "content": "how are you?"}]}, - stream_mode=["values","debug"], - metadata={"name":"my_run"}, - context={"model_name": "anthropic"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - feedback_keys=["my_feedback_key_1","my_feedback_key_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ): - print(chunk) - ``` - - ```shell - - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - - StreamPart(event='metadata', data={'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2'}) - StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}]}) - StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}, {'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}) - StreamPart(event='end', data=None) - ``` - - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "config": config, - "context": context, - "metadata": metadata, - "stream_mode": stream_mode, - "stream_subgraphs": stream_subgraphs, - "stream_resumable": stream_resumable, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "feedback_keys": feedback_keys, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "checkpoint_during": checkpoint_during, - "multitask_strategy": multitask_strategy, - "if_not_exists": if_not_exists, - "on_disconnect": on_disconnect, - "on_completion": on_completion, - "after_seconds": after_seconds, - "durability": durability, - } - endpoint = ( - f"/threads/{thread_id}/runs/stream" - if thread_id is not None - else "/runs/stream" - ) - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - return self.http.stream( - endpoint, - "POST", - json={k: v for k, v in payload.items() if v is not None}, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - - @overload - async def create( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - checkpoint_during: bool | None = None, - config: Config | None = None, - context: Context | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Run: ... - - @overload - async def create( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Run: ... - - async def create( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, # deprecated - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - on_completion: OnCompletionBehavior | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> Run: - """Create a background run. - - Args: - thread_id: the thread ID to assign to the thread. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to stream from. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: A command to execute. Cannot be combined with input. - stream_mode: The stream mode(s) to use. - stream_subgraphs: Whether to stream output from subgraphs. - stream_resumable: Whether the stream is considered resumable. - If true, the stream can be resumed and replayed in its entirety even after disconnection. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - headers: Optional custom headers to include with the request. - on_run_created: Optional callback to call when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - Returns: - The created background run. - - ???+ example "Example Usage" - - ```python - - background_run = await client.runs.create( - thread_id="my_thread_id", - assistant_id="my_assistant_id", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ) - print(background_run) - ``` - - ```shell - -------------------------------------------------------------------------------- - - { - 'run_id': 'my_run_id', - 'thread_id': 'my_thread_id', - 'assistant_id': 'my_assistant_id', - 'created_at': '2024-07-25T15:35:42.598503+00:00', - 'updated_at': '2024-07-25T15:35:42.598503+00:00', - 'metadata': {}, - 'status': 'pending', - 'kwargs': - { - 'input': - { - 'messages': [ - { - 'role': 'user', - 'content': 'how are you?' - } - ] - }, - 'config': - { - 'metadata': - { - 'created_by': 'system' - }, - 'configurable': - { - 'run_id': 'my_run_id', - 'user_id': None, - 'graph_id': 'agent', - 'thread_id': 'my_thread_id', - 'checkpoint_id': None, - 'assistant_id': 'my_assistant_id' - }, - }, - 'context': - { - 'model_name': 'openai' - } - 'webhook': "https://my.fake.webhook.com", - 'temporary': False, - 'stream_mode': ['values'], - 'feedback_keys': None, - 'interrupt_after': ["node_to_stop_after_1","node_to_stop_after_2"], - 'interrupt_before': ["node_to_stop_before_1","node_to_stop_before_2"] - }, - 'multitask_strategy': 'interrupt' - } - ``` - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "stream_mode": stream_mode, - "stream_subgraphs": stream_subgraphs, - "stream_resumable": stream_resumable, - "config": config, - "context": context, - "metadata": metadata, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "checkpoint_during": checkpoint_during, - "multitask_strategy": multitask_strategy, - "if_not_exists": if_not_exists, - "on_completion": on_completion, - "after_seconds": after_seconds, - "durability": durability, - } - payload = {k: v for k, v in payload.items() if v is not None} - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - return await self.http.post( - f"/threads/{thread_id}/runs" if thread_id else "/runs", - json=payload, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - - async def create_batch( - self, - payloads: list[RunCreate], - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Run]: - """Create a batch of stateless background runs.""" - - def filter_payload(payload: RunCreate): - return {k: v for k, v in payload.items() if v is not None} - - filtered = [filter_payload(payload) for payload in payloads] - return await self.http.post( - "/runs/batch", json=filtered, headers=headers, params=params - ) - - @overload - async def wait( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> list[dict] | dict[str, Any]: ... - - @overload - async def wait( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> list[dict] | dict[str, Any]: ... - - async def wait( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, # deprecated - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> list[dict] | dict[str, Any]: - """Create a run, wait until it finishes and return the final state. - - Args: - thread_id: the thread ID to create the run on. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to run. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: A command to execute. Cannot be combined with input. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - on_disconnect: The disconnect mode to use. - Must be one of 'cancel' or 'continue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - headers: Optional custom headers to include with the request. - on_run_created: Optional callback to call when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - Returns: - The output of the run. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - final_state_of_run = await client.runs.wait( - thread_id=None, - assistant_id="agent", - input={"messages": [{"role": "user", "content": "how are you?"}]}, - metadata={"name":"my_run"}, - context={"model_name": "anthropic"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ) - print(final_state_of_run) - ``` - - ```shell - ------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'messages': [ - { - 'content': 'how are you?', - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'human', - 'name': None, - 'id': 'f51a862c-62fe-4866-863b-b0863e8ad78a', - 'example': False - }, - { - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'ai', - 'name': None, - 'id': 'run-bf1cd3c6-768f-4c16-b62d-ba6f17ad8b36', - 'example': False, - 'tool_calls': [], - 'invalid_tool_calls': [], - 'usage_metadata': None - } - ] - } - ``` - - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "config": config, - "context": context, - "metadata": metadata, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "multitask_strategy": multitask_strategy, - "checkpoint_during": checkpoint_during, - "if_not_exists": if_not_exists, - "on_disconnect": on_disconnect, - "on_completion": on_completion, - "after_seconds": after_seconds, - "durability": durability, - } - endpoint = ( - f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" - ) - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - response = await self.http.request_reconnect( - endpoint, - "POST", - json={k: v for k, v in payload.items() if v is not None}, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - if ( - raise_error - and isinstance(response, dict) - and "__error__" in response - and isinstance(response["__error__"], dict) - ): - raise Exception( - f"{response['__error__'].get('error')}: {response['__error__'].get('message')}" - ) - return response - - async def list( - self, - thread_id: str, - *, - limit: int = 10, - offset: int = 0, - status: RunStatus | None = None, - select: list[RunSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Run]: - """List runs. - - Args: - thread_id: The thread ID to list runs for. - limit: The maximum number of results to return. - offset: The number of results to skip. - status: The status of the run to filter by. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The runs for the thread. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.runs.list( - thread_id="thread_id", - limit=5, - offset=5, - ) - ``` - - """ - query_params: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if status is not None: - query_params["status"] = status - if select: - query_params["select"] = select - if params: - query_params.update(params) - return await self.http.get( - f"/threads/{thread_id}/runs", params=query_params, headers=headers - ) - - async def get( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Get a run. - - Args: - thread_id: The thread ID to get. - run_id: The run ID to get. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `Run` object. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - run = await client.runs.get( - thread_id="thread_id_to_delete", - run_id="run_id_to_delete", - ) - ``` - - """ - - return await self.http.get( - f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params - ) - - async def cancel( - self, - thread_id: str, - run_id: str, - *, - wait: bool = False, - action: CancelAction = "interrupt", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Get a run. - - Args: - thread_id: The thread ID to cancel. - run_id: The run ID to cancel. - wait: Whether to wait until run has completed. - action: Action to take when cancelling the run. Possible values - are `interrupt` or `rollback`. Default is `interrupt`. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.runs.cancel( - thread_id="thread_id_to_cancel", - run_id="run_id_to_cancel", - wait=True, - action="interrupt" - ) - ``` - - """ - query_params = { - "wait": 1 if wait else 0, - "action": action, - } - if params: - query_params.update(params) - if wait: - return await self.http.request_reconnect( - f"/threads/{thread_id}/runs/{run_id}/cancel", - "POST", - params=query_params, - headers=headers, - ) - else: - return await self.http.post( - f"/threads/{thread_id}/runs/{run_id}/cancel", - json=None, - params=query_params, - headers=headers, - ) - - async def join( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> dict: - """Block until a run is done. Returns the final state of the thread. - - Args: - thread_id: The thread ID to join. - run_id: The run ID to join. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - result =await client.runs.join( - thread_id="thread_id_to_join", - run_id="run_id_to_join" - ) - ``` - - """ - return await self.http.request_reconnect( - f"/threads/{thread_id}/runs/{run_id}/join", - "GET", - headers=headers, - params=params, - ) - - def join_stream( - self, - thread_id: str, - run_id: str, - *, - cancel_on_disconnect: bool = False, - stream_mode: StreamMode | Sequence[StreamMode] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - last_event_id: str | None = None, - ) -> AsyncIterator[StreamPart]: - """Stream output from a run in real-time, until the run is done. - Output is not buffered, so any output produced before this call will - not be received here. - - Args: - thread_id: The thread ID to join. - run_id: The run ID to join. - cancel_on_disconnect: Whether to cancel the run when the stream is disconnected. - stream_mode: The stream mode(s) to use. Must be a subset of the stream modes passed - when creating the run. Background runs default to having the union of all - stream modes. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - last_event_id: The last event ID to use for the stream. - - Returns: - The stream of parts. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - async for part in client.runs.join_stream( - thread_id="thread_id_to_join", - run_id="run_id_to_join", - stream_mode=["values", "debug"] - ): - print(part) - ``` - - """ - query_params = { - "cancel_on_disconnect": cancel_on_disconnect, - "stream_mode": stream_mode, - } - if params: - query_params.update(params) - return self.http.stream( - f"/threads/{thread_id}/runs/{run_id}/stream", - "GET", - params=query_params, - headers={ - **({"Last-Event-ID": last_event_id} if last_event_id else {}), - **(headers or {}), - } - or None, - ) - - async def delete( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a run. - - Args: - thread_id: The thread ID to delete. - run_id: The run ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.runs.delete( - thread_id="thread_id_to_delete", - run_id="run_id_to_delete" - ) - ``` - - """ - await self.http.delete( - f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params - ) - - -class CronClient: - """Client for managing recurrent runs (cron jobs) in LangGraph. - - A run is a single invocation of an assistant with optional input, config, and context. - This client allows scheduling recurring runs to occur automatically. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024")) - cron_job = await client.crons.create_for_thread( - thread_id="thread_123", - assistant_id="asst_456", - schedule="0 9 * * *", - input={"message": "Daily update"} - ) - ``` - - !!! note "Feature Availability" - - The crons client functionality is not supported on all licenses. - Please check the relevant license documentation for the most up-to-date - details on feature availability. - """ - - def __init__(self, http_client: HttpClient) -> None: - self.http = http_client - - async def create_for_thread( - self, - thread_id: str, - assistant_id: str, - *, - schedule: str, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - webhook: str | None = None, - multitask_strategy: str | None = None, - end_time: datetime | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Create a cron job for a thread. - - Args: - thread_id: the thread ID to run the cron job on. - assistant_id: The assistant ID or graph name to use for the cron job. - If using graph name, will default to first assistant created from that graph. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. - enabled: Whether the cron job is enabled or not. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The cron run. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - cron_run = await client.crons.create_for_thread( - thread_id="my-thread-id", - assistant_id="agent", - schedule="27 15 * * *", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt", - enabled=True, - ) - ``` - """ - payload = { - "schedule": schedule, - "input": input, - "config": config, - "metadata": metadata, - "context": context, - "assistant_id": assistant_id, - "checkpoint_during": checkpoint_during, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "end_time": end_time.isoformat() if end_time else None, - "enabled": enabled, - } - if multitask_strategy: - payload["multitask_strategy"] = multitask_strategy - payload = {k: v for k, v in payload.items() if v is not None} - return await self.http.post( - f"/threads/{thread_id}/runs/crons", - json=payload, - headers=headers, - params=params, - ) - - async def create( - self, - assistant_id: str, - *, - schedule: str, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - webhook: str | None = None, - on_run_completed: OnCompletionBehavior | None = None, - multitask_strategy: str | None = None, - end_time: datetime | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Create a cron run. - - Args: - assistant_id: The assistant ID or graph name to use for the cron job. - If using graph name, will default to first assistant created from that graph. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - on_run_completed: What to do with the thread after the run completes. - Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread - after execution. 'keep' creates a new thread for each execution but does not - clean them up. Clients are responsible for cleaning up kept threads. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. - enabled: Whether the cron job is enabled or not. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The cron run. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - cron_run = client.crons.create( - assistant_id="agent", - schedule="27 15 * * *", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt", - enabled=True, - ) - ``` - - """ - payload = { - "schedule": schedule, - "input": input, - "config": config, - "metadata": metadata, - "context": context, - "assistant_id": assistant_id, - "checkpoint_during": checkpoint_during, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "on_run_completed": on_run_completed, - "end_time": end_time.isoformat() if end_time else None, - "enabled": enabled, - } - if multitask_strategy: - payload["multitask_strategy"] = multitask_strategy - payload = {k: v for k, v in payload.items() if v is not None} - return await self.http.post( - "/runs/crons", json=payload, headers=headers, params=params - ) - - async def delete( - self, - cron_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a cron. - - Args: - cron_id: The cron ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.crons.delete( - cron_id="cron_to_delete" - ) - ``` - - """ - await self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params) - - async def update( - self, - cron_id: str, - *, - schedule: str | None = None, - end_time: datetime | None = None, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - webhook: str | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - on_run_completed: OnCompletionBehavior | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Cron: - """Update a cron job by ID. - - Args: - cron_id: The cron ID to update. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - end_time: The end date to stop running the cron. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context added to the assistant. - webhook: Webhook to call after LangGraph API call is done. - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to interrupt immediately after they get executed. - on_run_completed: What to do with the thread after the run completes. - Must be one of 'delete' or 'keep'. 'delete' removes the thread - after execution. 'keep' creates a new thread for each execution but does not - clean them up. - enabled: Enable or disable the cron job. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The updated cron job. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - updated_cron = await client.crons.update( - cron_id="1ef3cefa-4c09-6926-96d0-3dc97fd5e39b", - schedule="0 10 * * *", - enabled=False, - ) - ``` - - """ - payload = { - "schedule": schedule, - "end_time": end_time.isoformat() if end_time else None, - "input": input, - "metadata": metadata, - "config": config, - "context": context, - "webhook": webhook, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "on_run_completed": on_run_completed, - "enabled": enabled, - } - payload = {k: v for k, v in payload.items() if v is not None} - return await self.http.patch( - f"/runs/crons/{cron_id}", - json=payload, - headers=headers, - params=params, - ) - - async def search( - self, - *, - assistant_id: str | None = None, - thread_id: str | None = None, - enabled: bool | None = None, - limit: int = 10, - offset: int = 0, - sort_by: CronSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[CronSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Cron]: - """Get a list of cron jobs. - - Args: - assistant_id: The assistant ID or graph name to search for. - thread_id: the thread ID to search for. - enabled: The enabled status to search for. - limit: The maximum number of results to return. - offset: The number of results to skip. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The list of cron jobs returned by the search, - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - cron_jobs = await client.crons.search( - assistant_id="my_assistant_id", - thread_id="my_thread_id", - enabled=True, - limit=5, - offset=5, - ) - print(cron_jobs) - ``` - ```shell - - ---------------------------------------------------------- - - [ - { - 'cron_id': '1ef3cefa-4c09-6926-96d0-3dc97fd5e39b', - 'assistant_id': 'my_assistant_id', - 'thread_id': 'my_thread_id', - 'user_id': None, - 'payload': - { - 'input': {'start_time': ''}, - 'schedule': '4 * * * *', - 'assistant_id': 'my_assistant_id' - }, - 'schedule': '4 * * * *', - 'next_run_date': '2024-07-25T17:04:00+00:00', - 'end_time': None, - 'created_at': '2024-07-08T06:02:23.073257+00:00', - 'updated_at': '2024-07-08T06:02:23.073257+00:00' - } - ] - ``` - - """ - payload = { - "assistant_id": assistant_id, - "thread_id": thread_id, - "enabled": enabled, - "limit": limit, - "offset": offset, - } - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - payload = {k: v for k, v in payload.items() if v is not None} - return await self.http.post( - "/runs/crons/search", json=payload, headers=headers, params=params - ) - - async def count( - self, - *, - assistant_id: str | None = None, - thread_id: str | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count cron jobs matching filters. - - Args: - assistant_id: Assistant ID to filter by. - thread_id: Thread ID to filter by. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of crons matching the criteria. - """ - payload: dict[str, Any] = {} - if assistant_id: - payload["assistant_id"] = assistant_id - if thread_id: - payload["thread_id"] = thread_id - return await self.http.post( - "/runs/crons/count", json=payload, headers=headers, params=params - ) - - -class StoreClient: - """Client for interacting with the graph's shared storage. - - The Store provides a key-value storage system for persisting data across graph executions, - allowing for stateful operations and data sharing across threads. - - ???+ example "Example" - - ```python - client = get_client(url="http://localhost:2024") - await client.store.put_item(["users", "user123"], "mem-123451342", {"name": "Alice", "score": 100}) - ``` - """ - - def __init__(self, http: HttpClient) -> None: - self.http = http - - async def put_item( - self, - namespace: Sequence[str], - /, - key: str, - value: Mapping[str, Any], - index: Literal[False] | list[str] | None = None, - ttl: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Store or update an item. - - Args: - namespace: A list of strings representing the namespace path. - key: The unique identifier for the item within the namespace. - value: A dictionary containing the item's data. - index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index. - ttl: Optional time-to-live in minutes for the item, or None for no expiration. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.store.put_item( - ["documents", "user123"], - key="item456", - value={"title": "My Document", "content": "Hello World"} - ) - ``` - """ - for label in namespace: - if "." in label: - raise ValueError( - f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." - ) - payload = { - "namespace": namespace, - "key": key, - "value": value, - "index": index, - "ttl": ttl, - } - await self.http.put( - "/store/items", json=_provided_vals(payload), headers=headers, params=params - ) - - async def get_item( - self, - namespace: Sequence[str], - /, - key: str, - *, - refresh_ttl: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Item: - """Retrieve a single item. - - Args: - key: The unique identifier for the item. - namespace: Optional list of strings representing the namespace path. - refresh_ttl: Whether to refresh the TTL on this read operation. If `None`, uses the store's default behavior. - - Returns: - Item: The retrieved item. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - item = await client.store.get_item( - ["documents", "user123"], - key="item456", - ) - print(item) - ``` - ```shell - - ---------------------------------------------------------------- - - { - 'namespace': ['documents', 'user123'], - 'key': 'item456', - 'value': {'title': 'My Document', 'content': 'Hello World'}, - 'created_at': '2024-07-30T12:00:00Z', - 'updated_at': '2024-07-30T12:00:00Z' - } - ``` - """ - for label in namespace: - if "." in label: - raise ValueError( - f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." - ) - get_params = {"namespace": ".".join(namespace), "key": key} - if refresh_ttl is not None: - get_params["refresh_ttl"] = refresh_ttl - if params: - get_params = {**get_params, **params} - return await self.http.get("/store/items", params=get_params, headers=headers) - - async def delete_item( - self, - namespace: Sequence[str], - /, - key: str, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete an item. - - Args: - key: The unique identifier for the item. - namespace: Optional list of strings representing the namespace path. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - await client.store.delete_item( - ["documents", "user123"], - key="item456", - ) - ``` - """ - await self.http.delete( - "/store/items", - json={"namespace": namespace, "key": key}, - headers=headers, - params=params, - ) - - async def search_items( - self, - namespace_prefix: Sequence[str], - /, - filter: Mapping[str, Any] | None = None, - limit: int = 10, - offset: int = 0, - query: str | None = None, - refresh_ttl: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> SearchItemsResponse: - """Search for items within a namespace prefix. - - Args: - namespace_prefix: List of strings representing the namespace prefix. - filter: Optional dictionary of key-value pairs to filter results. - limit: Maximum number of items to return (default is 10). - offset: Number of items to skip before returning results (default is 0). - query: Optional query for natural language search. - refresh_ttl: Whether to refresh the TTL on items returned by this search. If `None`, uses the store's default behavior. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - A list of items matching the search criteria. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - items = await client.store.search_items( - ["documents"], - filter={"author": "John Doe"}, - limit=5, - offset=0 - ) - print(items) - ``` - ```shell - - ---------------------------------------------------------------- - - { - "items": [ - { - "namespace": ["documents", "user123"], - "key": "item789", - "value": { - "title": "Another Document", - "author": "John Doe" - }, - "created_at": "2024-07-30T12:00:00Z", - "updated_at": "2024-07-30T12:00:00Z" - }, - # ... additional items ... - ] - } - ``` - """ - payload = { - "namespace_prefix": namespace_prefix, - "filter": filter, - "limit": limit, - "offset": offset, - "query": query, - "refresh_ttl": refresh_ttl, - } - - return await self.http.post( - "/store/items/search", - json=_provided_vals(payload), - headers=headers, - params=params, - ) - - async def list_namespaces( - self, - prefix: list[str] | None = None, - suffix: list[str] | None = None, - max_depth: int | None = None, - limit: int = 100, - offset: int = 0, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ListNamespaceResponse: - """List namespaces with optional match conditions. - - Args: - prefix: Optional list of strings representing the prefix to filter namespaces. - suffix: Optional list of strings representing the suffix to filter namespaces. - max_depth: Optional integer specifying the maximum depth of namespaces to return. - limit: Maximum number of namespaces to return (default is 100). - offset: Number of namespaces to skip before returning results (default is 0). - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - A list of namespaces matching the criteria. - - ???+ example "Example Usage" - - ```python - client = get_client(url="http://localhost:2024") - namespaces = await client.store.list_namespaces( - prefix=["documents"], - max_depth=3, - limit=10, - offset=0 - ) - print(namespaces) - - ---------------------------------------------------------------- - - [ - ["documents", "user123", "reports"], - ["documents", "user456", "invoices"], - ... - ] - ``` - """ - payload = { - "prefix": prefix, - "suffix": suffix, - "max_depth": max_depth, - "limit": limit, - "offset": offset, - } - return await self.http.post( - "/store/namespaces", - json=_provided_vals(payload), - headers=headers, - params=params, - ) - - -def get_sync_client( - *, - url: str | None = None, - api_key: str | None = NOT_PROVIDED, - headers: Mapping[str, str] | None = None, - timeout: TimeoutTypes | None = None, -) -> SyncLangGraphClient: - """Get a synchronous LangGraphClient instance. - - Args: - url: The URL of the LangGraph API. - api_key: API key for authentication. Can be: - - A string: use this exact API key - - `None`: explicitly skip loading from environment variables - - Not provided (default): auto-load from environment in this order: - 1. `LANGGRAPH_API_KEY` - 2. `LANGSMITH_API_KEY` - 3. `LANGCHAIN_API_KEY` - headers: Optional custom headers - timeout: Optional timeout configuration for the HTTP client. - Accepts an httpx.Timeout instance, a float (seconds), or a tuple of timeouts. - Tuple format is (connect, read, write, pool) - If not provided, defaults to connect=5s, read=300s, write=300s, and pool=5s. - Returns: - SyncLangGraphClient: The top-level synchronous client for accessing AssistantsClient, - ThreadsClient, RunsClient, and CronClient. - - ???+ example "Example" - - ```python - from langgraph_sdk import get_sync_client - - # get top-level synchronous LangGraphClient - client = get_sync_client(url="http://localhost:8123") - - # example usage: client..() - assistant = client.assistants.get(assistant_id="some_uuid") - ``` - - ???+ example "Skip auto-loading API key from environment:" - - ```python - from langgraph_sdk import get_sync_client - - # Don't load API key from environment variables - client = get_sync_client( - url="http://localhost:8123", - api_key=None - ) - ``` - """ - - if url is None: - url = "http://localhost:8123" - - transport = httpx.HTTPTransport(retries=5) - client = httpx.Client( - base_url=url, - transport=transport, - timeout=( - httpx.Timeout(timeout) # ty: ignore[invalid-argument-type] - if timeout is not None - else httpx.Timeout(connect=5, read=300, write=300, pool=5) - ), - headers=_get_headers(api_key, headers), - ) - return SyncLangGraphClient(client) - - -class SyncLangGraphClient: - """Synchronous client for interacting with the LangGraph API. - - This class provides synchronous access to LangGraph API endpoints for managing - assistants, threads, runs, cron jobs, and data storage. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:2024") - assistant = client.assistants.get("asst_123") - ``` - """ - - def __init__(self, client: httpx.Client) -> None: - self.http = SyncHttpClient(client) - self.assistants = SyncAssistantsClient(self.http) - self.threads = SyncThreadsClient(self.http) - self.runs = SyncRunsClient(self.http) - self.crons = SyncCronClient(self.http) - self.store = SyncStoreClient(self.http) - - def __enter__(self) -> SyncLangGraphClient: - """Enter the sync context manager.""" - return self - - def __exit__( - self, - exc_type: type[BaseException] | None, - exc_val: BaseException | None, - exc_tb: TracebackType | None, - ) -> None: - """Exit the sync context manager.""" - self.close() - - def close(self) -> None: - """Close the underlying HTTP client.""" - if hasattr(self, "http"): - self.http.client.close() - - -class SyncHttpClient: - """Handle synchronous requests to the LangGraph API. - - Provides error messaging and content handling enhancements above the - underlying httpx client, mirroring the interface of [HttpClient](#HttpClient) - but for sync usage. - - Attributes: - client (httpx.Client): Underlying HTTPX sync client. - """ - - def __init__(self, client: httpx.Client) -> None: - self.client = client - - def get( - self, - path: str, - *, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `GET` request.""" - r = self.client.get(path, params=params, headers=headers) - if on_response: - on_response(r) - _raise_for_status_typed(r) - return _decode_json(r) - - def post( - self, - path: str, - *, - json: dict[str, Any] | list | None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `POST` request.""" - if json is not None: - request_headers, content = _encode_json(json) - else: - request_headers, content = {}, b"" - if headers: - request_headers.update(headers) - r = self.client.post( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - _raise_for_status_typed(r) - return _decode_json(r) - - def put( - self, - path: str, - *, - json: dict, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `PUT` request.""" - request_headers, content = _encode_json(json) - if headers: - request_headers.update(headers) - - r = self.client.put( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - _raise_for_status_typed(r) - return _decode_json(r) - - def patch( - self, - path: str, - *, - json: dict, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Any: - """Send a `PATCH` request.""" - request_headers, content = _encode_json(json) - if headers: - request_headers.update(headers) - r = self.client.patch( - path, headers=request_headers, content=content, params=params - ) - if on_response: - on_response(r) - _raise_for_status_typed(r) - return _decode_json(r) - - def delete( - self, - path: str, - *, - json: Any | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> None: - """Send a `DELETE` request.""" - r = self.client.request( - "DELETE", path, json=json, params=params, headers=headers - ) - if on_response: - on_response(r) - _raise_for_status_typed(r) - - def request_reconnect( - self, - path: str, - method: str, - *, - json: dict[str, Any] | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - reconnect_limit: int = 5, - ) -> Any: - """Send a request that automatically reconnects to Location header.""" - request_headers, content = _encode_json(json) - if headers: - request_headers.update(headers) - with self.client.stream( - method, path, headers=request_headers, content=content, params=params - ) as r: - if on_response: - on_response(r) - try: - r.raise_for_status() - except httpx.HTTPStatusError as e: - body = r.read().decode() - if sys.version_info >= (3, 11): - e.add_note(body) - else: - logger.error(f"Error from langgraph-api: {body}", exc_info=e) - raise e - loc = r.headers.get("location") - if reconnect_limit <= 0 or not loc: - return _decode_json(r) - try: - return _decode_json(r) - except httpx.HTTPError: - warnings.warn( - f"Request failed, attempting reconnect to Location: {loc}", - stacklevel=2, - ) - r.close() - return self.request_reconnect( - loc, - "GET", - headers=request_headers, - # don't pass on_response so it's only called once - reconnect_limit=reconnect_limit - 1, - ) - - def stream( - self, - path: str, - method: str, - *, - json: dict[str, Any] | None = None, - params: QueryParamTypes | None = None, - headers: Mapping[str, str] | None = None, - on_response: Callable[[httpx.Response], None] | None = None, - ) -> Iterator[StreamPart]: - """Stream the results of a request using SSE.""" - if json is not None: - request_headers, content = _encode_json(json) - else: - request_headers, content = {}, None - request_headers["Accept"] = "text/event-stream" - request_headers["Cache-Control"] = "no-store" - if headers: - request_headers.update(headers) - - reconnect_headers = { - key: value - for key, value in request_headers.items() - if key.lower() not in {"content-length", "content-type"} - } - - last_event_id: str | None = None - reconnect_path: str | None = None - reconnect_attempts = 0 - max_reconnect_attempts = 5 - - while True: - current_headers = dict( - request_headers if reconnect_path is None else reconnect_headers - ) - if last_event_id is not None: - current_headers["Last-Event-ID"] = last_event_id - - current_method = method if reconnect_path is None else "GET" - current_content = content if reconnect_path is None else None - current_params = params if reconnect_path is None else None - - retry = False - with self.client.stream( - current_method, - reconnect_path or path, - headers=current_headers, - content=current_content, - params=current_params, - ) as res: - if reconnect_path is None and on_response: - on_response(res) - # check status - _raise_for_status_typed(res) - # check content type - content_type = res.headers.get("content-type", "").partition(";")[0] - if "text/event-stream" not in content_type: - raise httpx.TransportError( - "Expected response header Content-Type to contain 'text/event-stream', " - f"got {content_type!r}" - ) - - reconnect_location = res.headers.get("location") - if reconnect_location: - reconnect_path = reconnect_location - - decoder = SSEDecoder() - try: - for line in iter_lines_raw(res): - sse = decoder.decode(cast(bytes, line).rstrip(b"\n")) - if sse is not None: - if decoder.last_event_id is not None: - last_event_id = decoder.last_event_id - if sse.event or sse.data is not None: - yield sse - except httpx.HTTPError: - # httpx.TransportError inherits from HTTPError, so transient - # disconnects during streaming land here. - if reconnect_path is None: - raise - retry = True - else: - if sse := decoder.decode(b""): - if decoder.last_event_id is not None: - last_event_id = decoder.last_event_id - if sse.event or sse.data is not None: - # See async stream implementation for rationale on - # skipping empty flush events. - yield sse - if retry: - reconnect_attempts += 1 - if reconnect_attempts > max_reconnect_attempts: - raise httpx.TransportError( - "Exceeded maximum SSE reconnection attempts" - ) - continue - break - - -def _encode_json(json: Any) -> tuple[dict[str, str], bytes]: - body = orjson.dumps( - json, - _orjson_default, - orjson.OPT_SERIALIZE_NUMPY | orjson.OPT_NON_STR_KEYS, - ) - content_length = str(len(body)) - content_type = "application/json" - headers = {"Content-Length": content_length, "Content-Type": content_type} - return headers, body - - -def _decode_json(r: httpx.Response) -> Any: - body = r.read() - return orjson.loads(body) if body else None - - -class SyncAssistantsClient: - """Client for managing assistants in LangGraph synchronously. - - This class provides methods to interact with assistants, which are versioned configurations of your graph. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:2024") - assistant = client.assistants.get("assistant_id_123") - ``` - """ - - def __init__(self, http: SyncHttpClient) -> None: - self.http = http - - def get( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Get an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get OR the name of the graph (to use the default assistant). - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `Assistant` Object. - - ???+ example "Example Usage" - - ```python - assistant = client.assistants.get( - assistant_id="my_assistant_id" - ) - print(assistant) - ``` - - ```shell - ---------------------------------------------------- - - { - 'assistant_id': 'my_assistant_id', - 'graph_id': 'agent', - 'created_at': '2024-06-25T17:10:33.109781+00:00', - 'updated_at': '2024-06-25T17:10:33.109781+00:00', - 'config': {}, - 'context': {}, - 'metadata': {'created_by': 'system'} - } - ``` - - """ - return self.http.get( - f"/assistants/{assistant_id}", headers=headers, params=params - ) - - def get_graph( - self, - assistant_id: str, - *, - xray: int | bool = False, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> dict[str, list[dict[str, Any]]]: - """Get the graph of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the graph of. - xray: Include graph representation of subgraphs. If an integer value is provided, only subgraphs with a depth less than or equal to the value will be included. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The graph information for the assistant in JSON format. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - graph_info = client.assistants.get_graph( - assistant_id="my_assistant_id" - ) - print(graph_info) - - -------------------------------------------------------------------------------------------------------------------------- - - { - 'nodes': - [ - {'id': '__start__', 'type': 'schema', 'data': '__start__'}, - {'id': '__end__', 'type': 'schema', 'data': '__end__'}, - {'id': 'agent','type': 'runnable','data': {'id': ['langgraph', 'utils', 'RunnableCallable'],'name': 'agent'}}, - ], - 'edges': - [ - {'source': '__start__', 'target': 'agent'}, - {'source': 'agent','target': '__end__'} - ] - } - ``` - - """ - query_params = {"xray": xray} - if params: - query_params.update(params) - return self.http.get( - f"/assistants/{assistant_id}/graph", params=query_params, headers=headers - ) - - def get_schemas( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> GraphSchema: - """Get the schemas of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the schema of. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - GraphSchema: The graph schema for the assistant. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - schema = client.assistants.get_schemas( - assistant_id="my_assistant_id" - ) - print(schema) - ``` - ```shell - ---------------------------------------------------------------------------------------------------------------------------- - - { - 'graph_id': 'agent', - 'state_schema': - { - 'title': 'LangGraphInput', - '$ref': '#/definitions/AgentState', - 'definitions': - { - 'BaseMessage': - { - 'title': 'BaseMessage', - 'description': 'Base abstract Message class. Messages are the inputs and outputs of ChatModels.', - 'type': 'object', - 'properties': - { - 'content': - { - 'title': 'Content', - 'anyOf': [ - {'type': 'string'}, - {'type': 'array','items': {'anyOf': [{'type': 'string'}, {'type': 'object'}]}} - ] - }, - 'additional_kwargs': - { - 'title': 'Additional Kwargs', - 'type': 'object' - }, - 'response_metadata': - { - 'title': 'Response Metadata', - 'type': 'object' - }, - 'type': - { - 'title': 'Type', - 'type': 'string' - }, - 'name': - { - 'title': 'Name', - 'type': 'string' - }, - 'id': - { - 'title': 'Id', - 'type': 'string' - } - }, - 'required': ['content', 'type'] - }, - 'AgentState': - { - 'title': 'AgentState', - 'type': 'object', - 'properties': - { - 'messages': - { - 'title': 'Messages', - 'type': 'array', - 'items': {'$ref': '#/definitions/BaseMessage'} - } - }, - 'required': ['messages'] - } - } - }, - 'config_schema': - { - 'title': 'Configurable', - 'type': 'object', - 'properties': - { - 'model_name': - { - 'title': 'Model Name', - 'enum': ['anthropic', 'openai'], - 'type': 'string' - } - } - }, - 'context_schema': - { - 'title': 'Context', - 'type': 'object', - 'properties': - { - 'model_name': - { - 'title': 'Model Name', - 'enum': ['anthropic', 'openai'], - 'type': 'string' - } - } - } - } - ``` - - """ - return self.http.get( - f"/assistants/{assistant_id}/schemas", headers=headers, params=params - ) - - def get_subgraphs( - self, - assistant_id: str, - namespace: str | None = None, - recurse: bool = False, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Subgraphs: - """Get the schemas of an assistant by ID. - - Args: - assistant_id: The ID of the assistant to get the schema of. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - Subgraphs: The graph schema for the assistant. - - """ - get_params = {"recurse": recurse} - if params: - get_params = {**get_params, **params} - if namespace is not None: - return self.http.get( - f"/assistants/{assistant_id}/subgraphs/{namespace}", - params=get_params, - headers=headers, - ) - else: - return self.http.get( - f"/assistants/{assistant_id}/subgraphs", - params=get_params, - headers=headers, - ) - - def create( - self, - graph_id: str | None, - config: Config | None = None, - *, - context: Context | None = None, - metadata: Json = None, - assistant_id: str | None = None, - if_exists: OnConflictBehavior | None = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - description: str | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Create a new assistant. - - Useful when graph is configurable and you want to create different assistants based on different configurations. - - Args: - graph_id: The ID of the graph the assistant should use. The graph ID is normally set in your langgraph.json configuration. - config: Configuration to use for the graph. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - metadata: Metadata to add to assistant. - assistant_id: Assistant ID to use, will default to a random UUID if not provided. - if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. - Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing assistant). - name: The name of the assistant. Defaults to 'Untitled' under the hood. - headers: Optional custom headers to include with the request. - description: Optional description of the assistant. - The description field is available for langgraph-api server version>=0.0.45 - params: Optional query parameters to include with the request. - - Returns: - The created assistant. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - assistant = client.assistants.create( - graph_id="agent", - context={"model_name": "openai"}, - metadata={"number":1}, - assistant_id="my-assistant-id", - if_exists="do_nothing", - name="my_name" - ) - ``` - """ - payload: dict[str, Any] = { - "graph_id": graph_id, - } - if config: - payload["config"] = config - if context: - payload["context"] = context - if metadata: - payload["metadata"] = metadata - if assistant_id: - payload["assistant_id"] = assistant_id - if if_exists: - payload["if_exists"] = if_exists - if name: - payload["name"] = name - if description: - payload["description"] = description - return self.http.post( - "/assistants", json=payload, headers=headers, params=params - ) - - def update( - self, - assistant_id: str, - *, - graph_id: str | None = None, - config: Config | None = None, - context: Context | None = None, - metadata: Json = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - description: str | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Update an assistant. - - Use this to point to a different graph, update the configuration, or change the metadata of an assistant. - - Args: - assistant_id: Assistant to update. - graph_id: The ID of the graph the assistant should use. - The graph ID is normally set in your langgraph.json configuration. If `None`, assistant will keep pointing to same graph. - config: Configuration to use for the graph. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - metadata: Metadata to merge with existing assistant metadata. - name: The new name for the assistant. - headers: Optional custom headers to include with the request. - description: Optional description of the assistant. - The description field is available for langgraph-api server version>=0.0.45 - - Returns: - The updated assistant. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - assistant = client.assistants.update( - assistant_id='e280dad7-8618-443f-87f1-8e41841c180f', - graph_id="other-graph", - context={"model_name": "anthropic"}, - metadata={"number":2} - ) - ``` - """ - payload: dict[str, Any] = {} - if graph_id: - payload["graph_id"] = graph_id - if config: - payload["config"] = config - if context: - payload["context"] = context - if metadata: - payload["metadata"] = metadata - if name: - payload["name"] = name - if description: - payload["description"] = description - return self.http.patch( - f"/assistants/{assistant_id}", - json=payload, - headers=headers, - params=params, - ) - - def delete( - self, - assistant_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete an assistant. - - Args: - assistant_id: The assistant ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.assistants.delete( - assistant_id="my_assistant_id" - ) - ``` - - """ - self.http.delete(f"/assistants/{assistant_id}", headers=headers, params=params) - - @overload - def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["object"], - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> AssistantsSearchResponse: ... - - @overload - def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["array"] = "array", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Assistant]: ... - - def search( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - limit: int = 10, - offset: int = 0, - sort_by: AssistantSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[AssistantSelectField] | None = None, - response_format: Literal["array", "object"] = "array", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> AssistantsSearchResponse | list[Assistant]: - """Search for assistants. - - Args: - metadata: Metadata to filter by. Exact match filter for each KV pair. - graph_id: The ID of the graph to filter by. - The graph ID is normally set in your langgraph.json configuration. - name: The name of the assistant to filter by. - The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. - limit: The maximum number of results to return. - offset: The number of results to skip. - sort_by: The field to sort by. - sort_order: The order to sort by. - select: Specific assistant fields to include in the response. - response_format: Controls the response shape. Use ``"array"`` (default) - to return a bare list of assistants, or ``"object"`` to return - a mapping containing assistants plus pagination metadata. - Defaults to "array", though this default will be changed to "object" in a future release. - headers: Optional custom headers to include with the request. - - Returns: - A list of assistants (when ``response_format=\"array\"``) or a mapping - with the assistants and the next pagination cursor (when - ``response_format=\"object\"``). - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - response = client.assistants.search( - metadata = {"name":"my_name"}, - graph_id="my_graph_id", - limit=5, - offset=5, - response_format="object", - ) - assistants = response["assistants"] - next_cursor = response["next"] - ``` - """ - if response_format not in ("array", "object"): - raise ValueError("response_format must be 'array' or 'object'") - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - if graph_id: - payload["graph_id"] = graph_id - if name: - payload["name"] = name - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - next_cursor: str | None = None - - def capture_pagination(response: httpx.Response) -> None: - nonlocal next_cursor - next_cursor = response.headers.get("X-Pagination-Next") - - assistants = cast( - list[Assistant], - self.http.post( - "/assistants/search", - json=payload, - headers=headers, - params=params, - on_response=capture_pagination if response_format == "object" else None, - ), - ) - if response_format == "object": - return {"assistants": assistants, "next": next_cursor} - return assistants - - def count( - self, - *, - metadata: Json = None, - graph_id: str | None = None, - name: str | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count assistants matching filters. - - Args: - metadata: Metadata to filter by. Exact match for each key/value. - graph_id: Optional graph id to filter by. - name: Optional name to filter by. - The filtering logic will match assistants where 'name' is a substring (case insensitive) of the assistant name. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of assistants matching the criteria. - """ - payload: dict[str, Any] = {} - if metadata: - payload["metadata"] = metadata - if graph_id: - payload["graph_id"] = graph_id - if name: - payload["name"] = name - return self.http.post( - "/assistants/count", json=payload, headers=headers, params=params - ) - - def get_versions( - self, - assistant_id: str, - metadata: Json = None, - limit: int = 10, - offset: int = 0, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[AssistantVersion]: - """List all versions of an assistant. - - Args: - assistant_id: The assistant ID to get versions for. - metadata: Metadata to filter versions by. Exact match filter for each KV pair. - limit: The maximum number of versions to return. - offset: The number of versions to skip. - headers: Optional custom headers to include with the request. - - Returns: - A list of assistants. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - assistant_versions = client.assistants.get_versions( - assistant_id="my_assistant_id" - ) - ``` - - """ - - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - return self.http.post( - f"/assistants/{assistant_id}/versions", - json=payload, - headers=headers, - params=params, - ) - - def set_latest( - self, - assistant_id: str, - version: int, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Assistant: - """Change the version of an assistant. - - Args: - assistant_id: The assistant ID to delete. - version: The version to change to. - headers: Optional custom headers to include with the request. - - Returns: - `Assistant` Object. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - new_version_assistant = client.assistants.set_latest( - assistant_id="my_assistant_id", - version=3 - ) - ``` - - """ - - payload: dict[str, Any] = {"version": version} - - return self.http.post( - f"/assistants/{assistant_id}/latest", - json=payload, - headers=headers, - params=params, - ) - - -class SyncThreadsClient: - """Synchronous client for managing threads in LangGraph. - - This class provides methods to create, retrieve, and manage threads, - which represent conversations or stateful interactions. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:2024") - thread = client.threads.create(metadata={"user_id": "123"}) - ``` - """ - - def __init__(self, http: SyncHttpClient) -> None: - self.http = http - - def get( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Get a thread by ID. - - Args: - thread_id: The ID of the thread to get. - headers: Optional custom headers to include with the request. - - Returns: - `Thread` object. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - thread = client.threads.get( - thread_id="my_thread_id" - ) - print(thread) - ``` - ```shell - ----------------------------------------------------- - - { - 'thread_id': 'my_thread_id', - 'created_at': '2024-07-18T18:35:15.540834+00:00', - 'updated_at': '2024-07-18T18:35:15.540834+00:00', - 'metadata': {'graph_id': 'agent'} - } - ``` - - """ - - return self.http.get(f"/threads/{thread_id}", headers=headers, params=params) - - def create( - self, - *, - metadata: Json = None, - thread_id: str | None = None, - if_exists: OnConflictBehavior | None = None, - supersteps: Sequence[dict[str, Sequence[dict[str, Any]]]] | None = None, - graph_id: str | None = None, - ttl: int | Mapping[str, Any] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Create a new thread. - - Args: - metadata: Metadata to add to thread. - thread_id: ID of thread. - If `None`, ID will be a randomly generated UUID. - if_exists: How to handle duplicate creation. Defaults to 'raise' under the hood. - Must be either 'raise' (raise error if duplicate), or 'do_nothing' (return existing thread). - supersteps: Apply a list of supersteps when creating a thread, each containing a sequence of updates. - Each update has `values` or `command` and `as_node`. Used for copying a thread between deployments. - graph_id: Optional graph ID to associate with the thread. - ttl: Optional time-to-live in minutes for the thread. You can pass an - integer (minutes) or a mapping with keys `ttl` and optional - `strategy` (defaults to "delete"). - headers: Optional custom headers to include with the request. - - Returns: - The created `Thread`. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - thread = client.threads.create( - metadata={"number":1}, - thread_id="my-thread-id", - if_exists="raise" - ) - ``` - ) - """ - payload: dict[str, Any] = {} - if thread_id: - payload["thread_id"] = thread_id - if metadata or graph_id: - payload["metadata"] = { - **(metadata or {}), - **({"graph_id": graph_id} if graph_id else {}), - } - if if_exists: - payload["if_exists"] = if_exists - if supersteps: - payload["supersteps"] = [ - { - "updates": [ - { - "values": u["values"], - "command": u.get("command"), - "as_node": u["as_node"], - } - for u in s["updates"] - ] - } - for s in supersteps - ] - if ttl is not None: - if isinstance(ttl, (int, float)): - payload["ttl"] = {"ttl": ttl, "strategy": "delete"} - else: - payload["ttl"] = ttl - - return self.http.post("/threads", json=payload, headers=headers, params=params) - - def update( - self, - thread_id: str, - *, - metadata: Mapping[str, Any], - ttl: int | Mapping[str, Any] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Thread: - """Update a thread. - - Args: - thread_id: ID of thread to update. - metadata: Metadata to merge with existing thread metadata. - ttl: Optional time-to-live in minutes for the thread. You can pass an - integer (minutes) or a mapping with keys `ttl` and optional - `strategy` (defaults to "delete"). - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The created `Thread`. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - thread = client.threads.update( - thread_id="my-thread-id", - metadata={"number":1}, - ttl=43_200, - ) - ``` - """ - payload: dict[str, Any] = {"metadata": metadata} - if ttl is not None: - if isinstance(ttl, (int, float)): - payload["ttl"] = {"ttl": ttl, "strategy": "delete"} - else: - payload["ttl"] = ttl - return self.http.patch( - f"/threads/{thread_id}", - json=payload, - headers=headers, - params=params, - ) - - def delete( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a thread. - - Args: - thread_id: The ID of the thread to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client.threads.delete( - thread_id="my_thread_id" - ) - ``` - - """ - self.http.delete(f"/threads/{thread_id}", headers=headers, params=params) - - def search( - self, - *, - metadata: Json = None, - values: Json = None, - ids: Sequence[str] | None = None, - status: ThreadStatus | None = None, - limit: int = 10, - offset: int = 0, - sort_by: ThreadSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[ThreadSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Thread]: - """Search for threads. - - Args: - metadata: Thread metadata to filter on. - values: State values to filter on. - ids: List of thread IDs to filter by. - status: Thread status to filter on. - Must be one of 'idle', 'busy', 'interrupted' or 'error'. - limit: Limit on number of threads to return. - offset: Offset in threads table to start search from. - headers: Optional custom headers to include with the request. - - Returns: - List of the threads matching the search parameters. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - threads = client.threads.search( - metadata={"number":1}, - status="interrupted", - limit=15, - offset=5 - ) - ``` - """ - payload: dict[str, Any] = { - "limit": limit, - "offset": offset, - } - if metadata: - payload["metadata"] = metadata - if values: - payload["values"] = values - if ids: - payload["ids"] = ids - if status: - payload["status"] = status - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - return self.http.post( - "/threads/search", json=payload, headers=headers, params=params - ) - - def count( - self, - *, - metadata: Json = None, - values: Json = None, - status: ThreadStatus | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count threads matching filters. - - Args: - metadata: Thread metadata to filter on. - values: State values to filter on. - status: Thread status to filter on. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of threads matching the criteria. - """ - payload: dict[str, Any] = {} - if metadata: - payload["metadata"] = metadata - if values: - payload["values"] = values - if status: - payload["status"] = status - return self.http.post( - "/threads/count", json=payload, headers=headers, params=params - ) - - def copy( - self, - thread_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Copy a thread. - - Args: - thread_id: The ID of the thread to copy. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.threads.copy( - thread_id="my_thread_id" - ) - ``` - - """ - return self.http.post( - f"/threads/{thread_id}/copy", json=None, headers=headers, params=params - ) - - def get_state( - self, - thread_id: str, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, # deprecated - *, - subgraphs: bool = False, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ThreadState: - """Get the state of a thread. - - Args: - thread_id: The ID of the thread to get the state of. - checkpoint: The checkpoint to get the state of. - subgraphs: Include subgraphs states. - headers: Optional custom headers to include with the request. - - Returns: - The thread of the state. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - thread_state = client.threads.get_state( - thread_id="my_thread_id", - checkpoint_id="my_checkpoint_id" - ) - print(thread_state) - ``` - - ```shell - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'values': { - 'messages': [ - { - 'content': 'how are you?', - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'human', - 'name': None, - 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', - 'example': False - }, - { - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'ai', - 'name': None, - 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', - 'example': False, - 'tool_calls': [], - 'invalid_tool_calls': [], - 'usage_metadata': None - } - ] - }, - 'next': [], - 'checkpoint': - { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1' - } - 'metadata': - { - 'step': 1, - 'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2', - 'source': 'loop', - 'writes': - { - 'agent': - { - 'messages': [ - { - 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', - 'name': None, - 'type': 'ai', - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'example': False, - 'tool_calls': [], - 'usage_metadata': None, - 'additional_kwargs': {}, - 'response_metadata': {}, - 'invalid_tool_calls': [] - } - ] - } - }, - 'user_id': None, - 'graph_id': 'agent', - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'created_by': 'system', - 'assistant_id': 'fe096781-5601-53d2-b2f6-0d3403f7e9ca'}, - 'created_at': '2024-07-25T15:35:44.184703+00:00', - 'parent_config': - { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-d80d-6fa7-8000-9300467fad0f' - } - } - ``` - - """ - if checkpoint: - return self.http.post( - f"/threads/{thread_id}/state/checkpoint", - json={"checkpoint": checkpoint, "subgraphs": subgraphs}, - headers=headers, - params=params, - ) - elif checkpoint_id: - get_params = {"subgraphs": subgraphs} - if params: - get_params = {**get_params, **params} - return self.http.get( - f"/threads/{thread_id}/state/{checkpoint_id}", - params=get_params, - headers=headers, - ) - else: - get_params = {"subgraphs": subgraphs} - if params: - get_params = {**get_params, **params} - return self.http.get( - f"/threads/{thread_id}/state", - params=get_params, - headers=headers, - ) - - def update_state( - self, - thread_id: str, - values: dict[str, Any] | Sequence[dict] | None, - *, - as_node: str | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, # deprecated - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ThreadUpdateStateResponse: - """Update the state of a thread. - - Args: - thread_id: The ID of the thread to update. - values: The values to update the state with. - as_node: Update the state as if this node had just executed. - checkpoint: The checkpoint to update the state of. - headers: Optional custom headers to include with the request. - - Returns: - Response after updating a thread's state. - - ???+ example "Example Usage" - - ```python - - response = await client.threads.update_state( - thread_id="my_thread_id", - values={"messages":[{"role": "user", "content": "hello!"}]}, - as_node="my_node", - ) - print(response) - - ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'checkpoint': { - 'thread_id': 'e2496803-ecd5-4e0c-a779-3226296181c2', - 'checkpoint_ns': '', - 'checkpoint_id': '1ef4a9b8-e6fb-67b1-8001-abd5184439d1', - 'checkpoint_map': {} - } - } - ``` - - """ - payload: dict[str, Any] = { - "values": values, - } - if checkpoint_id: - payload["checkpoint_id"] = checkpoint_id - if checkpoint: - payload["checkpoint"] = checkpoint - if as_node: - payload["as_node"] = as_node - return self.http.post( - f"/threads/{thread_id}/state", json=payload, headers=headers, params=params - ) - - def get_history( - self, - thread_id: str, - *, - limit: int = 10, - before: str | Checkpoint | None = None, - metadata: Mapping[str, Any] | None = None, - checkpoint: Checkpoint | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[ThreadState]: - """Get the state history of a thread. - - Args: - thread_id: The ID of the thread to get the state history for. - checkpoint: Return states for this subgraph. If empty defaults to root. - limit: The maximum number of states to return. - before: Return states before this checkpoint. - metadata: Filter states by metadata key-value pairs. - headers: Optional custom headers to include with the request. - - Returns: - The state history of the `Thread`. - - ???+ example "Example Usage" - - ```python - - thread_state = client.threads.get_history( - thread_id="my_thread_id", - limit=5, - before="my_timestamp", - metadata={"name":"my_name"} - ) - ``` - - """ - payload: dict[str, Any] = { - "limit": limit, - } - if before: - payload["before"] = before - if metadata: - payload["metadata"] = metadata - if checkpoint: - payload["checkpoint"] = checkpoint - return self.http.post( - f"/threads/{thread_id}/history", - json=payload, - headers=headers, - params=params, - ) - - def join_stream( - self, - thread_id: str, - *, - stream_mode: ThreadStreamMode | Sequence[ThreadStreamMode] = "run_modes", - last_event_id: str | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Iterator[StreamPart]: - """Get a stream of events for a thread. - - Args: - thread_id: The ID of the thread to get the stream for. - last_event_id: The ID of the last event to get. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - An iterator of stream parts. - - ???+ example "Example Usage" - - ```python - - for chunk in client.threads.join_stream( - thread_id="my_thread_id", - last_event_id="my_event_id", - stream_mode="run_modes", - ): - print(chunk) - ``` - - """ - query_params = { - "stream_mode": stream_mode, - } - if params: - query_params.update(params) - return self.http.stream( - f"/threads/{thread_id}/stream", - "GET", - headers={ - **({"Last-Event-ID": last_event_id} if last_event_id else {}), - **(headers or {}), - }, - params=query_params, - ) - - -class SyncRunsClient: - """Synchronous client for managing runs in LangGraph. - - This class provides methods to create, retrieve, and manage runs, which represent - individual executions of graphs. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:2024") - run = client.runs.create(thread_id="thread_123", assistant_id="asst_456") - ``` - """ - - def __init__(self, http: SyncHttpClient) -> None: - self.http = http - - @overload - def stream( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Iterator[StreamPart]: ... - - @overload - def stream( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - webhook: str | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Iterator[StreamPart]: ... - - def stream( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, # deprecated - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - feedback_keys: Sequence[str] | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> Iterator[StreamPart]: - """Create a run and stream the results. - - Args: - thread_id: the thread ID to assign to the thread. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to stream from. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: The command to execute. - stream_mode: The stream mode(s) to use. - stream_subgraphs: Whether to stream output from subgraphs. - stream_resumable: Whether the stream is considered resumable. - If true, the stream can be resumed and replayed in its entirety even after disconnection. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - feedback_keys: Feedback keys to assign to run. - on_disconnect: The disconnect mode to use. - Must be one of 'cancel' or 'continue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - headers: Optional custom headers to include with the request. - on_run_created: Optional callback to call when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - - Returns: - Iterator of stream results. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - async for chunk in client.runs.stream( - thread_id=None, - assistant_id="agent", - input={"messages": [{"role": "user", "content": "how are you?"}]}, - stream_mode=["values","debug"], - metadata={"name":"my_run"}, - context={"model_name": "anthropic"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - feedback_keys=["my_feedback_key_1","my_feedback_key_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ): - print(chunk) - ``` - ```shell - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ - - StreamPart(event='metadata', data={'run_id': '1ef4a9b8-d7da-679a-a45a-872054341df2'}) - StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}]}) - StreamPart(event='values', data={'messages': [{'content': 'how are you?', 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'human', 'name': None, 'id': 'fe0a5778-cfe9-42ee-b807-0adaa1873c10', 'example': False}, {'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", 'additional_kwargs': {}, 'response_metadata': {}, 'type': 'ai', 'name': None, 'id': 'run-159b782c-b679-4830-83c6-cef87798fe8b', 'example': False, 'tool_calls': [], 'invalid_tool_calls': [], 'usage_metadata': None}]}) - StreamPart(event='end', data=None) - ``` - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "config": config, - "context": context, - "metadata": metadata, - "stream_mode": stream_mode, - "stream_subgraphs": stream_subgraphs, - "stream_resumable": stream_resumable, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "feedback_keys": feedback_keys, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "checkpoint_during": checkpoint_during, - "multitask_strategy": multitask_strategy, - "if_not_exists": if_not_exists, - "on_disconnect": on_disconnect, - "on_completion": on_completion, - "after_seconds": after_seconds, - "durability": durability, - } - endpoint = ( - f"/threads/{thread_id}/runs/stream" - if thread_id is not None - else "/runs/stream" - ) - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - return self.http.stream( - endpoint, - "POST", - json={k: v for k, v in payload.items() if v is not None}, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - - @overload - def create( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Run: ... - - @overload - def create( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> Run: ... - - def create( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - stream_mode: StreamMode | Sequence[StreamMode] = "values", - stream_subgraphs: bool = False, - stream_resumable: bool = False, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, # deprecated - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - on_completion: OnCompletionBehavior | None = None, - after_seconds: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> Run: - """Create a background run. - - Args: - thread_id: the thread ID to assign to the thread. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to stream from. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: The command to execute. - stream_mode: The stream mode(s) to use. - stream_subgraphs: Whether to stream output from subgraphs. - stream_resumable: Whether the stream is considered resumable. - If true, the stream can be resumed and replayed in its entirety even after disconnection. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - headers: Optional custom headers to include with the request. - on_run_created: Optional callback to call when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - Returns: - The created background `Run`. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - background_run = client.runs.create( - thread_id="my_thread_id", - assistant_id="my_assistant_id", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ) - print(background_run) - ``` - - ```shell - -------------------------------------------------------------------------------- - - { - 'run_id': 'my_run_id', - 'thread_id': 'my_thread_id', - 'assistant_id': 'my_assistant_id', - 'created_at': '2024-07-25T15:35:42.598503+00:00', - 'updated_at': '2024-07-25T15:35:42.598503+00:00', - 'metadata': {}, - 'status': 'pending', - 'kwargs': - { - 'input': - { - 'messages': [ - { - 'role': 'user', - 'content': 'how are you?' - } - ] - }, - 'config': - { - 'metadata': - { - 'created_by': 'system' - }, - 'configurable': - { - 'run_id': 'my_run_id', - 'user_id': None, - 'graph_id': 'agent', - 'thread_id': 'my_thread_id', - 'checkpoint_id': None, - 'assistant_id': 'my_assistant_id' - } - }, - 'context': - { - 'model_name': 'openai' - }, - 'webhook': "https://my.fake.webhook.com", - 'temporary': False, - 'stream_mode': ['values'], - 'feedback_keys': None, - 'interrupt_after': ["node_to_stop_after_1","node_to_stop_after_2"], - 'interrupt_before': ["node_to_stop_before_1","node_to_stop_before_2"] - }, - 'multitask_strategy': 'interrupt' - } - ``` - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "stream_mode": stream_mode, - "stream_subgraphs": stream_subgraphs, - "stream_resumable": stream_resumable, - "config": config, - "context": context, - "metadata": metadata, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "checkpoint_during": checkpoint_during, - "multitask_strategy": multitask_strategy, - "if_not_exists": if_not_exists, - "on_completion": on_completion, - "after_seconds": after_seconds, - "durability": durability, - } - payload = {k: v for k, v in payload.items() if v is not None} - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - return self.http.post( - f"/threads/{thread_id}/runs" if thread_id else "/runs", - json=payload, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - - def create_batch( - self, - payloads: list[RunCreate], - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Run]: - """Create a batch of stateless background runs.""" - - def filter_payload(payload: RunCreate): - return {k: v for k, v in payload.items() if v is not None} - - filtered = [filter_payload(payload) for payload in payloads] - return self.http.post( - "/runs/batch", json=filtered, headers=headers, params=params - ) - - @overload - def wait( - self, - thread_id: str, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> list[dict] | dict[str, Any]: ... - - @overload - def wait( - self, - thread_id: None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - ) -> list[dict] | dict[str, Any]: ... - - def wait( - self, - thread_id: str | None, - assistant_id: str, - *, - input: Input | None = None, - command: Command | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, # deprecated - checkpoint: Checkpoint | None = None, - checkpoint_id: str | None = None, - interrupt_before: All | Sequence[str] | None = None, - interrupt_after: All | Sequence[str] | None = None, - webhook: str | None = None, - on_disconnect: DisconnectMode | None = None, - on_completion: OnCompletionBehavior | None = None, - multitask_strategy: MultitaskStrategy | None = None, - if_not_exists: IfNotExists | None = None, - after_seconds: int | None = None, - raise_error: bool = True, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - on_run_created: Callable[[RunCreateMetadata], None] | None = None, - durability: Durability | None = None, - ) -> list[dict] | dict[str, Any]: - """Create a run, wait until it finishes and return the final state. - - Args: - thread_id: the thread ID to create the run on. - If `None` will create a stateless run. - assistant_id: The assistant ID or graph name to run. - If using graph name, will default to first assistant created from that graph. - input: The input to the graph. - command: The command to execute. - metadata: Metadata to assign to the run. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint: The checkpoint to resume from. - checkpoint_during: (deprecated) Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - on_disconnect: The disconnect mode to use. - Must be one of 'cancel' or 'continue'. - on_completion: Whether to delete or keep the thread created for a stateless run. - Must be one of 'delete' or 'keep'. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - if_not_exists: How to handle missing thread. Defaults to 'reject'. - Must be either 'reject' (raise error if missing), or 'create' (create new thread). - after_seconds: The number of seconds to wait before starting the run. - Use to schedule future runs. - raise_error: Whether to raise an error if the run fails. - headers: Optional custom headers to include with the request. - on_run_created: Optional callback to call when a run is created. - durability: The durability to use for the run. Values are "sync", "async", or "exit". - "async" means checkpoints are persisted async while next graph step executes, replaces checkpoint_during=True - "sync" means checkpoints are persisted sync after graph step executes, replaces checkpoint_during=False - "exit" means checkpoints are only persisted when the run exits, does not save intermediate steps - - Returns: - The output of the `Run`. - - ???+ example "Example Usage" - - ```python - - final_state_of_run = client.runs.wait( - thread_id=None, - assistant_id="agent", - input={"messages": [{"role": "user", "content": "how are you?"}]}, - metadata={"name":"my_run"}, - context={"model_name": "anthropic"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt" - ) - print(final_state_of_run) - ``` - - ```shell - - ------------------------------------------------------------------------------------------------------------------------------------------- - - { - 'messages': [ - { - 'content': 'how are you?', - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'human', - 'name': None, - 'id': 'f51a862c-62fe-4866-863b-b0863e8ad78a', - 'example': False - }, - { - 'content': "I'm doing well, thanks for asking! I'm an AI assistant created by Anthropic to be helpful, honest, and harmless.", - 'additional_kwargs': {}, - 'response_metadata': {}, - 'type': 'ai', - 'name': None, - 'id': 'run-bf1cd3c6-768f-4c16-b62d-ba6f17ad8b36', - 'example': False, - 'tool_calls': [], - 'invalid_tool_calls': [], - 'usage_metadata': None - } - ] - } - ``` - - """ - if checkpoint_during is not None: - warnings.warn( - "`checkpoint_during` is deprecated and will be removed in a future version. Use `durability` instead.", - DeprecationWarning, - stacklevel=2, - ) - payload = { - "input": input, - "command": ( - {k: v for k, v in command.items() if v is not None} if command else None - ), - "config": config, - "context": context, - "metadata": metadata, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "checkpoint": checkpoint, - "checkpoint_id": checkpoint_id, - "multitask_strategy": multitask_strategy, - "if_not_exists": if_not_exists, - "on_disconnect": on_disconnect, - "checkpoint_during": checkpoint_during, - "on_completion": on_completion, - "after_seconds": after_seconds, - "raise_error": raise_error, - "durability": durability, - } - - def on_response(res: httpx.Response): - """Callback function to handle the response.""" - if on_run_created and (metadata := _get_run_metadata_from_response(res)): - on_run_created(metadata) - - endpoint = ( - f"/threads/{thread_id}/runs/wait" if thread_id is not None else "/runs/wait" - ) - return self.http.request_reconnect( - endpoint, - "POST", - json={k: v for k, v in payload.items() if v is not None}, - params=params, - headers=headers, - on_response=on_response if on_run_created else None, - ) - - def list( - self, - thread_id: str, - *, - limit: int = 10, - offset: int = 0, - status: RunStatus | None = None, - select: list[RunSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Run]: - """List runs. - - Args: - thread_id: The thread ID to list runs for. - limit: The maximum number of results to return. - offset: The number of results to skip. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The runs for the thread. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.runs.list( - thread_id="thread_id", - limit=5, - offset=5, - ) - ``` - - """ - query_params: dict[str, Any] = {"limit": limit, "offset": offset} - if status is not None: - query_params["status"] = status - if select: - query_params["select"] = select - if params: - query_params.update(params) - return self.http.get( - f"/threads/{thread_id}/runs", params=query_params, headers=headers - ) - - def get( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Get a run. - - Args: - thread_id: The thread ID to get. - run_id: The run ID to get. - headers: Optional custom headers to include with the request. - - Returns: - `Run` object. - - ???+ example "Example Usage" - - ```python - - run = client.runs.get( - thread_id="thread_id_to_delete", - run_id="run_id_to_delete", - ) - ``` - """ - - return self.http.get( - f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params - ) - - def cancel( - self, - thread_id: str, - run_id: str, - *, - wait: bool = False, - action: CancelAction = "interrupt", - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Get a run. - - Args: - thread_id: The thread ID to cancel. - run_id: The run ID to cancel. - wait: Whether to wait until run has completed. - action: Action to take when cancelling the run. Possible values - are `interrupt` or `rollback`. Default is `interrupt`. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.runs.cancel( - thread_id="thread_id_to_cancel", - run_id="run_id_to_cancel", - wait=True, - action="interrupt" - ) - ``` - - """ - query_params = { - "wait": 1 if wait else 0, - "action": action, - } - if params: - query_params.update(params) - if wait: - return self.http.request_reconnect( - f"/threads/{thread_id}/runs/{run_id}/cancel", - "POST", - json=None, - params=query_params, - headers=headers, - ) - return self.http.post( - f"/threads/{thread_id}/runs/{run_id}/cancel", - json=None, - params=query_params, - headers=headers, - ) - - def join( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> dict: - """Block until a run is done. Returns the final state of the thread. - - Args: - thread_id: The thread ID to join. - run_id: The run ID to join. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.runs.join( - thread_id="thread_id_to_join", - run_id="run_id_to_join" - ) - ``` - - """ - return self.http.request_reconnect( - f"/threads/{thread_id}/runs/{run_id}/join", - "GET", - headers=headers, - params=params, - ) - - def join_stream( - self, - thread_id: str, - run_id: str, - *, - cancel_on_disconnect: bool = False, - stream_mode: StreamMode | Sequence[StreamMode] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - last_event_id: str | None = None, - ) -> Iterator[StreamPart]: - """Stream output from a run in real-time, until the run is done. - Output is not buffered, so any output produced before this call will - not be received here. - - Args: - thread_id: The thread ID to join. - run_id: The run ID to join. - stream_mode: The stream mode(s) to use. Must be a subset of the stream modes passed - when creating the run. Background runs default to having the union of all - stream modes. - cancel_on_disconnect: Whether to cancel the run when the stream is disconnected. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - last_event_id: The last event ID to use for the stream. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.runs.join_stream( - thread_id="thread_id_to_join", - run_id="run_id_to_join", - stream_mode=["values", "debug"] - ) - ``` - - """ - query_params = { - "stream_mode": stream_mode, - "cancel_on_disconnect": cancel_on_disconnect, - } - if params: - query_params.update(params) - return self.http.stream( - f"/threads/{thread_id}/runs/{run_id}/stream", - "GET", - params=query_params, - headers={ - **({"Last-Event-ID": last_event_id} if last_event_id else {}), - **(headers or {}), - } - or None, - ) - - def delete( - self, - thread_id: str, - run_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a run. - - Args: - thread_id: The thread ID to delete. - run_id: The run ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:2024") - client.runs.delete( - thread_id="thread_id_to_delete", - run_id="run_id_to_delete" - ) - ``` - - """ - self.http.delete( - f"/threads/{thread_id}/runs/{run_id}", headers=headers, params=params - ) - - -class SyncCronClient: - """Synchronous client for managing cron jobs in LangGraph. - - This class provides methods to create and manage scheduled tasks (cron jobs) for automated graph executions. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:8123") - cron_job = client.crons.create_for_thread(thread_id="thread_123", assistant_id="asst_456", schedule="0 * * * *") - ``` - - !!! note "Feature Availability" - - The crons client functionality is not supported on all licenses. - Please check the relevant license documentation for the most up-to-date - details on feature availability. - """ - - def __init__(self, http_client: SyncHttpClient) -> None: - self.http = http_client - - def create_for_thread( - self, - thread_id: str, - assistant_id: str, - *, - schedule: str, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - webhook: str | None = None, - multitask_strategy: str | None = None, - end_time: datetime | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Create a cron job for a thread. - - Args: - thread_id: the thread ID to run the cron job on. - assistant_id: The assistant ID or graph name to use for the cron job. - If using graph name, will default to first assistant created from that graph. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. - enabled: Whether the cron job is enabled. By default, it is considered enabled. - headers: Optional custom headers to include with the request. - - Returns: - The cron `Run`. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - cron_run = client.crons.create_for_thread( - thread_id="my-thread-id", - assistant_id="agent", - schedule="27 15 * * *", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt", - enabled=True - ) - ``` - """ - payload = { - "schedule": schedule, - "input": input, - "config": config, - "metadata": metadata, - "context": context, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "checkpoint_during": checkpoint_during, - "webhook": webhook, - "multitask_strategy": multitask_strategy, - "end_time": end_time.isoformat() if end_time else None, - "enabled": enabled, - } - payload = {k: v for k, v in payload.items() if v is not None} - return self.http.post( - f"/threads/{thread_id}/runs/crons", - json=payload, - headers=headers, - params=params, - ) - - def create( - self, - assistant_id: str, - *, - schedule: str, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - checkpoint_during: bool | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - webhook: str | None = None, - on_run_completed: OnCompletionBehavior | None = None, - multitask_strategy: str | None = None, - end_time: datetime | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Run: - """Create a cron run. - - Args: - assistant_id: The assistant ID or graph name to use for the cron job. - If using graph name, will default to first assistant created from that graph. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context to add to the assistant. - !!! version-added "Added in version 0.6.0" - checkpoint_during: Whether to checkpoint during the run (or only at the end/interruption). - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to Nodes to interrupt immediately after they get executed. - webhook: Webhook to call after LangGraph API call is done. - on_run_completed: What to do with the thread after the run completes. - Must be one of 'delete' (default) or 'keep'. 'delete' removes the thread - after execution. 'keep' creates a new thread for each execution but does not - clean them up. Clients are responsible for cleaning up kept threads. - multitask_strategy: Multitask strategy to use. - Must be one of 'reject', 'interrupt', 'rollback', or 'enqueue'. - end_time: The time to stop running the cron job. If not provided, the cron job will run indefinitely. - enabled: Whether the cron job is enabled. By default, it is considered enabled. - headers: Optional custom headers to include with the request. - - Returns: - The cron `Run`. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - cron_run = client.crons.create( - assistant_id="agent", - schedule="27 15 * * *", - input={"messages": [{"role": "user", "content": "hello!"}]}, - metadata={"name":"my_run"}, - context={"model_name": "openai"}, - checkpoint_during=True, - interrupt_before=["node_to_stop_before_1","node_to_stop_before_2"], - interrupt_after=["node_to_stop_after_1","node_to_stop_after_2"], - webhook="https://my.fake.webhook.com", - multitask_strategy="interrupt", - enabled=True - ) - ``` - - """ - payload = { - "schedule": schedule, - "input": input, - "config": config, - "metadata": metadata, - "context": context, - "assistant_id": assistant_id, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "webhook": webhook, - "checkpoint_during": checkpoint_during, - "on_run_completed": on_run_completed, - "multitask_strategy": multitask_strategy, - "end_time": end_time.isoformat() if end_time else None, - "enabled": enabled, - } - payload = {k: v for k, v in payload.items() if v is not None} - return self.http.post( - "/runs/crons", json=payload, headers=headers, params=params - ) - - def delete( - self, - cron_id: str, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete a cron. - - Args: - cron_id: The cron ID to delete. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - client.crons.delete( - cron_id="cron_to_delete" - ) - ``` - - """ - self.http.delete(f"/runs/crons/{cron_id}", headers=headers, params=params) - - def update( - self, - cron_id: str, - *, - schedule: str | None = None, - end_time: datetime | None = None, - input: Input | None = None, - metadata: Mapping[str, Any] | None = None, - config: Config | None = None, - context: Context | None = None, - webhook: str | None = None, - interrupt_before: All | list[str] | None = None, - interrupt_after: All | list[str] | None = None, - on_run_completed: OnCompletionBehavior | None = None, - enabled: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Cron: - """Update a cron job by ID. - - Args: - cron_id: The cron ID to update. - schedule: The cron schedule to execute this job on. - Schedules are interpreted in UTC. - end_time: The end date to stop running the cron. - input: The input to the graph. - metadata: Metadata to assign to the cron job runs. - config: The configuration for the assistant. - context: Static context added to the assistant. - webhook: Webhook to call after LangGraph API call is done. - interrupt_before: Nodes to interrupt immediately before they get executed. - interrupt_after: Nodes to interrupt immediately after they get executed. - on_run_completed: What to do with the thread after the run completes. - Must be one of 'delete' or 'keep'. 'delete' removes the thread - after execution. 'keep' creates a new thread for each execution but does not - clean them up. - enabled: Enable or disable the cron job. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - The updated cron job. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - updated_cron = client.crons.update( - cron_id="1ef3cefa-4c09-6926-96d0-3dc97fd5e39b", - schedule="0 10 * * *", - enabled=False, - ) - ``` - - """ - payload = { - "schedule": schedule, - "end_time": end_time.isoformat() if end_time else None, - "input": input, - "metadata": metadata, - "config": config, - "context": context, - "webhook": webhook, - "interrupt_before": interrupt_before, - "interrupt_after": interrupt_after, - "on_run_completed": on_run_completed, - "enabled": enabled, - } - payload = {k: v for k, v in payload.items() if v is not None} - return self.http.patch( - f"/runs/crons/{cron_id}", - json=payload, - headers=headers, - params=params, - ) - - def search( - self, - *, - assistant_id: str | None = None, - thread_id: str | None = None, - enabled: bool | None = None, - limit: int = 10, - offset: int = 0, - sort_by: CronSortBy | None = None, - sort_order: SortOrder | None = None, - select: list[CronSelectField] | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> list[Cron]: - """Get a list of cron jobs. - - Args: - assistant_id: The assistant ID or graph name to search for. - thread_id: the thread ID to search for. - enabled: Whether the cron job is enabled. - limit: The maximum number of results to return. - offset: The number of results to skip. - headers: Optional custom headers to include with the request. - - Returns: - The list of cron jobs returned by the search, - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - cron_jobs = client.crons.search( - assistant_id="my_assistant_id", - thread_id="my_thread_id", - enabled=True, - limit=5, - offset=5, - ) - print(cron_jobs) - ``` - - ```shell - ---------------------------------------------------------- - - [ - { - 'cron_id': '1ef3cefa-4c09-6926-96d0-3dc97fd5e39b', - 'assistant_id': 'my_assistant_id', - 'thread_id': 'my_thread_id', - 'user_id': None, - 'payload': - { - 'input': {'start_time': ''}, - 'schedule': '4 * * * *', - 'assistant_id': 'my_assistant_id' - }, - 'schedule': '4 * * * *', - 'next_run_date': '2024-07-25T17:04:00+00:00', - 'end_time': None, - 'created_at': '2024-07-08T06:02:23.073257+00:00', - 'updated_at': '2024-07-08T06:02:23.073257+00:00' - } - ] - ``` - """ - payload = { - "assistant_id": assistant_id, - "thread_id": thread_id, - "enabled": enabled, - "limit": limit, - "offset": offset, - } - if sort_by: - payload["sort_by"] = sort_by - if sort_order: - payload["sort_order"] = sort_order - if select: - payload["select"] = select - payload = {k: v for k, v in payload.items() if v is not None} - return self.http.post( - "/runs/crons/search", json=payload, headers=headers, params=params - ) - - def count( - self, - *, - assistant_id: str | None = None, - thread_id: str | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> int: - """Count cron jobs matching filters. - - Args: - assistant_id: Assistant ID to filter by. - thread_id: Thread ID to filter by. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - int: Number of crons matching the criteria. - """ - payload: dict[str, Any] = {} - if assistant_id: - payload["assistant_id"] = assistant_id - if thread_id: - payload["thread_id"] = thread_id - return self.http.post( - "/runs/crons/count", json=payload, headers=headers, params=params - ) - - -class SyncStoreClient: - """A client for synchronous operations on a key-value store. - - Provides methods to interact with a remote key-value store, allowing - storage and retrieval of items within namespaced hierarchies. - - ???+ example "Example" - - ```python - client = get_sync_client(url="http://localhost:2024")) - client.store.put_item(["users", "profiles"], "user123", {"name": "Alice", "age": 30}) - ``` - """ - - def __init__(self, http: SyncHttpClient) -> None: - self.http = http - - def put_item( - self, - namespace: Sequence[str], - /, - key: str, - value: Mapping[str, Any], - index: Literal[False] | list[str] | None = None, - ttl: int | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Store or update an item. - - Args: - namespace: A list of strings representing the namespace path. - key: The unique identifier for the item within the namespace. - value: A dictionary containing the item's data. - index: Controls search indexing - None (use defaults), False (disable), or list of field paths to index. - ttl: Optional time-to-live in minutes for the item, or None for no expiration. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - client.store.put_item( - ["documents", "user123"], - key="item456", - value={"title": "My Document", "content": "Hello World"} - ) - ``` - """ - for label in namespace: - if "." in label: - raise ValueError( - f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." - ) - payload = { - "namespace": namespace, - "key": key, - "value": value, - "index": index, - "ttl": ttl, - } - self.http.put( - "/store/items", json=_provided_vals(payload), headers=headers, params=params - ) - - def get_item( - self, - namespace: Sequence[str], - /, - key: str, - *, - refresh_ttl: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> Item: - """Retrieve a single item. - - Args: - key: The unique identifier for the item. - namespace: Optional list of strings representing the namespace path. - refresh_ttl: Whether to refresh the TTL on this read operation. If `None`, uses the store's default behavior. - headers: Optional custom headers to include with the request. - - Returns: - The retrieved item. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - item = client.store.get_item( - ["documents", "user123"], - key="item456", - ) - print(item) - ``` - - ```shell - ---------------------------------------------------------------- - - { - 'namespace': ['documents', 'user123'], - 'key': 'item456', - 'value': {'title': 'My Document', 'content': 'Hello World'}, - 'created_at': '2024-07-30T12:00:00Z', - 'updated_at': '2024-07-30T12:00:00Z' - } - ``` - """ - for label in namespace: - if "." in label: - raise ValueError( - f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')." - ) - - query_params = {"key": key, "namespace": ".".join(namespace)} - if refresh_ttl is not None: - query_params["refresh_ttl"] = refresh_ttl - if params: - query_params.update(params) - return self.http.get("/store/items", params=query_params, headers=headers) - - def delete_item( - self, - namespace: Sequence[str], - /, - key: str, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> None: - """Delete an item. - - Args: - key: The unique identifier for the item. - namespace: Optional list of strings representing the namespace path. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - `None` - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - client.store.delete_item( - ["documents", "user123"], - key="item456", - ) - ``` - """ - self.http.delete( - "/store/items", - json={"key": key, "namespace": namespace}, - headers=headers, - params=params, - ) - - def search_items( - self, - namespace_prefix: Sequence[str], - /, - filter: Mapping[str, Any] | None = None, - limit: int = 10, - offset: int = 0, - query: str | None = None, - refresh_ttl: bool | None = None, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> SearchItemsResponse: - """Search for items within a namespace prefix. - - Args: - namespace_prefix: List of strings representing the namespace prefix. - filter: Optional dictionary of key-value pairs to filter results. - limit: Maximum number of items to return (default is 10). - offset: Number of items to skip before returning results (default is 0). - query: Optional query for natural language search. - refresh_ttl: Whether to refresh the TTL on items returned by this search. If `None`, uses the store's default behavior. - headers: Optional custom headers to include with the request. - params: Optional query parameters to include with the request. - - Returns: - A list of items matching the search criteria. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - items = client.store.search_items( - ["documents"], - filter={"author": "John Doe"}, - limit=5, - offset=0 - ) - print(items) - ``` - ```shell - ---------------------------------------------------------------- - - { - "items": [ - { - "namespace": ["documents", "user123"], - "key": "item789", - "value": { - "title": "Another Document", - "author": "John Doe" - }, - "created_at": "2024-07-30T12:00:00Z", - "updated_at": "2024-07-30T12:00:00Z" - }, - # ... additional items ... - ] - } - ``` - """ - payload = { - "namespace_prefix": namespace_prefix, - "filter": filter, - "limit": limit, - "offset": offset, - "query": query, - "refresh_ttl": refresh_ttl, - } - return self.http.post( - "/store/items/search", - json=_provided_vals(payload), - headers=headers, - params=params, - ) - - def list_namespaces( - self, - prefix: list[str] | None = None, - suffix: list[str] | None = None, - max_depth: int | None = None, - limit: int = 100, - offset: int = 0, - *, - headers: Mapping[str, str] | None = None, - params: QueryParamTypes | None = None, - ) -> ListNamespaceResponse: - """List namespaces with optional match conditions. - - Args: - prefix: Optional list of strings representing the prefix to filter namespaces. - suffix: Optional list of strings representing the suffix to filter namespaces. - max_depth: Optional integer specifying the maximum depth of namespaces to return. - limit: Maximum number of namespaces to return (default is 100). - offset: Number of namespaces to skip before returning results (default is 0). - headers: Optional custom headers to include with the request. - - Returns: - A list of namespaces matching the criteria. - - ???+ example "Example Usage" - - ```python - client = get_sync_client(url="http://localhost:8123") - namespaces = client.store.list_namespaces( - prefix=["documents"], - max_depth=3, - limit=10, - offset=0 - ) - print(namespaces) - ``` - - ```shell - ---------------------------------------------------------------- - - [ - ["documents", "user123", "reports"], - ["documents", "user456", "invoices"], - ... - ] - ``` - """ - payload = { - "prefix": prefix, - "suffix": suffix, - "max_depth": max_depth, - "limit": limit, - "offset": offset, - } - return self.http.post( - "/store/namespaces", - json=_provided_vals(payload), - headers=headers, - params=params, - ) - - -def _provided_vals(d: Mapping[str, Any]) -> dict[str, Any]: - return {k: v for k, v in d.items() if v is not None} - - -_registered_transports: list[httpx.ASGITransport] = [] - - -# Do not move; this is used in the server. -def configure_loopback_transports(app: Any) -> None: - for transport in _registered_transports: - transport.app = app - - -@functools.lru_cache(maxsize=1) -def get_asgi_transport() -> type[httpx.ASGITransport]: - try: - from langgraph_api import asgi_transport # type: ignore[unresolved-import] - - return asgi_transport.ASGITransport - except ImportError: - # Older versions of the server - return httpx.ASGITransport - - -TimeoutTypes = ( - None - | float - | tuple[float | None, float | None] - | tuple[float | None, float | None, float | None, float | None] - | httpx.Timeout -) +from langgraph_sdk._async.assistants import AssistantsClient + +# Re-export factory functions +# Re-export async clients +from langgraph_sdk._async.client import LangGraphClient, get_client +from langgraph_sdk._async.cron import CronClient +from langgraph_sdk._async.http import HttpClient, _adecode_json, _aencode_json +from langgraph_sdk._async.runs import RunsClient +from langgraph_sdk._async.store import StoreClient +from langgraph_sdk._async.threads import ThreadsClient +from langgraph_sdk._shared.utilities import configure_loopback_transports +from langgraph_sdk._sync.assistants import SyncAssistantsClient + +# Re-export sync clients +from langgraph_sdk._sync.client import SyncLangGraphClient, get_sync_client +from langgraph_sdk._sync.cron import SyncCronClient +from langgraph_sdk._sync.http import SyncHttpClient, _decode_json, _encode_json +from langgraph_sdk._sync.runs import SyncRunsClient +from langgraph_sdk._sync.store import SyncStoreClient +from langgraph_sdk._sync.threads import SyncThreadsClient + +__all__ = [ + "AssistantsClient", + "CronClient", + "HttpClient", + "LangGraphClient", + "RunsClient", + "StoreClient", + "SyncAssistantsClient", + "SyncCronClient", + "SyncHttpClient", + "SyncLangGraphClient", + "SyncRunsClient", + "SyncStoreClient", + "SyncThreadsClient", + "ThreadsClient", + "_adecode_json", + "_aencode_json", + "_decode_json", + "_encode_json", + "configure_loopback_transports", + "get_client", + "get_sync_client", +] diff --git a/libs/sdk-py/tests/test_client_exports.py b/libs/sdk-py/tests/test_client_exports.py new file mode 100644 index 000000000..ac45d5a0a --- /dev/null +++ b/libs/sdk-py/tests/test_client_exports.py @@ -0,0 +1,91 @@ +"""Test that all expected symbols are exported from langgraph_sdk.client. + +This test ensures backwards compatibility during refactoring. +""" + +import httpx + +from langgraph_sdk import get_client as public_get_client +from langgraph_sdk import get_sync_client as public_get_sync_client +from langgraph_sdk.client import ( + AssistantsClient, + CronClient, + HttpClient, + LangGraphClient, + RunsClient, + StoreClient, + SyncAssistantsClient, + SyncCronClient, + SyncHttpClient, + SyncLangGraphClient, + SyncRunsClient, + SyncStoreClient, + SyncThreadsClient, + ThreadsClient, + _adecode_json, + _aencode_json, + _decode_json, + _encode_json, + configure_loopback_transports, + get_client, + get_sync_client, +) + + +def test_client_exports(): + """Verify all expected symbols can be imported from langgraph_sdk.client.""" + # Factory functions (public API) + assert callable(get_client) + assert callable(get_sync_client) + + # Top-level client classes + assert LangGraphClient is not None + assert SyncLangGraphClient is not None + + # HTTP client classes + assert HttpClient is not None + assert SyncHttpClient is not None + + # Resource client classes - Async + assert AssistantsClient is not None + assert ThreadsClient is not None + assert RunsClient is not None + assert CronClient is not None + assert StoreClient is not None + + # Resource client classes - Sync + assert SyncAssistantsClient is not None + assert SyncThreadsClient is not None + assert SyncRunsClient is not None + assert SyncCronClient is not None + assert SyncStoreClient is not None + + # Internal utilities (used by tests) + assert callable(_aencode_json) + assert callable(_adecode_json) + + # Sync JSON utilities (might be used internally) + assert callable(_encode_json) + assert callable(_decode_json) + + # Loopback transport configuration (used by langgraph-api) + assert callable(configure_loopback_transports) + + +def test_public_api_exports(): + """Verify public API exports from langgraph_sdk package.""" + assert callable(public_get_client) + assert callable(public_get_sync_client) + + +def test_client_instantiation(): + """Verify that we can instantiate clients.""" + # Test async client instantiation + async_http = httpx.AsyncClient(base_url="http://test.example.com") + async_client = HttpClient(async_http) + assert async_client is not None + + # Test sync client instantiation + sync_http = httpx.Client(base_url="http://test.example.com") + sync_client = SyncHttpClient(sync_http) + assert sync_client is not None