mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-22 01:25:06 +02:00
chore: port tool node improvements back to langgraph (#6321)
namespace decisions ``` langgraph.prebuilt ├── ToolRuntime # new # all of the other stuff that was already there langgraph.prebuilt.tool_node ├── ToolNode ├── ToolCallRequest # new ├── ToolRuntime # new ├── InjectedState ├── InjectedStore ├── ToolCallWrapper ├── AsyncToolCallWrapper ├── tools_condition ``` ``` langchain.tools ├── ToolRuntime # now from langgraph.prebuilt ├── InjectedState # now from langgraph.prebuilt ├── InjectedStore # now from langgraph.prebuilt ├── ToolException ├── tool ├── BaseTool ├── InjectedToolArg ├── InjectedToolCallId ```
This commit is contained in:
@@ -8,6 +8,7 @@ from typing import (
|
||||
Literal,
|
||||
TypeVar,
|
||||
)
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
from langchain_core.language_models import BaseChatModel
|
||||
@@ -64,6 +65,29 @@ pytestmark = pytest.mark.anyio
|
||||
REACT_TOOL_CALL_VERSIONS = ["v1", "v2"]
|
||||
|
||||
|
||||
def _create_mock_runtime(store: BaseStore | None = None) -> Mock:
|
||||
"""Create a mock Runtime object for testing ToolNode outside of graph context.
|
||||
|
||||
This helper is needed because ToolNode._func expects a Runtime parameter
|
||||
which is injected by RunnableCallable from config["configurable"]["__pregel_runtime"].
|
||||
When testing ToolNode directly (outside a graph), we need to provide this manually.
|
||||
"""
|
||||
mock_runtime = Mock()
|
||||
mock_runtime.store = store
|
||||
mock_runtime.context = None
|
||||
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||
return mock_runtime
|
||||
|
||||
|
||||
def _create_config_with_runtime(store: BaseStore | None = None) -> RunnableConfig:
|
||||
"""Create a RunnableConfig with mock Runtime for testing ToolNode.
|
||||
|
||||
Returns:
|
||||
RunnableConfig with __pregel_runtime in configurable dict.
|
||||
"""
|
||||
return {"configurable": {"__pregel_runtime": _create_mock_runtime(store)}}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", REACT_TOOL_CALL_VERSIONS)
|
||||
def test_no_prompt(sync_checkpointer: BaseCheckpointSaver, version: str) -> None:
|
||||
model = FakeToolCallingModel()
|
||||
@@ -327,7 +351,8 @@ def test_model_with_tools(tool_style: str, version: str, include_builtin: bool):
|
||||
],
|
||||
)
|
||||
]
|
||||
}
|
||||
},
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_messages: ToolMessage = result["messages"][-2:]
|
||||
for tool_message in tool_messages:
|
||||
@@ -728,37 +753,13 @@ def test_tool_node_inject_state(schema_: type[T]) -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": "bar"}))
|
||||
result = node.invoke(
|
||||
schema_(**{"messages": [msg], "foo": "bar"}),
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "bar", f"Failed for tool={tool_name}"
|
||||
|
||||
if tool_name == "tool3":
|
||||
failure_input = None
|
||||
try:
|
||||
failure_input = schema_(**{"messages": [msg], "notfoo": "bar"})
|
||||
except Exception:
|
||||
pass
|
||||
if failure_input is not None:
|
||||
with pytest.raises(KeyError):
|
||||
node.invoke(failure_input)
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
node.invoke([msg])
|
||||
else:
|
||||
failure_input = None
|
||||
try:
|
||||
failure_input = schema_(**{"messages": [msg], "notfoo": "bar"})
|
||||
except Exception:
|
||||
# We'd get a validation error from pydantic state and wouldn't make it to the node
|
||||
# anyway
|
||||
pass
|
||||
if failure_input is not None:
|
||||
messages_ = node.invoke(failure_input)
|
||||
tool_message = messages_["messages"][-1]
|
||||
assert "KeyError" in tool_message.content
|
||||
tool_message = node.invoke([msg])[-1]
|
||||
assert "KeyError" in tool_message.content
|
||||
|
||||
tool_call = {
|
||||
"name": "tool4",
|
||||
"args": {"some_val": 1},
|
||||
@@ -766,11 +767,13 @@ def test_tool_node_inject_state(schema_: type[T]) -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
result = node.invoke(schema_(**{"messages": [msg], "foo": ""}))
|
||||
result = node.invoke(
|
||||
schema_(**{"messages": [msg], "foo": ""}), config=_create_config_with_runtime()
|
||||
)
|
||||
tool_message = result["messages"][-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
result = node.invoke([msg])
|
||||
result = node.invoke([msg], config=_create_config_with_runtime())
|
||||
tool_message = result[-1]
|
||||
assert tool_message.content == "hi?"
|
||||
|
||||
@@ -882,7 +885,9 @@ def test_tool_node_inject_store() -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke({"messages": [msg]}, store=store)
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg]}, config=_create_config_with_runtime(store=store)
|
||||
)
|
||||
graph_result = graph.invoke({"messages": [msg]})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
@@ -898,7 +903,10 @@ def test_tool_node_inject_store() -> None:
|
||||
"type": "tool_call",
|
||||
}
|
||||
msg = AIMessage("hi?", tool_calls=[tool_call])
|
||||
node_result = node.invoke({"messages": [msg], "bar": "baz"}, store=store)
|
||||
node_result = node.invoke(
|
||||
{"messages": [msg], "bar": "baz"},
|
||||
config=_create_config_with_runtime(store=store),
|
||||
)
|
||||
graph_result = graph.invoke({"messages": [msg], "bar": "baz"})
|
||||
for result in (node_result, graph_result):
|
||||
result["messages"][-1]
|
||||
@@ -923,7 +931,8 @@ def test_tool_node_ensure_utf8() -> None:
|
||||
tools = [get_day_list]
|
||||
tool_calls = [ToolCall(name=get_day_list.name, args={"days": data}, id="test_id")]
|
||||
outputs: list[ToolMessage] = ToolNode(tools).invoke(
|
||||
[AIMessage(content="", tool_calls=tool_calls)]
|
||||
[AIMessage(content="", tool_calls=tool_calls)],
|
||||
config=_create_config_with_runtime(),
|
||||
)
|
||||
assert outputs[0].content == json.dumps(data, ensure_ascii=False)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user