<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jerrie Adah</title>
    <description>The latest articles on DEV Community by Jerrie Adah (@adahjerrie).</description>
    <link>https://dev.to/adahjerrie</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4153458%2F25acf726-8dc7-495e-af5f-0cf608e64b7d.jpg</url>
      <title>DEV Community: Jerrie Adah</title>
      <link>https://dev.to/adahjerrie</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adahjerrie"/>
    <language>en</language>
    <item>
      <title>Sloth Queue — three things, then a nap</title>
      <dc:creator>Jerrie Adah</dc:creator>
      <pubDate>Sat, 24 Oct 2026 17:32:10 +0000</pubDate>
      <link>https://dev.to/adahjerrie/sloth-queue-three-things-then-a-nap-17j9</link>
      <guid>https://dev.to/adahjerrie/sloth-queue-three-things-then-a-nap-17j9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week3-2026-10-19"&gt;Hacktoberfest Open-Source AI Challenge Week 3: Slow and Steady&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;My laptop is not a GPU box, so "run a model locally" was never the interesting constraint. The interesting one was: can the lazy part be real code, and can the model be swapped without loosening it?&lt;/p&gt;

&lt;p&gt;Sloth Queue is a small web app for the pile you keep orbiting. A group chat. An inbox. The note that has been rewritten four times. You paste it. You get a two-line morning note, at most three things worth doing, drafts for the ones that are just a reply, and a list of what got held back.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://cursor.com/codebase/jerrie-kingdomdev/fest-finder" rel="noopener noreferrer"&gt;https://cursor.com/codebase/jerrie-kingdomdev/fest-finder&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What effort it saves
&lt;/h2&gt;

&lt;p&gt;The chore is triage. Not the work itself. The work is usually two messages and one errand, buried under newsletters, reaction GIFs, and tasks that belong to someone else. I reread that kind of pile because deciding feels harder than doing. The app does the deciding.&lt;/p&gt;

&lt;p&gt;The samples are the demo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Sunday chat with fourteen messages. Two of them are asks. The reel, the song, and "react if you see this" are dropped. Ice can wait, because someone already offered.&lt;/li&gt;
&lt;li&gt;An inbox where a landlord needs a yes or no before the electrician arrives, a boss will accept two lines, and a mom wants a call about a dog. The permission slip is due Friday, so it waits until Sunday evening. The lamp sale does not get a click.&lt;/li&gt;
&lt;li&gt;A meeting note that tried to assign you a flaky test with no owner, a 14-page PDF, and two actual asks. The PDF needs a sitting. The asks are a spare minute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one is the payoff. A helpful model will turn a meeting into homework. The cap will not.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is built
&lt;/h2&gt;

&lt;p&gt;The agent is a &lt;a href="https://mastra.ai" rel="noopener noreferrer"&gt;Mastra&lt;/a&gt; agent, &lt;code&gt;sloth-agent&lt;/code&gt;. Mastra is the open-source harness: instructions, a tool, and a model you can point somewhere else. The tool is &lt;code&gt;apply-sloth-cap&lt;/code&gt;. The model is told to be a normal helpful assistant and to mark anything slightly useful as "today". Then the tool, which is just TypeScript, keeps three.&lt;/p&gt;

