mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-19 22:25:44 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
656737009b | ||
|
|
ecdf70a2ab |
@@ -10,7 +10,6 @@
|
||||
"One of the most common use cases for persistence is to use it to keep track of conversation history. This is great - it makes it easy to continue conversations. As conversations get longer and longer, however, this conversation history can build up and take up more and more of the context window. This can often be undesirable as it leads to more expensive and longer calls to the LLM, and potentially ones that error. One way to work around that is to create a summary of the conversation to date, and use that with the past N messages. This guide will go through an example of how to do that.\n",
|
||||
"\n",
|
||||
"This will involve a few steps:\n",
|
||||
"\n",
|
||||
"- Check if the conversation is too long (can be done by checking number of messages or length of messages)\n",
|
||||
"- If yes, the create summary (will need a prompt for this)\n",
|
||||
"- Then remove all except the last N messages\n",
|
||||
|
||||
Generated
+480
-629
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "2.0.16"
|
||||
version = "2.0.15"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -530,7 +530,6 @@ class AsyncSqliteSaver(BaseCheckpointSaver[str]):
|
||||
for idx, (channel, value) in enumerate(writes)
|
||||
],
|
||||
)
|
||||
await self.conn.commit()
|
||||
|
||||
def get_next_version(self, current: Optional[str], channel: ChannelProtocol) -> str:
|
||||
"""Generate the next version ID for a channel.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "2.0.6"
|
||||
version = "2.0.5"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -682,7 +682,7 @@ class BaseStore(ABC):
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return self.batch([GetOp(namespace, str(key), refresh_ttl)])[0]
|
||||
return self.batch([GetOp(namespace, key, refresh_ttl)])[0]
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -811,7 +811,7 @@ class BaseStore(ABC):
|
||||
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, str(key), value, index=index, ttl=ttl)])
|
||||
self.batch([PutOp(namespace, key, value, index=index, ttl=ttl)])
|
||||
|
||||
def delete(self, namespace: tuple[str, ...], key: str) -> None:
|
||||
"""Delete an item.
|
||||
@@ -820,7 +820,7 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
"""
|
||||
self.batch([PutOp(namespace, str(key), None, ttl=None)])
|
||||
self.batch([PutOp(namespace, key, None, ttl=None)])
|
||||
|
||||
def list_namespaces(
|
||||
self,
|
||||
@@ -887,7 +887,7 @@ class BaseStore(ABC):
|
||||
Returns:
|
||||
The retrieved item or None if not found.
|
||||
"""
|
||||
return (await self.abatch([GetOp(namespace, str(key), refresh_ttl)]))[0]
|
||||
return (await self.abatch([GetOp(namespace, key, refresh_ttl)]))[0]
|
||||
|
||||
async def asearch(
|
||||
self,
|
||||
@@ -1027,7 +1027,7 @@ class BaseStore(ABC):
|
||||
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, str(key), value, index=index, ttl=ttl)])
|
||||
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.
|
||||
@@ -1036,7 +1036,7 @@ class BaseStore(ABC):
|
||||
namespace: Hierarchical path for the item.
|
||||
key: Unique identifier within the namespace.
|
||||
"""
|
||||
await self.abatch([PutOp(namespace, str(key), None)])
|
||||
await self.abatch([PutOp(namespace, key, None)])
|
||||
|
||||
async def alist_namespaces(
|
||||
self,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "2.0.18"
|
||||
version = "2.0.17"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -10,7 +10,6 @@ from typing import (
|
||||
TypeVar,
|
||||
Union,
|
||||
cast,
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from langchain_core.language_models import (
|
||||
@@ -58,27 +57,13 @@ class AgentState(TypedDict):
|
||||
remaining_steps: RemainingSteps
|
||||
|
||||
|
||||
class AgentStatePydantic(BaseModel):
|
||||
"""The state of the agent."""
|
||||
|
||||
messages: Annotated[Sequence[BaseMessage], add_messages]
|
||||
|
||||
remaining_steps: RemainingSteps = 25
|
||||
|
||||
|
||||
class AgentStateWithStructuredResponse(AgentState):
|
||||
"""The state of the agent with a structured response."""
|
||||
|
||||
structured_response: StructuredResponse
|
||||
|
||||
|
||||
class AgentStateWithStructuredResponsePydantic(AgentStatePydantic):
|
||||
"""The state of the agent with a structured response."""
|
||||
|
||||
structured_response: StructuredResponse
|
||||
|
||||
|
||||
StateSchema = TypeVar("StateSchema", bound=Union[AgentState, AgentStatePydantic])
|
||||
StateSchema = TypeVar("StateSchema", bound=AgentState)
|
||||
StateSchemaType = Type[StateSchema]
|
||||
|
||||
PROMPT_RUNNABLE_NAME = "Prompt"
|
||||
@@ -91,29 +76,21 @@ Prompt = Union[
|
||||
]
|
||||
|
||||
|
||||
def _get_state_value(state: StateSchema, key: str, default: Any = None) -> Any:
|
||||
return (
|
||||
state.get(key, default)
|
||||
if isinstance(state, dict)
|
||||
else getattr(state, key, default)
|
||||
)
|
||||
|
||||
|
||||
def _get_prompt_runnable(prompt: Optional[Prompt]) -> Runnable:
|
||||
prompt_runnable: Runnable
|
||||
if prompt is None:
|
||||
prompt_runnable = RunnableCallable(
|
||||
lambda state: _get_state_value(state, "messages"), name=PROMPT_RUNNABLE_NAME
|
||||
lambda state: state["messages"], name=PROMPT_RUNNABLE_NAME
|
||||
)
|
||||
elif isinstance(prompt, str):
|
||||
_system_message: BaseMessage = SystemMessage(content=prompt)
|
||||
prompt_runnable = RunnableCallable(
|
||||
lambda state: [_system_message] + _get_state_value(state, "messages"),
|
||||
lambda state: [_system_message] + state["messages"],
|
||||
name=PROMPT_RUNNABLE_NAME,
|
||||
)
|
||||
elif isinstance(prompt, SystemMessage):
|
||||
prompt_runnable = RunnableCallable(
|
||||
lambda state: [prompt] + _get_state_value(state, "messages"),
|
||||
lambda state: [prompt] + state["messages"],
|
||||
name=PROMPT_RUNNABLE_NAME,
|
||||
)
|
||||
elif inspect.iscoroutinefunction(prompt):
|
||||
@@ -306,7 +283,7 @@ def create_react_agent(
|
||||
The graph will make a separate call to the LLM to generate the structured response after the agent loop is finished.
|
||||
This is not the only strategy to get structured responses, see more options in [this guide](https://langchain-ai.github.io/langgraph/how-tos/react-agent-structured-output/).
|
||||
state_schema: An optional state schema that defines graph state.
|
||||
Must have `messages` and `remaining_steps` keys.
|
||||
Must have `messages` and `is_last_step` keys.
|
||||
Defaults to `AgentState` that defines those two keys.
|
||||
config_schema: An optional schema for configuration.
|
||||
Use this to expose configurable parameters via agent.config_specs.
|
||||
@@ -618,8 +595,7 @@ def create_react_agent(
|
||||
if response_format is not None:
|
||||
required_keys.add("structured_response")
|
||||
|
||||
schema_keys = set(get_type_hints(state_schema))
|
||||
if missing_keys := required_keys - set(schema_keys):
|
||||
if missing_keys := required_keys - set(state_schema.__annotations__):
|
||||
raise ValueError(f"Missing required key(s) {missing_keys} in state_schema")
|
||||
|
||||
if state_schema is None:
|
||||
@@ -660,34 +636,35 @@ def create_react_agent(
|
||||
# our graph needs to check if these were called
|
||||
should_return_direct = {t.name for t in tool_classes if t.return_direct}
|
||||
|
||||
def _are_more_steps_needed(state: StateSchema, response: BaseMessage) -> bool:
|
||||
# Define the function that calls the model
|
||||
def call_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
||||
_validate_chat_history(state["messages"])
|
||||
response = cast(AIMessage, model_runnable.invoke(state, config))
|
||||
# add agent name to the AIMessage
|
||||
response.name = name
|
||||
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
|
||||
all_tools_return_direct = (
|
||||
all(call["name"] in should_return_direct for call in response.tool_calls)
|
||||
if isinstance(response, AIMessage)
|
||||
else False
|
||||
)
|
||||
remaining_steps = _get_state_value(state, "remaining_steps", None)
|
||||
is_last_step = _get_state_value(state, "is_last_step", False)
|
||||
return (
|
||||
(remaining_steps is None and is_last_step and has_tool_calls)
|
||||
if (
|
||||
(
|
||||
"remaining_steps" not in state
|
||||
and state.get("is_last_step", False)
|
||||
and has_tool_calls
|
||||
)
|
||||
or (
|
||||
remaining_steps is not None
|
||||
and remaining_steps < 1
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 1
|
||||
and all_tools_return_direct
|
||||
)
|
||||
or (remaining_steps is not None and remaining_steps < 2 and has_tool_calls)
|
||||
)
|
||||
|
||||
# Define the function that calls the model
|
||||
def call_model(state: StateSchema, config: RunnableConfig) -> StateSchema:
|
||||
messages = _get_state_value(state, "messages")
|
||||
_validate_chat_history(messages)
|
||||
response = cast(AIMessage, model_runnable.invoke(state, config))
|
||||
# add agent name to the AIMessage
|
||||
response.name = name
|
||||
|
||||
if _are_more_steps_needed(state, response):
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 2
|
||||
and has_tool_calls
|
||||
)
|
||||
):
|
||||
return {
|
||||
"messages": [
|
||||
AIMessage(
|
||||
@@ -699,13 +676,34 @@ def create_react_agent(
|
||||
# We return a list, because this will get added to the existing list
|
||||
return {"messages": [response]}
|
||||
|
||||
async def acall_model(state: StateSchema, config: RunnableConfig) -> StateSchema:
|
||||
messages = _get_state_value(state, "messages")
|
||||
_validate_chat_history(messages)
|
||||
async def acall_model(state: AgentState, config: RunnableConfig) -> AgentState:
|
||||
_validate_chat_history(state["messages"])
|
||||
response = cast(AIMessage, await model_runnable.ainvoke(state, config))
|
||||
# add agent name to the AIMessage
|
||||
response.name = name
|
||||
if _are_more_steps_needed(state, response):
|
||||
has_tool_calls = isinstance(response, AIMessage) and response.tool_calls
|
||||
all_tools_return_direct = (
|
||||
all(call["name"] in should_return_direct for call in response.tool_calls)
|
||||
if isinstance(response, AIMessage)
|
||||
else False
|
||||
)
|
||||
if (
|
||||
(
|
||||
"remaining_steps" not in state
|
||||
and state.get("is_last_step", False)
|
||||
and has_tool_calls
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 1
|
||||
and all_tools_return_direct
|
||||
)
|
||||
or (
|
||||
"remaining_steps" in state
|
||||
and state["remaining_steps"] < 2
|
||||
and has_tool_calls
|
||||
)
|
||||
):
|
||||
return {
|
||||
"messages": [
|
||||
AIMessage(
|
||||
@@ -718,11 +716,11 @@ def create_react_agent(
|
||||
return {"messages": [response]}
|
||||
|
||||
def generate_structured_response(
|
||||
state: StateSchema, config: RunnableConfig
|
||||
) -> StateSchema:
|
||||
state: AgentState, config: RunnableConfig
|
||||
) -> AgentState:
|
||||
# NOTE: we exclude the last message because there is enough information
|
||||
# for the LLM to generate the structured response
|
||||
messages = _get_state_value(state, "messages")[:-1]
|
||||
messages = state["messages"][:-1]
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
@@ -735,11 +733,11 @@ def create_react_agent(
|
||||
return {"structured_response": response}
|
||||
|
||||
async def agenerate_structured_response(
|
||||
state: StateSchema, config: RunnableConfig
|
||||
) -> StateSchema:
|
||||
state: AgentState, config: RunnableConfig
|
||||
) -> AgentState:
|
||||
# NOTE: we exclude the last message because there is enough information
|
||||
# for the LLM to generate the structured response
|
||||
messages = _get_state_value(state, "messages")[:-1]
|
||||
messages = state["messages"][:-1]
|
||||
structured_response_schema = response_format
|
||||
if isinstance(response_format, tuple):
|
||||
system_prompt, structured_response_schema = response_format
|
||||
@@ -775,8 +773,8 @@ def create_react_agent(
|
||||
)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: StateSchema) -> Union[str, list]:
|
||||
messages = _get_state_value(state, "messages")
|
||||
def should_continue(state: AgentState) -> Union[str, list]:
|
||||
messages = state["messages"]
|
||||
last_message = messages[-1]
|
||||
# If there is no function call, then we finish
|
||||
if not isinstance(last_message, AIMessage) or not last_message.tool_calls:
|
||||
@@ -826,8 +824,8 @@ def create_react_agent(
|
||||
path_map=should_continue_destinations,
|
||||
)
|
||||
|
||||
def route_tool_responses(state: StateSchema) -> Literal["agent", "__end__"]:
|
||||
for m in reversed(_get_state_value(state, "messages")):
|
||||
def route_tool_responses(state: AgentState) -> Literal["agent", "__end__"]:
|
||||
for m in reversed(state["messages"]):
|
||||
if not isinstance(m, ToolMessage):
|
||||
break
|
||||
if m.name in should_return_direct:
|
||||
|
||||
@@ -5,7 +5,6 @@ from functools import partial
|
||||
from typing import (
|
||||
Annotated,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
@@ -36,8 +35,6 @@ from langgraph.prebuilt import (
|
||||
)
|
||||
from langgraph.prebuilt.chat_agent_executor import (
|
||||
AgentState,
|
||||
AgentStatePydantic,
|
||||
StateSchemaType,
|
||||
_get_model,
|
||||
_should_bind_tools,
|
||||
_validate_chat_history,
|
||||
@@ -531,31 +528,22 @@ def test_react_agent_with_structured_response(version: str) -> None:
|
||||
assert response["messages"][-2].content == "The weather is sunny and 75°F."
|
||||
|
||||
|
||||
class CustomState(AgentState):
|
||||
user_name: str
|
||||
|
||||
|
||||
class CustomStatePydantic(AgentStatePydantic):
|
||||
user_name: Optional[str] = None
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
not IS_LANGCHAIN_CORE_030_OR_GREATER,
|
||||
reason="Langchain core 0.3.0 or greater is required",
|
||||
)
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize("state_schema", [CustomState, CustomStatePydantic])
|
||||
def test_react_agent_update_state(
|
||||
request: pytest.FixtureRequest,
|
||||
checkpointer_name: str,
|
||||
version: str,
|
||||
state_schema: StateSchemaType,
|
||||
request: pytest.FixtureRequest, checkpointer_name: str, version: str
|
||||
) -> None:
|
||||
checkpointer: BaseCheckpointSaver = request.getfixturevalue(
|
||||
"checkpointer_" + checkpointer_name
|
||||
)
|
||||
|
||||
class State(AgentState):
|
||||
user_name: str
|
||||
|
||||
@dec_tool
|
||||
def get_user_name(tool_call_id: Annotated[str, InjectedToolCallId]):
|
||||
"""Retrieve user name"""
|
||||
@@ -571,31 +559,20 @@ def test_react_agent_update_state(
|
||||
}
|
||||
)
|
||||
|
||||
if issubclass(state_schema, AgentStatePydantic):
|
||||
def prompt(state: State):
|
||||
user_name = state.get("user_name")
|
||||
if user_name is None:
|
||||
return state["messages"]
|
||||
|
||||
def prompt(state: CustomStatePydantic):
|
||||
user_name = state.user_name
|
||||
if user_name is None:
|
||||
return state.messages
|
||||
|
||||
system_msg = f"User name is {user_name}"
|
||||
return [{"role": "system", "content": system_msg}] + state.messages
|
||||
else:
|
||||
|
||||
def prompt(state: CustomState):
|
||||
user_name = state.get("user_name")
|
||||
if user_name is None:
|
||||
return state["messages"]
|
||||
|
||||
system_msg = f"User name is {user_name}"
|
||||
return [{"role": "system", "content": system_msg}] + state["messages"]
|
||||
system_msg = f"User name is {user_name}"
|
||||
return [{"role": "system", "content": system_msg}] + state["messages"]
|
||||
|
||||
tool_calls = [[{"args": {}, "id": "1", "name": "get_user_name"}]]
|
||||
model = FakeToolCallingModel(tool_calls=tool_calls)
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
[get_user_name],
|
||||
state_schema=state_schema,
|
||||
state_schema=State,
|
||||
prompt=prompt,
|
||||
checkpointer=checkpointer,
|
||||
version=version,
|
||||
@@ -825,45 +802,23 @@ def test_tool_node_inject_state(schema_: Type[T]) -> None:
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
|
||||
class AgentStateExtraKey(AgentState):
|
||||
foo: int
|
||||
|
||||
|
||||
class AgentStateExtraKeyPydantic(AgentStatePydantic):
|
||||
foo: int
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
@pytest.mark.parametrize(
|
||||
"state_schema", [AgentStateExtraKey, AgentStateExtraKeyPydantic]
|
||||
)
|
||||
def test_create_react_agent_inject_vars(
|
||||
version: str, state_schema: StateSchemaType
|
||||
) -> None:
|
||||
def test_create_react_agent_inject_vars(version: str) -> None:
|
||||
class AgentStateExtraKey(AgentState):
|
||||
foo: int
|
||||
|
||||
store = InMemoryStore()
|
||||
namespace = ("test",)
|
||||
store.put(namespace, "test_key", {"bar": 3})
|
||||
|
||||
if issubclass(state_schema, AgentStatePydantic):
|
||||
|
||||
def tool1(
|
||||
some_val: int,
|
||||
state: Annotated[AgentStateExtraKeyPydantic, InjectedState],
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["bar"]
|
||||
return some_val + state.foo + store_val
|
||||
else:
|
||||
|
||||
def tool1(
|
||||
some_val: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["bar"]
|
||||
return some_val + state["foo"] + store_val
|
||||
def tool1(
|
||||
some_val: int,
|
||||
state: Annotated[dict, InjectedState],
|
||||
store: Annotated[BaseStore, InjectedStore()],
|
||||
) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
store_val = store.get(namespace, "test_key").value["bar"]
|
||||
return some_val + state["foo"] + store_val
|
||||
|
||||
tool_call = {
|
||||
"name": "tool1",
|
||||
@@ -875,7 +830,7 @@ def test_create_react_agent_inject_vars(
|
||||
agent = create_react_agent(
|
||||
model,
|
||||
[tool1],
|
||||
state_schema=state_schema,
|
||||
state_schema=AgentStateExtraKey,
|
||||
store=store,
|
||||
version=version,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@langchain/langgraph-sdk",
|
||||
"version": "0.0.51",
|
||||
"version": "0.0.49",
|
||||
"description": "Client library for interacting with the LangGraph API",
|
||||
"type": "module",
|
||||
"packageManager": "yarn@1.22.19",
|
||||
|
||||
@@ -2,8 +2,4 @@ import { bootstrapUiContext } from "./client.js";
|
||||
bootstrapUiContext();
|
||||
|
||||
export { useStreamContext, LoadExternalComponent } from "./client.js";
|
||||
export {
|
||||
uiMessageReducer,
|
||||
type UIMessage,
|
||||
type RemoveUIMessage,
|
||||
} from "./types.js";
|
||||
export type { UIMessage, RemoveUIMessage } from "./types.js";
|
||||
|
||||
@@ -2,10 +2,6 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import type { ComponentPropsWithoutRef, ElementType } from "react";
|
||||
import type { RemoveUIMessage, UIMessage } from "../types.js";
|
||||
|
||||
interface MessageLike {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export const typedUi = <Decl extends Record<string, ElementType>>(config: {
|
||||
writer?: (chunk: unknown) => void;
|
||||
runId?: string;
|
||||
@@ -14,7 +10,7 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
|
||||
runName?: string;
|
||||
}) => {
|
||||
type PropMap = { [K in keyof Decl]: ComponentPropsWithoutRef<Decl[K]> };
|
||||
let items: (UIMessage | RemoveUIMessage)[] = [];
|
||||
let collect: (UIMessage | RemoveUIMessage)[] = [];
|
||||
|
||||
const runId = (config.metadata?.run_id as string | undefined) ?? config.runId;
|
||||
if (!runId) throw new Error("run_id is required");
|
||||
@@ -26,37 +22,28 @@ export const typedUi = <Decl extends Record<string, ElementType>>(config: {
|
||||
run_id: runId,
|
||||
};
|
||||
|
||||
const handlePush = <K extends keyof PropMap & string>(
|
||||
message: {
|
||||
id?: string;
|
||||
name: K;
|
||||
content: PropMap[K];
|
||||
additional_kwargs?: Record<string, unknown>;
|
||||
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);
|
||||
},
|
||||
options?: { message?: MessageLike },
|
||||
): UIMessage => {
|
||||
const evt: UIMessage = {
|
||||
type: "ui" as const,
|
||||
id: message?.id ?? uuidv4(),
|
||||
name: message?.name,
|
||||
content: message?.content,
|
||||
additional_kwargs: {
|
||||
...metadata,
|
||||
...message?.additional_kwargs,
|
||||
...(options?.message ? { message_id: options.message.id } : null),
|
||||
},
|
||||
};
|
||||
items.push(evt);
|
||||
config.writer?.(evt);
|
||||
return evt;
|
||||
};
|
||||
|
||||
const handleDelete = (id: string): RemoveUIMessage => {
|
||||
const evt: RemoveUIMessage = { type: "remove-ui", id };
|
||||
items.push(evt);
|
||||
config.writer?.(evt);
|
||||
return evt;
|
||||
};
|
||||
|
||||
return { push: handlePush, delete: handleDelete, items };
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ export interface UIMessage {
|
||||
content: Record<string, unknown>;
|
||||
additional_kwargs: {
|
||||
run_id: string;
|
||||
message_id?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -464,11 +464,6 @@ interface UseStreamOptions<
|
||||
*/
|
||||
onCustomEvent?: (
|
||||
data: CustomStreamEvent<GetCustomEventType<Bag>>["data"],
|
||||
options: {
|
||||
mutate: (
|
||||
update: Partial<StateType> | ((prev: StateType) => Partial<StateType>),
|
||||
) => void;
|
||||
},
|
||||
) => void;
|
||||
|
||||
/**
|
||||
@@ -839,18 +834,7 @@ export function useStream<
|
||||
}
|
||||
|
||||
if (event === "updates") options.onUpdateEvent?.(data);
|
||||
if (event === "custom")
|
||||
options.onCustomEvent?.(data, {
|
||||
mutate: (update) =>
|
||||
setStreamValues((prev) => {
|
||||
// should not happen
|
||||
if (prev == null) return prev;
|
||||
return {
|
||||
...prev,
|
||||
...(typeof update === "function" ? update(prev) : update),
|
||||
};
|
||||
}),
|
||||
});
|
||||
if (event === "custom") options.onCustomEvent?.(data);
|
||||
if (event === "metadata") options.onMetadataEvent?.(data);
|
||||
|
||||
if (event === "values") setStreamValues(data);
|
||||
|
||||
@@ -26,7 +26,7 @@ export type AIMessage = {
|
||||
tool_calls?:
|
||||
| {
|
||||
name: string;
|
||||
args: { [x: string]: any };
|
||||
args: { [x: string]: { [x: string]: any } };
|
||||
id?: string | undefined;
|
||||
type?: "tool_call" | undefined;
|
||||
}[]
|
||||
|
||||
Reference in New Issue
Block a user