How Was My Dog Today? Turning a Day of Bowl Events into One Trustworthy Paragraph

How Was My Dog Today? Turning a Day of Bowl Events into One Trustworthy Paragraph

Everbowl logs a dog's day in numbers. Load-cell weights on every bowl visit, water refills, eye-temperature frames from the thermal camera, ambient temperature and humidity, audio classifications of chewing and licking. Across a fleet this runs into thousands of events per dog per day. Almost none of it means anything to the person who has to look at it, which is a pet parent sitting on a sofa wondering if their dog is fine.

The question a pet parent actually has is short. How was my dog today compared with their usual pattern? The daily AI summary is our attempt to answer that in one paragraph.

Why a Daily Summary at All

Real-time alerts already handle the sharp edges: a bowl that hasn't been touched all day, a temperature spike, a device that dropped offline. The notification stack that carries those is a separate system, and it earns its keep. But there is a whole middle band of pet health that sharp alerts don't cover well. A dog eating 15% less than usual over three days is not going to trip a threshold. It's just going to look a little smaller each week until someone notices. A dog whose meal duration keeps creeping up. A dog that has started drinking more, but only on days above 32°C.

These are patterns humans could see if they stared at a chart every night, but nobody stares at a chart every night. The daily summary is meant to be the thing pet parents open with their morning coffee, quickly enough that it earns the habit. That constrains what it can be. One short paragraph, observational rather than diagnostic, calibrated to say less when it saw less.

What Goes Into the Aggregation

The pipeline splits in half on purpose. Everything numeric happens in deterministic code, and the language model's only job is to choose which of a fixed set of observations to prioritize and to write them in a plain, warm voice. The model does not do arithmetic, does not compute comparisons, and does not see raw events. It picks from evidence sentences the aggregator has already written for it.

The deterministic layers stack. Raw daily state at the bottom, personal baselines above that, trend analysis across thirty days, rule-based behavior tags, and a confidence grade on top.

The daily state is a per-slot meal record for breakfast, lunch and dinner, with start and end times, food offered, food eaten, food left over, and meal duration. Daily totals for food, water, and leftovers. Water refills and estimated intake. Low-water incidents. Mean, max, and latest eye-temperature. An audio class histogram, with anything that isn't eating, chewing, swallowing or licking flagged as an anomaly. Ambient temperature and humidity means. Every one of these is a straight aggregation over the day's events; nothing here is inferred.

Personal baselines sit above the daily state. Seven-day means for food, water, and leftovers. Per-slot baselines for meal size and meal duration. A fourteen-day window for feeding times. A seven-day mean for eye temperature. Fleet averages were never on the table, since 300 grams a day is a normal Labrador and a worrying pug. Any number without the dog's own history behind it is misleading.

Trend analysis runs across the last thirty days. Seven-day and thirty-day rolling means for food, water, leftovers, meal duration, and temperature. A slope-based trend label per signal (rising, falling, stable) with strength and persistence tags. Weekday-vs-weekend deltas. Same-day-of-week comparisons. On days that are noticeably hotter than the recent average, water intake gets softened, since a dog drinking more on a 34°C day is not the same signal as a dog drinking more on a 24°C day.

Behavior tags come out of these comparisons as rule-based flags. ate_less_than_usual when the ratio drops below 85% of baseline. ate_more_than_usual above 115%. temperature_above_personal_range for a +1.5°F swing. feeding_shifted_late for a slot 45 minutes late. These are the observations the model chooses from. Rule-derived rather than model-derived, because we wanted the auditable trail to run all the way from raw events to the sentence in the paragraph.

Confidence is a score from 100 that gets docked for data-quality issues: no meals recorded, no temperature readings, missing baselines, inactive heartbeat, gaps in the day's data. It maps to high, medium, or low, and it feeds back into what the language model is allowed to say.

Keeping the Model Honest

