mirror of
https://github.com/hwchase17/langchain.git
synced 2026-07-01 14:47:02 +00:00
fix(core): type structured tool error handler output (#38003)
`handle_tool_error` callables can already return structured message content at runtime, but the public typing only allowed strings. The tool error handling API now reflects the existing output formatting path, including clearer docs for how handled errors become `ToolMessage(status="error")` results.
This commit is contained in:
@@ -398,6 +398,13 @@ class ToolException(Exception): # noqa: N818
|
||||
|
||||
|
||||
ArgsSchema = TypeBaseModel | dict[str, Any]
|
||||
ToolExceptionHandlerOutput = str | list[str | dict[str, Any]]
|
||||
"""Content returned by a `handle_tool_error` callable.
|
||||
|
||||
Error handlers may return plain text or structured message content blocks.
|
||||
When the original tool call includes a `tool_call_id`, this content is used
|
||||
as the content of a `ToolMessage` with `status="error"`.
|
||||
"""
|
||||
|
||||
_EMPTY_SET: frozenset[str] = frozenset()
|
||||
|
||||
@@ -496,8 +503,20 @@ class ChildTool(BaseTool):
|
||||
You can use these to, e.g., identify a specific instance of a tool with its usecase.
|
||||
"""
|
||||
|
||||
handle_tool_error: bool | str | Callable[[ToolException], str] | None = False
|
||||
"""Handle the content of the `ToolException` thrown."""
|
||||
handle_tool_error: (
|
||||
bool | str | Callable[[ToolException], ToolExceptionHandlerOutput] | None
|
||||
) = False
|
||||
"""Handle `ToolException` raised by tool execution.
|
||||
|
||||
If `False`, the exception is re-raised. If `True`, the exception message is
|
||||
returned as tool output. If a string is passed, that string is returned
|
||||
as tool output. If a callable is passed, it receives the exception and
|
||||
its return value is used as the tool output.
|
||||
|
||||
Callable handlers may return either a string or a list of message
|
||||
content blocks. If the tool was invoked with a `tool_call_id`, the handled
|
||||
content is wrapped in a `ToolMessage` with `status="error"`.
|
||||
"""
|
||||
|
||||
handle_validation_error: (
|
||||
bool | str | Callable[[ValidationError | ValidationErrorV1], str] | None
|
||||
@@ -1182,16 +1201,23 @@ def _handle_validation_error(
|
||||
def _handle_tool_error(
|
||||
e: ToolException,
|
||||
*,
|
||||
flag: Literal[True] | str | Callable[[ToolException], str] | None,
|
||||
) -> str:
|
||||
"""Handle tool execution errors based on the configured flag.
|
||||
flag: Literal[True]
|
||||
| str
|
||||
| Callable[[ToolException], ToolExceptionHandlerOutput]
|
||||
| None,
|
||||
) -> ToolExceptionHandlerOutput:
|
||||
"""Convert a `ToolException` into handled tool output content.
|
||||
|
||||
Args:
|
||||
e: The tool exception that occurred.
|
||||
flag: How to handle the error (`bool`, `str`, or `Callable`).
|
||||
flag: How to handle the error. `True` uses the exception message, a string
|
||||
replaces the message, and a callable computes replacement content from
|
||||
the exception.
|
||||
|
||||
Returns:
|
||||
The error message to return.
|
||||
The handled error content. This may be plain text or structured message
|
||||
content blocks; callers pass it through normal tool
|
||||
output formatting.
|
||||
|
||||
Raises:
|
||||
ValueError: If the flag type is unexpected.
|
||||
|
||||
@@ -826,6 +826,23 @@ def test_exception_handling_callable() -> None:
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_exception_handling_callable_message_content_blocks() -> None:
|
||||
expected: list[str | dict[str, Any]] = [{"type": "text", "text": "handled error"}]
|
||||
|
||||
def handling(e: ToolException) -> list[str | dict[str, Any]]:
|
||||
return expected
|
||||
|
||||
tool_ = _FakeExceptionTool(handle_tool_error=handling)
|
||||
actual = tool_.invoke(
|
||||
{"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}
|
||||
)
|
||||
|
||||
assert isinstance(actual, ToolMessage)
|
||||
assert actual.content == expected
|
||||
assert actual.status == "error"
|
||||
assert actual.tool_call_id == "call_1"
|
||||
|
||||
|
||||
def test_exception_handling_non_tool_exception() -> None:
|
||||
tool_ = _FakeExceptionTool(exception=ValueError("some error"))
|
||||
with pytest.raises(ValueError, match="some error"):
|
||||
@@ -857,6 +874,23 @@ async def test_async_exception_handling_callable() -> None:
|
||||
assert expected == actual
|
||||
|
||||
|
||||
async def test_async_exception_handling_callable_message_content_blocks() -> None:
|
||||
expected: list[str | dict[str, Any]] = [{"type": "text", "text": "handled error"}]
|
||||
|
||||
def handling(e: ToolException) -> list[str | dict[str, Any]]:
|
||||
return expected
|
||||
|
||||
tool_ = _FakeExceptionTool(handle_tool_error=handling)
|
||||
actual = await tool_.ainvoke(
|
||||
{"type": "tool_call", "args": {}, "name": "exception", "id": "call_1"}
|
||||
)
|
||||
|
||||
assert isinstance(actual, ToolMessage)
|
||||
assert actual.content == expected
|
||||
assert actual.status == "error"
|
||||
assert actual.tool_call_id == "call_1"
|
||||
|
||||
|
||||
async def test_async_exception_handling_non_tool_exception() -> None:
|
||||
tool_ = _FakeExceptionTool(exception=ValueError("some error"))
|
||||
with pytest.raises(ValueError, match="some error"):
|
||||
|
||||
Reference in New Issue
Block a user