From 83089c3a7fc42d15200d9606a9b49882c6469cac Mon Sep 17 00:00:00 2001 From: Vadym Barda Date: Wed, 17 Jul 2024 21:33:46 -0400 Subject: [PATCH] langgraph: add incorrect tool name handling to ToolNode (#1052) --- .../langgraph/langgraph/prebuilt/tool_node.py | 27 +++++++++++++++++-- libs/langgraph/tests/test_prebuilt.py | 25 +++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index 0ed69a1e5..1aec005fb 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -9,6 +9,11 @@ from langchain_core.tools import tool as create_tool from langgraph.utils import RunnableCallable +INVALID_TOOL_NAME_ERROR_TEMPLATE = ( + "Error: {requested_tool} is not a valid tool, try one of [{available_tools}]." +) +TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes." + class ToolNode(RunnableCallable): """A node that runs the tools called in the last AIMessage. @@ -68,13 +73,22 @@ class ToolNode(RunnableCallable): raise ValueError("Last message is not an AIMessage") def run_one(call: ToolCall): + if (requested_tool := call["name"]) not in self.tools_by_name: + content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( + requested_tool=requested_tool, + available_tools=", ".join(self.tools_by_name.keys()), + ) + return ToolMessage( + content, name=requested_tool, tool_call_id=call["id"] + ) + try: input = {**call, **{"type": "tool_call"}} return self.tools_by_name[call["name"]].invoke(input, config) except Exception as e: if not self.handle_tool_errors: raise e - content = f"Error: {repr(e)}\n Please fix your mistakes." + content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) with get_executor_for_config(config) as executor: @@ -100,13 +114,22 @@ class ToolNode(RunnableCallable): raise ValueError("Last message is not an AIMessage") async def run_one(call: ToolCall): + if (requested_tool := call["name"]) not in self.tools_by_name: + content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( + requested_tool=requested_tool, + available_tools=", ".join(self.tools_by_name.keys()), + ) + return ToolMessage( + content, name=requested_tool, tool_call_id=call["id"] + ) + try: input = {**call, **{"type": "tool_call"}} return await self.tools_by_name[call["name"]].ainvoke(input, config) except Exception as e: if not self.handle_tool_errors: raise e - content = f"Error: {repr(e)}\n Please fix your mistakes." + content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) outputs = await asyncio.gather(*(run_one(call) for call in message.tool_calls)) diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 67caddfe7..264f30db6 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -253,6 +253,31 @@ async def test_tool_node(): } ) + # incorrect tool name + result_incorrect_name = ToolNode([tool1, tool2]).invoke( + { + "messages": [ + AIMessage( + "hi?", + tool_calls=[ + { + "name": "tool3", + "args": {"some_val": 1, "some_other_val": "foo"}, + "id": "some 0", + } + ], + ) + ] + } + ) + tool_message: ToolMessage = result_incorrect_name["messages"][-1] + assert tool_message.type == "tool" + assert ( + tool_message.content + == "Error: tool3 is not a valid tool, try one of [tool1, tool2]." + ) + assert tool_message.tool_call_id == "some 0" + def my_function(some_val: int, some_other_val: str) -> str: return f"{some_val} - {some_other_val}"