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
28 changed files with 1029 additions and 834 deletions
@@ -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",
+480 -629
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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"
+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
+1 -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."""
@@ -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:
+24 -69
View File
@@ -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 -1
View File
@@ -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",
+1 -5
View File
@@ -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";
+23 -36
View File
@@ -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 };
};
-1
View File
@@ -6,7 +6,6 @@ export interface UIMessage {
content: Record<string, unknown>;
additional_kwargs: {
run_id: string;
message_id?: string;
[key: string]: unknown;
};
}
+1 -17
View File
@@ -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);
+1 -1
View File
@@ -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;
}[]