A LangGraph agent that answers natural-language questions about wildfire risk for a utility service territory by combining (a) a trained susceptibility model (XGBoost on terrain/climate/fuel features), (b) live NASA wildfire feeds and NWS red-flag warnings via tool calls, and (c) RAG over public wildfire-mitigation documents (a real utility Wildfire Mitigation Plan filing + FEMA wildfire-safety guidance). Outputs risk maps and prioritized inspection lists.
Directly targets the utility wildfire-mitigation market (real-world case studies show ~50% ignition/outage reductions from exactly this kind of tooling). Reuses RAG + LangGraph patterns but adds a predictive-ML component and live hazard feeds, differentiating it from a generic chatbot.
| Feature | Source | Auth |
|---|---|---|
| Elevation | USGS Elevation Point Query Service | none |
| Climate normals | Open-Meteo Climate API | none |
| Fuel-type proxy | MRLC NLCD 2021 land cover (WMS) | none |
| NDVI / vegetation-dryness trend | MODIS MOD13Q1.061 via NASA AppEEARS1 | free Earthdata login (offline bake only) |
| Historical fire occurrence (training labels) | NIFC WFIGS InterAgencyFirePerimeterHistory | none |
| Live wildfire events | NASA EONET | none |
| Live fire-hotspot detections | NASA FIRMS | free key (register) |
| Fire-weather warnings | NWS active alerts | none |
| Roads / powerlines (ignition-proxy distance) | OpenStreetMap Overpass API | none |
| Place-name search + reverse geocoding (frontend) | OpenStreetMap Nominatim | none |
| ZIP code (ZCTA) boundaries (frontend choropleth) | US Census Bureau TIGERweb | none |
| Vegetation management / mitigation policy (RAG corpus) | PG&E 2026-2028 Wildfire Mitigation Plan2 (public regulatory filing) | none |
| Wildfire preparedness guidance (RAG corpus) | FEMA Ready.gov — Wildfires3 (public guidance, committed snapshot) | none |
Substitutions from the original spec, made after checking what's actually
available without extra friction: LANDFIRE fuel models → NLCD land cover (simpler
point-query API, same role); ERA5-Land/WorldClim → Open-Meteo (keyless, no Copernicus
CDS registration); Ollama/local open LLM → Google Gemini via the Gemini Developer
API (no multi-GB model download). Bedrock was tried first (reusing AWS credentials
already configured in this environment) but was dropped after botocore[crt],
cross-region inference profiles, and a one-time account-level "use case details"
approval gate turned out to be more setup friction than a direct API key; the direct
Anthropic API was tried next but requires a funded account, which stalled the live
demo. Gemini's free tier (aistudio.google.com) needs neither a credit card nor an
approval gate. The agent isn't fully local either way — swap agent.py's
ChatGoogleGenerativeAI for any other LangChain-compatible chat model (e.g.
ChatAnthropic, langchain_aws.ChatBedrockConverse, or a local Ollama model) if
that's undesirable.
Binary XGBoost classifier: label=1 is the centroid of a historical fire perimeter
(NIFC, 2015+), label=0 is a random background point in the same territory (standard
presence/background approach — true absence data isn't available). Features:
elevation, slope/aspect (finite-differenced from elevation, aspect as sin/cos to
avoid a false 0/360 discontinuity), mean daily max temp + total precip (climate
normals), an NLCD-derived fuel-load proxy, distance to nearest road/powerline
(OSM Overpass — an ignition-proxy signal, since human-caused ignitions cluster near
infrastructure unlike lightning-caused ones), and real MODIS NDVI + a ~70-day
vegetation-dryness trend (NASA AppEEARS — offline-baked, see bake_ndvi.py, since
AppEEARS point tasks are async and don't fit a live per-click request). Trained on
300 points across a Northern California utility-territory bbox; see
data/models/metrics.json for the held-out AUC after training.
Train/test is split by spatial block (features.spatial_block_split), not by
individual point: the study area is divided into blocks and whole blocks are held
out for test, so a test point can't sit meters from a training point. Since these
features vary smoothly over space, a naive point-wise random split lets that
happen and inflates held-out AUC without real spatial generalization — the honest,
spatially-blocked AUC (0.70) is meaningfully lower than the original point-wise
split's (0.82), which is the fix working as intended, not a regression.
This is a real but modest model — a portfolio-scale demonstration of the full pipeline (label sourcing, live feature engineering, training, evaluation, serving), not an operational-grade susceptibility surface. See Next steps for what a production version would need.
python3.11 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Optional: a general-purpose geospatial data-science toolkit (geopandas,
# rasterio, xarray, jupyterlab, ...) not required by this project's own
# code -- see pyproject.toml's `geo` extra.
pip install -e ".[geo]"
# Bake the NDVI/vegetation-dryness grid training depends on (needs a free
# Earthdata login -- see Data sources above; the committed
# data/models/ndvi_features.json already has this, so skip unless you're
# regenerating it):
python -m wildfire_copilot.bake_ndvi
# Train the susceptibility model (~45-50 min -- fetches 300 points x ~6
# live API calls each; the 2 OSM Overpass calls per point are the slow
# part, deliberately throttled to 2 concurrent requests so as not to
# hammer that shared public instance. Live feature lookups are disk-cached
# (cache.py) -- a second run over overlapping points is much faster):
python -m wildfire_copilot.train
# Build the RAG index over the two source documents (see Data sources
# above; ~1 min, downloads a small local embedding model on first run):
python -m wildfire_copilot.ragNASA FIRMS is optional — register for a free key and set
WILDFIRE_COPILOT_FIRMS_MAP_KEY (env var or .env) if you want raw satellite fire
detections in addition to EONET's curated events. NASA Earthdata is only needed to
regenerate ndvi_features.json (register free at
urs.earthdata.nasa.gov, then set
WILDFIRE_COPILOT_EARTHDATA_USERNAME / _PASSWORD) — the deployed app and a normal
train run just use the committed file. Everything else works unconfigured.
Requires a Google Gemini API key — free, no credit card, get one at
aistudio.google.com/apikey and set
GOOGLE_API_KEY (env var or .env; ChatGoogleGenerativeAI reads it
automatically, no project-specific env var name).
uvicorn wildfire_copilot.api.main:app --reload
# then open http://localhost:8000/app -- interactive map: click a location
# (or search a place, or a ZIP-code region) to score susceptibility and see
# the resolved address (OSM Nominatim reverse geocoding), then ask the agent
# a free-form follow-up about that location -- active fires nearby, red-flag
# warnings, mitigation/preparedness guidance, or anything else it has a tool for. Switch
# between 5 basemaps (streets/light/dark/satellite/terrain) from the layer
# control. Or hit the JSON API directly:
# GET /susceptibility?lat=39.3&lon=-121.4
# GET /geocode?q=Paradise,CA
# GET /reverse-geocode?lat=39.3&lon=-121.4
# GET /zip-scores/precomputed (per-ZIP choropleth data)
# GET /inspection-list?bbox=-122.6,38.2,-120.8,39.8&top_n=10
# GET /active-fires?bbox=-122.6,38.2,-120.8,39.8
# GET /map?bbox=-122.6,38.2,-120.8,39.8 (risk map HTML)
# GET /ask?q=What+does+the+plan+say+about+PSPS+criteriaTwo offline bake steps power the /app overlays -- rerun after retraining the
model (~10-20 min each, live API calls per point, same throttling/caching as
training):
python -m wildfire_copilot.bake_risk_grid # data/models/risk_grid.json
python -m wildfire_copilot.bake_zip_scores # data/models/zip_scores.json -- ZCTA boundaries via Census TIGERwebOr use the agent directly:
python -m wildfire_copilot.agentSee docs/ARCHITECTURE.md for full diagrams of the system end to end (data sources through deployment) and the live request lifecycle (a map click, and an agent follow-up question).
data/terrain.py,data/climate.py,data/landcover.py,data/infrastructure.py— susceptibility-model feature sources (elevation/slope/aspect, climate normals, fuel proxy, road/powerline distance).data/fire_history.py— NIFC historical fire perimeters (training labels) + random background point sampling.data/live_fires.py,data/red_flag.py— live wildfire/fire-weather signal.cache.py— disk cache (SQLite-backed, thread-safe) wrapping the point-feature lookups above -- training and serving hit the same live calls per point, and nearby queries (e.g. a scoring grid) round to the same cache key.features.py— assembles the labeled training set (concurrent fetch + enrich, retries on transient timeouts) and the spatial-block train/test split.train.py/predict.py— trains and serves the XGBoost susceptibility model.rag.py— chunks + indexes the two source documents (utility WMP PDF + FEMA guidance snapshot) into Chroma (local embedding model, no API cost), retrieval at query time.mapping.py— risk map (Folium). Susceptibility is a continuous [0,1] magnitude, so it's a one-hue sequential ramp (orange→red) rather than the categorical palette used for discrete hazard types in the sibling hazard-mcp project — see the dataviz skill's categorical-vs-sequential rule.data/geocode.py— place-name ↔ coordinates via OSM Nominatim (keyless, rate-limited client-side to respect its usage policy) -- both forward (search) and reverse (map click → address) geocoding.data/zipcodes.py— ZIP Code Tabulation Area (ZCTA) boundaries via US Census TIGERweb (keyless), generalized server-side for the frontend choropleth.bake_risk_grid.py,bake_zip_scores.py— precompute the point-grid and per-ZIP susceptibility overlays once, offline, rather than scoring ~100-200 points live on every page load (see the/appoverlays below).agent.py— LangGraph ReAct agent (Google Gemini via the Gemini Developer API) wiring all of the above as tools.api/main.py— FastAPI endpoints, including a free-form/askthat lets the agent decide which tools to call, and/appserving the interactive frontend.api/frontend.py— the/apppage: a single self-contained HTML string (Leaflet via CDN) rather than a templates dir, since the package installs non-editable in Docker and this avoids needing setuptools package-data config to ship a static file. Renders the precomputed ZIP-code choropleth (yellow→red, ColorBrewer YlOrRd) over a choice of 5 basemaps (streets/light/dark/satellite/terrain), plus four optional overlays off by default (historical fires, live active fires, the NDVI grid, point-resolution risk grid — each fetched lazily, only once actually toggled on) with a legend covering all of it. Clicking anywhere shows an instant nearest-ZIP estimate while the exact point score and reverse-geocoded address load, plus a free-text box to ask the agent about that location — questions accumulate in a running log rather than replacing each other. A separate persistent side-panel chat answers general questions or (when a location is selected) location-aware ones, using the same agent.
pytest tests/Offline unit tests cover map rendering/color-ramp logic and background-point
sampling. Each data/*.py module has a python -m wildfire_copilot.data.<module>
smoke test against the real live API.
Live demo: wildfire-copilot.onrender.com/app (API docs; Render free tier — spins down after 15 min idle, so the first request after a while cold-starts slowly).
Working MVP, fully verified end-to-end against the live deployment, not just
locally: susceptibility model trained on real historical fire/terrain/climate/
road-distance data with a spatially-blocked (leakage-free) train/test split;
RAG verified retrieving the correct passage from both source documents (/mitigation-docs/search);
/susceptibility verified; the LangGraph agent (/ask) verified giving a real,
correct response — switched from the direct Anthropic API to Google Gemini
(ChatGoogleGenerativeAI) specifically because Anthropic's API requires a funded
account and Gemini's free tier doesn't, which had been blocking live verification
of the agent end of this project. Landed on gemini-flash-lite-latest after two
more model swaps: gemini-3.6-flash/gemini-flash-latest both hit a 20
requests/day free-tier quota wall (unusable for a live demo), and
gemini-2.5-flash is no longer available to new API keys. The lite model has
never hit that quota, at the cost of an intermittent (~1/3 of responses) garbage
token prepended to answers, for which agent.py's _strip_garbage_preamble is a
narrow, tested mitigation. Docker image bakes the trained model, RAG index, and
both precomputed overlays in at build time (no manual post-deploy step, works on
Render's free tier with no persistent disk).
The /app interactive map — click a location or search a place name, see the
reverse-geocoded address and a susceptibility score, switch basemaps, ask the
agent a free-text follow-up — was verified with an actual headless-browser
click-through against a running server, not just curl against the JSON
endpoints, including the ZIP-code choropleth render, the agent correctly using
its other tools (e.g. live active-fire lookups) for questions beyond the score,
and correctly declining off-topic questions rather than answering them anyway.
Also fixed a real performance bug: susceptibility scoring made its ~6
independent feature lookups sequentially, so a single click could take
20-90+ seconds; they now run concurrently, and clicking additionally shows an
instant estimate from the precomputed ZIP overlay while the exact score loads.
Replace random background points with a proper spatial-blocking negative-sampling strategy— done (features.spatial_block_split); see Susceptibility model above.Add more features: slope/aspect, distance to nearest road/powerline— done.Add NDVI / vegetation-dryness trend— done. Real MODIS NDVI via NASA AppEEARS (a free Earthdata login,WILDFIRE_COPILOT_EARTHDATA_USERNAME/_PASSWORD— only needed to rebake the grid withbake_ndvi.py, not by the deployed app itself, since the result ships as a committed JSON lookup like the other two precomputed overlays). AppEEARS point tasks are async (minutes, not seconds), so this can't run on a live click — seedata/ndvi.py's module docstring.Cache live API responses— done (cache.py).Deploy behind a live demo URL— done, see Status above.
Footnotes
-
AppEEARS point-sample tasks are asynchronous (minutes, not seconds) — a real API, but the wrong shape for a live per-click request, so this is only ever run offline (
bake_ndvi.py) and its output committed as a static lookup, like the risk grid and ZIP scores. The deployed app itself needs no Earthdata credentials at all. ↩ -
Used here as a real, public source document for the RAG corpus — this project is not affiliated with or endorsed by PG&E. See their Community Wildfire Safety Program for the program the plan describes. ↩
-
A U.S. government work (public domain). Indexed from a committed text snapshot of ready.gov/wildfires rather than fetched live — CAL FIRE's own site (fire.ca.gov, readyforwildfire.org) was the first choice for this second source, but blocks all automated requests (403 on every path tested, including PDFs), so FEMA's guidance was used instead. ↩
