<?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: Bipul Chamoli</title>
    <description>The latest articles on DEV Community by Bipul Chamoli (@bipul_chamoli_09b5f0cc719).</description>
    <link>https://dev.to/bipul_chamoli_09b5f0cc719</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%2F4156842%2F52493be1-d68f-495a-ac53-ad4fc2ce65d9.png</url>
      <title>DEV Community: Bipul Chamoli</title>
      <link>https://dev.to/bipul_chamoli_09b5f0cc719</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bipul_chamoli_09b5f0cc719"/>
    <language>en</language>
    <item>
      <title>Availazy: Gemma 4 forgot to cancel Thursday. A keyword list didn’t.</title>
      <dc:creator>Bipul Chamoli</dc:creator>
      <pubDate>Fri, 23 Oct 2026 15:19:32 +0000</pubDate>
      <link>https://dev.to/bipul_chamoli_09b5f0cc719/availazy-gemma-4-reads-the-volunteer-group-chat-on-my-laptop-python-does-the-dates-and-i-just-1nm8</link>
      <guid>https://dev.to/bipul_chamoli_09b5f0cc719/availazy-gemma-4-reads-the-volunteer-group-chat-on-my-laptop-python-does-the-dates-and-i-just-1nm8</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;blockquote&gt;
&lt;p&gt;🦥 &lt;strong&gt;TL;DR.&lt;/strong&gt; Paste a week of volunteer chat messages. &lt;strong&gt;Gemma 4&lt;/strong&gt;, running locally in Ollama, copies the words that matter ("Tuesday", "after 3", "if my work shift gets moved"). Plain &lt;strong&gt;Python&lt;/strong&gt; turns them into dates, times and a day-by-day history. You &lt;strong&gt;accept or reject&lt;/strong&gt; each suggestion beside the message it came from, and only what you accept reaches the final sheet.&lt;/p&gt;

&lt;p&gt;▶️ &lt;a href="https://www.youtube.com/watch?v=Qlq_kEsDr4g" rel="noopener noreferrer"&gt;5:48 demo&lt;/a&gt; · 🧪 &lt;a href="https://availability-ledger-replay.onrender.com/app" rel="noopener noreferrer"&gt;Try the replay in your browser&lt;/a&gt; · 💻 &lt;a href="https://github.com/bipul724/availability-ledger" rel="noopener noreferrer"&gt;Code, MIT licence&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Thursday, 9:10 a.m.
&lt;/h2&gt;

&lt;p&gt;On Monday, Maria posts in the volunteer group chat:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I can do Tuesday and Thursday mornings.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On Thursday she posts again:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Change of plans — Thursday doesn't work anymore, sorry! Tuesday morning still good. I can add Friday 2-5.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are 48 more messages that week. Priya can do "Monday after 3". Kofi "might be able to do Thursday afternoon if my work shift gets moved". Rui writes "Next Tuesday" on a Monday, which could mean tomorrow or the Tuesday after. Somebody has to read all of it, remember who changed their mind, and type it into a sheet. Then somebody changes their mind again.&lt;/p&gt;

&lt;p&gt;Riverside Community Pantry doesn't exist. The pantry, its 18 volunteers and all 50 messages are invented, so this could be built and tested without touching anyone's real chat. But if you've ever coordinated volunteers, you've lived this week.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Availazy&lt;/strong&gt; is a local review tool for the person who asks "when can you help?" and then has to make sense of the answers.&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%2F7r1a2mxhe5zw00owm3fw.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%2F7r1a2mxhe5zw00owm3fw.png" alt="Availazy landing page: Local AI for Coordinators Who Have Better Things to Do" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Availazy turns volunteer messages into suggestions you can review beside their source.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You paste dated chat messages (or a WhatsApp text export) and your volunteer list, pick the week, and Availazy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📝 &lt;strong&gt;suggests availability records&lt;/strong&gt;, each shown beside its message with the quoted words highlighted;&lt;/li&gt;
&lt;li&gt;🔁 &lt;strong&gt;keeps everyone's history day by day&lt;/strong&gt;, so a correction replaces the earlier answer, a cancellation removes it, and a "maybe" stays a maybe until the person confirms;&lt;/li&gt;
&lt;li&gt;✋ &lt;strong&gt;refuses to guess.&lt;/strong&gt; Anything the words don't settle goes on a &lt;strong&gt;Needs a decision&lt;/strong&gt; list;&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;exports a final sheet built only from what you accepted.&lt;/strong&gt; It says &lt;em&gt;complete&lt;/em&gt; only when every message is marked reviewed, nothing is pending and nothing is unresolved. Otherwise it says &lt;em&gt;provisional&lt;/em&gt;, and it still downloads.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It records &lt;strong&gt;availability&lt;/strong&gt;, not shift assignments. The person who gets to be lazy is the coordinator: they stop retyping and start deciding.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Qlq_kEsDr4g" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For readable interface details, select ⚙️ → Quality → 1080p HD.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The video opens with the local app: Gemma 4 reads two invented messages live, through Ollama, on my laptop. Then it switches to the hosted replay for the full week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it yourself:&lt;/strong&gt; &lt;a href="https://availability-ledger-replay.onrender.com/app" rel="noopener noreferrer"&gt;https://availability-ledger-replay.onrender.com/app&lt;/a&gt;. This is a &lt;strong&gt;replay&lt;/strong&gt;, not live inference. It serves the fictional week with Gemma's answers saved in advance, so it runs on free hosting without a model. Reviewing, history, the chart and both exports all work; pasting your own messages is switched off. Free hosting sleeps when idle, so the first visit can take about a minute.&lt;/p&gt;

&lt;p&gt;There's also an illustrated guide with diagrams at &lt;a href="https://availability-ledger-replay.onrender.com/docs" rel="noopener noreferrer"&gt;/docs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Gemma missed Maria's cancellation. That's the point.
&lt;/h2&gt;

&lt;p&gt;When Gemma read Maria's Thursday message in the saved run, it got her Tuesday right and her new Friday right, and it &lt;strong&gt;missed the cancellation completely&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So Availazy doesn't trust one reader. In the default mode, a plain keyword reader reads every message alongside the model. When the rules see a cancellation the model didn't, the app adds it, &lt;strong&gt;flagged&lt;/strong&gt;, so I can check it:&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%2Fwot524ihf0ror9x07yvx.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%2Fwot524ihf0ror9x07yvx.png" alt="Maria's change of plans message with three suggestions; the Thursday cancellation is marked Added by the rules" width="800" height="670"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Gemma found Tuesday and Friday. The rules added the Thursday cancellation for review.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Until I accept it, the sheet shows Thursday as a &lt;em&gt;pending change&lt;/em&gt;. One click on &lt;strong&gt;Accept&lt;/strong&gt; and Thursday is gone from the final sheet. In &lt;strong&gt;Model only&lt;/strong&gt; mode, the same replay leaves Maria on Thursday morning. That one message is the whole argument for running two readers.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It: the model reads, code decides
&lt;/h2&gt;

