Skip to content
Learn Agentic AI13 min read0 views

AI Agent for Last-Mile Delivery: Customer Communication, Rescheduling, and Proof of Delivery

Create an AI agent that manages last-mile delivery operations including customer notifications, delivery window management, rescheduling requests, and proof of delivery capture with photo and signature.

The Last-Mile Challenge

Last-mile delivery is the most expensive and customer-visible part of the logistics chain. It accounts for over 50 percent of total shipping costs and is the primary driver of customer satisfaction. Failed deliveries, missed time windows, and poor communication create frustration that erodes brand loyalty.

An AI last-mile agent sits between the delivery operations system and the customer, handling notifications, managing delivery windows, processing rescheduling requests, and capturing proof of delivery. It reduces failed delivery attempts, improves communication, and automates the repetitive interactions that consume dispatcher time.

Delivery and Customer Data Models

from dataclasses import dataclass, field
from datetime import datetime, date, time
from enum import Enum
from typing import Optional

class DeliveryStatus(str, Enum):
    SCHEDULED = "scheduled"
    OUT_FOR_DELIVERY = "out_for_delivery"
    ARRIVING_SOON = "arriving_soon"
    DELIVERED = "delivered"
    FAILED_ATTEMPT = "failed_attempt"
    RESCHEDULED = "rescheduled"
    RETURNED = "returned"

class ProofType(str, Enum):
    SIGNATURE = "signature"
    PHOTO = "photo"
    PIN_CODE = "pin_code"
    SAFE_DROP = "safe_drop"

@dataclass
class DeliveryWindow:
    date: date
    start_time: time
    end_time: time

@dataclass
class Customer:
    customer_id: str
    name: str
    phone: str
    email: str
    address: str
    delivery_instructions: str = ""
    preferred_contact: str = "sms"

@dataclass
class Delivery:
    delivery_id: str
    order_id: str
    customer: Customer
    window: DeliveryWindow
    status: DeliveryStatus
    driver_name: str
    estimated_arrival: Optional[datetime] = None
    actual_arrival: Optional[datetime] = None
    proof_type: Optional[ProofType] = None
    proof_data: Optional[str] = None
    attempt_count: int = 0
    notes: list[str] = field(default_factory=list)

Notification Flow Tool

The notification tool sends context-aware messages at each delivery stage:

from agents import function_tool

DELIVERIES = {
    "DEL-9001": Delivery(
        delivery_id="DEL-9001",
        order_id="ORD-60001",
        customer=Customer("C-001", "Rachel Chen", "+1-555-0201",
                          "rachel@example.com", "742 Evergreen Terrace, Springfield",
                          "Leave at side door if not home"),
        window=DeliveryWindow(date(2026, 3, 17), time(14, 0), time(18, 0)),
        status=DeliveryStatus.OUT_FOR_DELIVERY,
        driver_name="Tom Wilson",
        estimated_arrival=datetime(2026, 3, 17, 15, 30),
        attempt_count=0,
    ),
    "DEL-9002": Delivery(
        delivery_id="DEL-9002",
        order_id="ORD-60002",
        customer=Customer("C-002", "David Park", "+1-555-0202",
                          "david@example.com", "1600 Pennsylvania Ave, Washington DC",
                          "Ring doorbell twice"),
        window=DeliveryWindow(date(2026, 3, 17), time(9, 0), time(12, 0)),
        status=DeliveryStatus.FAILED_ATTEMPT,
        driver_name="Lisa Brown",
        attempt_count=1,
        notes=["Attempt 1: No one home, building locked"],
    ),
}

NOTIFICATION_TEMPLATES = {
    "out_for_delivery": (
        "Hi {name}, your order {order_id} is out for delivery! "
        "Expected between {start} - {end}. Driver: {driver}."
    ),
    "arriving_soon": (
        "Hi {name}, your delivery is arriving in approximately {eta_minutes} minutes. "
        "Driver {driver} is on the way."
    ),
    "delivered": (
        "Hi {name}, your order {order_id} has been delivered! "
        "Proof of delivery: {proof_type}. Thank you!"
    ),
    "failed_attempt": (
        "Hi {name}, we attempted delivery of {order_id} but were unable to complete it. "
        "Reason: {reason}. Reply RESCHEDULE to pick a new time."
    ),
}

