<?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: πn</title>
    <description>The latest articles on DEV Community by πn (@ptbdtq).</description>
    <link>https://dev.to/ptbdtq</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%2F1408910%2F0a00c928-2f60-4e4a-88c5-007023b2ac0f.jpg</url>
      <title>DEV Community: πn</title>
      <link>https://dev.to/ptbdtq</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ptbdtq"/>
    <language>en</language>
    <item>
      <title>TouchGrass FC: a Telegram bot whose only goal is to make you close Telegram</title>
      <dc:creator>πn</dc:creator>
      <pubDate>Mon, 12 Oct 2026 06:05:27 +0000</pubDate>
      <link>https://dev.to/ptbdtq/touchgrass-fc-a-telegram-bot-whose-only-goal-is-to-make-you-close-telegram-43lp</link>
      <guid>https://dev.to/ptbdtq/touchgrass-fc-a-telegram-bot-whose-only-goal-is-to-make-you-close-telegram-43lp</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;Every pickup football group has the same thread. "Saturday?" "Maybe." "What time?" "After work." "Which Saturday?" Twenty messages later, nobody has committed and nobody has played.&lt;/p&gt;

&lt;p&gt;TouchGrass FC is a Telegram group bot that replaces that thread with one question and one poll. Someone types &lt;code&gt;/play&lt;/code&gt;, and the bot asks everyone to reply with when they can make it, in their own words and whichever language they type in (it's built for Ukrainian, English, Spanish, German, Polish, French, Russian). A local Gemma model turns each reply into structured availability. From there it's plain code: check the 7-day forecast, drop any slot that wouldn't finish before sunset, pick the best one, find the nearest pitch on OpenStreetMap, and post exactly one poll with a map pin. Then the bot goes quiet, except for a reminder three hours before kickoff and one question afterwards: did you actually play?&lt;/p&gt;

&lt;p&gt;That's the whole design for this week's theme. Per player, the interaction is one message and one tap on the poll. Per match, it's one &lt;code&gt;/played&lt;/code&gt; from whoever remembers. The only metric I care about is the one &lt;code&gt;/stats&lt;/code&gt; reports: &lt;code&gt;⚽ Matches actually played via this bot: N&lt;/code&gt;. (The bot's reply to &lt;code&gt;/played&lt;/code&gt; is "Legendary. Logged. Go shower." Good advice, honestly.)&lt;/p&gt;

&lt;h3&gt;
  
  
  Did anyone actually touch grass?
&lt;/h3&gt;

&lt;p&gt;Not yet, and I'd rather say so than invent a match. I've run the full loop in a live Telegram chat: messy replies in; weather score, pitch pin and poll out; reminder queued. But any number you see on &lt;code&gt;/stats&lt;/code&gt; in my demo comes from me pressing the button to test it, not from a game. I'll update this post after the first real one.&lt;/p&gt;

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

&lt;p&gt;The bot runs on my machine, so there's no live link. Here's a walkthrough in a real Telegram chat:&lt;/p&gt;

&lt;p&gt;No bot token? &lt;code&gt;python -m touchgrass simulate&lt;/code&gt; replays a scripted group chat in your terminal (more on that below).&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/budulay" rel="noopener noreferrer"&gt;
        budulay
      &lt;/a&gt; / &lt;a href="https://github.com/budulay/touchgrass-fc" rel="noopener noreferrer"&gt;
        touchgrass-fc
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A Telegram group bot that turns messy "when can you play?" chatter into ONE poll for a real-life pickup football match — then goes quiet so people actually go outside.
    &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;touchgrass-fc&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;A Telegram group bot that turns messy "when can you play?" chatter into ONE poll for a real-life pickup football match — then goes quiet so people actually go outside.&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/budulay/touchgrass-fc" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;MIT licensed, and a brand-new repo started during the challenge window.&lt;/p&gt;

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