&lt;p&gt;The design is one strict split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;message ──▶ Gemma 4 E2B (Ollama, local) ──▶ copied words + a proposed status
        └─▶ keyword rules ─────────────────▶ a second opinion
                         │
                         ▼
     Python: who, which date, which hours, does the quote exist,
             what does this replace, what is still unsure
                         │
                         ▼
     You: accept / reject / edit, mark each message reviewed
                         │
                         ▼
     Final sheet (accepted records only) + review log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;1. Gemma copies words; it doesn't do dates.&lt;/strong&gt; For each message, Gemma fills a JSON schema that Ollama enforces. This is its real saved answer for Maria's "Tuesday morning still good":&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"person"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENDER"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"day_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tuesday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"time_text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"morning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"available"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"replaces_earlier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"excerpt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Tuesday morning still good"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No date and no hours, just the words. Gemma didn't even mark it as a replacement (&lt;code&gt;"replaces_earlier": false&lt;/code&gt;). Python spotted "Change of plans" and marked Maria's new answers as replacing her old ones, with a visible note on each card. &lt;strong&gt;Python&lt;/strong&gt; also works out that "Tuesday", sent on Thursday 5 November for the week of 9 November, means &lt;strong&gt;10 November&lt;/strong&gt;, and that "morning" means a fixed window:&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;DEFAULT_PERIODS&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;morning&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;09:00&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;12:00&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;afternoon&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;13:00&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;17:00&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;evening&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;17:00&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;20:00&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Open ranges are code too. "After 3" runs to 20:00, the end of the planning day, and the card says so:&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%2Fq4kln5ds2u4mavxbs55h.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%2Fq4kln5ds2u4mavxbs55h.png" alt="Priya's message becomes Monday 15:00-20:00 and Wednesday 13:00-17:00, with the note that after 3 runs to 20:00" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;“After 3” becomes 15:00–20:00, with the assumption written on the card.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Every quote is checked against the message.&lt;/strong&gt; If the model's quote &lt;em&gt;and&lt;/em&gt; its day/time words are both missing from the message, the suggestion is &lt;strong&gt;not applied&lt;/strong&gt; unless you choose "Use it anyway". If only some words are missing, it's flagged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. History is kept in order.&lt;/strong&gt; Later messages change only the days and times they mention. Kofi's "if my work shift gets moved" stays &lt;em&gt;conditional&lt;/em&gt; until his later "my shift got moved, so Thursday afternoon is a yes". Olu's "I have to drop out for the rest of this week", sent on a Wednesday, cancels Wednesday through Sunday, taking out his Thursday evening and Saturday morning:&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%2F8wrzpdxkrqesey5bp3my.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%2F8wrzpdxkrqesey5bp3my.png" alt="Changes over time: replacements and cancellations in order, each linked to its message" width="800" height="1460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An excerpt of the change history, with replacements and cancellations linked to their source messages.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. When the words don't settle it, it asks.&lt;/strong&gt; "Next Tuesday" sent on a Monday could be two dates. "This Saturday" sent the week before is outside the week. A name that matches nobody on the roster isn't guessed:&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%2Fl9gm10yhhpkwk84qh65v.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%2Fl9gm10yhhpkwk84qh65v.png" alt="Needs a decision list with ten items, including next tuesday that could be two dates and a date outside the week" width="800" height="1234"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Ambiguous dates, unmatched names and unresolved conditions stay on the decision list.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. The calm wrong answer.&lt;/strong&gt; Marta asks: &lt;em&gt;"Is Maria still doing Tuesday morning? If not I can take it."&lt;/em&gt; Gemma filed Marta's offer &lt;strong&gt;under Maria&lt;/strong&gt;, as a conditional Tuesday. It's wrong, and it looks perfectly reasonable. That's why every suggestion sits beside its message, and why this one carries "Only one of the two readers found this":&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%2F9ro468mpp7iqoo2fgu2j.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%2F9ro468mpp7iqoo2fgu2j.png" alt="Marta's message, with the model's suggestion wrongly filed under Maria Lopez and flagged Only one of the two readers found this" width="799" height="321"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Marta wrote the message, but Gemma assigned the offer to Maria. Reviewing the source makes the mistake visible.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One click on &lt;strong&gt;Reject&lt;/strong&gt; and Maria's Tuesday is firm again.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Update, 24 Oct: after a reader's comment, Availazy now also warns when a suggestion is for someone other than the sender, or when the message names another volunteer. Both warnings block one-click accept, so it now takes 36 suggestions instead of the 39 shown in the video.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Only accepted records reach the sheet.&lt;/strong&gt; Here are the first rows of the real export from the replay, after I accepted Maria's cancellation, rejected the m48 mix-up, accepted the 36 warning-free suggestions and marked two messages reviewed:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# availability-final v1; week=2026-11-09; status=provisional; unreviewed=m01 m03 … m50; pending=24; blocking_issues=0
volunteer,date,start,end,status,condition,cell_state,sources
Maria Lopez,2026-11-10,09:00,12:00,available,,confirmed,m28.1
Maria Lopez,2026-11-13,14:00,17:00,available,,confirmed,m28.2
Marta Lopes,2026-11-09,09:00,12:00,available,,needs_review,m17.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;em&gt;(The &lt;code&gt;unreviewed&lt;/code&gt; list is shortened here.)&lt;/em&gt; Maria has Tuesday and Friday, and no Thursday. Her messages are marked reviewed, so her cells are &lt;code&gt;confirmed&lt;/code&gt;. Everyone else is &lt;code&gt;needs_review&lt;/code&gt;, and the whole file honestly says &lt;code&gt;provisional&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The stack is boring on purpose:&lt;/strong&gt; a Python standard-library server, plain HTML/CSS/JS with no framework or CDN, and Ollama on &lt;code&gt;127.0.0.1&lt;/code&gt;. The only optional dependency is &lt;code&gt;sentry-sdk&lt;/code&gt;, for opt-in tracing.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Lazy Payoff 🦥
&lt;/h2&gt;

&lt;p&gt;Here's the fictional week, by the numbers, from the replay:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Messages&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;50&lt;/strong&gt; (one exact repeat is collapsed automatically)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Suggestions, each beside its quote&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;67&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accepted with &lt;strong&gt;one click&lt;/strong&gt; (no warnings)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;36&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Left for a real decision (a warning, or not applied)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;31&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Items on the &lt;em&gt;Needs a decision&lt;/em&gt; list&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;10&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma + rules reading the week on my 8 GB Apple M2&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;131 s&lt;/strong&gt; end to end (one dated run, not a benchmark)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fsma7zvjdi8hi458l9vsk.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%2Fsma7zvjdi8hi458l9vsk.png" alt="People available chart after the one-click accept: accepted, pending and conditional slots for the week" width="800" height="563"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After the one-click accept: dark green is accepted, light green still waits for review, striped orange is conditional.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So the work shifts from reading 50 messages and typing a sheet to &lt;strong&gt;checking 31 flagged suggestions&lt;/strong&gt;, each with its quote right next to it. The model reads the whole week in about two minutes: long enough to make a cup of tea.&lt;/p&gt;

&lt;p&gt;I haven't timed a real coordinator, so I won't claim minutes saved. The repo has a written protocol for that study; it hasn't been run.&lt;/p&gt;
&lt;h2&gt;
  
  
  How well does it work?
&lt;/h2&gt;