&lt;p&gt;![&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0agr1wmryohowxtjzihx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0agr1wmryohowxtjzihx.png" alt="home view" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;](...)&lt;/p&gt;

&lt;p&gt;The rules are dull on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Today holds three items at most.&lt;/li&gt;
&lt;li&gt;Energy of 4 or 5 means it needs a sitting. It waits.&lt;/li&gt;
&lt;li&gt;Newsletters, promo, and other people's tasks are noise.&lt;/li&gt;
&lt;li&gt;A person who is waiting, with a real deadline, is the one thing the sloth will pull back onto the list if a draft tried to drop them.&lt;/li&gt;
&lt;li&gt;Something you already deferred stays deferred unless someone is blocked again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the model returns, the server runs the same function again. If the model skips the tool, or sweetens the buckets on the way out, the cap still happens. The screen shows the difference: "The model wanted 8 things today. The cap kept 3."&lt;/p&gt;

&lt;p&gt;The model is not on the laptop. The default preset is &lt;code&gt;openai/gpt-oss-20b&lt;/code&gt; on Groq, which is open weights behind a fast API. There is a larger gpt-oss preset, an OpenRouter preset for Gemma 3 12B (Google's open-weight model, for the Gemma category), and a box for any OpenAI-compatible host. Swapping the model is a settings change. Swapping the model does not change the cap.&lt;/p&gt;

&lt;p&gt;With no key, the sample piles still run. An eager hand-labeled pass stands in for the model, and the same function edits it. A pasted pile with no key uses a smaller local sorter, and the screen says so. I would rather show a boring honest result than pretend a heuristic is Gemma.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open, here
&lt;/h2&gt;

&lt;p&gt;A closed chat window will help you have a productive day. That is the failure. You cannot see the rule, you cannot cap it, and you cannot point it at a different open model when the first one is slow or expensive or gone.&lt;/p&gt;

&lt;p&gt;Three open pieces are doing the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Mastra, which is the agent. The instructions and the tool are in the repo.&lt;/li&gt;
&lt;li&gt;The cap, which is a function with tests. It does not care which model proposed the list.&lt;/li&gt;
&lt;li&gt;The model id, which is a string. gpt-oss, Gemma, Qwen, whatever speaks chat completions. The weights stay on someone else's machine, which is the whole point on a low-spec laptop, and you can change them without rewriting the chore.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Personal piles stay in the request you choose to send. Demo mode never leaves the machine. There is no account and no database. History is six morning notes in the browser.&lt;/p&gt;

&lt;p&gt;The lazy payoff is the morning note. Two lines. Three things. Then you can stop reading the pile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://127.0.0.1:3847" rel="noopener noreferrer"&gt;http://127.0.0.1:3847&lt;/a&gt;, pick "The inbox" or "The meeting", and hand it over. Add a Groq or OpenRouter key in Model if you want the open-weight model to read a pile of your own.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>ShopCast: a sales forecast a small shop can actually pay for</title>
      <dc:creator>Jerrie Adah</dc:creator>
      <pubDate>Sun, 18 Oct 2026 23:09:10 +0000</pubDate>
      <link>https://dev.to/adahjerrie/shopcast-a-sales-forecast-a-small-shop-can-actually-pay-for-4k4l</link>
      <guid>https://dev.to/adahjerrie/shopcast-a-sales-forecast-a-small-shop-can-actually-pay-for-4k4l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;The shop owner already knows Saturday is busy. What they cannot price is &lt;em&gt;how much&lt;/em&gt; busier, or what happens if they run a Friday promo, or whether last month's "slow Wednesday" was just Wednesday.&lt;/p&gt;

&lt;p&gt;Closed forecasting tools want a seat licence in dollars and the whole sales history on a server they control. For a bakery or mini-mart, that is not a product. That is a cost centre.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ShopCast&lt;/strong&gt; is the version I would actually charge for. Export daily sales as a CSV (&lt;code&gt;date&lt;/code&gt;, &lt;code&gt;sales&lt;/code&gt;, optional &lt;code&gt;promo&lt;/code&gt;). Click &lt;strong&gt;Start forecast&lt;/strong&gt;. You get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the next 1–8 weeks of sales, with a range, not a single smug number&lt;/li&gt;
&lt;li&gt;unusual days the model did not expect (generator died, surprise catering order)&lt;/li&gt;
&lt;li&gt;a backtest against the owner's own rule: "same as last week"&lt;/li&gt;
&lt;li&gt;a chat box that answers "what if I promo every Friday?" with TabPFN's numbers, then Gemma's words&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Who pays: a shop that would spend a few thousand naira a month to stop over-baking on Tuesdays.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;welcome screen with &lt;strong&gt;Start forecast&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o4h59gputgqhj7wjiyj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0o4h59gputgqhj7wjiyj.png" alt="welcome screen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;video:&lt;/strong&gt; &lt;br&gt;
  &lt;iframe src="https://www.youtube.com/embed/JQ3YhtBR5t4" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;live app (Render):&lt;/strong&gt; &lt;a href="https://shopcast.onrender.com" rel="noopener noreferrer"&gt;https://shopcast.onrender.com&lt;/a&gt; Note that a free Render box sleeps, so the first load can take a minute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;forecast chart:&lt;/strong&gt;  — &lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwxr5mzql65rstx6qe4q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpwxr5mzql65rstx6qe4q.png" alt="forecast chart" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;blue history, orange dashed future.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Unusual days table:&lt;/strong&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr4bbez0th5xp99odr3f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkr4bbez0th5xp99odr3f.png" alt="unusual days" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ask ShopCast answering a promo or forecast question, with &lt;strong&gt;Numbers behind this answer&lt;/strong&gt; open.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7avvp4jsn22xesf770tm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7avvp4jsn22xesf770tm.png" alt="Askshopcast" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;blockquote&gt;

&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/AdahJerrie" rel="noopener noreferrer"&gt;
        AdahJerrie
      &lt;/a&gt; / &lt;a href="https://github.com/AdahJerrie/shopcast" rel="noopener noreferrer"&gt;
        shopcast
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      The shop owner uploads a sales CSV. The TabPFN API predicts next month's sales and flags unusual days, and Gemma explains the results in plain English.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;ShopCast&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Sales forecasts for small shops. Upload a daily sales CSV and ShopCast:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;forecasts the next 1 to 8 weeks with &lt;strong&gt;TabPFN&lt;/strong&gt; (Prior Labs' tabular foundation model), with a 10-90% band&lt;/li&gt;
&lt;li&gt;flags unusual days (spikes and drops) by scoring each day with a model that never saw it&lt;/li&gt;
&lt;li&gt;backtests itself on the last 28 days against "same as last week", so the owner can see whether to trust it&lt;/li&gt;
&lt;li&gt;answers questions in plain English through a small &lt;strong&gt;Gemma&lt;/strong&gt; agent: forecast, unusual days, and "what if I run a promo every Friday?"&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;CSV format&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;column&lt;/th&gt;
&lt;th&gt;required&lt;/th&gt;
&lt;th&gt;notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;date&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;also accepts &lt;code&gt;day&lt;/code&gt;, &lt;code&gt;sale_date&lt;/code&gt;, &lt;code&gt;transaction_date&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;sales&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;also accepts &lt;code&gt;revenue&lt;/code&gt;, &lt;code&gt;amount&lt;/code&gt;, &lt;code&gt;total&lt;/code&gt;, &lt;code&gt;turnover&lt;/code&gt;; multiple rows per day are summed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;promo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1&lt;/code&gt; on promo days; enables the what-if tool&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;At least 8 weeks of history is needed. Missing days are treated as zero sales. &lt;code&gt;sample_data/lagos_bakery.csv&lt;/code&gt; is…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/AdahJerrie/shopcast" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;

&lt;/blockquote&gt;

&lt;p&gt;The files that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;shopcast/forecast.py&lt;/code&gt; — TabPFN, one week per call, walk forward&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shopcast/anomaly.py&lt;/code&gt; — score each day with a model that never saw it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;shopcast/agent.py&lt;/code&gt; — tools, keyword routing, "copy the naira exactly"&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app.py&lt;/code&gt; — upload, preview, &lt;strong&gt;Start forecast&lt;/strong&gt;, then the dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;I built this on a five-year-old Latitude: 2-core i5, 8 GB RAM, no usable GPU. About a gigabyte free on a good day. So the first design decision was boring and correct: &lt;strong&gt;do not run TabPFN or a 27B model on this laptop.&lt;/strong&gt; TabPFN goes through Prior Labs. Gemma goes through Google AI Studio. The laptop runs Streamlit and pandas.&lt;/p&gt;

&lt;p&gt;The second decision was how to make forecasting tabular. Each day is a row: weekday, payday window, Nigerian public holidays, festive season, promo flag, sales from 7 and 14 days ago. Lags start at 7 on purpose. That way TabPFN predicts a whole week in one call, then those predictions become next week's lags. Four API calls for 28 days, not 28.&lt;/p&gt;

&lt;p&gt;Unusual days use the same model in 5-fold out-of-sample scoring. I first flagged on robust z-score alone and the sample bakery lit up like a Christmas tree — 13 "anomalies" on ordinary noise. So a day now has to be statistically weird &lt;em&gt;and&lt;/em&gt; at least 25% off expected. On the planted sample, TabPFN then flagged exactly the four days I had sabotaged.&lt;/p&gt;

&lt;p&gt;Then the chat started lying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 1: Gemma 3 was gone.&lt;/strong&gt; First Ask ShopCast reply on the real key:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;404 NOT_FOUND … models/gemma-3-27b-it is not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;I had copied a model name from last week's docs. &lt;code&gt;ListModels&lt;/code&gt; on &lt;em&gt;this&lt;/em&gt; key returned two Gemma 4 models. Switched to &lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;. One environment variable. That is the open-weight pitch in practice: the product did not die with the model card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 2: the 1B model cannot route.&lt;/strong&gt; I tried local &lt;code&gt;gemma3:1b&lt;/code&gt; because the laptop can just about hold it. It answered "what if I run a Friday promo?" by calling &lt;code&gt;forecast&lt;/code&gt; for 14 days. Clear questions now skip the model: promo / strange / next week go straight to the tool. Gemma only routes the vague ones. Routing went from 1/3 to 4/4.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 3: JSON ate the naira.&lt;/strong&gt; &lt;code&gt;json.dumps&lt;/code&gt; escaped &lt;code&gt;₦1,452,027&lt;/code&gt; as &lt;code&gt;\u20a61,452,027&lt;/code&gt;. Gemma read the trailing 6 and told the owner they would sell "61,452,027 dollars." &lt;code&gt;ensure_ascii=False&lt;/code&gt;, and every answer now has a &lt;strong&gt;Numbers behind this answer&lt;/strong&gt; panel. Trust the table, not the prose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure 4: it ran before anyone asked.&lt;/strong&gt; First version analysed the sample CSV the moment the page loaded — minutes of TabPFN on a shop owner's first click. Now: upload (or sample toggle) → local preview of the last 10 days → &lt;strong&gt;Start forecast&lt;/strong&gt;. Change the file or the horizon and it waits again.&lt;/p&gt;

&lt;p&gt;I also tried to use the Gemma 4 E2B I already had in Ollama (4.3 GB). After "closing heavy apps" I still had 0.9 GB free. Cursor, VS Code, Edge, Chrome, and Notion were sitting on the rest. We skipped it. Hosted Gemma stays the default. That is not ideology. That is an 8 GB machine.&lt;/p&gt;

&lt;p&gt;Measured, on the sample bakery, &lt;strong&gt;real TabPFN + Gemma 4&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;TabPFN average daily error&lt;/td&gt;
&lt;td&gt;₦4,671&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Same as last week"&lt;/td&gt;
&lt;td&gt;₦11,248&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error vs naive&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;58.5% lower&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unusual days on planted sample&lt;/td&gt;
&lt;td&gt;4 of 4&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model vanished and the app did not.&lt;/strong&gt; Gemma 3 27B 404'd. I listed what the key actually serves and swapped. A closed "forecasting API" with one SKU does not let you do that on a Sunday afternoon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small data is the product, not the edge case.&lt;/strong&gt; A bakery has hundreds of daily rows, not millions. TabPFN is built for that. Asking a general LLM to "look at the CSV and predict October" is how you get confident fiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost stays at shop scale.&lt;/strong&gt; TabPFN's free non-commercial tier plus Gemma on AI Studio is the difference between "₦2,000/month might work" and "the API bill is the business." Offline, Gemma 3 1B still explains a cached forecast on this laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The owner's file is not a training set I keep.&lt;/strong&gt; Preview and Start are local. The hosted path sends features to TabPFN and a short tool result to Gemma, not a dump of every receipt line.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="Claude Code"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Building ShopCast: TabPFN forecasts and a Gemma agent that stopped inventing naira&lt;/span&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-scroll"&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Go through the Hacktoberfest Week 2 prompt, then give 3 to 5 project options that fit this laptop. Avoid anything too memory-draining.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;The laptop is a Latitude 7480: 2-core i5-7300U, 8 GB RAM, Intel HD 620, about 1 GB free. Local models bigger than ~1B will thrash. I recommended ShopCast: a small-shop sales forecaster. TabPFN via Prior Labs API does the numbers. Hosted Gemma explains them. Optional Ollama fallback for offline chat.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Implement ShopCast as specified. Do not edit the plan file.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Scaffolded the Streamlit app, TabPFN week-by-week forecast, residual anomaly check, 28-day backtest vs same-weekday-last-week, and a Gemma agent with forecast / anomalies / what-if tools.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-write"&gt;Write&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;shopcast/forecast.py, agent.py, app.py, sample_data/lagos_bakery.csv&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;shopcast/forecast.py, agent.py, app.py, sample_data/lagos_bakery.csv&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Pipeline ran offline in 8s. Keyword routing sent promo / strange-days / next-two-weeks / accuracy to the right tools.&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;I already have gemma4:e2b-it-qat locally. Can that serve instead of gemma3:1b?&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Yes, ShopCast reads OLLAMA_MODEL. Gemma 4 E2B is 4.3 GB. Only 0.9 GB RAM was free, so loading it would swap hard. Hosted Gemma stays the default.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Ask ShopCast returned 404 NOT_FOUND for models/gemma-3-27b-it.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Listed models on the key. Only gemma-4-26b-a4b-it and gemma-4-31b-it accept generateContent. Switched the default. Live TabPFN + Gemma 4 test: MAE ₦4,671 vs naive ₦11,248 (58.5% lower). Flagged the 4 planted unusual days.&lt;/p&gt;

                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-listgemmamodels"&gt;list_gemma_models&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;GOOGLE_API_KEY from .env&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;GOOGLE_API_KEY from .env&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;gemma-4-26b-a4b-it, gemma-4-31b-it&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;It starts processing the moment ShopCast launches. I need to upload a real shop CSV, then click Start before it runs.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Welcome screen now. File is checked locally first (days, date range, promo column, last 10 rows). Start forecast is disabled until there is data. Changing the file or horizon waits for another click.&lt;/p&gt;

                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;
  &lt;/div&gt;

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        10 of 12 messages
          · some content was hidden by the author
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN&lt;/strong&gt; — TabPFN 3.5 is the forecast, the anomaly scores, and the promo what-if, inside a small prediction agent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 4 26B hosted (after Gemma 3 404'd); optional Gemma 3 1B locally&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — &lt;code&gt;render.yaml&lt;/code&gt; web service. ** &lt;a href="https://shopcast.onrender.com" rel="noopener noreferrer"&gt;https://shopcast.onrender.com&lt;/a&gt; **&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Entire&lt;/strong&gt; — agent session above&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>gemma</category>
      <category>python</category>
    </item>
    <item>
      <title>Touch Grass FC: A Local AI That Reads Your Football Group Chat and Builds Fair Teams</title>
      <dc:creator>Jerrie Adah</dc:creator>
      <pubDate>Sun, 11 Oct 2026 15:31:12 +0000</pubDate>
      <link>https://dev.to/adahjerrie/touch-grass-fc-a-local-ai-that-reads-your-football-group-chat-and-builds-fair-teams-5fff</link>
      <guid>https://dev.to/adahjerrie/touch-grass-fc-a-local-ai-that-reads-your-football-group-chat-and-builds-fair-teams-5fff</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I love football. Every so often, a group of us gets together to play a kickabout, and it's honestly the best part of my week.&lt;/p&gt;

&lt;p&gt;Getting to the pitch is the easy part. Getting &lt;em&gt;ready&lt;/em&gt; for the pitch is the problem.&lt;/p&gt;

&lt;p&gt;Before every game, the group chat turns into chaos:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"I'm in"&lt;/li&gt;
&lt;li&gt;"maybe, depends on my shift"&lt;/li&gt;
&lt;li&gt;"count me in, bringing my cousin"&lt;/li&gt;
&lt;li&gt;"sorry guys, out this week"&lt;/li&gt;
&lt;li&gt;"update: I'm in after all!"&lt;/li&gt;
&lt;li&gt;...and then, ten minutes before kickoff, someone asks, "so how many are we?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Someone has to scroll through 40 messages, count heads, decide whether "maybe" means "yes", and then pick two fair teams. If you've ever organised a game, you know the second part is even harder. Put all the strong players on one side and the match is dead after ten minutes. Everyone knows who the good players are, but nobody wants to be the one who splits them up.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Touch Grass FC&lt;/strong&gt;: a small app that does the admin so you can get outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You paste the group chat exactly as it is, messy and all.&lt;/li&gt;
&lt;li&gt;An open-weight AI model running &lt;strong&gt;on your own laptop&lt;/strong&gt; reads it and sorts everyone into &lt;strong&gt;in&lt;/strong&gt;, &lt;strong&gt;out&lt;/strong&gt; or &lt;strong&gt;maybe&lt;/strong&gt;. It even picks up guests ("bringing my cousin Brian") as separate players.&lt;/li&gt;
&lt;li&gt;You choose how many teams you want, from 2 to 6.&lt;/li&gt;
&lt;li&gt;It builds &lt;strong&gt;balanced teams&lt;/strong&gt; using the player ratings (1 to 5) that you give it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The screen part takes about a minute. After that, the screen's job is done, and the rest of the evening is spent on the pitch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who is it for?&lt;/strong&gt; Anyone who organises a pickup game: five-a-side with friends, a church or office team, a Sunday league. It's built for the organiser, who is the person always stuck doing the maths while everyone else is already warming up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmzydtru5y11ac9rrxq8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frmzydtru5y11ac9rrxq8.png" alt="home screen" width="800" height="590"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Pasted group chat and player ratings&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmujzj4fef3r0pjxqg6zj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmujzj4fef3r0pjxqg6zj.png" alt="group chat" width="800" height="581"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7kst5gfqjjex8mt15lh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb7kst5gfqjjex8mt15lh.png" alt="teams formed" width="800" height="565"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The teams formed by touch grass fc with balanced sides!
&lt;/h2&gt;

&lt;p&gt;I tested it with a 20-person group chat that I wrote with traps in it on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;two guests ("bringing my brother Dennis", "Moses is coming with me")&lt;/li&gt;
&lt;li&gt;people who change their mind (maybe to in, out to in, in to out)&lt;/li&gt;
&lt;li&gt;random chatter ("who has the ball?", "don't forget the bibs")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I ran the test chat for the first time, the model skipped the people who had said they were out, instead of listing them as "out".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Took it outside:&lt;/strong&gt; Yes and I will be taking it outside more often because everyone loves it, it helps save a lot of time, reduces the screen time of the coach and makes us  really touch grass!!.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feq9vplluyu2dbtrermpg.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feq9vplluyu2dbtrermpg.jpeg" alt="Touched grass" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/AdahJerrie/touch-grass-fc.git" rel="noopener noreferrer"&gt;https://github.com/AdahJerrie/touch-grass-fc.git&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The project is deliberately small, because I only had about four days:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;touch-grass-fc/
├── app.py        # the web page (Streamlit)
├── rsvp.py       # the "secretary": asks the local AI to read the chat
├── teams.py      # the "coach": builds balanced teams
└── .streamlit/
    └── config.toml   # the green football theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Python 3.9 or newer&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/strong&gt; installed and running (this runs open-weight models on your computer)&lt;/li&gt;
&lt;li&gt;About &lt;strong&gt;4 GB of free RAM&lt;/strong&gt; for the model&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Install and run
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Download the open-weight model (about 4 GB, one time, needs internet)&lt;/span&gt;
ollama pull gemma4:e2b-it-qat

&lt;span class="c"&gt;# 2. Install the Python libraries&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; pip &lt;span class="nb"&gt;install &lt;/span&gt;requests streamlit

&lt;span class="c"&gt;# 3. Start the app&lt;/span&gt;
python &lt;span class="nt"&gt;-m&lt;/span&gt; streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your browser opens the app. After the one-time model download, &lt;strong&gt;everything works with no internet at all&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;On Windows, I run it with &lt;code&gt;python -m streamlit&lt;/code&gt; instead of just &lt;code&gt;streamlit&lt;/code&gt;, because the plain command isn't always on the PATH.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The models and tools
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Piece&lt;/th&gt;
&lt;th&gt;What I used&lt;/th&gt;
&lt;th&gt;Open?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;The AI "brain"&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;), a small open-weight model&lt;/td&gt;
&lt;td&gt;Open weights (Apache 2.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Running the model&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Ollama&lt;/strong&gt;, which serves the model on &lt;code&gt;localhost&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The logic&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Python&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;The interface&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Streamlit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Open source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;My laptop has &lt;strong&gt;no GPU and 8 GB of RAM&lt;/strong&gt;, so the model had to be small. Gemma 4's smallest version runs entirely on the CPU. The first request is slow (it took me about two minutes, because the model has to be loaded from disk into memory), but after that it stays loaded and answers much faster.&lt;/p&gt;

&lt;h3&gt;
  
  
  The idea: a secretary and a coach
&lt;/h3&gt;

&lt;p&gt;I split the job in two, because AI is good at one half and plain code is better at the other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The secretary (AI):&lt;/strong&gt; reads messy human language and turns it into clean data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The coach (plain Python):&lt;/strong&gt; takes that clean data and makes fair decisions with simple, predictable maths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't ask the AI to build the teams, because a small model can get maths and fairness wrong. Code does that part the same way every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 1: the secretary
&lt;/h3&gt;

&lt;p&gt;Ollama runs a small server on my laptop, so my Python code just sends a message to &lt;code&gt;localhost&lt;/code&gt; and gets an answer back. Nothing leaves the machine.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:11434/api/chat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gemma4:e2b-it-qat&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# force a structured answer
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keep_alive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30m&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# keep the model loaded between requests
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;  &lt;span class="c1"&gt;# consistent, not creative
&lt;/span&gt;    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;players&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two settings made a big difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"format": "json"&lt;/code&gt; forces the model to answer with structured data my code can read, instead of friendly paragraphs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"temperature": 0&lt;/code&gt; makes it behave consistently. I want it to read a chat, not be creative with it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The prompt tells the model exactly what to return and sets a few rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;status must be exactly one of: "in", "out", "maybe".
If someone brings a guest, add the guest as a separate player with status "in".
Include every person who posted, even those who are out.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last rule exists because of a real mistake. In my first test, the model &lt;strong&gt;skipped the people who had said they were out&lt;/strong&gt;, instead of listing them as "out". I fixed it by adding a rule to the prompt. &lt;strong&gt;FILL IN: confirm whether this fixed it for you.&lt;/strong&gt; Small models follow most instructions, but not always all of them, so you have to test them properly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 2: the coach
&lt;/h3&gt;

&lt;p&gt;The team builder works like dealing cards. Players are sorted from strongest to weakest, and each one goes to whichever team is currently the weakest. A cap on team size stops one team from getting too many players.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_teams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ratings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_teams&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;players&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ratings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;teams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_teams&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;scores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_teams&lt;/span&gt;
    &lt;span class="n"&gt;max_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;num_teams&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="n"&gt;num_teams&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;players&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ratings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;open_teams&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_teams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_size&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;weakest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;open_teams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;weakest&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;scores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;weakest&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;teams&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scores&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ratings are &lt;strong&gt;optional&lt;/strong&gt;. If you don't enter any, everyone gets a default of 3 and the teams are simply split evenly by number. A guest nobody has rated also gets that default.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 3: making it usable by non-developers
&lt;/h3&gt;

&lt;p&gt;My first version was a command-line script, which is useless to a friend who has never opened a terminal. So I added a web page with Streamlit: a box for the chat, a box for ratings, a slider for the number of teams, and a button. Each team gets its own coloured "kit" card, and the page has a green pitch-style banner instead of Streamlit's default look.&lt;/p&gt;

&lt;h3&gt;
  
  
  Challenges
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Running on an 8 GB laptop with no GPU.&lt;/strong&gt; I had to choose the smallest useful model and accept a slow first load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small models make mistakes.&lt;/strong&gt; Changes of mind ("maybe" turned into "in") and skipped players needed careful prompt tuning.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keeping the AI's job small.&lt;/strong&gt; The more I let plain code do the work, the more reliable it became.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations and what's next
&lt;/h3&gt;

&lt;p&gt;I want to be honest about what this version can't do yet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The organiser has to run it, and needs Ollama and the model installed. It's not a one-click install yet.&lt;/li&gt;
&lt;li&gt;Ratings are typed in by hand each time.&lt;/li&gt;
&lt;li&gt;Names in the ratings box must match the chat spelling exactly.&lt;/li&gt;
&lt;li&gt;It handles one game at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Next:&lt;/strong&gt; upload a WhatsApp chat export directly, save ratings between sessions, and package it so a non-developer can install it with one click.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;A tool like this reads your &lt;strong&gt;friends' names and what they say in a private group chat&lt;/strong&gt;. That's the kind of data I don't want to send to a company's server just to figure out who is playing on Saturday.&lt;/p&gt;

&lt;p&gt;Open-weight models gave me things a closed API couldn't:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Privacy by default.&lt;/strong&gt; The chat never leaves my laptop. Ollama also offers larger hosted &lt;code&gt;-cloud&lt;/code&gt; versions of Gemma, and I deliberately avoided them, because they would send the chat to someone else's servers and defeat the point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works with no network.&lt;/strong&gt; Data and Wi-Fi aren't guaranteed at the pitch or on the way there. After the one-time download, Touch Grass FC doesn't need either.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; No API key, no per-request bill, no usage limit. A weekend kickabout shouldn't need a subscription.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I control the behaviour.&lt;/strong&gt; I can tune the prompt, set the temperature, keep the model loaded, or swap in a different model by changing one line, without asking permission or waiting for an API change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I can see how it works.&lt;/strong&gt; Because the whole stack is open, I could understand each piece, which also made this a real learning project for me.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;em&gt;The honest trade-off is that a small local model is less capable than a giant cloud one. But for reading a football group chat, small was enough, and the privacy and offline benefits made it the better fit.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; Gemma 4 (E2B) is the whole "brain" of the app. It runs locally through Ollama, reads the raw group chat, and turns it into structured data. Without it, the project doesn't work.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>SafeBite AI: Allergy-Aware Meal Planner Agent Powered by Mastra &amp; Gemini</title>
      <dc:creator>Jerrie Adah</dc:creator>
      <pubDate>Mon, 05 Oct 2026 10:27:13 +0000</pubDate>
      <link>https://dev.to/adahjerrie/safebite-ai-allergy-aware-meal-planner-powered-by-mastra-gemini-7m</link>
      <guid>https://dev.to/adahjerrie/safebite-ai-allergy-aware-meal-planner-powered-by-mastra-gemini-7m</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the Hacktoberfest Weekend Challenge: Build for a Friend&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Check it out
&lt;/h3&gt;

&lt;h2&gt;
  
  
  SafeBite AI — Allergy-Aware Agentic Meal Planner
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live Demo:&lt;/strong&gt; &lt;a href="https://safebite-agent.onrender.com/" rel="noopener noreferrer"&gt;https://safebite-agent.onrender.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hacktoberfest Submissions:&lt;/strong&gt; Best Use of Mastra | Best Use of Render&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code: Github Repo&lt;/strong&gt; &lt;a href="https://github.com/AdahJerrie/safebite-agent.git" rel="noopener noreferrer"&gt;https://github.com/AdahJerrie/safebite-agent.git&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Planning daily meals while managing food allergies or strict dietary restrictions is a constant source of stress for my friend who is very selective because of allergies. Standard recipe generators often output unsafe suggestions or fail to spot hidden allergen risks (such as dairy derivatives in pre-packaged items or peanut cross-contamination).&lt;/p&gt;

&lt;p&gt;I created &lt;strong&gt;SafeBite AI&lt;/strong&gt; to solve this problem with true agentic reliability. Powered by &lt;strong&gt;Mastra AI&lt;/strong&gt; and &lt;strong&gt;Google Gemini&lt;/strong&gt;, SafeBite doesn't simply prompt a static LLM—it runs an autonomous agent that screens pantry ingredients through real-time tool checks before generating a verified recipe.&lt;/p&gt;




&lt;h2&gt;
  
  
  📸 Proof of Functionality &amp;amp; Screenshots
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Interactive Dashboard &amp;amp; Restrictions Selector
&lt;/h3&gt;

&lt;p&gt;Users select active dietary restrictions (e.g., Peanuts, Dairy) and specify available pantry ingredients along with a maximum prep time limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjqmvxv8mmn1uwvuzf8hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjqmvxv8mmn1uwvuzf8hx.png" alt="Interactive dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check out the demo: &lt;a href="https://safebite-agent.onrender.com/" rel="noopener noreferrer"&gt;https://safebite-agent.onrender.com/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Autonomous Agentic Output &amp;amp; Recipe Verification
&lt;/h3&gt;

&lt;p&gt;The Mastra agent validates ingredients against all listed restrictions and generates a structured, step-by-step recipe ("Quick Garlic Chicken &amp;amp; Wilted Spinach Rice Bowl").&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohurix0h65d1r4dd9dy3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fohurix0h65d1r4dd9dy3.png" alt="detailed recipe" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Restriction &amp;amp; Pantry Inputs:&lt;/strong&gt; Allows users to toggle active allergens (Peanuts, Tree Nuts, Dairy, Eggs, Soy, Gluten, Shellfish, Fish) and input exact pantry items.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous Ingredient Screening:&lt;/strong&gt; The &lt;code&gt;safeBiteAgent&lt;/code&gt; leverages Mastra's tool-calling engine to execute &lt;code&gt;allergenCheckerTool&lt;/code&gt;, inspecting every ingredient before finalizing the recipe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution Telemetry:&lt;/strong&gt; Displays live agent status badges and execution telemetry on the interface so users can confirm active safety validation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clear Markdown Recipes:&lt;/strong&gt; Outputs comprehensive cooking instructions complete with prep time breakdowns, safe ingredient lists, and safety disclaimers.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Framework:&lt;/strong&gt; &lt;strong&gt;Mastra AI&lt;/strong&gt; (&lt;code&gt;@mastra/core&lt;/code&gt;) for agent instructions, tool binding, and runtime execution.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM Model Provider:&lt;/strong&gt; &lt;strong&gt;Google Gemini&lt;/strong&gt; (&lt;code&gt;gemini-3.8-flash&lt;/code&gt;) via &lt;code&gt;@ai-sdk/google&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend &amp;amp; Design:&lt;/strong&gt; &lt;strong&gt;Next.js 15 (App Router)&lt;/strong&gt; with &lt;strong&gt;Tailwind CSS v4&lt;/strong&gt; dark mode and &lt;code&gt;react-markdown&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deployment Platform:&lt;/strong&gt; Deployed directly as a Node.js web service on &lt;strong&gt;Render&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Challenges I Ran Into
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Module Resolution &amp;amp; Build Errors:&lt;/strong&gt; Configured PostCSS and Tailwind v4 to use &lt;code&gt;.cjs&lt;/code&gt; extensions to handle Next.js ESM module constraints (&lt;code&gt;"type": "module"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI SDK Compatibility:&lt;/strong&gt; Resolved version specification mismatches between &lt;code&gt;@mastra/core&lt;/code&gt; and &lt;code&gt;@ai-sdk/google&lt;/code&gt; to enable seamless tool calling with &lt;code&gt;agent.generate()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Build Dependencies:&lt;/strong&gt; Adjusted dependency configurations to ensure Render's automated build pipeline cleanly installs &lt;code&gt;@tailwindcss/postcss&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Accomplishments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True Tool-Driven Workflow:&lt;/strong&gt; Built an AI agent that actively invokes validation tools instead of relying solely on raw text generation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seamless Render Deployment:&lt;/strong&gt; Successfully hosted the application at &lt;a href="https://safebite-agent.onrender.com" rel="noopener noreferrer"&gt;safebite-agent.onrender.com&lt;/a&gt; with automated builds and fast response times.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Prize Category Submissions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra:&lt;/strong&gt; Architected using Mastra's core Agent framework, custom tool orchestration (&lt;code&gt;allergenCheckerTool&lt;/code&gt;), and execution telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; Deployed and actively running in production as a Render Web Service.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>hacktoberfest</category>
      <category>llm</category>
    </item>
  </channel>
</rss>