&lt;p&gt;One rule shaped everything: the model only gets the job code can't do, which is reading human messages. Everything else is boring, testable code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.11+, aiogram 3&lt;/strong&gt;: long polling (no server needed), privacy mode ON&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 3 via Ollama&lt;/strong&gt;: local inference, with Pydantic v2 validating every output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite (aiosqlite)&lt;/strong&gt;: all state stays on the machine&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open-Meteo, Nominatim, Overpass&lt;/strong&gt;: free and keyless; weather and map lookups are the only things that leave the machine&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Reading messy messages
&lt;/h3&gt;

&lt;p&gt;People don't write schemas. "можу в суботу після 18, якщо не дощ", "Sat morning I'm in" and "пятница после обеда" all have to land in the same structure. Ollama's &lt;code&gt;format&lt;/code&gt; parameter accepts a JSON schema, so each request carries the Pydantic model's schema, at temperature 0 (abridged):&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;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stream&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_SCHEMA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Pydantic JSON schema
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;messages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&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="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&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;For the first message above, the output looks roughly like this (abridged):&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="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"can_play"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"slots"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"weekday"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saturday"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"18:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"end"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"conditions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"no_rain"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"uk"&lt;/span&gt;&lt;span class="w"&gt;
&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;If validation fails, the error goes back to the model together with the original message, once. If that fails too, the bot asks the player to rephrase instead of guessing. One gotcha worth passing on: Ollama's &lt;code&gt;format&lt;/code&gt; wants the complete JSON Schema object, not just the &lt;code&gt;$defs&lt;/code&gt; block that Pydantic nests sub-models under.&lt;/p&gt;

&lt;h3&gt;
  
  
  The planner is deliberately dumb
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;score = player_count × weather_score&lt;/code&gt;. Weather score is a 60% rain penalty plus a 40% wind penalty, averaged over the slot window. Any slot ending after sunset (with a 15-minute buffer) is filtered out before scoring. No LLM involved: it's just numbers, which means it's unit-tested on recorded fixtures instead of vibes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does the model actually work?
&lt;/h3&gt;

&lt;p&gt;The repo has 40 labelled messages in Ukrainian, Russian and English: slang, negations, conditionals, vague ranges like "можу в будь-який день після 19". The eval harness runs them against each Gemma size:&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;Overall&lt;/th&gt;
&lt;th&gt;can_play&lt;/th&gt;
&lt;th&gt;language&lt;/th&gt;
&lt;th&gt;slots&lt;/th&gt;
&lt;th&gt;conditions&lt;/th&gt;
&lt;th&gt;avg_lat&lt;/th&gt;
&lt;th&gt;p95_lat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:1b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;td&gt;45%&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;55%&lt;/td&gt;
&lt;td&gt;80%&lt;/td&gt;
&lt;td&gt;6.6s&lt;/td&gt;
&lt;td&gt;16.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;gemma3:4b&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;28%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;50%&lt;/td&gt;
&lt;td&gt;72%&lt;/td&gt;
&lt;td&gt;88%&lt;/td&gt;
&lt;td&gt;23.2s&lt;/td&gt;
&lt;td&gt;36.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Overall column is the harshest number in the table, and I'm showing it anyway. Per field, &lt;code&gt;gemma3:4b&lt;/code&gt; is reasonable at what the planner needs most: whether someone can play (88%), under what conditions (88%), and roughly when (72%). Language detection sits around 50% for both sizes; it mostly decides which language the invite is written in, and I haven't dug into why it's so weak yet.&lt;/p&gt;

&lt;p&gt;Speed is the other cost. On my machine [TODO: one line on your hardware, e.g. "a laptop with no GPU"], &lt;code&gt;gemma3:4b&lt;/code&gt; takes 23 seconds per message on average (p95: 36 s). &lt;code&gt;gemma3:1b&lt;/code&gt; is about 3.5× faster but gets &lt;code&gt;can_play&lt;/code&gt; wrong more than half the time. So 4b is the default, and when it returns garbage the bot asks instead of guessing. For a group that coordinates once a week, I can live with 23 seconds. For anything conversational, I couldn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it without Telegram
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;python -m touchgrass simulate&lt;/code&gt; replays a scripted group conversation in your terminal, runs the real planner on synthetic weather, and prints a transcript. If Ollama is running, it uses the real model, so you'll wait as long as the numbers above suggest. If it isn't, it falls back to canned parses: you see the flow in seconds, but you're not seeing the model.&lt;/p&gt;