&lt;p&gt;I didn't want to grade my own homework, so a &lt;strong&gt;separate ChatGPT session wrote a test week I'd never seen&lt;/strong&gt; (set C: 24 fictional messages; I rewrote five of them). Its files were frozen by hash before any implementing agent saw them, and then it was &lt;strong&gt;scored once&lt;/strong&gt;. That first score is kept exactly as it came out:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Reader&lt;/th&gt;
&lt;th&gt;Person-days right&lt;/th&gt;
&lt;th&gt;Silent errors&lt;/th&gt;
&lt;th&gt;Cancellations missed on the sheet&lt;/th&gt;
&lt;th&gt;Sheet agreement with the answer key&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Keyword rules only&lt;/td&gt;
&lt;td&gt;15/20&lt;/td&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;4/8&lt;/td&gt;
&lt;td&gt;0.433&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E2B only&lt;/td&gt;
&lt;td&gt;17/20&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;4/8&lt;/td&gt;
&lt;td&gt;0.403&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Gemma 4 E2B + rules&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;17/20&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;2/8&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.527&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All suggestions were left pending and nobody reviewed them: this scores the draft sheet &lt;em&gt;before&lt;/em&gt; a human touches it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Four things I didn't expect:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The keyword list earned its seat.&lt;/strong&gt; On the sheet, rules alone (0.433) actually beat Gemma alone (0.403). Gemma read more person-days right, but each reader caught things the other missed. Together they halved the missed cancellations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Mostly right" can still make a wrong sheet.&lt;/strong&gt; Gemma + rules read 17 of 20 person-days right, yet the sheet matched only about half the time slots. The biggest culprit was &lt;em&gt;"Please change my availability on monday to 1-4 pm"&lt;/em&gt;. The hours were right, but nothing treated it as a &lt;em&gt;replacement&lt;/em&gt;, because "change" wasn't one of my replacement words. So the old all-day Monday stayed underneath.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every fix went into Python, not the prompt.&lt;/strong&gt; The prompt is the same version that was scored. Five deterministic fixes followed: explicit dates beat weekday words, "change" became a replacement cue, a firm follow-up settles a maybe, "T. Vekar" is an ambiguity rather than a stranger, and the rules learned more negations. Re-scored from the &lt;em&gt;same saved Gemma outputs&lt;/em&gt;, set C now reaches 1.0, but I label that &lt;strong&gt;post-hoc&lt;/strong&gt;: I fixed what I'd seen, so it proves little about new messages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honest numbers are less impressive and more useful.&lt;/strong&gt; An earlier set B (20 messages) scored 30/35 person-days and 0.776 sheet agreement on its first scoring. Every frozen table, every caveat and every after-the-fact change is in &lt;a href="https://github.com/bipul724/availability-ledger/blob/main/evaluation/RESULTS.md" rel="noopener noreferrer"&gt;RESULTS.md&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Built with
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🧠 &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (&lt;a href="https://ollama.com/library/gemma4:e2b-it-qat" rel="noopener noreferrer"&gt;&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt;&lt;/a&gt;, Apache 2.0) in &lt;strong&gt;Ollama&lt;/strong&gt;. About a 4.3 GB download and about 1.6 GB of memory once loaded on my 8 GB Mac, so it runs alongside a browser. It's the reader in the local app, and it produced every saved answer in the replay.&lt;/li&gt;
&lt;li&gt;☁️ &lt;strong&gt;Render&lt;/strong&gt; hosts the &lt;a href="https://availability-ledger-replay.onrender.com/app" rel="noopener noreferrer"&gt;fictional replay&lt;/a&gt;, so judges can click through a full week without installing anything. The server runs in a demo mode that refuses any message, roster or week it didn't serve, and it never calls a model.&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;GitHub Actions&lt;/strong&gt; runs on every push and pull request to &lt;code&gt;main&lt;/code&gt;, with three jobs: 128 unit tests (8 tracing tests skip without the optional SDK); a SHA-256 check that the frozen evaluation results haven't been touched; and a re-score of all three test sets from the saved model outputs, with no model calls. &lt;a href="https://github.com/bipul724/availability-ledger/actions/runs/40124840971" rel="noopener noreferrer"&gt;Run #29&lt;/a&gt; passed all three jobs.&lt;/li&gt;
&lt;li&gt;🔭 &lt;strong&gt;Sentry Agent Tracing&lt;/strong&gt; is opt-in and off by default. When on, two allowlists in my adapter limit what's sent to structure: timings, token counts, model and stage names. Message text, names, quotes, prompts and replies are excluded, and offline tests check that planted fake content never appears. The traces earned their keep. In one run, &lt;strong&gt;four model calls hit the 300-second client deadline&lt;/strong&gt; waiting for Ollama. A later run made &lt;strong&gt;49 calls in 1.42 minutes with no errors&lt;/strong&gt;. (The traces don't say &lt;em&gt;why&lt;/em&gt; the earlier calls stalled, and I won't pretend they do.) One caveat I found: received traces showed location (geo) attributes that my code doesn't send. After a settings change, the four I inspected appeared filtered, but I haven't audited everything Sentry receives (&lt;a href="https://github.com/bipul724/availability-ledger/blob/main/docs/SENTRY_HANDOFF.md" rel="noopener noreferrer"&gt;dated evidence&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&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%2Fddgw27fknlzgl8iqzi2h.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%2Fddgw27fknlzgl8iqzi2h.png" alt="Sentry timeline: four Gemma calls in a row each last 5 minutes and fail; the selected call shows No input for this span" width="799" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;One traced run: four Gemma calls in a row each hit the 5-minute client timeout. The selected call shows "No input for this span": no message text was sent.&lt;/em&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%2Fceagkr59zsqrvrzpheyu.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%2Fceagkr59zsqrvrzpheyu.png" alt="GitHub Actions run 29: unit tests, protected evidence check and cache-only re-score all passed" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every push to main runs three jobs: unit tests, a SHA-256 check of the frozen evidence, and a re-score from saved Gemma replies with no model calls.&lt;/em&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%2F1vsc32zw5h8vn3nklqxl.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%2F1vsc32zw5h8vn3nklqxl.png" alt="Render dashboard: free availability-ledger-replay web service, Live, auto-deploying each push to main" width="799" height="363"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The replay is one free Render web service, managed by a Blueprint. Every push to main auto-deploys it, including the commit that added the new sender warnings (84d5fec).&lt;/em&gt;&lt;/p&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/bipul724" rel="noopener noreferrer"&gt;
        bipul724
      &lt;/a&gt; / &lt;a href="https://github.com/bipul724/availability-ledger" rel="noopener noreferrer"&gt;
        availability-ledger
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;Availazy&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Local AI for Coordinators Who Have Better Things to Do.&lt;/strong&gt; Let AI read. You decide.&lt;/p&gt;

&lt;p&gt;Availazy was called Availability Ledger until 2026-10-22 (BRAND-01). The repository and Render service keep their existing &lt;code&gt;availability-ledger&lt;/code&gt; identifiers; the Sentry organization remains &lt;code&gt;availability-ledger&lt;/code&gt; and its project remains &lt;code&gt;python&lt;/code&gt;. Older records in this repository use the old product name.&lt;/p&gt;

&lt;p&gt;A local review tool for volunteer coordinators who collect availability through ordinary messages. You paste a week of dated chat messages and your volunteer roster; the tool suggests availability records, shows each one beside the message it came from, and keeps a per-person, per-day history of offers, corrections, cancellations and conditions. You review the suggestions, explicitly mark each message reviewed, and export a reconciled final sheet built from accepted records. A separate review log preserves the record trail.&lt;/p&gt;

