๐ฏ What You'll Learn Today
LangGraph Tutorial: Enhanced State Management for Multi-Tool Agents - Unit 2.2 Exercise 1
Try It Yourself
๐ข Joint Initiative
This tutorial is part of a collaboration between
AI Product Engineer
and
Nebius Academy
.
This tutorial demonstrates how to implement an advanced state management system for agents that can use multiple tools, with features like rate limiting and usage tracking.
Key Concepts Covered
- Advanced State Management
- Tool Usage Tracking
- Rate Limiting Implementation
- Type-Safe State Handling
from typing import Annotated, Any, TypedDict
#!pip install langchain-core
#!pip install langgraph
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messagesStep 1: State Definition
We define our enhanced state structure for multi-tool operations.
Why This Matters
Advanced state structure is crucial because
- Enables controlled tool access
- Maintains usage history
- Enforces rate limits
- Supports type safety
Debug Tips
- State Structure:
- Verify field initialization
- Check type annotations
- Monitor optional fields
- Validate tool configurations
class State(TypedDict, total=False):
"""Enhanced state container for multi-tool operations.
This state implementation uses TypedDict with total=False for:
1. Optional field support
2. Backward compatibility
3. Type safety maintenance
4. Clean state updates
Attributes:
messages: Conversation history with add_messages annotation
available_tools: List of accessible tools
tool_usage: Usage count per tool
rate_limits: Maximum uses per tool
tool_name: Currently selected tool (optional)
tool_outputs: Tool execution results (optional)
"""
messages: Annotated[list[BaseMessage], add_messages]
available_tools: list[Any]
tool_usage: dict[str, int]
rate_limits: dict[str, int]
tool_name: str | None
tool_outputs: list[str]class ToolLimitExceeded(Exception):
"""Exception for tool usage limit violations."""
passStep 2: State Initialization
We implement the state initialization logic.
Why This Matters
Proper initialization is crucial because
- Ensures consistent starting state
- Sets up tool availability
- Configures rate limits
- Enables clean tracking
Debug Tips
- Initialization:
- Verify default values
- Check rate limit setup
- Monitor tool registration
- Validate state structure
def initialize_state(
available_tools: list[str] = ["calculator", "weather"],
rate_limits: dict[str, int] | None = None,
) -> State:
"""Initialize state with tools and limits.
Args:
available_tools: Tools to make available
rate_limits: Optional tool usage limits
Returns:
Initialized State object
"""
if rate_limits is None:
rate_limits = {
"calculator": 3,
"weather": 1,
"search": 2,
}
for tool in available_tools:
if tool not in rate_limits:
rate_limits[tool] = 1
return {
"messages": [],
"available_tools": available_tools,
"tool_usage": {tool: 0 for tool in available_tools},
"rate_limits": rate_limits,
}Step 3: Tool Usage Management
We implement tool usage checking and tracking.
Why This Matters
Usage management is crucial because
- Prevents tool overuse
- Maintains rate limits
- Tracks usage patterns
- Enables monitoring
Debug Tips
- Usage Tracking:
- Monitor usage counts
- Verify limit checks
- Track state updates
- Check error handling
def can_use_tool(state: State, tool_name: str) -> bool:
"""Check tool availability based on usage and limits.
Args:
state: Current state
tool_name: Tool to check
Returns:
Whether tool can be used
"""
if tool_name not in state["available_tools"]:
raise KeyError(f"Tool '{tool_name}' is not available")
current_usage = state["tool_usage"][tool_name]
limit = state["rate_limits"][tool_name]
return current_usage < limitdef use_tool(state: State, tool_name: str) -> State:
"""Record tool usage in state.
Args:
state: Current state
tool_name: Tool being used
Returns:
Updated state
"""
if not can_use_tool(state, tool_name):
limit = state["rate_limits"][tool_name]
raise ToolLimitExceeded(
f"Tool '{tool_name}' has reached its limit of {limit} uses"
)
new_state = state.copy()
new_state["tool_usage"] = state["tool_usage"].copy()
new_state["tool_usage"][tool_name] += 1
new_state["tool_name"] = tool_name
return new_stateStep 4: Status Reporting
We implement tool status monitoring.
Why This Matters
Status reporting is crucial because
- Enables usage monitoring
- Facilitates debugging
- Supports decision making
- Maintains transparency
Debug Tips
- Status Tracking:
- Verify calculations
- Check format consistency
- Monitor updates
- Validate accessibility
def get_tool_status(state: State) -> dict[str, dict[str, Any]]:
"""Get complete tool status summary.
Args:
state: Current state
Returns:
Tool status information
"""
status = {}
for tool in state["available_tools"]:
uses_left = state["rate_limits"][tool] - state["tool_usage"][tool]
status[tool] = {
"current_usage": state["tool_usage"][tool],
"limit": state["rate_limits"][tool],
"uses_remaining": uses_left,
"available": uses_left > 0,
}
return statusdef demonstrate_usage():
"""Demonstrate the state management system."""
state = initialize_state(
available_tools=["calculator", "weather", "search"],
rate_limits={"calculator": 2, "weather": 1, "search": 3},
)
print("\nInitial tool status:")
print_tool_status(state)
try:
state = use_tool(state, "calculator")
print("\nAfter using calculator once:")
print_tool_status(state)
state = use_tool(state, "calculator")
print("\nAfter using calculator twice:")
print_tool_status(state)
state = use_tool(state, "calculator")
except ToolLimitExceeded as e:
print(f"\nError: {e}")def print_tool_status(state: State) -> None:
"""Print readable tool status."""
status = get_tool_status(state)
for tool, info in status.items():
print(
f"{tool}: {info['current_usage']}/{info['limit']} uses "
f"({info['uses_remaining']} remaining)"
)
if __name__ == "__main__":
demonstrate_usage()Common Pitfalls
- Not copying state during updates
- Missing tool validation
- Incorrect limit tracking
- Poor error handling
Key Takeaways
- State Design: TypedDict ensures type safety
- Usage Tracking: Clean tracking prevents overuse
- Rate Limiting: Proper limits maintain control
- Status Monitoring: Clear reporting enables oversight
Next Steps
- Add tool priority system
- Implement cooldown periods
- Add usage statistics
- Enhance error handling
- Add state persistence
Expected Output
Initial tool status
calculator: 0/2 uses (2 remaining)
weather: 0/1 uses (1 remaining)
search: 0/3 uses (3 remaining)After using calculator once
calculator: 1/2 uses (1 remaining)
weather: 0/1 uses (1 remaining)
search: 0/3 uses (3 remaining)
Error: Tool 'calculator' has reached its limit of 2 uses

