From e332869a0cecbb32db0a4d1df417c44d90ea2edb Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Mon, 30 Sep 2024 09:54:47 -0700 Subject: [PATCH 1/3] Ensure configurable (#1912) --- libs/langgraph/langgraph/utils/config.py | 2 ++ libs/langgraph/tests/test_runnable.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/libs/langgraph/langgraph/utils/config.py b/libs/langgraph/langgraph/utils/config.py index 6f75c64ba..3993df9b2 100644 --- a/libs/langgraph/langgraph/utils/config.py +++ b/libs/langgraph/langgraph/utils/config.py @@ -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 diff --git a/libs/langgraph/tests/test_runnable.py b/libs/langgraph/tests/test_runnable.py index 64d858fd4..2bd2e20d5 100644 --- a/libs/langgraph/tests/test_runnable.py +++ b/libs/langgraph/tests/test_runnable.py @@ -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" From 78f3ee9cae52bce5bb1b555b95241c3816b1c4ef Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Mon, 30 Sep 2024 13:01:47 -0400 Subject: [PATCH 2/3] langgraph: check if model passed as runnable binding with tools in create_react_agent (#1647) --- .../langgraph/prebuilt/chat_agent_executor.py | 56 +++++++++++-- libs/langgraph/tests/test_prebuilt.py | 80 ++++++++++++++++++- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py index 5e8274de4..21ad0e7b0 100644 --- a/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/langgraph/langgraph/prebuilt/chat_agent_executor.py @@ -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__"]: diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 6e1cd081a..42ab73957 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -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.""" From 9ad617bb7aac967f17b93785c16412e9312fd839 Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:16:50 -0700 Subject: [PATCH 3/3] [Release] 1.0.13 (#1903) --- libs/checkpoint/pyproject.toml | 2 +- libs/langgraph/poetry.lock | 25 +++++++------------------ libs/langgraph/pyproject.toml | 4 ++-- 3 files changed, 10 insertions(+), 21 deletions(-) diff --git a/libs/checkpoint/pyproject.toml b/libs/checkpoint/pyproject.toml index b3532da2a..d62c8095d 100644 --- a/libs/checkpoint/pyproject.toml +++ b/libs/checkpoint/pyproject.toml @@ -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" diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 24de562c4..850d2d48f 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -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" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 91d8a71fa..c58f724bb 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -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"