@function_tool
def send_delivery_notification(
    delivery_id: str,
    notification_type: str,
    custom_message: Optional[str] = None,
) -> str:
    """Send a delivery notification to the customer via their preferred channel."""
    delivery = DELIVERIES.get(delivery_id)
    if not delivery:
        return f"Delivery {delivery_id} not found."

    customer = delivery.customer

    if custom_message:
        message = custom_message
    elif notification_type in NOTIFICATION_TEMPLATES:
        template = NOTIFICATION_TEMPLATES[notification_type]
        eta_minutes = "15"
        if delivery.estimated_arrival:
            delta = delivery.estimated_arrival - datetime.now()
            eta_minutes = str(max(1, int(delta.total_seconds() / 60)))

        message = template.format(
            name=customer.name,
            order_id=delivery.order_id,
            start=delivery.window.start_time.strftime("%I:%M %p"),
            end=delivery.window.end_time.strftime("%I:%M %p"),
            driver=delivery.driver_name,
            eta_minutes=eta_minutes,
            proof_type=delivery.proof_type.value if delivery.proof_type else "N/A",
            reason=delivery.notes[-1] if delivery.notes else "Unknown",
        )
    else:
        return f"Unknown notification type: {notification_type}"

    # In production, call Twilio/SendGrid based on preferred_contact
    channel = customer.preferred_contact.upper()
    return (
        f"[{channel}] Notification sent to {customer.name} ({customer.phone}):\n"
        f"{message}"
    )

Rescheduling Tool

When delivery fails or the customer requests a change, the agent handles rescheduling:

See AI Voice Agents Handle Real Calls

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

AVAILABLE_WINDOWS = {
    "2026-03-18": [
        DeliveryWindow(date(2026, 3, 18), time(9, 0), time(12, 0)),
        DeliveryWindow(date(2026, 3, 18), time(13, 0), time(17, 0)),
        DeliveryWindow(date(2026, 3, 18), time(17, 0), time(20, 0)),
    ],
    "2026-03-19": [
        DeliveryWindow(date(2026, 3, 19), time(9, 0), time(12, 0)),
        DeliveryWindow(date(2026, 3, 19), time(13, 0), time(17, 0)),
    ],
}

@function_tool
def get_available_delivery_windows(delivery_id: str) -> str:
    """Get available delivery windows for rescheduling."""
    delivery = DELIVERIES.get(delivery_id)
    if not delivery:
        return "Delivery not found."

    lines = [f"Available delivery windows for {delivery_id}:"]
    for day, windows in AVAILABLE_WINDOWS.items():
        for w in windows:
            lines.append(
                f"  {day}: {w.start_time.strftime('%I:%M %p')} - "
                f"{w.end_time.strftime('%I:%M %p')}"
            )
    return "\n".join(lines)

@function_tool
def reschedule_delivery(
    delivery_id: str,
    new_date: str,
    window_start: str,
    updated_instructions: Optional[str] = None,
) -> str:
    """Reschedule a delivery to a new date and time window."""
    delivery = DELIVERIES.get(delivery_id)
    if not delivery:
        return "Delivery not found."

    if new_date not in AVAILABLE_WINDOWS:
        return f"No availability on {new_date}."

    try:
        start = datetime.strptime(window_start, "%H:%M").time()
    except ValueError:
        return "Invalid time format. Use HH:MM."

    matching_window = next(
        (w for w in AVAILABLE_WINDOWS[new_date]
         if w.start_time == start), None
    )
    if not matching_window:
        return f"No window starting at {window_start} on {new_date}."

    delivery.window = matching_window
    delivery.status = DeliveryStatus.RESCHEDULED
    delivery.attempt_count = 0

    if updated_instructions:
        delivery.customer.delivery_instructions = updated_instructions

    return (
        f"Delivery {delivery_id} rescheduled:\n"
        f"New Date: {new_date}\n"
        f"Window: {matching_window.start_time.strftime('%I:%M %p')} - "
        f"{matching_window.end_time.strftime('%I:%M %p')}\n"
        f"{'Updated Instructions: ' + updated_instructions if updated_instructions else ''}"
        f"Customer will be notified."
    )

