langgraph: relax constraints in ToolNode Command validation (#2778)

This commit is contained in:
Vadym Barda
2024-12-16 14:00:45 -05:00
committed by GitHub
parent d2794eda0a
commit d9b7aaa5cc
2 changed files with 54 additions and 80 deletions
+8 -16
View File
@@ -548,33 +548,25 @@ class ToolNode(RunnableCallable):
# convert to message objects if updates are in a dict format
messages_update = convert_to_messages(messages_update)
have_seen_tool_messages = False
has_matching_tool_message = False
for message in messages_update:
if not isinstance(message, ToolMessage):
continue
if have_seen_tool_messages:
raise ValueError(
f"Expected at most one ToolMessage in Command.update for tool '{call['name']}', got multiple: {messages_update}."
)
if message.tool_call_id == call["id"]:
message.name = call["name"]
has_matching_tool_message = True
if message.tool_call_id != call["id"]:
raise ValueError(
f"ToolMessage.tool_call_id must match the tool call id. Expected: {call['id']}, got: {message.tool_call_id} for tool '{call['name']}'."
)
message.name = call["name"]
have_seen_tool_messages = True
# validate that we always have exactly one ToolMessage in Command.update if command is sent to the CURRENT graph
if updated_command.graph is None and not have_seen_tool_messages:
# validate that we always have a ToolMessage matching the tool call in
# Command.update if command is sent to the CURRENT graph
if updated_command.graph is None and not has_matching_tool_message:
example_update = (
'`Command(update={"messages": [ToolMessage("Success", tool_call_id=tool_call_id), ...]}, ...)`'
if input_type == "dict"
else '`Command(update=[ToolMessage("Success", tool_call_id=tool_call_id), ...], ...)`'
)
raise ValueError(
f"Expected exactly one message (ToolMessage) in Command.update for tool '{call['name']}', got: {messages_update}. "
f"Expected to have a matching ToolMessage in Command.update for tool '{call['name']}', got: {messages_update}. "
"Every tool call (LLM requesting to call a tool) in the message history MUST have a corresponding ToolMessage. "
f"You can fix it by modifying the tool to return {example_update}."
)
+46 -64
View File
@@ -1249,6 +1249,33 @@ async def test_tool_node_command():
}
)
# test validation (tool message with a wrong tool call ID)
with pytest.raises(ValueError):
@dec_tool
def mismatching_tool_call_id_tool():
"""My tool"""
return Command(
update={"messages": [ToolMessage(content="foo", tool_call_id="2")]}
)
ToolNode([mismatching_tool_call_id_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{
"args": {},
"id": "1",
"name": "mismatching_tool_call_id_tool",
}
],
)
]
}
)
# test validation (missing tool message in the update for parent graph is OK)
@dec_tool
def node_update_parent_tool():
@@ -1268,40 +1295,6 @@ async def test_tool_node_command():
}
) == [Command(update={"messages": []}, graph=Command.PARENT)]
# test validation (multiple tool messages)
with pytest.raises(ValueError):
for graph in (None, Command.PARENT):
@dec_tool
def multiple_tool_messages_tool():
"""My tool"""
return Command(
update={
"messages": [
ToolMessage(content="foo", tool_call_id=""),
ToolMessage(content="bar", tool_call_id=""),
]
},
graph=graph,
)
ToolNode([multiple_tool_messages_tool]).invoke(
{
"messages": [
AIMessage(
"",
tool_calls=[
{
"args": {},
"id": "1",
"name": "multiple_tool_messages_tool",
}
],
)
]
}
)
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,
@@ -1524,6 +1517,25 @@ async def test_tool_node_command_list_input():
]
)
# test validation (tool message with a wrong tool call ID)
with pytest.raises(ValueError):
@dec_tool
def mismatching_tool_call_id_tool():
"""My tool"""
return Command(update=[ToolMessage(content="foo", tool_call_id="2")])
ToolNode([mismatching_tool_call_id_tool]).invoke(
[
AIMessage(
"",
tool_calls=[
{"args": {}, "id": "1", "name": "mismatching_tool_call_id_tool"}
],
)
]
)
# test validation (missing tool message in the update for parent graph is OK)
@dec_tool
def node_update_parent_tool():
@@ -1539,36 +1551,6 @@ async def test_tool_node_command_list_input():
]
) == [Command(update=[], graph=Command.PARENT)]
# test validation (multiple tool messages)
with pytest.raises(ValueError):
for graph in (None, Command.PARENT):
@dec_tool
def multiple_tool_messages_tool():
"""My tool"""
return Command(
update=[
ToolMessage(content="foo", tool_call_id=""),
ToolMessage(content="bar", tool_call_id=""),
],
graph=graph,
)
ToolNode([multiple_tool_messages_tool]).invoke(
[
AIMessage(
"",
tool_calls=[
{
"args": {},
"id": "1",
"name": "multiple_tool_messages_tool",
}
],
)
]
)
@pytest.mark.skipif(
not IS_LANGCHAIN_CORE_030_OR_GREATER,