732 lines
26 KiB
Python

"""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,
)