Skip to content
Learn Agentic AI11 min read0 views

Building a Public Transit Information Agent: Route Planning, Delays, and Accessibility

Build an AI agent that provides real-time public transit information including route planning, live delay updates, accessibility features, and multimodal trip suggestions for city residents.

Why Transit Agencies Need AI Agents

Public transit systems are information-heavy. A mid-size city might operate 40 bus routes, a light rail line, and paratransit services — each with different schedules for weekdays, weekends, and holidays. Add real-time delays, service alerts, detours, and accessibility concerns, and the information landscape becomes overwhelming for riders.

Existing transit apps show maps and schedules but cannot answer questions like "I use a wheelchair and need to get from downtown to the hospital by 2 PM — what are my options?" That requires combining schedule data, real-time vehicle positions, accessibility information, and trip planning logic into a conversational interface. This is exactly what an AI agent can do.

Understanding GTFS: The Transit Data Standard

Nearly every public transit agency publishes data in GTFS (General Transit Feed Specification) format. GTFS is a standardized set of CSV files that describe routes, stops, schedules, and geographic shapes. The agent needs to ingest and query this data.

import csv
from dataclasses import dataclass, field
from pathlib import Path


@dataclass
class Stop:
    stop_id: str
    stop_name: str
    latitude: float
    longitude: float
    wheelchair_accessible: bool = False
    shelter: bool = False


@dataclass
class Route:
    route_id: str
    route_name: str
    route_type: str  # bus, rail, ferry, etc.
    stops: list[Stop] = field(default_factory=list)


def load_stops(gtfs_path: Path) -> dict[str, Stop]:
    """Load stops from GTFS stops.txt file."""
    stops = {}
    with open(gtfs_path / "stops.txt", newline="") as f:
        reader = csv.DictReader(f)
        for row in reader:
            stops[row["stop_id"]] = Stop(
                stop_id=row["stop_id"],
                stop_name=row["stop_name"],
                latitude=float(row["stop_lat"]),
                longitude=float(row["stop_lon"]),
                wheelchair_accessible=row.get("wheelchair_boarding") == "1",
            )
    return stops


def load_routes(gtfs_path: Path) -> dict[str, Route]:
    """Load routes from GTFS routes.txt file."""
    route_types = {
        "0": "light_rail", "1": "subway", "2": "rail",
        "3": "bus", "4": "ferry", "5": "cable_car",
    }
    routes = {}
    with open(gtfs_path / "routes.txt", newline="") as f:
        reader = csv.DictReader(f)
        for row in reader:
            routes[row["route_id"]] = Route(
                route_id=row["route_id"],
                route_name=row.get("route_long_name", row.get("route_short_name", "")),
                route_type=route_types.get(row["route_type"], "bus"),
            )
    return routes

Real-Time Delay Integration

Static schedules are only half the picture. The agent must also consume GTFS-Realtime feeds that provide live vehicle positions and service alerts.

import httpx
from datetime import datetime, timedelta
from dataclasses import dataclass


@dataclass
class ServiceAlert:
    alert_id: str
    route_ids: list[str]
    header: str
    description: str
    severity: str  # info, warning, severe
    start_time: datetime | None = None
    end_time: datetime | None = None


@dataclass
class TripDelay:
    route_id: str
    trip_id: str
    stop_id: str
    delay_seconds: int
    timestamp: datetime


class RealtimeTransitClient:
    """Client for consuming GTFS-Realtime feeds."""

    def __init__(self, alerts_url: str, updates_url: str):
        self.alerts_url = alerts_url
        self.updates_url = updates_url
        self._alerts_cache: list[ServiceAlert] = []
        self._delays_cache: dict[str, TripDelay] = {}
        self._last_fetch: datetime | None = None

    async def fetch_alerts(self) -> list[ServiceAlert]:
        """Fetch current service alerts from the transit agency."""
        async with httpx.AsyncClient() as client:
            response = await client.get(self.alerts_url)
            data = response.json()

        alerts = []
        for entry in data.get("entity", []):
            alert_data = entry.get("alert", {})
            route_ids = [
                s["route_id"]
                for s in alert_data.get("informed_entity", [])
                if "route_id" in s
            ]
            alerts.append(ServiceAlert(
                alert_id=entry["id"],
                route_ids=route_ids,
                header=alert_data.get("header_text", {})
                    .get("translation", [{}])[0].get("text", ""),
                description=alert_data.get("description_text", {})
                    .get("translation", [{}])[0].get("text", ""),
                severity=alert_data.get("severity_level", "info"),
            ))

        self._alerts_cache = alerts
        return alerts

    def get_alerts_for_route(self, route_id: str) -> list[ServiceAlert]:
        """Get active alerts affecting a specific route."""
        return [a for a in self._alerts_cache if route_id in a.route_ids]

Building the Trip Planner

The trip planner combines static schedule data with real-time delays to suggest routes. Accessibility filtering is built in from the start, not bolted on as an afterthought.

See AI Voice Agents Handle Real Calls

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

