feat(prebuilt): Split tool node to individual tool nodes (#5888)

Add option to split tool node to individual nodes. 

Summary:
* User code (specifically streaming) may break if it's relying on the
name of the `tools` node
* The boolean flag in the interface is likely **temporary** (especially
if there are no major breaking changes)
* We'll need to decide if we can get rid of the version in create react
agent. "v1" is not consistent conceptually with a node per tool.
This commit is contained in:
Eugene Yurtsev
2025-08-13 09:49:27 -04:00
committed by GitHub
parent 7e257dadd6
commit 9e9a5d2498
5 changed files with 382 additions and 27 deletions
+50 -5
View File
@@ -780,14 +780,24 @@ class AgentStateExtraKeyPydantic(AgentStatePydantic):
foo: int
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
@pytest.mark.parametrize("version", ["v1", "v2"])
@pytest.mark.parametrize(
"state_schema", [AgentStateExtraKey, AgentStateExtraKeyPydantic]
)
@pytest.mark.parametrize(
"use_individual_tool_nodes",
[False, True],
ids=["single_tool_node", "node_per_tool"],
)
def test_create_react_agent_inject_vars(
version: Literal["v1", "v2"], state_schema: StateSchemaType
version: Literal["v1", "v2"],
state_schema: StateSchemaType,
use_individual_tool_nodes: bool,
) -> None:
"""Test that the agent can inject state and store into tool functions."""
if version == "v1" and use_individual_tool_nodes:
pytest.skip("v1 does not support individual tool nodes")
store = InMemoryStore()
namespace = ("test",)
store.put(namespace, "test_key", {"bar": 3})
@@ -826,6 +836,7 @@ def test_create_react_agent_inject_vars(
state_schema=state_schema,
store=store,
version=version,
use_individual_tool_nodes=use_individual_tool_nodes,
)
result = agent.invoke({"messages": [{"role": "user", "content": "hi"}], "foo": 2})
assert result["messages"] == [
@@ -967,7 +978,17 @@ def test_tool_node_messages_key() -> None:
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
async def test_return_direct(version: str) -> None:
@pytest.mark.parametrize(
"use_individual_tool_nodes",
[False, True],
ids=["single_tool_node", "node_per_tool"],
)
async def test_return_direct(
version: Literal["v1", "v2"], use_individual_tool_nodes: bool
) -> None:
if version == "v1" and use_individual_tool_nodes:
pytest.skip("v1 does not support individual tool nodes")
@dec_tool(return_direct=True)
def tool_return_direct(input: str) -> str:
"""A tool that returns directly."""
@@ -995,6 +1016,7 @@ async def test_return_direct(version: str) -> None:
model,
[tool_return_direct, tool_normal],
version=version,
use_individual_tool_nodes=use_individual_tool_nodes,
)
# Test direct return for tool_return_direct
@@ -1088,15 +1110,27 @@ def test__get_state_args() -> None:
def test_inspect_react() -> None:
"""Test that we can inspect the agent and its nodes."""
model = FakeToolCallingModel(tool_calls=[])
agent = create_react_agent(model, [])
inspect.getclosurevars(agent.nodes["agent"].bound.func)
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
@pytest.mark.parametrize(
"use_individual_tool_nodes",
[False, True],
ids=["single_tool_node", "node_per_tool"],
)
def test_react_with_subgraph_tools(
sync_checkpointer: BaseCheckpointSaver, version: Literal["v1", "v2"]
sync_checkpointer: BaseCheckpointSaver,
version: Literal["v1", "v2"],
use_individual_tool_nodes: bool,
) -> None:
"""Test React agent with subgraph tools."""
if version == "v1" and use_individual_tool_nodes:
pytest.skip("v1 does not support individual tool nodes")
class State(TypedDict):
a: int
b: int
@@ -1152,6 +1186,7 @@ def test_react_with_subgraph_tools(
tool_node,
checkpointer=sync_checkpointer,
version=version,
use_individual_tool_nodes=use_individual_tool_nodes,
)
result = agent.invoke(
{"messages": [HumanMessage(content="What's 2 + 3 and 2 * 3?")]},
@@ -1237,8 +1272,17 @@ def test_tool_node_stream_writer() -> None:
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> None:
@pytest.mark.parametrize(
"use_individual_tool_nodes",
[False, True],
ids=["single_tool_node", "node_per_tool"],
)
def test_react_agent_subgraph_streaming_sync(
version: Literal["v1", "v2"], use_individual_tool_nodes: bool
) -> None:
"""Test React agent streaming when used as a subgraph node sync version"""
if version == "v1" and use_individual_tool_nodes:
pytest.skip("v1 does not support individual tool nodes")
@dec_tool
def get_weather(city: str) -> str:
@@ -1258,6 +1302,7 @@ def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> No
tools=[get_weather],
prompt="You are a helpful travel assistant.",
version=version,
use_individual_tool_nodes=use_individual_tool_nodes,
)
# Create a subgraph that uses the React agent as a node