Library with high-level APIs for creating and executing LangGraph agents and tools.
Annotations for injecting graph state and persistent storage into tool arguments, enabling context-aware tools without exposing internal state management to the language model.
Annotation for injecting graph state into tool arguments. Enables tools to access graph state without exposing state management details to the language model.
class InjectedState(InjectedToolArg):
def __init__(self, field: Optional[str] = None) -> NoneParameters:
field: Optional key to extract from the state dictionary. If None, the entire state is injected.Usage Examples:
from typing import List
from typing_extensions import Annotated, TypedDict
from langchain_core.messages import BaseMessage
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedState, ToolNode
class AgentState(TypedDict):
messages: List[BaseMessage]
user_id: str
session_data: dict
# Inject entire state
@tool
def state_aware_tool(
query: str,
state: Annotated[dict, InjectedState]
) -> str:
"""Tool that accesses full graph state."""
user_id = state.get("user_id", "unknown")
message_count = len(state.get("messages", []))
if message_count > 5:
return f"Continuing conversation for user {user_id}: {query}"
else:
return f"Starting fresh conversation for user {user_id}: {query}"
# Inject specific state field
@tool
def user_specific_tool(
action: str,
user_id: Annotated[str, InjectedState("user_id")]
) -> str:
"""Tool that accesses specific state field."""
return f"Performing {action} for user {user_id}"
# Create tool node
tool_node = ToolNode([state_aware_tool, user_specific_tool])
# Example state
state = {
"messages": [BaseMessage(content="Hello"), BaseMessage(content="How are you?")],
"user_id": "user_123",
"session_data": {"theme": "dark", "language": "en"}
}
# Tool calls with state injection
tool_calls = [
{"name": "state_aware_tool", "args": {"query": "weather"}, "id": "1", "type": "tool_call"},
{"name": "user_specific_tool", "args": {"action": "save_preference"}, "id": "2", "type": "tool_call"}
]
# State is automatically injected
result = tool_node.invoke({"messages": [...], **state})Annotation for injecting persistent store into tool arguments. Enables tools to access LangGraph's persistent storage system for cross-session data persistence.
class InjectedStore(InjectedToolArg):
passUsage Examples:
from typing_extensions import Annotated
from langchain_core.tools import tool
from langgraph.prebuilt import InjectedStore, ToolNode
from langgraph.store.base import BaseStore
@tool
def save_user_preference(
key: str,
value: str,
user_id: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""Save user preference to persistent storage."""
namespace = ("user_preferences", user_id)
store.put(namespace, key, value)
return f"Saved {key} = {value} for user {user_id}"
@tool
def get_user_preference(
key: str,
user_id: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""Retrieve user preference from persistent storage."""
namespace = ("user_preferences", user_id)
result = store.get(namespace, key)
return result.value if result else f"No preference found for {key}"
@tool
def list_user_data(
user_id: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""List all stored data for a user."""
namespace = ("user_preferences", user_id)
items = store.search(namespace)
if items:
return f"Stored preferences: {', '.join(item.key for item in items)}"
return "No stored preferences found"
# Usage with graph compilation
from langgraph.graph import StateGraph
from langgraph.store.memory import InMemoryStore
store = InMemoryStore()
tool_node = ToolNode([save_user_preference, get_user_preference, list_user_data])
graph = StateGraph(AgentState)
graph.add_node("tools", tool_node)
compiled_graph = graph.compile(store=store) # Store is injected automatically@tool
def adaptive_tool(
query: str,
state: Annotated[dict, InjectedState]
) -> str:
"""Tool that adapts behavior based on state."""
messages = state.get("messages", [])
user_tier = state.get("user_tier", "basic")
# Adapt behavior based on conversation length
if len(messages) < 3:
response_style = "detailed"
elif len(messages) > 10:
response_style = "concise"
else:
response_style = "balanced"
# Adapt features based on user tier
advanced_features = user_tier in ["premium", "enterprise"]
return f"Processing '{query}' with {response_style} style, advanced features: {advanced_features}"@tool
def context_rich_tool(
task: str,
user_id: Annotated[str, InjectedState("user_id")],
session_data: Annotated[dict, InjectedState("session_data")],
messages: Annotated[list, InjectedState("messages")]
) -> str:
"""Tool with multiple injected state fields."""
user_language = session_data.get("language", "en")
conversation_length = len(messages)
return f"Task: {task} | User: {user_id} | Language: {user_language} | Messages: {conversation_length}"@tool
def save_conversation_summary(
summary: str,
conversation_id: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""Save conversation summary with organized namespacing."""
namespace = ("conversations", "summaries")
store.put(namespace, conversation_id, {
"summary": summary,
"created_at": datetime.now().isoformat(),
"version": "1.0"
})
return f"Saved summary for conversation {conversation_id}"
@tool
def get_user_history(
user_id: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""Retrieve user's conversation history."""
namespace = ("users", user_id, "history")
items = store.search(namespace)
return f"Found {len(items)} historical items for user {user_id}"@tool
def update_global_stats(
action: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
"""Update global application statistics."""
namespace = ("global", "stats")
current_stats = store.get(namespace, "counters")
if current_stats:
counters = current_stats.value
else:
counters = {}
counters[action] = counters.get(action, 0) + 1
store.put(namespace, "counters", counters)
return f"Updated {action} count to {counters[action]}"ToolNode automatically handles injection during tool execution:
from langgraph.prebuilt import ToolNode
# Tools with injected dependencies
tools_with_injection = [
save_user_preference,
get_user_preference,
state_aware_tool,
context_rich_tool
]
tool_node = ToolNode(tools_with_injection)
# Injection happens automatically during invoke()
state = {
"messages": [...],
"user_id": "user_123",
"session_data": {"language": "en", "theme": "dark"}
}
# Both state and store are injected as needed
result = tool_node.invoke(state, store=my_store)For custom routing or Send API usage:
# Manually inject arguments for custom routing
tool_call = {
"name": "save_user_preference",
"args": {"key": "theme", "value": "dark", "user_id": "user_123"},
"id": "tool_1",
"type": "tool_call"
}
# Inject state and store
injected_call = tool_node.inject_tool_args(tool_call, state, store)
# Use with Send API
from langgraph.types import Send
def custom_router(state):
tool_calls = state["messages"][-1].tool_calls
injected_calls = [
tool_node.inject_tool_args(call, state, store)
for call in tool_calls
]
return [Send("tools", call) for call in injected_calls]# Error occurs if tool requires store injection but none provided
@tool
def requires_store_tool(
data: str,
store: Annotated[BaseStore, InjectedStore()]
) -> str:
return store.get(("data",), "key").value
tool_node = ToolNode([requires_store_tool])
# This will raise ValueError about missing store
try:
result = tool_node.invoke(state) # No store provided
except ValueError as e:
print(f"Error: {e}") # "Cannot inject store into tools with InjectedStore annotations"@tool
def requires_field_tool(
query: str,
user_id: Annotated[str, InjectedState("user_id")]
) -> str:
return f"Query for user {user_id}: {query}"
# If state doesn't contain "user_id" field, KeyError occurs
state_without_user_id = {"messages": []}
try:
result = tool_node.invoke(state_without_user_id)
except KeyError as e:
print(f"Missing required state field: {e}")# Good: Clear, typed state schema
class ApplicationState(TypedDict):
messages: List[BaseMessage]
user_id: str
session_metadata: dict
preferences: dict
# Avoid: Overly nested or unclear state structure# Good: Consistent namespacing strategy
namespace = ("domain", "entity_type", "entity_id")
store.put(("users", "preferences", user_id), "theme", "dark")
store.put(("conversations", "summaries", conv_id), "summary", text)
# Good: Version your stored data
data = {"value": content, "version": 1, "created_at": timestamp}# Good: Clear parameter separation
@tool
def well_designed_tool(
user_input: str, # From model
user_id: Annotated[str, InjectedState("user_id")], # From state
store: Annotated[BaseStore, InjectedStore()] # From system
) -> str:
"""Clear separation of concerns."""
pass
# Avoid: Mixing injected and model parameters confusinglyInstall with Tessl CLI
npx tessl i tessl/pypi-langgraph-prebuilt