Skip to content

KangJustin/urbanpilot

Repository files navigation

UrbanPilot

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.

Architecture

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.

Setup

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

Required environment variables

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).

Google Cloud setup

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 is REACT_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.

Midjourney setup

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.

Run it

Two terminals:

npm start                    # frontend, http://localhost:3000
cd server && node index.js   # backend, http://localhost:3001

The 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.)

Token compression (The Token Company)

Standalone benchmark

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.

Our own compact-encoding layer

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.

Production integration

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.

Known limitations

  • No test suite. npm test reports "No tests found" — this is the current state of the repo, not a missing setup step.
  • Midjourney can't fetch localhost or 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.js and googleMapsService.js for the reasoning.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors