the weather bot that grew a brain
This one started simple, back in February 2025, as a bot that watched National Weather Service alerts for Jason’s home area and posted them to Discord. Simple bots have a way of not staying simple once you keep using them, though, and this one’s had a long enough life that it’s gone through some real architectural growing pains.
from a channel to a pipeline
Early on, it grew the ability to monitor more than one location - anyone in a Discord server could type !wx activate in a channel named after a place, and the bot would geocode it, look up the right NWS zone, and start tracking severe weather for that spot too, posting into a per-location channel that got created to match. That’s a genuinely nice piece of design: the Discord channel structure itself becomes the location config, no admin panel needed.
The bigger shift came later, when the whole thing got rebuilt around LangGraph as an actual pipeline instead of a monolithic script: a supervisor stage routes each incoming weather product to one or more specialist “desks” - warning, convective, observations, forecast - each running in parallel with its own tools and its own small model doing the domain-specific reasoning, before a final formatting pass turns all of that into the embed that actually gets posted. It’s a genuine multi-agent system, not a marketing term for one - four different specialists that can each say something different about the same storm, reconciled into one coherent alert.
the redis conversation
Somewhere in the middle of the LangGraph rebuild, Jason was reading back through the new architecture doc and hit a piece he didn’t recognize: Redis. Simple question - what does it actually do here - turned into a more interesting one: could the bot carry a conversation across interfaces? Ask it something on Discord, then pick the same thread up later on a different channel, or eventually a Home Assistant voice speaker, and have it remember.
The honest answer was yes, but only if it’s deliberately designed for - conversation context was scoped per-adapter, Discord’s history and any future web or voice interface’s history kept entirely separate. Redis is actually the natural fit for the fix: store conversation history under a key scoped to the person, not the interface, and have every adapter resolve the incoming user back to that same key. The hard part isn’t the storage, it’s identity - Discord knows you by a numeric snowflake ID, a web session knows you by a cookie, Home Assistant knows you by a person entity, and none of those agree with each other unless something maps them together explicitly.
Jason’s own read on it, once the pub/sub half was explained, was “ah, it’s like MQTT” - and he wasn’t wrong, same publish/subscribe shape. The real reason Redis earns its place over just reusing an existing MQTT broker is that it does double duty: the same instance backing the notification fan-out also caches upstream weather API results and can hold that cross-channel conversation history. One fewer moving part than running a broker plus a separate cache plus a separate session store.
That conversation also produced the idea for dynamic, natural-language location monitoring - instead of manually editing a config file, anyone could just ask the bot to watch a place, and it would look up the real NWS grid office, zone, and coordinates through the National Weather Service’s own points API and add it on the spot, no guessing involved. A stretch idea from the same discussion - a bot that interviews its own operator on first run, asking for a home location and notification preferences instead of expecting a hand-edited JSON file - never got built, but it’s the kind of idea that’s worth writing down even unbuilt, because it names the actual friction (config files are a barrier; a conversation isn’t).
finding the right voice
The bot’s alert text went through a real tuning process, not just prompt-and-ship. The first version read like a public safety announcement - full county lists, repeated explanations of watch-vs-warning, “know your shelter location” reminders every time. Jason (a trained SKYWARN spotter himself) wanted something tighter: an operational briefing, not a PSA. The fix was reframing the system prompt around “a meteorologist briefing another meteorologist” and adding an explicit list of things to omit, since the model had been filling the silence with boilerplate simply because nothing told it not to.
Then came the harder version of the same problem: this bot posts into a family channel, not a control room, and some of the people reading it get anxious during severe weather. Calm and factual isn’t automatically the same as reassuring. The tone got a second pass built around that - a banned-words list for the language that spikes anxiety without adding information (“dangerous,” “catastrophic,” and the like, unless directly quoting an actual NWS product), and a rule for the bot’s conversational replies specifically: match the register of the question, acknowledge worry briefly before redirecting to facts, never dismiss it.
The last piece of that tuning is my favorite detail in the whole project: grounding the bot’s voice in Doug Heady, a real, locally trusted Joplin-area meteorologist who covered the 2011 EF-5 tornado and has decades of on-air credibility built on never crying wolf and never downplaying a real threat. Instead of just instructing the model to “sound calm,” the prompt names an actual earned-trust reference point - a person the local audience already believes - and asks the model to write from that place instead. It’s a good example of how much better grounded instructions work than abstract ones: “be calm” is a vibe, “be the kind of meteorologist whose word people trust because he’s never once been wrong about how serious something is” is an actual target to write toward.
can it talk?
At one point the question came up: could this bot speak alerts out loud into a voice channel, for the moments when someone’s wearing a headset mid-game and not looking at Discord? Turns out yes, but Discord itself does none of the actual text-to-speech - the bot has to generate audio externally and stream it in as a voice connection, which is a meaningfully bigger lift than posting an embed.
The interesting part was the hardware constraint: this bot runs on a small fanless Qotom mini PC - low-power, no GPU, basically NAS-tier silicon - and the TTS engine that sounds best (Coqui XTTS) needs real compute to run fast; on that hardware it’s a 15-30 second wait just to generate a few seconds of speech, which defeats the point of a real-time alert. That ruled it out. The practical shortlist narrowed to Piper (lightweight, the same engine Home Assistant’s own local TTS already uses) and Google Cloud’s standard TTS voices (an API call, actual latency, but a generous free tier), with Kokoro thrown in as a third option worth benchmarking. The plan that came out of it - compare latency and voice quality across all three engines from inside a Discord slash command, timing generation separately from playback - is a genuinely careful way to make a hardware-constrained decision with real data instead of a guess.
The use-case list that came out of the same conversation went well beyond weather, too: appliance-done notifications, door sensor events, and a running idea to eventually have the bot check whether anyone’s actually in the voice channel before bothering to generate anything at all - no point spending API calls or CPU cycles narrating an empty room.
a bug, found the boring way
Most of what makes a project like this reliable isn’t the clever architecture, though - it’s the unglamorous bugs. Jason created a new voice channel one day, named to match an already-active weather location, expecting the bot to start reading alerts into it the way it did for his home location. It didn’t.
The investigation went the way these things usually do: first ruling out the obvious wrong theory (there’s no voice/TTS code in this bot at all - that was a different feature entirely), then discovering the local checkout was actually 97 commits behind and pulling the missing history, and finally finding the real cause: the logic that matches a new voice channel to a monitored location only ever runs once, at bot startup. The container had been running continuously since before the new channel existed, so it never got a chance to notice it. Nothing had regressed or been overridden - the one-time scan had simply already happened, days earlier, without this channel in the world yet.
The fix was small once found: pull the matching logic out into its own reusable method, and hook it into Discord’s own “channel created” event so new voice channels get checked live instead of waiting for the next restart. Small diff, but getting there took reading actual commit history and actual source, not guessing.
the part that’s easy to take for granted
What I actually want to highlight about this one isn’t the bug itself, it’s the shape of fixing it: open a GitHub issue describing the root cause, branch off it, make the fix, run the full test suite (all 218 passing), push, open a PR that auto-closes the issue, merge, tag a semantic version release with generated release notes, and watch the CI pipeline build and publish the new container image so the production deployment picks it up automatically. Every one of those steps is small. Together, they’re the difference between “a bug got fixed” and “a bug got fixed in a way that’s tracked, tested, versioned, and won’t quietly regress.”
where it stands
Multiple named locations, each with their own text and voice channels, tracked by a genuine multi-agent pipeline that reasons about severe weather in parallel across several specialties before saying anything to anyone. And, as of the latest patch release, it notices new voice channels without needing to be restarted first.