&lt;p&gt;It records &lt;strong&gt;availability&lt;/strong&gt; (when people say they can or cannot help), not shift assignments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A local open&lt;/strong&gt;…&lt;/p&gt;&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/bipul724/availability-ledger" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;Volunteer availability is quietly personal. "I'm at a wedding." "If my work shift gets moved." Read together, a week of these messages maps people's families, jobs and lives. Here's what an open model on the coordinator's own laptop makes possible:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The messages stay on the computer.&lt;/strong&gt; In the local app they go only to Ollama on &lt;code&gt;127.0.0.1&lt;/code&gt;. There's no cloud AI service in the loop, and optional tracing is off unless you turn it on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No account, no API key, no per-message bill.&lt;/strong&gt; Once Ollama and the model are installed, it works offline. A small food pantry can afford "free forever".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It fits the hardware people actually have.&lt;/strong&gt; An 8 GB laptop is enough, so the volunteer coordinator doesn't need a GPU or a budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can be checked.&lt;/strong&gt; The prompt, the schema, the rules, the test sets and the frozen scores are all in the repo. Anyone can re-score them from the saved outputs without running a model, which is exactly what CI does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It can be swapped.&lt;/strong&gt; The model is a menu choice of whatever Ollama has installed. I also evaluated Qwen3.5 4B against the same answer keys.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A hosted API might be smarter. But the right answer to "where do my volunteers' messages go?" should be "nowhere".&lt;/p&gt;

&lt;h2&gt;
  
  
  What it can't do (yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It has only seen &lt;strong&gt;fictional&lt;/strong&gt; messages. No real coordinator has used it, and the test sets share an author with the code, except set C.&lt;/li&gt;
&lt;li&gt;Times are read as written; the timezone field is stored but not converted. "At 9" isn't read.&lt;/li&gt;
&lt;li&gt;Up to 20 volunteers, one browser tab, and nothing is saved between sessions.&lt;/li&gt;
&lt;li&gt;The keyword reader still misreads some phrasings. It read "Saturday 9–12 if it isn't raining" as &lt;em&gt;unavailable&lt;/em&gt; instead of &lt;em&gt;conditional&lt;/em&gt;, which is exactly why every suggestion sits beside its message.&lt;/li&gt;
&lt;li&gt;Not tested in Safari, Firefox or with a screen reader.&lt;/li&gt;
&lt;/ul&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 local reader, held to a strict "copy the words, code does the maths" contract, measured against a rules baseline and a frozen test set written by a separate AI session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt;: the interactive replay that lets anyone review a full fictional week without installing a model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of GitHub Copilot&lt;/strong&gt; (via GitHub Actions): unit tests, a frozen-evidence integrity check and a cache-only re-score of every test set on every push and pull request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing&lt;/strong&gt;: opt-in, content-free model-call traces that surfaced the 300-second timeouts.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Slow and steady: the model reads, the code reconciles, and a person decides. If you coordinate volunteers, how do you collect availability today, and what's the message that always breaks your spreadsheet? 🦥&lt;/p&gt;

</description>
      <category>gemma</category>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>"red label adha kg 2 box": an offline WhatsApp order reader for Indian shops that asks before it guesses</title>
      <dc:creator>Bipul Chamoli</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:06:31 +0000</pubDate>
      <link>https://dev.to/bipul_chamoli_09b5f0cc719/red-label-adha-kg-2-box-an-offline-whatsapp-order-reader-for-indian-shops-that-asks-before-it-i96</link>
      <guid>https://dev.to/bipul_chamoli_09b5f0cc719/red-label-adha-kg-2-box-an-offline-whatsapp-order-reader-for-indian-shops-that-asks-before-it-i96</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;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%2F5e9nd6p5epb5d6d53sut.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%2F5e9nd6p5epb5d6d53sut.png" alt="ParchiMatch review screen: WhatsApp order lines with green checked rows and amber questions" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Small Indian shops get orders on WhatsApp in a mix of Hindi, English and shorthand. &lt;strong&gt;ParchiMatch Lite&lt;/strong&gt; turns each line into a checked billing row, on the shop's own 8 GB laptop with the Wi-Fi off. When it isn't sure, it asks one short question instead of guessing. On 80 locked test orders it filled in &lt;strong&gt;0 wrong rows&lt;/strong&gt;, and on messages my friends typed by hand, also &lt;strong&gt;0&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;This is built for small Indian stores. Their orders come in on WhatsApp, and one message can mix Hindi, English and abbreviations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Namaste bhaiya
kit kat 10/- darjan 4
red label adha kg 2 box
12 pack lais peela 10 wala
teen petti marrie 10 rs
Kal subah tak bhej dena
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Someone has to retype every line into billing software like Tally, Busy or Marg. &lt;em&gt;Petti&lt;/em&gt; is a carton, &lt;em&gt;darjan&lt;/em&gt; a dozen, &lt;em&gt;adha kg&lt;/em&gt; half a kilo, &lt;em&gt;lais peela&lt;/em&gt; the yellow Lay's. And if a line is matched to the wrong product, that's expensive: the wrong stock gets packed, billed and put on the van.&lt;/p&gt;

&lt;p&gt;So I tried to build something that needs &lt;strong&gt;minimal input from a person, but asks for their full attention exactly where a mistake would cost money.&lt;/strong&gt; It reads each line, matches it to the shop's own catalogue, and sorts it into one of three colours:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟩 &lt;strong&gt;Green&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Matched and checked: &lt;code&gt;teen petti marrie 10 rs&lt;/code&gt; → 3 cartons of Marie ₹10 (180 packets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟧 &lt;strong&gt;Amber&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Not sure, so it asks &lt;strong&gt;one&lt;/strong&gt; short question with its best guess filled in: &lt;code&gt;marie 30 wala 20 jodi&lt;/code&gt; → &lt;em&gt;What is "jodi"? packet or carton?&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;⬜ &lt;strong&gt;Grey&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Greetings and notes, ignored: &lt;code&gt;Kal subah tak bhej dena&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Then it exports a CSV for the billing software. It also &lt;strong&gt;learns the shop's own words&lt;/strong&gt;: confirm "lais peela" once with &lt;em&gt;Remember&lt;/em&gt; ticked, and next time it goes straight through.&lt;/p&gt;

