<?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: Dariusz Dumnicki</title>
    <description>The latest articles on DEV Community by Dariusz Dumnicki (@dumnicki).</description>
    <link>https://dev.to/dumnicki</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%2F4155891%2F97f9ef02-2eec-41cc-9013-453cc16dc301.jpg</url>
      <title>DEV Community: Dariusz Dumnicki</title>
      <link>https://dev.to/dumnicki</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dumnicki"/>
    <language>en</language>
    <item>
      <title>Weekend Micro-Adventure</title>
      <dc:creator>Dariusz Dumnicki</dc:creator>
      <pubDate>Sun, 11 Oct 2026 21:37:03 +0000</pubDate>
      <link>https://dev.to/dumnicki/weekend-micro-adventure-3bee</link>
      <guid>https://dev.to/dumnicki/weekend-micro-adventure-3bee</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge: Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Weekend Micro-Adventure&lt;/strong&gt; is a one-screen web app that gets you off the screen.&lt;/p&gt;

&lt;p&gt;You pick three things — how long you have, how you'll get there, and what mood you're in — type a city, and press &lt;strong&gt;Send me out&lt;/strong&gt;. You get exactly one card: one real place nearby, a short plan, and a leave-by time. There is one button underneath, &lt;strong&gt;Not feeling it&lt;/strong&gt;, which gives you a different place exactly once. Then it's gone.&lt;/p&gt;

&lt;p&gt;There is no feed, no list of options, no login, no settings. The design constraint I set myself was that the screen must be the shortest part of the experience, so I kept a timer in the footer that shows how long you've been staring at it. The intended caption for the demo is &lt;em&gt;"Screen time: 10s. Outside time: 45+ min."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It's live: &lt;a href="https://weekend-micro-adventure.onrender.com/" rel="noopener noreferrer"&gt;weekend-micro-adventure.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The reason a language model is genuinely useful here isn't "it writes text." It's three specific jobs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pick the best place from a shortlist for your mood and constraints.&lt;/li&gt;
&lt;li&gt;Turn raw search results into something readable in under ten seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest.&lt;/strong&gt; If a detail isn't in the data, say "check before you go" instead of making it up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That third one was the hard part, and most of this write-up is about how I made it hold.&lt;/p&gt;

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

&lt;p&gt;Live app deployed on Render:&lt;br&gt;
&lt;a href="https://weekend-micro-adventure.onrender.com/" rel="noopener noreferrer"&gt;weekend-micro-adventure.onrender.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One screen, three choices, one card.&lt;/p&gt;

&lt;p&gt;Type the city, address or share your location, choose how much time you can spend on your adventure, how you want to get there and the vibe you want.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbb4mir6lqhjxcf1q97bb.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%2Fbb4mir6lqhjxcf1q97bb.png" alt="Select options" width="800" height="786"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click on "Send me out" and Gemma will prepare a plan for your adventure, taking into account the current weather, time and the which places are open. You can always re-roll for a different plan using "not feeling it" button.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq950wns4wan55ukr7c8e.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%2Fq950wns4wan55ukr7c8e.png" alt="Adventure plan" width="800" height="744"&gt;&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/dumnicki" rel="noopener noreferrer"&gt;
        dumnicki
      &lt;/a&gt; / &lt;a href="https://github.com/dumnicki/weekend-micro-adventure" rel="noopener noreferrer"&gt;
        weekend-micro-adventure
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Weekend Micro-Adventure - Discover new places around you and plan your adventure
    &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;Weekend Micro-Adventure&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;One screen. Pick time, transport, vibe, location. Press &lt;strong&gt;Send me out&lt;/strong&gt;. Get one real
