LangGraph-based conversational agent for Singapore bus queries using real-time LTA DataMall APIs
- Project Overview
- Architecture & Workflow Design
- Key Features
- Assumptions Made
- Setup Instructions
- Usage
- Deployment Considerations
This project demonstrates:
- Part 1: Transport query agent using LangGraph for workflow orchestration
- Part 2: 10-user simulation with results analysis
The agent converts natural language queries (e.g., "When is the next bus at Marina Bay Sands?") into real-time bus arrival information via Singapore's LTA DataMall APIs.
| Component | Technology | Purpose |
|---|---|---|
| Framework | LangGraph v0.2+ | Agent orchestration |
| LLM | Google Gemini 2.0 Flash Lite | Function calling |
| APIs | LTA DataMall BusArrival v3 | Real-time data |
| Validation | Pydantic | Type safety |
| Language | Python 3.11+ | Async/await patterns |
graph TB
User[User Query] --> Agent[LangGraph Agent]
Agent --> Tool1[find_bus_stop_code]
Agent --> Tool2[get_bus_arrival_times]
Tool1 --> Cache[(Bus Stop Cache<br/>5000+ locations)]
Tool2 --> LTA[LTA DataMall API]
LTA --> Response[Formatted Response]
Cache --> Response
flowchart LR
A[User Query] --> B[Agent Node]
B --> C{should_continue?}
C -->|Has tool_calls| D[Tool Node]
C -->|No tool_calls| E[Final Response]
D --> B
Chosen over simpler frameworks for:
- Explicit Control: StateGraph makes decisions transparent (vs black-box agents)
- State Management: Built-in message accumulation across tool calls
- Conditional Routing: Intelligent loop management via
should_continue()edge - Production Maturity: Async execution, error boundaries, scalability
Nodes (Processing Units):
agent_node: Gemini 2.0 reasoning for tool selectiontool_node: Executes landmark resolution or bus arrivals
Edges (Flow Control):
- Conditional: Routes based on presence of tool calls
- Return: Loops tool results back to agent
State:
AgentState: TypedDict withoperator.addfor message accumulation- Preserves full conversation context
Two-tool architecture:
- find_bus_stop_code: Converts landmarks to 5-digit codes
- Fuzzy matching: "Bugis Junction" → "Bugis Jct" → code 03211
- Multi-strategy search: exact → normalized → keyword
- get_bus_arrival_times: Fetches real-time arrivals
- Optional service filtering (e.g., "bus 196 only")
- GPS-tracked arrival times with seat availability
graph TD
A[User Query] --> B{Query Type?}
B -->|"Direct Code<br/>(e.g., stop 01012)"| C[get_bus_arrival_times]
B -->|"Landmark Name<br/>(e.g., Marina Bay)"| D[find_bus_stop_code]
B -->|"Service + Code<br/>(e.g., bus 196 at 01012)"| C
B -->|"Service + Landmark<br/>(e.g., bus 196 at Marina Bay)"| D
D --> E[Returns Bus Stop Code]
E --> C
C --> F[Returns Real-time Arrivals]
F --> G[Natural Language Response]
Scenario: Multi-step query
User: "Bus times at Marina Bay Sands"
Step 1: Agent detects landmark name (no bus stop code)
Step 2: Call find_bus_stop_code("Marina Bay Sands")
→ Returns: "04169"
Step 3: Call get_bus_arrival_times("04169")
→ Returns: [Bus 133: 3min (SEA), Bus 502A: 7min (SEA)]
Step 4: Generate response: "Bus 133 in 3 mins, Bus 502A in 7 mins"
Scenario: Direct code
User: "Arrivals at stop 77009"
Step 1: Agent detects 5-digit code
Step 2: Call get_bus_arrival_times("77009")
→ Returns: Real-time data
Step 3: Format and respond
- Handles colloquial location names
- Fuzzy matching for naming variations
- Extracts parameters (stop code, service number, landmark)
- Live GPS-tracked bus arrivals (LTA updates every 20 seconds)
- Seat availability status (SEA/SDA/LSD)
- Service-specific filtering when requested
- Pydantic validation for API responses
- Retry logic with exponential backoff (tenacity)
- Graceful degradation for API failures
- Clear error messages for invalid inputs
- 10 diverse test scenarios (Part 2 deliverable)
- Covers: direct codes, landmarks, service filtering, error cases
- 100% appropriate response handling achieved
- Valid LTA API key with sufficient quota (5000 calls/month free tier)
- LTA updates bus positions every 20 seconds
- Stable JSON response schemas per LTA documentation
- 5-digit bus stop code format consistent
- English language queries
- Geographic scope: Singapore public transport only
- Query intent: Bus arrival information
- Operating hours: 5:30 AM - 12:30 AM SGT
- Gemini 2.0 Flash Lite provides reliable function calling
- Stable internet connectivity (2-5 second acceptable latency)
- Conversation stays within model's context window
- Python 3.11+ environment available
- Python 3.11+
- LTA DataMall API Key
- Google AI API Key
-
Clone Repository
git clone <repository-url> cd singapore-transport-agent
-
Virtual Environment
python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate
-
Install Dependencies
pip install -r requirements.txt
-
Configure Environment
Create
.envfile:LTA_API_KEY=your_lta_api_key_here GOOGLE_API_KEY=your_google_api_key_here TZ=Asia/Singapore
-
Verify Setup
python -c " import os from dotenv import load_dotenv load_dotenv() assert os.getenv('LTA_API_KEY'), 'Missing LTA API key' assert os.getenv('GOOGLE_API_KEY'), 'Missing Google API key' print('✅ Environment configured') "
-
Start Jupyter Notebook
jupyter notebook Transit.ipynb
-
Execute Cells Sequentially
- Run from top to bottom
- First execution caches 5000+ bus stops (2-3 minutes)
- Subsequent runs use cached data
-
View Results
- Cell 7 contains 10-user simulation
- Results table shows query responses
- Success metrics displayed at end
Landmark-based:
- "Show me bus arrivals at Marina Bay Sands"
- "Next bus at Raffles Hotel"
- "Buses at Suntec City"
Service-specific:
- "When is bus 196 arriving at stop 01012?"
- "Service 175 at Marina Bay Sands"
Direct codes:
- "Arrivals at stop 77009"
- "Check bus times for 04169"
- Cached landmarks: 2-3 seconds
- New landmarks: 5-8 seconds
- Direct codes: 3-5 seconds
- API Layer: FastAPI (async endpoints)
- Agent: LangGraph + Gemini 2.0 Flash Lite
- Storage: Redis (conversation state + bus stop cache)
- Platform: Docker containerized
- LangGraph Cloud: Managed deployment, easiest for prototypes
- AWS: ECS Fargate + ElastiCache Redis
- Google Cloud: Cloud Run + Memorystore
- Azure: Container Apps + Azure Cache for Redis
LangGraph agents maintain conversation context across requests - naive scaling breaks this.
Solution:
- Store conversation state in Redis per user (session:{user_id})
- Session TTL: 30 minutes
- Async processing for concurrent requests
- Cache bus stop data (24hr TTL, ~2MB for 5000 stops)
- Reuse HTTP connections to external APIs
- Rate limiting respects LTA quota (5000 calls/month)
- Tool call frequency and success rates
- Agent loop iterations (detect infinite loops)
- LLM token usage and costs
- LangSmith: Trace agent workflows, debug tool decisions
- Logging: JSON logs with user correlation IDs
- Alerts: Trigger when LTA API fails (agent cannot function)
LTA_API_KEY = os.getenv("LTA_API_KEY") # Never hardcode
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") # Environment variables