&lt;h3&gt;
  
  
  What doesn't work (yet)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The pitch search finds pitches that exist in OpenStreetMap. It has no idea whether one is free or bookable.&lt;/li&gt;
&lt;li&gt;One location per chat, set with &lt;code&gt;/setlocation&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Privacy mode stays on, so the bot only reads replies to its own message and &lt;code&gt;/in&lt;/code&gt;. If you don't reply to the bot, as far as it knows you don't exist.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;I didn't benchmark a hosted model, so I won't pretend to know by how much it would beat a 4B Gemma on accuracy or speed. My guess is easily. Here's what going open bought me instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nobody new reads the chat.&lt;/strong&gt; The messages already live in Telegram. The question is whether a second company's API also gets to read who's free on Thursday evenings. Here, inference runs on the same machine as the bot, so it doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A model I can pin, swap and measure.&lt;/strong&gt; I picked an exact model tag and ran the same 40 fixtures against two sizes; the table above is the result. Going from 1b to 4b is one environment variable (&lt;code&gt;OLLAMA_MODEL&lt;/code&gt;), and nothing changes underneath me between runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contributors don't need keys.&lt;/strong&gt; The extraction schema is a Pydantic model in &lt;code&gt;nlu.py&lt;/code&gt;, and the 29 tests run without network access. Anyone can change the schema, rerun the suite and open a PR without asking me, or a vendor, for credentials.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Cost, honestly, isn't an argument at this scale: a few dozen inferences are cents on any hosted API. It only becomes one when a bot like this has real users.&lt;/p&gt;

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

&lt;p&gt;I built this in Google's Antigravity from one written brief, split into three parallel workstreams: language understanding plus the eval fixtures; planner, weather and pitch lookup; and the Telegram layer plus the simulator. The agent wrote the code, the 29 tests and the first draft of this post. I ran the result in a live Telegram chat, then edited this write-up.&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 3 (via Ollama) is the only model in the project. It does the multilingual message understanding and writes the two-sentence invite in the group's dominant language. It runs locally, and the repo benchmarks it at two sizes (&lt;code&gt;gemma3:1b&lt;/code&gt; and &lt;code&gt;gemma3:4b&lt;/code&gt;) on the same 40 fixtures. The results are in the eval section above, including the parts that don't flatter it.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The bot's best feature is that it knows when to stop talking. This post is trying to learn from it.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ollama</category>
      <category>python</category>
    </item>
    <item>
      <title>CustomsBuddy: Your customs broker for international parcels</title>
      <dc:creator>πn</dc:creator>
      <pubDate>Wed, 04 Mar 2026 20:06:54 +0000</pubDate>
      <link>https://dev.to/ptbdtq/customsbuddy-your-customs-broker-for-international-parcels-1fi0</link>
      <guid>https://dev.to/ptbdtq/customsbuddy-your-customs-broker-for-international-parcels-1fi0</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/mlh/built-with-google-gemini-02-25-26"&gt;Built with Google Gemini: Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built with Google Gemini
&lt;/h2&gt;

&lt;p&gt;Sending international packages is a pain. Especially when it comes to filling out customs declarations (CN22/CN23).&lt;/p&gt;

&lt;p&gt;The average user doesn't know what "HS Codes" are, doesn't know how to correctly translate the names of specific items into English customs language, and often doesn't even realize that their favorite homemade cookies or cold pills are prohibited from being imported into the destination country.&lt;/p&gt;

&lt;p&gt;To solve this problem, I created &lt;strong&gt;CustomsBuddy&lt;/strong&gt; — a React application that converts a user's chaotic text description of a package into a rigorous, validated array of data.&lt;/p&gt;

&lt;p&gt;In this article, I'll show how we abandoned unreliable text parsing and used &lt;strong&gt;Structured Output (JSON Schema) and dynamic System Instructions&lt;/strong&gt; in the Gemini API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://customs-buddy-528833377593.us-central1.run.app/" rel="noopener noreferrer"&gt;Demo:&lt;/a&gt;&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://customs-buddy-528833377593.us-central1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;h2&gt;
  
  
  🛑 Why don't regular prompts work for UI?
