mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-08-23 16:12:25 +02:00
Merge branch 'main' into wfh/js_store_client
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.12"
|
||||
version = "1.0.13"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
authors = []
|
||||
license = "MIT"
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
from typing import Callable, Literal, Optional, Sequence, Type, TypeVar, Union
|
||||
from typing import Callable, Literal, Optional, Sequence, Type, TypeVar, Union, cast
|
||||
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
from langchain_core.language_models import BaseChatModel, LanguageModelLike
|
||||
from langchain_core.messages import AIMessage, BaseMessage, SystemMessage, ToolMessage
|
||||
from langchain_core.runnables import Runnable, RunnableConfig, RunnableLambda
|
||||
from langchain_core.runnables import (
|
||||
Runnable,
|
||||
RunnableBinding,
|
||||
RunnableConfig,
|
||||
RunnableLambda,
|
||||
)
|
||||
from langchain_core.tools import BaseTool
|
||||
from typing_extensions import Annotated, TypedDict
|
||||
|
||||
@@ -115,9 +120,43 @@ def _get_model_preprocessing_runnable(
|
||||
return _get_state_modifier_runnable(state_modifier)
|
||||
|
||||
|
||||
def _should_bind_tools(model: LanguageModelLike, tools: Sequence[BaseTool]) -> bool:
|
||||
if not isinstance(model, RunnableBinding):
|
||||
return False
|
||||
|
||||
if "tools" not in model.kwargs:
|
||||
return False
|
||||
|
||||
bound_tools = model.kwargs["tools"]
|
||||
if len(tools) != len(bound_tools):
|
||||
raise ValueError(
|
||||
"Number of tools in the model.bind_tools() and tools passed to create_react_agent must match"
|
||||
)
|
||||
|
||||
tool_names = set(tool.name for tool in tools)
|
||||
bound_tool_names = set()
|
||||
for bound_tool in bound_tools:
|
||||
# OpenAI-style tool
|
||||
if bound_tool.get("type") == "function":
|
||||
bound_tool_name = bound_tool["function"]["name"]
|
||||
# Anthropic-style tool
|
||||
elif bound_tool.get("name"):
|
||||
bound_tool_name = bound_tool["name"]
|
||||
else:
|
||||
# unknown tool type so we'll ignore it
|
||||
continue
|
||||
|
||||
bound_tool_names.add(bound_tool_name)
|
||||
|
||||
if missing_tools := tool_names - bound_tool_names:
|
||||
raise ValueError(f"Missing tools '{missing_tools}' in the model.bind_tools()")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@deprecated_parameter("messages_modifier", "0.1.9", "state_modifier", removal="0.3.0")
|
||||
def create_react_agent(
|
||||
model: BaseChatModel,
|
||||
model: LanguageModelLike,
|
||||
tools: Union[ToolExecutor, Sequence[BaseTool], ToolNode],
|
||||
*,
|
||||
state_schema: Optional[StateSchemaType] = None,
|
||||
@@ -412,9 +451,12 @@ def create_react_agent(
|
||||
tool_classes = list(tools.tools_by_name.values())
|
||||
tool_node = tools
|
||||
else:
|
||||
tool_classes = tools
|
||||
tool_node = ToolNode(tool_classes)
|
||||
model = model.bind_tools(tool_classes)
|
||||
tool_node = ToolNode(tools)
|
||||
# get the tool functions wrapped in a tool class from the ToolNode
|
||||
tool_classes = list(tool_node.tools_by_name.values())
|
||||
|
||||
if _should_bind_tools(model, tool_classes):
|
||||
model = cast(BaseChatModel, model).bind_tools(tool_classes)
|
||||
|
||||
# Define the function that determines whether to continue or not
|
||||
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
|
||||
|
||||
@@ -120,6 +120,8 @@ def merge_configs(*configs: Optional[RunnableConfig]) -> RunnableConfig:
|
||||
base["recursion_limit"] = config["recursion_limit"]
|
||||
else:
|
||||
base[key] = config[key] # type: ignore[literal-required]
|
||||
if CONF not in base:
|
||||
base[CONF] = {}
|
||||
return base
|
||||
|
||||
|
||||
|
||||
Generated
+7
-18
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aiosqlite"
|
||||
@@ -1238,7 +1238,7 @@ typing-extensions = ">=4.7"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint"
|
||||
version = "1.0.10"
|
||||
version = "1.0.13"
|
||||
description = "Library with base interfaces for LangGraph checkpoint savers."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1255,7 +1255,7 @@ url = "../checkpoint"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-postgres"
|
||||
version = "1.0.7"
|
||||
version = "1.0.9"
|
||||
description = "Library with a Postgres implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0,<4.0"
|
||||
@@ -1263,7 +1263,7 @@ files = []
|
||||
develop = true
|
||||
|
||||
[package.dependencies]
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
langgraph-checkpoint = "^1.0.11"
|
||||
orjson = ">=3.10.1"
|
||||
psycopg = "^3.0.0"
|
||||
psycopg-pool = "^3.0.0"
|
||||
@@ -1274,7 +1274,7 @@ url = "../checkpoint-postgres"
|
||||
|
||||
[[package]]
|
||||
name = "langgraph-checkpoint-sqlite"
|
||||
version = "1.0.3"
|
||||
version = "1.0.4"
|
||||
description = "Library with a SQLite implementation of LangGraph checkpoint saver."
|
||||
optional = false
|
||||
python-versions = "^3.9.0"
|
||||
@@ -1283,7 +1283,7 @@ develop = true
|
||||
|
||||
[package.dependencies]
|
||||
aiosqlite = "^0.20.0"
|
||||
langgraph-checkpoint = "^1.0.8"
|
||||
langgraph-checkpoint = "^1.0.11"
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
@@ -2385,7 +2385,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
|
||||
{file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
|
||||
@@ -2393,16 +2392,8 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
|
||||
{file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
|
||||
{file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
|
||||
{file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
|
||||
@@ -2419,7 +2410,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
|
||||
{file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
|
||||
@@ -2427,7 +2417,6 @@ files = [
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
|
||||
{file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
|
||||
{file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
|
||||
@@ -3216,4 +3205,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.9.0,<4.0"
|
||||
content-hash = "2c74c10f4650f14f2757e1a688761a9680ecd251da088ea1e8c5ceda51aec067"
|
||||
content-hash = "272370fa28665231d6b09e9225330a224fc604cb7ec2cd5178b52e7f5c7ab15b"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "langgraph"
|
||||
version = "0.2.28"
|
||||
version = "0.2.29"
|
||||
description = "Building stateful, multi-actor applications with LLMs"
|
||||
authors = []
|
||||
license = "MIT"
|
||||
@@ -10,7 +10,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
|
||||
[tool.poetry.dependencies]
|
||||
python = ">=3.9.0,<4.0"
|
||||
langchain-core = ">=0.2.39,<0.4"
|
||||
langgraph-checkpoint = "^1.0.2"
|
||||
langgraph-checkpoint = "^1.0.13"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.2"
|
||||
|
||||
@@ -6,6 +6,7 @@ from typing import (
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Literal,
|
||||
Optional,
|
||||
Sequence,
|
||||
Type,
|
||||
@@ -49,6 +50,7 @@ pytestmark = pytest.mark.anyio
|
||||
class FakeToolCallingModel(BaseChatModel):
|
||||
tool_calls: Optional[list[list[ToolCall]]] = None
|
||||
index: int = 0
|
||||
tool_style: Literal["openai", "anthropic"] = "openai"
|
||||
|
||||
def _generate(
|
||||
self,
|
||||
@@ -79,7 +81,31 @@ class FakeToolCallingModel(BaseChatModel):
|
||||
tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],
|
||||
**kwargs: Any,
|
||||
) -> Runnable[LanguageModelInput, BaseMessage]:
|
||||
return self
|
||||
tool_dicts = []
|
||||
for tool in tools:
|
||||
if not isinstance(tool, BaseTool):
|
||||
raise TypeError(
|
||||
"Only BaseTool is supported by FakeToolCallingModel.bind_tools"
|
||||
)
|
||||
|
||||
# NOTE: this is a simplified tool spec for testing purposes only
|
||||
if self.tool_style == "openai":
|
||||
tool_dicts.append(
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": tool.name,
|
||||
},
|
||||
}
|
||||
)
|
||||
elif self.tool_style == "anthropic":
|
||||
tool_dicts.append(
|
||||
{
|
||||
"name": tool.name,
|
||||
}
|
||||
)
|
||||
|
||||
return self.bind(tools=tool_dicts)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("checkpointer_name", ALL_CHECKPOINTERS_SYNC)
|
||||
@@ -242,6 +268,58 @@ def test_runnable_state_modifier():
|
||||
assert response == expected_response
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_style", ["openai", "anthropic"])
|
||||
def test_model_with_tools(tool_style: str):
|
||||
model = FakeToolCallingModel(tool_style=tool_style)
|
||||
|
||||
@dec_tool
|
||||
def tool1(some_val: int) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
return f"Tool 1: {some_val}"
|
||||
|
||||
@dec_tool
|
||||
def tool2(some_val: int) -> str:
|
||||
"""Tool 2 docstring."""
|
||||
return f"Tool 2: {some_val}"
|
||||
|
||||
# check valid agent constructor
|
||||
agent = create_react_agent(model.bind_tools([tool1, tool2]), [tool1, tool2])
|
||||
result = agent.nodes["tools"].invoke(
|
||||
{
|
||||
"messages": [
|
||||
AIMessage(
|
||||
"hi?",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "tool1",
|
||||
"args": {"some_val": 2},
|
||||
"id": "some 1",
|
||||
},
|
||||
{
|
||||
"name": "tool2",
|
||||
"args": {"some_val": 2},
|
||||
"id": "some 2",
|
||||
},
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
)
|
||||
tool_messages: ToolMessage = result["messages"][-2:]
|
||||
for tool_message in tool_messages:
|
||||
assert tool_message.type == "tool"
|
||||
assert tool_message.content in {"Tool 1: 2", "Tool 2: 2"}
|
||||
assert tool_message.tool_call_id in {"some 1", "some 2"}
|
||||
|
||||
# test mismatching tool lengths
|
||||
with pytest.raises(ValueError):
|
||||
create_react_agent(model.bind_tools([tool1]), [tool1, tool2])
|
||||
|
||||
# test missing bound tools
|
||||
with pytest.raises(ValueError):
|
||||
create_react_agent(model.bind_tools([tool1]), [tool2])
|
||||
|
||||
|
||||
async def test_tool_node():
|
||||
def tool1(some_val: int, some_other_val: str) -> str:
|
||||
"""Tool 1 docstring."""
|
||||
|
||||
@@ -41,3 +41,21 @@ def test_runnable_callable_func_accepts():
|
||||
for name, runnable in runnables.items():
|
||||
assert runnable.func_accepts["writer"] == expected_writer.get(name, False)
|
||||
assert runnable.func_accepts["store"] == expected_store.get(name, False)
|
||||
|
||||
|
||||
async def test_runnable_callable_basic():
|
||||
def sync_func(x: Any) -> str:
|
||||
return f"{x}"
|
||||
|
||||
async def async_func(x: Any) -> str:
|
||||
return f"{x}"
|
||||
|
||||
runnable_sync = RunnableCallable(sync_func)
|
||||
runnable_async = RunnableCallable(func=None, afunc=async_func)
|
||||
|
||||
result_sync = runnable_sync.invoke("test")
|
||||
assert result_sync == "test"
|
||||
|
||||
# Test asynchronous ainvoke
|
||||
result_async = await runnable_async.ainvoke("test")
|
||||
assert result_async == "test"
|
||||
|
||||
Reference in New Issue
Block a user