adventure card — one place, a short plan, and a leave-by time. No lists, no feed, no login.&lt;/p&gt;
&lt;p&gt;Gemma (open-weights) runs through &lt;strong&gt;Backboard&lt;/strong&gt;. Places come from SerpApi
Weather and daylight from Open-Meteo (free, no key).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install -r requirements.txt
cp .env.example .env      &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; then fill in the keys&lt;/span&gt;
uvicorn app.main:app --reload&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Open &lt;a href="http://127.0.0.1:8000" rel="nofollow noopener noreferrer"&gt;http://127.0.0.1:8000&lt;/a&gt; . Health check: &lt;code&gt;GET /api/health&lt;/code&gt; -&amp;gt; &lt;code&gt;{"status":"ok"}&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run tests&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pytest -q&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Tests never call a live API — they use saved fixtures and mocks.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Current model&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;BACKBOARD_MODEL=google/gemma-4-26b-a4b-it
BACKBOARD_PROVIDER=openrouter
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;scripts/list_models.py&lt;/code&gt; lists every Backboard model matching &lt;code&gt;gemma&lt;/code&gt; (there are ~1000,
mostly community fine-tunes on the &lt;code&gt;featherless&lt;/code&gt; provider). Pick the &lt;code&gt;openrouter&lt;/code&gt; entry
for the Google-namespaced model: 262k context, versus 32k on the &lt;code&gt;featherless&lt;/code&gt; duplicate.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Typical model latency&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;A one-shot &lt;code&gt;generate_json&lt;/code&gt; call against Gemma returns in about &lt;strong&gt;1.5–2s&lt;/strong&gt;. That…&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/dumnicki/weekend-micro-adventure" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 4 26B-A4B&lt;/a&gt; (open weights) served through &lt;strong&gt;Backboard&lt;/strong&gt;, &lt;a href="https://serpapi.com/google-maps-api" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt; for real places with coordinates and ratings, &lt;a href="https://open-meteo.com/" rel="noopener noreferrer"&gt;Open-Meteo&lt;/a&gt; for weather and sunrise/sunset (free, no API key), FastAPI, and one &lt;code&gt;index.html&lt;/code&gt; with plain JavaScript. No front-end framework, no build step, no database.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one architectural decision that mattered
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Code decides, the model writes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every decision that can be made from a number is made in Python: distance filtering, the time math, the daylight cutoff, scoring, deduplication, even the radius per transport. The model does the only two things code is bad at — picking one item from eight, and writing it in a human voice.&lt;/p&gt;

&lt;p&gt;This is not a stylistic preference. It meant that when the model failed — and it did, repeatedly — the failure degraded to a plain template card instead of an error page. &lt;strong&gt;The app has never once returned a 500.&lt;/strong&gt; Model timeout, malformed JSON, Backboard down: all of it falls back to a code-built card.&lt;/p&gt;

&lt;p&gt;It also made the model swappable. &lt;code&gt;BACKBOARD_MODEL&lt;/code&gt; is an environment variable. Gemma does the least critical job in the pipeline, so swapping it out doesn't break the safety properties — it just changes the prose.&lt;/p&gt;

&lt;h3&gt;
  
  
  Controlling hallucination
&lt;/h3&gt;

&lt;p&gt;The risk is a made-up trail or a made-up opening time, and a confident lie about where to walk is worse than no answer. Five layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The model only ever sees 8 pre-filtered candidates and must pick by ID.&lt;/li&gt;
&lt;li&gt;The system prompt forbids anything not present in the candidate data.&lt;/li&gt;
&lt;li&gt;The server validates the output — this is the part I got wrong twice.&lt;/li&gt;
&lt;li&gt;Every card carries a &lt;code&gt;check_before_you_go&lt;/code&gt; field.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The place name, address, coordinates and map link come from code, never from the model.&lt;/strong&gt; A hallucinated title cannot move you to the wrong park, because the model isn't allowed to supply the coordinates.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is the same territory people are actively writing about — &lt;a href="https://dev.to/veaceslav/stop-prompting-for-valid-json-an-llm-output-layer-that-does-not-break-5264"&gt;Stop prompting for valid JSON: an LLM output layer that does not break&lt;/a&gt; and &lt;a href="https://dev.to/jeff_pdc/llm-structured-outputs-and-json-schema-tool-calling-that-never-drifts-edl"&gt;LLM Structured Outputs and JSON Schema: Tool Calling That Never Drifts&lt;/a&gt; — and my experience matched theirs: schema validation is table stakes, but schema validation does not catch a &lt;em&gt;wrong number inside a valid shape&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug that taught me the most
&lt;/h3&gt;

