mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-20 06:35:46 +02:00
Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656737009b | ||
|
|
ecdf70a2ab | ||
|
|
d86502421d | ||
|
|
cbf26a5d98 | ||
|
|
09bd5990d4 | ||
|
|
79595d43a5 | ||
|
|
49a6704bdc | ||
|
|
a430b7fcfb | ||
|
|
48c287d107 | ||
|
|
0fdc787597 | ||
|
|
3bd86fc6f5 | ||
|
|
f1024f7341 | ||
|
|
f9ac88012f | ||
|
|
9d0186b5bf | ||
|
|
40062c40df | ||
|
|
36bbe059df | ||
|
|
52627010cb | ||
|
|
f6781d19ab | ||
|
|
fcf134a452 | ||
|
|
80331b88e0 | ||
|
|
b37f894f38 | ||
|
|
79de3dbad3 | ||
|
|
677fd3ce28 | ||
|
|
4ee863f30d | ||
|
|
7e060f88a2 | ||
|
|
f49856af0b | ||
|
|
5a7d384e2f | ||
|
|
b73b34ddd5 | ||
|
|
c9ffd753f5 | ||
|
|
e85e157e8f | ||
|
|
003226cef4 | ||
|
|
098a199cb9 |
@@ -20,7 +20,7 @@ from langgraph.store.base import (
|
||||
)
|
||||
from langgraph.store.base.batch import AsyncBatchedBaseStore
|
||||
from langgraph.store.postgres.base import (
|
||||
_PLACEHOLDER,
|
||||
PLACEHOLDER,
|
||||
BasePostgresStore,
|
||||
PoolConfig,
|
||||
PostgresIndexConfig,
|
||||
@@ -360,7 +360,7 @@ class AsyncPostgresStore(AsyncBatchedBaseStore, BasePostgresStore[_ainternal.Con
|
||||
for (idx, _), vector in zip(embedding_requests, vectors):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is _PLACEHOLDER:
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = vector
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
|
||||
@@ -370,7 +370,7 @@ class BasePostgresStore(Generic[C]):
|
||||
if op.query and self.index_config:
|
||||
embedding_requests.append((idx, op.query))
|
||||
|
||||
score_operator, post_operator = _get_distance_operator(self)
|
||||
score_operator, post_operator = get_distance_operator(self)
|
||||
vector_type = (
|
||||
cast(PostgresIndexConfig, self.index_config)
|
||||
.get("ann_index_config", {})
|
||||
@@ -430,10 +430,10 @@ class BasePostgresStore(Generic[C]):
|
||||
OFFSET %s
|
||||
"""
|
||||
params = [
|
||||
_PLACEHOLDER, # Vector placeholder
|
||||
PLACEHOLDER, # Vector placeholder
|
||||
*ns_args,
|
||||
*filter_params,
|
||||
_PLACEHOLDER,
|
||||
PLACEHOLDER,
|
||||
expanded_limit,
|
||||
op.limit,
|
||||
op.offset,
|
||||
@@ -828,7 +828,7 @@ class PostgresStore(BaseStore, BasePostgresStore[_pg_internal.Conn]):
|
||||
for (idx, _), embedding in zip(embedding_requests, embeddings):
|
||||
_paramslist = queries[idx][1]
|
||||
for i in range(len(_paramslist)):
|
||||
if _paramslist[i] is _PLACEHOLDER:
|
||||
if _paramslist[i] is PLACEHOLDER:
|
||||
_paramslist[i] = embedding
|
||||
|
||||
for (idx, _), (query, params) in zip(search_ops, queries):
|
||||
@@ -1055,7 +1055,7 @@ def _decode_ns_bytes(namespace: Union[str, bytes, list]) -> tuple[str, ...]:
|
||||
return tuple(namespace.split("."))
|
||||
|
||||
|
||||
def _get_distance_operator(store: Any) -> tuple[str, str]:
|
||||
def get_distance_operator(store: Any) -> tuple[str, str]:
|
||||
"""Get the distance operator and score expression based on config."""
|
||||
# Note: Today, we are not using ANN indices due to restrictions
|
||||
# on PGVector's support for mixing vector and non-vector filters
|
||||
@@ -1121,4 +1121,4 @@ def _ensure_index_config(
|
||||
return embeddings, index_config
|
||||
|
||||
|
||||
_PLACEHOLDER = object()
|
||||
PLACEHOLDER = object()
|
||||
|
||||
@@ -166,6 +166,13 @@ class GetOp(NamedTuple):
|
||||
"doc456" # For a document
|
||||
```
|
||||
"""
|
||||
refresh_ttl: bool = True
|
||||
"""Whether to refresh TTLs for the returned item.
|
||||
|
||||
If no TTL was specified for the original item(s),
|
||||
or if TTL support is not enabled for your adapter,
|
||||
this argument is ignored.
|
||||
"""
|
||||
|
||||
|
||||
class SearchOp(NamedTuple):
|
||||
@@ -260,6 +267,13 @@ class SearchOp(NamedTuple):
|
||||
- "technical documentation about REST APIs"
|
||||
- "machine learning papers from 2023"
|
||||
"""
|
||||
refresh_ttl: bool = True
|
||||
"""Whether to refresh TTLs for the returned item.
|
||||
|
||||
If no TTL was specified for the original item(s),
|
||||
or if TTL support is not enabled for your adapter,
|
||||
this argument is ignored.
|
||||
"""
|
||||
|
||||
|
||||
# Type representing a namespace path that can include wildcards
|
||||
@@ -463,6 +477,15 @@ class PutOp(NamedTuple):
|
||||
]
|
||||
```
|
||||
"""
|
||||
ttl: Optional[float] = None
|
||||
"""Controls the TTL (time-to-live) for the item in minutes.
|
||||
|
||||
If provided, and if the store you are using supports this feature, the item
|
||||
will expire this many minutes after it was last accessed. The expiration timer
|
||||
refreshes on both read operations (get/search) and write operations (put/update).
|
||||
When the TTL expires, the item will be scheduled for deletion on a best-effort basis.
|
||||
Defaults to None (no expiration).
|
||||
"""
|
||||
|
||||
|
||||
Op = Union[GetOp, SearchOp, PutOp, ListNamespacesOp]
|
||||
@@ -612,8 +635,13 @@ class BaseStore(ABC):
|
||||
by providing an `index` configuration at creation time. Without this
|
||||
configuration, semantic search is disabled and any `index` arguments
|
||||
to storage operations will have no effect.
|
||||
|
||||
Similarly, TTL (time-to-live) support is disabled by default.
|
||||
Subclasses must explicitly set `supports_ttl = True` to enable this feature.
|
||||
"""
|
||||
|
||||
supports_ttl = False
|
||||
|
||||
__slots__ = ("__weakref__",)
|
||||
|
||||
@abstractmethod
|
||||
@@ -640,17 +668,21 @@ class BaseStore(ABC):
|
||||
The order of results matches the order of input operations.
|
||||
"""
|
||||
|
||||
def get(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
|
||||
def get(
|
||||
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
|
||||
) -> Optional[Item]:
|
||||
"""Retrieve a single item.
|
||||
|
||||
Args:
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned item.
|
||||
If no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return self.batch([GetOp(namespace, key)])[0]
|
||||
return self.batch([GetOp(namespace, key, refresh_ttl)])[0]
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -661,6 +693,7 @@ class BaseStore(ABC):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: bool = True,
|
||||
) -> list[SearchItem]:
|
||||
"""Search for items within a namespace prefix.
|
||||
|
||||
@@ -670,6 +703,8 @@ class BaseStore(ABC):
|
||||
filter: Key-value pairs to filter results.
|
||||
limit: Maximum number of items to return.
|
||||
offset: Number of items to skip before returning results.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||
If no TTL is specified, this argument is ignored.
|
||||
|
||||
Returns:
|
||||
List of items matching the search criteria.
|
||||
@@ -707,7 +742,9 @@ class BaseStore(ABC):
|
||||
Note: Natural language search support depends on your store implementation
|
||||
and requires proper embedding configuration.
|
||||
"""
|
||||
return self.batch([SearchOp(namespace_prefix, filter, limit, offset, query)])[0]
|
||||
return self.batch(
|
||||
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
|
||||
)[0]
|
||||
|
||||
def put(
|
||||
self,
|
||||
@@ -715,6 +752,8 @@ class BaseStore(ABC):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Optional[float] = None,
|
||||
) -> None:
|
||||
"""Store or update an item in the store.
|
||||
|
||||
@@ -735,12 +774,20 @@ class BaseStore(ABC):
|
||||
- Nested fields: "metadata.title"
|
||||
- Array access: "chapters[*].content" (each indexed separately)
|
||||
- Specific indices: "authors[0].name"
|
||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||
None means no expiration. Expired runs will be deleted opportunistically.
|
||||
By default, the expiration timer refreshes on both read operations (get/search)
|
||||
and write operations (put/update), whenever the item is included in the operation.
|
||||
|
||||
Note:
|
||||
Indexing support depends on your store implementation.
|
||||
If you do not initialize the store with indexing capabilities,
|
||||
the `index` parameter will be ignored.
|
||||
|
||||
Similarly, TTL support depends on the specific store implementation.
|
||||
Some implementations may not support expiration of items.
|
||||
|
||||
???+ example "Examples"
|
||||
Store item. Indexing depends on how you configure the store.
|
||||
```python
|
||||
@@ -759,7 +806,12 @@ class BaseStore(ABC):
|
||||
```
|
||||
"""
|
||||
_validate_namespace(namespace)
|
||||
self.batch([PutOp(namespace, key, value, index=index)])
|
||||
if ttl is not None and not self.supports_ttl:
|
||||
raise NotImplementedError(
|
||||
f"TTL is not supported by {self.__class__.__name__}. "
|
||||
f"Use a store implementation that supports TTL or set ttl=None."
|
||||
)
|
||||
self.batch([PutOp(namespace, key, value, index=index, ttl=ttl)])
|
||||
|
||||
def delete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item.
|
||||
@@ -768,7 +820,7 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
"""
|
||||
self.batch([PutOp(namespace, key, None)])
|
||||
self.batch([PutOp(namespace, key, None, ttl=None)])
|
||||
|
||||
def list_namespaces(
|
||||
self,
|
||||
@@ -823,7 +875,9 @@ class BaseStore(ABC):
|
||||
)
|
||||
return self.batch([op])[0]
|
||||
|
||||
async def aget(self, namespace: tuple[str, ...], key: str) -> Optional[Item]:
|
||||
async def aget(
|
||||
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
|
||||
) -> Optional[Item]:
|
||||
"""Asynchronously retrieve a single item.
|
||||
|
||||
Args:
|
||||
@@ -833,7 +887,7 @@ class BaseStore(ABC):
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return (await self.abatch([GetOp(namespace, key)]))[0]
|
||||
return (await self.abatch([GetOp(namespace, key, refresh_ttl)]))[0]
|
||||
|
||||
async def asearch(
|
||||
self,
|
||||
@@ -844,6 +898,7 @@ class BaseStore(ABC):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: bool = True,
|
||||
) -> list[SearchItem]:
|
||||
"""Asynchronously search for items within a namespace prefix.
|
||||
|
||||
@@ -853,6 +908,9 @@ class BaseStore(ABC):
|
||||
filter: Key-value pairs to filter results.
|
||||
limit: Maximum number of items to return.
|
||||
offset: Number of items to skip before returning results.
|
||||
refresh_ttl: Whether to refresh TTLs for the returned items.
|
||||
Defaults to True. If no TTL is specified, this argument
|
||||
is ignored.
|
||||
|
||||
Returns:
|
||||
List of items matching the search criteria.
|
||||
@@ -892,7 +950,7 @@ class BaseStore(ABC):
|
||||
"""
|
||||
return (
|
||||
await self.abatch(
|
||||
[SearchOp(namespace_prefix, filter, limit, offset, query)]
|
||||
[SearchOp(namespace_prefix, filter, limit, offset, query, refresh_ttl)]
|
||||
)
|
||||
)[0]
|
||||
|
||||
@@ -902,6 +960,8 @@ class BaseStore(ABC):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Optional[float] = None,
|
||||
) -> None:
|
||||
"""Asynchronously store or update an item in the store.
|
||||
|
||||
@@ -922,12 +982,20 @@ class BaseStore(ABC):
|
||||
- Nested fields: "metadata.title"
|
||||
- Array access: "chapters[*].content" (each indexed separately)
|
||||
- Specific indices: "authors[0].name"
|
||||
ttl: Time to live in minutes. Support for this argument depends on your store adapter.
|
||||
If specified, the item will expire after this many minutes from when it was last accessed.
|
||||
None means no expiration. Expired runs will be deleted opportunistically.
|
||||
By default, the expiration timer refreshes on both read operations (get/search)
|
||||
and write operations (put/update), whenever the item is included in the operation.
|
||||
|
||||
Note:
|
||||
Indexing support depends on your store implementation.
|
||||
If you do not initialize the store with indexing capabilities,
|
||||
the `index` parameter will be ignored.
|
||||
|
||||
Similarly, TTL support depends on the specific store implementation.
|
||||
Some implementations may not support expiration of items.
|
||||
|
||||
???+ example "Examples"
|
||||
Store item. Indexing depends on how you configure the store.
|
||||
```python
|
||||
@@ -954,7 +1022,12 @@ class BaseStore(ABC):
|
||||
```
|
||||
"""
|
||||
_validate_namespace(namespace)
|
||||
await self.abatch([PutOp(namespace, key, value, index=index)])
|
||||
if ttl is not None and not self.supports_ttl:
|
||||
raise NotImplementedError(
|
||||
f"TTL is not supported by {self.__class__.__name__}. "
|
||||
f"Use a store implementation that supports TTL or set ttl=None."
|
||||
)
|
||||
await self.abatch([PutOp(namespace, key, value, index=index, ttl=ttl)])
|
||||
|
||||
async def adelete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Asynchronously delete an item.
|
||||
|
||||
@@ -65,13 +65,11 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
pass
|
||||
|
||||
async def aget(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
|
||||
) -> Optional[Item]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, GetOp(namespace, key)))
|
||||
self._aqueue.put_nowait((fut, GetOp(namespace, key, refresh_ttl=refresh_ttl)))
|
||||
return await fut
|
||||
|
||||
async def asearch(
|
||||
@@ -83,11 +81,22 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: bool = True,
|
||||
) -> list[SearchItem]:
|
||||
assert not self._task.done()
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait(
|
||||
(fut, SearchOp(namespace_prefix, filter, limit, offset, query))
|
||||
(
|
||||
fut,
|
||||
SearchOp(
|
||||
namespace_prefix,
|
||||
filter,
|
||||
limit,
|
||||
offset,
|
||||
query,
|
||||
refresh_ttl=refresh_ttl,
|
||||
),
|
||||
)
|
||||
)
|
||||
return await fut
|
||||
|
||||
@@ -97,11 +106,13 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Optional[float] = None,
|
||||
) -> None:
|
||||
assert not self._task.done()
|
||||
_validate_namespace(namespace)
|
||||
fut = self._loop.create_future()
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index)))
|
||||
self._aqueue.put_nowait((fut, PutOp(namespace, key, value, index, ttl=ttl)))
|
||||
return await fut
|
||||
|
||||
async def adelete(
|
||||
@@ -146,12 +157,10 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
|
||||
@_check_loop
|
||||
def get(
|
||||
self,
|
||||
namespace: tuple[str, ...],
|
||||
key: str,
|
||||
self, namespace: tuple[str, ...], key: str, *, refresh_ttl: bool = True
|
||||
) -> Optional[Item]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.aget(namespace, key=key), self._loop
|
||||
self.aget(namespace, key=key, refresh_ttl=refresh_ttl), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
@@ -164,10 +173,16 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
filter: Optional[dict[str, Any]] = None,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
refresh_ttl: bool = True,
|
||||
) -> list[SearchItem]:
|
||||
return asyncio.run_coroutine_threadsafe(
|
||||
self.asearch(
|
||||
namespace_prefix, query=query, filter=filter, limit=limit, offset=offset
|
||||
namespace_prefix,
|
||||
query=query,
|
||||
filter=filter,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
refresh_ttl=refresh_ttl,
|
||||
),
|
||||
self._loop,
|
||||
).result()
|
||||
@@ -179,10 +194,12 @@ class AsyncBatchedBaseStore(BaseStore):
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
*,
|
||||
ttl: Optional[float] = None,
|
||||
) -> None:
|
||||
_validate_namespace(namespace)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.aput(namespace, key=key, value=value, index=index), self._loop
|
||||
self.aput(namespace, key=key, value=value, index=index, ttl=ttl), self._loop
|
||||
).result()
|
||||
|
||||
@_check_loop
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.16"
|
||||
version = "2.0.17"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -148,8 +148,8 @@ async def test_async_batch_store(mocker: MockerFixture) -> None:
|
||||
assert abatch.call_count == 1
|
||||
assert [tuple(c.args[0]) for c in abatch.call_args_list] == [
|
||||
(
|
||||
GetOp(("a",), "b"),
|
||||
GetOp(("c",), "d"),
|
||||
GetOp(("a",), "b", refresh_ttl=True),
|
||||
GetOp(("c",), "d", refresh_ttl=True),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -467,8 +467,8 @@ async def test_async_batch_store_deduplication(mocker: MockerFixture) -> None:
|
||||
assert len(abatch.call_args_list) == 1
|
||||
ops = list(abatch.call_args_list[0].args[1])
|
||||
assert len(ops) == 2
|
||||
assert GetOp(("test",), "same") in ops
|
||||
assert GetOp(("test",), "different") in ops
|
||||
assert GetOp(("test",), "same", refresh_ttl=True) in ops
|
||||
assert GetOp(("test",), "different", refresh_ttl=True) in ops
|
||||
|
||||
abatch.reset_mock()
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# Editor
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS specific
|
||||
.DS_Store
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 LangChain, Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,24 @@
|
||||
.PHONY: install format lint clean build publish
|
||||
|
||||
install:
|
||||
poetry install
|
||||
|
||||
format:
|
||||
poetry run ruff format langgraph_cli_install tests
|
||||
|
||||
lint:
|
||||
poetry run ruff check langgraph_cli_install tests
|
||||
|
||||
test:
|
||||
poetry run pytest
|
||||
|
||||
clean:
|
||||
rm -rf dist/
|
||||
rm -rf build/
|
||||
rm -rf *.egg-info/
|
||||
|
||||
build: clean
|
||||
poetry build
|
||||
|
||||
publish: build
|
||||
poetry publish
|
||||
@@ -0,0 +1,50 @@
|
||||
# LangGraph CLI Installer
|
||||
|
||||
A simple installer for the LangGraph CLI that uses `uv` to create an isolated environment.
|
||||
|
||||
## Why?
|
||||
|
||||
This lightweight installer creates an isolated installation of LangGraph CLI without worrying about Python environment conflicts or dependencies. It uses [uv](https://github.com/astral-sh/uv) to create a standalone environment with LangGraph CLI.
|
||||
|
||||
Key benefits:
|
||||
- Prevents conflicts with other Python packages
|
||||
- No knowledge of virtual environments needed
|
||||
- Adds to your PATH automatically
|
||||
- Installs the latest version of LangGraph CLI
|
||||
|
||||
## Quick Install
|
||||
|
||||
Simply run:
|
||||
|
||||
```bash
|
||||
pip install langgraph-cli-install && langgraph-cli-install
|
||||
```
|
||||
|
||||
This will:
|
||||
1. Install the uv package if not already installed
|
||||
2. Create an isolated environment with the latest LangGraph CLI
|
||||
3. Add the CLI to your PATH automatically
|
||||
|
||||
After installation, you can run `langgraph --help` to get started.
|
||||
|
||||
## How It Works
|
||||
|
||||
This installer is similar to [aider-install](https://github.com/paul-gauthier/aider/blob/main/aider_install/main.py). It:
|
||||
|
||||
1. Uses the `uv` Python installer to create an isolated environment
|
||||
2. Installs the latest `langgraph-cli` in that environment
|
||||
3. Adds the installed binary to your PATH
|
||||
|
||||
This approach dramatically reduces installation issues caused by Python environment conflicts.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
If you prefer not to use this installer, you can install LangGraph CLI directly:
|
||||
|
||||
```bash
|
||||
pip install langgraph-cli
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
@@ -0,0 +1,7 @@
|
||||
"""LangGraph CLI Installer package."""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .main import main
|
||||
|
||||
__all__ = ["main"]
|
||||
@@ -0,0 +1,109 @@
|
||||
"""LangGraph CLI Installation Script.
|
||||
|
||||
Main entry point for installing langgraph-cli in an isolated environment.
|
||||
This script uses uv to create an isolated installation of langgraph-cli.
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import uv
|
||||
|
||||
|
||||
def main():
|
||||
"""Install langgraph-cli using uv in an isolated environment."""
|
||||
print("Installing LangGraph CLI...")
|
||||
|
||||
try:
|
||||
uv_bin = uv.find_uv_bin()
|
||||
|
||||
# Get best Python version for installation (prefer 3.12 if available)
|
||||
python_version = get_latest_python_version()
|
||||
|
||||
# Create an isolated environment with langgraph-cli
|
||||
print(f"Creating isolated environment using {python_version}...")
|
||||
subprocess.check_call(
|
||||
[
|
||||
uv_bin,
|
||||
"tool",
|
||||
"install",
|
||||
"--force",
|
||||
"--python",
|
||||
python_version,
|
||||
"langgraph-cli@latest",
|
||||
]
|
||||
)
|
||||
|
||||
# Update PATH so the tool is available
|
||||
subprocess.check_call([uv_bin, "tool", "update-shell"])
|
||||
|
||||
# Show install location and help
|
||||
show_success_message(uv_bin)
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"\nFailed to install langgraph-cli: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"\nAn error occurred: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_latest_python_version() -> str:
|
||||
"""Get the latest compatible Python version for installation."""
|
||||
# Try to use Python 3.13 if possible, otherwise fall back to the current version
|
||||
target_version = "3.13"
|
||||
try:
|
||||
# Check if this version is available through uv
|
||||
uv_bin = uv.find_uv_bin()
|
||||
result = subprocess.run(
|
||||
[uv_bin, "python", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if target_version in result.stdout:
|
||||
return f"python{target_version}"
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Fall back to current version
|
||||
major, minor = sys.version_info.major, sys.version_info.minor
|
||||
return f"python{major}.{minor}"
|
||||
|
||||
|
||||
def show_success_message(uv_bin):
|
||||
"""Show success message and installation details."""
|
||||
# Get installation path
|
||||
result = subprocess.run(
|
||||
[uv_bin, "tool", "list"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
install_path = None
|
||||
for line in result.stdout.splitlines():
|
||||
if "langgraph-cli" in line:
|
||||
parts = line.strip().split()
|
||||
if len(parts) >= 2:
|
||||
install_path = parts[1]
|
||||
break
|
||||
|
||||
# Success message
|
||||
print("\n🎉 LangGraph CLI has been successfully installed!\n")
|
||||
print("You can now use it by running:")
|
||||
print(" langgraph --help")
|
||||
|
||||
if install_path:
|
||||
print(f"\nInstalled at: {install_path}")
|
||||
|
||||
# Provide hint about shell restart if needed
|
||||
if platform.system() != "Windows":
|
||||
print("\nNote: You may need to restart your terminal or run:")
|
||||
print(" source ~/.bashrc # or ~/.zshrc depending on your shell")
|
||||
print("to ensure the langgraph command is available in your PATH.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Generated
+44
@@ -0,0 +1,44 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.2"
|
||||
description = "Core utilities for Python packages"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
|
||||
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uv"
|
||||
version = "0.1.45"
|
||||
description = "An extremely fast Python package installer and resolver, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "uv-0.1.45-py3-none-linux_armv6l.whl", hash = "sha256:088af576fb0e0462cd5f718d03fb1a9f16ce5ae61fdb2a9d3ea938fc826cecc1"},
|
||||
{file = "uv-0.1.45-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b94180009264f3f7ee74250f8e4f99c8cb0cb3633e3a9c9c66cdef3eb69be575"},
|
||||
{file = "uv-0.1.45-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e5d55f0f8b6ae416c72d78106e224c8e8338356da21ddebecc7b1723de80924"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:7fdb235aaf420fa8ac9009999b1654a23540f03e25c35094543c2f48d7c41aef"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:de81501c0b03160d0944906d1a713f108258360e20c58385974acb7253b56166"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346aa2d0a4ad3c0c3f7852c1edf5e5a8e5d2ef34c7474e9089877291c2da979c"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a601eed14d484d36d421e4208911a56aaf758ea6c385ef8edf8ad9f8ead57ce1"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ca2d5a5e06c5f71c7b213e14fa59129e63b77de3ffbcf84ecc98d647d73a821"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:90b68c80dddebeca69b26a2af1e2e683804bcf2b5f22d107af03d9156d6218c6"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd7f2f64fdded940342dc37234c11ae3508222c3c9b6b0eac5879dcd586010fa"},
|
||||
{file = "uv-0.1.45-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:a39141e179fea043151a165c9155031e7976b0e4b076c0c33a45b58a420134e0"},
|
||||
{file = "uv-0.1.45-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:68718add6ee2cef2816f9bf8a1dbf2d8cf63d98ddf45840f340029f65a49fd89"},
|
||||
{file = "uv-0.1.45-py3-none-musllinux_1_1_i686.whl", hash = "sha256:110e0f45ddb2fe832ce50b0308be90e5439e0c02d3ffe042feeb3f759811f31f"},
|
||||
{file = "uv-0.1.45-py3-none-musllinux_1_1_ppc64le.whl", hash = "sha256:0f6cfe885f109bacc055edd5df2c837616ae2238b9324a9d37835a96b204ab2f"},
|
||||
{file = "uv-0.1.45-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:87e77d25e8f358c0d5de1983497ee4cf4cea8fc73373d1ef1063533352db2f89"},
|
||||
{file = "uv-0.1.45-py3-none-win32.whl", hash = "sha256:ddb93620c9e01fa83573c2648df4bee3fa548ca940de51c8a2c3566a23a0c776"},
|
||||
{file = "uv-0.1.45-py3-none-win_amd64.whl", hash = "sha256:8e2eeea4eec0e09f7d67378152428b5308dba8b33990d045d7a31d19bf18ca1f"},
|
||||
{file = "uv-0.1.45.tar.gz", hash = "sha256:40fab956bc7af50dfa4bda14e5871528f57603eb9bf8595eb3144aace0ed8c47"},
|
||||
]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.8.0,<4.0"
|
||||
content-hash = "ac29a6587488fe83583561554cb37b0812f4609cf34fedc4afae8df804db0d73"
|
||||
@@ -0,0 +1,36 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli-install"
|
||||
version = "0.0.1-rc1"
|
||||
description = "Simple installer for langgraph-cli"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
packages = [{ include = "langgraph_cli_install" }]
|
||||
|
||||
[tool.poetry.scripts]
|
||||
langgraph-cli-install = "langgraph_cli_install.main:main"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.9.0,<4.0"
|
||||
packaging = ">=23.0"
|
||||
uv = ">=0.6.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.ruff]
|
||||
lint.select = [
|
||||
# pycodestyle
|
||||
"E",
|
||||
# Pyflakes
|
||||
"F",
|
||||
# pyupgrade
|
||||
"UP",
|
||||
# flake8-bugbear
|
||||
"B",
|
||||
# isort
|
||||
"I",
|
||||
]
|
||||
lint.ignore = ["E501", "B008"]
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Setup script for the langgraph-cli-install package."""
|
||||
|
||||
from setuptools import find_packages, setup
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="langgraph-cli-install",
|
||||
version="0.1.0",
|
||||
description="Simple installer for langgraph-cli",
|
||||
author="",
|
||||
author_email="",
|
||||
license="MIT",
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
entry_points={
|
||||
"console_scripts": [
|
||||
"langgraph-cli-install=langgraph_cli_install.main:main",
|
||||
],
|
||||
},
|
||||
python_requires=">=3.8",
|
||||
install_requires=[
|
||||
"uv>=0.1.24",
|
||||
"packaging>=23.0",
|
||||
],
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
"""Test package for langgraph-cli-install."""
|
||||
@@ -0,0 +1,50 @@
|
||||
"""Tests for the main module."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from langgraph_cli_install.main import get_latest_python_version, main
|
||||
|
||||
|
||||
def test_get_latest_python_version():
|
||||
"""Test that the get_latest_python_version function returns a string."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "python3.12"
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
with patch("uv.find_uv_bin", return_value="/path/to/uv"):
|
||||
version = get_latest_python_version()
|
||||
assert isinstance(version, str)
|
||||
assert "python" in version
|
||||
|
||||
|
||||
def test_get_latest_python_version_fallback():
|
||||
"""Test fallback to current version when 3.12 is not available."""
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = "python3.8" # No 3.12 here
|
||||
mock_run.return_value = mock_result
|
||||
|
||||
with patch("uv.find_uv_bin", return_value="/path/to/uv"):
|
||||
# Mock sys.version_info
|
||||
old_version_info = sys.version_info
|
||||
sys.version_info = MagicMock()
|
||||
sys.version_info.major = 3
|
||||
sys.version_info.minor = 9
|
||||
|
||||
try:
|
||||
version = get_latest_python_version()
|
||||
assert isinstance(version, str)
|
||||
assert "python3.9" in version
|
||||
finally:
|
||||
# Restore original version_info
|
||||
sys.version_info = old_version_info
|
||||
|
||||
|
||||
def test_main_exception():
|
||||
"""Test main function handles exceptions."""
|
||||
with patch("uv.find_uv_bin", side_effect=Exception("Test error")):
|
||||
with patch("sys.exit") as mock_exit:
|
||||
main()
|
||||
mock_exit.assert_called_once_with(1)
|
||||
@@ -0,0 +1,8 @@
|
||||
"""Test that the version is defined."""
|
||||
|
||||
import langgraph_cli_install
|
||||
|
||||
|
||||
def test_version():
|
||||
"""Test that the version is defined."""
|
||||
assert langgraph_cli_install.__version__ is not None
|
||||
@@ -198,7 +198,7 @@ class CorsConfig(TypedDict, total=False):
|
||||
allow_origin_regex: str
|
||||
"""Optional. A regex pattern for matching allowed origins, used if you have dynamic subdomains.
|
||||
|
||||
Example: "^https://.*\.mycompany\.com$"
|
||||
Example: "^https://\\.*\\.mycompany\\.com$"
|
||||
"""
|
||||
expose_headers: list[str]
|
||||
"""Optional. List of headers that browsers are allowed to read from the response in cross-origin contexts."""
|
||||
@@ -334,6 +334,10 @@ class Config(TypedDict, total=False):
|
||||
and how cross-origin requests are handled.
|
||||
"""
|
||||
|
||||
ui: Optional[dict[str, str]]
|
||||
"""Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.
|
||||
"""
|
||||
|
||||
|
||||
def _parse_version(version_str: str) -> tuple[int, int]:
|
||||
"""Parse a version string into a tuple of (major, minor)."""
|
||||
@@ -369,6 +373,7 @@ def validate_config(config: Config) -> Config:
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
}
|
||||
if config.get("node_version")
|
||||
else {
|
||||
@@ -381,6 +386,7 @@ def validate_config(config: Config) -> Config:
|
||||
"store": config.get("store"),
|
||||
"auth": config.get("auth"),
|
||||
"http": config.get("http"),
|
||||
"ui": config.get("ui"),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1029,6 +1035,7 @@ ADD . {faux_path}
|
||||
RUN cd {faux_path} && {install_cmd}
|
||||
{env_additional_config}
|
||||
ENV LANGSERVE_GRAPHS='{json.dumps(config["graphs"])}'
|
||||
{f"ENV LANGGRAPH_UI='{json.dumps(config['ui'])}'" if config.get("ui") else ""}
|
||||
|
||||
WORKDIR {faux_path}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-cli"
|
||||
version = "0.1.74"
|
||||
version = "0.1.75"
|
||||
description = "CLI for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -96,6 +96,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -187,6 +201,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -96,6 +96,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -187,6 +201,20 @@
|
||||
}
|
||||
],
|
||||
"description": "Optional. Configuration for the built-in long-term memory store, including semantic search indexing.\n\nIf omitted, no vector index is set up (the object store will still be present, however).\n"
|
||||
},
|
||||
"ui": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "Optional. Named definitions of UI components emitted by the agent, each pointing to a JS/TS file.\n"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -33,6 +33,7 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
**expected_config,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
@@ -52,6 +53,7 @@ def test_validate_config():
|
||||
"store": None,
|
||||
"auth": None,
|
||||
"http": None,
|
||||
"ui": None,
|
||||
}
|
||||
actual_config = validate_config(expected_config)
|
||||
assert actual_config == expected_config
|
||||
@@ -467,6 +469,7 @@ def test_config_to_docker_nodejs():
|
||||
"node_version": "20",
|
||||
"graphs": graphs,
|
||||
"dockerfile_lines": ["ARG meow", "ARG foo"],
|
||||
"ui": {"agent": "./graphs/agent.ui.jsx"},
|
||||
}
|
||||
),
|
||||
"langchain/langgraphjs-api",
|
||||
@@ -477,6 +480,7 @@ ARG foo
|
||||
ADD . /deps/unit_tests
|
||||
RUN cd /deps/unit_tests && npm i
|
||||
ENV LANGSERVE_GRAPHS='{"agent": "./graphs/agent.js:graph"}'
|
||||
ENV LANGGRAPH_UI='{"agent": "./graphs/agent.ui.jsx"}'
|
||||
WORKDIR /deps/unit_tests
|
||||
RUN (test ! -f /api/langgraph_api/js/build.mts && echo "Prebuild script not found, skipping") || tsx /api/langgraph_api/js/build.mts"""
|
||||
|
||||
|
||||
@@ -10,6 +10,14 @@ react.cjs
|
||||
react.js
|
||||
react.d.ts
|
||||
react.d.cts
|
||||
react-ui.cjs
|
||||
react-ui.js
|
||||
react-ui.d.ts
|
||||
react-ui.d.cts
|
||||
react-ui/server.cjs
|
||||
react-ui/server.js
|
||||
react-ui/server.d.ts
|
||||
react-ui/server.d.cts
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
|
||||
@@ -11,7 +11,13 @@ function abs(relativePath) {
|
||||
|
||||
export const config = {
|
||||
internals: [/react/],
|
||||
entrypoints: { index: "index", client: "client", react: "react/index" },
|
||||
entrypoints: {
|
||||
index: "index",
|
||||
client: "client",
|
||||
react: "react/index",
|
||||
"react-ui": "react-ui/index",
|
||||
"react-ui/server": "react-ui/server/index",
|
||||
},
|
||||
tsConfigPath: resolve("./tsconfig.json"),
|
||||
cjsSource: "./dist-cjs",
|
||||
cjsDestination: "./dist",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.46",
|
||||
"version": "0.0.49",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"scripts": {
|
||||
"clean": "rm -rf dist/ dist-cjs/",
|
||||
"build": "yarn clean && yarn lc_build --create-entrypoints --pre --tree-shaking",
|
||||
"prepublish": "yarn run build",
|
||||
"prepack": "yarn run build",
|
||||
"format": "prettier --write src",
|
||||
"lint": "prettier --check src && tsc --noEmit",
|
||||
"test": "NODE_OPTIONS=--experimental-vm-modules jest --testPathIgnorePatterns=\\.int\\.test.ts",
|
||||
@@ -29,7 +29,8 @@
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/node": "^20.12.12",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"@types/react": "18.3.2",
|
||||
"@types/react": "^19.0.8",
|
||||
"@types/react-dom": "^19.0.3",
|
||||
"concat-md": "^0.5.1",
|
||||
"jest": "^29.7.0",
|
||||
"prettier": "^3.2.5",
|
||||
@@ -37,7 +38,8 @@
|
||||
"typedoc": "^0.27.7",
|
||||
"typedoc-plugin-markdown": "^4.4.2",
|
||||
"typescript": "^5.4.5",
|
||||
"react": "^18.3.1"
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18 || ^19",
|
||||
@@ -79,6 +81,24 @@
|
||||
"import": "./react.js",
|
||||
"require": "./react.cjs"
|
||||
},
|
||||
"./react-ui": {
|
||||
"types": {
|
||||
"import": "./react-ui.d.ts",
|
||||
"require": "./react-ui.d.cts",
|
||||
"default": "./react-ui.d.ts"
|
||||
},
|
||||
"import": "./react-ui.js",
|
||||
"require": "./react-ui.cjs"
|
||||
},
|
||||
"./react-ui/server": {
|
||||
"types": {
|
||||
"import": "./react-ui/server.d.ts",
|
||||
"require": "./react-ui/server.d.cts",
|
||||
"default": "./react-ui/server.d.ts"
|
||||
},
|
||||
"import": "./react-ui/server.js",
|
||||
"require": "./react-ui/server.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"files": [
|
||||
@@ -94,6 +114,14 @@
|
||||
"react.cjs",
|
||||
"react.js",
|
||||
"react.d.ts",
|
||||
"react.d.cts"
|
||||
"react.d.cts",
|
||||
"react-ui.cjs",
|
||||
"react-ui.js",
|
||||
"react-ui.d.ts",
|
||||
"react-ui.d.cts",
|
||||
"react-ui/server.cjs",
|
||||
"react-ui/server.js",
|
||||
"react-ui/server.d.ts",
|
||||
"react-ui/server.d.cts"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1088,12 +1088,28 @@ export class StoreClient extends BaseClient {
|
||||
* @param namespace A list of strings representing the namespace path.
|
||||
* @param key The unique identifier for the item within the namespace.
|
||||
* @param value A dictionary containing the item's data.
|
||||
* @param options.index Controls search indexing - null (use defaults), false (disable), or list of field paths to index.
|
||||
* @param options.ttl Optional time-to-live in minutes for the item, or null for no expiration.
|
||||
* @returns Promise<void>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* await client.store.putItem(
|
||||
* ["documents", "user123"],
|
||||
* "item456",
|
||||
* { title: "My Document", content: "Hello World" },
|
||||
* { ttl: 60 } // expires in 60 minutes
|
||||
* );
|
||||
* ```
|
||||
*/
|
||||
async putItem(
|
||||
namespace: string[],
|
||||
key: string,
|
||||
value: Record<string, any>,
|
||||
options?: {
|
||||
index?: false | string[] | null;
|
||||
ttl?: number | null;
|
||||
},
|
||||
): Promise<void> {
|
||||
namespace.forEach((label) => {
|
||||
if (label.includes(".")) {
|
||||
@@ -1107,6 +1123,8 @@ export class StoreClient extends BaseClient {
|
||||
namespace,
|
||||
key,
|
||||
value,
|
||||
index: options?.index,
|
||||
ttl: options?.ttl,
|
||||
};
|
||||
|
||||
return this.fetch<void>("/store/items", {
|
||||
@@ -1120,9 +1138,33 @@ export class StoreClient extends BaseClient {
|
||||
*
|
||||
* @param namespace A list of strings representing the namespace path.
|
||||
* @param key The unique identifier for the item.
|
||||
* @param options.refreshTtl Whether to refresh the TTL on this read operation. If null, uses the store's default behavior.
|
||||
* @returns Promise<Item>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const item = await client.store.getItem(
|
||||
* ["documents", "user123"],
|
||||
* "item456",
|
||||
* { refreshTtl: true }
|
||||
* );
|
||||
* console.log(item);
|
||||
* // {
|
||||
* // namespace: ["documents", "user123"],
|
||||
* // key: "item456",
|
||||
* // value: { title: "My Document", content: "Hello World" },
|
||||
* // createdAt: "2024-07-30T12:00:00Z",
|
||||
* // updatedAt: "2024-07-30T12:00:00Z"
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
async getItem(namespace: string[], key: string): Promise<Item | null> {
|
||||
async getItem(
|
||||
namespace: string[],
|
||||
key: string,
|
||||
options?: {
|
||||
refreshTtl?: boolean | null;
|
||||
},
|
||||
): Promise<Item | null> {
|
||||
namespace.forEach((label) => {
|
||||
if (label.includes(".")) {
|
||||
throw new Error(
|
||||
@@ -1131,8 +1173,17 @@ export class StoreClient extends BaseClient {
|
||||
}
|
||||
});
|
||||
|
||||
const params: Record<string, any> = {
|
||||
namespace: namespace.join("."),
|
||||
key,
|
||||
};
|
||||
|
||||
if (options?.refreshTtl !== undefined) {
|
||||
params.refresh_ttl = options.refreshTtl;
|
||||
}
|
||||
|
||||
const response = await this.fetch<APIItem>("/store/items", {
|
||||
params: { namespace: namespace.join("."), key },
|
||||
params,
|
||||
});
|
||||
|
||||
return response
|
||||
@@ -1174,7 +1225,33 @@ export class StoreClient extends BaseClient {
|
||||
* @param options.limit Maximum number of items to return (default is 10).
|
||||
* @param options.offset Number of items to skip before returning results (default is 0).
|
||||
* @param options.query Optional search query.
|
||||
* @param options.refreshTtl Whether to refresh the TTL on items returned by this search. If null, uses the store's default behavior.
|
||||
* @returns Promise<SearchItemsResponse>
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const results = await client.store.searchItems(
|
||||
* ["documents"],
|
||||
* {
|
||||
* filter: { author: "John Doe" },
|
||||
* limit: 5,
|
||||
* refreshTtl: true
|
||||
* }
|
||||
* );
|
||||
* console.log(results);
|
||||
* // {
|
||||
* // items: [
|
||||
* // {
|
||||
* // namespace: ["documents", "user123"],
|
||||
* // key: "item789",
|
||||
* // value: { title: "Another Document", author: "John Doe" },
|
||||
* // createdAt: "2024-07-30T12:00:00Z",
|
||||
* // updatedAt: "2024-07-30T12:00:00Z"
|
||||
* // },
|
||||
* // // ... additional items ...
|
||||
* // ]
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
async searchItems(
|
||||
namespacePrefix: string[],
|
||||
@@ -1183,6 +1260,7 @@ export class StoreClient extends BaseClient {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
query?: string;
|
||||
refreshTtl?: boolean | null;
|
||||
},
|
||||
): Promise<SearchItemsResponse> {
|
||||
const payload = {
|
||||
@@ -1191,6 +1269,7 @@ export class StoreClient extends BaseClient {
|
||||
limit: options?.limit ?? 10,
|
||||
offset: options?.offset ?? 0,
|
||||
query: options?.query,
|
||||
refresh_ttl: options?.refreshTtl,
|
||||
};
|
||||
|
||||
const response = await this.fetch<APISearchItemsResponse>(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { Client } from "./client.js";
|
||||
|
||||
export type {
|
||||
AssistantBase,
|
||||
Assistant,
|
||||
AssistantVersion,
|
||||
AssistantGraph,
|
||||
|
||||
@@ -0,0 +1,251 @@
|
||||
import { useStream } from "../react/index.js";
|
||||
import type { UIMessage } from "./types.js";
|
||||
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import * as JsxRuntime from "react/jsx-runtime";
|
||||
import type { UseStream } from "../react/stream.js";
|
||||
|
||||
const UseStreamContext = React.createContext<{
|
||||
stream: ReturnType<typeof useStream>;
|
||||
meta: unknown;
|
||||
}>(null!);
|
||||
|
||||
type BagTemplate = {
|
||||
ConfigurableType?: Record<string, unknown>;
|
||||
InterruptType?: unknown;
|
||||
CustomEventType?: unknown;
|
||||
UpdateType?: unknown;
|
||||
MetaType?: unknown;
|
||||
};
|
||||
|
||||
type GetMetaType<Bag extends BagTemplate> = Bag extends { MetaType: unknown }
|
||||
? Bag["MetaType"]
|
||||
: unknown;
|
||||
|
||||
interface UseStreamContext<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
Bag extends BagTemplate = BagTemplate,
|
||||
> extends UseStream<StateType, Bag> {
|
||||
meta?: GetMetaType<Bag>;
|
||||
}
|
||||
|
||||
export function useStreamContext<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
Bag extends {
|
||||
ConfigurableType?: Record<string, unknown>;
|
||||
InterruptType?: unknown;
|
||||
CustomEventType?: unknown;
|
||||
UpdateType?: unknown;
|
||||
MetaType?: unknown;
|
||||
} = BagTemplate,
|
||||
>(): UseStreamContext<StateType, Bag> {
|
||||
const ctx = React.useContext(UseStreamContext);
|
||||
if (!ctx) {
|
||||
throw new Error(
|
||||
"useStreamContext must be used within a LoadExternalComponent",
|
||||
);
|
||||
}
|
||||
|
||||
return new Proxy(ctx, {
|
||||
get(target, prop: keyof UseStreamContext<StateType, Bag>) {
|
||||
if (prop === "meta") return target.meta;
|
||||
return target.stream[prop];
|
||||
},
|
||||
}) as unknown as UseStreamContext<StateType, Bag>;
|
||||
}
|
||||
|
||||
interface ComponentTarget {
|
||||
comp: React.FunctionComponent | React.ComponentClass;
|
||||
target: HTMLElement;
|
||||
}
|
||||
|
||||
class ComponentStore {
|
||||
private cache: Record<string, ComponentTarget> = {};
|
||||
private boundCache: Record<
|
||||
string,
|
||||
{
|
||||
subscribe: (onStoreChange: () => void) => () => void;
|
||||
getSnapshot: () => ComponentTarget | undefined;
|
||||
}
|
||||
> = {};
|
||||
private callbacks: Record<
|
||||
string,
|
||||
((
|
||||
comp: React.FunctionComponent | React.ComponentClass,
|
||||
el: HTMLElement,
|
||||
) => void)[]
|
||||
> = {};
|
||||
|
||||
respond(
|
||||
shadowRootId: string,
|
||||
comp: React.FunctionComponent | React.ComponentClass,
|
||||
targetElement: HTMLElement,
|
||||
) {
|
||||
this.cache[shadowRootId] = { comp, target: targetElement };
|
||||
this.callbacks[shadowRootId]?.forEach((c) => c(comp, targetElement));
|
||||
}
|
||||
|
||||
getBoundStore(shadowRootId: string) {
|
||||
this.boundCache[shadowRootId] ??= {
|
||||
subscribe: (onStoreChange: () => void) => {
|
||||
this.callbacks[shadowRootId] ??= [];
|
||||
this.callbacks[shadowRootId].push(onStoreChange);
|
||||
return () => {
|
||||
this.callbacks[shadowRootId] = this.callbacks[shadowRootId].filter(
|
||||
(c) => c !== onStoreChange,
|
||||
);
|
||||
};
|
||||
},
|
||||
getSnapshot: () => this.cache[shadowRootId],
|
||||
};
|
||||
|
||||
return this.boundCache[shadowRootId];
|
||||
}
|
||||
}
|
||||
|
||||
const COMPONENT_STORE = new ComponentStore();
|
||||
const COMPONENT_PROMISE_CACHE: Record<string, Promise<string> | undefined> = {};
|
||||
|
||||
const EXT_STORE_SYMBOL = Symbol.for("LGUI_EXT_STORE");
|
||||
const REQUIRE_SYMBOL = Symbol.for("LGUI_REQUIRE");
|
||||
|
||||
interface LoadExternalComponentProps
|
||||
extends Pick<React.HTMLAttributes<HTMLDivElement>, "style" | "className"> {
|
||||
/** API URL of the LangGraph Platform */
|
||||
apiUrl?: string;
|
||||
|
||||
/** ID of the assistant */
|
||||
assistantId: string;
|
||||
|
||||
/** Stream of the assistant */
|
||||
stream: ReturnType<typeof useStream>;
|
||||
|
||||
/** UI message to be rendered */
|
||||
message: UIMessage;
|
||||
|
||||
/** Additional context to be passed to the child component */
|
||||
meta?: unknown;
|
||||
|
||||
/** Fallback to be rendered when the component is loading */
|
||||
fallback?: React.ReactNode;
|
||||
|
||||
/**
|
||||
* Map of components that can be rendered directly without fetching the UI code
|
||||
* from the server.
|
||||
*/
|
||||
components?: Record<string, React.FunctionComponent | React.ComponentClass>;
|
||||
}
|
||||
|
||||
function fetchComponent(
|
||||
apiUrl: string,
|
||||
assistantId: string,
|
||||
agentName: string,
|
||||
): Promise<string> {
|
||||
const cacheKey = `${apiUrl}-${assistantId}-${agentName}`;
|
||||
if (COMPONENT_PROMISE_CACHE[cacheKey] != null) {
|
||||
return COMPONENT_PROMISE_CACHE[cacheKey] as Promise<string>;
|
||||
}
|
||||
|
||||
const request: Promise<string> = fetch(`${apiUrl}/ui/${assistantId}`, {
|
||||
headers: { Accept: "text/html", "Content-Type": "application/json" },
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: agentName }),
|
||||
}).then((a) => a.text());
|
||||
|
||||
COMPONENT_PROMISE_CACHE[cacheKey] = request;
|
||||
return request;
|
||||
}
|
||||
|
||||
export function LoadExternalComponent({
|
||||
apiUrl = "http://localhost:2024",
|
||||
assistantId,
|
||||
stream,
|
||||
message,
|
||||
meta,
|
||||
fallback,
|
||||
components,
|
||||
...props
|
||||
}: LoadExternalComponentProps) {
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const id = React.useId();
|
||||
const shadowRootId = `child-shadow-${id}`;
|
||||
|
||||
const store = React.useMemo(
|
||||
() => COMPONENT_STORE.getBoundStore(shadowRootId),
|
||||
[shadowRootId],
|
||||
);
|
||||
const state = React.useSyncExternalStore(store.subscribe, store.getSnapshot);
|
||||
|
||||
const clientComponent = components?.[message.name];
|
||||
const hasClientComponent = clientComponent != null;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasClientComponent) return;
|
||||
fetchComponent(apiUrl, assistantId, message.name).then((html) => {
|
||||
const dom = ref.current;
|
||||
if (!dom) return;
|
||||
const root = dom.shadowRoot ?? dom.attachShadow({ mode: "open" });
|
||||
const fragment = document
|
||||
.createRange()
|
||||
.createContextualFragment(
|
||||
html.replace("{{shadowRootId}}", shadowRootId),
|
||||
);
|
||||
root.appendChild(fragment);
|
||||
});
|
||||
}, [apiUrl, assistantId, message.name, shadowRootId, hasClientComponent]);
|
||||
|
||||
if (hasClientComponent) {
|
||||
return React.createElement(clientComponent, message.content);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id={shadowRootId} ref={ref} {...props} />
|
||||
|
||||
<UseStreamContext.Provider value={{ stream, meta }}>
|
||||
{state?.target != null
|
||||
? ReactDOM.createPortal(
|
||||
React.createElement(state.comp, message.content),
|
||||
state.target,
|
||||
)
|
||||
: fallback}
|
||||
</UseStreamContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
[EXT_STORE_SYMBOL]: ComponentStore;
|
||||
[REQUIRE_SYMBOL]: (name: string) => unknown;
|
||||
}
|
||||
}
|
||||
|
||||
export function bootstrapUiContext() {
|
||||
if (typeof window === "undefined") {
|
||||
console.warn(
|
||||
"Attempting to bootstrap UI context outside of browser environment. " +
|
||||
"Avoid importing from `@langchain/langgraph-sdk/react-ui` in server context.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
window[EXT_STORE_SYMBOL] = COMPONENT_STORE;
|
||||
window[REQUIRE_SYMBOL] = (name: string) => {
|
||||
if (name === "react") return React;
|
||||
if (name === "react-dom") return ReactDOM;
|
||||
if (name === "react/jsx-runtime") return JsxRuntime;
|
||||
if (name === "@langchain/langgraph-sdk/react") return { useStream };
|
||||
if (name === "@langchain/langgraph-sdk/react-ui") {
|
||||
return {
|
||||
useStreamContext,
|
||||
LoadExternalComponent: () => {
|
||||
throw new Error("Nesting LoadExternalComponent is not supported");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unknown module...: ${name}`);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { bootstrapUiContext } from "./client.js";
|
||||
bootstrapUiContext();
|
||||
|
||||
export { useStreamContext, LoadExternalComponent } from "./client.js";
|
||||
export type { UIMessage, RemoveUIMessage } from "./types.js";
|
||||
@@ -0,0 +1,6 @@
|
||||
export { typedUi } from "./server.js";
|
||||
export {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "../types.js";
|
||||
@@ -0,0 +1,49 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { ComponentPropsWithoutRef, ElementType } from "react";
|
||||
import type { RemoveUIMessage, UIMessage } from "../types.js";
|
||||
|
||||
export const typedUi = <Decl extends Record<string, ElementType>>(config: {
|
||||
writer?: (chunk: unknown) => void;
|
||||
runId?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
tags?: string[];
|
||||
runName?: string;
|
||||
}) => {
|
||||
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
|
||||
let collect: (UIMessage | RemoveUIMessage)[] = [];
|
||||
|
||||
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
|
||||
if (!runId) throw new Error("run_id is required");
|
||||
|
||||
const metadata = {
|
||||
...config.metadata,
|
||||
tags: config.tags,
|
||||
name: config.runName,
|
||||
run_id: runId,
|
||||
};
|
||||
|
||||
const create = <K extends keyof PropMap & string>(
|
||||
name: K,
|
||||
props: PropMap[K],
|
||||
): UIMessage => ({
|
||||
type: "ui" as const,
|
||||
id: uuidv4(),
|
||||
name,
|
||||
content: props,
|
||||
additional_kwargs: metadata,
|
||||
});
|
||||
|
||||
const remove = (id: string): RemoveUIMessage => ({ type: "remove-ui", id });
|
||||
|
||||
return {
|
||||
create,
|
||||
remove,
|
||||
|
||||
collect,
|
||||
write: <K extends keyof PropMap & string>(name: K, props: PropMap[K]) => {
|
||||
const evt: UIMessage = create(name, props);
|
||||
collect.push(evt);
|
||||
config.writer?.(evt);
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export interface UIMessage {
|
||||
type: "ui";
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
content: Record<string, unknown>;
|
||||
additional_kwargs: {
|
||||
run_id: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface RemoveUIMessage {
|
||||
type: "remove-ui";
|
||||
id: string;
|
||||
}
|
||||
|
||||
export function uiMessageReducer(
|
||||
state: UIMessage[],
|
||||
update: UIMessage | RemoveUIMessage | (UIMessage | RemoveUIMessage)[],
|
||||
) {
|
||||
const events = Array.isArray(update) ? update : [update];
|
||||
let newState = state.slice();
|
||||
|
||||
for (const event of events) {
|
||||
if (event.type === "remove-ui") {
|
||||
newState = newState.filter((ui) => ui.id !== event.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const index = state.findIndex((ui) => ui.id === event.id);
|
||||
if (index !== -1) {
|
||||
newState[index] = event;
|
||||
} else {
|
||||
newState.push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
@@ -482,7 +482,7 @@ interface UseStreamOptions<
|
||||
onThreadId?: (threadId: string) => void;
|
||||
}
|
||||
|
||||
interface UseStream<
|
||||
export interface UseStream<
|
||||
StateType extends Record<string, unknown> = Record<string, unknown>,
|
||||
Bag extends BagTemplate = BagTemplate,
|
||||
> {
|
||||
|
||||
@@ -110,6 +110,9 @@ export interface AssistantBase {
|
||||
|
||||
/** The version of the assistant. */
|
||||
version: number;
|
||||
|
||||
/** The name of the assistant */
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AssistantVersion extends AssistantBase {}
|
||||
@@ -117,9 +120,6 @@ export interface AssistantVersion extends AssistantBase {}
|
||||
export interface Assistant extends AssistantBase {
|
||||
/** The last time the assistant was updated. */
|
||||
updated_at: string;
|
||||
|
||||
/** The name of the assistant */
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface AssistantGraph {
|
||||
|
||||
+25
-23
@@ -1005,17 +1005,16 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz#56e2cc26c397c038fab0e3a917a12d5c5909e901"
|
||||
integrity sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.14"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.14.tgz#1433419d73b2a7ebfc6918dcefd2ec0d5cd698f2"
|
||||
integrity sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==
|
||||
"@types/react-dom@^19.0.3":
|
||||
version "19.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.0.3.tgz#0804dfd279a165d5a0ad8b53a5b9e65f338050a4"
|
||||
integrity sha512-0Knk+HJiMP/qOZgMyNFamlIjw9OFCsyC2ZbigmEEyXXixgre6IQpm/4V+r3qH4GC1JPvRJKInw+on2rV6YZLeA==
|
||||
|
||||
"@types/react@18.3.2":
|
||||
version "18.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.2.tgz#462ae4904973bc212fa910424d901e3d137dbfcd"
|
||||
integrity sha512-Btgg89dAnqD4vV7R3hlwOxgqobUQKgx3MmrQRi0yYbs/P0ym8XozIAlkqVilPqHQwXs4e9Tf63rrCgl58BcO4w==
|
||||
"@types/react@^19.0.8":
|
||||
version "19.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.0.9.tgz#675255eb7d978bdaf71f9d08f6b740d41b0b7f32"
|
||||
integrity sha512-FedNTYgmMwSZmD1Sru/W1gJKuiYCN/3SuBkmZkcxX+FpO5zL76B22A9YNfAKg4HQO3Neh/30AiynP6BELdU0qQ==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/retry@0.12.0":
|
||||
@@ -2896,7 +2895,7 @@ js-tiktoken@^1.0.12:
|
||||
dependencies:
|
||||
base64-js "^1.5.1"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||
js-tokens@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
@@ -2991,13 +2990,6 @@ longest-streak@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-2.0.4.tgz#b8599957da5b5dab64dee3fe316fa774597d90e4"
|
||||
integrity sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==
|
||||
|
||||
loose-envify@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
lru-cache@^10.2.0:
|
||||
version "10.4.3"
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119"
|
||||
@@ -3640,17 +3632,22 @@ quick-lru@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f"
|
||||
integrity sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==
|
||||
|
||||
react-dom@^19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0.tgz#43446f1f01c65a4cd7f7588083e686a6726cfb57"
|
||||
integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==
|
||||
dependencies:
|
||||
scheduler "^0.25.0"
|
||||
|
||||
react-is@^18.0.0:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.3.1.tgz#e83557dc12eae63a99e003a46388b1dcbb44db7e"
|
||||
integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
|
||||
|
||||
react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
react@^19.0.0:
|
||||
version "19.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.0.0.tgz#6e1969251b9f108870aa4bff37a0ce9ddfaaabdd"
|
||||
integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==
|
||||
|
||||
read-pkg-up@^7.0.1:
|
||||
version "7.0.1"
|
||||
@@ -3840,6 +3837,11 @@ safe-regex-test@^1.0.3:
|
||||
es-errors "^1.3.0"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
scheduler@^0.25.0:
|
||||
version "0.25.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0.tgz#336cd9768e8cceebf52d3c80e3dcf5de23e7e015"
|
||||
integrity sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==
|
||||
|
||||
"semver@2 || 3 || 4 || 5":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
|
||||
|
||||
@@ -388,7 +388,9 @@ class AssistantsClient:
|
||||
'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'}
|
||||
'metadata': {'created_by': 'system'},
|
||||
'version': 1,
|
||||
'name': 'my_assistant'
|
||||
}
|
||||
|
||||
""" # noqa: E501
|
||||
@@ -742,7 +744,7 @@ class AssistantsClient:
|
||||
offset: The number of versions to skip.
|
||||
|
||||
Returns:
|
||||
list[Assistant]: A list of assistants.
|
||||
list[AssistantVersion]: A list of assistant versions.
|
||||
|
||||
Example Usage:
|
||||
|
||||
@@ -2127,6 +2129,7 @@ class StoreClient:
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
ttl: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Store or update an item.
|
||||
|
||||
@@ -2135,6 +2138,7 @@ class StoreClient:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
None
|
||||
@@ -2152,15 +2156,29 @@ class StoreClient:
|
||||
raise ValueError(
|
||||
f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')."
|
||||
)
|
||||
payload = {"namespace": namespace, "key": key, "value": value, "index": index}
|
||||
payload = {
|
||||
"namespace": namespace,
|
||||
"key": key,
|
||||
"value": value,
|
||||
"index": index,
|
||||
"ttl": ttl,
|
||||
}
|
||||
await self.http.put("/store/items", json=payload)
|
||||
|
||||
async def get_item(self, namespace: Sequence[str], /, key: str) -> Item:
|
||||
async def get_item(
|
||||
self,
|
||||
namespace: Sequence[str],
|
||||
/,
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = 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.
|
||||
@@ -2188,9 +2206,10 @@ class StoreClient:
|
||||
raise ValueError(
|
||||
f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')."
|
||||
)
|
||||
return await self.http.get(
|
||||
"/store/items", params={"namespace": ".".join(namespace), "key": key}
|
||||
)
|
||||
params = {"namespace": ".".join(namespace), "key": key}
|
||||
if refresh_ttl is not None:
|
||||
params["refresh_ttl"] = refresh_ttl
|
||||
return await self.http.get("/store/items", params=params)
|
||||
|
||||
async def delete_item(self, namespace: Sequence[str], /, key: str) -> None:
|
||||
"""Delete an item.
|
||||
@@ -2221,6 +2240,7 @@ class StoreClient:
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
query: Optional[str] = None,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> SearchItemsResponse:
|
||||
"""Search for items within a namespace prefix.
|
||||
|
||||
@@ -2230,6 +2250,7 @@ class StoreClient:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
List[Item]: A list of items matching the search criteria.
|
||||
@@ -2268,6 +2289,7 @@ class StoreClient:
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"query": query,
|
||||
"refresh_ttl": refresh_ttl,
|
||||
}
|
||||
|
||||
return await self.http.post("/store/items/search", json=_provided_vals(payload))
|
||||
@@ -4252,6 +4274,7 @@ class SyncStoreClient:
|
||||
key: str,
|
||||
value: dict[str, Any],
|
||||
index: Optional[Union[Literal[False], list[str]]] = None,
|
||||
ttl: Optional[int] = None,
|
||||
) -> None:
|
||||
"""Store or update an item.
|
||||
|
||||
@@ -4260,7 +4283,7 @@ class SyncStoreClient:
|
||||
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.
|
||||
Returns:
|
||||
None
|
||||
|
||||
@@ -4282,15 +4305,24 @@ class SyncStoreClient:
|
||||
"key": key,
|
||||
"value": value,
|
||||
"index": index,
|
||||
"ttl": ttl,
|
||||
}
|
||||
self.http.put("/store/items", json=payload)
|
||||
|
||||
def get_item(self, namespace: Sequence[str], /, key: str) -> Item:
|
||||
def get_item(
|
||||
self,
|
||||
namespace: Sequence[str],
|
||||
/,
|
||||
key: str,
|
||||
*,
|
||||
refresh_ttl: Optional[bool] = 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.
|
||||
@@ -4319,9 +4351,10 @@ class SyncStoreClient:
|
||||
f"Invalid namespace label '{label}'. Namespace labels cannot contain periods ('.')."
|
||||
)
|
||||
|
||||
return self.http.get(
|
||||
"/store/items", params={"key": key, "namespace": ".".join(namespace)}
|
||||
)
|
||||
params = {"key": key, "namespace": ".".join(namespace)}
|
||||
if refresh_ttl is not None:
|
||||
params["refresh_ttl"] = refresh_ttl
|
||||
return self.http.get("/store/items", params=params)
|
||||
|
||||
def delete_item(self, namespace: Sequence[str], /, key: str) -> None:
|
||||
"""Delete an item.
|
||||
@@ -4350,6 +4383,7 @@ class SyncStoreClient:
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
query: Optional[str] = None,
|
||||
refresh_ttl: Optional[bool] = None,
|
||||
) -> SearchItemsResponse:
|
||||
"""Search for items within a namespace prefix.
|
||||
|
||||
@@ -4359,6 +4393,7 @@ class SyncStoreClient:
|
||||
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.
|
||||
|
||||
Returns:
|
||||
List[Item]: A list of items matching the search criteria.
|
||||
@@ -4397,6 +4432,7 @@ class SyncStoreClient:
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"query": query,
|
||||
"refresh_ttl": refresh_ttl,
|
||||
}
|
||||
return self.http.post("/store/items/search", json=_provided_vals(payload))
|
||||
|
||||
|
||||
@@ -169,6 +169,8 @@ class AssistantBase(TypedDict):
|
||||
"""The assistant metadata."""
|
||||
version: int
|
||||
"""The version of the assistant"""
|
||||
name: str
|
||||
"""The name of the assistant"""
|
||||
|
||||
|
||||
class AssistantVersion(AssistantBase):
|
||||
@@ -182,8 +184,6 @@ class Assistant(AssistantBase):
|
||||
|
||||
updated_at: datetime
|
||||
"""The last time the assistant was updated."""
|
||||
name: str
|
||||
"""The name of the assistant"""
|
||||
|
||||
|
||||
class Interrupt(TypedDict, total=False):
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-sdk"
|
||||
version = "0.1.53"
|
||||
version = "0.1.55"
|
||||
description = "SDK for interacting with LangGraph API"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
Reference in New Issue
Block a user