Tutorial Image: LangGraph Tutorial: Direct Tool Execution System - Unit 2.2 Exercise 5

LangGraph Tutorial: Direct Tool Execution System - Unit 2.2 Exercise 5

Learn how to build a direct tool execution system in LangGraph with safe mathematical evaluations, robust error handling, and state management. This tutorial guides you through defining tools, implementing an execution engine, and testing with various scenarios to ensure reliability and scalability.

๐ŸŽฏ What You'll Learn Today

LangGraph Tutorial: Direct Tool Execution System - Unit 2.2 Exercise 5

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 and execute tools directly in LangGraph, focusing on safe execution, state management, and proper error handling.

Key Concepts Covered

  1. Tool Definition and Decoration
  2. Safe Mathematical Evaluation
  3. State Management
  4. Error Handling and Type Safety
import uuid
from typing import Annotated, Any, TypedDict
#!pip install langchain-core
#!pip install langgraph
import numexpr
from langchain_core.messages import AIMessage, BaseMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.graph.message import add_messages

Step 1: State Definition

Define the state structure for tool execution tracking.

Why This Matters

Proper state structure is crucial because

  1. Tracks tool execution history
  2. Maintains conversation context
  3. Enables proper error handling

Debug Tips

  1. State Structure Issues:

    • Verify TypedDict fields initialization
    • Check message list structure
    • Monitor tool output tracking
class State(TypedDict):
    """State container for tool execution system.

    Attributes:
        messages: Conversation history
        tool_name: Active tool identifier
        tool_outputs: Results from executions
        tool_input: Current tool input
    """

    messages: Annotated[list[BaseMessage], add_messages]
    tool_name: str | None
    tool_outputs: list[str]
    tool_input: str | None

Step 2: Tool Implementation

Create safe, well-documented tools with proper decorators.

Why This Matters

Tool implementation is critical because

  1. Ensures safe execution
  2. Provides clear documentation
  3. Maintains type safety

Debug Tips

  1. Tool Implementation Issues:

    • Check error handling
    • Verify input validation
    • Monitor return types
@tool
def calculator(expression: str) -> str:
    """Safely evaluate mathematical expressions.

    Args:
        expression: Mathematical expression to evaluate

    Returns:
        String representation of result

    Examples:
        >>> calculator("2 + 2")
        '4'
    """
    try:
        result = numexpr.evaluate(expression.strip()).item()
        return str(result)
    except Exception as e:
        return f"Error evaluating expression: {e!s}"
@tool
def weather_check(location: str) -> str:
    """Simulate weather information retrieval.

    Args:
        location: Location name

    Returns:
        Simulated weather data
    """
    return f"Weather in {location}: 22ยฐC, Sunny"

Step 3: Tool Execution Engine

Implement the core tool execution logic with proper error handling.

Why This Matters

Execution engine is essential because

  1. Manages tool lifecycle
  2. Handles errors gracefully
  3. Updates state properly

Debug Tips

  1. Execution Issues:

    • Log tool selection
    • Monitor state updates
    • Track error handling
def execute_direct_tool(state: State, tools: dict[str, Any]) -> State:
    """Execute selected tool and update state.

    Args:
        state: Current system state
        tools: Available tool mapping

    Returns:
        Updated state with execution results
    """
    if not state.get("tool_name") or not state.get("tool_input"):
        return {**state, "tool_outputs": []}

    tool_name = state["tool_name"]
    tool_input = state["tool_input"]

    tool = tools.get(tool_name)
    if not tool:
        error_msg = f"Tool '{tool_name}' not found"
        return {
            **state,
            "tool_outputs": [error_msg],
            "messages": state.get("messages", []) + [AIMessage(content=error_msg)],
        }

    try:
        output = tool.invoke(tool_input)
        tool_message = ToolMessage(
            content=output, tool_call_id=str(uuid.uuid4()), name=tool_name
        )
        ai_message = AIMessage(content=f"Tool execution result: {output}")

        return {
            **state,
            "tool_outputs": [output],
            "messages": state.get("messages", []) + [tool_message, ai_message],
        }

    except Exception as e:
        error_msg = f"Error executing {tool_name}: {e!s}"
        return {
            **state,
            "tool_outputs": [error_msg],
            "messages": state.get("messages", []) + [AIMessage(content=error_msg)],
        }

Step 4: System Demonstration

Create comprehensive test cases for the tool execution system.

Why This Matters

System demonstration is valuable because

  1. Verifies functionality
  2. Shows error handling
  3. Provides usage examples

Debug Tips

  1. Testing Issues:

    • Verify edge cases
    • Check error scenarios
    • Monitor state transitions
def demonstrate_tool_execution():
    """Demonstrate tool execution with various test cases."""
    tools = {"calculator": calculator, "weather": weather_check}

    test_cases = [
        {
            "tool_name": "calculator",
            "tool_input": "2 + 2",
            "description": "Simple addition",
        },
        {
            "tool_name": "calculator",
            "tool_input": "3 * (4 + 5)",
            "description": "Complex calculation",
        },
        {"tool_name": "weather", "tool_input": "Paris", "description": "Weather check"},
        {
            "tool_name": "unknown_tool",
            "tool_input": "test",
            "description": "Invalid tool",
        },
    ]

    print("Tool Execution Demonstration")
    print("===========================")

    for case in test_cases:
        print(f"\nTest: {case['description']}")

        state = {
            "messages": [],
            "tool_name": case["tool_name"],
            "tool_input": case["tool_input"],
            "tool_outputs": [],
        }

        result = execute_direct_tool(state, tools)

        print(f"Tool: {case['tool_name']}")
        print(f"Input: {case['tool_input']}")
        print(
            f"Output: {result['tool_outputs'][0] if result['tool_outputs'] else 'No output'}"
        )
        print("-" * 40)

Common Pitfalls

  1. Unsafe expression evaluation
  2. Missing error handling
  3. Improper state updates
  4. Incomplete type checking

Key Takeaways

  1. Safe tool execution requires proper validation
  2. Error handling is crucial for stability
  3. State updates must be immutable
  4. Type safety prevents runtime errors

Next Steps

  1. Add tool execution history
  2. Implement tool rate limiting
  3. Add result caching
  4. Enhance error reporting

Expected Output

Tool Execution Demonstration

Test: Simple addition
Tool: calculator
Input: 2 + 2

## Output: 4

Test: Complex calculation
Tool: calculator
Input: 3 * (4 + 5)

## Output: 27

Test: Weather check
Tool: weather
Input: Paris

## Output: Weather in Paris: 22ยฐC, Sunny

Test: Invalid tool
Tool: unknown_tool
Input: test

## Output: Tool 'unknown_tool' not found
if __name__ == "__main__":
    demonstrate_tool_execution()

Rod Rivera

๐Ÿ‡ฌ๐Ÿ‡ง Chapter