&lt;/h2&gt;

&lt;p&gt;Let's imagine a typical input: "&lt;em&gt;I'm sending two used t-shirts, a new book, homemade cookies, and 50 bucks worth of medicine to Canada.&lt;/em&gt;"&lt;/p&gt;

&lt;p&gt;If you ask a regular LLM to "make a table out of this," it will return Markdown. Markdown looks great in chat, but as a developer, I need to draw React components: green checkmarks for allowed items, red warnings for prohibited items, and calculate the total.&lt;/p&gt;

&lt;p&gt;I need predictable JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 Solution: Gemini Structured Output
&lt;/h2&gt;

&lt;p&gt;Instead of begging the model to "please return only JSON," we use the &lt;code&gt;responseSchema&lt;/code&gt; parameter in the Gemini API. This forces the model to strictly follow the structure we specify.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Define the Ideal Structure (The Schema)
&lt;/h2&gt;

&lt;p&gt;Here is the configuration object that we will pass to the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const responseSchema = {
  type: "OBJECT",
  properties: {
    items: {
      type: "ARRAY",
      items: {
        type: "OBJECT",
        properties: {
          original_description: { type: "STRING" },
          customs_description_en: { type: "STRING" },
          hs_code: { type: "STRING" }, // International customs code
          quantity: { type: "INTEGER" },
          weight_kg: { type: "NUMBER" },
          value_usd: { type: "NUMBER" },
          is_allowed: { type: "BOOLEAN" }, // The main trigger for UI!
          warning_reason: { type: "STRING" } // Explanation if prohibited
        },
        required: ["original_description", "customs_description_en", "hs_code", "quantity", "weight_kg", "value_usd", "is_allowed", "warning_reason"]
      }
    },
    general_warnings: {
      type: "ARRAY",
      items: { type: "STRING" }
    }
  },
  required: ["items", "general_warnings"]
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Dynamic System Instructions
&lt;/h2&gt;

&lt;p&gt;Our app supports different destination countries (USA, Canada, Germany, etc.) and different interface languages. The US customs rules are different from Germany. That's why we make the &lt;code&gt;systemInstruction&lt;/code&gt; dynamic!&lt;/p&gt;

&lt;p&gt;In the React component, before calling the API, we form the instruction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Get the country name in English and the language for the explanation
const destCountryEn = countries.find(c =&amp;gt; c.code === destination).name.en;
const explanationLang = lang === 'uk' ? 'українською мовою' : 'in English';

const systemInstruction = `You are a professional international customs broker.
Your task: to analyze user input to send parcels to the country: ${destCountryEn}.
1. Translate the product descriptions into strict, standardized English.
2. Determine the exact 6-digit HS Code.
3. Logically distribute the cost/weight if only the total is specified.
4. STRONG VALIDATION: Check the customs rules of the country ${destCountryEn}. If the item is prohibited (e.g., homemade food, certain medications), set is_allowed: false and explain why, ${explanationLang} is mandatory. Respond strictly in JSON format.`;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 3: Calling the Gemini API
&lt;/h2&gt;

&lt;p&gt;Now we send all this to &lt;code&gt;gemini-2.5-flash&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-09-2025:generateContent?key=${apiKey}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
  contents: [{ parts: [{ text: inputText }] }],
  systemInstruction: { parts: [{ text: systemInstruction }] },
  generationConfig: {
    responseMimeType: "application/json",
    responseSchema: responseSchema // Inserting our schema!
  }
 })
});

const data = await response.json();
const jsonText = data.candidates[0].content.parts[0].text;
const parsedDeclaration = JSON.parse(jsonText);
// That's it! No regular expressions or text truncation!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🎯 How does this look in practice?
&lt;/h2&gt;

&lt;p&gt;When a user types: "&lt;em&gt;I'm sending two T-shirts, a book, homemade cookies, and $50 worth of cold medicine to Canada&lt;/em&gt;"...&lt;/p&gt;

