DEV Community

Hardik Kumar
Hardik Kumar

Posted on

๐ŸŒฑ Last Frost: A Garden Planner That Only Works If You Go Outside

Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass Submission ๐ŸŒฟ

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass

What I Built

Last Frost is an offline garden planner. You type your ZIP code and what you want to grow; it looks up your local first fall frost and last spring frost dates, then a small open-weight model running on your own machine tells you what to plant this week, what to hold back, and why โ€” based on the actual number of frost-free days you have left.

It's for anyone whose gardening app experience is: open app โ†’ get generic advice โ†’ still not know if it's too late to plant tomatoes. Last Frost replaces "generic" with local and time-aware: every recommendation is anchored to a frost date and a days-to-maturity window.

It's also deliberately built to get you off the screen. The app takes ten seconds โ€” type ZIP, get plan, close laptop, go do what it said.

I tested it the way it's meant to be used: wifi off. On October 6, I asked it for a kale plan in Chicago โ€” it correctly counted 14 days until the October 20 first frost, put fast-maturing greens in "plant now," and moved anything slower to "wait until" with the reason stated. That's the whole app doing its one job with no network involved.

Demo

No deployed link needed โ€” the whole point is it runs with no internet. Here's the architecture, and the app itself is a one-command local run (ollama pull gemma3:4b && streamlit run app.py).

Flow in one breath: user enters ZIP + crop โ†’ app looks up frost dates from a bundled CSV โ†’ builds a prompt with a strict JSON schema โ†’ Gemma 3 4B runs locally via Ollama โ†’ structured weekly plan renders in the browser. The dashed fallback keeps the UI usable while Ollama isn't installed, clearly labeled as demo output.

Code

No repo link โ€” here it is inline. The whole app is one file, app.py:

"""
Last Frost โ€” offline garden planner for the Hacktoberfest "Touch Grass" challenge.
Runs Gemma 3 locally via Ollama. Falls back to a demo mode if Ollama isn't running.
"""
import json
import datetime as dt
import pandas as pd
import streamlit as st

st.set_page_config(page_title="Last Frost ๐ŸŒฑ", page_icon="๐ŸŒฑ")

SYSTEM_PROMPT = """You are a practical, concise gardening assistant.
Given a location, its first fall frost date, last spring frost date, today's date,
and what the user wants to grow, output ONLY valid JSON with this shape:
{{
  "summary": "one sentence about this week in the garden",
  "plant_now": ["...", "..."],
  "wait_until": ["...", "..."],
  "tasks": ["...", "..."],
  "why": "one sentence on how the frost dates drove this advice"
}}
Be specific to the local climate. No markdown, no commentary, JSON only."""

@st.cache_data
def load_frost_data():
    return pd.read_csv("frost_dates.csv", dtype={"zip": str})

def lookup(zipcode):
    df = load_frost_data()
    row = df[df["zip"] == zipcode]
    return None if row.empty else row.iloc[0].to_dict()

def ask_gemma(location_info, crop, today):
    """Call Gemma 3 running locally in Ollama. Raises if unavailable."""
    import ollama
    user = (f"Today is {today}. Location: {location_info['city']}, {location_info['state']} "
            f"(ZIP {location_info['zip']}). Last spring frost: {location_info['last_spring_frost']}. "
            f"First fall frost: {location_info['first_fall_frost']}. "
            f"I want to grow: {crop}. Give me this week's plan.")
    r = ollama.chat(model="gemma3:4b", format="json",
                    messages=[{"role": "system", "content": SYSTEM_PROMPT},
                              {"role": "user", "content": user}])
    return json.loads(r["message"]["content"])

def demo_plan(location_info, crop, today):
    """Offline fallback so the app always runs. Clearly labeled in the UI."""
    fall = dt.date.fromisoformat(location_info["first_fall_frost"])
    spring = dt.date.fromisoformat(location_info["last_spring_frost"])
    t = dt.date.fromisoformat(today)
    if spring <= t <= fall:
        growing = True
    elif t > fall:
        growing = False
    else:
        growing = (fall - t).days < 120  # near fall, short window
    if growing:
        days_left = (fall - t).days
        return {
            "summary": f"Growing season is ON in {location_info['city']} โ€” about {days_left} days until first frost.",
            "plant_now": [f"Fast-maturing {crop} (under {days_left} days to harvest)", "Succession sow greens every 2 weeks"],
            "wait_until": ["Long-season crops that won't mature before frost" if days_left < 90 else "Nothing โ€” full season available"],
            "tasks": ["Mulch to hold moisture", "Scout for pests in the cool mornings"],
            "why": "Demo mode: advice driven by the first fall frost date.",
        }
    return {
        "summary": f"Too cold to plant in {location_info['city']} right now.",
        "plant_now": ["Nothing outdoors", "Start seeds indoors 6-8 weeks before last frost"],
        "wait_until": [f"Transplant {crop} outdoors after {location_info['last_spring_frost']}"],
        "tasks": ["Plan beds", "Order seeds", "Compost leaves"],
        "why": "Demo mode: advice driven by the last spring frost date.",
    }

# ---------------- UI ----------------
st.title("๐ŸŒฑ Last Frost")
st.caption("Offline garden planner ยท open-weight Gemma 3, running locally ยท Hacktoberfest 2026")

zipcode = st.text_input("ZIP code", value="10001", max_chars=5)
crop = st.text_input("What do you want to grow?", value="kale and tomatoes")
today = st.date_input("Today's date", value=dt.date.today()).isoformat()

