Skip to content
Learn Agentic AI14 min read0 views

Building a Dispatch Agent: Intelligent Route Planning and Driver Assignment

Learn how to build an AI dispatch agent that optimizes delivery routes, matches drivers to orders based on constraints like location and capacity, and handles real-time changes to the delivery schedule.

The Dispatch Optimization Problem

Dispatch is one of the hardest problems in logistics. Given a set of delivery orders with time windows, a fleet of drivers with different locations and capacities, and real-time traffic conditions, a dispatcher must assign orders to drivers and sequence stops to minimize total distance while meeting every delivery window.

Human dispatchers juggle this with experience and intuition, but they struggle as order volume grows. An AI dispatch agent combines route optimization algorithms with conversational tools, letting dispatchers interact naturally while the agent handles the computational heavy lifting.

Modeling Orders, Drivers, and Routes

Start with data models that capture the dispatch domain:

from dataclasses import dataclass, field
from datetime import datetime, time
from typing import Optional
import math

@dataclass
class DeliveryOrder:
    order_id: str
    pickup_address: str
    delivery_address: str
    pickup_lat: float
    pickup_lng: float
    delivery_lat: float
    delivery_lng: float
    weight_lbs: float
    window_start: time
    window_end: time
    priority: str = "standard"
    status: str = "pending"

@dataclass
class Driver:
    driver_id: str
    name: str
    current_lat: float
    current_lng: float
    vehicle_capacity_lbs: float
    current_load_lbs: float = 0.0
    active_orders: list[str] = field(default_factory=list)
    status: str = "available"
    shift_end: time = time(18, 0)

def haversine_miles(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
    """Calculate distance between two coordinates in miles."""
    R = 3959
    dlat = math.radians(lat2 - lat1)
    dlng = math.radians(lng2 - lng1)
    a = (math.sin(dlat / 2) ** 2 +
         math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) *
         math.sin(dlng / 2) ** 2)
    return R * 2 * math.asin(math.sqrt(a))

Driver Matching Tool

The matching algorithm scores each driver against an order based on proximity, remaining capacity, and schedule fit:

See AI Voice Agents Handle Real Calls

Book a free demo or calculate how much you can save with AI voice automation.

from agents import function_tool

DRIVERS = [
    Driver("DRV-01", "Alex Rivera", 37.7749, -122.4194, 2000.0, 350.0,
           ["ORD-101"], "active"),
    Driver("DRV-02", "Priya Sharma", 37.3382, -121.8863, 1500.0, 0.0,
           [], "available"),
    Driver("DRV-03", "Carlos Mendez", 37.5485, -122.0574, 3000.0, 1200.0,
           ["ORD-102", "ORD-103"], "active"),
]

ORDERS = [
    DeliveryOrder("ORD-201", "Warehouse A", "123 Market St", 37.5600, -122.0700,
                  37.7850, -122.4100, 250.0, time(10, 0), time(14, 0)),
    DeliveryOrder("ORD-202", "Warehouse B", "456 Mission St", 37.3400, -121.8900,
                  37.7600, -122.4300, 180.0, time(9, 0), time(12, 0), "express"),
    DeliveryOrder("ORD-203", "Warehouse A", "789 Howard St", 37.5600, -122.0700,
                  37.7820, -122.3950, 500.0, time(11, 0), time(16, 0)),
]

@function_tool
def find_best_driver(order_id: str) -> str:
    """Find the best available driver for a delivery order based on proximity and capacity."""
    order = next((o for o in ORDERS if o.order_id == order_id), None)
    if not order:
        return f"Order {order_id} not found."

    candidates = []
    for driver in DRIVERS:
        remaining_capacity = driver.vehicle_capacity_lbs - driver.current_load_lbs
        if remaining_capacity < order.weight_lbs:
            continue

        distance = haversine_miles(
            driver.current_lat, driver.current_lng,
            order.pickup_lat, order.pickup_lng,
        )
        load_ratio = driver.current_load_lbs / driver.vehicle_capacity_lbs
        order_count_penalty = len(driver.active_orders) * 2.0

        score = distance + (load_ratio * 10) + order_count_penalty
        if order.priority == "express":
            score *= 0.8 if driver.status == "available" else 1.2

        candidates.append((driver, distance, remaining_capacity, score))

    candidates.sort(key=lambda x: x[3])

    if not candidates:
        return f"No drivers available for order {order_id} ({order.weight_lbs} lbs)."

    lines = [f"Driver rankings for {order_id} ({order.weight_lbs} lbs):"]
    for driver, dist, cap, score in candidates:
        lines.append(
            f"  {driver.name} | {dist:.1f} mi away | "
            f"Capacity: {cap:.0f} lbs remaining | "
            f"Active orders: {len(driver.active_orders)} | Score: {score:.1f}"
        )
    return "\n".join(lines)

Route Optimization Tool