&lt;p&gt;My first prompt run scored &lt;strong&gt;7 out of 10&lt;/strong&gt;. Both failures were the same mistake, and it was a genuinely interesting one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The model kept setting &lt;code&gt;duration_minutes&lt;/code&gt; to the &lt;strong&gt;total time budget&lt;/strong&gt;, ignoring the shorter &lt;code&gt;activity_minutes_available&lt;/code&gt; on the specific place it had chosen.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It read the global budget (60 minutes) instead of the per-candidate allowance (30 minutes). A perfectly well-formed JSON object with a confidently wrong number in it — which no schema catches.&lt;/p&gt;

&lt;p&gt;The obvious fix doesn't work, because the rule was correct in the first draft. I had already written &lt;em&gt;"duration_minutes must not exceed activity_minutes_available"&lt;/em&gt;, and the model ignored it. Burying a hard constraint in a list of eight soft rules doesn't make it a hard constraint.&lt;/p&gt;

&lt;p&gt;What worked was making it &lt;strong&gt;structural instead of advisory&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;max_duration_minutes&lt;/code&gt; copy-target field sitting next to each candidate.&lt;/li&gt;
&lt;li&gt;A worked example in the system prompt showing the wrong answer alongside the right one, including the exact wrong numbers.&lt;/li&gt;
&lt;li&gt;Restating the rule in the user template &lt;em&gt;immediately below&lt;/em&gt; the candidate JSON, ending with the instruction: &lt;em&gt;"Do not use {budget_minutes}."&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next run: &lt;strong&gt;10 out of 10.&lt;/strong&gt; The lesson generalizes past this app — if a small open-weight model won't follow a constraint stated in prose, stop stating it in prose and change the shape of the data it's reading.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug my tests could never have caught
&lt;/h3&gt;

&lt;p&gt;Later, a live smoke test returned a card where &lt;code&gt;check_before_you_go&lt;/code&gt; was the literal string &lt;code&gt;"None"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That passed every single validation rule I'd written. It's a non-empty string in a string field. My entire suite — 244 tests — was green while this bug existed, because every test used a mocked model that returns well-formed text.&lt;/p&gt;

&lt;p&gt;Tests verify &lt;em&gt;structure&lt;/em&gt;. Only a real call shows you what the model actually emits. I now treat bare null markers (&lt;code&gt;None&lt;/code&gt;, &lt;code&gt;N/A&lt;/code&gt;, &lt;code&gt;null&lt;/code&gt;) as empty fields, which triggers the retry path. And I added &lt;code&gt;used_fallback&lt;/code&gt; to the API response specifically so I can assert that Gemma wrote the card rather than the template — the smoke test now fails the deploy if &lt;code&gt;used_fallback&lt;/code&gt; is ever true.&lt;/p&gt;

&lt;p&gt;That field exists for one reason: the whole claim of this project is that a model did the work, and I wanted that claim to be checkable rather than assumed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Smaller things worth knowing
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A per-request cache was spending money on every re-roll.&lt;/strong&gt; The re-roll button existed specifically to avoid re-querying SerpApi, but the cache was constructed inside the request handler, so it was always empty. One line moved it to process scope.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daylight is planned against the destination's clock, not the server's&lt;/strong&gt;, using the forecast's UTC offset. Getting this wrong silently schedules the trip against the wrong sunset.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SerpApi's docs were wrong twice.&lt;/strong&gt; They documented a &lt;code&gt;type_ids&lt;/code&gt; field that doesn't exist in the real response, and omitted &lt;code&gt;description&lt;/code&gt; from their sample when it actually appears on ~20% of results. Both only showed up by checking a saved real response.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;244 tests, none of which touch the network.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The open-weight model is doing the one job where a closed API would have been most tempting, and it turned out to be the &lt;em&gt;least&lt;/em&gt; critical job in the pipeline.&lt;/p&gt;

&lt;p&gt;Gemma 4 26B-A4B is a mixture-of-experts model — 26B total parameters but only ~4B active — which is why a full request lands in about two seconds and costs almost nothing. That's not a benchmark win; it's what made a 250-search/month free tier and a free hosting tier enough to actually ship and demo this.&lt;/p&gt;