Proof of Delivery Tool

@function_tool
def record_proof_of_delivery(
    delivery_id: str,
    proof_type: str,
    proof_data: str,
    recipient_name: Optional[str] = None,
) -> str:
    """Record proof of delivery (photo URL, signature data, or PIN code)."""
    delivery = DELIVERIES.get(delivery_id)
    if not delivery:
        return "Delivery not found."

    valid_types = ["signature", "photo", "pin_code", "safe_drop"]
    if proof_type not in valid_types:
        return f"Invalid proof type. Choose from: {', '.join(valid_types)}"

    delivery.status = DeliveryStatus.DELIVERED
    delivery.actual_arrival = datetime.now()
    delivery.proof_type = ProofType(proof_type)
    delivery.proof_data = proof_data

    result_lines = [
        f"Delivery {delivery_id} marked as DELIVERED.\n",
        f"Proof Type: {proof_type.replace('_', ' ').title()}",
        f"Proof Data: {proof_data}",
        f"Time: {delivery.actual_arrival.strftime('%Y-%m-%d %I:%M %p')}",
    ]
    if recipient_name:
        result_lines.append(f"Received By: {recipient_name}")

    result_lines.append("\nDelivery confirmation will be sent to the customer.")
    return "\n".join(result_lines)

Assembling the Last-Mile Agent

from agents import Agent, Runner

lastmile_agent = Agent(
    name="Last-Mile Delivery",
    instructions="""You are a last-mile delivery assistant. Help with:
    1. Sending delivery notifications (out for delivery, arriving soon, delivered, failed)
    2. Rescheduling failed or inconvenient deliveries
    3. Recording proof of delivery (photo, signature, PIN, safe drop)
    Always check delivery instructions before confirming. For failed attempts,
    proactively offer rescheduling options.""",
    tools=[
        send_delivery_notification,
        get_available_delivery_windows,
        reschedule_delivery,
        record_proof_of_delivery,
    ],
)

result = Runner.run_sync(
    lastmile_agent,
    "DEL-9002 failed delivery. The customer David Park wants to reschedule "
    "for tomorrow evening and says to leave it with the doorman."
)
print(result.final_output)

The agent will look up the failed delivery, show available evening windows, reschedule to the 5-8 PM slot, update the delivery instructions, and send a confirmation notification.

FAQ

How do I implement real-time driver tracking for the "arriving soon" notification?

Use the driver's GPS coordinates from their delivery app. Calculate the driving distance and ETA to the next stop using a routing API like Google Maps or Mapbox. Trigger the "arriving soon" notification when the ETA drops below a configurable threshold, typically 10-15 minutes. Use a geofence around the delivery address to trigger the final approach notification.

What proof of delivery method should I use?

It depends on the delivery context. Signature capture works for high-value items and is legally defensible. Photo proof is most common for residential deliveries and captures the package at the door. PIN codes verify that the intended recipient is present. Safe drop with photo is suitable for low-risk deliveries when the customer pre-authorizes leaving the package. Many carriers use a combination, requiring photo plus either signature or PIN.

How do I handle delivery exceptions beyond "not home"?

Build an exception taxonomy: no access (gate code needed, building locked), address issue (wrong address, unit number missing), package issue (damaged, wrong item), customer refusal, and safety concern (dog, road closure). Each exception type triggers a different workflow. The agent should capture the specific reason, take a photo if relevant, and route to the appropriate resolution path.


#LastMileDelivery #CustomerCommunication #ProofOfDelivery #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.