mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-12 12:47:53 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8cb23c9ccc |
@@ -891,6 +891,9 @@ class ToolNode(RunnableCallable):
|
|||||||
return self._validate_tool_command(response, request.tool_call, input_type)
|
return self._validate_tool_command(response, request.tool_call, input_type)
|
||||||
if isinstance(response, ToolMessage):
|
if isinstance(response, ToolMessage):
|
||||||
response.content = cast("str | list", msg_content_output(response.content))
|
response.content = cast("str | list", msg_content_output(response.content))
|
||||||
|
# Enrich ToolMessage with name if not set (e.g., from fallback handlers)
|
||||||
|
if response.name is None:
|
||||||
|
response.name = call["name"]
|
||||||
return response
|
return response
|
||||||
|
|
||||||
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
||||||
@@ -1048,6 +1051,9 @@ class ToolNode(RunnableCallable):
|
|||||||
return self._validate_tool_command(response, request.tool_call, input_type)
|
return self._validate_tool_command(response, request.tool_call, input_type)
|
||||||
if isinstance(response, ToolMessage):
|
if isinstance(response, ToolMessage):
|
||||||
response.content = cast("str | list", msg_content_output(response.content))
|
response.content = cast("str | list", msg_content_output(response.content))
|
||||||
|
# Enrich ToolMessage with name if not set (e.g., from fallback handlers)
|
||||||
|
if response.name is None:
|
||||||
|
response.name = call["name"]
|
||||||
return response
|
return response
|
||||||
|
|
||||||
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
msg = f"Tool {call['name']} returned unexpected type: {type(response)}"
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
"""Test to reproduce the tool error fallback issue.
|
||||||
|
|
||||||
|
When using ToolNode(...).with_fallbacks(...) with an error handler that returns
|
||||||
|
ToolMessage objects, the messages are losing:
|
||||||
|
1. The `name` field (becomes `None` instead of the tool name)
|
||||||
|
2. The `status` field (becomes `success` instead of `error`)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from langchain_core.messages import AIMessage, ToolMessage
|
||||||
|
from langchain_core.runnables import RunnableLambda
|
||||||
|
from langchain_core.runnables.config import RunnableConfig
|
||||||
|
from langchain_core.tools import tool
|
||||||
|
|
||||||
|
from langgraph.prebuilt import ToolNode
|
||||||
|
|
||||||
|
|
||||||
|
def _create_mock_runtime():
|
||||||
|
"""Create a mock Runtime object for testing ToolNode outside of graph context."""
|
||||||
|
mock_runtime = Mock()
|
||||||
|
mock_runtime.store = None
|
||||||
|
mock_runtime.context = None
|
||||||
|
mock_runtime.stream_writer = lambda *args, **kwargs: None
|
||||||
|
return mock_runtime
|
||||||
|
|
||||||
|
|
||||||
|
def _create_config_with_runtime() -> RunnableConfig:
|
||||||
|
"""Create a RunnableConfig with mock Runtime for testing ToolNode."""
|
||||||
|
return {"configurable": {"__pregel_runtime": _create_mock_runtime()}}
|
||||||
|
|
||||||
|
|
||||||
|
@tool
|
||||||
|
def failing_tool(x: int) -> str:
|
||||||
|
"""A tool that always fails."""
|
||||||
|
raise RuntimeError("This tool always fails!")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_tool_error(state) -> dict:
|
||||||
|
"""Error handler that returns ToolMessages."""
|
||||||
|
print(f"handle_tool_error called with state: {state}")
|
||||||
|
print(f"State type: {type(state)}")
|
||||||
|
print(f"State keys: {state.keys() if isinstance(state, dict) else 'N/A'}")
|
||||||
|
error = state.get("error")
|
||||||
|
print(f"Error: {error}")
|
||||||
|
tool_calls = state["messages"][-1].tool_calls
|
||||||
|
print(f"Tool calls: {tool_calls}")
|
||||||
|
return {
|
||||||
|
"messages": [
|
||||||
|
ToolMessage(
|
||||||
|
content=f"Error: {repr(error)}\n please fix your mistakes.",
|
||||||
|
tool_call_id=tc["id"],
|
||||||
|
)
|
||||||
|
for tc in tool_calls
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fallback_runnable = RunnableLambda(handle_tool_error)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_error_with_fallbacks():
|
||||||
|
"""Test that ToolMessages from fallback handlers preserve name and status."""
|
||||||
|
# Create a ToolNode with a fallback
|
||||||
|
tool_node = ToolNode([failing_tool], handle_tool_errors=True).with_fallbacks(
|
||||||
|
[fallback_runnable],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create an AI message with a tool call
|
||||||
|
messages = [
|
||||||
|
AIMessage(
|
||||||
|
content="",
|
||||||
|
tool_calls=[
|
||||||
|
{
|
||||||
|
"name": "failing_tool",
|
||||||
|
"args": {"x": 1},
|
||||||
|
"id": "call_123",
|
||||||
|
"type": "tool_call",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
# Invoke the tool node
|
||||||
|
result = tool_node.invoke({"messages": messages}, config=_create_config_with_runtime())
|
||||||
|
|
||||||
|
print("Result:", result)
|
||||||
|
print("Result type:", type(result))
|
||||||
|
print("Result keys:", result.keys() if isinstance(result, dict) else "N/A")
|
||||||
|
print("\nTool messages:")
|
||||||
|
for msg in result["messages"]:
|
||||||
|
if isinstance(msg, ToolMessage):
|
||||||
|
print(f" - content: {msg.content[:50]}...")
|
||||||
|
print(f" name: {msg.name}")
|
||||||
|
print(f" tool_call_id: {msg.tool_call_id}")
|
||||||
|
print(f" status: {msg.status}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Check expectations
|
||||||
|
assert msg.name == "failing_tool", f"Expected name='failing_tool', got {msg.name}"
|
||||||
|
assert msg.status == "error", f"Expected status='error', got {msg.status}"
|
||||||
|
print("✓ Test passed!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
test_tool_error_with_fallbacks()
|
||||||
Reference in New Issue
Block a user