Skip to content
latentSource

I Built a Transit Assistant That Has to Calculate the Route

A Gemini agent connects to MCP tools backed by PostGIS and pgRouting. The interesting part is resolving a place, choosing stations, and showing where the answer came from.

··3 min read
Share

Watch the app in action

The assistant searches for Balderas and Bellas Artes, calculates the route with its tools, and shows it on the map.

English AI narration. Unmute the player to listen.

Open video

You can also explore a saved GTFS stop sequence.

I built a Mexico City transit map, then gave it an assistant. A map lets you click a station. An assistant has to work out what you meant by "near the museum," find the right stations, and explain the trip without inventing a connection.

That last requirement shaped the project. The route comes from a graph in PostgreSQL. The model handles the conversation and chooses tools. I can inspect the steps between the question and the answer.

The source is oorona/metro. The explorer above uses one trip from the saved GTFS feed. It shows the underlying stop sequence without calling the assistant or pretending to be a live service advisory.

Give the agent something worth querying

The backend ingests GTFS files into PostGIS tables for stops, trips, stop times, and shapes. It builds ride and transfer edges for pgRouting. Ridership CSVs provide another view of the network, with different levels of detail depending on the transport mode.

That mismatch matters. A line-level count cannot support a station-level heatmap. The application keeps the ridership heatmap specific to Metro, where station-level records exist, instead of spreading totals over stops and presenting the result as a measurement.

The repository documents an October 2022 GTFS snapshot and ridership files through March 2026. Those dates belong next to any claim about coverage. A route calculated from that graph is an estimate under its stored assumptions, not evidence that every service is operating today.

From a question to a route

The MCP server wraps backend operations as tools. The Gemini agent discovers those tools and runs a bounded loop. In the inspected implementation, it allows eight iterations and uses a 30-second tool timeout.

For a station-to-station question, the agent resolves both station identifiers and calls the routing tool. Landmarks take more work: geocode the place, find nearby stations, compare candidate trips, and include the walking distance at both ends.

The prompt explicitly distinguishes "take the light rail" from "use only light rail." Restricting the entire trip to one mode can eliminate the transfer that makes the journey possible. That is a small language distinction with a very concrete routing consequence.

I also prefer comparing candidate stations to blindly picking the nearest stop. A short walk to a useful rail connection can beat a slightly closer bus stop. The prompt asks the agent to compare total journey time, including walking. Whether it follows that instruction consistently needs an evaluation set; the instruction itself is not a measured success rate.

Let me see the tool calls

The backend streams typed events for loop starts, model output, actions, and tool completion. The frontend has a trace panel alongside the map. That makes a wrong answer easier to investigate: did the agent select the wrong place, call the wrong tool, or describe a valid route incorrectly?

A useful evaluation includes duplicate station names, landmarks with abbreviations, trips needing a transfer, missing origins, and places that cannot be resolved. I want to see the failed lookup and the clarification, not a plausible route assembled from memory.

The public source snapshot is pinned in the downloadable manifest. It includes the agent, MCP server, and routing engine. The browser sample contains real saved stop data; it is not a recorded model session. A full conversation recording belongs beside it once captured with its model configuration and returned route.

What this build demonstrates

The work sits across data ingestion, spatial queries, tool interfaces, and the UI that explains their result. PostgreSQL gives the assistant a concrete object to discuss. The trace gives me a way to check whether it used that object correctly.

For reproduction, start with the repository instructions and an isolated PostgreSQL instance with PostGIS and pgRouting. The existing deployment uses a shared database host; there is no reason for a reader reproducing this project to restart that host or touch its other databases.