feat(prebuilt): add support for handle_tool_errors with single exception type (#6021)

This PR extends the `ToolNode` `handle_tool_errors` property to also
accept a single exception type: `type[Exception]`.

Example:
```python
tool_node_with_error_handling = ToolNode(
    [tool1, tool2, tool3], handle_tool_errors=ValueError
).invoke(...)
"""A tool node that will only retry a failed tool call if a ValueError is raised"""
```
This commit is contained in:
Caspar Broekhuizen
2025-08-26 18:49:55 -04:00
committed by GitHub
parent b36b7e2730
commit 4eaab0284a
2 changed files with 29 additions and 9 deletions
+27 -9
View File
@@ -157,6 +157,7 @@ def _handle_tool_error(
bool,
str,
Callable[..., str],
type[Exception],
tuple[type[Exception], ...],
],
) -> str:
@@ -184,12 +185,14 @@ def _handle_tool_error(
The tuple case is handled by the caller through exception type checking,
not by this function directly.
"""
if isinstance(flag, (bool, tuple)):
if isinstance(flag, (bool, tuple)) or (
isinstance(flag, type) and issubclass(flag, Exception)
):
content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e))
elif isinstance(flag, str):
content = flag
elif callable(flag):
content = flag(e)
content = flag(e) # type: ignore [assignment, call-arg]
else:
raise ValueError(
f"Got unexpected type of `handle_tool_error`. Expected bool, str "
@@ -309,6 +312,7 @@ class ToolNode(RunnableCallable):
error template containing the exception details.
- **str**: Catch all errors and return a ToolMessage with this custom
error message string.
- **type[Exception]**: Only catch exceptions with the specified type and return the default error message for it.
- **tuple[type[Exception], ...]**: Only catch exceptions with the specified
types and return default error messages for them.
- **Callable[..., str]**: Catch exceptions matching the callable's signature
@@ -373,7 +377,7 @@ class ToolNode(RunnableCallable):
name: str = "tools",
tags: Optional[list[str]] = None,
handle_tool_errors: Union[
bool, str, Callable[..., str], tuple[type[Exception], ...]
bool, str, Callable[..., str], type[Exception], tuple[type[Exception], ...]
] = _default_handle_tool_errors,
messages_key: str = "messages",
) -> None:
@@ -518,9 +522,16 @@ class ToolNode(RunnableCallable):
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self._handle_tool_errors, tuple):
handled_types: tuple = self._handle_tool_errors
elif callable(self._handle_tool_errors):
handled_types: tuple[type[Exception], ...]
if isinstance(self._handle_tool_errors, type) and issubclass(
self._handle_tool_errors, Exception
):
handled_types = (self._handle_tool_errors,)
elif isinstance(self._handle_tool_errors, tuple):
handled_types = self._handle_tool_errors
elif callable(self._handle_tool_errors) and not isinstance(
self._handle_tool_errors, type
):
handled_types = _infer_handled_types(self._handle_tool_errors)
else:
# default behavior is catching all exceptions
@@ -580,9 +591,16 @@ class ToolNode(RunnableCallable):
except GraphBubbleUp as e:
raise e
except Exception as e:
if isinstance(self._handle_tool_errors, tuple):
handled_types: tuple = self._handle_tool_errors
elif callable(self._handle_tool_errors):
handled_types: tuple[type[Exception], ...]
if isinstance(self._handle_tool_errors, type) and issubclass(
self._handle_tool_errors, Exception
):
handled_types = (self._handle_tool_errors,)
elif isinstance(self._handle_tool_errors, tuple):
handled_types = self._handle_tool_errors
elif callable(self._handle_tool_errors) and not isinstance(
self._handle_tool_errors, type
):
handled_types = _infer_handled_types(self._handle_tool_errors)
else:
# default behavior is catching all exceptions
+2
View File
@@ -274,10 +274,12 @@ async def test_tool_node_error_handling() -> None:
# test catching all exceptions, via:
# - handle_tool_errors = True
# - passing a single exception
# - passing a tuple of all exceptions
# - passing a callable with all exceptions in the signature
for handle_tool_errors in (
True,
Exception,
(ValueError, ToolException, ToolInvocationError),
handle_all,
):