diff --git a/libs/langgraph/uv.lock b/libs/langgraph/uv.lock index 0a17b3ef5..83a75a41a 100644 --- a/libs/langgraph/uv.lock +++ b/libs/langgraph/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.13' and python_full_version < '4.0'", @@ -1441,6 +1442,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-watcher" }, { name = "ruff" }, + { name = "syrupy" }, ] [[package]] diff --git a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py index 855d35e7b..87dd71e25 100644 --- a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py @@ -670,13 +670,16 @@ def create_react_agent( # This means that this node is the first one called workflow.set_entry_point(entrypoint) - agent_paths = ["tools", END] - post_model_hook_paths = [entrypoint, "tools", END] + agent_paths = [] + post_model_hook_paths = [entrypoint, "tools"] # Add a post model hook node if post_model_hook is provided if post_model_hook is not None: workflow.add_node("post_model_hook", post_model_hook) agent_paths.append("post_model_hook") + workflow.add_edge("agent", "post_model_hook") + else: + agent_paths.append("tools") # Add a structured output node if response_format is provided if response_format is not None: @@ -690,6 +693,11 @@ def create_react_agent( post_model_hook_paths.append("generate_structured_response") else: agent_paths.append("generate_structured_response") + else: + if post_model_hook is not None: + post_model_hook_paths.append(END) + else: + agent_paths.append(END) if post_model_hook is not None: diff --git a/libs/prebuilt/pyproject.toml b/libs/prebuilt/pyproject.toml index f73b8aece..28cefc7d4 100644 --- a/libs/prebuilt/pyproject.toml +++ b/libs/prebuilt/pyproject.toml @@ -32,6 +32,7 @@ dev = [ "langgraph-checkpoint", "langgraph-checkpoint-sqlite", "langgraph-checkpoint-postgres", + "syrupy", ] [tool.uv] diff --git a/libs/prebuilt/tests/__snapshots__/test_react_agent_graph.ambr b/libs/prebuilt/tests/__snapshots__/test_react_agent_graph.ambr new file mode 100644 index 000000000..dca7fc751 --- /dev/null +++ b/libs/prebuilt/tests/__snapshots__/test_react_agent_graph.ambr @@ -0,0 +1,173 @@ +# serializer version: 1 +# name: test_react_agent_graph_structure[None-None-None-tools0] + ''' + graph TD; + __start__ --> agent; + agent --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[None-None-None-tools1] + ''' + graph TD; + __start__ --> agent; + agent -.-> __end__; + agent -.-> tools; + tools --> agent; + + ''' +# --- +# name: test_react_agent_graph_structure[None-None-pre_model_hook-tools0] + ''' + graph TD; + __start__ --> pre_model_hook; + pre_model_hook --> agent; + agent --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[None-None-pre_model_hook-tools1] + ''' + graph TD; + __start__ --> pre_model_hook; + agent -.-> __end__; + agent -.-> tools; + pre_model_hook --> agent; + tools --> pre_model_hook; + + ''' +# --- +# name: test_react_agent_graph_structure[None-post_model_hook-None-tools0] + ''' + graph TD; + __start__ --> agent; + agent --> post_model_hook; + post_model_hook --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[None-post_model_hook-None-tools1] + ''' + graph TD; + __start__ --> agent; + agent --> post_model_hook; + post_model_hook -.-> __end__; + post_model_hook -.-> agent; + post_model_hook -.-> tools; + tools --> agent; + + ''' +# --- +# name: test_react_agent_graph_structure[None-post_model_hook-pre_model_hook-tools0] + ''' + graph TD; + __start__ --> pre_model_hook; + agent --> post_model_hook; + pre_model_hook --> agent; + post_model_hook --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[None-post_model_hook-pre_model_hook-tools1] + ''' + graph TD; + __start__ --> pre_model_hook; + agent --> post_model_hook; + post_model_hook -.-> __end__; + post_model_hook -.-> pre_model_hook; + post_model_hook -.-> tools; + pre_model_hook --> agent; + tools --> pre_model_hook; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-None-None-tools0] + ''' + graph TD; + __start__ --> agent; + agent --> generate_structured_response; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-None-None-tools1] + ''' + graph TD; + __start__ --> agent; + agent -.-> generate_structured_response; + agent -.-> tools; + tools --> agent; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-None-pre_model_hook-tools0] + ''' + graph TD; + __start__ --> pre_model_hook; + agent --> generate_structured_response; + pre_model_hook --> agent; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-None-pre_model_hook-tools1] + ''' + graph TD; + __start__ --> pre_model_hook; + agent -.-> generate_structured_response; + agent -.-> tools; + pre_model_hook --> agent; + tools --> pre_model_hook; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-post_model_hook-None-tools0] + ''' + graph TD; + __start__ --> agent; + agent --> post_model_hook; + post_model_hook --> generate_structured_response; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-post_model_hook-None-tools1] + ''' + graph TD; + __start__ --> agent; + agent --> post_model_hook; + post_model_hook -.-> agent; + post_model_hook -.-> generate_structured_response; + post_model_hook -.-> tools; + tools --> agent; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-post_model_hook-pre_model_hook-tools0] + ''' + graph TD; + __start__ --> pre_model_hook; + agent --> post_model_hook; + post_model_hook --> generate_structured_response; + pre_model_hook --> agent; + generate_structured_response --> __end__; + + ''' +# --- +# name: test_react_agent_graph_structure[ResponseFormat-post_model_hook-pre_model_hook-tools1] + ''' + graph TD; + __start__ --> pre_model_hook; + agent --> post_model_hook; + post_model_hook -.-> generate_structured_response; + post_model_hook -.-> pre_model_hook; + post_model_hook -.-> tools; + pre_model_hook --> agent; + tools --> pre_model_hook; + generate_structured_response --> __end__; + + ''' +# --- diff --git a/libs/prebuilt/tests/test_react_agent_graph.py b/libs/prebuilt/tests/test_react_agent_graph.py new file mode 100644 index 000000000..50d9f1846 --- /dev/null +++ b/libs/prebuilt/tests/test_react_agent_graph.py @@ -0,0 +1,52 @@ +from typing import Callable, Union + +import pytest +from pydantic import BaseModel +from syrupy import SnapshotAssertion + +from langgraph.prebuilt import create_react_agent +from tests.model import FakeToolCallingModel + +model = FakeToolCallingModel() + + +def tool() -> None: + """Testing tool.""" + ... + + +def pre_model_hook() -> None: + """Pre-model hook.""" + ... + + +def post_model_hook() -> None: + """Post-model hook.""" + ... + + +class ResponseFormat(BaseModel): + """Response format for the agent.""" + + result: str + + +@pytest.mark.parametrize("tools", [[], [tool]]) +@pytest.mark.parametrize("pre_model_hook", [None, pre_model_hook]) +@pytest.mark.parametrize("post_model_hook", [None, post_model_hook]) +@pytest.mark.parametrize("response_format", [None, ResponseFormat]) +def test_react_agent_graph_structure( + snapshot: SnapshotAssertion, + tools: list[Callable], + pre_model_hook: Union[Callable, None], + post_model_hook: Union[Callable, None], + response_format: Union[type[BaseModel], None], +) -> None: + agent = create_react_agent( + model, + tools=tools, + pre_model_hook=pre_model_hook, + post_model_hook=post_model_hook, + response_format=response_format, + ) + assert agent.get_graph().draw_mermaid(with_styles=False) == snapshot diff --git a/libs/prebuilt/uv.lock b/libs/prebuilt/uv.lock index 08a3546b7..0892daa79 100644 --- a/libs/prebuilt/uv.lock +++ b/libs/prebuilt/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.12.4'", @@ -480,6 +481,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-watcher" }, { name = "ruff" }, + { name = "syrupy" }, ] [package.metadata] @@ -501,6 +503,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-watcher" }, { name = "ruff" }, + { name = "syrupy" }, ] [[package]] @@ -1084,6 +1087,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6a/98/e8bc58b178266eae2fcf4c9c7a8303a8d41164d781b32d71097924a6bebe/sqlite_vec-0.1.6-py3-none-win_amd64.whl", hash = "sha256:c65bcfd90fa2f41f9000052bcb8bb75d38240b2dae49225389eca6c3136d3f0c", size = 281540 }, ] +[[package]] +name = "syrupy" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8c/f8/022d8704a3314f3e96dbd6bbd16ebe119ce30e35f41aabfa92345652fceb/syrupy-4.9.1.tar.gz", hash = "sha256:b7d0fcadad80a7d2f6c4c71917918e8ebe2483e8c703dfc8d49cdbb01081f9a4", size = 52492 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/9d/aef9ec5fd5a4ee2f6a96032c4eda5888c5c7cec65cef6b28c4fc37671d88/syrupy-4.9.1-py3-none-any.whl", hash = "sha256:b94cc12ed0e5e75b448255430af642516842a2374a46936dd2650cfb6dd20eda", size = 52214 }, +] + [[package]] name = "tenacity" version = "9.1.2" diff --git a/libs/scheduler-kafka/uv.lock b/libs/scheduler-kafka/uv.lock index 9f04053e7..c1ee02c3f 100644 --- a/libs/scheduler-kafka/uv.lock +++ b/libs/scheduler-kafka/uv.lock @@ -584,6 +584,7 @@ dev = [ { name = "pytest-mock" }, { name = "pytest-watcher" }, { name = "ruff" }, + { name = "syrupy" }, ] [[package]]