langgraph[patch]: ToolNode support for tools outputting msg (#977)

ToolNode passes ToolCall to tools directly and outputs the generated ToolMessages directly
This commit is contained in:
Bagatur
2024-07-15 16:40:37 -07:00
committed by GitHub
parent 5444563fdc
commit 2a1cdd31c5
5 changed files with 27 additions and 36 deletions
+12 -26
View File
@@ -1,5 +1,4 @@
import asyncio
import json
from typing import Any, Callable, Dict, Literal, Optional, Sequence, Union
from langchain_core.messages import AIMessage, AnyMessage, ToolCall, ToolMessage
@@ -11,20 +10,11 @@ from langchain_core.tools import tool as create_tool
from langgraph.utils import RunnableCallable
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 requested in the last AIMessage. It can be used
either in StateGraph with a "messages" key or in MessageGraph. If multiple
tool calls are requested, they will be run in parallel. The output will be
"""A node that runs the tools called in the last AIMessage.
It can be used either in StateGraph with a "messages" key or in MessageGraph. If
multiple tool calls are requested, they will be run in parallel. The output will be
a list of ToolMessages, one for each tool call.
The `ToolNode` is roughly analogous to:
@@ -79,14 +69,13 @@ class ToolNode(RunnableCallable):
def run_one(call: ToolCall):
try:
output = self.tools_by_name[call["name"]].invoke(call["args"], config)
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
output = f"Error: {repr(e)}\n Please fix your mistakes."
return ToolMessage(
content=str_output(output), name=call["name"], tool_call_id=call["id"]
)
content = f"Error: {repr(e)}\n Please fix your mistakes."
return ToolMessage(content, name=call["name"], tool_call_id=call["id"])
with get_executor_for_config(config) as executor:
outputs = [*executor.map(run_one, message.tool_calls)]
@@ -112,16 +101,13 @@ class ToolNode(RunnableCallable):
async def run_one(call: ToolCall):
try:
output = await self.tools_by_name[call["name"]].ainvoke(
call["args"], config
)
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
output = f"Error: {repr(e)}\n Please fix your mistakes."
return ToolMessage(
content=str_output(output), name=call["name"], tool_call_id=call["id"]
)
content = f"Error: {repr(e)}\n Please fix your mistakes."
return ToolMessage(content, name=call["name"], tool_call_id=call["id"])
outputs = await asyncio.gather(*(run_one(call) for call in message.tool_calls))
if output_type == "list":
+4 -4
View File
@@ -1746,13 +1746,13 @@ langchain-core = ">=0.2.2rc1,<0.3"
[[package]]
name = "langchain-core"
version = "0.2.16"
version = "0.2.19"
description = "Building applications with LLMs through composability"
optional = false
python-versions = "<4.0,>=3.8.1"
files = [
{file = "langchain_core-0.2.16-py3-none-any.whl", hash = "sha256:68dd6fbdd8d704e124ee14c39d0b0dcf38a47bb32234328b63ad2e98385b1c80"},
{file = "langchain_core-0.2.16.tar.gz", hash = "sha256:cc03083e059bff86ca3e6ba60097f91d7765a929edd04eb226bb20c208447caf"},
{file = "langchain_core-0.2.19-py3-none-any.whl", hash = "sha256:5b3cd34395be274c89e822c84f0e03c4da14168c177a83921c5b9414ac7a0651"},
{file = "langchain_core-0.2.19.tar.gz", hash = "sha256:13043a83e5c9ab58b9f5ce2a56896e7e88b752e8891b2958960a98e71801471e"},
]
[package.dependencies]
@@ -4130,4 +4130,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
[metadata]
lock-version = "2.0"
python-versions = ">=3.9.0,<4.0"
content-hash = "19250230952cb11ee6b5c820ae0a2b589ce520a59e0723204dcee0c46c3b739e"
content-hash = "170eaa0e542a02d5f2fb0d42d1f04c5d010bd3735a44b927b28e6b742c689eb0"
+2 -2
View File
@@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph"
[tool.poetry.dependencies]
python = ">=3.9.0,<4.0"
langchain-core = ">=0.2.15,<0.3"
langchain-core = ">=0.2.19,<0.3"
[tool.poetry.group.dev.dependencies]
@@ -61,7 +61,7 @@ omit = ["tests/*"]
[tool.pytest-watcher]
now = true
delay = 0.1
runner_args = ["-x", "--ff", "-vv", "--snapshot-update"]
runner_args = ["--ff", "-vv", "--snapshot-update"]
patterns = ["*.py"]
[build-system]
File diff suppressed because one or more lines are too long
+7 -2
View File
@@ -3143,6 +3143,7 @@ Some examples of past conversations:
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),
@@ -3154,7 +3155,7 @@ Some examples of past conversations:
),
AIMessage(content="answer", id=AnyStr()),
]
assert app.invoke(
actual = app.invoke(
{"messages": "what is weather in sf"},
{
"configurable": {
@@ -3162,7 +3163,9 @@ Some examples of past conversations:
"expected_examples": [],
},
},
) == {"messages": first_messages}
)
expected = {"messages": first_messages}
assert actual == expected
# get first checkpoint
chkpnt_tuple_1 = saver.get_tuple({"configurable": {"thread_id": "1"}})
@@ -3193,6 +3196,7 @@ Some examples of past conversations:
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),
@@ -3252,6 +3256,7 @@ Some examples of past conversations:
"name": "search_api",
"args": {"query": "query"},
"id": "tool_call123",
"type": "tool_call",
}
],
),