Once orders are assigned, the agent optimizes the stop sequence using a nearest-neighbor heuristic:

@function_tool
def optimize_route(driver_id: str, order_ids: list[str]) -> str:
    """Optimize delivery sequence for a driver using nearest-neighbor routing."""
    driver = next((d for d in DRIVERS if d.driver_id == driver_id), None)
    if not driver:
        return f"Driver {driver_id} not found."

    orders = [o for o in ORDERS if o.order_id in order_ids]
    if not orders:
        return "No valid orders provided."

    # Nearest-neighbor heuristic
    route = []
    current_lat, current_lng = driver.current_lat, driver.current_lng
    remaining = list(orders)
    total_distance = 0.0

    while remaining:
        nearest = min(
            remaining,
            key=lambda o: haversine_miles(
                current_lat, current_lng, o.pickup_lat, o.pickup_lng
            ),
        )
        pickup_dist = haversine_miles(
            current_lat, current_lng, nearest.pickup_lat, nearest.pickup_lng
        )
        delivery_dist = haversine_miles(
            nearest.pickup_lat, nearest.pickup_lng,
            nearest.delivery_lat, nearest.delivery_lng,
        )
        total_distance += pickup_dist + delivery_dist

        route.append(
            f"  {len(route)+1}. Pickup {nearest.order_id} at {nearest.pickup_address} "
            f"({pickup_dist:.1f} mi) -> Deliver to {nearest.delivery_address} "
            f"({delivery_dist:.1f} mi)"
        )
        current_lat, current_lng = nearest.delivery_lat, nearest.delivery_lng
        remaining.remove(nearest)

    lines = [
        f"Optimized route for {driver.name}:",
        *route,
        f"\nTotal distance: {total_distance:.1f} miles",
        f"Estimated time: {total_distance / 25 * 60:.0f} minutes (avg 25 mph city)",
    ]
    return "\n".join(lines)

Order Assignment Tool

@function_tool
def assign_order(order_id: str, driver_id: str) -> str:
    """Assign a delivery order to a specific driver."""
    order = next((o for o in ORDERS if o.order_id == order_id), None)
    driver = next((d for d in DRIVERS if d.driver_id == driver_id), None)

    if not order:
        return f"Order {order_id} not found."
    if not driver:
        return f"Driver {driver_id} not found."

    remaining = driver.vehicle_capacity_lbs - driver.current_load_lbs
    if order.weight_lbs > remaining:
        return (
            f"Cannot assign: {order.weight_lbs} lbs exceeds "
            f"{driver.name}'s remaining capacity of {remaining} lbs."
        )

    driver.current_load_lbs += order.weight_lbs
    driver.active_orders.append(order_id)
    driver.status = "active"
    order.status = "assigned"

    return (
        f"Order {order_id} assigned to {driver.name}. "
        f"New load: {driver.current_load_lbs}/{driver.vehicle_capacity_lbs} lbs. "
        f"Active orders: {len(driver.active_orders)}"
    )

Assembling the Dispatch Agent

from agents import Agent, Runner

dispatch_agent = Agent(
    name="Dispatch Coordinator",
    instructions="""You are an intelligent dispatch assistant. You can:
    1. Find the best driver for each order based on proximity and capacity
    2. Assign orders to drivers
    3. Optimize delivery routes for assigned orders
    Prioritize express orders. Always explain your driver recommendations.""",
    tools=[find_best_driver, assign_order, optimize_route],
)

result = Runner.run_sync(
    dispatch_agent,
    "I have three new orders: ORD-201, ORD-202, and ORD-203. Assign them to the best drivers and optimize routes."
)
print(result.final_output)

FAQ

Why use nearest-neighbor instead of a more optimal routing algorithm?

Nearest-neighbor is a greedy heuristic that runs in O(n squared) time and produces routes within 20-25 percent of optimal. For real-time dispatch where decisions must be made in seconds, it strikes a good balance. For batch optimization, use Google OR-Tools or the OSRM Trip API which implement more sophisticated algorithms like branch-and-bound or Lin-Kernighan.

How do I handle real-time order changes (cancellations, additions)?

Build a re-optimization tool that the agent calls whenever the order set changes. The tool takes the driver's current position and remaining orders, re-runs the routing algorithm, and returns an updated sequence. Use webhooks or polling to detect order state changes and trigger re-optimization automatically.

Can the agent handle multi-stop pickups where one warehouse has multiple orders?

Yes. Group orders by pickup location before routing. The tool should recognize when multiple orders share the same warehouse and batch them into a single pickup stop. This significantly reduces total distance by avoiding redundant trips to the same location.


#Dispatch #RouteOptimization #DriverAssignment #LogisticsAI #Python #AgenticAI #LearnAI #AIEngineering

Share this article
C

CallSphere Team

Expert insights on AI voice agents and customer communication automation.

Try CallSphere AI Voice Agents

See how AI voice agents work for your industry. Live demo available -- no signup required.