mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-17 15:17:57 +02:00
cr
This commit is contained in:
@@ -347,6 +347,23 @@ class ToolCallRequestMismatchError(ValueError):
|
||||
"""`tool_call["name"]` and `tool` disagree on a `ToolCallRequest`."""
|
||||
|
||||
|
||||
def _check_not_redirected_without_tool(
|
||||
request: ToolCallRequest, original_name: str, original_tool: BaseTool | None
|
||||
) -> None:
|
||||
"""Raise if an interceptor renamed the call but left `tool` as the resolved one."""
|
||||
if (
|
||||
original_tool is not None
|
||||
and request.tool is original_tool
|
||||
and request.tool_call["name"] != original_name
|
||||
):
|
||||
msg = (
|
||||
f"Interceptor set tool_call name to {request.tool_call['name']!r} but left "
|
||||
f"`tool` as {original_tool.name!r}. Redirecting a call requires setting both; "
|
||||
f"resolve the replacement from `ToolCallRequest.available_tools`."
|
||||
)
|
||||
raise ToolCallRequestMismatchError(msg)
|
||||
|
||||
|
||||
class ToolInvocationError(ToolException):
|
||||
"""An error occurred while invoking a tool due to invalid arguments.
|
||||
|
||||
@@ -1062,17 +1079,7 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
if (
|
||||
original_tool is not None
|
||||
and req.tool is original_tool
|
||||
and req.tool_call["name"] != original_name
|
||||
):
|
||||
msg = (
|
||||
f"Interceptor set tool_call name to {req.tool_call['name']!r} but left "
|
||||
f"`tool` as {original_tool.name!r}. Redirecting a call requires setting "
|
||||
f"both; resolve the replacement from `ToolCallRequest.available_tools`."
|
||||
)
|
||||
raise ToolCallRequestMismatchError(msg)
|
||||
_check_not_redirected_without_tool(req, original_name, original_tool)
|
||||
return self._execute_tool_sync(req, input_type, config)
|
||||
|
||||
# Call wrapper with request and execute callable
|
||||
@@ -1225,21 +1232,12 @@ class ToolNode(RunnableCallable):
|
||||
|
||||
async def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
if (
|
||||
original_tool is not None
|
||||
and req.tool is original_tool
|
||||
and req.tool_call["name"] != original_name
|
||||
):
|
||||
msg = (
|
||||
f"Interceptor set tool_call name to {req.tool_call['name']!r} but left "
|
||||
f"`tool` as {original_tool.name!r}. Redirecting a call requires setting "
|
||||
f"both; resolve the replacement from `ToolCallRequest.available_tools`."
|
||||
)
|
||||
raise ToolCallRequestMismatchError(msg)
|
||||
_check_not_redirected_without_tool(req, original_name, original_tool)
|
||||
return await self._execute_tool_async(req, input_type, config)
|
||||
|
||||
def _sync_execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Sync execute fallback for sync wrapper."""
|
||||
_check_not_redirected_without_tool(req, original_name, original_tool)
|
||||
return self._execute_tool_sync(req, input_type, config)
|
||||
|
||||
# Call wrapper with request and execute callable
|
||||
|
||||
@@ -1499,7 +1499,12 @@ async def test_interceptor_can_redirect_to_another_tool() -> None:
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 5, "b": 3}, "id": "1", "type": "tool_call"}
|
||||
{
|
||||
"name": "add",
|
||||
"args": {"a": 5, "b": 3},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
@@ -1517,7 +1522,9 @@ def test_interceptor_tool_call_name_and_tool_must_agree() -> None:
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
return execute(request.override(tool_call={**request.tool_call, "name": "other"}))
|
||||
return execute(
|
||||
request.override(tool_call={**request.tool_call, "name": "other"})
|
||||
)
|
||||
|
||||
@tool
|
||||
def other(a: int, b: int) -> int:
|
||||
@@ -1532,7 +1539,49 @@ def test_interceptor_tool_call_name_and_tool_must_agree() -> None:
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 1, "b": 2}, "id": "1", "type": "tool_call"}
|
||||
{
|
||||
"name": "add",
|
||||
"args": {"a": 1, "b": 2},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
|
||||
async def test_sync_interceptor_under_ainvoke_also_validates_redirect() -> None:
|
||||
"""The sync-wrapper fallback used by `ainvoke` must validate too, not just `invoke`."""
|
||||
|
||||
def rename_only(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||
) -> ToolMessage | Command:
|
||||
return execute(
|
||||
request.override(tool_call={**request.tool_call, "name": "other"})
|
||||
)
|
||||
|
||||
@tool
|
||||
def other(a: int, b: int) -> int:
|
||||
"""Another tool."""
|
||||
return 0
|
||||
|
||||
# Only a sync wrapper is configured, so `ainvoke` routes through `_sync_execute`.
|
||||
node = ToolNode([add, other], wrap_tool_call=rename_only)
|
||||
with pytest.raises(ToolCallRequestMismatchError, match="other"):
|
||||
await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{
|
||||
"name": "add",
|
||||
"args": {"a": 1, "b": 2},
|
||||
"id": "1",
|
||||
"type": "tool_call",
|
||||
}
|
||||
],
|
||||
)
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user