mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-17 21:25:46 +02:00
chore: Re-organize client files. (#6787)
Additional guidelines: - Make sure optional dependencies are imported within a function. - Please do not add dependencies to `pyproject.toml` files (even optional ones) unless they are **required** for unit tests. - Most PRs should not touch more than one package. - Changes should be backwards compatible. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
f5e56e200d
commit
9f0ae94f27
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.<model>.<method_name>()
|
||||
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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Shared utilities for async and sync clients."""
|
||||
@@ -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]
|
||||
)
|
||||
@@ -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<thread_id>.+))?\/runs\/(?P<run_id>.+)"
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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.<model>.<method_name>()
|
||||
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()
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
+44
-7081
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
Reference in New Issue
Block a user