&lt;p&gt;More importantly, &lt;strong&gt;the open part is the part I was able to inspect and constrain.&lt;/strong&gt; I could read the model's behavior across ten identical runs, find the precise number it kept getting wrong, and fix it by reshaping the input. With a closed API, the same bug would have been "tune the prompt and hope." Because the weights are swappable and the model is one module behind an interface (&lt;code&gt;app/services/llm.py&lt;/code&gt; is the only file that knows Backboard exists), I can run Gemma today and something else tomorrow without touching the validation, the filtering, or the front end.&lt;/p&gt;

&lt;p&gt;And the architecture means openness is load-bearing rather than decorative. Because &lt;strong&gt;code&lt;/strong&gt; computes the distances, the times, the daylight cutoff and the map link, the app's correctness does not depend on the model's good behavior. If the model is down, un-swappable, or replaced with something worse, you still get a correct adventure — just in plainer language.&lt;/p&gt;

&lt;p&gt;That's the argument I'd make for open models in general: the value isn't only that the weights are free. It's that being able to look closely at a component, and swap it without permission, is what makes the rest of the system trustworthy.&lt;/p&gt;

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

&lt;p&gt;The full build transcript, including the failures and two misdiagnoses I made along the way:&lt;/p&gt;


&lt;div class="ltag-agent-session"&gt;
  &lt;div class="agent-session-header"&gt;
    
    &lt;span class="agent-session-tool-icon-badge" title="OpenCode"&gt;
&lt;/span&gt;
    &lt;span class="agent-session-title"&gt;Weekend Micro-Adventure: three bugs a mocked test suite could not catch&lt;/span&gt;
  &lt;/div&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Check this project. Look at PLAN.md. We are currently on step 1. You can use google/gemma-4-26b-a4b-it.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Nearly every file in the repo was an empty 1-line stub, so step 1 was accurate. Only config.py, services/llm.py and scripts/list_models.py had real content.&lt;/p&gt;

&lt;p&gt;Step 1's job was to find the exact model id rather than trust the plan's guess, so I ran list_models.py against the live API. It returned 1034 Gemma matches, almost all community fine-tunes on the featherless provider. The model the user named existed twice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;provider&lt;/th&gt;
&lt;th&gt;model&lt;/th&gt;
&lt;th&gt;context&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;openrouter&lt;/td&gt;
&lt;td&gt;google/gemma-4-26b-a4b-it&lt;/td&gt;
&lt;td&gt;262,144&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;featherless&lt;/td&gt;
&lt;td&gt;google/gemma-4-26B-A4B-it&lt;/td&gt;
&lt;td&gt;32,768&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;I set BACKBOARD_PROVIDER=openrouter because the featherless duplicate of the same model caps at 32k, and the planner will eventually push full candidate JSON through it.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
              &lt;div class="agent-session-tool-call"&gt;
                
                  &lt;span class="agent-session-chevron"&gt;▸&lt;/span&gt;
                  &lt;span class="tool-name tool-name-shell"&gt;shell&lt;/span&gt;
                    &lt;span class="tool-input-preview"&gt;python scripts/list_models.py&lt;/span&gt;
                
                &lt;div class="agent-session-tool-detail"&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Input&lt;/div&gt;
                      &lt;pre class="agent-session-pre agent-session-terminal"&gt;&lt;code&gt;$ python scripts/list_models.py&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                    &lt;div class="tool-section"&gt;
                      &lt;div class="tool-section-label"&gt;Output&lt;/div&gt;
                      &lt;pre class="agent-session-pre"&gt;&lt;code&gt;Found 1034 Gemma model(s):
  provider=featherless  model=google/gemma-4-26B-A4B-it  context=32768
  provider=openrouter  model=google/gemma-4-26b-a4b-it  context=262144&lt;/code&gt;&lt;/pre&gt;
                    &lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Step 1 check passed: a real call to generate_json returned a parsed dict in 1.65s.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Run the model call just 10 times. Let me know of the results.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;7 out of 10 passed, and honestly only 9 model calls happened: one configured run short-circuited before reaching the model because no place survived filtering.&lt;/p&gt;

