Stormy AI is a LangGraph weather-briefing agent. You give it a place name; it geocodes the location, pulls live NOAA and partner data (NWS products, GFS, HRRR, MRMS, NEXRAD, GOES GLM, model soundings), runs deterministic precipitation diagnostics, and uses a configurable LLM (local Ollama or Hugging Face Inference Providers) to write a detailed markdown briefing.
Each run produces a structured weather briefing with:
- Headline and bottom line
- Forecast area (NWS forecast-zone locator map)
- Active NWS alerts
- Current weather (surface obs + radar/precip/lightning + diagnosis)
- Current synoptic setup (Area Forecast Discussion plus latest-cycle GFS)
- GFS regional charts and point guidance through 72 hours
- HRRR analysis and HRRR-derived model sounding
- Outlook from the forecast discussion
- Day-by-day forecast for the next three days
Briefings are written locally under briefings/ and, by default, uploaded to S3. Each saved file includes Updated and Next update times aligned to the EventBridge cadence (hourly 8am–8pm US Eastern, plus 4am overnight). After upload, a bucket-root latest.txt pointer is updated with the newest briefing s3:// URI. Radar PNGs from plot_nexrad_level2, METAR station-model PNGs from plot_metar_observations, GFS chart PNGs from get_gfs_guidance, and cached forecast-zone maps from get_forecast are uploaded to the same bucket. Embedded images in the markdown use public HTTPS URLs. Pass --local (or set storage.upload_to_s3: false) to keep briefings and plots on disk only.
User (CLI or ECS task)
→ briefing.run_briefing(location)
→ LangGraph: reset → agent ⇄ tools → collect_weather → agent
→ LLM + 12 weather tools
→ diagnose_precipitation when MRMS + HRRR are ready
→ markdown briefing (+ forecast-zone / radar / GFS images)
→ local file (+ S3 upload / latest.txt when upload_to_s3 is enabled)
- LangGraph orchestrates the tool loop and shared weather state.
- Tools fetch facts from public APIs and open data (no weather API keys).
- Diagnostics fuse MRMS/HRRR/(optional NEXRAD/GLM) into an authoritative precip/storm JSON block injected into the system prompt.
- The LLM narrates and structures the briefing; it is instructed not to invent observations.
Deep dive: docs/AGENT.md. Per-tool docs: docs/tools/. Deployment: docs/DEPLOYMENT.md.
- Python ≥ 3.14
- uv (recommended) or pip
- LLM backend — one of:
- Ollama running locally with a chat model (e.g.
gemma4:latest), or - Hugging Face Inference Providers with
HF_TOKENset (see Configuration)
- Ollama running locally with a chat model (e.g.
- System libraries for GRIB/NetCDF and cartography as needed by
cfgrib/eccodes/ Cartopy / Py-ART on your OS - AWS credentials (optional locally) — required only when S3 uploads are enabled (
storage.upload_to_s3: true, the default)
# Install core package + dependencies (add --group dev for LangSmith Studio / lint)
uv sync
# Copy secrets template; add HF_TOKEN and LANGSMITH_API_KEY as needed
cp .env.example .env
# For Ollama: pull / start the model (name must match config.yaml llm.model)
ollama pull gemma4:latestpython main.py # default: Atco, NJ 08004
python main.py "Denver, CO"
python main.py --local # local files only; skip S3 uploads
python main.py --local "Denver, CO"The CLI prints the briefing and writes a timestamped file under briefings/. With S3 uploads enabled (default), it also uploads the briefing and plot images when credentials are available, and updates s3://<bucket>/latest.txt with the new briefing URI. Forecast-zone, radar, and GFS images are embedded as sized HTML <img> tags (zone maps use width="480"; radar/GFS use 720) — public HTTPS URLs when uploaded, otherwise local absolute paths. With --local, artifacts stay under briefings/, radar_plots/, metar_plots/, model_plots/, and forecast_zones/.
langgraph.json registers the compiled graph (src/stormy_ai/agent.py:graph) so you can inspect and invoke it in LangSmith Studio.
# Installs langgraph-cli[inmem] from the dev dependency group
uv sync --group dev
# Ensure .env has LANGSMITH_API_KEY (see .env.example)
uv run langgraph devThe CLI starts a local Agent Server and prints a Studio URL (typically https://smith.langchain.com/studio/?baseUrl=http://127.0.0.1:2024). In Studio, select the agent graph and invoke it with a messages input — for example a human message asking for a weather briefing for a location (same shape as graph.invoke in briefing.py).
On Safari, use uv run langgraph dev --tunnel if Studio cannot reach localhost.
Docker builds, linting, and ECS/Terraform helpers live in the Makefile. List every target and what it does:
make helpRunning plain make (no target) shows the same summary.
Build and run the agent container (linux/arm64):
make build
make local_run # uses HF_TOKEN and AWS creds from .env / aws configureOr build and run directly (pass a location as the final argument):
docker buildx build --platform linux/arm64 -t wx_briefing_agent .
docker run --rm \
-e HF_TOKEN=... \
-e AWS_ACCESS_KEY_ID=... \
-e AWS_SECRET_ACCESS_KEY=... \
-e AWS_DEFAULT_REGION=us-east-1 \
wx_briefing_agent "Denver, CO"See docs/DEPLOYMENT.md for ECR push, ECS Fargate, and the EventBridge Scheduler cadence (hourly 8am–8pm ET, plus 4am overnight).
Settings live in config.yaml. Copy .env.example to .env for secrets.
Ollama — local model, no API key:
llm:
provider: ollama
model: gemma4:latestHugging Face Inference Providers — cloud model via the HF router:
llm:
provider: huggingface
model: zai-org/GLM-5.3-Flash
huggingface:
inference_provider: basetenSet HF_TOKEN in .env (create a token at huggingface.co/settings/tokens). inference_provider is appended to the model ID (:baseten) for the HF router.
Environment variables override config.yaml when set:
| Variable | Purpose |
|---|---|
STORMY_CONFIG |
Path to config file (default: config.yaml) |
STORMY_LLM_PROVIDER |
ollama or huggingface |
STORMY_LLM_MODEL |
Model ID / Ollama tag |
STORMY_LLM_TEMPERATURE |
Chat temperature |
STORMY_HF_INFERENCE_PROVIDER |
HF Inference Provider (e.g. baseten, deepinfra) |
HF_TOKEN |
Hugging Face API token (required for huggingface provider) |
OLLAMA_MODEL |
Ollama model tag (legacy override for llm.model) |
OLLAMA_BASE_URL |
Ollama API base URL |
| Variable | Default | Purpose |
|---|---|---|
BRIEFING_DIR |
briefings |
Local markdown output directory |
BRIEFING_IMAGE_WIDTH |
720 |
Default width (px) for embedded <img> tags (forecast-zone maps use 480) |
RADAR_PLOT_DIR |
radar_plots |
Local NEXRAD PNG directory |
METAR_PLOT_DIR |
metar_plots |
Local METAR station-model PNG directory |
GFS_MODEL_PLOT_DIR |
model_plots |
Local GFS chart PNG directory |
FORECAST_ZONE_PLOT_DIR |
forecast_zones |
Local forecast-zone PNG cache directory |
STORMY_UPLOAD_TO_S3 |
true |
Upload briefings/plots to S3 (false = same as --local) |
BRIEFING_S3_BUCKET |
stormy-ai-files |
S3 bucket for briefing uploads |
BRIEFING_S3_PREFIX |
briefings |
Key prefix for briefing markdown |
BRIEFING_LATEST_S3_KEY |
latest.txt |
Bucket-root key holding the newest briefing s3:// URI |
RADAR_S3_BUCKET |
stormy-ai-files |
S3 bucket for radar plot uploads |
RADAR_S3_PREFIX |
radar |
Key prefix for radar PNGs |
METAR_S3_BUCKET |
stormy-ai-files |
S3 bucket for METAR plot uploads |
METAR_S3_PREFIX |
metar |
Key prefix for METAR PNGs |
GFS_S3_BUCKET |
stormy-ai-files |
S3 bucket for GFS chart uploads |
GFS_S3_PREFIX |
models/gfs |
Key prefix for GFS chart uploads |
FORECAST_ZONE_S3_BUCKET |
same as briefing bucket | S3 bucket for cached forecast-zone PNGs |
FORECAST_ZONE_S3_PREFIX |
forecast_zones |
Key prefix (<prefix>/<ZONE_ID>.png) |
STORMY_S3_PUBLIC_BASE |
(empty) | Override HTTPS base for embed URLs (e.g. CloudFront) |
In config.yaml, the same toggle is storage.upload_to_s3 (default true).
S3 object layout:
s3://stormy-ai-files/briefings/<YYYY-MM-DD>/<zip_code>/<HH_MM>.md
s3://stormy-ai-files/latest.txt ← s3:// URI of newest briefing
s3://stormy-ai-files/radar/<YYYY-MM-DD>/<HH>_<MM>.png
s3://stormy-ai-files/metar/<YYYY-MM-DD>/<HH>_<MM>.png
s3://stormy-ai-files/models/gfs/<YYYY-MM-DD>/<image_type>/<forecast_hour>.png
s3://stormy-ai-files/forecast_zones/<ZONE_ID>.png ← cached; reused across briefings
Public embeds require a bucket policy that allows s3:GetObject on briefings/*, radar/*, metar/*, models/*, and forecast_zones/*.
No API keys are required for NWS, Open-Meteo geocoding, MRMS, HRRR (via Herbie), NEXRAD, or GLM open data. The NWS client sends a fixed User-Agent.
make help # list all Makefile targets
make lint # flake8 + isort check
make format # black + isort
uv run python -m unittest discover -s tests -v
uv run langgraph dev # LangSmith Studio (requires uv sync --group dev)| Tool | Why it is used |
|---|---|
geocode_location |
Place name → lat/lon for every other tool |
current_conditions |
Official nearest METAR/ASOS observation |
get_alerts |
Authoritative watches / warnings / advisories |
get_forecast |
Official 12-hour periods + hourly rows (~3 days) + cached forecast-zone map |
forecast_discussion |
WFO reasoning for synoptic setup and outlook |
get_mrms_precipitation |
Current precip rate and nearby echoes (CONUS) |
get_hrrr_environment |
Model thermo, precip-type flags, CAPE/CIN |
get_gfs_guidance |
Latest coherent GFS cycle, point guidance, and day 1–3 surface/500/850/300-mb charts |
analyze_nexrad_level2 |
Site radar structure and dual-pol detail |
plot_nexrad_level2 |
Radar image for the briefing (local; S3 when uploads enabled) |
plot_metar_observations |
Regional METAR/ASOS station-model map |
get_lightning |
Recent GLM total-lightning activity |
analyze_current_skewt |
Full MetPy sounding analysis from HRRR |
After MRMS and HRRR return, diagnose_precipitation builds the deterministic diagnosis the model must respect for current precip and storm character. See docs/tools/DIAGNOSTICS.md.
stormy_ai/
├── main.py CLI entry
├── config.yaml LLM, briefing, paths, and storage settings
├── langgraph.json LangGraph CLI / LangSmith Studio config
├── .env.example Secrets template (copy to .env)
├── Dockerfile Container image (linux/arm64)
├── Makefile Docker build, lint, Terraform/ECS helpers
├── README.md This file
├── pyproject.toml Package and dependencies
├── infra/ Terraform for ECS Fargate + EventBridge Scheduler
├── docs/
│ ├── AGENT.md LangGraph agent orchestration
│ ├── DEPLOYMENT.md Docker, ECR, and ECS Fargate
│ └── tools/ Per-tool and diagnostics docs
├── tests/ Unit tests
├── briefings/ Generated markdown briefings
├── radar_plots/ Generated radar PNGs
├── metar_plots/ Generated METAR station-model PNGs
├── model_plots/ Generated regional GFS chart PNGs
├── forecast_zones/ Cached forecast-zone PNGs (local)
└── src/stormy_ai/
├── agent.py LangGraph definition (`graph`; Studio entry)
├── briefing.py run_briefing(), markdown + optional S3 output
├── config.py config.yaml loader (`upload_to_s3`, paths)
├── llm.py LLM provider factory
├── utils.py S3 upload (gated), tool-content parsing
├── diagnostics.py Deterministic precip / storm fusion
├── prompts/ SYSTEM_PROMPT
└── tools/ LangChain tool implementations
| Doc | Contents |
|---|---|
docs/AGENT.md |
LangGraph graph, state, tool loop, diagnosis injection |
docs/DEPLOYMENT.md |
Docker image, ECR, Terraform, ECS Fargate, scheduled runs |
docs/tools/GEOCODE.md |
Open-Meteo place → lat/lon |
docs/tools/NWS.md |
Forecast, zone maps, alerts, obs, AFD |
docs/tools/MRMS.md |
Current precip / reflectivity mosaic |
docs/tools/HRRR.md |
Model environment and precip-type guidance |
docs/tools/GFS.md |
GFS point guidance and regional charts |
docs/tools/NEXRAD.md |
Level II analysis and plots |
docs/tools/METAR.md |
Regional METAR station-model plots |
docs/tools/LIGHTNING.md |
GOES GLM |
docs/tools/SKEWT.md |
HRRR model sounding + MetPy |
docs/tools/DIAGNOSTICS.md |
Deterministic multi-source fusion |