From 4151861ca2b73df334b77a03b5ea42ff3124baa0 Mon Sep 17 00:00:00 2001 From: Sydney Runkle <54324534+sydney-runkle@users.noreply.github.com> Date: Tue, 19 Aug 2025 14:30:32 -0400 Subject: [PATCH] chore(prebuilt): remove v1 (#5960) --- .../langgraph/prebuilt/chat_agent_executor.py | 39 +---- libs/prebuilt/tests/test_react_agent.py | 147 +++++------------- 2 files changed, 46 insertions(+), 140 deletions(-) diff --git a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py index 5180dd017..f54b7b2e9 100644 --- a/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py +++ b/libs/prebuilt/langgraph/prebuilt/chat_agent_executor.py @@ -5,7 +5,6 @@ from typing import ( Any, Awaitable, Callable, - Literal, Optional, Sequence, Type, @@ -204,7 +203,6 @@ class _AgentBuilder: post_model_hook: Optional[RunnableLike] = None, state_schema: Optional[StateSchemaType] = None, context_schema: Optional[Type[Any]] = None, - version: Literal["v1", "v2"] = "v2", name: Optional[str] = None, store: Optional[BaseStore] = None, ): @@ -216,7 +214,6 @@ class _AgentBuilder: self.post_model_hook = post_model_hook self.state_schema = state_schema self.context_schema = context_schema - self.version = version self.name = name self.store = store @@ -604,16 +601,13 @@ class _AgentBuilder: else: return END else: - if self.version == "v1": - return "tools" - elif self.version == "v2": - if self.post_model_hook is not None: - return "post_model_hook" - tool_calls = [ - self._tool_node.inject_tool_args(call, state, self.store) # type: ignore[arg-type] - for call in last_message.tool_calls - ] - return [Send("tools", [tool_call]) for tool_call in tool_calls] + if self.post_model_hook is not None: + return "post_model_hook" + tool_calls = [ + self._tool_node.inject_tool_args(call, state, self.store) # type: ignore[arg-type] + for call in last_message.tool_calls + ] + return [Send("tools", [tool_call]) for tool_call in tool_calls] return should_continue @@ -794,7 +788,6 @@ def create_react_agent( interrupt_before: Optional[list[str]] = None, interrupt_after: Optional[list[str]] = None, debug: bool = False, - version: Literal["v1", "v2"] = "v2", name: Optional[str] = None, **deprecated_kwargs: Any, ) -> CompiledStateGraph: @@ -906,9 +899,6 @@ def create_react_agent( post_model_hook: An optional node to add after the `agent` node (i.e., the node that calls the LLM). Useful for implementing human-in-the-loop, guardrails, validation, or other post-processing. Post-model hook must be a callable or a runnable that takes in current graph state and returns a state update. - - !!! Note - Only available with `version="v2"`. state_schema: An optional state schema that defines graph state. Must have `messages` and `remaining_steps` keys. Defaults to `AgentState` that defines those two keys. @@ -924,15 +914,6 @@ def create_react_agent( Should be one of the following: "agent", "tools". This is useful if you want to return directly or run additional processing on an output. debug: A flag indicating whether to enable debug mode. - version: Determines the version of the graph to create. - Can be one of: - - - `"v1"`: The tool node processes a single message. All tool - calls in the message are executed in parallel within the tool node. - - `"v2"`: The tool node processes a tool call. - Tool calls are distributed across multiple instances of the tool - node using the [Send](https://langchain-ai.github.io/langgraph/concepts/low_level/#send) - API. name: An optional name for the CompiledStateGraph. This name will be automatically used when adding ReAct agent graph to another graph as a subgraph node - particularly useful for building multi-agent systems. @@ -1000,11 +981,6 @@ def create_react_agent( f"create_react_agent() got unexpected keyword arguments: {deprecated_kwargs}" ) - if version not in ("v1", "v2"): - raise ValueError( - f"Invalid version {version}. Supported versions are 'v1' and 'v2'." - ) - if response_format and not isinstance(response_format, ToolOutput): # Then it's a pydantic model or JSONSchema. We'll automatically convert # it to the tool output strategy as it is widely supported. @@ -1031,7 +1007,6 @@ def create_react_agent( post_model_hook=post_model_hook, state_schema=state_schema, context_schema=context_schema, - version=version, name=name, store=store, ) diff --git a/libs/prebuilt/tests/test_react_agent.py b/libs/prebuilt/tests/test_react_agent.py index 164e68585..d95d43944 100644 --- a/libs/prebuilt/tests/test_react_agent.py +++ b/libs/prebuilt/tests/test_react_agent.py @@ -2,7 +2,6 @@ import dataclasses import inspect from typing import ( Annotated, - Literal, Optional, Union, ) @@ -53,18 +52,14 @@ from tests.model import FakeToolCallingModel pytestmark = pytest.mark.anyio -REACT_TOOL_CALL_VERSIONS = ["v1", "v2"] - -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_no_prompt(sync_checkpointer: BaseCheckpointSaver, version: str) -> None: +def test_no_prompt(sync_checkpointer: BaseCheckpointSaver) -> None: model = FakeToolCallingModel() agent = create_react_agent( model, [], checkpointer=sync_checkpointer, - version=version, ) inputs = [HumanMessage("hi?")] thread = {"configurable": {"thread_id": "123"}} @@ -172,8 +167,7 @@ def test_runnable_prompt(): assert response == expected_response -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_prompt_with_store(version: Literal["v1", "v2"]): +def test_prompt_with_store(): def add(a: int, b: int): """Adds a and b""" return a + b @@ -198,7 +192,6 @@ def test_prompt_with_store(version: Literal["v1", "v2"]): [add], prompt=prompt, store=in_memory_store, - version=version, ) response = agent.invoke( {"messages": [("user", "hi")]}, {"configurable": {"user_id": "1"}} @@ -211,7 +204,6 @@ def test_prompt_with_store(version: Literal["v1", "v2"]): [add], prompt=prompt_no_store, store=in_memory_store, - version=version, ) response = agent.invoke( {"messages": [("user", "hi")]}, {"configurable": {"user_id": "2"}} @@ -262,9 +254,8 @@ async def test_prompt_with_store_async(): @pytest.mark.parametrize("tool_style", ["openai", "anthropic"]) -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) @pytest.mark.parametrize("include_builtin", [True, False]) -def test_model_with_tools(tool_style: str, version: str, include_builtin: bool) -> None: +def test_model_with_tools(tool_style: str, include_builtin: bool) -> None: model = FakeToolCallingModel(tool_style=tool_style) @dec_tool @@ -299,7 +290,6 @@ def test_model_with_tools(tool_style: str, version: str, include_builtin: bool) create_react_agent( model.bind_tools(tools), tools, - version=version, ) @@ -429,8 +419,7 @@ def test__infer_handled_types() -> None: _infer_handled_types(handler) -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_react_agent_with_structured_response(version: str) -> None: +def test_react_agent_with_structured_response() -> None: class WeatherResponse(BaseModel): temperature: float = Field(description="The temperature in fahrenheit") @@ -451,7 +440,6 @@ def test_react_agent_with_structured_response(version: str) -> None: model, [get_weather], response_format=WeatherResponse, - version=version, ) response = agent.invoke({"messages": [HumanMessage("What's the weather?")]}) assert response["structured_response"] == expected_structured_response @@ -484,11 +472,9 @@ class CustomStatePydantic(AgentStatePydantic): user_name: Optional[str] = None -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) @pytest.mark.parametrize("state_schema", [CustomState, CustomStatePydantic]) def test_react_agent_update_state( sync_checkpointer: BaseCheckpointSaver, - version: Literal["v1", "v2"], state_schema: StateSchemaType, ) -> None: @dec_tool @@ -533,7 +519,6 @@ def test_react_agent_update_state( state_schema=state_schema, prompt=prompt, checkpointer=sync_checkpointer, - version=version, ) config = {"configurable": {"thread_id": "1"}} # Run until interrupted @@ -549,9 +534,8 @@ def test_react_agent_update_state( assert tool_message.name == "get_user_name" -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) def test_react_agent_parallel_tool_calls( - sync_checkpointer: BaseCheckpointSaver, version: str + sync_checkpointer: BaseCheckpointSaver, ) -> None: human_assistance_execution_count = 0 @@ -584,7 +568,6 @@ def test_react_agent_parallel_tool_calls( model, [human_assistance, get_weather], checkpointer=sync_checkpointer, - version=version, ) config = {"configurable": {"thread_id": "1"}} query = "Get user assistance and also check the weather" @@ -595,17 +578,11 @@ def test_react_agent_parallel_tool_calls( if messages := event.get("messages"): message_types.append([m.type for m in messages]) - if version == "v1": - assert message_types == [ - ["human"], - ["human", "ai"], - ] - elif version == "v2": - assert message_types == [ - ["human"], - ["human", "ai"], - ["human", "ai", "tool"], - ] + assert message_types == [ + ["human"], + ["human", "ai"], + ["human", "ai", "tool"], + ] # Resume message_types = [] @@ -621,12 +598,8 @@ def test_react_agent_parallel_tool_calls( ["human", "ai", "tool", "tool", "ai"], ] - if version == "v1": - assert human_assistance_execution_count == 1 - assert get_weather_execution_count == 2 - elif version == "v2": - assert human_assistance_execution_count == 1 - assert get_weather_execution_count == 1 + assert human_assistance_execution_count == 1 + assert get_weather_execution_count == 1 class AgentStateExtraKey(AgentState): @@ -637,13 +610,10 @@ 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: Literal["v1", "v2"], state_schema: StateSchemaType -) -> None: +def test_create_react_agent_inject_vars(state_schema: StateSchemaType) -> None: """Test that the agent can inject state and store into tool functions.""" store = InMemoryStore() namespace = ("test",) @@ -682,7 +652,6 @@ def test_create_react_agent_inject_vars( ToolNode([tool1], handle_tool_errors=False), state_schema=state_schema, store=store, - version=version, ) result = agent.invoke({"messages": [{"role": "user", "content": "hi"}], "foo": 2}) assert result["messages"] == [ @@ -694,8 +663,7 @@ def test_create_react_agent_inject_vars( assert result["foo"] == 2 -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -async def test_return_direct(version: str) -> None: +async def test_return_direct() -> None: @dec_tool(return_direct=True) def tool_return_direct(input: str) -> str: """A tool that returns directly.""" @@ -722,7 +690,6 @@ async def test_return_direct(version: str) -> None: agent = create_react_agent( model, [tool_return_direct, tool_normal], - version=version, ) # Test direct return for tool_return_direct @@ -747,9 +714,7 @@ async def test_return_direct(version: str) -> None: ), ] model = FakeToolCallingModel(tool_calls=[second_tool_call, []]) - agent = create_react_agent( - model, [tool_return_direct, tool_normal], version=version - ) + agent = create_react_agent(model, [tool_return_direct, tool_normal]) result = agent.invoke( {"messages": [HumanMessage(content="Test normal", id="hum1")]} ) @@ -778,9 +743,7 @@ async def test_return_direct(version: str) -> None: ), ] model = FakeToolCallingModel(tool_calls=[both_tool_calls, []]) - agent = create_react_agent( - model, [tool_return_direct, tool_normal], version=version - ) + agent = create_react_agent(model, [tool_return_direct, tool_normal]) result = agent.invoke({"messages": [HumanMessage(content="Test both", id="hum2")]}) assert result["messages"] == [ HumanMessage(content="Test both", id="hum2"), @@ -821,9 +784,8 @@ def test_inspect_react() -> None: inspect.getclosurevars(agent.nodes["agent"].bound.func) -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) def test_react_with_subgraph_tools( - sync_checkpointer: BaseCheckpointSaver, version: Literal["v1", "v2"] + sync_checkpointer: BaseCheckpointSaver, ) -> None: class State(TypedDict): a: int @@ -879,7 +841,6 @@ def test_react_with_subgraph_tools( model, tool_node, checkpointer=sync_checkpointer, - version=version, ) result = agent.invoke( {"messages": [HumanMessage(content="What's 2 + 3 and 2 * 3?")]}, @@ -910,8 +871,7 @@ def test_react_with_subgraph_tools( ] -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> None: +def test_react_agent_subgraph_streaming_sync() -> None: """Test React agent streaming when used as a subgraph node sync version""" @dec_tool @@ -931,7 +891,6 @@ def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> No model, tools=[get_weather], prompt="You are a helpful travel assistant.", - version=version, ) # Create a subgraph that uses the React agent as a node @@ -1001,8 +960,7 @@ def test_react_agent_subgraph_streaming_sync(version: Literal["v1", "v2"]) -> No assert msg.content.startswith("The weather of Tokyo is sunny.") -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -async def test_react_agent_subgraph_streaming(version: Literal["v1", "v2"]) -> None: +async def test_react_agent_subgraph_streaming() -> None: """Test React agent streaming when used as a subgraph node.""" @dec_tool @@ -1022,7 +980,6 @@ async def test_react_agent_subgraph_streaming(version: Literal["v1", "v2"]) -> N model, tools=[get_weather], prompt="You are a helpful travel assistant.", - version=version, ) # Create a subgraph that uses the React agent as a node @@ -1094,9 +1051,8 @@ async def test_react_agent_subgraph_streaming(version: Literal["v1", "v2"]) -> N assert msg.content.startswith("The weather of Tokyo is sunny.") -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) def test_tool_node_node_interrupt( - sync_checkpointer: BaseCheckpointSaver, version: str + sync_checkpointer: BaseCheckpointSaver, ) -> None: def tool_normal(some_val: int) -> str: """Tool docstring.""" @@ -1122,7 +1078,6 @@ def test_tool_node_node_interrupt( model, [tool_interrupt, tool_normal], checkpointer=sync_checkpointer, - version=version, ) result = agent.invoke({"messages": [HumanMessage("hi?")]}, config) expected_messages = [ @@ -1147,11 +1102,7 @@ def test_tool_node_node_interrupt( ), _AnyIdToolMessage(content="normal", name="tool_normal", tool_call_id="2"), ] - if version == "v1": - # Interrupt blocks second tool result - assert result["messages"] == expected_messages[:-1] - elif version == "v2": - assert result["messages"] == expected_messages + assert result["messages"] == expected_messages state = agent.get_state(config) assert state.next == ("tools",) @@ -1165,8 +1116,7 @@ def test_tool_node_node_interrupt( ) -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_basic(version: str) -> None: +def test_dynamic_model_basic() -> None: """Test basic dynamic model functionality.""" def dynamic_model(state, runtime: Runtime): @@ -1176,7 +1126,7 @@ def test_dynamic_model_basic(version: str) -> None: else: return FakeToolCallingModel(tool_calls=[]) - agent = create_react_agent(dynamic_model, [], version=version) + agent = create_react_agent(dynamic_model, []) result = agent.invoke({"messages": [HumanMessage("hello")]}) assert len(result["messages"]) == 2 @@ -1187,8 +1137,7 @@ def test_dynamic_model_basic(version: str) -> None: assert result["messages"][-1].content == "urgent help" -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_with_tools(version: Literal["v1", "v2"]) -> None: +def test_dynamic_model_with_tools() -> None: """Test dynamic model with tool calling.""" @dec_tool @@ -1215,9 +1164,7 @@ def test_dynamic_model_with_tools(version: Literal["v1", "v2"]) -> None: tool_calls=[[{"args": {"x": 1}, "id": "1", "name": "basic_tool"}], []] ) - agent = create_react_agent( - dynamic_model, [basic_tool, advanced_tool], version=version - ) + agent = create_react_agent(dynamic_model, [basic_tool, advanced_tool]) # Test basic tool usage result = agent.invoke({"messages": [HumanMessage("basic request")]}) @@ -1239,8 +1186,7 @@ class Context: user_id: str -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_with_context(version: str) -> None: +def test_dynamic_model_with_context() -> None: """Test dynamic model using config parameters.""" def dynamic_model(state, runtime: Runtime[Context]): @@ -1251,9 +1197,7 @@ def test_dynamic_model_with_context(version: str) -> None: else: return FakeToolCallingModel(tool_calls=[]) - agent = create_react_agent( - dynamic_model, [], context_schema=Context, version=version - ) + agent = create_react_agent(dynamic_model, [], context_schema=Context) # Test with basic user result = agent.invoke( @@ -1270,8 +1214,7 @@ def test_dynamic_model_with_context(version: str) -> None: assert len(result["messages"]) == 2 -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_with_state_schema(version: Literal["v1", "v2"]) -> None: +def test_dynamic_model_with_state_schema() -> None: """Test dynamic model with custom state schema.""" class CustomDynamicState(AgentState): @@ -1284,9 +1227,7 @@ def test_dynamic_model_with_state_schema(version: Literal["v1", "v2"]) -> None: else: return FakeToolCallingModel(tool_calls=[]) - agent = create_react_agent( - dynamic_model, [], state_schema=CustomDynamicState, version=version - ) + agent = create_react_agent(dynamic_model, [], state_schema=CustomDynamicState) result = agent.invoke( {"messages": [HumanMessage("hello")], "model_preference": "advanced"} @@ -1295,15 +1236,14 @@ def test_dynamic_model_with_state_schema(version: Literal["v1", "v2"]) -> None: assert result["model_preference"] == "advanced" -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_with_prompt(version: Literal["v1", "v2"]) -> None: +def test_dynamic_model_with_prompt() -> None: """Test dynamic model with different prompt types.""" def dynamic_model(state: AgentState, runtime: Runtime) -> BaseChatModel: return FakeToolCallingModel(tool_calls=[]) # Test with string prompt - agent = create_react_agent(dynamic_model, [], prompt="system_msg", version=version) + agent = create_react_agent(dynamic_model, [], prompt="system_msg") result = agent.invoke({"messages": [HumanMessage("human_msg")]}) assert result["messages"][-1].content == "system_msg-human_msg" @@ -1312,9 +1252,7 @@ def test_dynamic_model_with_prompt(version: Literal["v1", "v2"]) -> None: """Generate a dynamic system message based on state.""" return [{"role": "system", "content": "system_msg"}] + list(state["messages"]) - agent = create_react_agent( - dynamic_model, [], prompt=dynamic_prompt, version=version - ) + agent = create_react_agent(dynamic_model, [], prompt=dynamic_prompt) result = agent.invoke({"messages": [HumanMessage("human_msg")]}) assert result["messages"][-1].content == "system_msg-human_msg" @@ -1332,8 +1270,7 @@ async def test_dynamic_model_async() -> None: assert result["messages"][-1].content == "hello async" -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_with_structured_response(version: str) -> None: +def test_dynamic_model_with_structured_response() -> None: """Test dynamic model with structured response format.""" class TestResponse(BaseModel): @@ -1354,9 +1291,7 @@ def test_dynamic_model_with_structured_response(version: str) -> None: ], ) - agent = create_react_agent( - dynamic_model, [], response_format=TestResponse, version=version - ) + agent = create_react_agent(dynamic_model, [], response_format=TestResponse) result = agent.invoke({"messages": [HumanMessage("hello")]}) assert "structured_response" in result @@ -1395,8 +1330,7 @@ def test_dynamic_model_with_checkpointer(sync_checkpointer): assert call_count >= 2 -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_state_dependent_tools(version: Literal["v1", "v2"]) -> None: +def test_dynamic_model_state_dependent_tools() -> None: """Test dynamic model that changes available tools based on state.""" @dec_tool @@ -1420,7 +1354,7 @@ def test_dynamic_model_state_dependent_tools(version: Literal["v1", "v2"]) -> No tool_calls=[[{"args": {"x": 1}, "id": "1", "name": "tool_a"}], []] ) - agent = create_react_agent(dynamic_model, [tool_a, tool_b], version=version) + agent = create_react_agent(dynamic_model, [tool_a, tool_b]) # Ask to use tool B result = agent.invoke({"messages": [HumanMessage("use_b please")]}) @@ -1435,8 +1369,7 @@ def test_dynamic_model_state_dependent_tools(version: Literal["v1", "v2"]) -> No assert last_message.content == "A: 1" -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_dynamic_model_error_handling(version: Literal["v1", "v2"]) -> None: +def test_dynamic_model_error_handling() -> None: """Test error handling in dynamic model.""" def failing_dynamic_model(state, runtime: Runtime): @@ -1444,7 +1377,7 @@ def test_dynamic_model_error_handling(version: Literal["v1", "v2"]) -> None: raise ValueError("Dynamic model failed") return FakeToolCallingModel(tool_calls=[]) - agent = create_react_agent(failing_dynamic_model, [], version=version) + agent = create_react_agent(failing_dynamic_model, []) # Normal operation should work result = agent.invoke({"messages": [HumanMessage("hello")]}) @@ -1788,8 +1721,7 @@ def test_create_react_agent_inject_vars_with_post_model_hook( assert result["foo"] == 2 -@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS) -def test_response_format_using_tool_choice(version: Literal["v1", "v2"]) -> None: +def test_response_format_using_tool_choice() -> None: """Test response format using tool choice.""" class WeatherResponse(BaseModel): @@ -1810,7 +1742,6 @@ def test_response_format_using_tool_choice(version: Literal["v1", "v2"]) -> None model, [get_weather], response_format=WeatherResponse, - version=version, ) response = agent.invoke( {