diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index 72c0d37e2..24622a4a7 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -1,4 +1,5 @@ import asyncio +import json from copy import copy from typing import ( Any, @@ -28,6 +29,16 @@ INVALID_TOOL_NAME_ERROR_TEMPLATE = ( TOOL_CALL_ERROR_TEMPLATE = "Error: {error}\n Please fix your mistakes." +def str_output(output: Any) -> str: + if isinstance(output, str): + return output + else: + try: + return json.dumps(output) + except Exception: + return str(output) + + class ToolNode(RunnableCallable): """A node that runs the tools called in the last AIMessage. @@ -94,7 +105,12 @@ class ToolNode(RunnableCallable): try: input = {**call, **{"type": "tool_call"}} - return self.tools_by_name[call["name"]].invoke(input, config) + tool_message: ToolMessage = self.tools_by_name[call["name"]].invoke( + input, config + ) + # TODO: handle this properly in core + tool_message.content = str_output(tool_message.content) + return tool_message except Exception as e: if not self.handle_tool_errors: raise e @@ -106,7 +122,12 @@ class ToolNode(RunnableCallable): return invalid_tool_message try: input = {**call, **{"type": "tool_call"}} - return await self.tools_by_name[call["name"]].ainvoke(input, config) + tool_message: ToolMessage = await self.tools_by_name[call["name"]].ainvoke( + input, config + ) + # TODO: handle this properly in core + tool_message.content = str_output(tool_message.content) + return tool_message except Exception as e: if not self.handle_tool_errors: raise e diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index e21766cd0..f08e9e7cc 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -267,6 +267,13 @@ async def test_tool_node(): raise ValueError("Test error") return f"tool2: {some_val} - {some_other_val}" + async def tool3(some_val: int, some_other_val: str) -> str: + """Tool 3 docstring.""" + return [ + {"key_1": some_val, "key_2": "foo"}, + {"key_1": some_other_val, "key_2": "baz"}, + ] + result = ToolNode([tool1]).invoke( { "messages": [ @@ -377,6 +384,31 @@ async def test_tool_node(): ) assert tool_message.tool_call_id == "some 0" + # list of dicts tool content + result3 = await ToolNode([tool3]).ainvoke( + { + "messages": [ + AIMessage( + "hi?", + tool_calls=[ + { + "name": "tool3", + "args": {"some_val": 2, "some_other_val": "bar"}, + "id": "some 0", + } + ], + ) + ] + } + ) + tool_message: ToolMessage = result3["messages"][-1] + assert tool_message.type == "tool" + assert ( + tool_message.content + == '[{"key_1": 2, "key_2": "foo"}, {"key_1": "bar", "key_2": "baz"}]' + ) + 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}"