&lt;p&gt;Both failures were the same bug, and it was a genuinely interesting one. On tight budgets the model set duration_minutes to the total budget_minutes, ignoring the shorter activity_minutes_available on the candidate it had actually chosen:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;run&lt;/th&gt;
&lt;th&gt;constraints&lt;/th&gt;
&lt;th&gt;duration_minutes&lt;/th&gt;
&lt;th&gt;activity available&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;1h / bike&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;1h / transit&lt;/td&gt;
&lt;td&gt;60&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A perfectly well-formed JSON object with a confidently wrong number inside it. All six half_day and full_day runs passed, because on longer budgets the cap never binds and the model never has to reason about it.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Proceed with the proposed fix and test with 10 calls.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Three coordinated changes, because the bug was the model conflating two numbers.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Promoted rule 5 from one advisory line buried among eight rules into a standalone HARD RULE block that names the confusion directly: budget_minutes is the whole trip including travel, activity_minutes_available is time at the place only, and the former is always bigger. Included a worked example listing the exact wrong numbers.&lt;/li&gt;
&lt;li&gt;Restated the rule in the user template immediately below the candidate JSON, ending with the instruction to not use budget_minutes.&lt;/li&gt;
&lt;li&gt;Added a max_duration_minutes string field next to each candidate, giving the model a literal copy target. Small models copy more reliably than they re-derive.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I also fixed the fixture. The nearest fake place was 1.82 km away, which at 4.5 km/h meant a 63-minute round trip inside a 60-minute budget, so the 1h/foot run correctly returned nothing and never tested the model. Added two places within walking distance.&lt;/p&gt;

&lt;p&gt;Result: 10 out of 10, with all three 1h runs passing, which are precisely the cases that failed before.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Let's proceed with step 7&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;Six planner tests failed on the first run. The planner was correct every time; my test data was wrong.&lt;/p&gt;

&lt;p&gt;At 16:20 local, daylight capped the budget to 83 minutes instead of the requested 240. That shrank Planquadratpark's activity allowance to 33 minutes, but my helper emitted duration_minutes of 60. validate_card rejected it, the retry returned the same card, and the fallback fired. Exactly as designed. The Step 2 validator was correctly catching the Step 2 bug inside a live pipeline.&lt;/p&gt;

&lt;p&gt;I also found a wrong assumption in my tomorrow-branch test: I had used 23:00 UTC believing it was evening in Vienna, but that is 01:00 local, which is before sunset, so today was the correct answer. My premise was wrong, not the code.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Yes, proceed with the step 8.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div&gt;
                  &lt;div class="agent-session-text agent-session-text-collapse"&gt;
                    &lt;p&gt;I got this diagnosis wrong twice before finding the real cause, and the wrong attempts are the useful part.&lt;/p&gt;

&lt;p&gt;Bug one was real and serious. The planner built a fresh TTLCache inside plan_adventure, so the cache was empty on every request. The re-roll button existed specifically to avoid re-querying SerpApi, and it was spending quota on every press. Fixed by moving it to process scope.&lt;/p&gt;

&lt;p&gt;Bug two was mine, in the tests. I misdiagnosed the dependency override twice: first blaming the Callable type alias on the route signature, then parameter defaults. I edited routes.py both times before reading the actual 422 body. The mystery missing query params were literally named s and u, because I had written a two-argument function with those names in my own diagnostic script.&lt;/p&gt;

&lt;p&gt;The real cause: Depends expects a factory that RETURNS the model function. I had supplied the function itself, so FastAPI called it during dependency resolution with empty strings. Consequences: on the happy path the planner received a dict instead of a function, the call failed, and the card silently fell back; on the broken-model test the RuntimeError escaped before the planner ran at all.&lt;/p&gt;

&lt;p&gt;I reverted both wrong route changes rather than leave them in with invented rationale.&lt;/p&gt;

                  &lt;/div&gt;
                  Show more
                &lt;/div&gt;
        &lt;/div&gt;
      &lt;/div&gt;

      &lt;div class="agent-session-message agent-session-user"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-user"&gt;
          You
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Now write the article. The demo will be a link to the live app.&lt;/p&gt;

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

      &lt;div class="agent-session-message agent-session-assistant"&gt;
        &lt;div class="agent-session-role-badge agent-session-role-assistant"&gt;
          Agent
        &lt;/div&gt;
        &lt;div class="agent-session-content"&gt;
                &lt;div class="agent-session-text"&gt;
                  &lt;p&gt;Filled the Hacktoberfest Touch Grass submission template. Updated the existing draft in place rather than creating a second post, and left it unpublished so the author could review it. Flagged the deadline as 11:59 PM PDT that night.&lt;/p&gt;

