mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-15 07:00:38 +00:00
Apply patch [skip ci]
This commit is contained in:
@@ -54,7 +54,6 @@ from typing import (
|
||||
get_type_hints,
|
||||
)
|
||||
|
||||
from langchain.utilities.asyncio import asyncio_timeout
|
||||
from langchain_core.messages import (
|
||||
AIMessage,
|
||||
AnyMessage,
|
||||
@@ -79,6 +78,8 @@ from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
||||
from langgraph.types import Command, Send
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
from langchain.utilities.asyncio import asyncio_timeout
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Sequence
|
||||
|
||||
@@ -457,7 +458,7 @@ class ToolNode(RunnableCallable):
|
||||
store: Optional[BaseStore], # noqa: UP045
|
||||
) -> Any:
|
||||
tool_calls, input_type = self._parse_input(input, store)
|
||||
|
||||
|
||||
# Apply timeout if specified
|
||||
if self._timeout is not None:
|
||||
try:
|
||||
@@ -1204,7 +1205,3 @@ def _get_store_arg(tool: BaseTool) -> str | None:
|
||||
return name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1486,16 +1486,16 @@ def test_tool_node_stream_writer() -> None:
|
||||
async def test_tool_node_timeout_single_tool():
|
||||
"""Test that ToolNode correctly times out when a tool takes longer than the specified timeout."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def slow_tool(duration: float) -> str:
|
||||
"""A tool that simulates a slow operation."""
|
||||
await asyncio.sleep(duration)
|
||||
return f"Completed after {duration} seconds"
|
||||
|
||||
|
||||
# Create ToolNode with 0.5 second timeout
|
||||
tool_node = ToolNode([slow_tool], timeout=0.5)
|
||||
|
||||
|
||||
# Test that a tool that takes 1 second times out
|
||||
with pytest.raises(asyncio.TimeoutError) as exc_info:
|
||||
await tool_node.ainvoke(
|
||||
@@ -1514,7 +1514,7 @@ async def test_tool_node_timeout_single_tool():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify the error message includes helpful information
|
||||
assert "Tool execution timed out after 0.5 seconds" in str(exc_info.value)
|
||||
assert "slow_tool" in str(exc_info.value)
|
||||
@@ -1523,15 +1523,15 @@ async def test_tool_node_timeout_single_tool():
|
||||
async def test_tool_node_timeout_propagation():
|
||||
"""Test that timeout exceptions are properly propagated and not silently caught."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def hanging_tool() -> str:
|
||||
"""A tool that hangs indefinitely."""
|
||||
await asyncio.sleep(10) # Simulate a hanging operation
|
||||
return "Should never reach here"
|
||||
|
||||
|
||||
tool_node = ToolNode([hanging_tool], timeout=0.1)
|
||||
|
||||
|
||||
# Ensure the timeout exception is raised, not caught silently
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await tool_node.ainvoke(
|
||||
@@ -1555,16 +1555,16 @@ async def test_tool_node_timeout_propagation():
|
||||
async def test_tool_node_no_timeout_backward_compatibility():
|
||||
"""Test that ToolNode works normally when no timeout is specified (backward compatibility)."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def normal_tool(message: str) -> str:
|
||||
"""A normal tool that completes quickly."""
|
||||
await asyncio.sleep(0.1)
|
||||
return f"Processed: {message}"
|
||||
|
||||
|
||||
# Create ToolNode without timeout (default behavior)
|
||||
tool_node = ToolNode([normal_tool])
|
||||
|
||||
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
"messages": [
|
||||
@@ -1581,7 +1581,7 @@ async def test_tool_node_no_timeout_backward_compatibility():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify the tool executes successfully without timeout
|
||||
assert result["messages"][0].content == "Processed: Hello"
|
||||
assert result["messages"][0].name == "normal_tool"
|
||||
@@ -1590,22 +1590,22 @@ async def test_tool_node_no_timeout_backward_compatibility():
|
||||
async def test_tool_node_timeout_multiple_tools_parallel():
|
||||
"""Test timeout behavior with multiple tools running in parallel."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def fast_tool(id: str) -> str:
|
||||
"""A tool that completes quickly."""
|
||||
await asyncio.sleep(0.1)
|
||||
return f"Fast tool {id} completed"
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def slow_tool(id: str) -> str:
|
||||
"""A tool that takes longer."""
|
||||
await asyncio.sleep(1.0)
|
||||
return f"Slow tool {id} completed"
|
||||
|
||||
|
||||
# Create ToolNode with 0.5 second timeout
|
||||
tool_node = ToolNode([fast_tool, slow_tool], timeout=0.5)
|
||||
|
||||
|
||||
# Test with multiple tools where one is slow
|
||||
with pytest.raises(asyncio.TimeoutError) as exc_info:
|
||||
await tool_node.ainvoke(
|
||||
@@ -1634,7 +1634,7 @@ async def test_tool_node_timeout_multiple_tools_parallel():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify error message mentions the tools being executed
|
||||
error_msg = str(exc_info.value)
|
||||
assert "Tool execution timed out after 0.5 seconds" in error_msg
|
||||
@@ -1645,16 +1645,16 @@ async def test_tool_node_timeout_multiple_tools_parallel():
|
||||
async def test_tool_node_timeout_with_successful_completion():
|
||||
"""Test that tools complete successfully when they finish before timeout."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def timed_tool(duration: float, message: str) -> str:
|
||||
"""A tool with configurable duration."""
|
||||
await asyncio.sleep(duration)
|
||||
return f"Completed: {message} after {duration}s"
|
||||
|
||||
|
||||
# Create ToolNode with 1 second timeout
|
||||
tool_node = ToolNode([timed_tool], timeout=1.0)
|
||||
|
||||
|
||||
# Test that tools completing within timeout work correctly
|
||||
result = await tool_node.ainvoke(
|
||||
{
|
||||
@@ -1677,7 +1677,7 @@ async def test_tool_node_timeout_with_successful_completion():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify both tools completed successfully
|
||||
assert len(result["messages"]) == 2
|
||||
assert "Completed: Quick task after 0.3s" in result["messages"][0].content
|
||||
@@ -1687,16 +1687,16 @@ async def test_tool_node_timeout_with_successful_completion():
|
||||
async def test_tool_node_timeout_with_error_handling():
|
||||
"""Test that timeout works correctly with error handling enabled."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def slow_failing_tool() -> str:
|
||||
"""A tool that would fail after a delay."""
|
||||
await asyncio.sleep(1.0)
|
||||
raise ValueError("This error should not be reached due to timeout")
|
||||
|
||||
|
||||
# Create ToolNode with timeout and error handling
|
||||
tool_node = ToolNode([slow_failing_tool], timeout=0.2, handle_tool_errors=True)
|
||||
|
||||
|
||||
# The timeout should occur before the tool error
|
||||
with pytest.raises(asyncio.TimeoutError) as exc_info:
|
||||
await tool_node.ainvoke(
|
||||
@@ -1715,7 +1715,7 @@ async def test_tool_node_timeout_with_error_handling():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify it's a timeout error, not the ValueError
|
||||
assert "Tool execution timed out" in str(exc_info.value)
|
||||
|
||||
@@ -1723,17 +1723,17 @@ async def test_tool_node_timeout_with_error_handling():
|
||||
async def test_tool_node_timeout_with_sync_tools():
|
||||
"""Test that timeout works with synchronous tools (they get run in executor)."""
|
||||
import time
|
||||
|
||||
|
||||
@dec_tool
|
||||
def sync_slow_tool(duration: float) -> str:
|
||||
"""A synchronous tool that blocks."""
|
||||
time.sleep(duration)
|
||||
return f"Sync completed after {duration}s"
|
||||
|
||||
|
||||
# Note: Sync tools run in thread executor, so timeout behavior may differ
|
||||
# This test verifies the implementation handles sync tools appropriately
|
||||
tool_node = ToolNode([sync_slow_tool], timeout=0.5)
|
||||
|
||||
|
||||
# Sync tools are executed via the sync _func method, not _afunc
|
||||
# So this tests the sync path
|
||||
result = tool_node.invoke(
|
||||
@@ -1752,7 +1752,7 @@ async def test_tool_node_timeout_with_sync_tools():
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Verify sync tool completes successfully when under timeout
|
||||
assert "Sync completed after 0.1s" in result["messages"][0].content
|
||||
|
||||
@@ -1760,15 +1760,15 @@ async def test_tool_node_timeout_with_sync_tools():
|
||||
async def test_tool_node_timeout_zero():
|
||||
"""Test edge case with zero timeout."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def instant_tool() -> str:
|
||||
"""A tool that should complete instantly."""
|
||||
return "Instant response"
|
||||
|
||||
|
||||
# Create ToolNode with zero timeout (should timeout immediately)
|
||||
tool_node = ToolNode([instant_tool], timeout=0.0)
|
||||
|
||||
|
||||
# Even instant operations should timeout with zero timeout
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await tool_node.ainvoke(
|
||||
@@ -1792,15 +1792,15 @@ async def test_tool_node_timeout_zero():
|
||||
async def test_tool_node_timeout_with_list_input():
|
||||
"""Test timeout functionality with list input format."""
|
||||
import asyncio
|
||||
|
||||
|
||||
@dec_tool
|
||||
async def list_input_tool(value: str) -> str:
|
||||
"""Tool for testing list input."""
|
||||
await asyncio.sleep(0.5)
|
||||
return f"Processed: {value}"
|
||||
|
||||
|
||||
tool_node = ToolNode([list_input_tool], timeout=0.2)
|
||||
|
||||
|
||||
# Test with list input format (not dict with messages key)
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await tool_node.ainvoke(
|
||||
@@ -1817,4 +1817,3 @@ async def test_tool_node_timeout_with_list_input():
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user