Two rules the prompt is not allowed to violate. No medical or diagnostic claims of any kind. Temperature is stated as an observation, never linked to a disease or condition. And when confidence is not high, the paragraph has to say so in plain words.

If the model call fails or returns nothing, a deterministic fallback stitches the top pre-written observations into a plainer summary. Every dog still gets a paragraph, and none of them are worse than the observation candidates the aggregator produced. Model unreliability is a fact of life, and we needed the language model to be a nice-to-have on top of the pipeline, not a load-bearing dependency inside it.

The Hard Problem: Absence Isn't Always Absence

Most of the interesting engineering was in one class of bug. A day with zero meals looks the same in the aggregation whether the dog didn't eat or the bowl was unplugged for eight hours. If we don't distinguish those two, the summary can quietly write "Buddy ate less than usual today, only 0 grams compared to his baseline of 340." Technically true. Completely useless. Alarming for exactly the wrong reason.

Fixing this needed a real coverage number first. Our first coverage estimate came from a device events table that turned out to be stale for our purposes; it would report a full day of coverage on a device that had actually been offline for hours. Coverage now comes directly from device heartbeats, which gives us a real percentage, the actual missing windows, and the largest gap of the day. Everything below sits on top of that number.

Once coverage was trustworthy, three rules covered the important cases:

Coverage on the day What the pipeline does
Under 10% with no meals recorded Short-circuit. Skip the model entirely. The insight tells the pet parent the bowl looks offline and asks them to check power or Wi-Fi.
Under 75% Suppress every "ate less" and "drank less" tag. Keep the "ate more" and "drank more" ones, since those are grounded in things we actually observed. Dock confidence and add an honest "missing about N hours of data today" caveat.
75% or above with zero meals recorded Emit no_food_recorded rather than a false ate_less_than_usual. The insight says a meal was likely not captured.

The asymmetry between "more" and "less" is the piece that took the longest to see. "Drank more" is grounded in something we saw. "Drank less" is grounded in something we did not see, and the not-seeing might be our own fault. Once you frame it that way, the coverage rules mostly write themselves.

The prompt gained matching rules to go with the aggregation changes. It mentions the coverage caveat when the day was patchy. It dedupes observations that carry the same signal, since earlier versions would sometimes say the same thing twice with slightly different framings. And it handles the missed-slot pattern explicitly, so a dog whose first meal of the day happens to be lunch doesn't get described as eating dinner four hours late.

Smaller Aggregation Bugs Along the Way

A handful of smaller issues turned up in the aggregation while we were sorting out coverage. Grouped for reference:

  • Timezone bucketing. The pipeline originally bucketed everything by UTC day, so a dog in Bengaluru getting breakfast at 7 AM local (1:30 AM UTC) had that meal filed under the previous day. Days are now bucketed by the device's local timezone, defaulting to Asia/Kolkata for our current fleet.
  • Leftover summing. Leftover food was being summed across meals, so 30 grams left in the bowl at breakfast got counted a second time when lunch was served on top of it. Daily leftover is now the bowl weight at end of day.
  • Baseline sanity guard. A baseline leftover greater than 5 kg is a load-cell malfunction, not a lazy dog. Baselines above that threshold are now discarded rather than propagated into comparisons.
  • Feeding-shift bound. A missed breakfast eaten at 1 PM used to get labeled as "lunch shifted by 240 minutes," which is not a shifted lunch, it's a missed breakfast. Shifts greater than 150 minutes are now discarded, and the prompt handles the missed-slot case separately.

The Pattern Underneath

The recurring shape of every fix above is the same one, applied in a different place each time. An aggregation is only as honest as its treatment of the data it did not get. The system should be confident where its inputs are trustworthy and honest about what it did not see. That is the whole design.

The paragraph a pet parent reads is short, and short paragraphs are hard, because you cannot hide from a wrong sentence in the middle of one. Everything in the pipeline below it is there to make sure the sentence is worth reading.

Read more