&lt;p&gt;Gemini returns a perfect JSON where for the book and T-shirts &lt;code&gt;is_allowed: true&lt;/code&gt; (and the model itself found the HS codes &lt;code&gt;4901.99&lt;/code&gt; and &lt;code&gt;6309.00&lt;/code&gt; for them). But for the cookies and medicine, the model returns &lt;code&gt;is_allowed: false&lt;/code&gt; and adds an explanation that Canadian customs blocks homemade food without factory labeling and over-the-counter medicines.&lt;/p&gt;

&lt;p&gt;In React, we simply map this array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{results.items.map((item, idx) =&amp;gt; ( 
 &amp;lt;div key={idx} className={item.is_allowed ? 'bg-slate-50' : 'bg-red-50'}&amp;gt; 
  {item.is_allowed ? &amp;lt;CheckCircle2 className="text-emerald-500" /&amp;gt; : &amp;lt;AlertOctagon className="text-red-500" /&amp;gt;} 

  &amp;lt;h3&amp;gt;{item.customs_description_en}&amp;lt;/h3&amp;gt; 
  &amp;lt;span&amp;gt;HS Code: {item.hs_code}&amp;lt;/span&amp;gt; 

  {!item.is_allowed &amp;amp;&amp;amp; ( 
  &amp;lt;div className="text-red-700"&amp;gt; 
   Failure reason: {item.warning_reason} 
  &amp;lt;/div&amp;gt; 
  )} 
 &amp;lt;/div&amp;gt;
))}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 Conclusion
&lt;/h2&gt;

&lt;p&gt;Structured Output (JSON Schema) completely changes the approach to developing AI applications. We no longer use LLMs simply as "smart interlocutors". We use them as reliable microservices for data analysis, classification and transformation that integrate perfectly into our frontend.&lt;/p&gt;

&lt;p&gt;Try adding &lt;code&gt;responseSchema&lt;/code&gt; to your next Gemini project - and you will forget about the pain of text parsing forever!&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>geminireflections</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Maplewood Heights (Community Connect)</title>
      <dc:creator>πn</dc:creator>
      <pubDate>Mon, 02 Mar 2026 07:55:04 +0000</pubDate>
      <link>https://dev.to/ptbdtq/maplewood-heights-community-connect-25fo</link>
      <guid>https://dev.to/ptbdtq/maplewood-heights-community-connect-25fo</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/weekend-2026-02-28"&gt;DEV Weekend Challenge: Community&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Today I’m excited to introduce Maplewood Heights, a warm and inviting hub for our local community! &lt;/p&gt;

&lt;p&gt;🏡 It’s a place where neighbors can learn about events, share news, and discover local gems.&lt;/p&gt;

