Tutorial Image: LangGraph Tutorial: Message Classification and Routing System - Unit 1.3 Exercise 5

LangGraph Tutorial: Message Classification and Routing System - Unit 1.3 Exercise 5

Learn how to build a dynamic message classification and routing system in LangGraph with this hands-on tutorial. Explore advanced state management, confidence-based classification, and multi-node response handling. Develop robust workflows with conditional edge routing and create flexible, scalable systems for intelligent conversation management.

Rod Rivera

๐Ÿ‡ฌ๐Ÿ‡ง Chapter

๐ŸŽฏ What You'll Learn Today

LangGraph Tutorial: Message Classification and Routing System - Unit 1.3 Exercise 5

Try It Yourself

Open In ColabDownload Notebook

๐Ÿ“ข Joint Initiative

This tutorial is part of a collaboration between AI Product Engineer and Nebius Academy.

This tutorial demonstrates how to build a message classification and routing system using LangGraph's state management and conditional routing capabilities.

Key Concepts Covered

  1. Advanced State Management with TypedDict
  2. Message Classification Strategies
  3. Conditional Edge Routing
  4. Multi-Node Response Handling
  5. Testing and Debugging Workflows
from typing import Annotated, TypedDict
!pip install langchain-core
!pip install langgraph
from langchain_core.messages import AIMessage, BaseMessage
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages

Step 1: Enhanced State Definition

We define our state structure that combines message handling with classification and confidence tracking through annotations and typed dictionaries.

Why This Matters

Advanced state management is crucial because

  1. Enables sophisticated message routing
  2. Supports confidence-based decision making
  3. Maintains clean separation of concerns
  4. Facilitates debugging and testing

Debug Tips

  1. State Management Issues:

    • Print state before and after transitions
    • Verify classification values
    • Monitor confidence scores
    • Check message list updates
class State(TypedDict):
    """Manages the state of our routing system.

    This state implementation introduces three key concepts:
    1. Annotated message lists for proper message handling
    2. Classification tracking for routing decisions
    3. Confidence scoring for uncertainty management

    Attributes:
        messages: List of conversation messages with LangGraph's add_messages
                 annotation for proper message management
        classification: Category of the current message (e.g., greeting, help)
        confidence: Confidence score for the classification (0.0 to 1.0)
    """

    messages: Annotated[list[BaseMessage], add_messages]
    classification: str
    confidence: float

Step 2: Message Classification Implementation

The classifier node implements our core message analysis logic with proper state management and confidence scoring.

Debug Tips

  1. Classification Issues:

    • Log all classification decisions
    • Track confidence distribution
    • Monitor unknown classifications
    • Test edge cases thoroughly
def classifier_node(state: State) -> State:
    """Classifies incoming messages into categories.

    This node demonstrates several advanced concepts:
    1. Defensive programming with empty message handling
    2. Confidence-based classification
    3. Fallback mechanisms for unknown inputs
    4. Structured state updates

    Args:
        state: Current system state containing messages

    Returns:
        Updated state with classification and confidence
    """
    if not state["messages"]:
        return {
            "messages": state["messages"],
            "classification": "unknown",
            "confidence": 0.0,
        }

    message = state["messages"][-1].content.lower()

    # Classification logic
    if "hello" in message or "hi" in message:
        return {
            "messages": state["messages"],
            "classification": "greeting",
            "confidence": 0.9,
        }
    elif "help" in message or "support" in message:
        return {
            "messages": state["messages"],
            "classification": "help",
            "confidence": 0.8,
        }
    elif "bye" in message or "goodbye" in message:
        return {
            "messages": state["messages"],
            "classification": "farewell",
            "confidence": 0.9,
        }

    return {
        "messages": state["messages"],
        "classification": "unknown",
        "confidence": 0.1,
    }

Step 3: Response Node Implementation

We implement specialized nodes for different types of responses, maintaining clean separation of concerns.

Why This Matters

Specialized response nodes are crucial because

  1. Enable targeted response generation
  2. Maintain clean separation of concerns
  3. Facilitate response customization
  4. Support easy system expansion

Debug Tips

  1. Response Generation:

    • Verify state preservation
    • Check message formatting
    • Monitor response appropriateness