if st.button("What's my plan this week?", type="primary"):
    loc = lookup(zipcode)
    if not loc:
        st.error("ZIP not in the sample dataset โ€” add it to frost_dates.csv.")
        st.stop()
    try:
        plan = ask_gemma(loc, crop, today)
        mode = "gemma"
    except Exception:
        plan = demo_plan(loc, crop, today)
        mode = "demo"
    if mode == "demo":
        st.warning("Ollama/Gemma not detected โ€” showing DEMO plan. Run `ollama pull gemma3:4b` for real AI output.")
    st.success(plan["summary"])
    c1, c2 = st.columns(2)
    with c1:
        st.subheader("๐ŸŒฟ Plant now")
        for x in plan["plant_now"]: st.write("โ€ข", x)
    with c2:
        st.subheader("โณ Wait until")
        for x in plan["wait_until"]: st.write("โ€ข", x)
    st.subheader("๐Ÿ›  This week's tasks")
    for x in plan["tasks"]: st.write("โ€ข", x)
    st.info("Why: " + plan["why"])
Enter fullscreen mode Exit fullscreen mode

frost_dates.csv holds frost dates per ZIP (10 sample US cities); requirements.txt is three lines (streamlit, pandas, ollama). Clone-by-copy works โ€” that's the point of a 120-line app.

How I Built It

The open piece that makes this project work is Gemma 3, Google's open-weight model, running locally via Ollama โ€” no API calls, no cloud, no account.

  1. Prompt development in Google AI Studio. I started on Gemma 3 27B with a strict spec: a system prompt defining the JSON schema, six behavioral rules (e.g., "if days-to-first-frost < 60, only fast-maturing crops go in plant_now"), and four few-shot examples covering mid-season, pre-spring, post-frost, and long-season edge cases. I iterated against a 6-case test checklist โ€” the make-or-break case being "pumpkins with 56 frost-free days left," where a correct model moves pumpkins to wait_until and admits in why that it can't mature in time.
  2. Local inference with Ollama. Same prompt, smaller model: ollama pull gemma3:4b. The 4B runs comfortably on a laptop, stays fully offline, and returns structured JSON via Ollama's format="json" mode โ€” the UI never parses prose.
  3. The glue is ~12 lines of Python. A CSV lookup for frost dates, a date comparison, one ollama.chat() call. Streamlit renders the plan: summary, plant now, wait until, weekly tasks, and the "why" sentence citing the frost dates the model actually used.

Why Does Open Innovation Matter?

I could have built this in an afternoon on a closed API. It would have worked worse in every way that matters for this project:

  • It runs where gardening happens โ€” with zero signal. Gardens and allotments have terrible wifi. Last Frost doesn't degrade offline; offline is its default state. A closed API can't do that without a phone plan.
  • It costs $0 to run, forever. No tokens, no meter, no rate limits. Ask it for a plan every week for a decade; the marginal cost is a few watt-hours.
  • Your data never leaves the device. Location and garden plans are processed locally โ€” there's no server holding a map of who's growing what where.
  • The model is swappable infrastructure, not a dependency. One Ollama command swaps gemma3:4b โ†” gemma3:12b โ†” any future open model. If a Gemma version is ever deprecated, the project doesn't break โ€” the weights are mine.

Open weights didn't just make this project cheaper. They made it possible in the environment it's designed for.

My Agent Session

I built Last Frost with an AI coding assistant (Kimi) acting as my pair programmer, and I want to show the process rather than just the result โ€” this challenge is about open-source AI, and the session below is open-source AI doing real work.

The session, condensed:

  1. Scoping. I dumped the challenge prompt and prize categories into the chat and asked for the easiest qualifying project. The assistant proposed Last Frost (garden planner, ZIP โ†’ frost dates โ†’ local LLM) with a difficulty comparison against the alternatives (bird-call ID, vision trail narrator) โ€” which is why the build is text-only instead of an audio pipeline I couldn't finish in 4 days.
  2. Spec engineering. I asked for "the entire spec to one-shot the model in Google AI Studio." What came back became my actual development artifact: a system prompt with 6 behavioral rules (the load-bearing one: "if days-to-first-frost < 60, only fast-maturing crops go in plant_now"), 4 few-shot examples covering mid-season / pre-spring / post-frost / long-season cases, and a 6-case test checklist.
  3. The test that mattered. Case #5 โ€” pumpkins with 56 frost-free days left. A model that says "plant pumpkins!" fails the whole point of the app. That's the case I iterated against in AI Studio until Gemma correctly moved pumpkins to wait_until and admitted in why that they can't mature in time. The assistant's framing ("if Gemma gets that right, your prompt is done") was my done-ness criterion.
  4. Code generation with verification. The assistant generated the full app (Streamlit + Ollama + CSV lookup + demo fallback), then ran the core logic itself before handing it over โ€” frost lookup for NYC and Chicago returned correct days-to-frost, unknown ZIPs were rejected. I ran the same checks locally.
  5. The fallback decision. Demo mode when Ollama isn't running was the assistant's suggestion, not mine โ€” and it's the difference between "broken on first run" and "testable UI on day one."

What I'd do differently: I should have saved the raw AI Studio chat (Gemma 27B iterations) as I went. The structured session above is reconstructed from notes; the real artifact would be better. Lesson for week 2: DevRelay from minute one.

Prize Categories

  • Best Use of Gemma โ€” Gemma 3 (open weights) is the core of the project: developed on 27B in Google AI Studio, served locally via Ollama for offline use.

<!-- Team Submissions: N/A โ€” solo build. -->

<!-- Thanks for participating! -->

Top comments (0)