&lt;p&gt;☁️ I'd love to hear your thoughts on the design!&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://community-connect-528833377593.us-central1.run.app"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;p&gt;&lt;em&gt;The app is already deployed to Google Cloud:&lt;/em&gt;: &lt;a href="https://community-connect-528833377593.us-central1.run.app" rel="noopener noreferrer"&gt;https://community-connect-528833377593.us-central1.run.app&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;🛠️ &lt;strong&gt;Functionality of the first version already has a lot of useful features:&lt;/strong&gt;&lt;br&gt;
📅 View upcoming events &lt;br&gt;
💬 Live feed of community updates &lt;br&gt;
☕ Favorite local places &lt;br&gt;
🤝 Neighbor profiles and statistics&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Updates feed: Multi-level comment threads, quick reactions, and emojis. &lt;/li&gt;
&lt;li&gt;Community: Get to know your neighbors and quickly message them. &lt;/li&gt;
&lt;li&gt;Events: A board of upcoming activities (from farmers' markets to volunteering). &lt;/li&gt;
&lt;li&gt;Local "gems": Recommendations for the best bakeries, parks, or coffee shops in the area.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My main goal was to combine the speed of modern frontend technologies with a stylish, warm UI.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack &amp;amp; Architecture:&lt;/strong&gt; &lt;br&gt;
⚡ React 18 + Vite (quick build via SWC) &lt;br&gt;
🎨 Tailwind CSS v3 + CSS-variables (fully custom light/dark theme in honey/sage green tones) &lt;br&gt;
🧱 shadcn/ui (integrated ~50 Radix UI components) &lt;br&gt;
✨ Framer Motion (controls orchestrating animations when scrolling and microinteractions) &lt;br&gt;
🚢 &lt;strong&gt;CI/CD:&lt;/strong&gt; Multi-stage Docker build on top of Nginx -&amp;gt; deployment to Google Cloud Run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's inside?&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watercolor hero block.&lt;/li&gt;
&lt;li&gt;Update feed with support for recursive N-level comments and reactions.&lt;/li&gt;
&lt;li&gt;Neighborhood mutual aid module.&lt;/li&gt;
&lt;li&gt;Own context-messaging right in the browser.&lt;/li&gt;
&lt;li&gt;When design (DM Serif Display + warm tones) works in synergy with cool UX, users really want to come back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Happy to connect people through technology.&lt;/strong&gt; ✨&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>showdev</category>
    </item>
    <item>
      <title>My Portfolio Has a Split Personality: GUI vs Terminal</title>
      <dc:creator>πn</dc:creator>
      <pubDate>Mon, 02 Feb 2026 07:58:24 +0000</pubDate>
      <link>https://dev.to/ptbdtq/my-portfolio-has-a-split-personality-gui-vs-terminal-3olc</link>
      <guid>https://dev.to/ptbdtq/my-portfolio-has-a-split-personality-gui-vs-terminal-3olc</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/new-year-new-you-google-ai-2025-12-31"&gt;New Year, New You Portfolio Challenge Presented by Google AI&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Hello, network! 👋&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For the "&lt;strong&gt;New Year, New You&lt;/strong&gt;" challenge, I didn't want to just update my CSS. I wanted to build something that represents the duality of a &lt;em&gt;Full Stack Developer&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;The Professional: &lt;em&gt;Clean, modern UI using the trendy Bento Grid layout.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Geek: &lt;em&gt;A hacker-style Terminal for those who prefer CLI over GUI.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it out!&lt;/strong&gt;&lt;br&gt;
Check out the live demo and let me know: are you team #GUI or team #Terminal? 👇&lt;br&gt;


&lt;/p&gt;
&lt;div class="ltag__cloud-run"&gt;
  &lt;iframe height="600px" src="https://devportfolio-528833377593.us-central1.run.app/"&gt;
  &lt;/iframe&gt;
&lt;/div&gt;




&lt;p&gt;&lt;em&gt;Available at&lt;/em&gt;: &lt;a href="https://devportfolio-528833377593.us-central1.run.app/" rel="noopener noreferrer"&gt;https://devportfolio-528833377593.us-central1.run.app/&lt;/a&gt; 🚀&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Concept&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Usually, portfolios are static. You read, you scroll, you leave. I wanted interaction.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GUI Mode:&lt;/strong&gt; Visualizes my projects and skills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal Mode:&lt;/strong&gt; Lets you type commands like &lt;code&gt;help&lt;/code&gt;, &lt;code&gt;skills&lt;/code&gt;, or even &lt;code&gt;ask&lt;/code&gt; to talk to AI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The AI Magic ✨&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instead of searching for my experience in a long text, recruiters can now ask:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Tell me about your skills?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The AI analyzes my JSON data and generates a professional answer instantly. It lives both in a chat modal (GUI) and as a command (Terminal).&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Tech Stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Framework:&lt;/strong&gt; React + Vite&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Styling:&lt;/strong&gt; Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Google Gemini API&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Icons:&lt;/strong&gt; Lucide React&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most time was spent not on the code, but on making the site feel "like me": a little mysterious, a little playful, but at the same time very professional.&lt;/p&gt;

&lt;p&gt;This is not just a portfolio. This is my manifesto for 2026: code with passion, use the best tools (thanks Google AI) and don't be afraid to be yourself.&lt;/p&gt;

&lt;p&gt;Thanks for stopping by.&lt;/p&gt;

&lt;p&gt;Write in the comments what you liked/what you would change - I'd be happy to hear your feedback.&lt;br&gt;
And if you also participated - drop a link, I'll take a look and like 🚀&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>googleaichallenge</category>
      <category>portfolio</category>
      <category>gemini</category>
    </item>
  </channel>
</rss>
