<?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: Alan Finney</title>
    <description>The latest articles on DEV Community by Alan Finney (@3dogs).</description>
    <link>https://dev.to/3dogs</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%2F4177794%2F73e97824-4469-49d5-8b3f-11d9ea7aa4e8.png</url>
      <title>DEV Community: Alan Finney</title>
      <link>https://dev.to/3dogs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/3dogs"/>
    <language>en</language>
    <item>
      <title>Ofrenda: a gentle oral-history keeper built on Gemma 3</title>
      <dc:creator>Alan Finney</dc:creator>
      <pubDate>Thu, 29 Oct 2026 21:07:49 +0000</pubDate>
      <link>https://dev.to/3dogs/ofrenda-a-gentle-oral-history-keeper-built-on-gemma-3-3imd</link>
      <guid>https://dev.to/3dogs/ofrenda-a-gentle-oral-history-keeper-built-on-gemma-3-3imd</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week4-2026-10-26"&gt;Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I built Ofrenda, a gentle oral-history keeper. You type a memory of someone who has died, a grandparent, a friend, a pet. The system interviews you with a few warm follow-up questions, one at a time, each one tied to something you actually said. At the end it gives you an ofrenda card with a title, a short remembrance, three concrete details, and one line in their own words if you shared one.&lt;/p&gt;

&lt;p&gt;The project is named for the ofrenda, the offering placed on a Día de los Muertos altar. I wanted something that would honor the past without inventing it.&lt;/p&gt;

&lt;p&gt;The worked example throughout the demo is a grandmother named Rosa. She made tamales every December in her tiny kitchen in Tucson, sang old rancheras in a blue apron, loved Cielito Lindo, and used to say "a kitchen with no music is a kitchen with no love." The card the system wrote for her is warm and accurate because everything on it came from what was typed.&lt;/p&gt;

&lt;p&gt;Privacy is built into the bones of the thing. The backend never stores, logs, or forwards any memory. Cards are saved only in the visitor's browser with localStorage. There is no account. You visit, you talk, you leave with something you can keep.&lt;/p&gt;

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

&lt;p&gt;Live demo: &lt;a href="https://wtiebbqscexgkzkest37g4ffly0zujci.lambda-url.us-east-2.on.aws/" rel="noopener noreferrer"&gt;https://wtiebbqscexgkzkest37g4ffly0zujci.lambda-url.us-east-2.on.aws/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Video walkthrough: &lt;a href="https://youtu.be/s2KdX96chDQ" rel="noopener noreferrer"&gt;https://youtu.be/s2KdX96chDQ&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The card shown in the video was generated live during recording, nothing was staged.&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/DKJager" rel="noopener noreferrer"&gt;
        DKJager
      &lt;/a&gt; / &lt;a href="https://github.com/DKJager/ofrenda" rel="noopener noreferrer"&gt;
        ofrenda
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Gentle oral-history keeper powered by Gemma 3
    &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;OFRENDA – A Gentle Oral-History Keeper&lt;/h1&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What / Why&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;People cherish memories of loved ones, friends, or even beloved pets that have passed
OFRENDA lets you capture those stories in a respectful "ofrenda card" that can be
saved locally and shared with family. The card contains a short remembrance,
three concrete details drawn from what you told the system, and, when available,
a line quoted from the person themselves. All content is generated by the
open-weight Gemma 3 model, honoring the spirit of Día de los Muertos while
keeping the process fully open-source.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live demo&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a href="https://wtiebbqscexgkzkest37g4ffly0zujci.lambda-url.us-east-2.on.aws/" rel="nofollow noopener noreferrer"&gt;https://wtiebbqscexgkzkest37g4ffly0zujci.lambda-url.us-east-2.on.aws/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Built for the DEV Hacktoberfest 2026 week-4 challenge (Ghost in the Machine) by 3Dogs Nexus. Runs on the open-weight google/gemma-3-27b-it model via an OpenAI-compatible endpoint.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How It Works (High-Level)&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Interview&lt;/strong&gt; – You type a short description of the person
The system asks up to five warm, short follow-up questions, each based on
something you…&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/DKJager/ofrenda" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Repository: &lt;a href="https://github.com/DKJager/ofrenda" rel="noopener noreferrer"&gt;https://github.com/DKJager/ofrenda&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The server is a single AWS Lambda Function URL written in Python 3.12 using only the standard library. It serves the page on GET and handles two JSON routes, one for the interview turns, one for card generation. The client is plain vanilla JavaScript. Nine automated tests run with fake model calls, no API key or network needed. The whole thing was built from an empty repository during the challenge window, first commit October 28th, 2026. Licensed Apache-2.0.&lt;/p&gt;

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

