mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-10-18 05:57:37 +02:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8ca30d8e0 | ||
|
|
6d20a0b9c7 |
@@ -124,7 +124,7 @@ class _ToolCallRequestOverrides(TypedDict, total=False):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class ToolCallRequest:
|
class ToolCallRequest(Generic[ContextT, StateT]):
|
||||||
"""Tool execution request passed to tool call interceptors.
|
"""Tool execution request passed to tool call interceptors.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@@ -133,18 +133,45 @@ class ToolCallRequest:
|
|||||||
registered with the `ToolNode`. When tool is `None`, interceptors can
|
registered with the `ToolNode`. When tool is `None`, interceptors can
|
||||||
handle the request without validation. If the interceptor calls `execute()`,
|
handle the request without validation. If the interceptor calls `execute()`,
|
||||||
validation will occur and raise an error for unregistered tools.
|
validation will occur and raise an error for unregistered tools.
|
||||||
state: Agent state (`dict`, `list`, or `BaseModel`).
|
|
||||||
runtime: LangGraph runtime context (optional, `None` if outside graph).
|
runtime: LangGraph runtime context (optional, `None` if outside graph).
|
||||||
|
state: Agent state (`dict`, `list`, or `BaseModel`). Pulled from `runtime.state`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tool_call: ToolCall
|
tool_call: ToolCall
|
||||||
tool: BaseTool | None
|
tool: BaseTool | None
|
||||||
state: Any
|
runtime: ToolRuntime[ContextT, StateT]
|
||||||
runtime: ToolRuntime
|
|
||||||
|
@property
|
||||||
|
def state(self) -> StateT:
|
||||||
|
"""Get the state from the runtime.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The current graph state from the runtime context.
|
||||||
|
"""
|
||||||
|
return self.runtime.state
|
||||||
|
|
||||||
|
def __setattr__(self, name: str, value: Any) -> None:
|
||||||
|
"""Raise deprecation warning when setting attributes directly.
|
||||||
|
|
||||||
|
Direct attribute assignment is deprecated. Use the `override()` method instead.
|
||||||
|
"""
|
||||||
|
import warnings
|
||||||
|
|
||||||
|
# Allow setting attributes during initialization
|
||||||
|
if not hasattr(self, "__dataclass_fields__") or not hasattr(self, name):
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
else:
|
||||||
|
warnings.warn(
|
||||||
|
f"Setting attribute '{name}' on ToolCallRequest is deprecated. "
|
||||||
|
"Use the override() method instead to create a new instance with modified values.",
|
||||||
|
DeprecationWarning,
|
||||||
|
stacklevel=2,
|
||||||
|
)
|
||||||
|
object.__setattr__(self, name, value)
|
||||||
|
|
||||||
def override(
|
def override(
|
||||||
self, **overrides: Unpack[_ToolCallRequestOverrides]
|
self, **overrides: Unpack[_ToolCallRequestOverrides]
|
||||||
) -> ToolCallRequest:
|
) -> ToolCallRequest[ContextT, StateT]:
|
||||||
"""Replace the request with a new request with the given overrides.
|
"""Replace the request with a new request with the given overrides.
|
||||||
|
|
||||||
Returns a new `ToolCallRequest` instance with the specified attributes replaced.
|
Returns a new `ToolCallRequest` instance with the specified attributes replaced.
|
||||||
@@ -202,8 +229,9 @@ Examples:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
def handler(request, execute):
|
def handler(request, execute):
|
||||||
request.tool_call["args"]["value"] *= 2
|
modified_call = {**request.tool_call, "args": {**request.tool_call["args"], "value": request.tool_call["args"]["value"] * 2}}
|
||||||
return execute(request)
|
modified_request = request.override(tool_call=modified_call)
|
||||||
|
return execute(modified_request)
|
||||||
```
|
```
|
||||||
|
|
||||||
Retry on error (execute multiple times):
|
Retry on error (execute multiple times):
|
||||||
@@ -927,11 +955,10 @@ class ToolNode(RunnableCallable):
|
|||||||
# to short-circuit requests for unregistered tools
|
# to short-circuit requests for unregistered tools
|
||||||
tool = self.tools_by_name.get(call["name"])
|
tool = self.tools_by_name.get(call["name"])
|
||||||
|
|
||||||
# Create the tool request with state and runtime
|
# Create the tool request with runtime
|
||||||
tool_request = ToolCallRequest(
|
tool_request = ToolCallRequest(
|
||||||
tool_call=call,
|
tool_call=call,
|
||||||
tool=tool,
|
tool=tool,
|
||||||
state=tool_runtime.state,
|
|
||||||
runtime=tool_runtime,
|
runtime=tool_runtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1084,11 +1111,10 @@ class ToolNode(RunnableCallable):
|
|||||||
# to short-circuit requests for unregistered tools
|
# to short-circuit requests for unregistered tools
|
||||||
tool = self.tools_by_name.get(call["name"])
|
tool = self.tools_by_name.get(call["name"])
|
||||||
|
|
||||||
# Create the tool request with state and runtime
|
# Create the tool request with runtime
|
||||||
tool_request = ToolCallRequest(
|
tool_request = ToolCallRequest(
|
||||||
tool_call=call,
|
tool_call=call,
|
||||||
tool=tool,
|
tool=tool,
|
||||||
state=tool_runtime.state,
|
|
||||||
runtime=tool_runtime,
|
runtime=tool_runtime,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from langgraph.types import Command
|
|||||||
from langgraph.prebuilt.tool_node import (
|
from langgraph.prebuilt.tool_node import (
|
||||||
ToolCallRequest,
|
ToolCallRequest,
|
||||||
ToolNode,
|
ToolNode,
|
||||||
|
ToolRuntime,
|
||||||
)
|
)
|
||||||
|
|
||||||
pytestmark = pytest.mark.anyio
|
pytestmark = pytest.mark.anyio
|
||||||
@@ -130,11 +131,17 @@ def test_modify_arguments() -> None:
|
|||||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||||
) -> ToolMessage | Command:
|
) -> ToolMessage | Command:
|
||||||
"""Handler that doubles the input arguments."""
|
"""Handler that doubles the input arguments."""
|
||||||
# Modify the arguments
|
# Modify the arguments using override method
|
||||||
request.tool_call["args"]["a"] *= 2
|
modified_call = {
|
||||||
request.tool_call["args"]["b"] *= 2
|
**request.tool_call,
|
||||||
|
"args": {
|
||||||
return execute(request)
|
**request.tool_call["args"],
|
||||||
|
"a": request.tool_call["args"]["a"] * 2,
|
||||||
|
"b": request.tool_call["args"]["b"] * 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
modified_request = request.override(tool_call=modified_call)
|
||||||
|
return execute(modified_request)
|
||||||
|
|
||||||
tool_node = ToolNode([add], wrap_tool_call=modify_args_handler)
|
tool_node = ToolNode([add], wrap_tool_call=modify_args_handler)
|
||||||
|
|
||||||
@@ -336,16 +343,23 @@ def test_tool_call_request_dataclass() -> None:
|
|||||||
"""Test ToolCallRequest dataclass."""
|
"""Test ToolCallRequest dataclass."""
|
||||||
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
|
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
|
||||||
state: dict = {"messages": []}
|
state: dict = {"messages": []}
|
||||||
runtime = None
|
tool_runtime = ToolRuntime(
|
||||||
|
state=state,
|
||||||
|
config={},
|
||||||
|
context=None,
|
||||||
|
store=None,
|
||||||
|
stream_writer=Mock(),
|
||||||
|
tool_call_id="call_1",
|
||||||
|
)
|
||||||
|
|
||||||
request = ToolCallRequest(
|
request = ToolCallRequest(
|
||||||
tool_call=tool_call, tool=add, state=state, runtime=runtime
|
tool_call=tool_call, tool=add, runtime=tool_runtime
|
||||||
) # type: ignore[arg-type]
|
)
|
||||||
|
|
||||||
assert request.tool_call == tool_call
|
assert request.tool_call == tool_call
|
||||||
assert request.tool == add
|
assert request.tool == add
|
||||||
assert request.state == state
|
assert request.state == state
|
||||||
assert request.runtime is None
|
assert request.runtime is tool_runtime
|
||||||
assert request.tool_call["name"] == "add"
|
assert request.tool_call["name"] == "add"
|
||||||
|
|
||||||
|
|
||||||
@@ -362,10 +376,17 @@ async def test_handler_with_async_execution() -> None:
|
|||||||
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
execute: Callable[[ToolCallRequest], ToolMessage | Command],
|
||||||
) -> ToolMessage | Command:
|
) -> ToolMessage | Command:
|
||||||
"""Handler that modifies arguments."""
|
"""Handler that modifies arguments."""
|
||||||
# Add 10 to both arguments
|
# Add 10 to both arguments using override method
|
||||||
request.tool_call["args"]["a"] += 10
|
modified_call = {
|
||||||
request.tool_call["args"]["b"] += 10
|
**request.tool_call,
|
||||||
return execute(request)
|
"args": {
|
||||||
|
**request.tool_call["args"],
|
||||||
|
"a": request.tool_call["args"]["a"] + 10,
|
||||||
|
"b": request.tool_call["args"]["b"] + 10,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
modified_request = request.override(tool_call=modified_call)
|
||||||
|
return execute(modified_request)
|
||||||
|
|
||||||
tool_node = ToolNode([async_add], wrap_tool_call=modifying_handler)
|
tool_node = ToolNode([async_add], wrap_tool_call=modifying_handler)
|
||||||
|
|
||||||
@@ -1305,3 +1326,74 @@ async def test_state_extraction_with_tool_call_with_context_async() -> None:
|
|||||||
assert state_seen[0] == actual_state
|
assert state_seen[0] == actual_state
|
||||||
assert "__type" not in state_seen[0]
|
assert "__type" not in state_seen[0]
|
||||||
assert "tool_call" not in state_seen[0]
|
assert "tool_call" not in state_seen[0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_call_request_is_frozen() -> None:
|
||||||
|
"""Test that ToolCallRequest raises deprecation warnings on direct attribute reassignment."""
|
||||||
|
tool_call: ToolCall = {"name": "add", "args": {"a": 1, "b": 2}, "id": "call_1"}
|
||||||
|
state: dict = {"messages": []}
|
||||||
|
tool_runtime = ToolRuntime(
|
||||||
|
state=state,
|
||||||
|
config={},
|
||||||
|
context=None,
|
||||||
|
store=None,
|
||||||
|
stream_writer=Mock(),
|
||||||
|
tool_call_id="call_1",
|
||||||
|
)
|
||||||
|
|
||||||
|
request = ToolCallRequest(
|
||||||
|
tool_call=tool_call, tool=add, runtime=tool_runtime
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test that direct attribute reassignment raises DeprecationWarning
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning,
|
||||||
|
match="Setting attribute 'tool_call' on ToolCallRequest is deprecated",
|
||||||
|
):
|
||||||
|
request.tool_call = {"name": "other", "args": {}, "id": "call_2"} # type: ignore[misc]
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning,
|
||||||
|
match="Setting attribute 'tool' on ToolCallRequest is deprecated",
|
||||||
|
):
|
||||||
|
request.tool = None # type: ignore[misc]
|
||||||
|
|
||||||
|
# state is now a property, so setting it will raise a deprecation warning
|
||||||
|
# (and then fail with AttributeError after the warning)
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning,
|
||||||
|
match="Setting attribute 'state' on ToolCallRequest is deprecated",
|
||||||
|
):
|
||||||
|
with pytest.raises(AttributeError):
|
||||||
|
request.state = {} # type: ignore[misc]
|
||||||
|
|
||||||
|
with pytest.warns(
|
||||||
|
DeprecationWarning,
|
||||||
|
match="Setting attribute 'runtime' on ToolCallRequest is deprecated",
|
||||||
|
):
|
||||||
|
request.runtime = None # type: ignore[misc]
|
||||||
|
|
||||||
|
# Test that override method works correctly
|
||||||
|
new_tool_call: ToolCall = {
|
||||||
|
"name": "multiply",
|
||||||
|
"args": {"x": 5, "y": 10},
|
||||||
|
"id": "call_3",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Original request should be unchanged (note: it was modified by the warnings tests above)
|
||||||
|
# So we create a fresh request to test override properly
|
||||||
|
fresh_request = ToolCallRequest(
|
||||||
|
tool_call=tool_call, tool=add, runtime=tool_runtime
|
||||||
|
)
|
||||||
|
fresh_new_request = fresh_request.override(tool_call=new_tool_call)
|
||||||
|
|
||||||
|
# Original request should be unchanged
|
||||||
|
assert fresh_request.tool_call == tool_call
|
||||||
|
assert fresh_request.tool_call["name"] == "add"
|
||||||
|
|
||||||
|
# New request should have the updated tool_call
|
||||||
|
assert fresh_new_request.tool_call == new_tool_call
|
||||||
|
assert fresh_new_request.tool_call["name"] == "multiply"
|
||||||
|
assert fresh_new_request.tool == add # Other fields should remain the same
|
||||||
|
assert fresh_new_request.state == state
|
||||||
|
assert fresh_new_request.runtime is tool_runtime
|
||||||
|
|||||||
@@ -1610,3 +1610,74 @@ def test_tool_node_stream_writer() -> None:
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_tool_call_request_setattr_deprecation_warning():
|
||||||
|
"""Test that ToolCallRequest raises a deprecation warning on direct attribute modification."""
|
||||||
|
import warnings
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from langgraph.prebuilt.tool_node import ToolCallRequest, ToolRuntime
|
||||||
|
|
||||||
|
# Create a mock ToolCall
|
||||||
|
tool_call = {"name": "test", "args": {"a": 1}, "id": "call_1", "type": "tool_call"}
|
||||||
|
|
||||||
|
# Create a ToolRuntime
|
||||||
|
tool_runtime = ToolRuntime(
|
||||||
|
state={"messages": []},
|
||||||
|
config={},
|
||||||
|
context=None,
|
||||||
|
store=None,
|
||||||
|
stream_writer=Mock(),
|
||||||
|
tool_call_id="call_1",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a ToolCallRequest
|
||||||
|
request = ToolCallRequest(
|
||||||
|
tool_call=tool_call,
|
||||||
|
tool=None,
|
||||||
|
runtime=tool_runtime,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test 1: Direct attribute assignment should raise deprecation warning but still work
|
||||||
|
with pytest.warns(DeprecationWarning, match="deprecated.*override"):
|
||||||
|
request.tool_call = {"name": "other", "args": {}, "id": "call_2"}
|
||||||
|
|
||||||
|
# Verify the attribute was actually modified
|
||||||
|
assert request.tool_call == {"name": "other", "args": {}, "id": "call_2"}
|
||||||
|
|
||||||
|
# Reset for further tests
|
||||||
|
with warnings.catch_warnings():
|
||||||
|
warnings.simplefilter("ignore")
|
||||||
|
request.tool_call = tool_call
|
||||||
|
|
||||||
|
# Test 2: override method should work without warnings
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
new_tool_call = {
|
||||||
|
"name": "new_tool",
|
||||||
|
"args": {"b": 2},
|
||||||
|
"id": "call_3",
|
||||||
|
"type": "tool_call",
|
||||||
|
}
|
||||||
|
new_request = request.override(tool_call=new_tool_call)
|
||||||
|
|
||||||
|
# Verify no warning was raised
|
||||||
|
assert len(w) == 0
|
||||||
|
|
||||||
|
# Verify original is unchanged
|
||||||
|
assert request.tool_call == tool_call
|
||||||
|
|
||||||
|
# Verify new request has updated values
|
||||||
|
assert new_request.tool_call == new_tool_call
|
||||||
|
|
||||||
|
# Test 3: Initialization should not trigger warning
|
||||||
|
with warnings.catch_warnings(record=True) as w:
|
||||||
|
warnings.simplefilter("always")
|
||||||
|
ToolCallRequest(
|
||||||
|
tool_call=tool_call,
|
||||||
|
tool=None,
|
||||||
|
runtime=tool_runtime,
|
||||||
|
)
|
||||||
|
# Verify no warning was raised during initialization
|
||||||
|
assert len(w) == 0
|
||||||
|
|||||||
Reference in New Issue
Block a user