๐ฏ What You'll Learn Today
LangGraph Tutorial: Enhanced State Management for Multi-Tool Agents - Unit 2.2 Exercise 1
This tutorial is also available in Google Colab here or for download here
Joint Initiative: This tutorial is part of a collaboration between AI Product Engineer and the 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_messages
Step 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."""
pass
Step 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 < limit
def 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_state
Step 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 status
def 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
๐ฌ๐ง Chapter