From e6d71a586d6f7cb52972a6b7beca86808c5c95f0 Mon Sep 17 00:00:00 2001 From: Eugene Yurtsev Date: Wed, 13 Aug 2025 22:32:21 -0400 Subject: [PATCH] chore(prebuilt): remove structured tool support from ToolNode (#5902) Remove structured tool support from ToolNode We'll handle structured tools directly in the call_model nodes. --- libs/prebuilt/langgraph/prebuilt/tool_node.py | 83 ++--------------- libs/prebuilt/tests/test_tool_node.py | 91 ------------------- 2 files changed, 10 insertions(+), 164 deletions(-) diff --git a/libs/prebuilt/langgraph/prebuilt/tool_node.py b/libs/prebuilt/langgraph/prebuilt/tool_node.py index a8f5def53..34e3e5356 100644 --- a/libs/prebuilt/langgraph/prebuilt/tool_node.py +++ b/libs/prebuilt/langgraph/prebuilt/tool_node.py @@ -257,12 +257,6 @@ class ToolNode(RunnableCallable): - Bypasses message parsing for direct tool execution - For programmatic tool invocation and testing - Tool Types: - 1. **Regular tools**: Functions or BaseTool instances that return values or - Commands. - 2. **Structured output tools**: Pydantic model classes for schema-validated - responses - Output Formats: Output format depends on input type and tool behavior: @@ -274,15 +268,10 @@ class ToolNode(RunnableCallable): - Returns ``[Command(...)]`` or mixed list with regular tool outputs - Commands can update state, trigger navigation, or send messages - **For Structured output tools**: - - Returns ``[Command(update={"messages": [...], "structured_response": schema_instance})]`` - - Includes both message and structured data in the graph state - Args: tools: A sequence of tools that can be invoked by this node. Supports: - **BaseTool instances**: Tools with schemas and metadata - **Plain functions**: Automatically converted to tools with inferred schemas - - **Pydantic model classes**: Treated as structured output tools name: The name identifier for this node in the graph. Used for debugging and visualization. Defaults to "tools". tags: Optional metadata tags to associate with the node for filtering @@ -349,7 +338,7 @@ class ToolNode(RunnableCallable): def __init__( self, - tools: Sequence[Union[BaseTool, BaseModel, Callable]], + tools: Sequence[Union[BaseTool, Callable]], *, name: str = "tools", tags: Optional[list[str]] = None, @@ -369,36 +358,24 @@ class ToolNode(RunnableCallable): """ super().__init__(self._func, self._afunc, name=name, tags=tags, trace=False) self._tools_by_name: dict[str, BaseTool] = {} - self._structured_output_tools_by_name: dict[str, type[BaseModel]] = {} self._tool_to_state_args: dict[str, dict[str, Optional[str]]] = {} self._tool_to_store_arg: dict[str, Optional[str]] = {} self._handle_tool_errors = handle_tool_errors self._messages_key = messages_key for tool in tools: - if inspect.isclass(tool) and issubclass(tool, BaseModel): - # Handle Pydantic model classes as structured output tools - self._structured_output_tools_by_name[tool.__name__] = tool - self._tool_to_state_args[tool.__name__] = {} - self._tool_to_store_arg[tool.__name__] = None + if not isinstance(tool, BaseTool): + tool_ = create_tool(cast(Type[BaseTool], tool)) else: - if not isinstance(tool, BaseTool): - tool_ = create_tool(cast(Type[BaseTool], tool)) - else: - tool_ = tool - self._tools_by_name[tool_.name] = tool_ - self._tool_to_state_args[tool_.name] = _get_state_args(tool_) - self._tool_to_store_arg[tool_.name] = _get_store_arg(tool_) + tool_ = tool + self._tools_by_name[tool_.name] = tool_ + self._tool_to_state_args[tool_.name] = _get_state_args(tool_) + self._tool_to_store_arg[tool_.name] = _get_store_arg(tool_) @property def tools_by_name(self) -> dict[str, BaseTool]: """Mapping from tool name to BaseTool instance.""" return self._tools_by_name - @property - def structured_output_tools(self) -> dict[str, type[BaseModel]]: - """Mapping from structured output tool name to Pydantic model class.""" - return self._structured_output_tools_by_name - def _func( self, input: Union[ @@ -492,22 +469,6 @@ class ToolNode(RunnableCallable): if invalid_tool_message := self._validate_tool_call(call): return invalid_tool_message - # Handle structured output tools - if call["name"] in self.structured_output_tools: - response_schema = self._structured_output_tools_by_name[call["name"]] - return Command( - update={ - "messages": [ - ToolMessage( - content="ok!", - name=call["name"], - tool_call_id=call["id"], - ) - ], - "structured_response": response_schema(**call["args"]), - } - ) - try: call_args = {**call, **{"type": "tool_call"}} tool = self.tools_by_name[call["name"]] @@ -566,22 +527,6 @@ class ToolNode(RunnableCallable): if invalid_tool_message := self._validate_tool_call(call): return invalid_tool_message - # Handle structured output tools - if call["name"] in self.structured_output_tools: - response_schema = self._structured_output_tools_by_name[call["name"]] - return Command( - update={ - "messages": [ - ToolMessage( - content="ok!", - name=call["name"], - tool_call_id=call["id"], - ) - ], - "structured_response": response_schema(**call["args"]), - } - ) - try: call_args = {**call, **{"type": "tool_call"}} tool = self.tools_by_name[call["name"]] @@ -673,13 +618,8 @@ class ToolNode(RunnableCallable): def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]: requested_tool = call["name"] - if ( - requested_tool not in self.tools_by_name - and requested_tool not in self._structured_output_tools_by_name - ): - all_tool_names = list(self.tools_by_name.keys()) + list( - self._structured_output_tools_by_name.keys() - ) + if requested_tool not in self.tools_by_name: + all_tool_names = list(self.tools_by_name.keys()) content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( requested_tool=requested_tool, available_tools=", ".join(all_tool_names), @@ -797,10 +737,7 @@ class ToolNode(RunnableCallable): The injection is performed on a copy of the tool call to avoid mutating the original. """ - if ( - tool_call["name"] not in self.tools_by_name - and tool_call["name"] not in self._structured_output_tools_by_name - ): + if tool_call["name"] not in self.tools_by_name: return tool_call tool_call_copy: ToolCall = copy(tool_call) diff --git a/libs/prebuilt/tests/test_tool_node.py b/libs/prebuilt/tests/test_tool_node.py index 3329f981d..b67b684d9 100644 --- a/libs/prebuilt/tests/test_tool_node.py +++ b/libs/prebuilt/tests/test_tool_node.py @@ -1482,94 +1482,3 @@ def test_tool_node_stream_writer() -> None: }, ), ] - - -def test_structured_output_tools_sync() -> None: - """Test that ToolNode handles Pydantic model classes as structured output tools.""" - - class OutputSchema(BaseModel): - name: str - age: int - location: str - - tool_node = ToolNode([OutputSchema]) - - # Test that the structured output tool is registered correctly - assert "OutputSchema" in tool_node.structured_output_tools - - # Create a tool call that matches the schema - tool_call = { - "name": "OutputSchema", - "args": {"name": "Alice", "age": 30, "location": "NYC"}, - "id": "call_123", - "type": "tool_call", - } - - # Test sync execution - result = tool_node.invoke( - {"messages": [AIMessage(content="", tool_calls=[tool_call])]} - ) - - # Should return a Command with structured response - assert isinstance(result, list) - assert len(result) == 1 - command = result[0] - assert isinstance(command, Command) - - # Check the update structure - assert "messages" in command.update - assert "structured_response" in command.update - - # Check the tool message - tool_message = command.update["messages"][0] - assert isinstance(tool_message, ToolMessage) - assert tool_message.name == "OutputSchema" - assert tool_message.tool_call_id == "call_123" - - # Check the structured response - structured_response = command.update["structured_response"] - assert isinstance(structured_response, OutputSchema) - assert structured_response.name == "Alice" - assert structured_response.age == 30 - assert structured_response.location == "NYC" - - -async def test_structured_output_tools_async() -> None: - """Test that ToolNode handles Pydantic model classes as structured output tools.""" - - class OutputSchema(BaseModel): - name: str - age: int - location: str - - tool_node = ToolNode([OutputSchema]) - - # Test that the structured output tool is registered correctly - assert "OutputSchema" not in tool_node.tools_by_name - assert "OutputSchema" in tool_node.structured_output_tools - - # Create a tool call that matches the schema - tool_call = { - "name": "OutputSchema", - "args": {"name": "Alice", "age": 30, "location": "NYC"}, - "id": "call_123", - "type": "tool_call", - } - - # Test async execution - result_async = await tool_node.ainvoke( - {"messages": [AIMessage(content="", tool_calls=[tool_call])]} - ) - - # Should produce the same result - assert isinstance(result_async, list) - assert len(result_async) == 1 - command_async = result_async[0] - assert isinstance(command_async, Command) - assert "structured_response" in command_async.update - - structured_response_async = command_async.update["structured_response"] - assert isinstance(structured_response_async, OutputSchema) - assert structured_response_async.name == "Alice" - assert structured_response_async.age == 30 - assert structured_response_async.location == "NYC"