&lt;p&gt;The architecture is deliberately thin. One Lambda function. No frameworks, no databases, no external storage. The model calls go to Gemma 3 (google/gemma-3-27b-it, Google's open-weight model) through the Nebius Token Factory OpenAI-compatible API. The function URL does everything else.&lt;/p&gt;

&lt;p&gt;The core mechanic is a grounding rule. When the card is generated, every detail must share at least two content words with what the user typed, stopwords removed. If a detail fails that check, it gets dropped. The prompt also instructs the model to use only facts the user gave and not to invent names, places, or events. This is the guardrail that keeps the ofrenda honest.&lt;/p&gt;

&lt;p&gt;The interview flow works like this. You type a memory. Gemma reads the conversation history and returns one short follow-up question based on something you mentioned. It does this up to five times, then writes the card. The history is capped at twelve turns, fifteen hundred characters each, to keep things within the Lambda timeout.&lt;/p&gt;

&lt;p&gt;I learned some practical lessons along the way. The first working version was a stub that never called the model. It always answered "Tell me more about that," and the tests passed anyway because the tests were stubs too. I had to rewrite the tests to require actual model calls before they would count as passing.&lt;/p&gt;

&lt;p&gt;The first grounding check used plain word overlap and let words like "the" and "and" count as shared words. An invented detail slipped through. I fixed it by removing stopwords before counting.&lt;/p&gt;

&lt;p&gt;Gemma rejected a conversation that began with an assistant turn, returning HTTP 400. Now the history is normalized to always start with the user.&lt;/p&gt;

&lt;p&gt;Long card outputs made the function hang near the Lambda timeout. I capped the card call at 350 tokens and set the client timeout to eighty seconds. That solved it.&lt;/p&gt;

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

&lt;p&gt;This project would not have the same meaning with a closed model. Gemma is named, pinned, and swappable. Any OpenAI-compatible host can serve it, and it can run locally. The remembrance prompt and grounding check are public in the repository, so anyone can audit exactly what the card may and may not contain. The memories never have to leave for a vendor's logs. A closed API simply could not give a family that guarantee.&lt;/p&gt;

&lt;p&gt;When you are asking someone to share a memory of their grandmother, the trust has to be real. Open-weight models and open-source code make that trust verifiable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Spirit of It
&lt;/h2&gt;

&lt;p&gt;This challenge asked us to honor the past in the spirit of Día de los Muertos, and I took that literally. An ofrenda is not a performance. It is a small, careful act of remembrance. I wanted the software to feel the same way. The interview is patient. The card stays close to what you said. Nothing is embellished for effect. The model listens more than it speaks.&lt;/p&gt;

&lt;p&gt;Rosa's card holds a blue apron, a December kitchen, one song, and one sentence she used to say. Those details matter because they are hers, not because they are dramatic. Ofrenda tries to leave room for that kind of quiet specificity. It does not try to summarize a life. It just holds a few things steady for a moment.&lt;/p&gt;

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

&lt;p&gt;Best Use of Gemma. Gemma 3 powers both the conversational interview and the card generation, and the whole system is built around its ability to ask grounded, non-inventive questions and produce a remembrance that stays faithful to what the user actually said.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>My Benchmark's First Leaderboard Measured My Grader, Not the Models</title>
      <dc:creator>Alan Finney</dc:creator>
      <pubDate>Sun, 11 Oct 2026 20:48:33 +0000</pubDate>
      <link>https://dev.to/3dogs/my-benchmarks-first-leaderboard-measured-my-grader-not-the-models-405d</link>
      <guid>https://dev.to/3dogs/my-benchmarks-first-leaderboard-measured-my-grader-not-the-models-405d</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;Scores and costs in this post are from my Kaggle benchmark page as of 11:10 AM PT on October 11, 2026.&lt;/p&gt;

&lt;p&gt;Numeric grounding: can a model answer using only the numbers a short context states, and abstain when the number is missing? In a financial or operations summary, a made-up number is worse than "I can't tell". Every company, figure and unit in the benchmark is invented ("Vendo Bin A", "brens per lune"), so no model can answer from memory.&lt;/p&gt;

&lt;p&gt;I treated two things as suspects, not just the models. The first was the grader: I replayed raw replies before believing any score. The second was the answer key: every key in the hardest task had to survive a blind vote by three models that are not being scored.&lt;/p&gt;

&lt;p&gt;The benchmark has two layers. Each row is scored 0 or 1 on the final ANSWER line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity gate (five tasks, 6 to 9 rows each).&lt;/strong&gt; Each one isolates a single classic grounding failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lookup: read a stated value&lt;/li&gt;
&lt;li&gt;abstain: say CANNOT BE DETERMINED when the value is not stated&lt;/li&gt;
&lt;li&gt;distractor: pick the right value out of several similar ones&lt;/li&gt;
&lt;li&gt;multistep: combine stated values with arithmetic&lt;/li&gt;
&lt;li&gt;units: convert using a made-up unit defined in the context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stress test: ng-hard (10 rows).&lt;/strong&gt; Each row stacks several traps in one paragraph:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a figure corrected twice, where only the last correction counts&lt;/li&gt;
&lt;li&gt;a correction that applies only to a neighbouring entity or year&lt;/li&gt;
&lt;li&gt;near-miss missing data ("fiscal 2024" when the question asks about calendar 2024)&lt;/li&gt;
&lt;li&gt;two-step unit chains with three or more distractor numbers&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;Gemini 3.7 Flash, GLM-5, Qwen 3 235B A22B Instruct and gpt-oss-120b, all run on Kaggle. They come from four different model makers (Google, Z.ai, Alibaba and OpenAI), so a parsing quirk tied to one model family would show up as a gap between families. They also span a wide price range, which mattered more than I expected (Finding 4).&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. The first leaderboard measured my grader, not the models.&lt;/strong&gt; The first run produced a clear spread: 0.98, 0.94, 0.78 and 0.62. Before publishing, I replayed the actual replies, and most of the "wrong" answers were right. The grader only accepted a line that started with &lt;code&gt;ANSWER:&lt;/code&gt;, so it scored these correct replies as 0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;27 ANSWER: 27&lt;/code&gt; (number and marker on one line): Qwen scored 0% on distractor because of this&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;**ANSWER: 72**&lt;/code&gt; (markdown bold): gpt-oss scored 16.7% on multistep&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;\text{ANSWER: } 135&lt;/code&gt; (LaTeX)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One abstain row was also keyed wrongly. The context said "No side reactions occurred" and the question asked how much side product formed. Zero is a correct answer, but my key required "cannot be determined".&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. With a fair grader, every model passed the sanity gate.&lt;/strong&gt; The fixed grader reads the last ANSWER marker anywhere in the reply. For a number, it takes the first number after that marker. For an abstention, it needs the full phrase CANNOT BE DETERMINED. A wrong number, a missing answer, or abstaining when a value is stated still scores 0. Before the grader went live, I checked it against a table of 21 reply formats modelled on the replies I saw, including 8 that must still score 0.&lt;/p&gt;

&lt;p&gt;Parsing and that one bad key together had moved a model's score on the five core tasks by as much as 0.38 (gpt-oss: 0.62 to 1.00). All four models then scored 100% on all five gate tasks. So on short, clean contexts the gate is a pass/fail floor, not a ranking: a model you trust with numbers should clear it, and all four did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. The stress test is where models start to differ.&lt;/strong&gt; Before running ng-hard, I had all 12 draft answer keys checked blind by three models that are not in the benchmark. I kept a row only if at least two of them agreed with its key, so 2 rows were dropped. On one dropped row, all three checkers answered 6.40 for "North Ridge" when only "North Ridge Annex" was stated. That near-miss fooled strong models, but a row I cannot defend is a coin flip with extra steps.&lt;/p&gt;

&lt;p&gt;Gemini 3.7 Flash, GLM-5 and Qwen 3 235B scored 100% on ng-hard. gpt-oss-120b scored 80% (8 of 10 rows), and its two misses were different kinds of failure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An arithmetic slip.&lt;/strong&gt; The context gave a rate that was corrected twice, ending at 11.60 brens per lune, plus a run of 3.0 lunes and a dock amount of 1.20 brens. The key is 11.60 x 3.0 + 1.20 = 36.00. gpt-oss replied &lt;code&gt;ANSWER: 4.20&lt;/code&gt;, which is exactly 3.0 + 1.20: the run length plus the dock amount, with the rate unused.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An incomplete abstention.&lt;/strong&gt; The question asked for actual 2025 capacity, but the context only stated planned 2025 and actual 2024 figures. gpt-oss's entire visible reply was &lt;code&gt;ANSWER: CANNOT BE&lt;/code&gt;. That was the right call, ending one word short of the required phrase. Kaggle's run log shows 164 output tokens for that reply. The full-phrase rule was fixed before this run, so the row scored 0, but it could fairly be argued either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So gpt-oss's overall 0.97, against 1.00 for the other three, rests on one clear error and one incomplete answer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Same score, very different bill.&lt;/strong&gt; Here is what Kaggle's Score vs. Total Cost chart shows for the full six-task run:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Qwen 3 235B: 100 for $0.006&lt;/li&gt;
&lt;li&gt;Gemini 3.7 Flash: 100 for $0.07&lt;/li&gt;
&lt;li&gt;GLM-5: 100 for $0.19&lt;/li&gt;
&lt;li&gt;gpt-oss-120b: 96.67 for $0.005&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Among the three perfect scorers, GLM-5 cost about 30 times as much as Qwen. Once accuracy ties, cost per flawless run is the useful tiebreaker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I would measure next:&lt;/strong&gt; the same traps inside long documents and tables, and repeated runs per model to see whether gpt-oss's incomplete abstention happens again or was a one-off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Indicative only:&lt;/strong&gt; each task has 6 to 10 rows, so a single row is worth 10 to 17 points, and each model ran once.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I take from it:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Replay raw replies before you trust a leaderboard. Most of the spread I first measured came from parsing, not from the models.&lt;/li&gt;
&lt;li&gt;Validate answer keys independently. A blind vote by models you are not scoring caught keys I could not defend.&lt;/li&gt;
&lt;li&gt;On short, clean contexts, single-trap tests are now a floor for these four models. Stacked traps are where differences start.&lt;/li&gt;
&lt;li&gt;When accuracy ties, cost decides. Among the three perfect scorers it varied about 30-fold.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.kaggle.com/benchmarks/alanfinney/numeric-grounding" rel="noopener noreferrer"&gt;https://www.kaggle.com/benchmarks/alanfinney/numeric-grounding&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
  </channel>
</rss>
