mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-29 04:55:09 +02:00
prefer ToolCallRequest.tool_call['name'] for deciding what tool is executed in ToolNode
This commit is contained in:
@@ -135,6 +135,9 @@ class ToolCallRequest:
|
||||
|
||||
Attributes:
|
||||
tool_call: Tool call dict with name, args, and id from model output.
|
||||
|
||||
If `tool_call["name"]` differs from `tool`, `tool_call["name"]` is
|
||||
preferred as the authoritative source for what tool is executed.
|
||||
tool: BaseTool instance to be invoked, or None if tool is not
|
||||
registered with the `ToolNode`. When tool is `None`, interceptors can
|
||||
handle the request without validation. If the interceptor calls `execute()`,
|
||||
@@ -1046,8 +1049,13 @@ class ToolNode(RunnableCallable):
|
||||
return self._execute_tool_sync(tool_request, input_type, config)
|
||||
|
||||
# Define execute callable that can be called multiple times
|
||||
original_name = call["name"]
|
||||
|
||||
def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
if req.tool_call["name"] != original_name:
|
||||
# An interceptor (e.g., HITL) redirected the tool call
|
||||
req = replace(req, tool=self.tools_by_name.get(req.tool_call["name"]))
|
||||
return self._execute_tool_sync(req, input_type, config)
|
||||
|
||||
# Call wrapper with request and execute callable
|
||||
@@ -1193,8 +1201,13 @@ class ToolNode(RunnableCallable):
|
||||
return await self._execute_tool_async(tool_request, input_type, config)
|
||||
|
||||
# Define async execute callable that can be called multiple times
|
||||
original_name = call["name"]
|
||||
|
||||
async def execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
"""Execute tool with given request. Can be called multiple times."""
|
||||
if req.tool_call["name"] != original_name:
|
||||
# An interceptor (e.g., HITL) redirected the tool call
|
||||
req = replace(req, tool=self.tools_by_name.get(req.tool_call["name"]))
|
||||
return await self._execute_tool_async(req, input_type, config)
|
||||
|
||||
def _sync_execute(req: ToolCallRequest) -> ToolMessage | Command:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Unit tests for tool call interceptor in ToolNode."""
|
||||
|
||||
import functools
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Awaitable, Callable
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
@@ -1471,3 +1471,36 @@ def test_tool_call_request_is_frozen() -> None:
|
||||
assert fresh_new_request.tool == add # Other fields should remain the same
|
||||
assert fresh_new_request.state == state
|
||||
assert fresh_new_request.runtime is None
|
||||
|
||||
|
||||
async def test_interceptor_can_redirect_to_another_tool() -> None:
|
||||
"""Overriding `tool_call["name"]` routes execution to the newly named tool."""
|
||||
|
||||
@tool
|
||||
def subtract(a: int, b: int) -> int:
|
||||
"""Subtract two numbers."""
|
||||
return a - b
|
||||
|
||||
async def redirect(
|
||||
request: ToolCallRequest,
|
||||
execute: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
||||
) -> ToolMessage | Command:
|
||||
return await execute(
|
||||
request.override(tool_call={**request.tool_call, "name": "subtract"})
|
||||
)
|
||||
|
||||
node = ToolNode([add, subtract], awrap_tool_call=redirect)
|
||||
result = await node.ainvoke(
|
||||
[
|
||||
AIMessage(
|
||||
"",
|
||||
tool_calls=[
|
||||
{"name": "add", "args": {"a": 5, "b": 3}, "id": "1", "type": "tool_call"}
|
||||
],
|
||||
)
|
||||
],
|
||||
_create_config_with_runtime(),
|
||||
)
|
||||
|
||||
# `add` would return 8; `subtract` returns 2.
|
||||
assert result[0].content == "2"
|
||||
|
||||
Reference in New Issue
Block a user