A multi-agent urban planning copilot. Search any real address, get an AI-driven analysis across climate resilience, accessibility, and housing, and see Current/2040/2075 scenarios — Current backed by a real Street View/satellite photo of the site, 2040/2075 generated by Midjourney.
src/ React frontend (Create React App + Tailwind + Leaflet)
App.js App shell — state/handlers, composes the components below
components/
TopHeader.js Logo, compact location search, live conditions bar
LocationSearch.js Google Places Autocomplete (proxied through the backend)
ConditionsBar.js Live weather/AQI/heat/flood badges
ControlStrip.js Planning-goal / target-year picker + Analyze trigger
AnalysisStatusBar.js Compact status line while agents are running
ReadyToAnalyzeCard.js Pre-analysis onboarding state — no fabricated scores/data
MainMapPanel.js Leaflet map workspace: marker, zoom controls, scenario overlay image
ProjectedScenarioChanges.js Per-scenario projected-change stat strip, beside the map
StreetViewPanel.js Collapsible wrapper around PresentDayView
PresentDayView.js Google Maps JS API panel: Street View / satellite toggle
VisualizeStreetscapeAction.js "Visualize Proposed Streetscape" trigger (Midjourney)
ReferenceImageInput.js Upload-your-own-photo workflow for the Midjourney reference image
CurrentConditionsPanel.js Verified climate/accessibility/housing snapshot
PlanningFindings.js Tabbed container: Risks / Recommendations / Interventions
RisksPanel.js, RecommendationsPanel.js, InterventionsPanel.js, InterventionCard.js
Right-hand analysis panel content (gated behind a completed analysis)
ScoreBreakdownPanel.js Per-category score breakdown
DataMethodologySection.js Collapsed-by-default section hosting the 4 full AgentCard.js cards
AgentCard.js One card per specialist agent (climate/accessibility/housing/urban design)
AIAssistantPanel.js Docked Ask-AI chat panel
ui/ shadcn/ui primitives (Card, Badge, Tabs, Tooltip, ScrollArea)
constants/planning.js Default location + shared planning constants
lib/utils.js shadcn's cn() className helper
utils/ formatters.js (null-safe display formatting), planningHelpers.js
(cost/weather icon + color maps)
services/analysisApi.js All fetch calls to the backend, in one place
server/ Node/Express backend
agents/ One file per Claude agent (climate, accessibility, housing,
urbanDesign, vision, ask) + coordinator.js orchestrating them
routes/ analysis, ask, conditions, location, upload, visualize, health
services/
claudeService.js Wraps the Anthropic SDK; the client is wrapped again with
the-token-company's withCompression (see Token compression below)
promptCompression.js Shared compact-encoding helpers used by the housing/climate/
accessibility agent prompts
censusService.js U.S. Census Geocoder → block group → ACS 5-Year housing metrics
openMeteoService.js Live weather + US AQI for Climate Agent grounding (no key needed)
femaNfhlService.js FEMA National Flood Hazard Layer flood-zone lookup (no key needed)
nlcdTccService.js NLCD Tree Canopy Cover lookup (no key needed)
transit511Service.js 511 SF Bay Regional GTFS → verified transit proximity metrics
*AgentParser.js Per-agent (housing/climate/accessibility) JSON extraction +
fallback repair for truncated/malformed Claude responses
conditionsService.js Live weather/AQI via Open-Meteo (no key needed) — powers the
frontend's top "Live Data" conditions bar specifically
googleMapsService.js Places (New) Autocomplete/Details, Street View status, image proxies
midjourneyMcpClient.js
OAuth + connection management for Midjourney's MCP server
midjourneyService.js generateImage() — the actual Midjourney call
renderingProvider.js FutureRenderingProvider abstraction over midjourneyService
scripts/ One-off scripts that hit real external APIs: verify-housing-census.js,
verify-climate-{fema,nlcd,openmeteo}.js, verify-accessibility-transit.js,
verify-ask-grounding.js, verify-vision-baselines.js, plus the token
compression benchmark (compression-bench.js, compression_benchmark_ttc*.py)
Data flow for an analysis: LocationSearch resolves an address to {placeId, formattedAddress, latitude, longitude, viewport} (the single source of truth, selectedLocation in App.js) → /api/analyze runs the climate/accessibility/housing/urbanDesign/vision agents in parallel, each grounding itself in a real verified data source (Census ACS, Open-Meteo, FEMA NFHL, NLCD, 511 GTFS) before asking Claude about the site → results populate the AI agent cards, Score Breakdown, Top Risks, and Top Recommendations panels (all empty/idle until that analysis completes — there's no bundled demo data to fall back to). The Current scenario shows a real photo (Street View if covered, otherwise satellite) fetched through /api/location/street-view-image and /api/location/satellite-image — these proxy routes exist so the Google API key never reaches the browser. 2040/2075 generate via Midjourney, using that same real photo as a composition reference by default (or your own uploaded photo).
A diagram of the agent pipeline (parallel specialist agents → synthesis → vision → response) is in
docs/agent-workflow.png.
git clone https://github.com/KangJustin/urbanpilot.git
cd urbanpilot
npm install
cd server && npm install && cd ..Copy the two .env.example files and fill in real values:
cp .env.example .env
cp server/.env.example server/.env| Variable | Where | What it's for |
|---|---|---|
ANTHROPIC_API_KEY |
server/.env |
Powers every Claude agent. Without it, each agent's Claude call fails and that section of the analysis shows as temporarily unavailable rather than a result (there's no mock-data fallback). |
GOOGLE_MAPS_SERVER_API_KEY |
server/.env |
Server-only key for Places API (New), Geocoding API, Street View Static API, Maps Static API. Never put this in the frontend. |
CENSUS_API_KEY |
server/.env |
U.S. Census Bureau key for the Housing Agent's verified ACS metrics. Without it, the Housing Agent still runs, just without verified Census grounding. |
TRANSIT_511_API_KEY |
server/.env |
511 SF Bay Open Data key for the Accessibility Agent's verified GTFS transit metrics. Same degrade-gracefully behavior without it. |
TTC_API_KEY |
server/.env |
The Token Company key. claudeService.js wraps the Anthropic client with their withCompression on every agent call — see Token compression below. |
REACT_APP_GOOGLE_MAPS_API_KEY |
.env (root) |
Client-side key for the Maps JavaScript API, used specifically by PresentDayView.js's Street View/satellite panel (the main map workspace itself is Leaflet, not Google Maps JS). Restrict it by HTTP referrer in Google Cloud Console — it's visible in the browser by design. |
Optional:
| Variable | Where | What it's for |
|---|---|---|
MIDJOURNEY_OAUTH_PORT |
server/.env |
Local callback port for the one-time Midjourney OAuth login (default 8090). |
ALLOWED_ORIGINS |
server/.env |
Comma-separated extra CORS origins, e.g. for sharing over LAN. |
REACT_APP_API_URL |
.env (root) |
Override the backend URL the frontend calls (default http://localhost:3001). |
In console.cloud.google.com, enable Billing, then enable: Maps JavaScript API, Places API (New), Geocoding API, Street View Static API, Maps Static API. Create two separate API keys under APIs & Services → Credentials:
- Client key — restrict to HTTP referrers (
http://localhost:3000/*for dev), API-restrict to Maps JavaScript API only. This isREACT_APP_GOOGLE_MAPS_API_KEY. - Server key — API-restrict to the other four APIs above (not Maps JavaScript API). This is
GOOGLE_MAPS_SERVER_API_KEY. Never expose it client-side.
No API key needed — the backend connects directly to Midjourney's hosted MCP server
(mcp.midjourney.com) via OAuth. The first time you click "Generate with Midjourney," a
browser window opens asking you to log in and authorize. After that, tokens are cached in
server/.mcp-auth/ (gitignored) and refresh automatically — you won't need to log in again
unless that cache is deleted.
Two terminals:
npm start # frontend, http://localhost:3000
cd server && node index.js # backend, http://localhost:3001The frontend hot-reloads on save. The backend doesn't — restart it after editing anything in
server/. (npm run dev in server/ uses node --watch if you'd rather it restart itself.)
We measured the-token-company's with_compression() wrapper
against UrbanPilot's own real production prompts — real Anthropic API calls, real
response.usage.input_tokens, no estimates. Two prompt shapes were tested against Downtown
Berkeley: a structured JSON-schema generation prompt (the Housing Agent's verified-ACS-census
prompt) and a long, prose-heavy context prompt (the Ask UrbanPilot AI assistant's context).
| Prompt type | Raw Anthropic (input tokens) | + the-token-company | Reduction |
|---|---|---|---|
| Structured JSON-schema prompt (Housing Agent) | 924 | 888 | 3.9% |
| Long, prose-heavy context prompt (Ask AI) | 4,757 | 3,775 | 20.6% |
In both cases, compression came at no cost to output quality — every compressed response still
parsed as valid structured JSON and cited the exact verified figures (Census income/rent, real
risk severities, scenario specifics) from the source data, with no hallucination observed.
The benchmark scripts that produced these numbers live in server/scripts/compression-bench.js,
compression_benchmark_ttc.py, and compression_benchmark_ttc_longcontext.py.
Independent of the-token-company, every scoring agent (housing.js, climate.js,
accessibility.js) builds its verified-data block and JSON response schema in a hand-written
terse encoding (services/promptCompression.js) instead of the original pretty-printed,
labeled-block format — no third party involved, just a denser prompt shape. Measured against
the original verbose prompts with real API calls (server/scripts/compression-bench.js):
| Agent | Original (input tokens) | Compact encoding | Reduction |
|---|---|---|---|
| Housing | 1,155 | 834 | 27.8% |
| Climate | 1,507 | 973 | 35.4% |
| Accessibility | 1,247 | 827 | 33.7% |
Output quality held in every case: valid JSON, exact verified-number citations, and intact risk/recommendation arrays.
server/services/claudeService.js wraps the live Anthropic client every agent uses
(the-token-company/anthropic's withCompression), so every real request — not just the
benchmark — passes through TTC's compression before reaching Claude. Because the compact
encoding above already strips most of the redundancy out of these prompts, TTC's incremental
contribution on top is small in practice (observed: ~1% additional reduction on an
already-compacted Housing Agent prompt) — the two techniques target the same redundancy, so
gains aren't simply additive. TTC's wrapper exposes live stats
(client.compression.totalTokensSaved), logged on every call.
- No test suite.
npm testreports "No tests found" — this is the current state of the repo, not a missing setup step. - Midjourney can't fetch
localhostor LAN-IP URLs. The 2040/2075 "use the real photo as a reference" feature silently falls back to text-only generation in local dev, since Midjourney rejects non-public reference URLs. It'll work automatically once deployed somewhere with a real public URL — no code change needed then. - No bundled mock data, anywhere. The old Berkeley demo dataset was removed entirely. The
pristine pre-search state is
ReadyToAnalyzeCard.js— purely presentational onboarding copy, never a fabricated score, risk, or recommendation. Every result panel is empty/idle until a real analysis completes; a failed analysis shows a real error, never substituted data. - Google Maps/Street View/satellite imagery is only ever displayed, never used as Midjourney
training/input data except for the explicit, accepted-risk reference-image case above — see
the comments in
renderingProvider.jsandgoogleMapsService.jsfor the reasoning.