def response_node_1(state: State) -> State:
    """Handles greeting messages with appropriate responses.

    Args:
        state: Current state with classification

    Returns:
        State with greeting response added
    """
    return {
        "messages": [AIMessage(content="Hello! How can I assist you today?")],
        "classification": state["classification"],
        "confidence": state["confidence"],
    }
def response_node_2(state: State) -> State:
    """Handles help requests with supportive responses.

    Args:
        state: Current state with classification

    Returns:
        State with help response added
    """
    return {
        "messages": [
            AIMessage(content="I'm here to help! What do you need assistance with?")
        ],
        "classification": state["classification"],
        "confidence": state["confidence"],
    }
def response_node_3(state: State) -> State:
    """Handles unknown or low confidence cases with clarification requests.

    Args:
        state: Current state with classification

    Returns:
        State with clarification response added
    """
    return {
        "messages": [
            AIMessage(
                content="I'm not quite sure what you mean. Could you please rephrase that?"
            )
        ],
        "classification": state["classification"],
        "confidence": state["confidence"],
    }

Step 4: Routing Logic Implementation

The routing function determines message flow based on classification and confidence.

Why This Matters

Proper routing logic is crucial because

  1. Ensures appropriate response selection
  2. Handles uncertainty gracefully
  3. Maintains system flexibility
  4. Supports easy extension
def get_next_node(state: State) -> str:
    """Routes messages to appropriate response nodes.

    This function demonstrates LangGraph's conditional routing by:
    1. Evaluating confidence thresholds
    2. Mapping classifications to handlers
    3. Implementing fallback logic

    Args:
        state: Current state with classification and confidence

    Returns:
        str: Name of the next node to route to
    """
    if state["confidence"] > 0.7:
        classification_routes = {"greeting": "response_1", "help": "response_2"}
        return classification_routes.get(state["classification"], "response_3")

    return "response_3"

Step 5: Graph Construction and Usage

We construct our graph with proper node connections and conditional edges.

Common Pitfalls

  1. Not handling all possible classifications
  2. Missing confidence threshold checks
  3. Incomplete route mappings
  4. Poor fallback handling
Create and configure the graph

graph = StateGraph(State)

graph.add_node("classifier", classifier_node)
graph.add_node("response_1", response_node_1)
graph.add_node("response_2", response_node_2)
graph.add_node("response_3", response_node_3)
Configure routing
graph.add_edge(START, "classifier")
graph.add_conditional_edges(
    "classifier",
    get_next_node,
    {
        "response_1": "response_1",
        "response_2": "response_2",
        "response_3": "response_3",
    },
)
Add terminal edges
graph.add_edge("response_1", END)
graph.add_edge("response_2", END)
graph.add_edge("response_3", END)
Compile the graph

chain = graph.compile()

Testing and Usage Example

Here we demonstrate the system with various test cases.

def process_message(message: str) -> None:
    """Process a message through the routing system.

    Args:
        message: Input message to process
    """
    initial_state = {
        "messages": [AIMessage(content=message)],
        "classification": "",
        "confidence": 0.0,
    }

    result = chain.invoke(initial_state)

    print(f"\nInput: {message}")
    print("Response:", result["messages"][-1].content)
    print(
        f"Classification: {result['classification']} (confidence: {result['confidence']})"
    )

if __name__ == "__main__":
    print("Testing the Message Routing System:")
    print("==================================")
    test_messages = [
        "Hello there!",  # Should trigger greeting response

        "I need help with something",  # Should trigger help response

        "What's the weather like?",  # Should trigger unknown response

        "goodbye",  # Should trigger farewell response

    ]
    ## for message in test_messages
    process_message(message)

Key Takeaways

  1. State Design: Proper state management enables sophisticated workflows
  2. Classification: Confidence-based classification supports robust routing
  3. Response Handling: Specialized nodes maintain clean separation of concerns
  4. Routing Logic: Conditional edges create flexible conversation paths

Next Steps

  1. Implement ML-based classification
  2. Add conversation history management
  3. Enhance response generation
  4. Add error recovery mechanisms
  5. Implement conversation analytics