๐ฏ What You'll Learn Today
LangGraph Tutorial: Tool Execution with Configuration Management - Unit 2.1 Exercise 3
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 tool execution in LangGraph with proper configuration management and API key handling. We combine robust tool execution with secure configuration management.
Key Concepts Covered
- Configuration Management
- Tool Execution Flow
- Error Handling
- State Management
import json
import os
from typing import Annotated, Any, TypedDict
!pip install langchain-core
!pip install langgraph
!pip install pydantic-settings
from langchain_community.tools import TavilySearchResults
from langchain_core.messages import BaseMessage
from langgraph.graph.message import add_messages
from pydantic_settings import BaseSettings
Step 1: Configuration Setup
We establish our configuration management using Pydantic.
Why This Matters
Proper configuration management is crucial because
- Ensures secure API key handling
- Provides consistent tool setup
- Supports different environments
- Enables easy testing
Debug Tips
-
Configuration Issues:
- Verify environment variables
- Check settings initialization
- Monitor API key handling
- Test fallback behavior
MOCK_TAVILY_KEY = "mock_tavily_api_key_12345"
class Settings(BaseSettings):
"""Configuration management for tool execution.
This class manages tool configuration with:
1. API key handling
2. Environment variable support
3. Default value management
Attributes:
tavily_api_key: API key for Tavily integration
"""
tavily_api_key: str = MOCK_TAVILY_KEY
Step 2: State Definition
We define our state structure for tool execution.
Why This Matters
Proper state structure is crucial because
- Maintains execution context
- Tracks tool operations
- Supports error recovery
- Enables debugging
class State(TypedDict):
"""State container for tool execution.
This state implementation tracks:
1. Message history with special handling
2. Tool call specifications
3. Tool execution results
Attributes:
messages: Conversation history with proper annotation
tool_calls: Tool call specifications
tool_outputs: Results from tool executions
"""
messages: Annotated[list[BaseMessage], add_messages]
tool_calls: list[dict]
tool_outputs: list[Any]
Step 3: Tool Executor Implementation
We implement the tool execution logic with proper configuration handling.
Why This Matters
Robust tool execution is crucial because
- Ensures reliable operations
- Handles errors gracefully
- Maintains consistent output
- Supports debugging
def setup_tool_environment():
"""Initialize the tool environment with configuration.
Returns:
tuple: Settings and tool instance
"""
settings = Settings()
os.environ["TAVILY_API_KEY"] = settings.tavily_api_key
tavily_tool = TavilySearchResults()
return settings, tavily_tool
settings = Settings()
os.environ["TAVILY_API_KEY"] = settings.tavily_api_key
tavily_tool = TavilySearchResults()
return settings, tavily_tool
def tool_executor(state: State) -> State:
"""Execute tools with proper configuration and error handling.
Args:
state: Current system state with tool calls
Returns:
Updated state with tool outputs
"""
if not state.get("tool_calls"):
return {"tool_outputs": []}
# Setup tool environment
_, tavily_tool = setup_tool_environment()
tool_call = state["tool_calls"][-1]
try:
if tool_call["tool_name"] == "TavilySearchResults":
output = tavily_tool.invoke(tool_call["args"])
return {"tool_outputs": [json.dumps(output)]}
except Exception as e:
return {"tool_outputs": [json.dumps({"error": str(e)})]}
return {"tool_outputs": []}
Step 4: Usage Demonstration
Example showing the complete tool execution flow.
Debug Tips
-
Testing Flow:
- Verify configuration loading
- Check tool initialization
- Monitor execution results
- Validate error handling
def demonstrate_tool_execution():
"""Demonstrates the complete tool execution workflow."""
# Initialize test state
initial_state = {
"tool_calls": [
{"tool_name": "TavilySearchResults", "args": {"query": "capital of France"}}
],
"messages": [],
"tool_outputs": [],
}
# Execute tool
result = tool_executor(initial_state)
Display results
print("Tool Execution Results:")
print(f"Tool output received: {bool(result['tool_outputs'])}")
if result["tool_outputs"]:
print("Output structure:", json.dumps(result["tool_outputs"][0], indent=2))
initial_state = {
"tool_calls": [
{"tool_name": "TavilySearchResults", "args": {"query": "capital of France"}}
],
"messages": [],
"tool_outputs": [],
}
Execute tool
result = tool_executor(initial_state)
Display results
print("Tool Execution Results:")
print(f"Tool output received: {bool(result['tool_outputs'])}")
if result["tool_outputs"]:
print("Output structure:", json.dumps(result["tool_outputs"][0], indent=2))
if __name__ == "__main__":
demonstrate_tool_execution()
Common Pitfalls
- Missing API key configuration
- Improper environment setup
- Incorrect error handling
- Poor state management
Key Takeaways
- Configuration: Proper settings management is essential
- Tool Setup: Clean initialization process
- Execution: Robust error handling
- State: Consistent state management
Next Steps
- Implement real API key management
- Add comprehensive error handling
- Enhance configuration options
- Add execution logging
- Implement security measures
Expected Output
Tool Execution Results
Tool output received: True
Output structure: {
"query": "capital of France",
"results": [...]
}
๐ฌ๐ง Chapter