Ammad Ashraf
Back to work

Agentic Systems

Wired an LLM to 4 backend tools instead of RAG, over a real 9,095-record transit dataset

Ask it how to get across Islamabad by CDA bus, in plain language, misspelled stop names and all — the assistant calls the right tools in the right order, corrects your spelling, and streams back a real route with real departure times.

Client
Capital Transit
Role
Solo build, on top of a university course dataset
Period
FAST-NUCES SE4009 coursework, extended solo
Domain
Public transit / urban mobility
Product
Academic / personal project
Platform
Web app (FastAPI + React, SSE)
Market
Pakistan

Stack

  • Python
  • OpenAI
  • React
  • JavaScript
  • 9,095

    route-stop records

    extracted from CDA’s published PDF schedules

  • 4

    backend tools

    called autonomously, chained when the first misses

  • 460

    lines of backend Python

    5 files — FastAPI + pandas + OpenAI

The brief

Overview

"Capital Transit" is an agentic AI trip-planning assistant for Islamabad’s CDA (Capital Development Authority) bus network, built on top of a FAST-NUCES process-mining course project (SE4009) whose core assignment was extracting structured route data from the transit authority’s published PDF schedules into an event log.

The course project stopped at the dataset. I took it further, solo: a full conversational product on a real 9,095-record dataset of route-stop pairs, with an LLM that calls backend tools directly rather than reasoning over retrieved text.

The architecture choice

Why tool-calling, not RAG

A transit query isn’t a search problem — it’s sequencing, transfer math and timestamp comparison.

Retrieval-augmented generation is the default reach for "answer questions over a dataset." Why wire the model to explicit tools instead?

RAG’s semantic search can’t reliably do sequencing, transfer math, or timestamp comparison — the actual operations a route query needs, not a fuzzy match over route descriptions.

Wiring `gpt-4o-mini` to 4 backend tools (`get_all_routes`, `find_direct_route`, `find_transfer_routes`, `get_schedule_and_time`) that it calls autonomously means the model reasons about WHICH tool to call and WHEN to chain a second one, while the actual route-finding stays deterministic Python and pandas — not a language model guessing at arithmetic.

The alternative

Embed route descriptions and retrieve the closest match to the user’s question.

What it cost

Tool-calling requires hand-authoring every operation the assistant can perform — there is no "just add more documents" path the way RAG has; the 4-tool surface is deliberately small rather than exhaustive.

Engineering details

What makes the agent trustworthy on a real query

  • 3-tier fuzzy stop resolution — Exact case-insensitive match → substring match → `difflib` fuzzy fallback, so a misspelled stop ("Khanapul") silently resolves to the real dataset value ("Khanna Pul") before any query runs — surfaced back to the user via the model’s own acknowledgement of the resolved name.

  • Bidirectional transfer-route discovery — Intersects the stop-sets of every route pair reachable from the origin and destination respectively, capped at 3 results per query to keep answers focused rather than exhaustive.

  • A real data-modelling bug, found and fixed — The extracted schedule data was sequence-numbered per-trip-per-day; naive index-difference math produced impossible travel times (thousands of minutes). Fixed by bounding the calculation to a single route’s own unique stop sequence.

  • Guardrails in the system prompt — The assistant must call `find_direct_route` before `find_transfer_routes`, must always follow a found route with `get_schedule_and_time`, must never answer outside the CDA transit domain, and must never fabricate a route no tool returned.

Two-phase streaming

A two-phase SSE architecture separates the model’s tool-use from its answer: an internal, non-streaming tool-execution loop emits `[AGENT_WORKING]` progress events to the frontend while tools run, followed by a genuinely streamed final completion once every tool call has resolved.

The UI shows "thinking" while the agent is actually calling tools, and a live typing effect only for the final, grounded answer — not a fake typing animation over an answer that was already fully generated.

Real departure times, estimated durations — said plainly

Outcome

Selected results

  • 9,095

    route-stop records

    extracted from published PDF schedules

  • 4

    backend tools

    autonomous chaining on a direct-route miss

  • 460

    lines of backend Python

    5 files, FastAPI + pandas + OpenAI

  • 5

    REST/SSE endpoints

    behind a 4-component React 19 frontend

Retrospective

What I would take to the next build

  • Pick tool-calling when the problem is operations, not documents — RAG is the reflexive choice for "ask questions over data" — but sequencing and arithmetic belong in deterministic code the model calls, not in a retrieved-context guess.

  • A resolver that fixes typos silently, then says so — Correcting "Khanapul" to "Khanna Pul" without failing the query, then having the model acknowledge the correction, is a small detail that’s most of what makes the assistant feel trustworthy on a real, messy query.

  • State an estimate’s limits in the product, not just the code comments — The travel-time heuristic is fine to ship — it just has to be labelled as one.

Built solo, extending a FAST-NUCES SE4009 (Process Mining and Simulation) course dataset into a full conversational product beyond what the course itself required.