&lt;p&gt;Pointed out what I deliberately did NOT claim: the challenge gives bonus points for actually taking it outside and reporting on it, and that step was still pending, so the post does not assert it happened.&lt;/p&gt;

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

  &lt;div class="agent-session-footer"&gt;
    &lt;span class="agent-session-meta"&gt;
        12 of 12 messages
    &lt;/span&gt;
  &lt;/div&gt;
&lt;/div&gt;


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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — &lt;code&gt;google/gemma-4-26b-a4b-it&lt;/code&gt; does the selection and prose for every card.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Backboard&lt;/strong&gt; — model access, provider routing, and the one call per request budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of SerpApi&lt;/strong&gt; — &lt;code&gt;google_maps&lt;/code&gt; engine for real places, coordinates and ratings; the entire candidate pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render&lt;/strong&gt; — the free web service this runs on, defined in &lt;code&gt;render.yaml&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>poligem: a Polish conversation tutor that runs entirely on your own laptop</title>
      <dc:creator>Dariusz Dumnicki</dc:creator>
      <pubDate>Sun, 04 Oct 2026 22:20:56 +0000</pubDate>
      <link>https://dev.to/dumnicki/poligem-a-polish-conversation-tutor-that-runs-entirely-on-your-own-laptop-24j6</link>
      <guid>https://dev.to/dumnicki/poligem-a-polish-conversation-tutor-that-runs-entirely-on-your-own-laptop-24j6</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;poligem&lt;/strong&gt; is a Polish conversation practice app that runs entirely on your own laptop. The conversation core is &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3 (4B)&lt;/a&gt;, an open-weight model served locally through &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. No account, no API key, no subscription — and no data leaving the computer.&lt;br&gt;
I've built it for my girlfriend who has recently started learning Polish. Since the app uses a locally served, open-weight model, she can practice even on the airplane when there's no wifi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Poligem&lt;/strong&gt; app has three parts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Chat that starts with a situation.&lt;/strong&gt; Most language apps open with "what would you like to talk about?", which is a hard question when you're a beginner — you don't have the vocabulary to answer it yet. poligem's tutor picks a scene and puts you inside it:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ćwiczmy. Wyobraź sobie, że jesteś w kawiarni, a kelner podchodzi i mówi: &lt;em&gt;„Dzień dobry, co podać?"&lt;/em&gt; — co odpowiesz?&lt;br&gt;
&lt;em&gt;example cafe scenario&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It stays in character and asks one natural follow-up at a time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When you don't understand.&lt;/strong&gt; A beginner hits an unfamiliar word and stalls. poligem gives you an &lt;strong&gt;EN&lt;/strong&gt; button under any sentence for an English translation. This is the feature that my girlfriend finds particularly useful. Since the translation is hidden at first, it makes her read the polish sentence first before reading the translation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exercises that adapt to you.&lt;/strong&gt; Three drill types — translate English→Polish, translate Polish→English, and fill-the-gap — with a level (A1/A2/B1) derived from your measured accuracy rather than a dropdown you set once and forget. Missed prompts feed back into generation, so exercises target your weak spots instead of repeating one difficulty.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/exLw2qBxHJg" width="710" height="399"&gt;
  &lt;/iframe&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/dumnicki" rel="noopener noreferrer"&gt;
        dumnicki
      &lt;/a&gt; / &lt;a href="https://github.com/dumnicki/poligem" rel="noopener noreferrer"&gt;
        poligem
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Poligem - an app for Polish language practice
    &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;poligem&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A Polish conversation practice app that runs entirely on your own machine.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;poligem&lt;/code&gt; pairs a free chat tutor with structured drills for Polish learners
