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
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.