{"id":51186242,"url":"https://github.com/youneslaaroussi/fireguard","last_synced_at":"2026-06-27T10:03:15.222Z","repository":{"id":356086216,"uuid":"1230860791","full_name":"youneslaaroussi/fireguard","owner":"youneslaaroussi","description":"Satellite-triggered wildfire evacuation coordination for the Google Cloud Rapid Agent Hackathon","archived":false,"fork":false,"pushed_at":"2026-06-11T22:12:56.000Z","size":69782,"stargazers_count":1,"open_issues_count":3,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-11T22:19:30.668Z","etag":null,"topics":["agent-builder","elastic-mcp","fastapi","gemini","pydantic"],"latest_commit_sha":null,"homepage":"https://fireguard-cehsilz6aa-uc.a.run.app","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/youneslaaroussi.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-06T11:48:44.000Z","updated_at":"2026-06-11T22:13:00.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/youneslaaroussi/fireguard","commit_stats":null,"previous_names":["youneslaaroussi/fireguard"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/youneslaaroussi/fireguard","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/youneslaaroussi%2Ffireguard","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/youneslaaroussi%2Ffireguard/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/youneslaaroussi%2Ffireguard/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/youneslaaroussi%2Ffireguard/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/youneslaaroussi","download_url":"https://codeload.github.com/youneslaaroussi/fireguard/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/youneslaaroussi%2Ffireguard/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34848949,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agent-builder","elastic-mcp","fastapi","gemini","pydantic"],"created_at":"2026-06-27T10:03:14.559Z","updated_at":"2026-06-27T10:03:15.209Z","avatar_url":"https://github.com/youneslaaroussi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# FireGuard Whitepaper\n\n\u003cdiv align=\"center\"\u003e\n\n**Satellite-triggered wildfire evacuation coordination for the Google Cloud Rapid Agent Hackathon**\n\n\u003cp\u003e\n  \u003cimg src=\"./docs/assets/logos/nasa.svg\" alt=\"NASA\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/elastic.svg\" alt=\"Elastic\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/elasticsearch.svg\" alt=\"Elasticsearch\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/googlecloud.svg\" alt=\"Google Cloud\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/gemini.svg\" alt=\"Google Gemini\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/googlemaps.svg\" alt=\"Google Maps\" width=\"42\" /\u003e\n  \u0026nbsp;\u0026nbsp;\n  \u003cimg src=\"./docs/assets/logos/mapbox.svg\" alt=\"Mapbox\" width=\"42\" /\u003e\n\u003c/p\u003e\n\n![Python](https://img.shields.io/badge/Python-3.12-3776AB?logo=python\u0026logoColor=white)\n![FastAPI](https://img.shields.io/badge/FastAPI-0.136-009688?logo=fastapi\u0026logoColor=white)\n![React](https://img.shields.io/badge/React-18-61DAFB?logo=react\u0026logoColor=black)\n![Vite](https://img.shields.io/badge/Vite-7.2-646CFF?logo=vite\u0026logoColor=white)\n![Elasticsearch](https://img.shields.io/badge/Elasticsearch-Geospatial-005571?logo=elasticsearch\u0026logoColor=white)\n![Google_Cloud](https://img.shields.io/badge/Google_Cloud-Vertex_AI-4285F4?logo=googlecloud\u0026logoColor=white)\n![NASA_FIRMS](https://img.shields.io/badge/NASA-FIRMS-E03C31)\n![Mapbox](https://img.shields.io/badge/Mapbox-GL_JS-000000?logo=mapbox\u0026logoColor=white)\n\n\u003c/div\u003e\n\n---\n\n## Table of Contents\n\n- [The Problem](#the-problem)\n- [What FireGuard Does](#what-fireguard-does)\n- [The Incident](#the-incident)\n- [Scenario](#scenario)\n- [Quick Start](#quick-start)\n- [System Design](#system-design)\n  - [Runtime Architecture](#runtime-architecture)\n  - [Replay and Trigger Loop](#replay-and-trigger-loop)\n  - [Evacuation Workflow](#evacuation-workflow)\n  - [Indexed Data](#indexed-data)\n- [Threat Scoring Methodology](#threat-scoring-methodology)\n- [Design Notes](#design-notes)\n  - [Stable Event Contracts](#stable-event-contracts)\n  - [Agent Tooling](#agent-tooling)\n  - [Map Annotations](#map-annotations)\n  - [Route What-Ifs](#route-what-ifs)\n- [Tech Stack](#tech-stack)\n- [Project Structure](#project-structure)\n- [Verification](#verification)\n\n\u003cimg src=\"./docs/assets/illustrations/architecture.png\" alt=\"FireGuard system architecture\" width=\"100%\" /\u003e\n\n---\n\n## The Problem\n\nEvery summer, BC incident commanders face the same cascade of questions. They have minutes, with lives at stake.\n\nA wildfire ignites near a populated area. The fire is moving fast. The commander needs to know: **Which evacuation zones are in range? How many people? Which shelters are open right now? Which roads are safe to use? How long do we have?**\n\nIn 2024, BC recorded **51 evacuation orders** covering more than **4,100 properties**, and **112 evacuation alerts** covering more than **12,500 properties**. Wildfire suppression cost **$621 million**. On the worst days, over 330 fires were burning simultaneously across the province.\n\n### The coordination gap\n\nToday, assembling a situational picture during an active wildfire threat looks like this:\n\n| Task | How it's done today |\n|---|---|\n| Identify affected evacuation zones | GIS analyst queries a separate provincial system |\n| Check shelter availability | Phone calls to ESS facility coordinators |\n| Find open evacuation routes | Check DriveBC manually, cross-reference road event feeds |\n| Assess satellite detections | FIRMS data requires GIS expertise to interpret |\n| Evaluate a specific route | Manual check against each known constraint |\n| Update the map for field crews | Separate workflow, often on a different system |\n\nEach of those steps takes time. Phone calls go unanswered. Systems are not integrated. Data is stale. A complete situational picture can take **20 to 45 minutes** to assemble, in an incident where the fire behaviour can change every 10.\n\nIn Williams Lake on July 21, 2024, the fire went from ignition to Rank 4 fire behaviour in under an hour. The mayor, a 50-year resident, said he had never seen growth that fast. Evacuation alerts were issued for 3,000 properties. The decisions had to be made immediately, with fragmented tools.\n\n### Before FireGuard\n\n```\nHotspot detected by satellite\n        ↓\nGIS analyst pulls FIRMS data  (~10 min)\n        ↓\nCoordinator identifies affected zones  (~10 min)\n        ↓\nPhone calls to ESS shelters  (~15 min, often incomplete)\n        ↓\nDriveBC manual road check  (~10 min)\n        ↓\nVerbal debrief to incident commander\n        ↓\nCommander makes decision  (40–45 min after detection)\n        ↓\nMap updated separately for field crews\n```\n\n### After FireGuard\n\n```\nHotspot detected by satellite\n        ↓\nFireGuard receives threat event  (automatic, \u003c1 sec)\n        ↓\nGemini agent queries Elasticsearch MCP in parallel:\n  zones · shelters · road events · route safety · fire context\n        ↓\nStructured brief + annotated map delivered  (\u003c60 sec)\n        ↓\nCommander acts on a complete picture\n```\n\nThe output gives a specific recommendation: which zone, which shelter, which route, and what caveats. If a road closes mid-incident, the what-if route evaluation reruns in seconds, not minutes.\n\n---\n\n## What FireGuard Does\n\nFireGuard is an AI-assisted wildfire evacuation coordination app. It combines NASA FIRMS satellite detections, BC Wildfire Service context, evacuation zones, ESS facilities, road events, weather context, Elasticsearch geospatial queries, and a FireGuard agent workflow into one operator interface.\n\nThe workflow checks:\n\n- affected evacuation zones\n- nearby ESS shelter status\n- active road events\n- route safety from the zone centroid to open shelters\n- nearby fire detections for intensity context\n- map annotations for hotspots, zone centroids, shelters, blockages, and evaluated routes\n\nThe result is an evacuation brief plus visual map context. The app is a trigger-to-action workflow.\n\n\u003cimg src=\"./docs/assets/illustrations/judge-guide.svg\" alt=\"FireGuard — trigger, evidence, decision\" width=\"100%\" /\u003e\n\n---\n\n## The Incident\n\nOn **July 21, 2024 at 5:45 PM**, a tree fell onto a power line in the River Valley on the west side of Williams Lake, British Columbia. Within minutes the fire was burning at Rank 3 to 4 intensity. The mayor, a 50-year resident, said he had never seen fire grow that fast. Thick black smoke was visible from every corner of the city. Planes dropped red fire retardant at rooftop level.\n\nBy the end of that night, **440 properties were under evacuation order** and **3,000 more were on evacuation alert**. The City declared a State of Local Emergency. Across BC, 330 wildfires were burning simultaneously, with 977 firefighters and 178 aviation crews deployed province-wide.\n\nBy July 23 the River Valley fire was classified as *held*. Crews had stopped its spread. The evacuation alert was lifted. The city had narrowly avoided a catastrophe.\n\n\u003ctable\u003e\n\u003ctr\u003e\n\u003ctd width=\"58%\"\u003e\n\u003cimg src=\"./docs/assets/press/williams-lake-fire-globalnews.png\" alt=\"Aerial view of the Williams Lake River Valley wildfire, July 2024\" width=\"100%\" /\u003e\n\u003c/td\u003e\n\u003ctd width=\"40%\" valign=\"top\"\u003e\n\u003cimg src=\"./docs/assets/press/williams-lake-emergency-declaration.jpg\" alt=\"Williams Lake emergency declaration, July 22 2024\" width=\"100%\" /\u003e\n\u003cbr/\u003e\u003cbr/\u003e\n\u003cimg src=\"./docs/assets/press/williams-lake-fire-calms.jpg\" alt=\"Williams Lake fire calming, July 22 2024\" width=\"100%\" /\u003e\n\u003c/td\u003e\n\u003c/tr\u003e\n\u003c/table\u003e\n\n**Coverage:**\n\n- [Williams Lake residents warned to be ready to leave as wildfire nears city — CBC News, July 21](https://www.cbc.ca/news/canada/british-columbia/new-wildfire-erupts-in-williams-lake-1.7270867)\n- [Drones and water access banned as wildfire burns in city — CBC News, July 22](https://www.cbc.ca/news/canada/british-columbia/williams-lake-evacuation-july-22-1.7271234)\n- [City declares state of emergency, expands evacuation alert — CFJC Today, July 22](https://cfjctoday.com/2024/07/22/city-of-williams-lake-declares-local-state-of-emergency-expands-evacuation-alert-related-to-river-valley-wildfire/)\n- [Close call: wildfire burns to edge of Williams Lake — Global News, July 22](https://globalnews.ca/news/10636715/wildfire-burns-edge-williams-lake/)\n- [Wildfire being held, evacuation alert lifted — CBC News, July 23](https://www.cbc.ca/news/canada/british-columbia/williams-lake-wildfire-held-1.7273125)\n\nFireGuard replays the FIRMS satellite detections from this event window (July 17 to 25, 2024) against the actual evacuation zones, ESS shelters, and road events that were active at the time.\n\n---\n\n## Scenario\n\nThe bundled replay window covers **July 17 to 25, 2024**, the week the River Valley fire ignited. The 45-row FIRMS CSV snapshot captures the satellite detections from that period. When `NASA_FIRMS_MAP_KEY` is set, the backend fetches the full FIRMS dataset live before replaying.\n\nWhen a hotspot exceeds the FRP threshold and falls within 150 km of a seeded evacuation zone centroid, the backend emits a `threat` event and the UI opens the `fireguard_evacuation` workflow automatically.\n\nTo walk the full flow:\n\n1. Start the replay from the web UI.\n2. Watch FIRMS detections stream onto the map and event feed.\n3. When the threat arrives, the `fireguard_evacuation` workflow opens automatically.\n4. Inspect the tool trace: zones, shelters, roads, routes, fire context, map annotation, completion payload.\n5. Ask a route what-if question and inspect the route reevaluation.\n\n---\n\n## Quick Start\n\n```bash\npython3 -m venv .venv\n. .venv/bin/activate\npip install -r requirements.txt\n\ncd web\nnpm install\ncd ..\n\ncp .env.example .env\n./start.sh\n```\n\n`./start.sh` launches:\n\n| Service | URL |\n|---|---|\n| FastAPI backend | `http://127.0.0.1:8100` |\n| Vite frontend | `http://127.0.0.1:5174` |\n\nConfigure `.env` from `.env.example` for Elasticsearch, Mapbox, NASA FIRMS, and Vertex AI.\n\n---\n\n## System Design\n\n### Runtime Architecture\n\n```mermaid\nflowchart LR\n    UI[\"React operator UI\u003cbr/\u003eweb/src/ui/App.tsx\"]\n    Map[\"Mapbox panel\u003cbr/\u003eevents, zones, routes\"]\n    API[\"FastAPI backend\u003cbr/\u003eapp/main.py\"]\n    AgentAPI[\"Mounted intelligence API\u003cbr/\u003e/api/intelligence\"]\n    Adapter[\"ADK route adapter\u003cbr/\u003eapp/agent_runtime/api.py\"]\n    Tools[\"FireGuard evidence tools\u003cbr/\u003eapp/agent_runtime/tools.py\"]\n    Agent[\"Google ADK app\u003cbr/\u003eapp/agent_runtime/fireguard_agent.py\"]\n    Runtime[\"Vertex AI Agent Runtime\u003cbr/\u003ereasoningEngine\"]\n    ElasticMCP[\"Elastic MCP server\"]\n    ES[\"Elasticsearch indexes\"]\n    Sources[\"FIRMS CSV snapshot\u003cbr/\u003eBC context snapshots\u003cbr/\u003eBCWS ArcGIS services\"]\n    Model[\"Gemini 3.1 Pro\u003cbr/\u003eglobal model endpoint\"]\n\n    UI --\u003e|\"POST /api/replay/stream\"| API\n    API --\u003e|\"NDJSON context, events, threat\"| UI\n    UI --\u003e Map\n    UI --\u003e|\"POST /api/intelligence/sessions/.../chat/runs\"| AgentAPI\n    AgentAPI --\u003e Adapter\n    Adapter --\u003e Tools\n    Tools --\u003e ElasticMCP\n    Adapter --\u003e Runtime\n    Runtime --\u003e Agent\n    Agent --\u003e Model\n    ElasticMCP --\u003e ES\n    API --\u003e ES\n    Sources --\u003e API\n```\n\n\u003cimg src=\"./docs/assets/illustrations/source-to-brief.svg\" alt=\"FireGuard source-to-brief pipeline\" width=\"100%\" /\u003e\n\n### Replay and Trigger Loop\n\n```mermaid\nsequenceDiagram\n    participant UI as React UI\n    participant API as FastAPI replay stream\n    participant ES as Elasticsearch\n    participant WF as Evacuation workflow\n\n    UI-\u003e\u003eAPI: POST /api/replay/stream\n    API-\u003e\u003eES: create indexes and seed packaged data\n    API--\u003e\u003eUI: context message\n    loop FIRMS detections\n        API--\u003e\u003eUI: event message\n        API-\u003e\u003eAPI: compare hotspot to zone centroids\n        alt FRP \u003e= 50 and zone within 150 km\n            API--\u003e\u003eUI: threat message\n            UI-\u003e\u003eWF: start fireguard_evacuation\n        end\n    end\n    API--\u003e\u003eUI: done message\n```\n\n### ADK Agent Flow\n\n```mermaid\nflowchart TD\n    H[\"Human trigger payload\u003cbr/\u003ehotspot + zone + replay window\"]\n    D[\"Data Checks phase\u003cbr/\u003ezones · shelters · roads · fires · route\"]\n    E[\"Elastic MCP\u003cbr/\u003egeospatial queries\"]\n    A[\"Google ADK Agent\u003cbr/\u003eapp/agent_runtime/fireguard_agent.py\"]\n    R[\"Vertex AI Agent Runtime\u003cbr/\u003ereasoningEngine\"]\n    G[\"Gemini 3.1 Pro\u003cbr/\u003elocations/global\"]\n    T[\"FireGuard tool events\u003cbr/\u003egraph + map annotations\"]\n    U[\"UI-compatible event stream\u003cbr/\u003e/api/intelligence\"]\n\n    H --\u003e U\n    U --\u003e D\n    D --\u003e E\n    D --\u003e T\n    D --\u003e R\n    R --\u003e A\n    A --\u003e G\n    R --\u003e U\n```\n\n\u003cimg src=\"./docs/assets/illustrations/evacuation-loop.svg\" alt=\"FireGuard evacuation workflow — signal to action\" width=\"100%\" /\u003e\n\nThe running intelligence path is Google ADK on Vertex AI Agent Runtime. The FastAPI route under `/api/intelligence` keeps the frontend contract stable for sessions, runs, graph events, and chat history. For replay threats, the route first performs the FireGuard evidence phase through Elastic MCP: evacuation zones, shelters, road events, FIRMS detections, BCWS context, route evaluation, map annotation, and action plan. It then sends the compact evidence package to Agent Runtime so Gemini 3.1 Pro writes the final incident brief.\n\nCurrent deployed resource:\n\n```text\nprojects/127704576091/locations/us-central1/reasoningEngines/3357933250239528960\n```\n\nThe Agent Runtime resource runs in `us-central1`. The Gemini model call is pinned to `gemini-3.1-pro-preview` with `GOOGLE_CLOUD_LOCATION=global`.\n\n### Indexed Data\n\n| Index suffix | Contents | Source in repo |\n|---|---|---|\n| `firms` | FIRMS hotspot detections and weather/place enrichment fields | `data/replay/bc_cariboo/firms_snapshot.csv` |\n| `firms-cache` | FIRMS fetch cache metadata | `app/main.py` |\n| `bcws-incidents` | BCWS active fire incidents | BCWS ArcGIS query path in `app/main.py` |\n| `bcws-perimeters` | BCWS perimeter shapes | BCWS ArcGIS query path in `app/main.py` |\n| `bcws-cache` | BCWS area cache metadata | `app/main.py` |\n| `zones` | Evacuation zone centroids and polygons | `data/public/bc/historical_fire_evacuation_zones_snapshot.json` |\n| `shelters` | ESS facility status and location | `data/public/bc/public_emergency_context_snapshot.json` |\n| `road-events` | Road event locations and shapes | `data/replay/bc_cariboo/road_events_snapshot.json` |\n\nPackaged data currently includes 45 FIRMS rows, 3 evacuation zones, 10 ESS facilities, 8 evacuation order or alert records, 4 policy snippets, 1 road event, and 1 weather snapshot.\n\n---\n\n## Why Elasticsearch\n\nThe packaged dataset is deliberately small: 45 FIRMS rows, 3 evacuation zones, 10 shelters, 1 road event. That is enough to run the replay and trigger a workflow without any external API keys.\n\nThe live cluster for this event window tells a different story:\n\n| Index | Documents | Size |\n|---|---|---|\n| `fireguard-firms` | **38,744** | 17.0 MB |\n| `fireguard-bcws-incidents` | 633 | 284 KB |\n| `fireguard-bcws-perimeters` | 182 | 3.9 MB |\n| `fireguard-shelters` | 10 | 15 KB |\n| `fireguard-zones` | 3 | 11 KB |\n| `fireguard-road-events` | 1 | 11 KB |\n| **Total** | **39,599** | **20.8 MB** |\n\n38,744 FIRMS detections for the Williams Lake / Cariboo region, each enriched with Open-Meteo weather (wind speed, wind direction, gusts, temperature, humidity, precipitation) and Google Maps reverse geocoding (formatted address, place type). 182 BCWS fire perimeter polygons as `geo_shape` documents. 633 active incident records.\n\nThat is the dataset the agent queries live during a workflow run. Every `fireguard_search_zones` call, every `fireguard_evaluate_route`, and every shelter lookup runs against this index as the workflow executes.\n\nThree things pushed the data layer toward Elasticsearch specifically:\n\nThe agent needs to find zones near a hotspot, shelters near a zone centroid, and road events near a route corridor, all at different radii, all on different indexes, all within the same workflow run. `geo_distance` and `_geo_distance` sort on a `geo_point` field handle this exactly, without any application-layer geometry math beyond the haversine check on the returned hits.\n\nFire perimeters are Polygon and MultiPolygon shapes covering thousands of hectares. Querying which perimeters overlap a given search area requires `geo_shape` with `relation: intersects`. There is no equivalent in a document store that only indexes points.\n\n`fireguard_evaluate_route` interpolates a straight-line route into segments, computes a corridor, and runs a `geo_distance` query to pull all fire detections and road closures near that corridor in one round trip. Pulling all records and filtering in Python does not scale once the FIRMS index holds thousands of detections per season.\n\nThe MCP boundary keeps this clean: the agent never touches Elasticsearch directly. It calls a tool, the tool constructs the query body, the Elastic MCP server executes it, and the agent gets structured results it can reason over. The query is auditable in the tool trace. The agent cannot construct arbitrary queries. It can only call the tools FireGuard exposes.\n\n## Elasticsearch MCP Integration\n\nElasticsearch is the evidence layer the agent reasons over. Every decision the Gemini agent makes during an evacuation workflow is grounded in an Elasticsearch query executed through the Elastic MCP server.\n\n### How the MCP server connects\n\nThe Elastic MCP server runs as a Docker stdio container launched at agent startup:\n\n```bash\ndocker run -i --rm \\\n  --add-host=host.docker.internal:host-gateway \\\n  -e ES_URL -e ES_API_KEY \\\n  docker.elastic.co/mcp/elasticsearch stdio\n```\n\nThe agent calls the MCP `search` tool with an index name and a full Elasticsearch query body. The MCP server executes the query against the configured cluster and returns the raw hits. FireGuard's tool layer parses the response and hands structured results back to the agent. This means the agent never touches Elasticsearch directly. All queries go through the MCP boundary, which keeps the tool interface clean and auditable in the trace panel.\n\n### What each tool actually queries\n\n| Tool | Index | Query type | What it finds |\n|---|---|---|---|\n| `fireguard_search_zones` | `fireguard-zones` | `geo_distance` + `_geo_distance` sort | Evacuation zones within 150 km of the hotspot, sorted by distance. Returns population, homes, and centroid coordinates. |\n| `fireguard_search_shelters` | `fireguard-shelters` | `geo_distance` + optional `term` on `status` | ESS facilities within 400 km of the zone centroid. The 400 km radius is intentional: the only open shelter during the Williams Lake event was 280 km south in Merritt. |\n| `fireguard_search_road_events` | `fireguard-road-events` | `geo_distance` + text matching on event type | Road closures and restrictions near the evacuation corridor. Closure detection uses keyword matching against `event_type`, `severity`, `title`, and `description` fields. |\n| `fireguard_evaluate_route` | `fireguard-firms` + `fireguard-road-events` | `geo_distance` on interpolated polyline corridor | Checks a straight-line route against both active fire detections (5 km buffer) and road closures (2 km buffer). Returns `safe: true/false` with specific risk flags and evidence. |\n| `fireguard_search_events` | `fireguard-firms` | `geo_distance` + `range` on `acquired_at` | FIRMS hotspot detections near the fire, filtered by date window. Returns FRP, confidence, source satellite, and weather enrichment. |\n| `fireguard_bcws_context` | `fireguard-bcws-incidents` + `fireguard-bcws-perimeters` | `geo_bounding_box` + `geo_shape` intersects | BCWS active incidents and fire perimeter polygons intersecting the search area. The perimeter query uses a `geo_shape` envelope with `relation: intersects`, the only full polygon intersection query in the workflow. |\n| `fireguard_stats` | `fireguard-firms` + incidents + perimeters | `min`/`max` aggregations + `terms` aggregation on `source` | Index counts, date span, and source breakdown for the sidebar stats panel. |\n\n### Route evaluation in detail\n\n`fireguard_evaluate_route` is the most complex query in the system. It interpolates the origin-to-destination route into 4 segments, computes a corridor around the midpoint, and runs two parallel Elasticsearch queries:\n\n1. A `geo_distance` query on `fireguard-firms` fetching up to 200 fire detections near the corridor, sorted by distance from the route midpoint. Each hit is tested with `min_distance_to_polyline_km`. If any fire falls within 5 km of the route polyline, the route is flagged unsafe.\n\n2. A `geo_distance` query on `fireguard-road-events` fetching up to 100 road events near the corridor. Each closure is tested against the same polyline. Closures within 2 km mark the route blocked.\n\nThe tool also accepts `hypothetical_closures`: a list of lat/lon points with labels that are injected as synthetic road constraints. This is how the what-if questions work: the operator types \"what if Highway 97 closes\" and the agent calls `fireguard_evaluate_route` with the closure coordinates, getting a new safe/blocked determination without touching the index.\n\n### Geo field types and why they matter\n\nThree indexes use `geo_shape` rather than `geo_point`:\n\n- **`fireguard-bcws-perimeters`**: fire perimeter polygons. Queried with an envelope shape and `relation: intersects` to find all perimeters overlapping a bounding box.\n- **`fireguard-zones`**: evacuation zone boundaries as Polygon or MultiPolygon GeoJSON. The centroid is stored separately as a `geo_point` for fast distance queries; the full shape is available for containment checks.\n- **`fireguard-road-events`**: road closure extents as LineString or MultiLineString. The representative point is a `geo_point` for distance filtering; the geometry captures the full closure extent.\n\nUsing `geo_shape` for perimeters and zone boundaries means FireGuard can answer spatial questions that `geo_point` cannot, like whether a fire perimeter overlaps a given search area, or whether a road closure geometry intersects an evacuation corridor.\n\n### Data ingestion and enrichment\n\nEach FIRMS hotspot record is enriched before indexing:\n\n- **Weather**: wind speed, wind direction, temperature, humidity, and precipitation from Open-Meteo archive API, cached by rounded coordinate and hour.\n- **Place**: formatted address and place type from Google Maps reverse geocoding, cached by coordinate at 2 decimal places.\n\nThe enriched records are indexed into `fireguard-firms` with stable field shapes declared in the mapping before any documents are inserted. Elasticsearch rejects later schema-incompatible inserts, so the mapping is the contract. It is defined once in `app/main.py` and never changed at runtime.\n\nBCWS incidents and perimeters are fetched live from the BC Wildfire Service ArcGIS feature servers on replay start, cached for 1 hour, and indexed into `fireguard-bcws-incidents` and `fireguard-bcws-perimeters`.\n\n---\n\n## Threat Scoring Methodology\n\nFireGuard computes a live **threat score** (0 to 100) for every NASA FIRMS satellite detection during replay. The scoring model is grounded in peer-reviewed remote sensing literature and uses the same two physical variables that drive the binary threat alert.\n\n### Why Fire Radiative Power?\n\nFire Radiative Power (FRP, in megawatts per pixel) is the instantaneous rate of radiative energy released by combustion. It is the primary intensity metric in the NASA FIRMS product family (MODIS MOD14/MYD14 and VIIRS VNP14) because it correlates directly with mass of fuel consumed, combustion completeness, and smoke emission rate.\n\n\u003e \"FRP provides an instantaneous measure of the fire's rate of energy release, making it a physically meaningful and sensor-agnostic indicator of fire intensity.\"\n\u003e *Wooster et al. (2005), \"Retrieval and analysis of combustion parameters from the Fire Radiative Energy measurements\"*\n\n#### Detection floor — 50 MW\n\nMODIS Collection 6 has a theoretical minimum detectable FRP of roughly 56 MW per pixel under daytime conditions; the VIIRS 750 m product reaches ~13 MW. However, the widely-used SEVIRI geostationary product, which provides the operational benchmark for live fire monitoring in Europe and Africa, struggles to detect fires below 50 MW and systematically underestimates regional totals by 40 to 50% for low-intensity fires in that range (Roberts \u0026 Wooster 2008; Kumar et al. 2011). The 50 MW gate therefore represents the practical lower boundary below which satellite detections become too unreliable to drive automated alerts.\n\n#### Scale ceiling — 300 MW\n\nIchoku et al. (2008) documented the full global MODIS FRP distribution as 0.02 to 1,866 MW per pixel. In the Canadian boreal forest context, extreme crown fires can push a single VIIRS pixel to several hundred megawatts, but the 300 MW value represents a practical saturation point above which the marginal threat increase per additional MW is small. The fire is already generating enough heat to spread into any adjacent fuel regardless of exact intensity. Using 300 MW as a ceiling normalises the FRP contribution to the 0 to 1 range:\n\n$$\\text{FRP component} = \\min\\!\\left(0.6,\\ \\frac{\\text{FRP} - 50}{250} \\times 0.6\\right)$$\n\n### Why proximity to evacuation zones?\n\nDistance from a fire detection to the nearest populated evacuation zone centroid is the direct operational variable: it determines how quickly fire behaviour can threaten populated areas and how much time coordinators have to act. Ohi \u0026 Kim (2022) model evacuation host-community catchment areas using a 150 km planning radius, which matches the BC context where rural communities are spread across large distances and inter-city evacuation corridors can easily span 100 to 200 km.\n\nThe 150 km radius is also the engagement boundary used in `threat_for_event()`, the function that fires the binary threat alert. The score therefore uses the identical gate to ensure the score is exactly consistent with when an alert fires.\n\nProximity contribution decays linearly from 40 points at zero distance to 0 points at the boundary:\n\n$$\\text{Proximity component} = \\left(1 - \\frac{d_{\\min}}{150}\\right) \\times 0.4$$\n\n### Full scoring formula\n\nBoth components are only evaluated when the event passes the binary threat gate ($\\text{FRP} \\geq 50$ and $d_{\\min} \\leq 150\\text{ km}$); events outside these bounds score zero.\n\n$$S = \\min\\!\\left(100,\\ \\underbrace{\\frac{\\text{FRP} - 50}{250} \\times 60}_{\\text{intensity (0–60)}} + \\underbrace{\\left(1 - \\frac{d_{\\min}}{150}\\right) \\times 40}_{\\text{proximity (0–40)}}\\right)$$\n\nWhere:\n- $\\text{FRP}$ — Fire Radiative Power in megawatts (NASA FIRMS pixel value)\n- $d_{\\min}$ — great-circle distance in kilometres to the nearest BC evacuation zone centroid (Haversine formula)\n\nThe **60/40 weight split** intentionally favours intensity over proximity. A very high-FRP fire further from a zone is operationally significant because spotting and wind-driven spread can close a 100 km gap in hours. A low-FRP smoulder adjacent to a zone, by contrast, is unlikely to trigger rapid spread without intensification.\n\n### Score thresholds visualised\n\n| Score range | Colour | Interpretation |\n|---|---|---|\n| 1–24 | 🟢 Green | Low concern — detection near boundary of engagement zone or low intensity |\n| 25–49 | 🟡 Yellow | Moderate — notable FRP or moderate proximity, warrants monitoring |\n| 50–74 | 🟠 Orange | High — significant intensity and/or within ~75 km of zone |\n| 75–100 | 🔴 Red | Critical — high-intensity fire within close proximity to populated zone |\n\nScores are emitted as `threat_score` on every `event` NDJSON message from the replay stream and rendered as scatter labels on the Mapbox map, coloured by threshold band.\n\n### References\n\n- Wooster, M.J., Roberts, G., Perry, G.L.W., \u0026 Kaufman, Y.J. (2005). Retrieval and analysis of combustion parameters from the Fire Radiative Energy measurements: FRP derivation and lava eruption. *Journal of Geophysical Research: Atmospheres*, 110(D24).\n- Roberts, G., \u0026 Wooster, M.J. (2008). Fire detection and fire characterization over Africa using Meteosat SEVIRI. *IEEE Transactions on Geoscience and Remote Sensing*, 46(4), 1200–1218.\n- Kumar, S.S., Roy, D.P., Boschetti, L., \u0026 Kremens, R. (2011). Exploiting the power law distribution properties of satellite fire radiative power retrievals. *Journal of Geophysical Research: Atmospheres*, 116(D19).\n- Ichoku, C., Giglio, L., Wooster, M.J., \u0026 Remer, L.A. (2008). Global characterization of biomass-burning patterns using satellite measurements of fire radiative energy. *Remote Sensing of Environment*, 112(6), 2950–2962.\n- NASA LP DAAC. MOD14A1 MODIS/Terra Thermal Anomalies/Fire Daily L3 Global 1 km SIN Grid V006. NASA EOSDIS Land Processes DAAC. https://doi.org/10.5067/MODIS/MOD14A1.006\n- Ohi, J.M., \u0026 Kim, G. (2022). Wildfire evacuation host community planning: Estimating capacity from population and distance. *International Journal of Disaster Risk Reduction*, 71, 102791.\n\n---\n\n## Design Notes\n\n### Stable Event Contracts\n\nElasticsearch mappings are declared in `app/main.py` before documents are inserted. The FireGuard event payload keeps fixed shapes for fields such as `location`, `weather`, `place`, `geometry`, and route output so later inserts do not collide with prior dynamic mappings.\n\nThe threat payload is narrow by design:\n\n```json\n{\n  \"type\": \"threat\",\n  \"hotspot\": {\n    \"lat\": 52.1,\n    \"lon\": -121.9,\n    \"frp\": 50.0,\n    \"confidence\": \"nominal\",\n    \"source\": \"VIIRS_NOAA20_SP\",\n    \"acquired_at\": \"2024-07-21T18:00:00+00:00\"\n  },\n  \"zone\": {\n    \"name\": \"Williams Lake River Valley\",\n    \"population\": 1548,\n    \"homes\": 618,\n    \"latitude\": 52.1,\n    \"longitude\": -122.1,\n    \"distance_km\": 12.34\n  }\n}\n```\n\n### Agent Tooling\n\nThe evacuation agent gets a constrained tool surface so it can move from detection to decision without broad exploration.\n\n| Tool | Purpose |\n|---|---|\n| `fireguard_stats` | Count indexed FIRMS and BCWS records |\n| `fireguard_search_events` | Search FIRMS detections by location, radius, and time |\n| `fireguard_search_zones` | Find evacuation zones near a point |\n| `fireguard_search_shelters` | Find ESS facilities near a zone centroid |\n| `fireguard_search_road_events` | Find road events near the evacuation area |\n| `fireguard_evaluate_route` | Check an origin-to-destination route against indexed fire and road constraints |\n| `fireguard_bcws_context` | Retrieve BCWS incident and perimeter context |\n| `fireguard_map_annotation` | Push markers and routes into the UI map |\n| `fireguard_actions` | Push a structured incident action plan into the UI |\n\n### Map Annotations\n\n`fireguard_map_annotation` returns a compact annotation object with `markers`, `routes`, and a short `message`. The React intelligence panel passes that object back to the map through `onAnnotation`, allowing the analysis to draw the same shelters, blockages, and recommended route that the brief describes.\n\n### Route What-Ifs\n\n`fireguard_evaluate_route` accepts normal route endpoints and can also evaluate operator constraints through `hypothetical_closures` or `ignore_closures`. That lets the agent answer questions such as how the evacuation action changes if a corridor closes or reopens during an incident.\n\n---\n\n## Tech Stack\n\n| Layer | Technology |\n|---|---|\n| Backend | FastAPI, Pydantic, uvicorn |\n| Agent runtime | Google ADK on Vertex AI Agent Runtime |\n| Data store | Elastic MCP over Elasticsearch with `geo_point` and `geo_shape` mappings |\n| Source data | NASA FIRMS, BCWS ArcGIS services, packaged BC public context snapshots |\n| Frontend | React 18, Vite, TypeScript |\n| Mapping | Mapbox GL JS |\n| Intelligence UI | React Markdown, Blueprint, Monaco, XYFlow |\n| Testing | pytest, TypeScript build |\n\n---\n\n## Project Structure\n\n```text\nfireguard/\n├── app/\n│   ├── main.py                 # FastAPI app, replay stream, index creation, threat trigger\n│   ├── geo.py                  # Geospatial helpers\n│   └── agent_runtime/\n│       ├── api.py              # Mounted intelligence API backed by Agent Runtime\n│       ├── fireguard_agent.py  # Google ADK agent definition\n│       ├── tools.py            # FireGuard Elastic MCP tools\n│       └── tool_models.py      # Tool payload contracts\n├── data/\n│   ├── public/bc/              # Evacuation, ESS, and policy snapshots\n│   └── replay/bc_cariboo/      # FIRMS, road, and weather snapshots\n├── docs/assets/                # Logos and README illustrations\n├── web/\n│   ├── src/ui/                 # Replay map interface\n│   └── src/intelligence/       # Workflow panel, graph, chat, tool feed\n├── tests/                      # API and data contract tests\n├── start.sh                    # Backend and frontend launcher\n└── .env.example                # Runtime keys and configuration values\n```\n\n---\n\n## Verification\n\nRun backend tests:\n\n```bash\npytest\n```\n\nBuild the frontend:\n\n```bash\ncd web\nnpm run build\n```\n\nFor a local smoke check, run `./start.sh`, open `http://127.0.0.1:5174`, send a FireGuard message, and confirm that the intelligence panel shows an ADK Agent run with Agent Runtime tool events.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyouneslaaroussi%2Ffireguard","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyouneslaaroussi%2Ffireguard","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyouneslaaroussi%2Ffireguard/lists"}