Skip to content
Learn Agentic AI
Learn Agentic AI14 min read7 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:

Hear it before you finish reading

Talk to a live CallSphere AI voice agent in your browser — 60 seconds, no signup.

Try Live Demo →
flowchart LR
    CALLER(["Client"])
    subgraph TEL["Telephony"]
        SIP["Twilio SIP and PSTN"]
    end
    subgraph BRAIN["Salon AI Agent"]
        STT["Streaming STT<br/>Deepgram or Whisper"]
        NLU{"Intent and<br/>Entity Extraction"}
        TOOLS["Tool Calls"]
        TTS["Streaming TTS<br/>ElevenLabs or Rime"]
    end
    subgraph DATA["Live Data Plane"]
        CRM[("CRM and Notes")]
        CAL[("Calendar and<br/>Schedule")]
        KB[("Knowledge Base<br/>and Policies")]
    end
    subgraph OUT["Outcomes"]
        O1(["Appointment booked"])
        O2(["Reschedule completed"])
        O3(["Stylist handoff"])
    end
    CALLER --> SIP --> STT --> NLU
    NLU -->|Lookup| TOOLS
    TOOLS <--> CRM
    TOOLS <--> CAL
    TOOLS <--> KB
    NLU --> TTS --> SIP --> CALLER
    NLU -->|Resolved| O1
    NLU -->|Schedule| O2
    NLU -->|Escalate| O3
    style CALLER fill:#f1f5f9,stroke:#64748b,color:#0f172a
    style NLU fill:#4f46e5,stroke:#4338ca,color:#fff
    style O1 fill:#059669,stroke:#047857,color:#fff
    style O2 fill:#0ea5e9,stroke:#0369a1,color:#fff
    style O3 fill:#f59e0b,stroke:#d97706,color:#1f2937
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:

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.

Still reading? Stop comparing — try CallSphere live.

CallSphere ships complete AI voice agents per industry — 14 tools for healthcare, 10 agents for real estate, 4 specialists for salons. See how it actually handles a call before you book a demo.

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

Try CallSphere AI Voice Agents

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