&lt;p&gt;The rule behind everything: &lt;strong&gt;a wrong row must never be filled in on its own. When unsure, ask.&lt;/strong&gt; A shop owner forgives a question. They don't forgive a wrong delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Who would pay:&lt;/strong&gt; small FMCG wholesalers and kirana suppliers who get dozens of WhatsApp orders a day and pay someone to retype them. It runs on the laptop they already have, works without internet, and costs nothing per order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing (an untested guess):&lt;/strong&gt; around &lt;strong&gt;₹299 a month&lt;/strong&gt; per shop. I haven't put this price in front of a real shop yet; that's the first thing I'll test.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/lsl2cNpkY6U" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The whole video is recorded &lt;strong&gt;with Wi-Fi off&lt;/strong&gt;, on a MacBook Air M2 with 8 GB of RAM. Even the project site in the last minute is served from the laptop. What you'll see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A real-looking order, matched in about 8 seconds:&lt;/strong&gt; 6 lines green, 5 amber, 2 notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model's guess can be wrong, and that's why it asks:&lt;/strong&gt; for &lt;em&gt;"lais peela"&lt;/em&gt; (the yellow Lay's) it suggests Magic Masala, but yellow is &lt;strong&gt;Classic Salted&lt;/strong&gt;. One tap to pick the right one, and tick &lt;em&gt;Remember&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It learns:&lt;/strong&gt; after export it says &lt;em&gt;"Learned 3 new names"&lt;/em&gt;, and the same order comes back with &lt;strong&gt;8 green lines instead of 6&lt;/strong&gt;, with the corrected products.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Four tricky lines&lt;/strong&gt;, and what the app does with each:&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;I typed&lt;/th&gt;
&lt;th&gt;What happened&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aashirvaad atta 10 kg 1,500 bag&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟩 1,500 bags&lt;/td&gt;
&lt;td&gt;An early version read this as &lt;strong&gt;1 bag&lt;/strong&gt; and accepted it (see &lt;em&gt;What I got wrong&lt;/em&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;maggi 1,5 pkt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟧 &lt;em&gt;What does "1,5" mean?&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;An unclear number is never guessed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;red label adha kg 2 box&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟧 &lt;em&gt;Is it Brooke Bond Red Label (250 g)?&lt;/em&gt;
&lt;/td&gt;
&lt;td&gt;The model guessed 250 g; &lt;em&gt;adha kg&lt;/em&gt; means 500 g. The code saw two sizes fit and asked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;पारले जी 5 वाला 2 पेटी&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;🟩 2 cartons Parle-G ₹5 (288 packets)&lt;/td&gt;
&lt;td&gt;Hindi in Devanagari works too&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Project site: &lt;a href="https://bipul724.github.io/parchimatch/" rel="noopener noreferrer"&gt;https://bipul724.github.io/parchimatch/&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&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/bipul724" rel="noopener noreferrer"&gt;
        bipul724
      &lt;/a&gt; / &lt;a href="https://github.com/bipul724/parchimatch" rel="noopener noreferrer"&gt;
        parchimatch
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;ParchiMatch Lite&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;a href="https://github.com/bipul724/parchimatch/actions/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/bipul724/parchimatch/actions/workflows/ci.yml/badge.svg" alt="CI"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Messy wholesale orders → clean billing rows, on the shop's own computer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A small FMCG wholesaler gets orders like these on WhatsApp:&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;Namaste bhaiya
parle g 5/- 2 petti
tata namak 1 kg 5 ctn, lays hara 10 rs 3 box
पारले जी 5 वाला 2 पेटी
fortune oil 2
Kal subah tak bhej dena
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Someone retypes every line into Tally, Busy or Marg. ParchiMatch Lite reads each line, matches it to the shop's own catalogue, and &lt;strong&gt;either fills in a checked row or asks one short question&lt;/strong&gt;. It never guesses a number, and it never auto-accepts a row it can't justify. It runs fully offline with an open-weight model on an 8 GB laptop.&lt;/p&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🟩 &lt;strong&gt;Green&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Matched and checked: &lt;code&gt;parle g 5/- 2 petti&lt;/code&gt; → 2 cartons Parle-G ₹5 (288 packets)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🟧 &lt;strong&gt;Amber&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;One question, best guess pre-filled: &lt;code&gt;marie 30 wala 20 jodi&lt;/code&gt; → &lt;em&gt;What is&lt;/em&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;…&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/bipul724/parchimatch" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;



&lt;p&gt;Zero npm dependencies; Node 24 runs the TypeScript directly. To try it: &lt;code&gt;npm run app -- --open&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  The model only picks the product. My code reads every number.
&lt;/h3&gt;

&lt;p&gt;Small models are good at &lt;em&gt;"which product did they mean?"&lt;/em&gt; and unreliable at &lt;em&gt;"is petti a packet or a carton?"&lt;/em&gt;. So the work is split:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Code reads every number:&lt;/strong&gt; quantities, unit words (petti, ctn, darjan, bori…), prices (&lt;code&gt;5/-&lt;/code&gt;, &lt;code&gt;10 rs&lt;/code&gt;), pack sizes (&lt;code&gt;1 kg&lt;/code&gt;, &lt;code&gt;500ml&lt;/code&gt;), Hindi number words (&lt;em&gt;teen&lt;/em&gt;, &lt;em&gt;paanch&lt;/em&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fuzzy text matching&lt;/strong&gt; handles most lines instantly, with no model at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only when fuzzy matching can't decide&lt;/strong&gt; does the local model see the line plus the 8 closest products. It answers in strict JSON, at temperature 0, and can only pick one of those 8.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;16 safety rules in code&lt;/strong&gt; check the pick: does the price match? The pack size? Is the product sold in that unit? Any failed check turns the row amber, and the question is written by the code, not the model.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Choosing the model: I tested five
&lt;/h3&gt;

&lt;p&gt;On my 8 GB MacBook I tried five open-weight models in that "decider" role, on 320 synthetic practice orders:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Auto-accepted&lt;/th&gt;
&lt;th&gt;Wrong rows&lt;/th&gt;
&lt;th&gt;No typing needed&lt;/th&gt;
&lt;th&gt;Right suggestion on amber rows&lt;/th&gt;
&lt;th&gt;Typical / slowest&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; (chosen)&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;96.1%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;65.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3.1 s / 9.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5 2B&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;94.6%&lt;/td&gt;
&lt;td&gt;52.2%&lt;/td&gt;
&lt;td&gt;2.5 s / 3.3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B&lt;/td&gt;
&lt;td&gt;88.7%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;95.6%&lt;/td&gt;
&lt;td&gt;60.9%&lt;/td&gt;
&lt;td&gt;5.3 s / 8.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3.5 4B&lt;/td&gt;
&lt;td&gt;88.2%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;95.6%&lt;/td&gt;
&lt;td&gt;62.5%&lt;/td&gt;
&lt;td&gt;5.2 s / 8.4 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 4 E4B&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;5.6 s / &lt;strong&gt;37 s&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I picked &lt;strong&gt;Gemma 4 E2B&lt;/strong&gt; because when it isn't sure, its suggestion is right far more often, so the shopkeeper taps &lt;em&gt;Confirm&lt;/em&gt; instead of typing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Then I tested it once, on orders I had locked away
&lt;/h3&gt;

&lt;p&gt;Before tuning anything, I set aside 80 test orders and locked them; CI fails if they change. Each method ran on them exactly once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Handled with no review&lt;/th&gt;
&lt;th&gt;Wrong rows auto-accepted&lt;/th&gt;
&lt;th&gt;No typing needed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact match&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;54.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fuzzy match&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model on every line&lt;/td&gt;
&lt;td&gt;84.3%&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;92.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Hybrid (the app)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;88.2%&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;92.2%&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The model was needed on only 31 of the 80 lines, about 1.5 s each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Watching the model be wrong, with Sentry
&lt;/h3&gt;

&lt;p&gt;I traced an evaluation run in Sentry, one trace per order line, with steps for fuzzy matching, the model call and the safety rules. My favourite trace is the Red Label line.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Adha kg&lt;/em&gt; means half a kilo, so the 500 g pack. &lt;strong&gt;The model answered the 250 g pack, with status "ok", confident and wrong:&lt;/strong&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%2F2uehufn4hww4gag6j795.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%2F2uehufn4hww4gag6j795.png" alt="Sentry: the model's JSON answer for " width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The safety rules didn't trust it. Both pack sizes fit the order's words equally well, so the line went to a human:&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%2Fx6qjudfs4hp6lfkmry79.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%2Fx6qjudfs4hp6lfkmry79.png" alt="Sentry trace: the safety-rules step for the Red Label line returns status needs_review because the 500 g and 250 g packs fit the order equally well" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The model suggests; the code decides.&lt;/strong&gt; The trace also shows every model call going to &lt;code&gt;http://127.0.0.1:11434&lt;/code&gt;: the laptop itself, not a cloud. Across all 320 practice orders: &lt;strong&gt;113 model calls, 640 tool calls, 58K tokens, $0.&lt;/strong&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%2Fx9w39z77e7ylpxkdf451.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%2Fx9w39z77e7ylpxkdf451.png" alt="Sentry Agents view: 113 LLM calls, 640 tool calls, 58K tokens, total cost $0" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tracing lives only in the evaluation tool, runs only on synthetic data, and only when switched on. The shop app itself sends nothing anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI keeps me honest
&lt;/h3&gt;

&lt;p&gt;Every push runs 210 tests on GitHub Actions, checks that the locked test orders haven't changed by a single byte, and posts the no-model baselines to the run page.&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%2Fadqu9dtg87zj7t5pk6g8.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%2Fadqu9dtg87zj7t5pk6g8.png" alt="GitHub Actions run: tests, " width="799" height="374"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Got Wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;My first design asked the model for everything:&lt;/strong&gt; product, quantity and unit. During development it was the worst design I measured, and on the final test it only tied plain fuzzy matching. That's why the model now picks only the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"1,500 bags" became 1 bag, and went green.&lt;/strong&gt; The comma split &lt;code&gt;aashirvaad atta 10 kg 1,500 bag&lt;/code&gt; into two lines. A wrong row, filled in on its own: the one thing the app must never do. Now Indian and Western digit groups (&lt;code&gt;1,500&lt;/code&gt;, &lt;code&gt;1,00,000&lt;/code&gt;) are read as one number, and anything unclear like &lt;code&gt;1,5&lt;/code&gt; gets a question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Remember" could learn the wrong size.&lt;/strong&gt; &lt;em&gt;"tata tea premium"&lt;/em&gt; was amber only because the order didn't say which size. Learning that name would have auto-accepted one size forever. Now only names the catalogue doesn't know can be learned, and a learned name means the product, never one pack size.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model won near-ties for a silly reason.&lt;/strong&gt; &lt;code&gt;lays 10 wala 2 ctn&lt;/code&gt; names no flavour, but the model's pick, Classic Salted, led by 0.074, only because its alias was short. Now a model pick is accepted only if the order's own words put it ahead by at least 0.08, the same margin fuzzy matching needs. That was the last wrong row in development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I tried a bigger model.&lt;/strong&gt; Gemma 4 E4B needs about 6 GB; on 8 GB, only 2.5 GB fit on the GPU and the slowest line took 37 seconds. On a shop laptop, RAM is the limit, not disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I nearly trusted my friends' first replies.&lt;/strong&gt; They were written by ChatGPT, not typed by hand. If I hadn't noticed that all four had the same shape, my "real-world" test would have been one more synthetic test (the full story is under &lt;em&gt;My First User&lt;/em&gt;).&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The orders are the business.&lt;/strong&gt; Customer names, what they buy, at what price: a shop won't paste that into someone else's cloud. With an open-weight model, nothing leaves the laptop. The app only listens on 127.0.0.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs ₹0 per order.&lt;/strong&gt; No API bill growing with every WhatsApp message. That's what makes small-shop pricing possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works when the internet doesn't.&lt;/strong&gt; Market internet is unreliable; this runs fully offline, as the video shows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I could test and choose.&lt;/strong&gt; Because the weights are open and run locally, I could put five models through the same orders and pick the one that fits an 8 GB laptop.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;Honestly, I don't have a paying user yet. No shopkeeper has used ParchiMatch. So I did the next best thing: I asked friends to type a restock order the way they'd really send it to a supplier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first replies were suspiciously perfect.&lt;/strong&gt; Four messages, each exactly 17 lines, every item in the same order as my list, one a light rewording of another. My friends had asked ChatGPT. 😄 I threw them away and asked again, with six items and &lt;em&gt;"please type it yourself."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Three real, messy messages came back, like this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bhai kal subh tk ye dal dena parle g 5wla 2 petti tata nmk 1 bori fotune tel pouch 1l 3 petti amul btar 100g 20 pis dairy milk 20rs 2 dabba or lifeboy 10 sabun 1 ptti bil sth bhej dena
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I labelled the 11 order pieces, locked them, and scored them once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;0 wrong rows filled in&lt;/strong&gt;, and 80% needed no typing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One strict miss:&lt;/strong&gt; &lt;code&gt;fortune oil 1 ltr ke 1 carton&lt;/code&gt; went through without a question, though I had labelled it as worth asking (pouch or bottle?). Its row matched my label exactly, but by my own strict rule it counts, so I report it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The app asked instead of guessing on every hard piece.&lt;/strong&gt; For items joined with &lt;em&gt;aur&lt;/em&gt;, though, its suggestion names only one product, so a busy operator could miss the second.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's 11 pieces from 3 people: a signal, not proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  What It Can't Do (Yet)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Items joined by &lt;em&gt;aur&lt;/em&gt;, or sent with no separator, aren't split.&lt;/strong&gt; The app asks about them, but its suggestion names only one product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Pouch" isn't a unit word yet.&lt;/strong&gt; And &lt;em&gt;Tata namak&lt;/em&gt; fits Tata Tea about as well as Tata Salt, so it asks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested on synthetic shop data plus 11 real pieces&lt;/strong&gt;, not on a real shop's catalogue yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It exports a CSV;&lt;/strong&gt; it doesn't write into Tally or Busy directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tested only on a MacBook Air M2 with 8 GB.&lt;/strong&gt; Windows and Linux are untested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next step is one real shop and one week of real orders.&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 (&lt;code&gt;gemma4:e2b-it-qat&lt;/code&gt; via Ollama) is the model in the app, chosen by testing five models and confirmed once on locked test orders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Sentry Agent Tracing:&lt;/strong&gt; the evaluation tool sends &lt;code&gt;invoke_agent&lt;/code&gt;, &lt;code&gt;chat&lt;/code&gt; and &lt;code&gt;execute_tool&lt;/code&gt; spans for every order line. The traces above show a confidently wrong model answer being caught by the safety rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of GitHub Copilot:&lt;/strong&gt; entered through the GitHub Actions route. CI runs the tests, the locked-test guard and the baselines on every push. &lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you run a shop, or know someone who does: clone it, load your own catalogue as a CSV, and paste in yesterday's WhatsApp orders. It never needs the internet, and it will ask before it guesses.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Prep Buddy: an AI interview coach for my classmate that runs on his laptop with the Wi-Fi off</title>
      <dc:creator>Bipul Chamoli</dc:creator>
      <pubDate>Fri, 02 Oct 2026 22:21:45 +0000</pubDate>
      <link>https://dev.to/bipul_chamoli_09b5f0cc719/prep-buddy-an-ai-interview-coach-for-my-classmate-that-runs-on-his-laptop-with-the-wi-fi-off-426b</link>
      <guid>https://dev.to/bipul_chamoli_09b5f0cc719/prep-buddy-an-ai-interview-coach-for-my-classmate-that-runs-on-his-laptop-with-the-wi-fi-off-426b</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Aman&lt;/strong&gt; is my classmate. We're both in final-year B.Tech, and like everyone in our batch he's preparing for campus placements: SDE-1 roles, which means technical rounds on DSA, DBMS, Operating Systems, Networks and OOP, plus an HR round.&lt;/p&gt;

&lt;p&gt;When I asked him how his prep was going, he wasn't sure. He was putting in the hours, but he couldn't tell which topics were actually weak. Nobody was asking him questions and telling him what he'd missed, so he kept revising the topics he was already comfortable with.&lt;/p&gt;

&lt;p&gt;The usual answers didn't fit him either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;He practises late, around 11 PM&lt;/strong&gt;, when nobody is free to give him a mock interview.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The hostel Wi-Fi is unreliable&lt;/strong&gt;, and paid mock-interview platforms need a stable connection (and money).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interview answers are personal.&lt;/strong&gt; A "tell me about yourself" answer shouldn't have to go to someone else's server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;strong&gt;Prep Buddy&lt;/strong&gt;: a mock-interview coach that runs &lt;strong&gt;entirely on his laptop&lt;/strong&gt;. It asks him a placement question, grades his typed answer against the points a real interviewer listens for, tells him what he missed, and &lt;strong&gt;remembers his weak topics&lt;/strong&gt; so the next question goes after them.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;₹0 to run · no API keys · no account · works with Wi-Fi off · his answers never leave his laptop&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A practice session works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prep Buddy asks a question from a bank of &lt;strong&gt;56 questions across 7 topics&lt;/strong&gt; (DSA, CS fundamentals, DBMS, OS, CN, OOP, HR), reworded the way a friendly interviewer would ask it.&lt;/li&gt;
&lt;li&gt;Aman types an answer and gets a &lt;strong&gt;score out of 10&lt;/strong&gt;, what went well, what was missing, an outline of a strong answer and the follow-up question an interviewer would likely ask next.&lt;/li&gt;
&lt;li&gt;If he missed something, the &lt;strong&gt;next question is picked to target that gap&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;At the end, he gets a summary with his strongest topic, the one to work on next and &lt;strong&gt;3 concrete next steps&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After a few sessions, the dashboard answers the question he couldn't: &lt;em&gt;where should I spend my next ten minutes?&lt;/em&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%2F2jv6b1n5xcqygvme614i.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%2F2jv6b1n5xcqygvme614i.png" alt="Prep Buddy dashboard showing average score, per-topic progress and a suggested topic to practise next" width="800" height="775"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The Wi-Fi is &lt;strong&gt;off&lt;/strong&gt; in this recording. Every question, grade and summary comes from Gemma running locally on my Apple M2 laptop with 8 GB of RAM.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/TgRr7m_U_FE" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;There's no hosted link, on purpose. Hosting Prep Buddy on a server would break the one promise it makes: your answers stay on your laptop. The repo below runs on any 8 GB+ machine.&lt;/p&gt;

&lt;p&gt;The badge in the top-right corner says &lt;strong&gt;AI ready&lt;/strong&gt; only when the database is up, Ollama is running and both models are downloaded. If something is missing, it shows the exact command to fix it (for example &lt;code&gt;ollama pull gemma3:4b&lt;/code&gt;) instead of an error page.&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%2Fef2dkz7fyresnuyvk9hm.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%2Fef2dkz7fyresnuyvk9hm.png" alt="End-of-session summary: a 9.1 out of 10 score, DBMS as the strongest topic, Operating Systems to work on next, and three next steps" width="799" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;Machine: Apple M2, 8 GB&lt;br&gt;&lt;br&gt;
Chat model: Gemma 3 4B&lt;br&gt;&lt;br&gt;
Question bank: 56 questions across 7 topics&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Cold&lt;/th&gt;
&lt;th&gt;Warm average&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Next question&lt;/td&gt;
&lt;td&gt;41.15 s&lt;/td&gt;
&lt;td&gt;20.37 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grade answer&lt;/td&gt;
&lt;td&gt;25.50 s&lt;/td&gt;
&lt;td&gt;14.81 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session summary&lt;/td&gt;
&lt;td&gt;23.81 s&lt;/td&gt;
&lt;td&gt;15.08 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the golden-answer test, all 5 questions passed the required&lt;br&gt;
Excellent &amp;gt; Average &amp;gt; Wrong ordering.&lt;/p&gt;

&lt;p&gt;The prompt-injection test scored 2/10 and was classified as weak.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code
&lt;/h2&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/bipul724" rel="noopener noreferrer"&gt;
        bipul724
      &lt;/a&gt; / &lt;a href="https://github.com/bipul724/prep-buddy" rel="noopener noreferrer"&gt;
        prep-buddy
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;Prep Buddy&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;An offline, private mock-interview coach for campus placements, powered by Google Gemma running on your own laptop.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Built for Aman for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01" rel="nofollow"&gt;DEV Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;₹0 to run · no API keys · works with Wi-Fi off · your answers never leave your laptop&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;▶️ &lt;strong&gt;Demo video&lt;/strong&gt;: &lt;a href="https://youtu.be/TgRr7m_U_FE" rel="nofollow noopener noreferrer"&gt;youtu.be/TgRr7m_U_FE&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/bipul724/prep-buddy/docs/screenshots/landing.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbipul724%2Fprep-buddy%2FHEAD%2Fdocs%2Fscreenshots%2Flanding.png" alt="Prep Buddy landing page"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What it does&lt;/h2&gt;
&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Asks you placement interview questions from a hand-written bank of &lt;strong&gt;56 questions across 7 topics&lt;/strong&gt; (DSA, CS fundamentals, DBMS, OS, CN, OOP, HR), in a friendly interviewer tone.&lt;/li&gt;
&lt;li&gt;Grades your typed answer: a score out of 10, strengths, gaps, an outline of a strong answer and a follow-up question.&lt;/li&gt;
&lt;li&gt;Remembers your &lt;strong&gt;weak topics&lt;/strong&gt; and picks the next question to target them (EmbeddingGemma + pgvector).&lt;/li&gt;
&lt;li&gt;Ends each session with a short summary and &lt;strong&gt;3 concrete next steps&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dashboard&lt;/th&gt;
&lt;th&gt;Session summary&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/bipul724/prep-buddy/docs/screenshots/dashboard.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbipul724%2Fprep-buddy%2FHEAD%2Fdocs%2Fscreenshots%2Fdashboard.png" alt="Dashboard with average score, topic progress and recent sessions"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;a rel="noopener noreferrer" href="https://github.com/bipul724/prep-buddy/docs/screenshots/summary.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fbipul724%2Fprep-buddy%2FHEAD%2Fdocs%2Fscreenshots%2Fsummary.png" alt="Session summary with score, strongest and weakest topic and next steps"&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why local, open-source AI&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Every model call goes to &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt;…&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/bipul724/prep-buddy" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;The setup is three steps: start Ollama and pull the two models, run &lt;code&gt;docker compose up&lt;/code&gt; for the database, then &lt;code&gt;npm run dev&lt;/code&gt;. The README has the full commands. The repo also has the PRD, the architecture, the API reference, the AI design notes and the test results in &lt;code&gt;docs/&lt;/code&gt;. The code is MIT-licensed.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The open-source AI at the core:&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;Piece&lt;/th&gt;
&lt;th&gt;What it does in Prep Buddy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Gemma 3 4B&lt;/strong&gt; (open-weight, 3.3 GB) via &lt;strong&gt;Ollama&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Rewords questions, grades answers, writes the session summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;EmbeddingGemma&lt;/strong&gt; (622 MB, 768-dim vectors) via &lt;strong&gt;Ollama&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;Turns questions and "what you missed" into vectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Mastra&lt;/strong&gt; (open-source TypeScript agent framework)&lt;/td&gt;
&lt;td&gt;Three agents: &lt;strong&gt;interviewer&lt;/strong&gt;, &lt;strong&gt;evaluator&lt;/strong&gt;, &lt;strong&gt;coach&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;PostgreSQL + pgvector&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stores progress &lt;em&gt;and&lt;/em&gt; does the "find a question about this gap" search&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The rest of the stack is Next.js 16, React 19, TypeScript, Tailwind, Prisma 7 and Zod.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      Browser (localhost:3000)
        | fetch JSON
        v
 Next.js API routes --&amp;gt; services: pick next question, grade, summarise
                              |                         |
                     Mastra agents                ollama JS client
          (interviewer, evaluator, coach)          (embeddings)
                              |                         |
                              v                         v
                  Ollama on localhost:11434: gemma3:4b + embeddinggemma

 PostgreSQL 17 + pgvector (Docker, 127.0.0.1): profiles, attempts, topic stats, question vectors
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Every arrow stays on the laptop. There is no cloud service in this diagram.&lt;/p&gt;

&lt;p&gt;Here are the decisions that made a &lt;strong&gt;4B model&lt;/strong&gt; good enough to trust with someone's interview prep.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. One job and one JSON schema per call, with no tool calling
&lt;/h3&gt;

&lt;p&gt;Small models get unreliable when you ask them to do several things at once, and Gemma 3 isn't listed as tool-capable on Ollama. So each agent does exactly one thing and must return JSON that matches a Zod schema. Mastra passes the schema to Ollama as its &lt;code&gt;format&lt;/code&gt; field, so the &lt;strong&gt;decoding itself is constrained&lt;/strong&gt; to valid JSON. Then I validate it again with Zod anyway:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prompt&lt;/span&gt; &lt;span class="p"&gt;}],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;structuredOutput&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;errorStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;strict&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;providerOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;ollama&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;num_predict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;abortSignal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AbortSignal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// never show half-broken JSON to the user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A failed call is retried once, and then the user sees a clear error. If Ollama isn't running, the API returns &lt;code&gt;503 MODEL_UNAVAILABLE&lt;/code&gt; with the command to start it, so the UI never just crashes.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Facts come from the question bank, not the model
&lt;/h3&gt;

&lt;p&gt;The 56 questions were written for this app, not copied from LeetCode or GFG, and each one has 2 to 6 &lt;strong&gt;key points&lt;/strong&gt; (for example, normalization → &lt;em&gt;reduces redundancy, avoids insert/update/delete anomalies, 1NF/2NF/3NF, trade-off: more joins&lt;/em&gt;). The model never invents a question. It only rewords one, and the evaluator grades against those key points as a rubric. That's how a 4B model can grade like an interviewer without making up what the "right" answer is.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Targeting weak spots with EmbeddingGemma and pgvector
&lt;/h3&gt;

&lt;p&gt;When the evaluator returns gaps like &lt;em&gt;"didn't mention update anomalies"&lt;/em&gt;, I embed those gaps and ask Postgres for the closest unasked question in the same topic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&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="n"&gt;embedding&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&amp;gt;&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;gaps&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"Question"&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;alreadyAsked&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, there's a plain difficulty ladder. A topic starts at EASY, moves to MEDIUM once his average is 7 or higher, and to HARD at 8.5. In "Auto" mode, each session starts with his weakest focus topic. The vectors and the progress data live in the same database, so I didn't need a separate vector store.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Not trusting the model with things code can do
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Arithmetic:&lt;/strong&gt; the overall session score is computed in code. The model's number is overwritten.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency:&lt;/strong&gt; Gemma sometimes said "strong" next to a score of 5, so the UI derives the verdict from the score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Double-clicks:&lt;/strong&gt; every submit sends an &lt;code&gt;Idempotency-Key&lt;/code&gt;. The attempt insert and the topic-average update run in one database transaction, so an answer can't be graded twice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every number here was measured on the laptop itself, not on a cloud GPU. To be honest about it: "next question" misses my 15-second target, because rewording costs an extra model call. Setting &lt;code&gt;REPHRASE_QUESTIONS=false&lt;/code&gt; skips it. On a laptop with less RAM, &lt;code&gt;CHAT_MODEL=gemma3:1b&lt;/code&gt; is a one-line switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that went wrong (and what I did about them)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The model got stuck in a loop.&lt;/strong&gt; Sometimes Gemma repeated &lt;code&gt;0} 0} 0}…&lt;/code&gt; until the context filled up, and a request hung for minutes. I tried capping &lt;code&gt;maxOutputTokens&lt;/code&gt;, and it did nothing. Reading the provider's source showed why: that setting is sent as a field Ollama's &lt;code&gt;/api/chat&lt;/code&gt; ignores. Ollama's own &lt;code&gt;num_predict&lt;/code&gt; works, so I used that, plus a 60-second timeout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The interviewer answered its own questions.&lt;/strong&gt; When asked to reword a question, the model would sometimes answer it instead. Now a reworded question is only used if it still ends with &lt;code&gt;?&lt;/code&gt;. Otherwise Prep Buddy shows the original wording.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prompt injection.&lt;/strong&gt; Answers are wrapped in &lt;code&gt;&amp;lt;answer&amp;gt;&lt;/code&gt; tags and treated as data. While tuning, I found that Gemma 3 4B followed injected text &lt;em&gt;more&lt;/em&gt; when my rules came after the answer, so the answer now goes last. I also had to tell it to penalise answers that use the right words but state the facts wrong.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For Aman, it's the difference between a tool he can use and one he can't.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What Aman needs&lt;/th&gt;
&lt;th&gt;With Gemma on his laptop&lt;/th&gt;
&lt;th&gt;With a typical closed AI API&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Practise at 11 PM on hostel Wi-Fi&lt;/td&gt;
&lt;td&gt;Works fully offline once the models are downloaded&lt;/td&gt;
&lt;td&gt;Needs a working connection for every answer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Practise every night as a student&lt;/td&gt;
&lt;td&gt;₹0 per answer, so 50 questions cost nothing&lt;/td&gt;
&lt;td&gt;Paid per token, or a subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Keep his answers private&lt;/td&gt;
&lt;td&gt;Nothing leaves the laptop: no account, no API key&lt;/td&gt;
&lt;td&gt;Every answer goes to a third-party server&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run on &lt;em&gt;his&lt;/em&gt; machine&lt;/td&gt;
&lt;td&gt;Swap &lt;code&gt;gemma3:4b&lt;/code&gt; ↔ &lt;code&gt;gemma3:1b&lt;/code&gt; with one env var&lt;/td&gt;
&lt;td&gt;Whatever model the vendor serves&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;There are two less obvious ways open mattered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I could read the code when things broke.&lt;/strong&gt; Both of my worst bugs (the token loop and the ignored output cap) were solved by reading the source of the Ollama provider and checking what was actually being sent. With a closed API, all I'd have seen was a request that took four minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The privacy promise shaped the product.&lt;/strong&gt; I planned voice input, then dropped it. The browser's speech recognition in Chrome sends audio to a server, and that would have broken the one promise this app makes.&lt;/p&gt;