The conversation core is &lt;strong&gt;Gemma 3 (4B) running locally through Ollama&lt;/strong&gt; — no
accounts, no API keys, no data leaving your laptop.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why it runs locally&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Polish learners are often self-studying, which means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The conversation is private.&lt;/strong&gt; Every sentence you type — including mistakes
and often your name, your work, your partner — stays on your machine. Nothing
is sent to a third party.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is free.&lt;/strong&gt; Ollama and Gemma 3 carry no per-token cost, so the app is not
gated behind a subscription and does not degrade after a trial period.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tutor is a file, not a service.&lt;/strong&gt; The entire teaching personality —
level handling, scenario framing, correction style, translation behaviour —
lives in &lt;a href="https://github.com/dumnicki/poligem/server/prompt.ts" rel="noopener noreferrer"&gt;&lt;code&gt;server/prompt.ts&lt;/code&gt;&lt;/a&gt;. Changing how &lt;code&gt;poligem&lt;/code&gt; teaches
Polish is editing…&lt;/li&gt;
&lt;/ul&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/dumnicki/poligem" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Gemma 3 (4B) is not one component among several — it &lt;em&gt;is&lt;/em&gt; the application. It writes every line the tutor says, generates every exercise, and drives its own difficulty adjustment based on how you perform. Nothing in the app is a canned response from a lookup table.&lt;/p&gt;

&lt;p&gt;It's served locally via Ollama and reached through an Express proxy. The proxy looks redundant since Ollama's CORS is permissive, but it earns its place: the client never depends on Ollama's API contract, so the model can be swapped without touching the frontend, and it gives one place to inject the tutor's system prompt.&lt;/p&gt;

&lt;p&gt;Two decisions came directly from watching a 4B model fail:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exercises are pre-generated into a pool.&lt;/strong&gt; Generating them takes ~35 seconds on CPU, which is unacceptable mid-lesson. So the server keeps three of each type ready in memory and refills in the background. Handing you an exercise is an array shift, not a model call: &lt;strong&gt;45–167 ms&lt;/strong&gt; measured. If Ollama is down, curated exercises are served instead, rotating so nothing repeats — the app never dead-ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloze blanks are applied by the server.&lt;/strong&gt; Asked to remove a word from a Polish sentence, Gemma reliably returns a complete sentence and sometimes inserts its own blank — producing sentences with two gaps and an answer that matched neither. Rather than trust the prompt, the server locates the answer word and blanks it deterministically. Refills also generate in batches of three, because asking for a single exercise measurably degraded output into fragments.&lt;/p&gt;

&lt;p&gt;Everything goes through a single request queue: a 4B model on CPU doesn't run two requests in parallel, they interleave and both slow down, and a background refill would otherwise stall your conversation.&lt;/p&gt;

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

&lt;p&gt;Running a small open-weight model &lt;strong&gt;locally&lt;/strong&gt; rather than calling a closed API is what made this app honest to its own premise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data stays on the machine.&lt;/strong&gt; Every sentence typed — including the mistakes, which are the most revealing thing you produce — never leaves the laptop. A closed API can't offer that at any price.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's free, permanently.&lt;/strong&gt; No token bill, no trial expiring mid-course, no feature degrading once credits run out. For a practice tool you use daily, that changes who can use it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works offline.&lt;/strong&gt; Once the model is pulled, practice works on a plane or on a machine that has never seen a wifi network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The tutor is a file.&lt;/strong&gt; The entire teaching personality — level handling, scenario framing, correction style, translation behaviour — lives in &lt;code&gt;server/prompt.ts&lt;/code&gt;. Changing how poligem teaches Polish is editing one file and restarting. There's no prompt-injection surface from a shared endpoint, and no vendor silently changing the model underneath you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's tunable by the learner.&lt;/strong&gt; Swap the model, adjust the level, add your own vocabulary. A closed API gives you none of that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything interesting in this project is a &lt;em&gt;consequence&lt;/em&gt; of the small open model rather than a feature bolted onto a commercial one.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; (featured) — Gemma 3 generates the tutor's dialogue, creates every exercise, and adapts its own difficulty. It is the core of the product, not an add-on.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built with &lt;a href="https://ai.google.dev/gemma" rel="noopener noreferrer"&gt;Gemma 3&lt;/a&gt; by Google, served locally via &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;. Drafted with AI assistance and verified by hand.&lt;/em&gt;&lt;/p&gt;

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