mirror of
https://github.com/langchain-ai/langgraph.git
synced 2026-09-17 15:17:57 +02:00
183 lines
7.3 KiB
Python
183 lines
7.3 KiB
Python
"""Test reserved keywords for tool injection."""
|
|
from typing import Annotated, List
|
|
from langchain_core.messages import AIMessage, AnyMessage
|
|
from langgraph.prebuilt import ToolNode, InjectedState, InjectedStore
|
|
from langgraph.store.memory import InMemoryStore
|
|
from langgraph.store.base import BaseStore
|
|
from langgraph.graph import MessagesState
|
|
|
|
|
|
def test_tool_node_inject_runtime_reserved_keyword() -> None:
|
|
"""Test that tools can use 'runtime' as a reserved keyword parameter."""
|
|
from langgraph.runtime import Runtime
|
|
|
|
def tool1(some_val: int, runtime) -> str:
|
|
"""Tool 1 with reserved keyword 'runtime'."""
|
|
assert isinstance(runtime, Runtime), "runtime should be a Runtime instance"
|
|
# Access store from runtime
|
|
if runtime.store:
|
|
store_val = runtime.store.get(("test",), "test_key")
|
|
if store_val:
|
|
return f"val: {some_val}, store: {store_val.value['foo']}"
|
|
return f"val: {some_val}, no store"
|
|
|
|
def tool2(some_val: int, runtime) -> str:
|
|
"""Tool 2 with reserved keyword 'runtime'."""
|
|
assert isinstance(runtime, Runtime), "runtime should be a Runtime instance"
|
|
# Access context from runtime
|
|
if runtime.context:
|
|
return f"val: {some_val}, context: {runtime.context.get('user_id', 'unknown')}"
|
|
return f"val: {some_val}, no context"
|
|
|
|
def tool3(x: int, y: str, runtime) -> str:
|
|
"""Tool 3 with reserved keyword 'runtime' and other params."""
|
|
assert isinstance(runtime, Runtime), "runtime should be a Runtime instance"
|
|
has_store = "yes" if runtime.store else "no"
|
|
has_context = "yes" if runtime.context else "no"
|
|
return f"x: {x}, y: {y}, store: {has_store}, context: {has_context}"
|
|
|
|
store = InMemoryStore()
|
|
store.put(("test",), "test_key", {"foo": "bar"})
|
|
|
|
node = ToolNode([tool1, tool2, tool3])
|
|
|
|
# Verify that 'runtime' is excluded from tool schemas
|
|
for tool in [tool1, tool2, tool3]:
|
|
schema = node.tools_by_name[tool.__name__].get_input_schema()
|
|
if hasattr(schema, 'model_fields'):
|
|
assert "runtime" not in schema.model_fields, f"'runtime' should be excluded from {tool.__name__} schema"
|
|
else:
|
|
assert "runtime" not in schema.__fields__, f"'runtime' should be excluded from {tool.__name__} schema"
|
|
|
|
# Test with store
|
|
tool_call = {
|
|
"name": "tool1",
|
|
"args": {"some_val": 1},
|
|
"id": "some 0",
|
|
"type": "tool_call",
|
|
}
|
|
msg = AIMessage("hi?", tool_calls=[tool_call])
|
|
result = node.invoke({"messages": [msg]}, store=store)
|
|
tool_message = result["messages"][-1]
|
|
assert tool_message.content == "val: 1, store: bar"
|
|
|
|
# Test with context
|
|
from langchain_core.runnables import RunnableConfig
|
|
config = RunnableConfig(configurable={"context": {"user_id": "test_user"}})
|
|
|
|
tool_call = {
|
|
"name": "tool2",
|
|
"args": {"some_val": 2},
|
|
"id": "some 1",
|
|
"type": "tool_call",
|
|
}
|
|
msg = AIMessage("hi?", tool_calls=[tool_call])
|
|
result = node.invoke({"messages": [msg]}, config=config)
|
|
tool_message = result["messages"][-1]
|
|
assert tool_message.content == "val: 2, context: test_user"
|
|
|
|
# Test with both store and context
|
|
tool_call = {
|
|
"name": "tool3",
|
|
"args": {"x": 3, "y": "test"},
|
|
"id": "some 2",
|
|
"type": "tool_call",
|
|
}
|
|
msg = AIMessage("hi?", tool_calls=[tool_call])
|
|
result = node.invoke({"messages": [msg]}, store=store, config=config)
|
|
tool_message = result["messages"][-1]
|
|
assert tool_message.content == "x: 3, y: test, store: yes, context: yes"
|
|
|
|
|
|
def test_tool_node_mixed_injection_styles() -> None:
|
|
"""Test that tools can mix reserved keywords and annotations."""
|
|
from langgraph.runtime import Runtime
|
|
|
|
def tool1(some_val: int, state) -> str:
|
|
"""Tool with reserved keyword 'state'."""
|
|
if isinstance(state, dict):
|
|
return f"reserved state: {state['foo']}"
|
|
else:
|
|
return f"reserved state: {getattr(state, 'foo')}"
|
|
|
|
def tool2(some_val: int, state: Annotated[dict, InjectedState]) -> str:
|
|
"""Tool with annotation-based state injection."""
|
|
return f"annotated state: {state['foo']}"
|
|
|
|
def tool3(some_val: int, runtime) -> str:
|
|
"""Tool with reserved keyword 'runtime'."""
|
|
assert isinstance(runtime, Runtime)
|
|
return f"reserved runtime: {runtime.context.get('user_id', 'none') if runtime.context else 'none'}"
|
|
|
|
def tool4(some_val: int, store: Annotated[BaseStore, InjectedStore()]) -> str:
|
|
"""Tool with annotation-based store injection."""
|
|
store_val = store.get(("test",), "test_key")
|
|
return f"annotated store: {store_val.value['foo'] if store_val else 'none'}"
|
|
|
|
def tool5(x: int, state, runtime) -> str:
|
|
"""Tool with both reserved keywords."""
|
|
assert isinstance(runtime, Runtime)
|
|
if isinstance(state, dict):
|
|
return f"both: state={state['foo']}, runtime={runtime.context.get('user_id', 'none') if runtime.context else 'none'}"
|
|
else:
|
|
return f"both: state={getattr(state, 'foo')}, runtime={runtime.context.get('user_id', 'none') if runtime.context else 'none'}"
|
|
|
|
store = InMemoryStore()
|
|
store.put(("test",), "test_key", {"foo": "bar"})
|
|
|
|
node = ToolNode([tool1, tool2, tool3, tool4, tool5])
|
|
|
|
# Verify schemas exclude injected parameters
|
|
for tool_name, expected_excluded in [
|
|
("tool1", ["state"]),
|
|
("tool2", ["state"]),
|
|
("tool3", ["runtime"]),
|
|
("tool4", ["store"]),
|
|
("tool5", ["state", "runtime"]),
|
|
]:
|
|
schema = node.tools_by_name[tool_name].get_input_schema()
|
|
if hasattr(schema, 'model_fields'):
|
|
fields = schema.model_fields
|
|
else:
|
|
fields = schema.__fields__
|
|
for param in expected_excluded:
|
|
assert param not in fields, f"'{param}' should be excluded from {tool_name} schema"
|
|
|
|
from langchain_core.runnables import RunnableConfig
|
|
config = RunnableConfig(configurable={"context": {"user_id": "test_user"}})
|
|
|
|
# Test each tool
|
|
test_cases = [
|
|
("tool1", {"some_val": 1}, "reserved state: baz"),
|
|
("tool2", {"some_val": 2}, "annotated state: baz"),
|
|
("tool3", {"some_val": 3}, "reserved runtime: test_user"),
|
|
("tool4", {"some_val": 4}, "annotated store: bar"),
|
|
("tool5", {"x": 5}, "both: state=baz, runtime=test_user"),
|
|
]
|
|
|
|
for tool_name, args, expected in test_cases:
|
|
tool_call = {
|
|
"name": tool_name,
|
|
"args": args,
|
|
"id": f"id_{tool_name}",
|
|
"type": "tool_call",
|
|
}
|
|
msg = AIMessage("test", tool_calls=[tool_call])
|
|
result = node.invoke({"messages": [msg], "foo": "baz"}, store=store, config=config)
|
|
tool_message = result["messages"][-1]
|
|
assert tool_message.content == expected, f"Failed for {tool_name}: got {tool_message.content}, expected {expected}"
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Testing runtime reserved keyword...")
|
|
test_tool_node_inject_runtime_reserved_keyword()
|
|
print("✓ Runtime reserved keyword test passed!")
|
|
|
|
print("\nTesting mixed injection styles...")
|
|
test_tool_node_mixed_injection_styles()
|
|
print("✓ Mixed injection styles test passed!")
|
|
|
|
print("\nAll tests passed!")
|
|
|
|
|