initial commit
This commit is contained in:
8
venv/Lib/site-packages/langgraph_sdk/__init__.py
Normal file
8
venv/Lib/site-packages/langgraph_sdk/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
||||
from langgraph_sdk.auth import Auth
|
||||
from langgraph_sdk.client import get_client, get_sync_client
|
||||
from langgraph_sdk.encryption import Encryption
|
||||
from langgraph_sdk.encryption.types import EncryptionContext
|
||||
|
||||
__version__ = "0.3.9"
|
||||
|
||||
__all__ = ["Auth", "Encryption", "EncryptionContext", "get_client", "get_sync_client"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
20
venv/Lib/site-packages/langgraph_sdk/_async/__init__.py
Normal file
20
venv/Lib/site-packages/langgraph_sdk/_async/__init__.py
Normal file
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
733
venv/Lib/site-packages/langgraph_sdk/_async/assistants.py
Normal file
733
venv/Lib/site-packages/langgraph_sdk/_async/assistants.py
Normal file
@@ -0,0 +1,733 @@
|
||||
"""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, **dict(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 is not None:
|
||||
payload["config"] = config
|
||||
if context is not None:
|
||||
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,
|
||||
*,
|
||||
delete_threads: bool = False,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> None:
|
||||
"""Delete an assistant.
|
||||
|
||||
Args:
|
||||
assistant_id: The assistant ID to delete.
|
||||
delete_threads: If true, delete all threads with `metadata.assistant_id`
|
||||
matching this assistant, along with runs and checkpoints belonging to
|
||||
those threads.
|
||||
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"
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
query_params: dict[str, Any] = {}
|
||||
if delete_threads:
|
||||
query_params["delete_threads"] = True
|
||||
if params:
|
||||
query_params.update(params)
|
||||
await self.http.delete(
|
||||
f"/assistants/{assistant_id}",
|
||||
headers=headers,
|
||||
params=query_params or None,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
178
venv/Lib/site-packages/langgraph_sdk/_async/client.py
Normal file
178
venv/Lib/site-packages/langgraph_sdk/_async/client.py
Normal file
@@ -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") # type: ignore[invalid-argument-type]
|
||||
_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") # type: ignore[invalid-argument-type]
|
||||
_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()
|
||||
511
venv/Lib/site-packages/langgraph_sdk/_async/cron.py
Normal file
511
venv/Lib/site-packages/langgraph_sdk/_async/cron.py
Normal file
@@ -0,0 +1,511 @@
|
||||
"""Async client for managing recurrent runs (cron jobs) in LangGraph."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
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,
|
||||
Durability,
|
||||
Input,
|
||||
OnCompletionBehavior,
|
||||
QueryParamTypes,
|
||||
Run,
|
||||
SortOrder,
|
||||
StreamMode,
|
||||
)
|
||||
|
||||
|
||||
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, # deprecated
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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: (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'.
|
||||
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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of '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
|
||||
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,
|
||||
)
|
||||
```
|
||||
"""
|
||||
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 = {
|
||||
"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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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, # deprecated
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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: (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_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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of '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
|
||||
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,
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
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 = {
|
||||
"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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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
|
||||
)
|
||||
305
venv/Lib/site-packages/langgraph_sdk/_async/http.py
Normal file
305
venv/Lib/site-packages/langgraph_sdk/_async/http.py
Normal file
@@ -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
|
||||
)
|
||||
1078
venv/Lib/site-packages/langgraph_sdk/_async/runs.py
Normal file
1078
venv/Lib/site-packages/langgraph_sdk/_async/runs.py
Normal file
File diff suppressed because it is too large
Load Diff
313
venv/Lib/site-packages/langgraph_sdk/_async/store.py
Normal file
313
venv/Lib/site-packages/langgraph_sdk/_async/store.py
Normal file
@@ -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, **dict(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,
|
||||
)
|
||||
732
venv/Lib/site-packages/langgraph_sdk/_async/threads.py
Normal file
732
venv/Lib/site-packages/langgraph_sdk/_async/threads.py
Normal file
@@ -0,0 +1,732 @@
|
||||
"""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,
|
||||
PruneStrategy,
|
||||
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,
|
||||
*,
|
||||
include: Sequence[str] | None = None,
|
||||
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.
|
||||
include: Additional fields to include in the response.
|
||||
Supported values: `"ttl"`.
|
||||
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'}
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
query_params: dict[str, Any] = {}
|
||||
if include:
|
||||
query_params["include"] = ",".join(include)
|
||||
if params:
|
||||
query_params.update(params)
|
||||
return await self.http.get(
|
||||
f"/threads/{thread_id}",
|
||||
headers=headers,
|
||||
params=query_params or None,
|
||||
)
|
||||
|
||||
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,
|
||||
extract: dict[str, str] | 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.
|
||||
select: List of fields to include in the response.
|
||||
extract: Dictionary mapping aliases to JSONB paths to extract
|
||||
from thread data. Paths use dot notation for nested keys and
|
||||
bracket notation for array indices (e.g.,
|
||||
`{"last_msg": "values.messages[-1]"}`). Extracted values are
|
||||
returned in an `extracted` field on each thread. Maximum 10
|
||||
paths per request.
|
||||
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
|
||||
if extract:
|
||||
payload["extract"] = extract
|
||||
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 prune(
|
||||
self,
|
||||
thread_ids: Sequence[str],
|
||||
*,
|
||||
strategy: PruneStrategy = "delete",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prune threads by ID.
|
||||
|
||||
Args:
|
||||
thread_ids: List of thread IDs to prune.
|
||||
strategy: The prune strategy. `"delete"` removes threads entirely.
|
||||
`"keep_latest"` prunes old checkpoints but keeps threads and their
|
||||
latest state. Defaults to `"delete"`.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
Returns:
|
||||
A dict containing `pruned_count` (number of threads pruned).
|
||||
|
||||
???+ example "Example Usage"
|
||||
|
||||
```python
|
||||
client = get_client(url="http://localhost:2024")
|
||||
result = await client.threads.prune(
|
||||
thread_ids=["thread_1", "thread_2"],
|
||||
)
|
||||
print(result) # {'pruned_count': 2}
|
||||
```
|
||||
|
||||
"""
|
||||
payload: dict[str, Any] = {
|
||||
"thread_ids": thread_ids,
|
||||
}
|
||||
if strategy != "delete":
|
||||
payload["strategy"] = strategy
|
||||
return await self.http.post(
|
||||
"/threads/prune", json=payload, 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, **dict(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, **dict(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,
|
||||
)
|
||||
1
venv/Lib/site-packages/langgraph_sdk/_shared/__init__.py
Normal file
1
venv/Lib/site-packages/langgraph_sdk/_shared/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Shared utilities for async and sync clients."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
10
venv/Lib/site-packages/langgraph_sdk/_shared/types.py
Normal file
10
venv/Lib/site-packages/langgraph_sdk/_shared/types.py
Normal file
@@ -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]
|
||||
)
|
||||
131
venv/Lib/site-packages/langgraph_sdk/_shared/utilities.py
Normal file
131
venv/Lib/site-packages/langgraph_sdk/_shared/utilities.py
Normal file
@@ -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
|
||||
20
venv/Lib/site-packages/langgraph_sdk/_sync/__init__.py
Normal file
20
venv/Lib/site-packages/langgraph_sdk/_sync/__init__.py
Normal file
@@ -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",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
731
venv/Lib/site-packages/langgraph_sdk/_sync/assistants.py
Normal file
731
venv/Lib/site-packages/langgraph_sdk/_sync/assistants.py
Normal file
@@ -0,0 +1,731 @@
|
||||
"""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, **dict(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 is not None:
|
||||
payload["config"] = config
|
||||
if context is not None:
|
||||
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,
|
||||
*,
|
||||
delete_threads: bool = False,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> None:
|
||||
"""Delete an assistant.
|
||||
|
||||
Args:
|
||||
assistant_id: The assistant ID to delete.
|
||||
delete_threads: If true, delete all threads with `metadata.assistant_id`
|
||||
matching this assistant, along with runs and checkpoints belonging to
|
||||
those threads.
|
||||
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"
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
query_params: dict[str, Any] = {}
|
||||
if delete_threads:
|
||||
query_params["delete_threads"] = True
|
||||
if params:
|
||||
query_params.update(params)
|
||||
self.http.delete(
|
||||
f"/assistants/{assistant_id}",
|
||||
headers=headers,
|
||||
params=query_params or None,
|
||||
)
|
||||
|
||||
@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,
|
||||
)
|
||||
127
venv/Lib/site-packages/langgraph_sdk/_sync/client.py
Normal file
127
venv/Lib/site-packages/langgraph_sdk/_sync/client.py
Normal file
@@ -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()
|
||||
498
venv/Lib/site-packages/langgraph_sdk/_sync/cron.py
Normal file
498
venv/Lib/site-packages/langgraph_sdk/_sync/cron.py
Normal file
@@ -0,0 +1,498 @@
|
||||
"""Synchronous cron client for LangGraph SDK."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import warnings
|
||||
from collections.abc import Mapping, Sequence
|
||||
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,
|
||||
Durability,
|
||||
Input,
|
||||
OnCompletionBehavior,
|
||||
QueryParamTypes,
|
||||
Run,
|
||||
SortOrder,
|
||||
StreamMode,
|
||||
)
|
||||
|
||||
|
||||
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, # deprecated
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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: (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'.
|
||||
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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of '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
|
||||
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
|
||||
)
|
||||
```
|
||||
"""
|
||||
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 = {
|
||||
"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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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, # deprecated
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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: (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_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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of '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
|
||||
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
|
||||
)
|
||||
```
|
||||
|
||||
"""
|
||||
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 = {
|
||||
"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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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,
|
||||
stream_mode: StreamMode | Sequence[StreamMode] | None = None,
|
||||
stream_subgraphs: bool | None = None,
|
||||
stream_resumable: bool | None = None,
|
||||
durability: Durability | 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.
|
||||
stream_mode: The stream mode(s) to use.
|
||||
stream_subgraphs: Whether to stream output from subgraphs.
|
||||
stream_resumable: Whether to persist the stream chunks in order to resume the stream later.
|
||||
durability: Durability level for the run. Must be one of 'sync', 'async', or 'exit'.
|
||||
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,
|
||||
"stream_mode": stream_mode,
|
||||
"stream_subgraphs": stream_subgraphs,
|
||||
"stream_resumable": stream_resumable,
|
||||
"durability": durability,
|
||||
}
|
||||
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
|
||||
)
|
||||
296
venv/Lib/site-packages/langgraph_sdk/_sync/http.py
Normal file
296
venv/Lib/site-packages/langgraph_sdk/_sync/http.py
Normal file
@@ -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
|
||||
1060
venv/Lib/site-packages/langgraph_sdk/_sync/runs.py
Normal file
1060
venv/Lib/site-packages/langgraph_sdk/_sync/runs.py
Normal file
File diff suppressed because it is too large
Load Diff
313
venv/Lib/site-packages/langgraph_sdk/_sync/store.py
Normal file
313
venv/Lib/site-packages/langgraph_sdk/_sync/store.py
Normal file
@@ -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,
|
||||
)
|
||||
721
venv/Lib/site-packages/langgraph_sdk/_sync/threads.py
Normal file
721
venv/Lib/site-packages/langgraph_sdk/_sync/threads.py
Normal file
@@ -0,0 +1,721 @@
|
||||
"""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,
|
||||
PruneStrategy,
|
||||
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,
|
||||
*,
|
||||
include: Sequence[str] | None = None,
|
||||
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.
|
||||
include: Additional fields to include in the response.
|
||||
Supported values: `"ttl"`.
|
||||
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_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'}
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
query_params: dict[str, Any] = {}
|
||||
if include:
|
||||
query_params["include"] = ",".join(include)
|
||||
if params:
|
||||
query_params.update(params)
|
||||
return self.http.get(
|
||||
f"/threads/{thread_id}",
|
||||
headers=headers,
|
||||
params=query_params or None,
|
||||
)
|
||||
|
||||
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,
|
||||
extract: dict[str, str] | 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.
|
||||
select: List of fields to include in the response.
|
||||
extract: Dictionary mapping aliases to JSONB paths to extract
|
||||
from thread data. Paths use dot notation for nested keys and
|
||||
bracket notation for array indices (e.g.,
|
||||
`{"last_msg": "values.messages[-1]"}`). Extracted values are
|
||||
returned in an `extracted` field on each thread. Maximum 10
|
||||
paths per request.
|
||||
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_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
|
||||
if extract:
|
||||
payload["extract"] = extract
|
||||
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 prune(
|
||||
self,
|
||||
thread_ids: Sequence[str],
|
||||
*,
|
||||
strategy: PruneStrategy = "delete",
|
||||
headers: Mapping[str, str] | None = None,
|
||||
params: QueryParamTypes | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Prune threads by ID.
|
||||
|
||||
Args:
|
||||
thread_ids: List of thread IDs to prune.
|
||||
strategy: The prune strategy. `"delete"` removes threads entirely.
|
||||
`"keep_latest"` prunes old checkpoints but keeps threads and their
|
||||
latest state. Defaults to `"delete"`.
|
||||
headers: Optional custom headers to include with the request.
|
||||
params: Optional query parameters to include with the request.
|
||||
|
||||
Returns:
|
||||
A dict containing `pruned_count` (number of threads pruned).
|
||||
|
||||
???+ example "Example Usage"
|
||||
|
||||
```python
|
||||
client = get_sync_client(url="http://localhost:2024")
|
||||
result = client.threads.prune(
|
||||
thread_ids=["thread_1", "thread_2"],
|
||||
)
|
||||
print(result) # {'pruned_count': 2}
|
||||
```
|
||||
|
||||
"""
|
||||
payload: dict[str, Any] = {
|
||||
"thread_ids": thread_ids,
|
||||
}
|
||||
if strategy != "delete":
|
||||
payload["strategy"] = strategy
|
||||
return self.http.post(
|
||||
"/threads/prune", json=payload, 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, **dict(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, **dict(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,
|
||||
)
|
||||
847
venv/Lib/site-packages/langgraph_sdk/auth/__init__.py
Normal file
847
venv/Lib/site-packages/langgraph_sdk/auth/__init__.py
Normal file
@@ -0,0 +1,847 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
import typing
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
from langgraph_sdk.auth import exceptions, types
|
||||
|
||||
TH = typing.TypeVar("TH", bound=types.Handler)
|
||||
AH = typing.TypeVar("AH", bound=types.Authenticator)
|
||||
|
||||
|
||||
class Auth:
|
||||
"""Add custom authentication and authorization management to your LangGraph application.
|
||||
|
||||
The Auth class provides a unified system for handling authentication and
|
||||
authorization in LangGraph applications. It supports custom user authentication
|
||||
protocols and fine-grained authorization rules for different resources and
|
||||
actions.
|
||||
|
||||
To use, create a separate python file and add the path to the file to your
|
||||
LangGraph API configuration file (`langgraph.json`). Within that file, create
|
||||
an instance of the Auth class and register authentication and authorization
|
||||
handlers as needed.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./my_agent/agent.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"auth": {
|
||||
"path": "./auth.py:my_auth"
|
||||
}
|
||||
```
|
||||
|
||||
Then the LangGraph server will load your auth file and run it server-side whenever a request comes in.
|
||||
|
||||
???+ example "Basic Usage"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Auth
|
||||
|
||||
my_auth = Auth()
|
||||
|
||||
async def verify_token(token: str) -> str:
|
||||
# Verify token and return user_id
|
||||
# This would typically be a call to your auth server
|
||||
return "user_id"
|
||||
|
||||
@auth.authenticate
|
||||
async def authenticate(authorization: str) -> str:
|
||||
# Verify token and return user_id
|
||||
result = await verify_token(authorization)
|
||||
if result != "user_id":
|
||||
raise Auth.exceptions.HTTPException(
|
||||
status_code=401, detail="Unauthorized"
|
||||
)
|
||||
return result
|
||||
|
||||
# Global fallback handler
|
||||
@auth.on
|
||||
async def authorize_default(params: Auth.on.value):
|
||||
return False # Reject all requests (default behavior)
|
||||
|
||||
@auth.on.threads.create
|
||||
async def authorize_thread_create(params: Auth.on.threads.create.value):
|
||||
# Allow the allowed user to create a thread
|
||||
assert params.get("metadata", {}).get("owner") == "allowed_user"
|
||||
|
||||
@auth.on.store
|
||||
async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on.store.value):
|
||||
# Automatically scope all store operations to the user's namespace.
|
||||
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
|
||||
assert isinstance(namespace, tuple)
|
||||
if not namespace or namespace[0] != ctx.user.identity:
|
||||
namespace = (ctx.user.identity, *namespace)
|
||||
value["namespace"] = namespace
|
||||
```
|
||||
|
||||
???+ note "Request Processing Flow"
|
||||
|
||||
1. Authentication (your `@auth.authenticate` handler) is performed first on **every request**
|
||||
2. For authorization, the most specific matching handler is called:
|
||||
* If a handler exists for the exact resource and action, it is used (e.g., `@auth.on.threads.create`)
|
||||
* Otherwise, if a handler exists for the resource with any action, it is used (e.g., `@auth.on.threads`)
|
||||
* Finally, if no specific handlers match, the global handler is used (e.g., `@auth.on`)
|
||||
* If no global handler is set, the request is accepted
|
||||
|
||||
This allows you to set default behavior with a global handler while
|
||||
overriding specific routes as needed.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_authenticate_handler",
|
||||
"_global_handlers",
|
||||
"_handler_cache",
|
||||
"_handlers",
|
||||
"on",
|
||||
)
|
||||
types = types
|
||||
"""Reference to auth type definitions.
|
||||
|
||||
Provides access to all type definitions used in the auth system,
|
||||
like ThreadsCreate, AssistantsRead, etc."""
|
||||
|
||||
exceptions = exceptions
|
||||
"""Reference to auth exception definitions.
|
||||
|
||||
Provides access to all exception definitions used in the auth system,
|
||||
like HTTPException, etc.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.on = _On(self)
|
||||
"""Entry point for authorization handlers that control access to specific resources.
|
||||
|
||||
The on class provides a flexible way to define authorization rules for different
|
||||
resources and actions in your application. It supports three main usage patterns:
|
||||
|
||||
1. Global handlers that run for all resources and actions
|
||||
2. Resource-specific handlers that run for all actions on a resource
|
||||
3. Resource and action specific handlers for fine-grained control
|
||||
|
||||
Each handler must be an async function that accepts two parameters:
|
||||
- ctx (AuthContext): Contains request context and authenticated user info
|
||||
- value: The data being authorized (type varies by endpoint)
|
||||
|
||||
The handler should return one of:
|
||||
|
||||
- None or True: Accept the request
|
||||
- False: Reject with 403 error
|
||||
- FilterType: Apply filtering rules to the response
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Global handler for all requests:
|
||||
|
||||
```python
|
||||
@auth.on
|
||||
async def reject_unhandled_requests(ctx: AuthContext, value: Any) -> None:
|
||||
print(f"Request to {ctx.path} by {ctx.user.identity}")
|
||||
return False
|
||||
```
|
||||
|
||||
Resource-specific handler. This would take precedence over the global handler
|
||||
for all actions on the `threads` resource:
|
||||
|
||||
```python
|
||||
@auth.on.threads
|
||||
async def check_thread_access(ctx: AuthContext, value: Any) -> bool:
|
||||
# Allow access only to threads created by the user
|
||||
return value.get("created_by") == ctx.user.identity
|
||||
```
|
||||
|
||||
Resource and action specific handler:
|
||||
|
||||
```python
|
||||
@auth.on.threads.delete
|
||||
async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool:
|
||||
# Only admins can delete threads
|
||||
return "admin" in ctx.user.permissions
|
||||
```
|
||||
|
||||
Multiple resources or actions:
|
||||
|
||||
```python
|
||||
@auth.on(resources=["threads", "runs"], actions=["create", "update"])
|
||||
async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool:
|
||||
# Implement rate limiting for write operations
|
||||
return await check_rate_limit(ctx.user.identity)
|
||||
```
|
||||
|
||||
Auth for the `store` resource is a bit different since its structure is developer defined.
|
||||
You typically want to scope store operations by rewriting the namespace to include the user's identity.
|
||||
The `value` dict is mutable — changes to `value["namespace"]` are used by the server for the actual operation.
|
||||
|
||||
```python
|
||||
@auth.on.store
|
||||
async def authorize_store(ctx: AuthContext, value: Auth.types.on.store.value):
|
||||
# Automatically scope all store operations to the user's namespace.
|
||||
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
|
||||
assert isinstance(namespace, tuple)
|
||||
if not namespace or namespace[0] != ctx.user.identity:
|
||||
namespace = (ctx.user.identity, *namespace)
|
||||
value["namespace"] = namespace
|
||||
```
|
||||
|
||||
You can also register handlers for specific store actions:
|
||||
|
||||
```python
|
||||
@auth.on.store.put
|
||||
async def on_put(ctx: AuthContext, value: Auth.types.on.store.put.value):
|
||||
# value has typed fields: namespace, key, value, index
|
||||
...
|
||||
|
||||
@auth.on.store.get
|
||||
async def on_get(ctx: AuthContext, value: Auth.types.on.store.get.value):
|
||||
# value has typed fields: namespace, key
|
||||
...
|
||||
```
|
||||
"""
|
||||
# These are accessed by the API. Changes to their names or types is
|
||||
# will be considered a breaking change.
|
||||
self._handlers: dict[tuple[str, str], list[types.Handler]] = {}
|
||||
self._global_handlers: list[types.Handler] = []
|
||||
self._authenticate_handler: types.Authenticator | None = None
|
||||
self._handler_cache: dict[tuple[str, str], types.Handler] = {}
|
||||
|
||||
def authenticate(self, fn: AH) -> AH:
|
||||
"""Register an authentication handler function.
|
||||
|
||||
The authentication handler is responsible for verifying credentials
|
||||
and returning user scopes. It can accept any of the following parameters
|
||||
by name:
|
||||
|
||||
- request (Request): The raw ASGI request object
|
||||
- path (str): The request path, e.g., "/threads/abcd-1234-abcd-1234/runs/abcd-1234-abcd-1234/stream"
|
||||
- method (str): The HTTP method, e.g., "GET"
|
||||
- path_params (dict[str, str]): URL path parameters, e.g., {"thread_id": "abcd-1234-abcd-1234", "run_id": "abcd-1234-abcd-1234"}
|
||||
- query_params (dict[str, str]): URL query parameters, e.g., {"stream": "true"}
|
||||
- headers (dict[bytes, bytes]): Request headers
|
||||
- authorization (str | None): The Authorization header value (e.g., "Bearer <token>")
|
||||
|
||||
Args:
|
||||
fn: The authentication handler function to register.
|
||||
Must return a representation of the user. This could be a:
|
||||
- string (the user id)
|
||||
- dict containing {"identity": str, "permissions": list[str]}
|
||||
- or an object with identity and permissions properties
|
||||
Permissions can be optionally used by your handlers downstream.
|
||||
|
||||
Returns:
|
||||
The registered handler function.
|
||||
|
||||
Raises:
|
||||
ValueError: If an authentication handler is already registered.
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Basic token authentication:
|
||||
|
||||
```python
|
||||
@auth.authenticate
|
||||
async def authenticate(authorization: str) -> str:
|
||||
user_id = verify_token(authorization)
|
||||
return user_id
|
||||
```
|
||||
|
||||
Accept the full request context:
|
||||
|
||||
```python
|
||||
@auth.authenticate
|
||||
async def authenticate(
|
||||
method: str,
|
||||
path: str,
|
||||
headers: dict[str, bytes]
|
||||
) -> str:
|
||||
user = await verify_request(method, path, headers)
|
||||
return user
|
||||
```
|
||||
|
||||
Return user name and permissions:
|
||||
|
||||
```python
|
||||
@auth.authenticate
|
||||
async def authenticate(
|
||||
method: str,
|
||||
path: str,
|
||||
headers: dict[str, bytes]
|
||||
) -> Auth.types.MinimalUserDict:
|
||||
permissions, user = await verify_request(method, path, headers)
|
||||
# Permissions could be things like ["runs:read", "runs:write", "threads:read", "threads:write"]
|
||||
return {
|
||||
"identity": user["id"],
|
||||
"permissions": permissions,
|
||||
"display_name": user["name"],
|
||||
}
|
||||
```
|
||||
"""
|
||||
if self._authenticate_handler is not None:
|
||||
raise ValueError(
|
||||
f"Authentication handler already set as {self._authenticate_handler}."
|
||||
)
|
||||
self._authenticate_handler = fn
|
||||
return fn
|
||||
|
||||
|
||||
## Helper types & utilities
|
||||
|
||||
V = typing.TypeVar("V", contravariant=True)
|
||||
|
||||
|
||||
class _ActionHandler(typing.Protocol[V]):
|
||||
async def __call__(
|
||||
self, *, ctx: types.AuthContext, value: V
|
||||
) -> types.HandlerResult: ...
|
||||
|
||||
|
||||
T = typing.TypeVar("T", covariant=True)
|
||||
|
||||
|
||||
class _ResourceActionOn(typing.Generic[T]):
|
||||
def __init__(
|
||||
self,
|
||||
auth: Auth,
|
||||
resource: typing.Literal["threads", "crons", "assistants"],
|
||||
action: typing.Literal[
|
||||
"create", "read", "update", "delete", "search", "create_run"
|
||||
],
|
||||
value: type[T],
|
||||
) -> None:
|
||||
self.auth = auth
|
||||
self.resource = resource
|
||||
self.action = action
|
||||
self.value = value
|
||||
|
||||
def __call__(self, fn: _ActionHandler[T]) -> _ActionHandler[T]:
|
||||
_validate_handler(fn)
|
||||
_register_handler(self.auth, self.resource, self.action, fn)
|
||||
return fn
|
||||
|
||||
|
||||
VCreate = typing.TypeVar("VCreate", covariant=True)
|
||||
VUpdate = typing.TypeVar("VUpdate", covariant=True)
|
||||
VRead = typing.TypeVar("VRead", covariant=True)
|
||||
VDelete = typing.TypeVar("VDelete", covariant=True)
|
||||
VSearch = typing.TypeVar("VSearch", covariant=True)
|
||||
|
||||
|
||||
class _ResourceOn(typing.Generic[VCreate, VRead, VUpdate, VDelete, VSearch]):
|
||||
"""
|
||||
Generic base class for resource-specific handlers.
|
||||
"""
|
||||
|
||||
value: type[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||
|
||||
Create: type[VCreate]
|
||||
Read: type[VRead]
|
||||
Update: type[VUpdate]
|
||||
Delete: type[VDelete]
|
||||
Search: type[VSearch]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth: Auth,
|
||||
resource: typing.Literal["threads", "crons", "assistants"],
|
||||
) -> None:
|
||||
self.auth = auth
|
||||
self.resource = resource
|
||||
self.create: _ResourceActionOn[VCreate] = _ResourceActionOn(
|
||||
auth, resource, "create", self.Create
|
||||
)
|
||||
self.read: _ResourceActionOn[VRead] = _ResourceActionOn(
|
||||
auth, resource, "read", self.Read
|
||||
)
|
||||
self.update: _ResourceActionOn[VUpdate] = _ResourceActionOn(
|
||||
auth, resource, "update", self.Update
|
||||
)
|
||||
self.delete: _ResourceActionOn[VDelete] = _ResourceActionOn(
|
||||
auth, resource, "delete", self.Delete
|
||||
)
|
||||
self.search: _ResourceActionOn[VSearch] = _ResourceActionOn(
|
||||
auth, resource, "search", self.Search
|
||||
)
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
self,
|
||||
fn: (
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||
| _ActionHandler[dict[str, typing.Any]]
|
||||
),
|
||||
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]: ...
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
resources: str | Sequence[str],
|
||||
actions: str | Sequence[str] | None = None,
|
||||
) -> Callable[
|
||||
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||
]: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
fn: (
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||
| _ActionHandler[dict[str, typing.Any]]
|
||||
| None
|
||||
) = None,
|
||||
*,
|
||||
resources: str | Sequence[str] | None = None,
|
||||
actions: str | Sequence[str] | None = None,
|
||||
) -> (
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]
|
||||
| Callable[
|
||||
[_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]],
|
||||
_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||
]
|
||||
):
|
||||
if fn is not None:
|
||||
_validate_handler(fn)
|
||||
return typing.cast(
|
||||
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
|
||||
_register_handler(self.auth, self.resource, "*", fn),
|
||||
)
|
||||
|
||||
def decorator(
|
||||
handler: _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch],
|
||||
) -> _ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]:
|
||||
_validate_handler(handler)
|
||||
return typing.cast(
|
||||
"_ActionHandler[VCreate | VUpdate | VRead | VDelete | VSearch]",
|
||||
_register_handler(self.auth, self.resource, "*", handler),
|
||||
)
|
||||
|
||||
# Accept keyword-only parameters for future filtering behavior; referenced to satisfy linters.
|
||||
_ = resources, actions
|
||||
return decorator
|
||||
|
||||
|
||||
class _AssistantsOn(
|
||||
_ResourceOn[
|
||||
types.AssistantsCreate,
|
||||
types.AssistantsRead,
|
||||
types.AssistantsUpdate,
|
||||
types.AssistantsDelete,
|
||||
types.AssistantsSearch,
|
||||
]
|
||||
):
|
||||
value = (
|
||||
types.AssistantsCreate
|
||||
| types.AssistantsRead
|
||||
| types.AssistantsUpdate
|
||||
| types.AssistantsDelete
|
||||
| types.AssistantsSearch
|
||||
)
|
||||
Create = types.AssistantsCreate
|
||||
Read = types.AssistantsRead
|
||||
Update = types.AssistantsUpdate
|
||||
Delete = types.AssistantsDelete
|
||||
Search = types.AssistantsSearch
|
||||
|
||||
|
||||
class _ThreadsOn(
|
||||
_ResourceOn[
|
||||
types.ThreadsCreate,
|
||||
types.ThreadsRead,
|
||||
types.ThreadsUpdate,
|
||||
types.ThreadsDelete,
|
||||
types.ThreadsSearch,
|
||||
]
|
||||
):
|
||||
value = (
|
||||
types.ThreadsCreate
|
||||
| types.ThreadsRead
|
||||
| types.ThreadsUpdate
|
||||
| types.ThreadsDelete
|
||||
| types.ThreadsSearch
|
||||
| types.RunsCreate
|
||||
)
|
||||
Create = types.ThreadsCreate
|
||||
Read = types.ThreadsRead
|
||||
Update = types.ThreadsUpdate
|
||||
Delete = types.ThreadsDelete
|
||||
Search = types.ThreadsSearch
|
||||
CreateRun = types.RunsCreate
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth: Auth,
|
||||
resource: typing.Literal["threads", "crons", "assistants"],
|
||||
) -> None:
|
||||
super().__init__(auth, resource)
|
||||
self.create_run: _ResourceActionOn[types.RunsCreate] = _ResourceActionOn(
|
||||
auth, resource, "create_run", self.CreateRun
|
||||
)
|
||||
|
||||
|
||||
class _CronsOn(
|
||||
_ResourceOn[
|
||||
types.CronsCreate,
|
||||
types.CronsRead,
|
||||
types.CronsUpdate,
|
||||
types.CronsDelete,
|
||||
types.CronsSearch,
|
||||
]
|
||||
):
|
||||
value = type[
|
||||
types.CronsCreate
|
||||
| types.CronsRead
|
||||
| types.CronsUpdate
|
||||
| types.CronsDelete
|
||||
| types.CronsSearch
|
||||
]
|
||||
|
||||
Create = types.CronsCreate
|
||||
Read = types.CronsRead
|
||||
Update = types.CronsUpdate
|
||||
Delete = types.CronsDelete
|
||||
Search = types.CronsSearch
|
||||
|
||||
|
||||
class _StoreActionOn(typing.Generic[T]):
|
||||
"""Decorator for registering a handler for a specific store action."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
auth: Auth,
|
||||
action: typing.Literal["put", "get", "search", "delete", "list_namespaces"],
|
||||
value: type[T],
|
||||
) -> None:
|
||||
self.auth = auth
|
||||
self.action = action
|
||||
self.value = value
|
||||
|
||||
def __call__(self, fn: _ActionHandler[T]) -> _ActionHandler[T]:
|
||||
_validate_handler(fn)
|
||||
_register_handler(self.auth, "store", self.action, fn)
|
||||
return fn
|
||||
|
||||
|
||||
class _StoreOn:
|
||||
def __init__(self, auth: Auth) -> None:
|
||||
self._auth = auth
|
||||
self.put = _StoreActionOn(auth, "put", types.StorePut)
|
||||
"""Register a handler for store put operations.
|
||||
|
||||
???+ example "Example"
|
||||
```python
|
||||
@auth.on.store.put
|
||||
async def on_store_put(ctx: Auth.types.AuthContext, value: Auth.types.on.store.put.value):
|
||||
# Scope puts to user's namespace
|
||||
...
|
||||
```
|
||||
"""
|
||||
self.get = _StoreActionOn(auth, "get", types.StoreGet)
|
||||
"""Register a handler for store get operations.
|
||||
|
||||
???+ example "Example"
|
||||
```python
|
||||
@auth.on.store.get
|
||||
async def on_store_get(ctx: Auth.types.AuthContext, value: Auth.types.on.store.get.value):
|
||||
# Scope gets to user's namespace
|
||||
...
|
||||
```
|
||||
"""
|
||||
self.search = _StoreActionOn(auth, "search", types.StoreSearch)
|
||||
"""Register a handler for store search operations.
|
||||
|
||||
???+ example "Example"
|
||||
```python
|
||||
@auth.on.store.search
|
||||
async def on_store_search(ctx: Auth.types.AuthContext, value: Auth.types.on.store.search.value):
|
||||
# Scope searches to user's namespace
|
||||
...
|
||||
```
|
||||
"""
|
||||
self.delete = _StoreActionOn(auth, "delete", types.StoreDelete)
|
||||
"""Register a handler for store delete operations.
|
||||
|
||||
???+ example "Example"
|
||||
```python
|
||||
@auth.on.store.delete
|
||||
async def on_store_delete(ctx: Auth.types.AuthContext, value: Auth.types.on.store.delete.value):
|
||||
# Scope deletes to user's namespace
|
||||
...
|
||||
```
|
||||
"""
|
||||
self.list_namespaces = _StoreActionOn(
|
||||
auth, "list_namespaces", types.StoreListNamespaces
|
||||
)
|
||||
"""Register a handler for store list_namespaces operations.
|
||||
|
||||
???+ example "Example"
|
||||
```python
|
||||
@auth.on.store.list_namespaces
|
||||
async def on_list_ns(ctx: Auth.types.AuthContext, value: Auth.types.on.store.list_namespaces.value):
|
||||
# Scope namespace listing to user's prefix
|
||||
...
|
||||
```
|
||||
"""
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
actions: (
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
| Sequence[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> Callable[[AHO], AHO]: ...
|
||||
|
||||
@typing.overload
|
||||
def __call__(self, fn: AHO) -> AHO: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
fn: AHO | None = None,
|
||||
*,
|
||||
actions: (
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
| Sequence[
|
||||
typing.Literal["put", "get", "search", "list_namespaces", "delete"]
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> AHO | Callable[[AHO], AHO]:
|
||||
"""Register a handler for specific resources and actions.
|
||||
|
||||
Can be used as a decorator or with explicit resource/action parameters:
|
||||
|
||||
@auth.on.store
|
||||
async def handler(): ... # Handle all store ops
|
||||
|
||||
@auth.on.store(actions=("put", "get", "search", "delete"))
|
||||
async def handler(): ... # Handle specific store ops
|
||||
|
||||
@auth.on.store.put
|
||||
async def handler(): ... # Handle store.put ops
|
||||
"""
|
||||
if fn is not None:
|
||||
# Used as a plain decorator
|
||||
_register_handler(self._auth, "store", None, fn)
|
||||
return fn
|
||||
|
||||
# Used with parameters, return a decorator
|
||||
def decorator(
|
||||
handler: AHO,
|
||||
) -> AHO:
|
||||
if isinstance(actions, str):
|
||||
action_list = [actions]
|
||||
else:
|
||||
action_list = list(actions) if actions is not None else ["*"]
|
||||
for action in action_list:
|
||||
_register_handler(self._auth, "store", action, handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
AHO = typing.TypeVar("AHO", bound=_ActionHandler[dict[str, typing.Any]])
|
||||
|
||||
|
||||
class _On:
|
||||
"""Entry point for authorization handlers that control access to specific resources.
|
||||
|
||||
The _On class provides a flexible way to define authorization rules for different resources
|
||||
and actions in your application. It supports three main usage patterns:
|
||||
|
||||
1. Global handlers that run for all resources and actions
|
||||
2. Resource-specific handlers that run for all actions on a resource
|
||||
3. Resource and action specific handlers for fine-grained control
|
||||
|
||||
Each handler must be an async function that accepts two parameters:
|
||||
- ctx (AuthContext): Contains request context and authenticated user info
|
||||
- value: The data being authorized (type varies by endpoint)
|
||||
|
||||
The handler should return one of:
|
||||
- None or True: Accept the request
|
||||
- False: Reject with 403 error
|
||||
- FilterType: Apply filtering rules to the response
|
||||
|
||||
???+ example "Examples"
|
||||
|
||||
Global handler for all requests:
|
||||
|
||||
```python
|
||||
@auth.on
|
||||
async def log_all_requests(ctx: AuthContext, value: Any) -> None:
|
||||
print(f"Request to {ctx.path} by {ctx.user.identity}")
|
||||
return True
|
||||
```
|
||||
|
||||
Resource-specific handler:
|
||||
|
||||
```python
|
||||
@auth.on.threads
|
||||
async def check_thread_access(ctx: AuthContext, value: Any) -> bool:
|
||||
# Allow access only to threads created by the user
|
||||
return value.get("created_by") == ctx.user.identity
|
||||
```
|
||||
|
||||
Resource and action specific handler:
|
||||
|
||||
```python
|
||||
@auth.on.threads.delete
|
||||
async def prevent_thread_deletion(ctx: AuthContext, value: Any) -> bool:
|
||||
# Only admins can delete threads
|
||||
return "admin" in ctx.user.permissions
|
||||
```
|
||||
|
||||
Multiple resources or actions:
|
||||
|
||||
```python
|
||||
@auth.on(resources=["threads", "runs"], actions=["create", "update"])
|
||||
async def rate_limit_writes(ctx: AuthContext, value: Any) -> bool:
|
||||
# Implement rate limiting for write operations
|
||||
return await check_rate_limit(ctx.user.identity)
|
||||
```
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_auth",
|
||||
"assistants",
|
||||
"crons",
|
||||
"runs",
|
||||
"store",
|
||||
"threads",
|
||||
"value",
|
||||
)
|
||||
|
||||
def __init__(self, auth: Auth) -> None:
|
||||
self._auth = auth
|
||||
self.assistants = _AssistantsOn(auth, "assistants")
|
||||
self.threads = _ThreadsOn(auth, "threads")
|
||||
self.crons = _CronsOn(auth, "crons")
|
||||
self.store = _StoreOn(auth)
|
||||
self.value = dict[str, typing.Any]
|
||||
|
||||
@typing.overload
|
||||
def __call__(
|
||||
self,
|
||||
*,
|
||||
resources: str | Sequence[str],
|
||||
actions: str | Sequence[str] | None = None,
|
||||
) -> Callable[[AHO], AHO]: ...
|
||||
|
||||
@typing.overload
|
||||
def __call__(self, fn: AHO) -> AHO: ...
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
fn: AHO | None = None,
|
||||
*,
|
||||
resources: str | Sequence[str] | None = None,
|
||||
actions: str | Sequence[str] | None = None,
|
||||
) -> AHO | Callable[[AHO], AHO]:
|
||||
"""Register a handler for specific resources and actions.
|
||||
|
||||
Can be used as a decorator or with explicit resource/action parameters:
|
||||
|
||||
@auth.on
|
||||
async def handler(): ... # Global handler
|
||||
|
||||
@auth.on(resources="threads")
|
||||
async def handler(): ... # types.Handler for all thread actions
|
||||
|
||||
@auth.on(resources="threads", actions="create")
|
||||
async def handler(): ... # types.Handler for thread creation
|
||||
"""
|
||||
if fn is not None:
|
||||
# Used as a plain decorator
|
||||
_register_handler(self._auth, None, None, fn)
|
||||
return fn
|
||||
|
||||
# Used with parameters, return a decorator
|
||||
def decorator(
|
||||
handler: AHO,
|
||||
) -> AHO:
|
||||
if isinstance(resources, str):
|
||||
resource_list = [resources]
|
||||
else:
|
||||
resource_list = list(resources) if resources is not None else ["*"]
|
||||
|
||||
if isinstance(actions, str):
|
||||
action_list = [actions]
|
||||
else:
|
||||
action_list = list(actions) if actions is not None else ["*"]
|
||||
for resource in resource_list:
|
||||
for action in action_list:
|
||||
_register_handler(self._auth, resource, action, handler)
|
||||
return handler
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def _register_handler(
|
||||
auth: Auth,
|
||||
resource: str | None,
|
||||
action: str | None,
|
||||
fn: types.Handler,
|
||||
) -> types.Handler:
|
||||
_validate_handler(fn)
|
||||
resource = resource or "*"
|
||||
action = action or "*"
|
||||
if resource == "*" and action == "*":
|
||||
if auth._global_handlers:
|
||||
raise ValueError("Global handler already set.")
|
||||
auth._global_handlers.append(fn)
|
||||
else:
|
||||
r = resource if resource is not None else "*"
|
||||
a = action if action is not None else "*"
|
||||
if (r, a) in auth._handlers:
|
||||
raise ValueError(f"types.Handler already set for {r}, {a}.")
|
||||
auth._handlers[(r, a)] = [fn]
|
||||
return fn
|
||||
|
||||
|
||||
def _validate_handler(fn: Callable[..., typing.Any]) -> None:
|
||||
"""Validates that an auth handler function meets the required signature.
|
||||
|
||||
Auth handlers must:
|
||||
1. Be async functions
|
||||
2. Accept a ctx parameter of type AuthContext
|
||||
3. Accept a value parameter for the data being authorized
|
||||
"""
|
||||
if not inspect.iscoroutinefunction(fn):
|
||||
raise ValueError(
|
||||
f"Auth handler '{getattr(fn, '__name__', fn)}' must be an async function. "
|
||||
"Add 'async' before 'def' to make it asynchronous and ensure"
|
||||
" any IO operations are non-blocking."
|
||||
)
|
||||
|
||||
sig = inspect.signature(fn)
|
||||
if "ctx" not in sig.parameters:
|
||||
raise ValueError(
|
||||
f"Auth handler '{getattr(fn, '__name__', fn)}' must have a 'ctx: AuthContext' parameter. "
|
||||
"Update the function signature to include this required parameter."
|
||||
)
|
||||
if "value" not in sig.parameters:
|
||||
raise ValueError(
|
||||
f"Auth handler '{getattr(fn, '__name__', fn)}' must have a 'value' parameter. "
|
||||
" The value contains the mutable data being sent to the endpoint."
|
||||
"Update the function signature to include this required parameter."
|
||||
)
|
||||
|
||||
|
||||
def is_studio_user(
|
||||
user: types.MinimalUser | types.BaseUser | types.MinimalUserDict,
|
||||
) -> bool:
|
||||
return (
|
||||
isinstance(user, types.StudioUser)
|
||||
or (isinstance(user, dict) and user.get("kind") == "StudioUser") # ty: ignore[invalid-argument-type]
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["Auth", "exceptions", "types"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
59
venv/Lib/site-packages/langgraph_sdk/auth/exceptions.py
Normal file
59
venv/Lib/site-packages/langgraph_sdk/auth/exceptions.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""Exceptions used in the auth system."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import http
|
||||
from collections.abc import Mapping
|
||||
|
||||
|
||||
class HTTPException(Exception):
|
||||
"""HTTP exception that you can raise to return a specific HTTP error response.
|
||||
|
||||
Since this is defined in the auth module, we default to a 401 status code.
|
||||
|
||||
Args:
|
||||
status_code: HTTP status code for the error. Defaults to 401 "Unauthorized".
|
||||
detail: Detailed error message. If `None`, uses a default
|
||||
message based on the status code.
|
||||
headers: Additional HTTP headers to include in the error response.
|
||||
|
||||
Example:
|
||||
Default:
|
||||
```python
|
||||
raise HTTPException()
|
||||
# HTTPException(status_code=401, detail='Unauthorized')
|
||||
```
|
||||
|
||||
Add headers:
|
||||
```python
|
||||
raise HTTPException(headers={"X-Custom-Header": "Custom Value"})
|
||||
# HTTPException(status_code=401, detail='Unauthorized', headers={"WWW-Authenticate": "Bearer"})
|
||||
```
|
||||
|
||||
Custom error:
|
||||
```python
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
```
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
status_code: int = 401,
|
||||
detail: str | None = None,
|
||||
headers: Mapping[str, str] | None = None,
|
||||
) -> None:
|
||||
if detail is None:
|
||||
detail = http.HTTPStatus(status_code).phrase
|
||||
self.status_code = status_code
|
||||
self.detail = detail
|
||||
self.headers = headers
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.status_code}: {self.detail}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
class_name = self.__class__.__name__
|
||||
return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})"
|
||||
|
||||
|
||||
__all__ = ["HTTPException"]
|
||||
1152
venv/Lib/site-packages/langgraph_sdk/auth/types.py
Normal file
1152
venv/Lib/site-packages/langgraph_sdk/auth/types.py
Normal file
File diff suppressed because it is too large
Load Diff
55
venv/Lib/site-packages/langgraph_sdk/client.py
Normal file
55
venv/Lib/site-packages/langgraph_sdk/client.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""The LangGraph client implementations connect to the LangGraph API.
|
||||
|
||||
This module provides both asynchronous (`get_client(url="http://localhost:2024")` or
|
||||
`LangGraphClient`) and synchronous (`get_sync_client(url="http://localhost:2024")` or
|
||||
`SyncLanggraphClient`) clients to interacting with the LangGraph API's core resources
|
||||
such as Assistants, Threads, Runs, and Cron jobs, as well as its persistent document
|
||||
Store.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from langgraph_sdk._async.assistants import AssistantsClient
|
||||
|
||||
# Re-export factory functions
|
||||
# Re-export async clients
|
||||
from langgraph_sdk._async.client import LangGraphClient, get_client
|
||||
from langgraph_sdk._async.cron import CronClient
|
||||
from langgraph_sdk._async.http import HttpClient, _adecode_json, _aencode_json
|
||||
from langgraph_sdk._async.runs import RunsClient
|
||||
from langgraph_sdk._async.store import StoreClient
|
||||
from langgraph_sdk._async.threads import ThreadsClient
|
||||
from langgraph_sdk._shared.utilities import configure_loopback_transports
|
||||
from langgraph_sdk._sync.assistants import SyncAssistantsClient
|
||||
|
||||
# Re-export sync clients
|
||||
from langgraph_sdk._sync.client import SyncLangGraphClient, get_sync_client
|
||||
from langgraph_sdk._sync.cron import SyncCronClient
|
||||
from langgraph_sdk._sync.http import SyncHttpClient, _decode_json, _encode_json
|
||||
from langgraph_sdk._sync.runs import SyncRunsClient
|
||||
from langgraph_sdk._sync.store import SyncStoreClient
|
||||
from langgraph_sdk._sync.threads import SyncThreadsClient
|
||||
|
||||
__all__ = [
|
||||
"AssistantsClient",
|
||||
"CronClient",
|
||||
"HttpClient",
|
||||
"LangGraphClient",
|
||||
"RunsClient",
|
||||
"StoreClient",
|
||||
"SyncAssistantsClient",
|
||||
"SyncCronClient",
|
||||
"SyncHttpClient",
|
||||
"SyncLangGraphClient",
|
||||
"SyncRunsClient",
|
||||
"SyncStoreClient",
|
||||
"SyncThreadsClient",
|
||||
"ThreadsClient",
|
||||
"_adecode_json",
|
||||
"_aencode_json",
|
||||
"_decode_json",
|
||||
"_encode_json",
|
||||
"configure_loopback_transports",
|
||||
"get_client",
|
||||
"get_sync_client",
|
||||
]
|
||||
466
venv/Lib/site-packages/langgraph_sdk/encryption/__init__.py
Normal file
466
venv/Lib/site-packages/langgraph_sdk/encryption/__init__.py
Normal file
@@ -0,0 +1,466 @@
|
||||
"""Custom encryption support for LangGraph.
|
||||
|
||||
.. warning::
|
||||
This API is in beta and may change in future versions.
|
||||
|
||||
This module provides a framework for implementing custom at-rest encryption
|
||||
in LangGraph applications. Similar to the Auth system, it allows developers
|
||||
to define custom encryption and decryption handlers that are executed
|
||||
server-side.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import inspect
|
||||
import typing
|
||||
import warnings
|
||||
|
||||
from langgraph_sdk.encryption import types
|
||||
|
||||
|
||||
class LangGraphBetaWarning(UserWarning):
|
||||
"""Warning for beta features in LangGraph SDK."""
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _warn_encryption_beta() -> None:
|
||||
warnings.warn(
|
||||
"The Encryption API is in beta and may change in future versions.",
|
||||
LangGraphBetaWarning,
|
||||
stacklevel=4,
|
||||
)
|
||||
|
||||
|
||||
class DuplicateHandlerError(Exception):
|
||||
"""Raised when attempting to register a duplicate encryption/decryption handler."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
def _validate_handler(fn: typing.Callable, handler_type: str) -> None:
|
||||
"""Validate that a handler function has the correct signature.
|
||||
|
||||
Args:
|
||||
fn: The handler function to validate
|
||||
handler_type: Description of the handler for error messages
|
||||
|
||||
Raises:
|
||||
TypeError: If the handler is not an async function or has wrong parameter count
|
||||
"""
|
||||
if not inspect.iscoroutinefunction(fn):
|
||||
raise TypeError(f"{handler_type} must be an async function, got {type(fn)}")
|
||||
|
||||
sig = inspect.signature(fn)
|
||||
params = [
|
||||
p
|
||||
for p in sig.parameters.values()
|
||||
if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
|
||||
]
|
||||
if len(params) != 2:
|
||||
raise TypeError(
|
||||
f"{handler_type} must accept exactly 2 parameters "
|
||||
f"(ctx, data), got {len(params)}"
|
||||
)
|
||||
|
||||
|
||||
class _EncryptDecorators:
|
||||
"""Decorators for encryption handlers.
|
||||
|
||||
Provides @encryption.encrypt.blob and @encryption.encrypt.json decorators for
|
||||
registering encryption functions.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Encryption):
|
||||
self._parent = parent
|
||||
|
||||
def blob(self, fn: types.BlobEncryptor) -> types.BlobEncryptor:
|
||||
"""Register a blob encryption handler.
|
||||
|
||||
The handler will be called to encrypt opaque data like checkpoint blobs.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@encryption.encrypt.blob
|
||||
async def encrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
||||
# Encrypt the blob using your encryption service
|
||||
return encrypted_blob
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The encryption handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
|
||||
Raises:
|
||||
DuplicateHandlerError: If blob encryptor already registered
|
||||
TypeError: If handler has invalid signature
|
||||
"""
|
||||
if self._parent._blob_encryptor is not None:
|
||||
raise DuplicateHandlerError("Blob encryptor already registered")
|
||||
_validate_handler(fn, "Blob encryptor")
|
||||
self._parent._blob_encryptor = fn
|
||||
return fn
|
||||
|
||||
def json(self, fn: types.JsonEncryptor) -> types.JsonEncryptor:
|
||||
"""Register the JSON encryption handler.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@encryption.encrypt.json
|
||||
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
# Encrypt the data
|
||||
return encrypt_data(data)
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The encryption handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
|
||||
Raises:
|
||||
DuplicateHandlerError: If JSON encryptor already registered
|
||||
TypeError: If handler has invalid signature
|
||||
"""
|
||||
if self._parent._json_encryptor is not None:
|
||||
raise DuplicateHandlerError("JSON encryptor already registered")
|
||||
_validate_handler(fn, "JSON encryptor")
|
||||
self._parent._json_encryptor = fn
|
||||
return fn
|
||||
|
||||
|
||||
class _DecryptDecorators:
|
||||
"""Decorators for decryption handlers.
|
||||
|
||||
Provides @encryption.decrypt.blob and @encryption.decrypt.json decorators for
|
||||
registering decryption functions.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: Encryption):
|
||||
self._parent = parent
|
||||
|
||||
def blob(self, fn: types.BlobDecryptor) -> types.BlobDecryptor:
|
||||
"""Register a blob decryption handler.
|
||||
|
||||
The handler will be called to decrypt opaque data like checkpoint blobs.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@encryption.decrypt.blob
|
||||
async def decrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
||||
# Decrypt the blob using your encryption service
|
||||
return decrypted_blob
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The decryption handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
|
||||
Raises:
|
||||
DuplicateHandlerError: If blob decryptor already registered
|
||||
TypeError: If handler has invalid signature
|
||||
"""
|
||||
if self._parent._blob_decryptor is not None:
|
||||
raise DuplicateHandlerError("Blob decryptor already registered")
|
||||
_validate_handler(fn, "Blob decryptor")
|
||||
self._parent._blob_decryptor = fn
|
||||
return fn
|
||||
|
||||
def json(self, fn: types.JsonDecryptor) -> types.JsonDecryptor:
|
||||
"""Register the JSON decryption handler.
|
||||
|
||||
Example:
|
||||
```python
|
||||
@encryption.decrypt.json
|
||||
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
# Decrypt the data
|
||||
return decrypt_data(data)
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The decryption handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
|
||||
Raises:
|
||||
DuplicateHandlerError: If JSON decryptor already registered
|
||||
TypeError: If handler has invalid signature
|
||||
"""
|
||||
if self._parent._json_decryptor is not None:
|
||||
raise DuplicateHandlerError("JSON decryptor already registered")
|
||||
_validate_handler(fn, "JSON decryptor")
|
||||
self._parent._json_decryptor = fn
|
||||
return fn
|
||||
|
||||
|
||||
class Encryption:
|
||||
"""Add custom at-rest encryption to your LangGraph application.
|
||||
|
||||
.. warning::
|
||||
This API is in beta and may change in future versions.
|
||||
|
||||
The Encryption class provides a system for implementing custom encryption
|
||||
of data at rest in LangGraph applications. It supports encryption of
|
||||
both opaque blobs (like checkpoints) and structured JSON data (like
|
||||
metadata, context, kwargs, values, etc.).
|
||||
|
||||
To use, create a separate Python file and add the path to the file to your
|
||||
LangGraph API configuration file (`langgraph.json`). Within that file, create
|
||||
an instance of the Encryption class and register encryption and decryption
|
||||
handlers as needed.
|
||||
|
||||
Example `langgraph.json` file:
|
||||
|
||||
```json
|
||||
{
|
||||
"dependencies": ["."],
|
||||
"graphs": {
|
||||
"agent": "./my_agent/agent.py:graph"
|
||||
},
|
||||
"env": ".env",
|
||||
"encryption": {
|
||||
"path": "./encryption.py:my_encryption"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then the LangGraph server will load your encryption file and use it to
|
||||
encrypt/decrypt data at rest.
|
||||
|
||||
!!! warning "JSON Encryptors Must Preserve Keys"
|
||||
|
||||
JSON encryptors **must not add or remove keys** from the input dict.
|
||||
Only values may be transformed. This constraint is **enforced at runtime
|
||||
by the server** and exists because SQL JSONB merge operations (used for
|
||||
partial updates) work at the key level.
|
||||
|
||||
**Correct (per-key encryption):**
|
||||
```python
|
||||
# Input: {"secret": "value", "plain": "x"}
|
||||
# Output: {"secret": "<encrypted>", "plain": "x"} ✓ Keys preserved
|
||||
```
|
||||
|
||||
**Incorrect (key consolidation):**
|
||||
```python
|
||||
# Input: {"secret": "value", "plain": "x"}
|
||||
# Output: {"__encrypted__": "<blob>", "plain": "x"} ✗ Key changed
|
||||
```
|
||||
|
||||
If your encryptor needs to store auxiliary data (DEK, IV, etc.), embed it
|
||||
within the encrypted value itself, not as separate keys.
|
||||
|
||||
???+ example "Basic Usage"
|
||||
|
||||
```python
|
||||
from langgraph_sdk import Encryption, EncryptionContext
|
||||
|
||||
my_encryption = Encryption()
|
||||
|
||||
SKIP_FIELDS = {"tenant_id", "owner", "thread_id", "assistant_id"}
|
||||
ENCRYPTED_PREFIX = "encrypted:"
|
||||
|
||||
@my_encryption.encrypt.blob
|
||||
async def encrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
||||
return your_encrypt_bytes(blob)
|
||||
|
||||
@my_encryption.decrypt.blob
|
||||
async def decrypt_blob(ctx: EncryptionContext, blob: bytes) -> bytes:
|
||||
return your_decrypt_bytes(blob)
|
||||
|
||||
@my_encryption.encrypt.json
|
||||
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
result = {}
|
||||
for k, v in data.items():
|
||||
if k in SKIP_FIELDS or v is None:
|
||||
result[k] = v
|
||||
else:
|
||||
result[k] = ENCRYPTED_PREFIX + your_encrypt_string(v)
|
||||
return result
|
||||
|
||||
@my_encryption.decrypt.json
|
||||
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
result = {}
|
||||
for k, v in data.items():
|
||||
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX):
|
||||
result[k] = your_decrypt_string(v[len(ENCRYPTED_PREFIX):])
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
```
|
||||
|
||||
???+ example "Field-Specific Logic"
|
||||
|
||||
The `ctx.model` and `ctx.field` attributes tell you which model type and
|
||||
specific field is being encrypted, allowing different logic:
|
||||
|
||||
```python
|
||||
@my_encryption.encrypt.json
|
||||
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
if ctx.field == "metadata":
|
||||
# Metadata - standard encryption
|
||||
return encrypt_standard(data)
|
||||
elif ctx.field == "values":
|
||||
# Thread values - more sensitive, use stronger encryption
|
||||
return encrypt_sensitive(data)
|
||||
else:
|
||||
return encrypt_standard(data)
|
||||
```
|
||||
|
||||
!!! warning "Model/Field May Differ Between Encrypt and Decrypt"
|
||||
|
||||
Data encrypted with one `(model, field)` pair is **not guaranteed**
|
||||
to be decrypted with the same pair. The server performs SQL JSONB
|
||||
merges that can move encrypted values between models (e.g., cron
|
||||
metadata → run metadata). Your decryption logic must handle data
|
||||
regardless of the `ctx.model` or `ctx.field` values at decrypt time.
|
||||
|
||||
**Safe:** Use `ctx.model`/`ctx.field` for logging or metrics only.
|
||||
|
||||
**Safe:** Encrypt different keys based on `ctx.field`, but use a
|
||||
single decrypt handler that decrypts any value with the encrypted
|
||||
prefix (and passes through plaintext unchanged):
|
||||
|
||||
```python
|
||||
ENCRYPTED_PREFIX = "enc:"
|
||||
|
||||
@my_encryption.encrypt.json
|
||||
async def encrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
# Encrypt different keys depending on the field
|
||||
if ctx.field == "context":
|
||||
keys_to_encrypt = {"api_key", "secret_token"}
|
||||
else:
|
||||
keys_to_encrypt = {"email", "ssn"}
|
||||
return {
|
||||
k: ENCRYPTED_PREFIX + encrypt(v) if k in keys_to_encrypt else v
|
||||
for k, v in data.items()
|
||||
}
|
||||
|
||||
@my_encryption.decrypt.json
|
||||
async def decrypt_json(ctx: EncryptionContext, data: dict) -> dict:
|
||||
# Decrypt ANY value with the prefix, regardless of model/field
|
||||
return {
|
||||
k: decrypt(v[len(ENCRYPTED_PREFIX):])
|
||||
if isinstance(v, str) and v.startswith(ENCRYPTED_PREFIX)
|
||||
else v
|
||||
for k, v in data.items()
|
||||
}
|
||||
```
|
||||
|
||||
**Unsafe:** Using different encryption keys or algorithms based on
|
||||
`ctx.model`/`ctx.field` will cause decryption failures.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_blob_decryptor",
|
||||
"_blob_encryptor",
|
||||
"_context_handler",
|
||||
"_json_decryptor",
|
||||
"_json_encryptor",
|
||||
"decrypt",
|
||||
"encrypt",
|
||||
)
|
||||
|
||||
types = types
|
||||
"""Reference to encryption type definitions.
|
||||
|
||||
Provides access to all type definitions used in the encryption system,
|
||||
including EncryptionContext, BlobEncryptor, BlobDecryptor,
|
||||
JsonEncryptor, and JsonDecryptor.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the Encryption instance."""
|
||||
_warn_encryption_beta()
|
||||
self.encrypt = _EncryptDecorators(self)
|
||||
self.decrypt = _DecryptDecorators(self)
|
||||
self._blob_encryptor: types.BlobEncryptor | None = None
|
||||
self._blob_decryptor: types.BlobDecryptor | None = None
|
||||
self._json_encryptor: types.JsonEncryptor | None = None
|
||||
self._json_decryptor: types.JsonDecryptor | None = None
|
||||
self._context_handler: types.ContextHandler | None = None
|
||||
|
||||
def context(self, fn: types.ContextHandler) -> types.ContextHandler:
|
||||
"""Register a context handler to derive encryption context from auth.
|
||||
|
||||
The handler receives the authenticated user and current EncryptionContext,
|
||||
and returns a dict that becomes ctx.metadata for encrypt/decrypt handlers.
|
||||
|
||||
This allows encryption context to be derived from JWT claims or other
|
||||
auth-derived data instead of requiring a separate X-Encryption-Context header.
|
||||
|
||||
Note: The context handler is called once per request in middleware,
|
||||
so ctx.model and ctx.field will be None in the handler.
|
||||
|
||||
Example:
|
||||
```python
|
||||
from langgraph_sdk import Encryption, EncryptionContext
|
||||
from starlette.authentication import BaseUser
|
||||
|
||||
encryption = Encryption()
|
||||
|
||||
@encryption.context
|
||||
async def get_context(user: BaseUser, ctx: EncryptionContext) -> dict:
|
||||
# Derive encryption context from authenticated user
|
||||
return {
|
||||
**ctx.metadata, # preserve X-Encryption-Context header if present
|
||||
"tenant_id": user.tenant_id,
|
||||
}
|
||||
```
|
||||
|
||||
Args:
|
||||
fn: The context handler function
|
||||
|
||||
Returns:
|
||||
The registered handler function
|
||||
"""
|
||||
self._context_handler = fn
|
||||
return fn
|
||||
|
||||
def get_json_encryptor(
|
||||
self,
|
||||
_model: str | None = None, # kept for langgraph-api compat
|
||||
) -> types.JsonEncryptor | None:
|
||||
"""Get the JSON encryptor.
|
||||
|
||||
Args:
|
||||
_model: Ignored. Kept for backwards compatibility with langgraph-api
|
||||
which passes model_type to this method.
|
||||
|
||||
Returns:
|
||||
The JSON encryptor, or None if not registered.
|
||||
"""
|
||||
return self._json_encryptor
|
||||
|
||||
def get_json_decryptor(
|
||||
self,
|
||||
_model: str | None = None, # kept for langgraph-api compat
|
||||
) -> types.JsonDecryptor | None:
|
||||
"""Get the JSON decryptor.
|
||||
|
||||
Args:
|
||||
_model: Ignored. Kept for backwards compatibility with langgraph-api
|
||||
which passes model_type to this method.
|
||||
|
||||
Returns:
|
||||
The JSON decryptor, or None if not registered.
|
||||
"""
|
||||
return self._json_decryptor
|
||||
|
||||
def __repr__(self) -> str:
|
||||
handlers = []
|
||||
if self._blob_encryptor:
|
||||
handlers.append("blob_encryptor")
|
||||
if self._blob_decryptor:
|
||||
handlers.append("blob_decryptor")
|
||||
if self._json_encryptor:
|
||||
handlers.append("json_encryptor")
|
||||
if self._json_decryptor:
|
||||
handlers.append("json_decryptor")
|
||||
if self._context_handler:
|
||||
handlers.append("context_handler")
|
||||
return f"Encryption(handlers=[{', '.join(handlers)}])"
|
||||
Binary file not shown.
Binary file not shown.
147
venv/Lib/site-packages/langgraph_sdk/encryption/types.py
Normal file
147
venv/Lib/site-packages/langgraph_sdk/encryption/types.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Encryption and decryption types for LangGraph.
|
||||
|
||||
This module defines the core types used for custom at-rest encryption
|
||||
in LangGraph. It includes context types and typed dictionaries for
|
||||
encryption operations.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
Json = dict[str, typing.Any]
|
||||
"""JSON-serializable dictionary type for structured data encryption."""
|
||||
|
||||
|
||||
class EncryptionContext:
|
||||
"""Context passed to encryption/decryption handlers.
|
||||
|
||||
Contains arbitrary non-secret key-values that will be stored on encrypt.
|
||||
These key-values are intended to be sent to an external service that
|
||||
manages keys and handles the actual encryption and decryption of data.
|
||||
|
||||
Attributes:
|
||||
model: The model type being encrypted (e.g., "assistant", "thread", "run", "checkpoint")
|
||||
field: The specific field being encrypted (e.g., "metadata", "context", "kwargs", "values")
|
||||
metadata: Additional context metadata that can be used for encryption decisions
|
||||
"""
|
||||
|
||||
__slots__ = ("field", "metadata", "model")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: str | None = None,
|
||||
metadata: dict[str, typing.Any] | None = None,
|
||||
field: str | None = None,
|
||||
):
|
||||
self.model = model
|
||||
self.field = field
|
||||
self.metadata = metadata or {}
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EncryptionContext(model={self.model!r}, field={self.field!r}, metadata={self.metadata!r})"
|
||||
|
||||
|
||||
BlobEncryptor = Callable[[EncryptionContext, bytes], Awaitable[bytes]]
|
||||
"""Handler for encrypting opaque blob data like checkpoints.
|
||||
|
||||
Note: Must be an async function. Encryption typically involves I/O operations
|
||||
(calling external KMS services), which should be async.
|
||||
|
||||
Args:
|
||||
ctx: Encryption context with model type and metadata
|
||||
blob: The raw bytes to encrypt
|
||||
|
||||
Returns:
|
||||
Awaitable that resolves to encrypted bytes
|
||||
"""
|
||||
|
||||
BlobDecryptor = Callable[[EncryptionContext, bytes], Awaitable[bytes]]
|
||||
"""Handler for decrypting opaque blob data like checkpoints.
|
||||
|
||||
Note: Must be an async function. Decryption typically involves I/O operations
|
||||
(calling external KMS services), which should be async.
|
||||
|
||||
Args:
|
||||
ctx: Encryption context with model type and metadata
|
||||
blob: The encrypted bytes to decrypt
|
||||
|
||||
Returns:
|
||||
Awaitable that resolves to decrypted bytes
|
||||
"""
|
||||
|
||||
JsonEncryptor = Callable[[EncryptionContext, Json], Awaitable[Json]]
|
||||
"""Handler for encrypting structured JSON data.
|
||||
|
||||
Note: Must be an async function. Encryption typically involves I/O operations
|
||||
(calling external KMS services), which should be async.
|
||||
|
||||
Used for encrypting structured data like metadata, context, kwargs, values,
|
||||
and other JSON-serializable fields across different model types.
|
||||
|
||||
Maps plaintext fields to encrypted fields. A practical approach:
|
||||
- Keep "owner" field unencrypted for search/filtering
|
||||
- Encrypt VALUES (not keys) for fields with specific prefix (e.g., "my.customer.org/")
|
||||
- Pass through all other fields unencrypted
|
||||
|
||||
Example:
|
||||
Input: {"owner": "user123", "my.customer.org/email": "john@example.com", "tenant_id": "t-456"}
|
||||
Output: {"owner": "user123", "my.customer.org/email": "ENCRYPTED", "tenant_id": "t-456"}
|
||||
|
||||
Note: Encrypted field VALUES cannot be reliably searched, as most real-world
|
||||
encryption implementations use nonces (non-deterministic encryption).
|
||||
Only unencrypted fields can be used in search queries.
|
||||
|
||||
Args:
|
||||
ctx: Encryption context with model type, field name, and metadata
|
||||
data: The plaintext JSON dictionary
|
||||
|
||||
Returns:
|
||||
Awaitable that resolves to encrypted JSON dictionary
|
||||
"""
|
||||
|
||||
JsonDecryptor = Callable[[EncryptionContext, Json], Awaitable[Json]]
|
||||
"""Handler for decrypting structured JSON data.
|
||||
|
||||
Note: Must be an async function. Decryption typically involves I/O operations
|
||||
(calling external KMS services), which should be async.
|
||||
|
||||
Inverse of JsonEncryptor. Must be able to decrypt data that
|
||||
was encrypted by the corresponding encryptor.
|
||||
|
||||
Args:
|
||||
ctx: Encryption context with model type, field name, and metadata
|
||||
data: The encrypted JSON dictionary
|
||||
|
||||
Returns:
|
||||
Awaitable that resolves to decrypted JSON dictionary
|
||||
"""
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from starlette.authentication import BaseUser
|
||||
|
||||
ContextHandler = Callable[
|
||||
["BaseUser", EncryptionContext], Awaitable[dict[str, typing.Any]]
|
||||
]
|
||||
"""Handler for deriving encryption context from authenticated user info.
|
||||
|
||||
Note: Must be an async function as it may involve I/O operations.
|
||||
|
||||
The context handler is called once per request in middleware (after auth),
|
||||
allowing encryption context to be derived from JWT claims, user properties,
|
||||
or other auth-derived data instead of requiring a separate X-Encryption-Context header.
|
||||
|
||||
The return value becomes ctx.metadata for subsequent encrypt/decrypt operations
|
||||
and is persisted with encrypted data for later decryption.
|
||||
|
||||
Note: ctx.model and ctx.field will be None in context handlers since
|
||||
the handler runs once per request before any specific model/field is known.
|
||||
|
||||
Args:
|
||||
user: The authenticated user (from Starlette's AuthenticationMiddleware)
|
||||
ctx: Current encryption context with metadata from X-Encryption-Context header
|
||||
|
||||
Returns:
|
||||
Awaitable that resolves to dict that becomes the new ctx.metadata
|
||||
"""
|
||||
231
venv/Lib/site-packages/langgraph_sdk/errors.py
Normal file
231
venv/Lib/site-packages/langgraph_sdk/errors.py
Normal file
@@ -0,0 +1,231 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Literal, cast
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LangGraphError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class APIError(httpx.HTTPStatusError, LangGraphError):
|
||||
message: str
|
||||
request: httpx.Request
|
||||
|
||||
body: object | None
|
||||
code: str | None
|
||||
param: str | None
|
||||
type: str | None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str,
|
||||
response_or_request: httpx.Response | httpx.Request,
|
||||
*,
|
||||
body: object | None,
|
||||
) -> None:
|
||||
if isinstance(response_or_request, httpx.Response):
|
||||
req = response_or_request.request
|
||||
response = response_or_request
|
||||
else:
|
||||
req = response_or_request
|
||||
response = None
|
||||
|
||||
httpx.HTTPStatusError.__init__(self, message, request=req, response=response) # type: ignore[arg-type]
|
||||
LangGraphError.__init__(self, message)
|
||||
|
||||
self.request = req
|
||||
self.message = message
|
||||
self.body = body
|
||||
|
||||
if isinstance(body, dict):
|
||||
b = cast("dict[str, Any]", body)
|
||||
# Best-effort extraction of common fields if present
|
||||
code_val = b.get("code")
|
||||
self.code = code_val if isinstance(code_val, str) else None
|
||||
param_val = b.get("param")
|
||||
self.param = param_val if isinstance(param_val, str) else None
|
||||
t = b.get("type")
|
||||
self.type = t if isinstance(t, str) else None
|
||||
else:
|
||||
self.code = None
|
||||
self.param = None
|
||||
self.type = None
|
||||
|
||||
|
||||
class APIResponseValidationError(APIError):
|
||||
response: httpx.Response
|
||||
status_code: int
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
response: httpx.Response,
|
||||
body: object | None,
|
||||
*,
|
||||
message: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(
|
||||
message or "Data returned by API invalid for expected schema.",
|
||||
response,
|
||||
body=body,
|
||||
)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
|
||||
|
||||
class APIStatusError(APIError):
|
||||
response: httpx.Response
|
||||
status_code: int
|
||||
request_id: str | None
|
||||
|
||||
def __init__(
|
||||
self, message: str, *, response: httpx.Response, body: object | None
|
||||
) -> None:
|
||||
super().__init__(message, response, body=body)
|
||||
self.response = response
|
||||
self.status_code = response.status_code
|
||||
self.request_id = response.headers.get("x-request-id")
|
||||
|
||||
|
||||
class APIConnectionError(APIError):
|
||||
def __init__(
|
||||
self, *, message: str = "Connection error.", request: httpx.Request
|
||||
) -> None:
|
||||
super().__init__(message, response_or_request=request, body=None)
|
||||
|
||||
|
||||
class APITimeoutError(APIConnectionError):
|
||||
def __init__(self, request: httpx.Request) -> None:
|
||||
super().__init__(message="Request timed out.", request=request)
|
||||
|
||||
|
||||
class BadRequestError(APIStatusError):
|
||||
status_code: Literal[400] = 400
|
||||
|
||||
|
||||
class AuthenticationError(APIStatusError):
|
||||
status_code: Literal[401] = 401
|
||||
|
||||
|
||||
class PermissionDeniedError(APIStatusError):
|
||||
status_code: Literal[403] = 403
|
||||
|
||||
|
||||
class NotFoundError(APIStatusError):
|
||||
status_code: Literal[404] = 404
|
||||
|
||||
|
||||
class ConflictError(APIStatusError):
|
||||
status_code: Literal[409] = 409
|
||||
|
||||
|
||||
class UnprocessableEntityError(APIStatusError):
|
||||
status_code: Literal[422] = 422
|
||||
|
||||
|
||||
class RateLimitError(APIStatusError):
|
||||
status_code: Literal[429] = 429
|
||||
|
||||
|
||||
class InternalServerError(APIStatusError):
|
||||
pass
|
||||
|
||||
|
||||
def _extract_error_message(body: object | None, fallback: str) -> str:
|
||||
if isinstance(body, dict):
|
||||
b = cast("dict[str, Any]", body)
|
||||
for key in ("message", "detail", "error"):
|
||||
val = b.get(key)
|
||||
if isinstance(val, str) and val:
|
||||
return val
|
||||
# Sometimes errors are structured like {"error": {"message": "..."}}
|
||||
err = b.get("error")
|
||||
if isinstance(err, dict):
|
||||
e = cast("dict[str, Any]", err)
|
||||
for key in ("message", "detail"):
|
||||
val = e.get(key)
|
||||
if isinstance(val, str) and val:
|
||||
return val
|
||||
return fallback
|
||||
|
||||
|
||||
async def _adecode_error_body(r: httpx.Response) -> object | None:
|
||||
try:
|
||||
data = await r.aread()
|
||||
except Exception:
|
||||
return None
|
||||
if not data:
|
||||
return None
|
||||
try:
|
||||
return orjson.loads(data)
|
||||
except Exception:
|
||||
try:
|
||||
return data.decode()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _decode_error_body(r: httpx.Response) -> object | None:
|
||||
try:
|
||||
data = r.read()
|
||||
except Exception:
|
||||
return None
|
||||
if not data:
|
||||
return None
|
||||
try:
|
||||
return orjson.loads(data)
|
||||
except Exception:
|
||||
try:
|
||||
return data.decode()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _map_status_error(response: httpx.Response, body: object | None) -> APIStatusError:
|
||||
status = response.status_code
|
||||
reason = response.reason_phrase or "HTTP Error"
|
||||
message = _extract_error_message(body, f"{status} {reason}")
|
||||
if status == 400:
|
||||
return BadRequestError(message, response=response, body=body)
|
||||
if status == 401:
|
||||
return AuthenticationError(message, response=response, body=body)
|
||||
if status == 403:
|
||||
return PermissionDeniedError(message, response=response, body=body)
|
||||
if status == 404:
|
||||
return NotFoundError(message, response=response, body=body)
|
||||
if status == 409:
|
||||
return ConflictError(message, response=response, body=body)
|
||||
if status == 422:
|
||||
return UnprocessableEntityError(message, response=response, body=body)
|
||||
if status == 429:
|
||||
return RateLimitError(message, response=response, body=body)
|
||||
if status >= 500:
|
||||
return InternalServerError(message, response=response, body=body)
|
||||
return APIStatusError(message, response=response, body=body)
|
||||
|
||||
|
||||
async def _araise_for_status_typed(r: httpx.Response) -> None:
|
||||
if r.status_code < 400:
|
||||
return
|
||||
body = await _adecode_error_body(r)
|
||||
err = _map_status_error(r, body)
|
||||
# Log for older Python versions without Exception notes
|
||||
if not (sys.version_info >= (3, 11)):
|
||||
logger.error(f"Error from langgraph-api: {getattr(err, 'message', '')}")
|
||||
raise err
|
||||
|
||||
|
||||
def _raise_for_status_typed(r: httpx.Response) -> None:
|
||||
if r.status_code < 400:
|
||||
return
|
||||
body = _decode_error_body(r)
|
||||
err = _map_status_error(r, body)
|
||||
if not (sys.version_info >= (3, 11)):
|
||||
logger.error(f"Error from langgraph-api: {getattr(err, 'message', '')}")
|
||||
raise err
|
||||
0
venv/Lib/site-packages/langgraph_sdk/py.typed
Normal file
0
venv/Lib/site-packages/langgraph_sdk/py.typed
Normal file
238
venv/Lib/site-packages/langgraph_sdk/runtime.py
Normal file
238
venv/Lib/site-packages/langgraph_sdk/runtime.py
Normal file
@@ -0,0 +1,238 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from typing import TYPE_CHECKING, Generic, Literal, TypeVar
|
||||
|
||||
if sys.version_info >= (3, 13):
|
||||
ContextT = TypeVar("ContextT", default=None)
|
||||
else:
|
||||
ContextT = TypeVar("ContextT")
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from typing import TypeAliasType
|
||||
else:
|
||||
from typing_extensions import TypeAliasType
|
||||
|
||||
from langgraph_sdk.auth.types import BaseUser
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from langgraph.store.base import BaseStore
|
||||
|
||||
__all__ = [
|
||||
"AccessContext",
|
||||
"ServerRuntime",
|
||||
]
|
||||
|
||||
|
||||
AccessContext = Literal[
|
||||
"threads.create_run",
|
||||
"threads.update",
|
||||
"threads.read",
|
||||
"assistants.read",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True, frozen=True)
|
||||
class _ServerRuntimeBase(Generic[ContextT]):
|
||||
"""Base for server runtime variants.
|
||||
|
||||
!!! warning "Beta"
|
||||
This API is in beta and may change in future releases.
|
||||
"""
|
||||
|
||||
access_context: AccessContext
|
||||
"""Why the graph factory is being called.
|
||||
|
||||
The server accesses graphs in several contexts beyond just executing runs.
|
||||
For example, it calls the graph factory to retrieve schemas, render the
|
||||
graph structure, or read state history. This field tells you which
|
||||
operation triggered the current call.
|
||||
|
||||
In all contexts, the returned graph must have the same topology (nodes,
|
||||
edges, state schema) as the graph used for execution. Use
|
||||
`.execution_runtime` to conditionally set up expensive *resources*
|
||||
(MCP servers, DB connections) without changing the graph structure.
|
||||
|
||||
Write contexts (graph is used to write state):
|
||||
|
||||
- `threads.create_run` (`graph.astream`) — full graph execution
|
||||
(nodes + edges). `context` is available (use `.execution_runtime`
|
||||
to narrow).
|
||||
- `threads.update` (`graph.aupdate_state`) — does NOT execute node
|
||||
functions or evaluate edges. Only runs the node's channel writers
|
||||
to apply the provided values to state channels as if the specified
|
||||
node had returned them. Reducers are applied and channel triggers
|
||||
are set, so the next `invoke`/`stream` call will evaluate edges
|
||||
from that node to determine the next step. Does not need access to
|
||||
external resources, but a different graph topology will apply
|
||||
writes to the wrong channels.
|
||||
|
||||
Read state contexts (graph used to format the returned
|
||||
`StateSnapshot`). A different topology may cause `get_state` to
|
||||
report incorrect pending tasks. Note that `useStream` uses the state
|
||||
history endpoint to render interrupts and support branching:
|
||||
|
||||
- `threads.read` (`graph.aget_state`, `graph.aget_state_history`) —
|
||||
the graph structure informs which tasks to include in the prepared
|
||||
view of the latest checkpoint and how to process subgraphs.
|
||||
|
||||
Introspection contexts (graph structure only, no execution).
|
||||
A different topology may cause schemas and visualizations to not
|
||||
match actual execution:
|
||||
|
||||
- `assistants.read` (`graph.aget_graph`, `graph.aget_subgraphs`,
|
||||
`graph.aget_schemas`) — return the graph definition, subgraph
|
||||
definitions, and input/output/config schemas. Used for
|
||||
visualization in the studio UI and to populate schemas for MCP,
|
||||
A2A, and other protocol integrations.
|
||||
"""
|
||||
|
||||
user: BaseUser | None = field(default=None)
|
||||
"""The authenticated user, or `None` if no custom auth is configured."""
|
||||
|
||||
store: BaseStore
|
||||
"""Store for the graph run, enabling persistence and memory."""
|
||||
|
||||
@property
|
||||
def execution_runtime(self) -> _ExecutionRuntime[ContextT] | None:
|
||||
"""Narrow to the execution runtime, or `None` if not in an execution context.
|
||||
|
||||
When the server calls the graph factory for `threads.create_run`, the returned
|
||||
object provides access to `context` (typed by the graph's
|
||||
`context_schema`). For all other access contexts (introspection, state
|
||||
reads, state updates), this returns `None`.
|
||||
|
||||
Use this to conditionally set up expensive resources (MCP tool servers,
|
||||
database connections, etc.) that are only needed during execution:
|
||||
|
||||
```python
|
||||
import contextlib
|
||||
from langgraph_sdk.runtime import ServerRuntime
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def my_factory(runtime: ServerRuntime[MyCtx]):
|
||||
if ert := runtime.execution_runtime:
|
||||
# Only connect to MCP servers when actually executing a run.
|
||||
# Introspection calls (get_schema, get_graph, ...) skip this.
|
||||
mcp_tools = await connect_mcp(ert.context.mcp_endpoint)
|
||||
yield create_agent(model, tools=mcp_tools)
|
||||
await disconnect_mcp()
|
||||
else:
|
||||
yield create_agent(model, tools=[])
|
||||
```
|
||||
"""
|
||||
if isinstance(self, _ExecutionRuntime):
|
||||
return self
|
||||
return None
|
||||
|
||||
def ensure_user(self) -> BaseUser:
|
||||
"""Return the authenticated user, or raise if not available.
|
||||
|
||||
When custom auth is configured, `user` is set for all access contexts
|
||||
(the factory is only called from HTTP handlers where the auth
|
||||
middleware has already run). This method raises only when no custom
|
||||
auth is configured.
|
||||
|
||||
Raises:
|
||||
PermissionError: If no user is authenticated.
|
||||
"""
|
||||
if self.user is None:
|
||||
raise PermissionError(
|
||||
f"No authenticated user available in access_context='{self.access_context}'. "
|
||||
"Ensure custom auth is configured for the server."
|
||||
)
|
||||
return self.user
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True, frozen=True)
|
||||
class _ExecutionRuntime(_ServerRuntimeBase[ContextT], Generic[ContextT]):
|
||||
"""Runtime for `threads.create_run` — the graph will be fully executed.
|
||||
|
||||
Access this via `.execution_runtime` on `ServerRuntime`. Do not
|
||||
construct directly.
|
||||
|
||||
!!! warning "Beta"
|
||||
This API is in beta and may change in future releases.
|
||||
"""
|
||||
|
||||
context: ContextT = field(default=None) # type: ignore[assignment]
|
||||
"""The graph run context, typed by the graph's `context_schema`.
|
||||
|
||||
Only available during `threads.create_run`.
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(kw_only=True, slots=True, frozen=True)
|
||||
class _ReadRuntime(_ServerRuntimeBase[ContextT], Generic[ContextT]):
|
||||
"""Runtime for non-execution access contexts.
|
||||
|
||||
Used for introspection (`assistants.read`), state operations
|
||||
(`threads.read`), and state updates (`threads.update`).
|
||||
No `context` is available.
|
||||
|
||||
!!! warning "Beta"
|
||||
This API is in beta and may change in future releases.
|
||||
"""
|
||||
|
||||
|
||||
ServerRuntime = TypeAliasType(
|
||||
"ServerRuntime",
|
||||
_ExecutionRuntime[ContextT] | _ReadRuntime[ContextT],
|
||||
type_params=(ContextT,),
|
||||
)
|
||||
"""Runtime context passed to graph builder factories within the Agent Server.
|
||||
|
||||
Requires version 0.7.30 or later of the agent server.
|
||||
|
||||
The server calls your graph factory in multiple contexts: executing runs,
|
||||
reading state, fetching schemas, and more. `ServerRuntime` provides
|
||||
the authenticated user, store, and access context for every call. Use
|
||||
`.execution_runtime` to narrow to the execution variant and access
|
||||
`context`.
|
||||
|
||||
Example — conditionally initialize MCP tools only during execution:
|
||||
|
||||
```python
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
|
||||
from langchain.agents import create_agent
|
||||
from langgraph_sdk.runtime import ServerRuntime
|
||||
from my_agent import connect_mcp, disconnect_mcp
|
||||
|
||||
@dataclass
|
||||
class MyCtx:
|
||||
mcp_endpoint: str
|
||||
|
||||
_readonly_agent = create_agent("anthropic:claude-3-5-haiku", tools=[])
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def my_factory(runtime: ServerRuntime[MyCtx]):
|
||||
if ert := runtime.execution_runtime:
|
||||
# Only connect to MCP servers for actual runs.
|
||||
# Schema / graph introspection calls skip this.
|
||||
user_id = runtime.ensure_user().identity
|
||||
mcp_tools = await connect_mcp(ert.context.mcp_endpoint, user_id)
|
||||
yield create_agent("anthropic:claude-3-5-haiku", tools=mcp_tools)
|
||||
await disconnect_mcp()
|
||||
else:
|
||||
yield _readonly_agent
|
||||
```
|
||||
|
||||
Example — simple factory that ignores context:
|
||||
|
||||
```python
|
||||
from langgraph_sdk.runtime import ServerRuntime
|
||||
|
||||
def build_graph(user: BaseUser) -> CompiledGraph:
|
||||
...
|
||||
|
||||
async def my_factory(runtime: ServerRuntime) -> CompiledGraph:
|
||||
# No generic needed if you don't use context.
|
||||
return build_graph(runtime.ensure_user())
|
||||
```
|
||||
|
||||
!!! warning "Beta"
|
||||
This API is in beta and may change in future releases.
|
||||
"""
|
||||
691
venv/Lib/site-packages/langgraph_sdk/schema.py
Normal file
691
venv/Lib/site-packages/langgraph_sdk/schema.py
Normal file
@@ -0,0 +1,691 @@
|
||||
"""Data models for interacting with the LangGraph API."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import Field
|
||||
from datetime import datetime
|
||||
from typing import (
|
||||
Any,
|
||||
ClassVar,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
Protocol,
|
||||
TypeAlias,
|
||||
Union,
|
||||
)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
Json = dict[str, Any] | None
|
||||
"""Represents a JSON-like structure, which can be None or a dictionary with string keys and any values."""
|
||||
|
||||
RunStatus = Literal["pending", "running", "error", "success", "timeout", "interrupted"]
|
||||
"""
|
||||
Represents the status of a run:
|
||||
- "pending": The run is waiting to start.
|
||||
- "running": The run is currently executing.
|
||||
- "error": The run encountered an error and stopped.
|
||||
- "success": The run completed successfully.
|
||||
- "timeout": The run exceeded its time limit.
|
||||
- "interrupted": The run was manually stopped or interrupted.
|
||||
"""
|
||||
|
||||
ThreadStatus = Literal["idle", "busy", "interrupted", "error"]
|
||||
"""
|
||||
Represents the status of a thread:
|
||||
- "idle": The thread is not currently processing any task.
|
||||
- "busy": The thread is actively processing a task.
|
||||
- "interrupted": The thread's execution was interrupted.
|
||||
- "error": An exception occurred during task processing.
|
||||
"""
|
||||
|
||||
ThreadStreamMode = Literal["run_modes", "lifecycle", "state_update"]
|
||||
"""
|
||||
Defines the mode of streaming:
|
||||
- "run_modes": Stream the same events as the runs on thread, as well as run_done events.
|
||||
- "lifecycle": Stream only run start/end events.
|
||||
- "state_update": Stream state updates on the thread.
|
||||
"""
|
||||
|
||||
StreamMode = Literal[
|
||||
"values",
|
||||
"messages",
|
||||
"updates",
|
||||
"events",
|
||||
"tasks",
|
||||
"checkpoints",
|
||||
"debug",
|
||||
"custom",
|
||||
"messages-tuple",
|
||||
]
|
||||
"""
|
||||
Defines the mode of streaming:
|
||||
- "values": Stream only the values.
|
||||
- "messages": Stream complete messages.
|
||||
- "updates": Stream updates to the state.
|
||||
- "events": Stream events occurring during execution.
|
||||
- "checkpoints": Stream checkpoints as they are created.
|
||||
- "tasks": Stream task start and finish events.
|
||||
- "debug": Stream detailed debug information.
|
||||
- "custom": Stream custom events.
|
||||
"""
|
||||
|
||||
DisconnectMode = Literal["cancel", "continue"]
|
||||
"""
|
||||
Specifies behavior on disconnection:
|
||||
- "cancel": Cancel the operation on disconnection.
|
||||
- "continue": Continue the operation even if disconnected.
|
||||
"""
|
||||
|
||||
MultitaskStrategy = Literal["reject", "interrupt", "rollback", "enqueue"]
|
||||
"""
|
||||
Defines how to handle multiple tasks:
|
||||
- "reject": Reject new tasks when busy.
|
||||
- "interrupt": Interrupt current task for new ones.
|
||||
- "rollback": Roll back current task and start new one.
|
||||
- "enqueue": Queue new tasks for later execution.
|
||||
"""
|
||||
|
||||
OnConflictBehavior = Literal["raise", "do_nothing"]
|
||||
"""
|
||||
Specifies behavior on conflict:
|
||||
- "raise": Raise an exception when a conflict occurs.
|
||||
- "do_nothing": Ignore conflicts and proceed.
|
||||
"""
|
||||
|
||||
OnCompletionBehavior = Literal["delete", "keep"]
|
||||
"""
|
||||
Defines action after completion:
|
||||
- "delete": Delete resources after completion.
|
||||
- "keep": Retain resources after completion.
|
||||
"""
|
||||
|
||||
Durability = Literal["sync", "async", "exit"]
|
||||
"""Durability mode for the graph execution.
|
||||
- `"sync"`: Changes are persisted synchronously before the next step starts.
|
||||
- `"async"`: Changes are persisted asynchronously while the next step executes.
|
||||
- `"exit"`: Changes are persisted only when the graph exits."""
|
||||
|
||||
All = Literal["*"]
|
||||
"""Represents a wildcard or 'all' selector."""
|
||||
|
||||
IfNotExists = Literal["create", "reject"]
|
||||
"""
|
||||
Specifies behavior if the thread doesn't exist:
|
||||
- "create": Create a new thread if it doesn't exist.
|
||||
- "reject": Reject the operation if the thread doesn't exist.
|
||||
"""
|
||||
|
||||
PruneStrategy = Literal["delete", "keep_latest"]
|
||||
"""
|
||||
Strategy for pruning threads:
|
||||
- "delete": Remove threads entirely.
|
||||
- "keep_latest": Prune old checkpoints but keep threads and their latest state.
|
||||
"""
|
||||
|
||||
CancelAction = Literal["interrupt", "rollback"]
|
||||
"""
|
||||
Action to take when cancelling the run.
|
||||
- "interrupt": Simply cancel the run.
|
||||
- "rollback": Cancel the run. Then delete the run and associated checkpoints.
|
||||
"""
|
||||
|
||||
BulkCancelRunsStatus = Literal["pending", "running", "all"]
|
||||
"""
|
||||
Filter runs by status when bulk-cancelling:
|
||||
- "pending": Cancel only pending runs.
|
||||
- "running": Cancel only running runs.
|
||||
- "all": Cancel all runs regardless of status.
|
||||
"""
|
||||
|
||||
AssistantSortBy = Literal[
|
||||
"assistant_id", "graph_id", "name", "created_at", "updated_at"
|
||||
]
|
||||
"""
|
||||
The field to sort by.
|
||||
"""
|
||||
|
||||
ThreadSortBy = Literal[
|
||||
"thread_id", "status", "created_at", "updated_at", "state_updated_at"
|
||||
]
|
||||
"""
|
||||
The field to sort by.
|
||||
"""
|
||||
|
||||
CronSortBy = Literal[
|
||||
"cron_id",
|
||||
"assistant_id",
|
||||
"thread_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"next_run_date",
|
||||
"end_time",
|
||||
]
|
||||
"""
|
||||
The field to sort by.
|
||||
"""
|
||||
|
||||
SortOrder = Literal["asc", "desc"]
|
||||
"""
|
||||
The order to sort by.
|
||||
"""
|
||||
|
||||
|
||||
class Config(TypedDict, total=False):
|
||||
"""Configuration options for a call."""
|
||||
|
||||
tags: list[str]
|
||||
"""
|
||||
Tags for this call and any sub-calls (eg. a Chain calling an LLM).
|
||||
You can use these to filter calls.
|
||||
"""
|
||||
|
||||
recursion_limit: int
|
||||
"""
|
||||
Maximum number of times a call can recurse. If not provided, defaults to 25.
|
||||
"""
|
||||
|
||||
configurable: dict[str, Any]
|
||||
"""
|
||||
Runtime values for attributes previously made configurable on this Runnable,
|
||||
or sub-Runnables, through .configurable_fields() or .configurable_alternatives().
|
||||
Check .output_schema() for a description of the attributes that have been made
|
||||
configurable.
|
||||
"""
|
||||
|
||||
|
||||
class Checkpoint(TypedDict):
|
||||
"""Represents a checkpoint in the execution process."""
|
||||
|
||||
thread_id: str
|
||||
"""Unique identifier for the thread associated with this checkpoint."""
|
||||
checkpoint_ns: str
|
||||
"""Namespace for the checkpoint; used internally to manage subgraph state."""
|
||||
checkpoint_id: str | None
|
||||
"""Optional unique identifier for the checkpoint itself."""
|
||||
checkpoint_map: dict[str, Any] | None
|
||||
"""Optional dictionary containing checkpoint-specific data."""
|
||||
|
||||
|
||||
class GraphSchema(TypedDict):
|
||||
"""Defines the structure and properties of a graph."""
|
||||
|
||||
graph_id: str
|
||||
"""The ID of the graph."""
|
||||
input_schema: dict | None
|
||||
"""The schema for the graph input.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
output_schema: dict | None
|
||||
"""The schema for the graph output.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
state_schema: dict | None
|
||||
"""The schema for the graph state.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
config_schema: dict | None
|
||||
"""The schema for the graph config.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
context_schema: dict | None
|
||||
"""The schema for the graph context.
|
||||
Missing if unable to generate JSON schema from graph."""
|
||||
|
||||
|
||||
Subgraphs = dict[str, GraphSchema]
|
||||
|
||||
|
||||
class AssistantBase(TypedDict):
|
||||
"""Base model for an assistant."""
|
||||
|
||||
assistant_id: str
|
||||
"""The ID of the assistant."""
|
||||
graph_id: str
|
||||
"""The ID of the graph."""
|
||||
config: Config
|
||||
"""The assistant config."""
|
||||
context: Context
|
||||
"""The static context of the assistant."""
|
||||
created_at: datetime
|
||||
"""The time the assistant was created."""
|
||||
metadata: Json
|
||||
"""The assistant metadata."""
|
||||
version: int
|
||||
"""The version of the assistant"""
|
||||
name: str
|
||||
"""The name of the assistant"""
|
||||
description: str | None
|
||||
"""The description of the assistant"""
|
||||
|
||||
|
||||
class AssistantVersion(AssistantBase):
|
||||
"""Represents a specific version of an assistant."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class Assistant(AssistantBase):
|
||||
"""Represents an assistant with additional properties."""
|
||||
|
||||
updated_at: datetime
|
||||
"""The last time the assistant was updated."""
|
||||
|
||||
|
||||
class AssistantsSearchResponse(TypedDict):
|
||||
"""Paginated response for assistant search results."""
|
||||
|
||||
assistants: list[Assistant]
|
||||
"""The assistants returned for the current search page."""
|
||||
next: str | None
|
||||
"""Pagination cursor from the ``X-Pagination-Next`` response header."""
|
||||
|
||||
|
||||
class Interrupt(TypedDict):
|
||||
"""Represents an interruption in the execution flow."""
|
||||
|
||||
value: Any
|
||||
"""The value associated with the interrupt."""
|
||||
id: str
|
||||
"""The ID of the interrupt. Can be used to resume the interrupt."""
|
||||
|
||||
|
||||
class Thread(TypedDict):
|
||||
"""Represents a conversation thread."""
|
||||
|
||||
thread_id: str
|
||||
"""The ID of the thread."""
|
||||
created_at: datetime
|
||||
"""The time the thread was created."""
|
||||
updated_at: datetime
|
||||
"""The last time the thread was updated."""
|
||||
metadata: Json
|
||||
"""The thread metadata."""
|
||||
status: ThreadStatus
|
||||
"""The status of the thread, one of 'idle', 'busy', 'interrupted'."""
|
||||
values: Json
|
||||
"""The current state of the thread."""
|
||||
interrupts: dict[str, list[Interrupt]]
|
||||
"""Mapping of task ids to interrupts that were raised in that task."""
|
||||
extracted: NotRequired[dict[str, Any]]
|
||||
"""Extracted values from thread data. Only present when `extract` is used in search."""
|
||||
|
||||
|
||||
class ThreadTask(TypedDict):
|
||||
"""Represents a task within a thread."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
error: str | None
|
||||
interrupts: list[Interrupt]
|
||||
checkpoint: Checkpoint | None
|
||||
state: ThreadState | None
|
||||
result: dict[str, Any] | None
|
||||
|
||||
|
||||
class ThreadState(TypedDict):
|
||||
"""Represents the state of a thread."""
|
||||
|
||||
values: list[dict] | dict[str, Any]
|
||||
"""The state values."""
|
||||
next: Sequence[str]
|
||||
"""The next nodes to execute. If empty, the thread is done until new input is
|
||||
received."""
|
||||
checkpoint: Checkpoint
|
||||
"""The ID of the checkpoint."""
|
||||
metadata: Json
|
||||
"""Metadata for this state"""
|
||||
created_at: str | None
|
||||
"""Timestamp of state creation"""
|
||||
parent_checkpoint: Checkpoint | None
|
||||
"""The ID of the parent checkpoint. If missing, this is the root checkpoint."""
|
||||
tasks: Sequence[ThreadTask]
|
||||
"""Tasks to execute in this step. If already attempted, may contain an error."""
|
||||
interrupts: list[Interrupt]
|
||||
"""Interrupts which were thrown in this thread."""
|
||||
|
||||
|
||||
class ThreadUpdateStateResponse(TypedDict):
|
||||
"""Represents the response from updating a thread's state."""
|
||||
|
||||
checkpoint: Checkpoint
|
||||
"""Checkpoint of the latest state."""
|
||||
|
||||
|
||||
class Run(TypedDict):
|
||||
"""Represents a single execution run."""
|
||||
|
||||
run_id: str
|
||||
"""The ID of the run."""
|
||||
thread_id: str
|
||||
"""The ID of the thread."""
|
||||
assistant_id: str
|
||||
"""The assistant that was used for this run."""
|
||||
created_at: datetime
|
||||
"""The time the run was created."""
|
||||
updated_at: datetime
|
||||
"""The last time the run was updated."""
|
||||
status: RunStatus
|
||||
"""The status of the run. One of 'pending', 'running', "error", 'success', "timeout", "interrupted"."""
|
||||
metadata: Json
|
||||
"""The run metadata."""
|
||||
multitask_strategy: MultitaskStrategy
|
||||
"""Strategy to handle concurrent runs on the same thread."""
|
||||
|
||||
|
||||
class Cron(TypedDict):
|
||||
"""Represents a scheduled task."""
|
||||
|
||||
cron_id: str
|
||||
"""The ID of the cron."""
|
||||
assistant_id: str
|
||||
"""The ID of the assistant."""
|
||||
thread_id: str | None
|
||||
"""The ID of the thread."""
|
||||
on_run_completed: OnCompletionBehavior | None
|
||||
"""What to do with the thread after the run completes. Only applicable for stateless crons."""
|
||||
end_time: datetime | None
|
||||
"""The end date to stop running the cron."""
|
||||
schedule: str
|
||||
"""The schedule to run, cron format."""
|
||||
created_at: datetime
|
||||
"""The time the cron was created."""
|
||||
updated_at: datetime
|
||||
"""The last time the cron was updated."""
|
||||
payload: dict
|
||||
"""The run payload to use for creating new run."""
|
||||
user_id: str | None
|
||||
"""The user ID of the cron."""
|
||||
next_run_date: datetime | None
|
||||
"""The next run date of the cron."""
|
||||
metadata: dict
|
||||
"""The metadata of the cron."""
|
||||
enabled: bool
|
||||
"""Whether the cron is enabled."""
|
||||
|
||||
|
||||
class CronUpdate(TypedDict, total=False):
|
||||
"""Payload for updating a cron job. All fields are optional."""
|
||||
|
||||
schedule: str
|
||||
"""The cron schedule to execute this job on."""
|
||||
end_time: datetime
|
||||
"""The end date to stop running the cron."""
|
||||
input: Input
|
||||
"""The input to the graph."""
|
||||
metadata: dict[str, Any]
|
||||
"""Metadata to assign to the cron job runs."""
|
||||
config: Config
|
||||
"""The configuration for the assistant."""
|
||||
context: Context
|
||||
"""Static context added to the assistant."""
|
||||
webhook: str
|
||||
"""Webhook to call after LangGraph API call is done."""
|
||||
interrupt_before: All | list[str]
|
||||
"""Nodes to interrupt immediately before they get executed."""
|
||||
interrupt_after: All | list[str]
|
||||
"""Nodes to interrupt immediately after they get executed."""
|
||||
on_run_completed: OnCompletionBehavior
|
||||
"""What to do with the thread after the run completes."""
|
||||
enabled: bool
|
||||
"""Enable or disable the cron job."""
|
||||
stream_mode: StreamMode | list[StreamMode]
|
||||
"""The stream mode(s) to use."""
|
||||
stream_subgraphs: bool
|
||||
"""Whether to stream output from subgraphs."""
|
||||
stream_resumable: bool
|
||||
"""Whether to persist the stream chunks in order to resume the stream later."""
|
||||
durability: Durability
|
||||
"""Durability level for the run. Must be one of 'sync', 'async', or 'exit'."""
|
||||
|
||||
|
||||
# Select field aliases for client-side typing of `select` parameters.
|
||||
# These mirror the server's allowed field sets.
|
||||
|
||||
AssistantSelectField = Literal[
|
||||
"assistant_id",
|
||||
"graph_id",
|
||||
"name",
|
||||
"description",
|
||||
"config",
|
||||
"context",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"metadata",
|
||||
"version",
|
||||
]
|
||||
|
||||
ThreadSelectField = Literal[
|
||||
"thread_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"metadata",
|
||||
"config",
|
||||
"context",
|
||||
"status",
|
||||
"values",
|
||||
"interrupts",
|
||||
]
|
||||
|
||||
RunSelectField = Literal[
|
||||
"run_id",
|
||||
"thread_id",
|
||||
"assistant_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"status",
|
||||
"metadata",
|
||||
"kwargs",
|
||||
"multitask_strategy",
|
||||
]
|
||||
|
||||
CronSelectField = Literal[
|
||||
"cron_id",
|
||||
"assistant_id",
|
||||
"thread_id",
|
||||
"end_time",
|
||||
"schedule",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"user_id",
|
||||
"payload",
|
||||
"next_run_date",
|
||||
"metadata",
|
||||
"now",
|
||||
"on_run_completed",
|
||||
"enabled",
|
||||
]
|
||||
|
||||
PrimitiveData = str | int | float | bool | None
|
||||
|
||||
QueryParamTypes = (
|
||||
Mapping[str, PrimitiveData | Sequence[PrimitiveData]]
|
||||
| list[tuple[str, PrimitiveData]]
|
||||
| tuple[tuple[str, PrimitiveData], ...]
|
||||
| str
|
||||
| bytes
|
||||
)
|
||||
|
||||
|
||||
class RunCreate(TypedDict):
|
||||
"""Defines the parameters for initiating a background run."""
|
||||
|
||||
thread_id: str | None
|
||||
"""The identifier of the thread to run. If not provided, the run is stateless."""
|
||||
assistant_id: str
|
||||
"""The identifier of the assistant to use for this run."""
|
||||
input: dict | None
|
||||
"""Initial input data for the run."""
|
||||
metadata: dict | None
|
||||
"""Additional metadata to associate with the run."""
|
||||
config: Config | None
|
||||
"""Configuration options for the run."""
|
||||
context: Context | None
|
||||
"""The static context of the run."""
|
||||
checkpoint_id: str | None
|
||||
"""The identifier of a checkpoint to resume from."""
|
||||
interrupt_before: list[str] | None
|
||||
"""List of node names to interrupt execution before."""
|
||||
interrupt_after: list[str] | None
|
||||
"""List of node names to interrupt execution after."""
|
||||
webhook: str | None
|
||||
"""URL to send webhook notifications about the run's progress."""
|
||||
multitask_strategy: MultitaskStrategy | None
|
||||
"""Strategy for handling concurrent runs on the same thread."""
|
||||
|
||||
|
||||
class Item(TypedDict):
|
||||
"""Represents a single document or data entry in the graph's Store.
|
||||
|
||||
Items are used to store cross-thread memories.
|
||||
"""
|
||||
|
||||
namespace: list[str]
|
||||
"""The namespace of the item. A namespace is analogous to a document's directory."""
|
||||
key: str
|
||||
"""The unique identifier of the item within its namespace.
|
||||
|
||||
In general, keys needn't be globally unique.
|
||||
"""
|
||||
value: dict[str, Any]
|
||||
"""The value stored in the item. This is the document itself."""
|
||||
created_at: datetime
|
||||
"""The timestamp when the item was created."""
|
||||
updated_at: datetime
|
||||
"""The timestamp when the item was last updated."""
|
||||
|
||||
|
||||
class ListNamespaceResponse(TypedDict):
|
||||
"""Response structure for listing namespaces."""
|
||||
|
||||
namespaces: list[list[str]]
|
||||
"""A list of namespace paths, where each path is a list of strings."""
|
||||
|
||||
|
||||
class SearchItem(Item, total=False):
|
||||
"""Item with an optional relevance score from search operations.
|
||||
|
||||
Attributes:
|
||||
score (Optional[float]): Relevance/similarity score. Included when
|
||||
searching a compatible store with a natural language query.
|
||||
"""
|
||||
|
||||
score: float | None
|
||||
|
||||
|
||||
class SearchItemsResponse(TypedDict):
|
||||
"""Response structure for searching items."""
|
||||
|
||||
items: list[SearchItem]
|
||||
"""A list of items matching the search criteria."""
|
||||
|
||||
|
||||
class StreamPart(NamedTuple):
|
||||
"""Represents a part of a stream response."""
|
||||
|
||||
event: str
|
||||
"""The type of event for this stream part."""
|
||||
data: dict
|
||||
"""The data payload associated with the event."""
|
||||
id: str | None = None
|
||||
"""The ID of the event."""
|
||||
|
||||
|
||||
class Send(TypedDict):
|
||||
"""Represents a message to be sent to a specific node in the graph.
|
||||
|
||||
This type is used to explicitly send messages to nodes in the graph, typically
|
||||
used within Command objects to control graph execution flow.
|
||||
"""
|
||||
|
||||
node: str
|
||||
"""The name of the target node to send the message to."""
|
||||
input: dict[str, Any] | None
|
||||
"""Optional dictionary containing the input data to be passed to the node.
|
||||
|
||||
If None, the node will be called with no input."""
|
||||
|
||||
|
||||
class Command(TypedDict, total=False):
|
||||
"""Represents one or more commands to control graph execution flow and state.
|
||||
|
||||
This type defines the control commands that can be returned by nodes to influence
|
||||
graph execution. It lets you navigate to other nodes, update graph state,
|
||||
and resume from interruptions.
|
||||
"""
|
||||
|
||||
goto: Send | str | Sequence[Send | str]
|
||||
"""Specifies where execution should continue. Can be:
|
||||
|
||||
- A string node name to navigate to
|
||||
- A Send object to execute a node with specific input
|
||||
- A sequence of node names or Send objects to execute in order
|
||||
"""
|
||||
update: dict[str, Any] | Sequence[tuple[str, Any]]
|
||||
"""Updates to apply to the graph's state. Can be:
|
||||
|
||||
- A dictionary of state updates to merge
|
||||
- A sequence of (key, value) tuples for ordered updates
|
||||
"""
|
||||
resume: Any
|
||||
"""Value to resume execution with after an interruption.
|
||||
Used in conjunction with interrupt() to implement control flow.
|
||||
"""
|
||||
|
||||
|
||||
class RunCreateMetadata(TypedDict):
|
||||
"""Metadata for a run creation request."""
|
||||
|
||||
run_id: str
|
||||
"""The ID of the run."""
|
||||
|
||||
thread_id: str | None
|
||||
"""The ID of the thread."""
|
||||
|
||||
|
||||
class _TypedDictLikeV1(Protocol):
|
||||
"""Protocol to represent types that behave like TypedDicts
|
||||
|
||||
Version 1: using `ClassVar` for keys."""
|
||||
|
||||
__required_keys__: ClassVar[frozenset[str]]
|
||||
__optional_keys__: ClassVar[frozenset[str]]
|
||||
|
||||
|
||||
class _TypedDictLikeV2(Protocol):
|
||||
"""Protocol to represent types that behave like TypedDicts
|
||||
|
||||
Version 2: not using `ClassVar` for keys."""
|
||||
|
||||
__required_keys__: frozenset[str]
|
||||
__optional_keys__: frozenset[str]
|
||||
|
||||
|
||||
class _DataclassLike(Protocol):
|
||||
"""Protocol to represent types that behave like dataclasses.
|
||||
|
||||
Inspired by the private _DataclassT from dataclasses that uses a similar protocol as a bound.
|
||||
"""
|
||||
|
||||
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
|
||||
|
||||
|
||||
class _BaseModelLike(Protocol):
|
||||
"""Protocol to represent types that behave like Pydantic `BaseModel`."""
|
||||
|
||||
model_config: ClassVar[dict[str, Any]]
|
||||
__pydantic_core_schema__: ClassVar[Any]
|
||||
|
||||
def model_dump(
|
||||
self,
|
||||
**kwargs: Any,
|
||||
) -> dict[str, Any]: ...
|
||||
|
||||
|
||||
_JSONLike: TypeAlias = None | str | int | float | bool
|
||||
_JSONMap: TypeAlias = Mapping[
|
||||
str, Union[_JSONLike, list[_JSONLike], "_JSONMap", list["_JSONMap"]]
|
||||
]
|
||||
|
||||
Input: TypeAlias = (
|
||||
_TypedDictLikeV1 | _TypedDictLikeV2 | _DataclassLike | _BaseModelLike | _JSONMap
|
||||
)
|
||||
|
||||
Context: TypeAlias = Input
|
||||
157
venv/Lib/site-packages/langgraph_sdk/sse.py
Normal file
157
venv/Lib/site-packages/langgraph_sdk/sse.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""Adapted from httpx_sse to split lines on \n, \r, \r\n per the SSE spec."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from collections.abc import AsyncIterator, Iterator
|
||||
from typing import cast
|
||||
|
||||
import httpx
|
||||
import orjson
|
||||
|
||||
from langgraph_sdk.schema import StreamPart
|
||||
|
||||
BytesLike = bytes | bytearray | memoryview
|
||||
|
||||
|
||||
class BytesLineDecoder:
|
||||
"""
|
||||
Handles incrementally reading lines from text.
|
||||
|
||||
Has the same behaviour as the stdllib bytes splitlines,
|
||||
but handling the input iteratively.
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.buffer = bytearray()
|
||||
self.trailing_cr: bool = False
|
||||
|
||||
def decode(self, text: bytes) -> list[BytesLike]:
|
||||
# See https://docs.python.org/3/glossary.html#term-universal-newlines
|
||||
NEWLINE_CHARS = b"\n\r"
|
||||
|
||||
# We always push a trailing `\r` into the next decode iteration.
|
||||
if self.trailing_cr:
|
||||
text = b"\r" + text
|
||||
self.trailing_cr = False
|
||||
if text.endswith(b"\r"):
|
||||
self.trailing_cr = True
|
||||
text = text[:-1]
|
||||
|
||||
if not text:
|
||||
# NOTE: the edge case input of empty text doesn't occur in practice,
|
||||
# because other httpx internals filter out this value
|
||||
return [] # pragma: no cover
|
||||
|
||||
trailing_newline = text[-1] in NEWLINE_CHARS
|
||||
lines = cast(list[BytesLike], text.splitlines())
|
||||
|
||||
if len(lines) == 1 and not trailing_newline:
|
||||
# No new lines, buffer the input and continue.
|
||||
self.buffer.extend(lines[0])
|
||||
return []
|
||||
|
||||
if self.buffer:
|
||||
# Include any existing buffer in the first portion of the
|
||||
# splitlines result.
|
||||
self.buffer.extend(lines[0])
|
||||
lines = cast(list[BytesLike], [self.buffer, *lines[1:]])
|
||||
self.buffer = bytearray()
|
||||
|
||||
if not trailing_newline:
|
||||
# If the last segment of splitlines is not newline terminated,
|
||||
# then drop it from our output and start a new buffer.
|
||||
self.buffer.extend(lines.pop())
|
||||
|
||||
return lines
|
||||
|
||||
def flush(self) -> list[BytesLike]:
|
||||
if not self.buffer and not self.trailing_cr:
|
||||
return []
|
||||
|
||||
lines = [self.buffer]
|
||||
self.buffer = bytearray()
|
||||
self.trailing_cr = False
|
||||
return lines
|
||||
|
||||
|
||||
class SSEDecoder:
|
||||
def __init__(self) -> None:
|
||||
self._event = ""
|
||||
self._data = bytearray()
|
||||
self._last_event_id = ""
|
||||
self._retry: int | None = None
|
||||
|
||||
@property
|
||||
def last_event_id(self) -> str | None:
|
||||
"""Return the last event identifier that was seen."""
|
||||
|
||||
return self._last_event_id or None
|
||||
|
||||
def decode(self, line: bytes) -> StreamPart | None:
|
||||
# See: https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation
|
||||
|
||||
if not line:
|
||||
if (
|
||||
not self._event
|
||||
and not self._data
|
||||
and not self._last_event_id
|
||||
and self._retry is None
|
||||
):
|
||||
return None
|
||||
|
||||
sse = StreamPart(
|
||||
event=self._event,
|
||||
data=orjson.loads(self._data) if self._data else None, # type: ignore[invalid-argument-type]
|
||||
id=self.last_event_id,
|
||||
)
|
||||
|
||||
# NOTE: as per the SSE spec, do not reset last_event_id.
|
||||
self._event = ""
|
||||
self._data = bytearray()
|
||||
self._retry = None
|
||||
|
||||
return sse
|
||||
|
||||
if line.startswith(b":"):
|
||||
return None
|
||||
|
||||
fieldname, _, value = line.partition(b":")
|
||||
|
||||
if value.startswith(b" "):
|
||||
value = value[1:]
|
||||
|
||||
if fieldname == b"event":
|
||||
self._event = value.decode()
|
||||
elif fieldname == b"data":
|
||||
self._data.extend(value)
|
||||
elif fieldname == b"id":
|
||||
if b"\0" in value:
|
||||
pass
|
||||
else:
|
||||
self._last_event_id = value.decode()
|
||||
elif fieldname == b"retry":
|
||||
with contextlib.suppress(TypeError, ValueError):
|
||||
self._retry = int(value)
|
||||
else:
|
||||
pass # Field is ignored.
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def aiter_lines_raw(response: httpx.Response) -> AsyncIterator[BytesLike]:
|
||||
decoder = BytesLineDecoder()
|
||||
async for chunk in response.aiter_bytes():
|
||||
for line in decoder.decode(chunk):
|
||||
yield line
|
||||
for line in decoder.flush():
|
||||
yield line
|
||||
|
||||
|
||||
def iter_lines_raw(response: httpx.Response) -> Iterator[BytesLike]:
|
||||
decoder = BytesLineDecoder()
|
||||
for chunk in response.iter_bytes():
|
||||
for line in decoder.decode(chunk):
|
||||
yield line
|
||||
for line in decoder.flush():
|
||||
yield line
|
||||
Reference in New Issue
Block a user