Compare commits

..
Author SHA1 Message Date
William Fu-Hinthorn 656737009b Update 2025-03-07 10:49:59 -08:00
William Fu-Hinthorn ecdf70a2ab langgraph-cli-install 2025-03-07 10:21:20 -08:00
William FHandGitHub d86502421d Add TTL args for SDKs (#3728) 2025-03-06 23:30:48 +00:00
William FHandGitHub cbf26a5d98 Fix indentation in docstring (#3727) 2025-03-06 13:35:20 -08:00
William FHandGitHub 09bd5990d4 Add TTL option for store items (#3704) 2025-03-06 13:20:32 -08:00
Nuno CamposandGitHub 79595d43a5 feat: bump sdk versions js and py (#3725) 2025-03-06 11:16:12 -08:00
Arjun Natarajan 49a6704bdc bump sdk versions js and py 2025-03-06 14:06:03 -05:00
David DuongandGitHub a430b7fcfb feat(cli): allow sending UI args (#3722) 2025-03-06 19:59:05 +01:00
Nuno CamposandGitHub 48c287d107 feat: update assistant version class (#3702) 2025-03-06 10:21:51 -08:00
Tat Dat Duong 0fdc787597 Bump to 0.1.75 2025-03-06 18:04:21 +01:00
Tat Dat Duong 3bd86fc6f5 Update schema 2025-03-06 17:57:51 +01:00
Tat Dat Duong f1024f7341 feat(cli): allow sending UI args 2025-03-06 17:55:42 +01:00
David DuongandGitHub f9ac88012f feat(sdk-js): loading external components (#3689)
- **chore: use prepack hook instead of prepublish**
- **feat(sdk-js): add react-ui implementation**
- **Add apiUrl, assistantId to props**
- **Bump to 0.0.46-experimental.0**
2025-03-06 17:11:50 +01:00
Tat Dat Duong 9d0186b5bf Bump to 0.0.47 2025-03-06 17:05:02 +01:00
Tat Dat Duong 40062c40df Add fallback for components defined at client level 2025-03-06 17:05:02 +01:00
Tat Dat Duong 36bbe059df Bump to 0.0.47-experimental.0 2025-03-06 17:05:01 +01:00
Tat Dat Duong 52627010cb Cache promises 2025-03-06 17:05:01 +01:00
Tat Dat Duong f6781d19ab Undo version experimental bump 2025-03-06 17:05:01 +01:00
Tat Dat Duong fcf134a452 Remove @langchain/langgraph-sdk/react-ui/types entrypoint 2025-03-06 17:05:01 +01:00
Tat Dat Duong 80331b88e0 Remove a nesting level 2025-03-06 17:05:01 +01:00
Tat Dat Duong b37f894f38 Stabilise boostrapping UI context 2025-03-06 17:05:01 +01:00
Tat Dat Duong 79de3dbad3 Fix require symbol 2025-03-06 17:05:00 +01:00
Tat Dat Duong 677fd3ce28 Update entrypoint 2025-03-06 17:05:00 +01:00
Tat Dat Duong 4ee863f30d Reexport as @langchain/langgraph-sdk/react-ui 2025-03-06 17:05:00 +01:00
Tat Dat Duong 7e060f88a2 Introduce useStreamContext 2025-03-06 17:05:00 +01:00
Tat Dat Duong f49856af0b Fix types for collect 2025-03-06 17:05:00 +01:00
Tat Dat Duong 5a7d384e2f Bump to 0.0.46-experimental.0 2025-03-06 17:05:00 +01:00
Tat Dat Duong b73b34ddd5 Add apiUrl, assistantId to props 2025-03-06 17:04:59 +01:00
Tat Dat Duong c9ffd753f5 feat(sdk-js): add react-ui implementation 2025-03-06 17:04:59 +01:00
Tat Dat Duong e85e157e8f chore: use prepack hook instead of prepublish 2025-03-06 17:04:59 +01:00
Arjun Natarajan 003226cef4 expose assistantbase 2025-03-05 16:47:18 -05:00
Arjun Natarajan 098a199cb9 update assistant version class 2025-03-05 15:39:34 -05:00
39 changed files with 1181 additions and 86 deletions
@@ -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.
+29 -12
View File
@@ -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 -1
View File
@@ -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"
+4 -4
View File
@@ -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()
+41
View File
@@ -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/
+21
View File
@@ -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.
+24
View File
@@ -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
+50
View File
@@ -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()
+44
View File
@@ -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"
+36
View File
@@ -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"]
+36
View File
@@ -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",
],
)
+1
View File
@@ -0,0 +1 @@
"""Test package for langgraph-cli-install."""
+50
View File
@@ -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)
+8
View File
@@ -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
+8 -1
View File
@@ -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 -1
View File
@@ -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"
+28
View File
@@ -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": [
+28
View File
@@ -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": [
+4
View File
@@ -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"""
+8
View File
@@ -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
+7 -1
View File
@@ -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",
+33 -5
View File
@@ -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"
]
}
+81 -2
View File
@@ -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
View File
@@ -1,6 +1,7 @@
export { Client } from "./client.js";
export type {
AssistantBase,
Assistant,
AssistantVersion,
AssistantGraph,
+251
View File
@@ -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}`);
};
}
+5
View File
@@ -0,0 +1,5 @@
import { bootstrapUiContext } from "./client.js";
bootstrapUiContext();
export { useStreamContext, LoadExternalComponent } from "./client.js";
export type { UIMessage, RemoveUIMessage } from "./types.js";
+6
View File
@@ -0,0 +1,6 @@
export { typedUi } from "./server.js";
export {
uiMessageReducer,
type UIMessage,
type RemoveUIMessage,
} from "../types.js";
+49
View File
@@ -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);
},
};
};
+40
View File
@@ -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;
}
+1 -1
View File
@@ -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,
> {
+3 -3
View File
@@ -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
View File
@@ -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"
+48 -12
View File
@@ -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))
+2 -2
View File
@@ -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 -1
View File
@@ -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"