&lt;p&gt;To be fair to closed models: a large hosted model would give richer feedback, and it would give it faster. But Aman can't use that one in the hostel at 11 PM, for free, without sending his answers to someone else's server. &lt;strong&gt;A 4B model he can use every night beats a better model he won't.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Handing it to Aman
&lt;/h2&gt;

&lt;p&gt;I gave it to Aman and sat with him while he did a practice session. His verdict:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What I liked about Prep Buddy was that after answering the questions, I was provided with instant feedback and it helped me to identify my weak areas. It helped me to know about my strengths and weaknesses prior to the interview."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Finding his weak areas before the interview is the part I built it for. He was already putting in the hours. What he didn't have was someone telling him &lt;em&gt;where&lt;/em&gt; to put them, and now his laptop does that, with the Wi-Fi off.&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 &lt;em&gt;is&lt;/em&gt; the product. Gemma 3 4B runs all three agents (rewording, grading against a rubric, coaching), and EmbeddingGemma powers the weak-spot targeting. Both run locally through Ollama, on an 8 GB laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Mastra&lt;/strong&gt;: the interviewer, evaluator and coach are Mastra agents. Mastra's &lt;code&gt;structuredOutput&lt;/code&gt; with Zod schemas is how every model call returns validated, schema-constrained JSON from a local model.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you're preparing for placements too, clone it, add your own questions to &lt;code&gt;data/questions.json&lt;/code&gt;, and practise without needing Wi-Fi.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
  </channel>
</rss>