@dataclass
class TripOption:
    departure_time: str
    arrival_time: str
    duration_minutes: int
    transfers: int
    routes_used: list[str]
    walking_minutes: int
    wheelchair_accessible: bool
    alerts: list[str]
    adjusted_for_delays: bool = False


def plan_trip(
    origin: str,
    destination: str,
    departure_after: datetime,
    require_accessible: bool = False,
    max_transfers: int = 2,
    routes: dict = None,
    stops: dict = None,
    realtime: RealtimeTransitClient = None,
) -> list[TripOption]:
    """Plan a transit trip with optional accessibility filtering."""

    # Step 1: Find nearest stops to origin and destination
    origin_stops = find_nearest_stops(origin, stops, radius_meters=400)
    dest_stops = find_nearest_stops(destination, stops, radius_meters=400)

    if require_accessible:
        origin_stops = [s for s in origin_stops if s.wheelchair_accessible]
        dest_stops = [s for s in dest_stops if s.wheelchair_accessible]

        if not origin_stops or not dest_stops:
            return []  # No accessible stops nearby

    # Step 2: Find connecting routes (simplified graph search)
    options = []
    for o_stop in origin_stops[:3]:
        for d_stop in dest_stops[:3]:
            route_options = find_routes_between(
                o_stop, d_stop, departure_after,
                max_transfers=max_transfers,
            )
            for route_opt in route_options:
                # Step 3: Apply real-time delays
                alerts = []
                if realtime:
                    for rid in route_opt["route_ids"]:
                        route_alerts = realtime.get_alerts_for_route(rid)
                        alerts.extend([a.header for a in route_alerts])

                options.append(TripOption(
                    departure_time=route_opt["depart"],
                    arrival_time=route_opt["arrive"],
                    duration_minutes=route_opt["duration"],
                    transfers=route_opt["transfers"],
                    routes_used=route_opt["route_ids"],
                    walking_minutes=route_opt["walk_min"],
                    wheelchair_accessible=route_opt["accessible"],
                    alerts=alerts,
                ))

    # Sort by arrival time
    options.sort(key=lambda x: x.arrival_time)
    return options[:5]


def find_nearest_stops(
    location: str, stops: dict, radius_meters: int = 400
) -> list[Stop]:
    """Find stops within walking distance of a location.
    In production, geocode the location string first."""
    # Placeholder: would use geocoding + haversine distance
    return list(stops.values())[:5]


def find_routes_between(origin, dest, depart_after, max_transfers=2):
    """Graph search through the transit network.
    In production, use a proper routing engine like OpenTripPlanner."""
    return []

Conversational Agent Layer

The conversational layer wraps the trip planner and real-time data into a natural dialogue.

from openai import OpenAI

client = OpenAI()

TRANSIT_AGENT_PROMPT = """You are a public transit information agent.
You help riders plan trips, check delays, and find accessible routes.

You have access to these tools:
- plan_trip(origin, destination, time, accessible): Plan a transit trip
- get_alerts(route_id): Get service alerts for a route
- find_stop(name): Find a transit stop by name

When a rider asks about accessibility, ALWAYS filter for wheelchair-accessible
stops and routes. Never suggest a route that includes an inaccessible stop
to a rider who needs accessibility.

When there are active delays or alerts on a suggested route, proactively
mention them — do not wait for the rider to ask.
"""

The accessibility-first design is essential for government services. The Americans with Disabilities Act requires that transit information systems provide equivalent access. Building accessibility filtering into the agent's core logic — rather than as an optional add-on — ensures compliance and serves all riders.

FAQ

How does the agent handle transit systems that do not publish GTFS data?

For agencies without GTFS feeds, the agent can ingest schedule data from other formats (spreadsheets, PDFs, or web scraping) and normalize it into the same internal data structures. The GTFS loader is just one ingest pathway. The trip planning and real-time layers work against the internal Stop, Route, and TripOption models regardless of the original data source. However, real-time delay information will be unavailable without a live feed, so the agent should clearly inform riders that arrival times are based on published schedules only.

How do you keep the schedule data current when agencies update routes seasonally?

Transit agencies typically publish new GTFS feeds quarterly or when service changes take effect. The agent runs an automated pipeline that checks the agency's GTFS feed URL on a daily schedule, downloads any updates, validates the data, and rebuilds the internal route graph. A version log tracks which GTFS feed is currently active. If a rider asks about a trip on a date that falls after an announced service change, the agent loads the future-effective schedule for planning.

Can the agent suggest multimodal trips combining transit with bikeshare or rideshare?

Yes. The agent extends the trip planner to include first-mile and last-mile options. If the nearest transit stop is more than a 10-minute walk, the agent checks bikeshare station availability via the GBFS (General Bikeshare Feed Specification) standard or suggests a rideshare connection. The trip option clearly labels each segment — "Walk 3 min to bikeshare station, ride 7 min to Metro station, take Blue Line 4 stops, walk 2 min to destination" — so the rider understands the full journey.


#GovernmentAI #PublicTransit #GTFS #RoutePlanning #Accessibility #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.