<?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: Sagar Maurya</title>
    <description>The latest articles on DEV Community by Sagar Maurya (@sagarmaurya).</description>
    <link>https://dev.to/sagarmaurya</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3019158%2Fbf3e71fa-5bc7-4293-8853-83a8f3915ad9.gif</url>
      <title>DEV Community: Sagar Maurya</title>
      <link>https://dev.to/sagarmaurya</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sagarmaurya"/>
    <language>en</language>
    <item>
      <title>I Built an Offline AI File Organizer for a Friend | Hacktoberfest 2026</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Mon, 05 Oct 2026 06:54:14 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/i-built-an-offline-ai-file-organizer-for-a-friend-hacktoberfest-2026-3bh9</link>
      <guid>https://dev.to/sagarmaurya/i-built-an-offline-ai-file-organizer-for-a-friend-hacktoberfest-2026-3bh9</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;My friend's Downloads folder is a dumping ground: lecture notes, an invoice, code files, installers, images, all mixed together under names like &lt;code&gt;doc_final&lt;/code&gt; and &lt;code&gt;scan&lt;/code&gt;. Extension-based sorters put every PDF in one folder, and cloud AI tools would mean uploading her private files to someone else's server.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Organizr&lt;/strong&gt;, an offline file organizer that sorts files by what's &lt;em&gt;inside&lt;/em&gt; them, not by their extension. It reads each file with a local Gemma model, proposes a folder and a short reason, and moves nothing until you click Apply. Every move can be undone.&lt;/p&gt;

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

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

&lt;p&gt;&lt;em&gt;The video has no voiceover (I have a cough), so it has English subtitles. The whole run is with Wi-Fi turned off.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I made a 20-file sample Downloads folder from the file names she sent me, using stand-in names because her real folder has IDs and certificates. Here is what Organizr did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;12 files sorted with confidence.&lt;/strong&gt; For example, a C assignment went to &lt;em&gt;Assignments&lt;/em&gt; and an invoice to &lt;em&gt;Fee Receipts&lt;/em&gt;, based on the text inside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 files flagged "Please check this one":&lt;/strong&gt; images, installers and a video, which have no readable text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply&lt;/strong&gt; moves the files, and &lt;strong&gt;Undo last move&lt;/strong&gt; puts everything back.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5kxfijt9cwyieesj7in7.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%2F5kxfijt9cwyieesj7in7.png" alt="Plan view" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Famdye73uf8awu5a2dcqx.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%2Famdye73uf8awu5a2dcqx.png" alt="View panel showing the text and Gemma's reason" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxudlf57x94n1jz2suvzw.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%2Fxudlf57x94n1jz2suvzw.png" alt="Done screen" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkauzrm0odudogln54ict.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%2Fkauzrm0odudogln54ict.png" alt="The folder after sorting" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What my friend said
&lt;/h3&gt;

&lt;p&gt;I sent her the plan and the result screenshots and asked for her reaction. She replied: "this is actually cool 😮 my lecture notes and the invoice went where I'd put them.&lt;br&gt;
timetable in Lecture Notes is fine too. I'd try it on my laptop..."&lt;/p&gt;

&lt;p&gt;She said the lecture notes, the invoice and the timetable landed where she'd have put them, and that she'd try it on her laptop. She hasn't run it on her own folder yet, and she didn't comment on the files it got wrong, so that is all I'm claiming.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8s52tpyr0bqzguxkwi1u.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%2F8s52tpyr0bqzguxkwi1u.png" alt="Her reply" width="800" height="692"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Where it got things wrong
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gemma put a LinkedIn strategy document and a research CSV in &lt;strong&gt;Lecture Notes&lt;/strong&gt;. Both are wrong. The plan view lets you change any category before applying.&lt;/li&gt;
&lt;li&gt;Images, installers and videos can't be read, so they're flagged for review. Earlier in testing, Gemma made up a reason for a file with no text. OCR isn't included yet.&lt;/li&gt;
&lt;li&gt;Gemma's wording changes from run to run.&lt;/li&gt;
&lt;/ul&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/mauryasagar" rel="noopener noreferrer"&gt;
        mauryasagar
      &lt;/a&gt; / &lt;a href="https://github.com/mauryasagar/ai-file-organizer" rel="noopener noreferrer"&gt;
        ai-file-organizer
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Offline AI file organizer that sorts files by content, not extension, using local Gemma via Ollama. Private, undoable, no uploads.
    &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;🗁 Organizr&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;
  &lt;b&gt;An offline AI file organizer that sorts your files by what's inside them, not by their extension.&lt;/b&gt;&lt;br&gt;
  It reads each document with a local open-weight model (Gemma via Ollama), proposes where it belongs,&lt;br&gt;
  and moves files only after you approve. Your private files never leave your laptop
&lt;/p&gt;

&lt;p&gt;
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#the-problem" rel="noopener noreferrer"&gt;Problem&lt;/a&gt; ·
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#the-solution" rel="noopener noreferrer"&gt;Solution&lt;/a&gt; ·
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#how-it-works" rel="noopener noreferrer"&gt;How it works&lt;/a&gt; ·
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#getting-started" rel="noopener noreferrer"&gt;Getting started&lt;/a&gt; ·
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#project-structure" rel="noopener noreferrer"&gt;Structure&lt;/a&gt; ·
  &lt;a href="https://github.com/mauryasagar/ai-file-organizer#limitations" rel="noopener noreferrer"&gt;Limitations&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;🎃 Built for the &lt;b&gt;DEV Hacktoberfest Weekend Challenge: Build for a Friend.&lt;/b&gt;&lt;/p&gt;




&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;The Problem&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Downloads folders turn into a dumping ground. This project started with a friend whose folder mixed lecture PDFs, fee receipts, assignment files, scholarship letters and random images, all with names like &lt;code&gt;doc_final.pdf&lt;/code&gt; and &lt;code&gt;scan (3).pdf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Existing options did not fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extension-based sorters&lt;/strong&gt; put every PDF in one folder, so a fee receipt lands next to lecture notes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud AI tools&lt;/strong&gt; would mean uploading ID scans, marksheets and bank statements to someone else's…&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/mauryasagar/ai-file-organizer" 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;Python, Streamlit, Ollama running &lt;code&gt;gemma3:4b&lt;/code&gt;, PyMuPDF and python-docx.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;scanner.py&lt;/code&gt; finds safe files, skipping unfinished downloads and files changed in the last 5 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;extractor.py&lt;/code&gt; reads the first ~1000 characters of text from each file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;classifier.py&lt;/code&gt; sends the content (not the filename) and her list of categories to Gemma, which returns JSON with a category and a reason. Anything invalid falls back to "Other".&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;organizer.py&lt;/code&gt; builds the plan, moves files without ever overwriting or deleting, and writes an undo log.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I tested &lt;code&gt;gemma3:1b&lt;/code&gt; and &lt;code&gt;gemma3:4b&lt;/code&gt; on six files with misleading names. The 1B model got 4 of 6 right and the 4B model got 5 of 6, so I chose 4B. It runs on CPU on a 16 GB laptop in a few seconds per file. Removing the filename from the prompt improved the results, because the model stopped trusting names like &lt;code&gt;doc_final&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Her folder holds ID scans, marksheets and bank documents. With a closed API, sorting it would mean uploading all of that. With an open-weight model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Her data stays on her laptop.&lt;/strong&gt; The demo runs with Wi-Fi off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run&lt;/strong&gt; after the model download, with no API keys and no per-request billing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The model is swappable.&lt;/strong&gt; Changing it is one line in &lt;code&gt;classifier.py&lt;/code&gt;, which is how I compared 1B and 4B.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; the whole classifier runs on a local Gemma model (&lt;code&gt;gemma3:4b&lt;/code&gt;) through Ollama.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
    </item>
    <item>
      <title>Building a Student Marks Dashboard: From Data to Live Web App</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:06:21 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/building-a-student-marks-dashboard-from-data-to-live-web-app-3hgd</link>
      <guid>https://dev.to/sagarmaurya/building-a-student-marks-dashboard-from-data-to-live-web-app-3hgd</guid>
      <description>&lt;p&gt;Most pandas tutorials teach you syntax. They show you &lt;code&gt;.mean()&lt;/code&gt;, &lt;code&gt;.groupby()&lt;/code&gt;, &lt;code&gt;.apply()&lt;/code&gt;. But they don't show you how the pieces connect — how a CSV becomes a chart, how a chart becomes a web app, how a web app ends up live on the internet.&lt;/p&gt;

&lt;p&gt;I wanted to see that full pipeline for myself. So I built a small project: a student marks dashboard that reads a CSV, calculates results, plots a chart, and runs as a live web app.&lt;/p&gt;

&lt;h2&gt;
  
  
  The data
&lt;/h2&gt;

&lt;p&gt;Eight students, three subjects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name,maths,science,english
Arjun,78,85,72
Priya,92,88,95
Rahul,45,50,40
Sneha,88,91,84
Vikram,30,35,28
Anita,67,72,70
Karan,55,60,58
Meera,95,93,97
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Small and clean. That's on purpose — the goal was to understand the pipeline, not to wrestle with messy data.&lt;/p&gt;
&lt;h2&gt;
  
  
  Loading a CSV into a DataFrame
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/marks.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;      &lt;span class="k"&gt;name&lt;/span&gt;  &lt;span class="k"&gt;maths&lt;/span&gt;  &lt;span class="k"&gt;science&lt;/span&gt;  &lt;span class="k"&gt;english&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;    &lt;span class="k"&gt;Arjun&lt;/span&gt;     &lt;span class="mf"&gt;78&lt;/span&gt;       &lt;span class="mf"&gt;85&lt;/span&gt;       &lt;span class="mf"&gt;72&lt;/span&gt;
&lt;span class="mf"&gt;1&lt;/span&gt;    &lt;span class="k"&gt;Priya&lt;/span&gt;     &lt;span class="mf"&gt;92&lt;/span&gt;       &lt;span class="mf"&gt;88&lt;/span&gt;       &lt;span class="mf"&gt;95&lt;/span&gt;
&lt;span class="mf"&gt;2&lt;/span&gt;    &lt;span class="k"&gt;Rahul&lt;/span&gt;     &lt;span class="mf"&gt;45&lt;/span&gt;       &lt;span class="mf"&gt;50&lt;/span&gt;       &lt;span class="mf"&gt;40&lt;/span&gt;
&lt;span class="mf"&gt;3&lt;/span&gt;    &lt;span class="k"&gt;Sneha&lt;/span&gt;     &lt;span class="mf"&gt;88&lt;/span&gt;       &lt;span class="mf"&gt;91&lt;/span&gt;       &lt;span class="mf"&gt;84&lt;/span&gt;
&lt;span class="mf"&gt;4&lt;/span&gt;   &lt;span class="k"&gt;Vikram&lt;/span&gt;     &lt;span class="mf"&gt;30&lt;/span&gt;       &lt;span class="mf"&gt;35&lt;/span&gt;       &lt;span class="mf"&gt;28&lt;/span&gt;
&lt;span class="mf"&gt;5&lt;/span&gt;    &lt;span class="k"&gt;Anita&lt;/span&gt;     &lt;span class="mf"&gt;67&lt;/span&gt;       &lt;span class="mf"&gt;72&lt;/span&gt;       &lt;span class="mf"&gt;70&lt;/span&gt;
&lt;span class="mf"&gt;6&lt;/span&gt;    &lt;span class="k"&gt;Karan&lt;/span&gt;     &lt;span class="mf"&gt;55&lt;/span&gt;       &lt;span class="mf"&gt;60&lt;/span&gt;       &lt;span class="mf"&gt;58&lt;/span&gt;
&lt;span class="mf"&gt;7&lt;/span&gt;    &lt;span class="k"&gt;Meera&lt;/span&gt;     &lt;span class="mf"&gt;95&lt;/span&gt;       &lt;span class="mf"&gt;93&lt;/span&gt;       &lt;span class="mf"&gt;97&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;pd.read_csv()&lt;/code&gt; returns a DataFrame — basically a table in memory. Each row has an index (0–7) and each column is a Series.&lt;/p&gt;

&lt;p&gt;At this point it's just numbers. No totals, no averages, no conclusions.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating new columns
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maths&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;science&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;average&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;      &lt;span class="k"&gt;name&lt;/span&gt;  &lt;span class="k"&gt;maths&lt;/span&gt;  &lt;span class="k"&gt;science&lt;/span&gt;  &lt;span class="k"&gt;english&lt;/span&gt;  &lt;span class="k"&gt;total&lt;/span&gt;  &lt;span class="k"&gt;average&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;    &lt;span class="k"&gt;Arjun&lt;/span&gt;     &lt;span class="mf"&gt;78&lt;/span&gt;       &lt;span class="mf"&gt;85&lt;/span&gt;       &lt;span class="mf"&gt;72&lt;/span&gt;    &lt;span class="mf"&gt;235&lt;/span&gt;    &lt;span class="mf"&gt;78.33&lt;/span&gt;
&lt;span class="mf"&gt;1&lt;/span&gt;    &lt;span class="k"&gt;Priya&lt;/span&gt;     &lt;span class="mf"&gt;92&lt;/span&gt;       &lt;span class="mf"&gt;88&lt;/span&gt;       &lt;span class="mf"&gt;95&lt;/span&gt;    &lt;span class="mf"&gt;275&lt;/span&gt;    &lt;span class="mf"&gt;91.67&lt;/span&gt;
&lt;span class="mf"&gt;2&lt;/span&gt;    &lt;span class="k"&gt;Rahul&lt;/span&gt;     &lt;span class="mf"&gt;45&lt;/span&gt;       &lt;span class="mf"&gt;50&lt;/span&gt;       &lt;span class="mf"&gt;40&lt;/span&gt;    &lt;span class="mf"&gt;135&lt;/span&gt;    &lt;span class="mf"&gt;45.00&lt;/span&gt;
&lt;span class="mf"&gt;3&lt;/span&gt;    &lt;span class="k"&gt;Sneha&lt;/span&gt;     &lt;span class="mf"&gt;88&lt;/span&gt;       &lt;span class="mf"&gt;91&lt;/span&gt;       &lt;span class="mf"&gt;84&lt;/span&gt;    &lt;span class="mf"&gt;263&lt;/span&gt;    &lt;span class="mf"&gt;87.67&lt;/span&gt;
&lt;span class="mf"&gt;4&lt;/span&gt;   &lt;span class="k"&gt;Vikram&lt;/span&gt;     &lt;span class="mf"&gt;30&lt;/span&gt;       &lt;span class="mf"&gt;35&lt;/span&gt;       &lt;span class="mf"&gt;28&lt;/span&gt;     &lt;span class="mf"&gt;93&lt;/span&gt;    &lt;span class="mf"&gt;31.00&lt;/span&gt;
&lt;span class="mf"&gt;5&lt;/span&gt;    &lt;span class="k"&gt;Anita&lt;/span&gt;     &lt;span class="mf"&gt;67&lt;/span&gt;       &lt;span class="mf"&gt;72&lt;/span&gt;       &lt;span class="mf"&gt;70&lt;/span&gt;    &lt;span class="mf"&gt;209&lt;/span&gt;    &lt;span class="mf"&gt;69.67&lt;/span&gt;
&lt;span class="mf"&gt;6&lt;/span&gt;    &lt;span class="k"&gt;Karan&lt;/span&gt;     &lt;span class="mf"&gt;55&lt;/span&gt;       &lt;span class="mf"&gt;60&lt;/span&gt;       &lt;span class="mf"&gt;58&lt;/span&gt;    &lt;span class="mf"&gt;173&lt;/span&gt;    &lt;span class="mf"&gt;57.67&lt;/span&gt;
&lt;span class="mf"&gt;7&lt;/span&gt;    &lt;span class="k"&gt;Meera&lt;/span&gt;     &lt;span class="mf"&gt;95&lt;/span&gt;       &lt;span class="mf"&gt;93&lt;/span&gt;       &lt;span class="mf"&gt;97&lt;/span&gt;    &lt;span class="mf"&gt;285&lt;/span&gt;    &lt;span class="mf"&gt;95.00&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;When you add two columns in pandas, it works row by row automatically. &lt;code&gt;df["maths"] + df["science"] + df["english"]&lt;/code&gt; adds the three marks for each student and returns a new Series. Assigning it to &lt;code&gt;df["total"]&lt;/code&gt; creates a new column.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;.round(2)&lt;/code&gt; keeps the average to two decimal places.&lt;/p&gt;

&lt;p&gt;Raw marks don't say much on their own. A total and an average turn three numbers into a single metric you can sort and compare.&lt;/p&gt;
&lt;h2&gt;
  
  
  When the rule defines the result
&lt;/h2&gt;

&lt;p&gt;Define "Pass" as: &lt;strong&gt;40 or more in every subject.&lt;/strong&gt;&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;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maths&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;science&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;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;average&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;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;      &lt;span class="k"&gt;name&lt;/span&gt;  &lt;span class="k"&gt;average&lt;/span&gt; &lt;span class="k"&gt;result&lt;/span&gt;
&lt;span class="mf"&gt;0&lt;/span&gt;    &lt;span class="k"&gt;Arjun&lt;/span&gt;    &lt;span class="mf"&gt;78.33&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;1&lt;/span&gt;    &lt;span class="k"&gt;Priya&lt;/span&gt;    &lt;span class="mf"&gt;91.67&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;2&lt;/span&gt;    &lt;span class="k"&gt;Rahul&lt;/span&gt;    &lt;span class="mf"&gt;45.00&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;3&lt;/span&gt;    &lt;span class="k"&gt;Sneha&lt;/span&gt;    &lt;span class="mf"&gt;87.67&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;4&lt;/span&gt;   &lt;span class="k"&gt;Vikram&lt;/span&gt;    &lt;span class="mf"&gt;31.00&lt;/span&gt;   &lt;span class="k"&gt;Fail&lt;/span&gt;
&lt;span class="mf"&gt;5&lt;/span&gt;    &lt;span class="k"&gt;Anita&lt;/span&gt;    &lt;span class="mf"&gt;69.67&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;6&lt;/span&gt;    &lt;span class="k"&gt;Karan&lt;/span&gt;    &lt;span class="mf"&gt;57.67&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;span class="mf"&gt;7&lt;/span&gt;    &lt;span class="k"&gt;Meera&lt;/span&gt;    &lt;span class="mf"&gt;95.00&lt;/span&gt;   &lt;span class="k"&gt;Pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Line by line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;df[["maths", "science", "english"]]&lt;/code&gt; selects only the three subject columns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.apply(..., axis=1)&lt;/code&gt; runs a function on each row. &lt;code&gt;axis=1&lt;/code&gt; means row-wise.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lambda row: ...&lt;/code&gt; is a small anonymous function. &lt;code&gt;row&lt;/code&gt; is one student's three marks.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;all(row &amp;gt;= 40)&lt;/code&gt; returns &lt;code&gt;True&lt;/code&gt; if every mark is 40 or more.&lt;/li&gt;
&lt;li&gt;The ternary &lt;code&gt;"Pass" if ... else "Fail"&lt;/code&gt; returns the label.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On this dataset, "every subject ≥ 40" and "average ≥ 40" happen to give the same answer. That's luck. A student with 95, 95, and 20 would pass on average but fail my rule.&lt;/p&gt;

&lt;p&gt;The definition of "pass" is a decision. I made it before writing any code, and it shaped every result after.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the data showed
&lt;/h2&gt;

&lt;p&gt;Looking at the finished table:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topper:&lt;/strong&gt; Meera, average 95.00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class average:&lt;/strong&gt; 69.5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only failure:&lt;/strong&gt; Vikram, average 31.00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Borderline:&lt;/strong&gt; Rahul scrapes through at 45.00&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Eight rows is small, but the shape of the class is already visible. One clear topper, one clear failure, and a wide middle.&lt;/p&gt;
&lt;h2&gt;
  
  
  Visualizing with matplotlib
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;matplotlib.pyplot&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;

&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;average&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;steelblue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average Marks per Student&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;xticks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rotation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;savefig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outputs/average_marks.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;plt.bar()&lt;/code&gt; draws the bars, &lt;code&gt;xticks(rotation=45)&lt;/code&gt; stops the names from overlapping, and &lt;code&gt;savefig()&lt;/code&gt; writes the chart to a file. Meera's bar is the tallest. Vikram's is the shortest. That's obvious at a glance, much faster than reading the table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpukl90iqciga59np0sc5.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%2Fpukl90iqciga59np0sc5.png" alt="Average Marks Chart" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Making it usable by anyone
&lt;/h2&gt;

&lt;p&gt;The script works. But only I can run it, only on my laptop, only with Python installed.&lt;/p&gt;

&lt;p&gt;Streamlit turns a Python script into a web app. Here's the full &lt;code&gt;app.py&lt;/code&gt;:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;streamlit&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;st&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pandas&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Student Marks Analysis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/marks.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maths&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;science&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;average&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;maths&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;science&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;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Pass&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fail&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;axis&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dataframe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subheader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Average Marks Chart&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bar_chart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;average&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;st.title()&lt;/code&gt; adds the heading.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;st.dataframe(df)&lt;/code&gt; renders the DataFrame as an interactive table — users can sort columns by clicking headers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;st.bar_chart(...)&lt;/code&gt; renders the chart in the browser. &lt;code&gt;set_index("name")&lt;/code&gt; makes student names the X-axis labels.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pandas logic is identical to the script. Streamlit just converts the output into HTML.&lt;/p&gt;

&lt;p&gt;Running it locally:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python &lt;span class="nt"&gt;-m&lt;/span&gt; streamlit run app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;I had to use &lt;code&gt;python -m streamlit&lt;/code&gt; instead of just &lt;code&gt;streamlit&lt;/code&gt;. I'd installed it correctly, but Windows didn't know where the executable was, so it kept saying "not recognized." Running it as a Python module sidesteps the PATH issue.&lt;/p&gt;
&lt;h2&gt;
  
  
  Deploying to the cloud
&lt;/h2&gt;

&lt;p&gt;Streamlit Community Cloud is free and connects directly to GitHub. Sign in, pick the repo, select &lt;code&gt;app.py&lt;/code&gt;, click Deploy. It installs dependencies from &lt;code&gt;requirements.txt&lt;/code&gt; and gives you a public URL in about two minutes.&lt;/p&gt;

&lt;p&gt;One thing went wrong. I'd edited the README on GitHub's website while also editing it locally, so &lt;code&gt;git push&lt;/code&gt; was rejected — the remote had changes I didn't have. I fixed it with:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull origin main
git push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Git opened Vim to write a merge message. I'd never seen Vim before and had no idea how to get out. Took me a few minutes of searching to find that &lt;code&gt;:wq&lt;/code&gt; saves and quits. Small thing, but it's the part I remember most clearly.&lt;/p&gt;
&lt;h2&gt;
  
  
  Project structure
&lt;/h2&gt;

&lt;p&gt;By the end, the folders looked like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;student-marks-analysis/
├── data/
│   └── marks.csv
├── outputs/
│   └── average_marks.png
├── src/
│   └── analysis.py
├── app.py
├── requirements.txt
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Data in &lt;code&gt;data/&lt;/code&gt;, scripts in &lt;code&gt;src/&lt;/code&gt;, generated files in &lt;code&gt;outputs/&lt;/code&gt;, the web app at the top level. When someone opens the repo, they immediately know what's where.&lt;/p&gt;

&lt;p&gt;Dependencies:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;pandas matplotlib streamlit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  What I'd improve
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a real dataset.&lt;/strong&gt; 8 rows with no missing values, no typos, no duplicates. Real data is messier, and cleaning is where half the work lives.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add filters.&lt;/strong&gt; A &lt;code&gt;st.selectbox()&lt;/code&gt; for subjects or a slider for marks would make the app actually interactive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;More charts.&lt;/strong&gt; Subject-wise comparison, or a trend over time if the data had dates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't flaws. They're the next things to learn.&lt;/p&gt;
&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;The pipeline, end to end:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Load&lt;/strong&gt; the CSV with pandas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Transform&lt;/strong&gt; by creating columns and applying rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visualize&lt;/strong&gt; so patterns become obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ship&lt;/strong&gt; as a web app and deploy it.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most tutorials cover steps 1 and 2.&lt;br&gt;
Steps 3 and 4 are where a script becomes something other people can actually use.&lt;/p&gt;

&lt;p&gt;It's a small project. But now I get how the pieces connect.&lt;/p&gt;
&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://student-marks-analysis-dashboard.streamlit.app/" rel="noopener noreferrer"&gt;Live dashboard&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub Repo
&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/mauryasagar" rel="noopener noreferrer"&gt;
        mauryasagar
      &lt;/a&gt; / &lt;a href="https://github.com/mauryasagar/student-marks-analysis" rel="noopener noreferrer"&gt;
        student-marks-analysis
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Interactive data analysis dashboard for student marks, built with Python, pandas, matplotlib, and Streamlit. Deployed live via Streamlit Cloud.
    &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;Student Marks Analysis&lt;/h1&gt;
&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Problem&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;Analyze a dataset of student marks to identify the topper, calculate the class average, determine pass/fail results, and visualize performance.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Dataset&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;A small dataset of 8 students with marks in Maths, Science, and English.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Key Findings&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Topper:&lt;/strong&gt; Meera with an average of 95.00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Class Average:&lt;/strong&gt; 69.5&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failed Students:&lt;/strong&gt; 1 (Vikram)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass Rule:&lt;/strong&gt; A student passes only if they score 40 or more in every subject&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Chart&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/mauryasagar/student-marks-analysis/outputs/average_marks.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmauryasagar%2Fstudent-marks-analysis%2FHEAD%2Foutputs%2Faverage_marks.png" width="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Live Dashboard&lt;/h2&gt;

&lt;/div&gt;

&lt;p&gt;&lt;a href="https://student-marks-analysis-dashboard.streamlit.app" rel="nofollow noopener noreferrer"&gt;View Live Dashboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run it locally:&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python -m streamlit run app.py&lt;/pre&gt;

&lt;/div&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Tools Used&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Python&lt;/li&gt;
&lt;li&gt;pandas (data loading and analysis)&lt;/li&gt;
&lt;li&gt;matplotlib (visualization)&lt;/li&gt;
&lt;li&gt;Streamlit (interactive dashboard)&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project Structure&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;student-marks-analysis/
├── data/
│   └── marks.csv          # Raw student marks data
├── outputs/
│   └── average_marks.png  # Generated bar chart
├── src/
│   └── analysis.py        # Analysis script
├── app.py                 # Streamlit dashboard
├── .gitignore
├── LICENSE
├── README.md
└── requirements.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How to Run&lt;/h2&gt;

&lt;/div&gt;


&lt;ol&gt;

&lt;li&gt;

&lt;p&gt;Install dependencies:&lt;/p&gt;

&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;pip install pandas matplotlib streamlit&lt;/pre&gt;

&lt;/div&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;…&lt;/p&gt;&lt;/li&gt;

&lt;/ol&gt;&lt;/div&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/mauryasagar/student-marks-analysis" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


</description>
      <category>python</category>
      <category>analytics</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Three Days, One Tool, and Every Bug Was Hiding Another One — MindMap Debugger, the Full Story</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:52:25 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/three-days-one-tool-and-every-bug-was-hiding-another-one-mindmap-debugger-the-full-story-2jhh</link>
      <guid>https://dev.to/sagarmaurya/three-days-one-tool-and-every-bug-was-hiding-another-one-mindmap-debugger-the-full-story-2jhh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;em&gt;Building MindMap Debugger for AWS First Commit by #WeMakeDevs — from the first working extraction to the bugs I found the night before submission.&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fntqwvxcou3iifl70x776.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%2Fntqwvxcou3iifl70x776.png" alt="MindMap Debugger landing page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;MindMap Debugger takes any argument or transcript you paste in, and finds two things in it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Contradictions&lt;/strong&gt; — two things that were said which can't both be true.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circular reasoning&lt;/strong&gt; — a chain of claims that loops back and ends up using itself as its own proof.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The idea came from a simple observation: reading carefully doesn't scale. A meeting transcript says the deadline is fixed. Ten minutes later someone says there isn't time to hit it. Nobody flags it because everyone's listening for their own part, not tracking the whole argument. The same thing happens in design docs, contracts, long debates — claims pile up faster than any one reader can cross-check them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 1 — Getting something to work at all
&lt;/h2&gt;

&lt;p&gt;I started with the basic shape: paste text in, call an LLM to extract "propositions" (claims) and "relations" (how those claims connect — supports, depends_on, contradicts), then run detection logic over the relations to find contradictions and cycles.&lt;/p&gt;

&lt;p&gt;For the model backend, I used Groq — free, no card required, which mattered because I don't have a debit or credit card and UPI isn't accepted by AWS account signup. That also meant pivoting from the "Ship It" track (needs a real AWS account) to "Build It" (no AWS account required). To still satisfy the "use AWS-native tooling" requirement, I wrapped the Groq call in AWS's Strands Agents SDK — Strands is model-agnostic, so it happily talks to Groq through an OpenAI-compatible endpoint even though Groq isn't Bedrock.&lt;/p&gt;

&lt;p&gt;The first real bug showed up almost immediately: I was using &lt;code&gt;gpt-oss-120b&lt;/code&gt;, a reasoning model, and its internal chain-of-thought was leaking straight into the output instead of staying hidden. Instead of clean JSON, I'd get a wall of the model "thinking out loud" followed by the actual answer buried somewhere inside.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc6txl9l1l169v9wdyd26.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%2Fc6txl9l1l169v9wdyd26.png" alt="Terminal showing the model's raw chain-of-thought leaking into the output instead of clean JSON" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The fix was passing &lt;code&gt;reasoning_format=hidden&lt;/code&gt; and &lt;code&gt;reasoning_effort=high&lt;/code&gt; through Strands' &lt;code&gt;extra_body&lt;/code&gt; parameter — plain &lt;code&gt;response_format&lt;/code&gt; alone and a top-level &lt;code&gt;reasoning_format&lt;/code&gt; both failed silently. &lt;code&gt;extra_body&lt;/code&gt; was the only path that actually worked.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7lrr8m49h999uxmo65em.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%2F7lrr8m49h999uxmo65em.png" alt="Terminal showing clean, correctly-structured JSON output after the fix" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the end of Day 1, the pipeline could take a paragraph, extract claims, detect an obvious contradiction, and show it in a rough UI. It felt like a huge milestone. It was also, I'd learn the next day, held together with tape.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 2 — Every bug I fixed was hiding another one
&lt;/h2&gt;

&lt;p&gt;Day 2 started with a simple question: does this work reliably, or did I just get lucky once?&lt;/p&gt;

&lt;p&gt;I ran the same input five times in a row. The number of findings bounced between 2 and 6. Same text, same prompt, same everything — wildly different output. That's when I learned Groq's serving of &lt;code&gt;gpt-oss-120b&lt;/code&gt; is genuinely non-deterministic across calls, even with low temperature. A single extraction call just isn't trustworthy on its own.&lt;/p&gt;

&lt;p&gt;The fix was &lt;strong&gt;consensus extraction&lt;/strong&gt;: run the same extraction three times, then merge the results. In theory this smooths out the noise — if two of three runs agree on a relation, that's more trustworthy than any single run.&lt;/p&gt;

&lt;p&gt;In practice, merging three noisy outputs together doesn't just cancel the noise. It creates new problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug: paraphrased duplicates weren't merging.&lt;/strong&gt; The model would phrase the exact same claim slightly differently between calls — "the deadline is fixed" in one run, "the deadline cannot move" in another. My merge logic was doing exact text matching, so these became two separate propositions instead of one. I added a similarity check: if two propositions shared enough words, treat them as the same claim and merge them, keeping the higher-confidence version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug: cycle detection had a blind spot.&lt;/strong&gt; My &lt;code&gt;find_cycles()&lt;/code&gt; function only built graph edges from &lt;code&gt;depends_on&lt;/code&gt; relations. But a circular reasoning chain in the wild doesn't neatly alternate through one relation type — a loop might go &lt;code&gt;depends_on → supports → depends_on&lt;/code&gt; back to the start. Because I was only looking at one edge type, I was missing real cycles that used a mix. Fixed by unioning both relation types when building the graph for cycle detection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bug: duplicate findings.&lt;/strong&gt; The cycle-finding DFS could rediscover the exact same cycle starting from a different node in the loop — so a single 3-claim circular chain would get reported three times, once per starting point. Added a dedup step that collapses cycles with identical node sets, regardless of which node the search happened to start from.&lt;/p&gt;

&lt;p&gt;By the end of Day 2, I had what looked like a solid pipeline: consensus extraction, similarity-based proposition merging, cross-relation-type cycle detection, deduped findings, and a Cedar policy gate on top (more on that below). I tested it, it looked right, and I moved on to building out the UI.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7db50wj2zy79w5qers57.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%2F7db50wj2zy79w5qers57.png" alt="Terminal showing the final pipeline run with consensus extraction and deduplication working — clean findings, no duplicates" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I should have kept testing longer.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 3 — The bugs the Day 2 fixes were hiding
&lt;/h2&gt;

&lt;p&gt;This is the part of the story I almost didn't catch, and it's the most important part.&lt;/p&gt;

&lt;p&gt;The pipeline "worked." But something felt off when testing edge cases, so instead of just looking at the UI, I went back to reading the raw console logs from pipeline.py — the same habit that had caught the Day 2 bugs.&lt;/p&gt;

&lt;p&gt;The pipeline "worked." But something felt off when testing edge cases, so instead of just looking at the UI, the session went back to reading the raw console logs from &lt;code&gt;pipeline.py&lt;/code&gt; — the same habit that had caught the Day 2 bugs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #1: the similarity merge was too aggressive
&lt;/h3&gt;

&lt;p&gt;Take these two propositions from a witness-testimony test case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;P1: "the witness is reliable because their testimony is consistent"
P2: "their testimony is consistent because they are telling the truth"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are about &lt;strong&gt;different things&lt;/strong&gt; — P1 is about the witness's reliability, P2 is about their truthfulness — but they share a lot of common English scaffolding: "the," "is," "because," "their," "testimony," "consistent."&lt;/p&gt;

&lt;p&gt;The old similarity formula was &lt;strong&gt;overlap divided by the smaller set's size&lt;/strong&gt;. With 6 shared words and a smaller set of 8 words, that's &lt;code&gt;6/8 = 0.75&lt;/code&gt; — well above the 0.7 merge threshold. The two distinct claims got collapsed into one.&lt;/p&gt;

&lt;p&gt;The fix was switching to &lt;strong&gt;Jaccard similarity&lt;/strong&gt; — overlap divided by the &lt;em&gt;union&lt;/em&gt; of both sets, not just the smaller one:&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;overlap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words_a&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;words_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;union&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;words_a&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;words_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;overlap&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;union&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same two propositions: &lt;code&gt;6 / 12 = 0.5&lt;/code&gt; — below threshold, correctly kept separate. The union denominator punishes cases where two sentences only look similar because they both use common connector words. Genuine paraphrases (same core claim, same key content words) still merge fine; superficially similar-but-different claims no longer do.&lt;/p&gt;

&lt;p&gt;After this fix, a 4-sentence test input that was collapsing into 3 propositions correctly produced 4. The fake circular finding (a nonsensical two-node "reliable → reliable" self-loop) disappeared, and the real 3-claim circular chain in that same text — reliable → consistent → truthful → reliable — was still correctly detected. The fix didn't suppress findings. It made them honest.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bug #2: the model was directionally inconsistent, and merging fabricated cycles
&lt;/h3&gt;

&lt;p&gt;This one was sneakier. Testing a "bridge maintenance" sample designed to have exactly one contradiction and zero circular chains, the tool reported one contradiction — correct — plus &lt;strong&gt;four fake circular chains&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The console logs showed why:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Run 1: P4 depends_on P5, P5 depends_on P6
Run 2: P5 depends_on P4, P6 depends_on P5
Run 3: P4 depends_on P5, P5 depends_on P6
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model was expressing the &lt;em&gt;same underlying relationship&lt;/em&gt; with the arrow pointing in different directions across runs. "Regular maintenance requires a budget" got labeled &lt;code&gt;maintenance depends_on budget&lt;/code&gt; in two runs and &lt;code&gt;budget depends_on maintenance&lt;/code&gt; in the third. Individually, none of the three runs contained a cycle — each one's relations were internally consistent. But the merge step was &lt;strong&gt;unioning all edges from all three runs&lt;/strong&gt;, and once you union &lt;code&gt;P4→P5&lt;/code&gt; from one run with &lt;code&gt;P5→P4&lt;/code&gt; from another, you've created a 2-node loop that never existed in any single reasoning chain. The cycle detector, doing its job correctly, found that loop and flagged it.&lt;/p&gt;

&lt;p&gt;The fix: after merging relations, prune reverse-direction &lt;code&gt;depends_on&lt;/code&gt; pairs, keeping only the higher-confidence direction:&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="nf"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;depends_on&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;reverse&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;from_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;from_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;from_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rel_type&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;all_rels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details mattered here. First, this only applies to &lt;code&gt;depends_on&lt;/code&gt; — a bidirectional &lt;code&gt;supports&lt;/code&gt; relation ("A supports B and B supports A") is genuinely circular reasoning by definition, so those are left alone. Second, when both directions exist, keep whichever one the model was more confident about, rather than just picking one arbitrarily.&lt;/p&gt;

&lt;p&gt;After this fix, the bridge sample dropped from 1 contradiction + 4 fake cycles to exactly &lt;strong&gt;1 contradiction, 0 circular&lt;/strong&gt; — correct. And the witness sample still correctly reported its real 3-claim cycle, unaffected, because that cycle used consistent-direction edges across all three runs.&lt;/p&gt;

&lt;p&gt;That's the actual engineering payoff of this whole project, more than the UI or the graph: a merge layer that's honest about disagreeing with itself instead of quietly averaging its way into fabricated findings.&lt;/p&gt;

&lt;p&gt;Here's what the fully-fixed pipeline looks like live in the browser, on the product-launch sample from the top of this post — real contradictions and real circular chains, no fakes mixed in:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7etily6rdl11qh5dq1sj.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%2F7etily6rdl11qh5dq1sj.png" alt="Live dashboard showing correct contradiction findings" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsc7wck7mc9n8w49tzp1j.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%2Fsc7wck7mc9n8w49tzp1j.png" alt="Live dashboard showing correct circular reasoning findings" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The smaller bug: a white flash on page load
&lt;/h3&gt;

&lt;p&gt;Separately, on a hard refresh, the hero section's 3D constellation would flash as a plain white box for a fraction of a second before rendering. Browsers paint a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element with a default white backing store until WebGL actually attaches and draws to it — and on a cold load, there's a small race between the DOM painting and the JS clearing the canvas.&lt;/p&gt;

&lt;p&gt;The fix was to keep the canvas invisible until the very first real frame has rendered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;canvas&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hero-canvas"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"opacity:0;background:transparent;transition:opacity .5s ease"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/canvas&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;requestAnimationFrame&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scene&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;canvasEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;opacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;animateHero&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;Small fix, but it's the kind of "everyone who's touched WebGL knows this" gotcha that never gets written down anywhere obvious.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I actually learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;LLMs aren't just noisy in what they say — they're noisy in which direction they say it.&lt;/strong&gt; Two runs can both be individually correct and still disagree in a way that, if merged carelessly, manufactures a finding that never existed in either run alone. If you're combining multiple LLM calls to reduce noise, you have to explicitly check for this, because naive merging can create errors that are more confident-looking than the noise you were trying to remove.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Similarity metrics have real semantics, not just a threshold to tune.&lt;/strong&gt; Overlap-over-smallest-set measures "how much of the smaller sentence is contained in the larger one." Jaccard similarity measures "how similar are these two sets overall, penalizing everything that's different." For deduplication, you almost always want the second one — the first one silently favors merging short, generic-sounding text with anything that shares its common words.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The UI shows correct-looking output" is not the same as "the output is correct."&lt;/strong&gt; The pipeline ran successfully the entire time on Day 2 and looked done. It just quietly produced wrong answers on certain inputs, and the only way to catch that was going back to raw console logs and deliberately trying to break it with adversarial test cases, instead of trusting that a clean-looking UI meant a clean-working pipeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design is mostly subtraction.&lt;/strong&gt; Not a pipeline lesson, but a real one from rebuilding the UI multiple times: every glow effect, every drop-shadow, every "make it look like the hero section" pass started too strong and ended up dialed back. The final cursor-following border glow on the dashboard cards is a 3px line with one radial gradient. Earlier versions had two stacked shadows and a masked pseudo-element and looked like a neon sign.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the tool does, end to end
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Paste text with claims in it.&lt;/li&gt;
&lt;li&gt;Extraction runs &lt;strong&gt;three times&lt;/strong&gt; through Groq via the Strands Agents SDK.&lt;/li&gt;
&lt;li&gt;Propositions are merged by &lt;strong&gt;Jaccard similarity&lt;/strong&gt; — genuine paraphrases collapse, distinct claims survive.&lt;/li&gt;
&lt;li&gt;Relations are unioned across runs, deduped by confidence, and &lt;strong&gt;reverse-direction &lt;code&gt;depends_on&lt;/code&gt; pairs are pruned&lt;/strong&gt; so the merge can't fabricate cycles.&lt;/li&gt;
&lt;li&gt;Contradictions are found by scanning &lt;code&gt;contradicts&lt;/code&gt; edges.&lt;/li&gt;
&lt;li&gt;Circular reasoning is found via depth-first search over &lt;code&gt;depends_on&lt;/code&gt; and &lt;code&gt;supports&lt;/code&gt; edges, with duplicate cycles collapsed.&lt;/li&gt;
&lt;li&gt;Everything is gated through a &lt;strong&gt;Cedar policy&lt;/strong&gt;: contradictions always surface regardless of confidence; circular findings only surface above 0.6 confidence.&lt;/li&gt;
&lt;li&gt;Findings render as a 3D relation graph and a plain-language summary — no jargon, just "these two things can't both be true" and "this loops back on itself."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Folztomras4w0gc6k0h9s.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%2Folztomras4w0gc6k0h9s.png" alt="Terminal smoke test of the Cedar policy gate — PERMIT and DENY decisions on sample findings, filtering out a weak circular finding" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Repo and stack
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/mauryasagar/mindmap-debugger" rel="noopener noreferrer"&gt;github.com/mauryasagar/mindmap-debugger&lt;/a&gt; — MIT licensed, runs locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;flask strands-agents openai
python app.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;code&gt;http://localhost:5000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Track:&lt;/strong&gt; WeMakeDevs × AWS First Commit, Build It&lt;br&gt;
&lt;strong&gt;Stack:&lt;/strong&gt; Strands Agents SDK + Cedar (the required AWS-native tools), Groq &lt;code&gt;gpt-oss-120b&lt;/code&gt;, Flask, Three.js&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Strands and Cedar, specifically
&lt;/h2&gt;

&lt;p&gt;This was built for the Build It track, which asks for AWS-native tooling — but I didn't want to bolt on an AWS SDK just to check a box, so here's the actual reasoning for each one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Strands Agents SDK&lt;/strong&gt; is the only thing standing between this project and Bedrock. I don't have a card to set up billing on a real AWS account, so Bedrock itself was off the table. Strands is model-agnostic by design, which meant I could point it at Groq's OpenAI-compatible endpoint instead and still get a real, working AWS agent framework doing the actual work of calling the model, handling the response, and giving me a structured place to add tools later if the project grows. It's not a token integration — every extraction call in the pipeline runs through it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cedar&lt;/strong&gt; does the actual gating. Contradictions always surface regardless of confidence, because a false contradiction is still worth a human's attention — better to over-flag than hide something real. Circular reasoning only surfaces above 0.6 confidence, because a weak circular claim is more often noise than signal. That's a real policy decision, not a default, and Cedar is what makes it declarative and auditable instead of an &lt;code&gt;if&lt;/code&gt; statement buried in &lt;code&gt;detect.py&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Between the two, they cover the actual shape of what "Build It" is asking for: a real agent framework driving the reasoning, and a real policy layer deciding what a user sees.&lt;/p&gt;




&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/mauryasagar/mindmap-debugger" rel="noopener noreferrer"&gt;github.com/mauryasagar/mindmap-debugger&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also read:&lt;br&gt;
Day 1 blog (Dev.to): &lt;a href="https://dev.to/sagarmaurya/i-hit-my-first-contradiction-before-building-one-for-the-first-commit-hackathon-k2k"&gt;I hit my first contradiction before building one&lt;/a&gt;&lt;br&gt;
Day 2 blog (Dev.to): &lt;a href="https://dev.to/sagarmaurya/every-bug-i-fixed-today-was-hiding-another-one-22df"&gt;Every bug I fixed today was hiding another one&lt;/a&gt;&lt;br&gt;
Day 3 blog (&lt;a href="https://builder.aws.com/post/3JbLtazyebHh3fULt1zvuE5dPoC_p/mindmap-debugger-three-days-one-tool-every-bug-hiding-another" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;)&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Built with &lt;strong&gt;Claude&lt;/strong&gt; and &lt;strong&gt;DeepSeek&lt;/strong&gt; as coding partners.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I set out to build something that catches contradictions. Turns out the biggest one was mine: thinking it was finished.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>wemakedevs</category>
      <category>aws</category>
      <category>hackathon</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>Every Bug I Fixed Today Was Hiding Another One</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sat, 19 Sep 2026 15:18:19 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/every-bug-i-fixed-today-was-hiding-another-one-22df</link>
      <guid>https://dev.to/sagarmaurya/every-bug-i-fixed-today-was-hiding-another-one-22df</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;em&gt;Building MindMap Debugger for AWS First Commit — Day 2&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;On &lt;a href="https://dev.to/sagarmaurya/i-hit-my-first-contradiction-before-building-one-for-the-first-commit-hackathon-k2k"&gt;Day 1&lt;/a&gt;, I got the first working version running — paste in an argument, and the tool reads through it and points out contradictions, powered by Groq's API. It worked. And in the process of testing it, I ran straight into a contradiction of my own, before I'd even finished building the thing meant to catch them.&lt;/p&gt;

&lt;p&gt;Yesterday ended with a working extraction pipeline and a deceptively simple plan for today: wrap it in Strands Agents SDK, build the Cedar policy gate, wire it all together, done by evening.&lt;/p&gt;

&lt;p&gt;That plan lasted about twenty minutes — turns out the tool wasn't the only thing finding contradictions today. My own assumptions kept getting caught out too.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #1: The AI started thinking out loud inside my JSON
&lt;/h2&gt;

&lt;p&gt;First task — swap the raw Groq client for Strands' &lt;code&gt;Agent&lt;/code&gt; wrapper, since Build It's "use AWS-native tooling" requirement means the model call itself needs to route through Strands, not straight to Groq. The SDK was already installed. My extraction logic already worked. This should have been a clean drop-in replacement.&lt;/p&gt;

&lt;p&gt;Instead, the model started returning this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"We need to extract propositions. Two statements: 'SQLite is fast enough for our needs.' and 'the system must survive process restarts...' Could split second into two claims? ... Now check pairwise contradictions. P1: SQLite is fast enough for our needs. No contradiction. P2: System must survive process restarts. No contradiction..."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;— followed, eventually, buried at the very end of several paragraphs of visible internal monologue, by the actual JSON I'd asked for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsnrzuyrcvt5fezz9srhs.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%2Fsnrzuyrcvt5fezz9srhs.png" alt="Strands reasoning leak bug" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What was actually happening:&lt;/strong&gt; &lt;code&gt;openai/gpt-oss-120b&lt;/code&gt; is a reasoning model. It always narrates its thinking process, and nothing in Strands' default parameters was telling it to suppress that at the API level. My JSON-extraction fallback could technically still dig the &lt;code&gt;{...}&lt;/code&gt; block out of the noise, but it was one weird sentence away from breaking completely.&lt;/p&gt;

&lt;p&gt;Fixing it took three attempts, and two of them were wrong in instructive ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Adding &lt;code&gt;response_format: {"type": "json_object"}&lt;/code&gt; alone&lt;/strong&gt; — no change. Still leaking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adding a top-level &lt;code&gt;reasoning_format: "hidden"&lt;/code&gt; parameter&lt;/strong&gt; — this didn't just fail quietly, it crashed outright: &lt;code&gt;AsyncCompletions.create() got an unexpected keyword argument 'reasoning_format'&lt;/code&gt;. Strands' OpenAI-compatible wrapper validates params against a fixed list and rejects anything it doesn't recognize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The actual fix&lt;/strong&gt; — nest it inside &lt;code&gt;extra_body&lt;/code&gt; instead, so Strands passes it through as a raw field straight to Groq's API rather than checking it against OpenAI's own parameter list:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;params&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;temperature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_format&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;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;json_object&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;extra_body&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;reasoning_format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hidden&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean JSON, first try, no rambling. First win of the day.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fh4s9djmncqbo7w3a4wju.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%2Fh4s9djmncqbo7w3a4wju.png" alt="Strands fixed, clean JSON output" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #2: The contradiction that refused to be found
&lt;/h2&gt;

&lt;p&gt;Fixing the leak didn't fix the actual reasoning underneath it. My test sentence — &lt;em&gt;"SQLite is fast enough for our needs. But the system must survive process restarts, and in-memory data does not survive restarts."&lt;/em&gt; — contains a genuine contradiction. Using SQLite in-memory conflicts with surviving a restart. But nothing in the wording says so directly; you have to connect three separate facts to see it.&lt;/p&gt;

&lt;p&gt;The model kept extracting all three propositions perfectly. It never once flagged the conflict.&lt;/p&gt;

&lt;p&gt;I tried two fixes, and both taught me something by failing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Adding a more explicit prompt rule&lt;/strong&gt;, spelling out that a "solution" claim can contradict a "requirement" claim even with zero shared wording. Result: &lt;em&gt;worse&lt;/em&gt;. The model went from finding one weak relation to finding none at all — the extra instruction diluted the one rule that actually mattered instead of reinforcing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cranking &lt;code&gt;reasoning_effort&lt;/code&gt; to &lt;code&gt;"high"&lt;/code&gt;&lt;/strong&gt;, forcing more internal reasoning before answering. Result: no change whatsoever. Same weak relation, same missing contradiction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part that actually surprised me: I'd assumed harder reasoning settings would obviously help with harder reasoning problems. They didn't move the needle at all. So I stopped guessing and looked up whether this was a known thing — and it is. Multi-hop implicit contradictions, where the conflicting facts are spread across separate premises with no shared vocabulary connecting them, are a documented, genuinely hard case for large language models in general. It's not unique to this model, this SDK, or my prompt. It's a real limitation of how these models reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The decision that mattered more than any prompt tweak:&lt;/strong&gt; stop fighting one adversarial sentence. Accept it as an honest, documented limitation instead of a bug to keep chasing, and pick clearer demo inputs where the model's actual strength — catching direct, explicit contradictions — gets to shine. Trying to force a model past its genuine reasoning limits, on a deadline, is a worse use of time than being honest about where those limits are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #3: Rebuilding a policy that no longer existed
&lt;/h2&gt;

&lt;p&gt;Time to move to Cedar — the AWS-native policy engine at the heart of the Build It track's story. Except the actual rules I'd designed for it — which findings get shown, under what conditions — had lived entirely in a conversation that had already expired. No notes. Nothing saved. The design was just gone.&lt;/p&gt;

&lt;p&gt;Rebuilding it meant re-deciding, from first principles, what the policy should actually reward. The real question wasn't technical — it was &lt;strong&gt;what earns trust in front of judges&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct contradictions: always surfaced&lt;/strong&gt;, no matter the confidence score. Missing a genuine contradiction is worse than showing one that's slightly uncertain — and contradictions are the entire point of the tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Circular reasoning: only surfaced above 0.6 confidence.&lt;/strong&gt; A shaky contradiction is still useful context for a reader. A shaky circularity claim just looks like the tool crying wolf. Being selective here signals judgment, not just pattern-matching.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That became the actual Cedar policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permit (
    principal == Service::"Detector",
    action == Action::"surface_finding",
    resource
)
when {
    resource.type == "direct_contradiction"
};

permit (
    principal == Service::"Detector",
    action == Action::"surface_finding",
    resource
)
when {
    resource.type == "circular" &amp;amp;&amp;amp;
    resource.confidence &amp;gt;= 0.6
};
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With a Python enforcement layer (&lt;code&gt;cedar_gate.py&lt;/code&gt;) mirroring the same logic, since standing up the full Cedar authorization engine was heavier setup than the timeline allowed — but the actual policy spec above is the real, canonical rule set, honestly documented as such rather than hidden.&lt;/p&gt;

&lt;p&gt;First smoke test, using two contradictions (one strong, one deliberately weak) and two circular findings (one strong, one deliberately weak):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft4djz3mzmt2v60or6q1t.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%2Ft4djz3mzmt2v60or6q1t.png" alt="Cedar gate working — PERMIT/DENY breakdown" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three of four findings passed through. Both contradictions surfaced — even the weak one, exactly per policy. The strong circular finding passed. The weak one got correctly denied. Real, demonstrable policy logic — not "show everything the model said and hope it looks intentional."&lt;/p&gt;




&lt;h2&gt;
  
  
  Bug #4: The wall at the end of the day
&lt;/h2&gt;

&lt;p&gt;Wiring &lt;code&gt;apply_gate()&lt;/code&gt; into the pipeline was one import and one function call — the smallest change of the day. Then I ran the full pipeline against real input for the very first time: a longer, five-sentence argument about launching a product under conflicting pressure, instead of my short toy test sentence.&lt;/p&gt;

&lt;p&gt;It broke immediately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;openai.APIError: Failed to validate JSON. Please adjust your prompt.
See 'failed_generation' for more details.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;failed_generation&lt;/code&gt;, the field that's supposed to show you the broken output, came back completely empty. No malformed JSON to inspect. No partial text. Just a flat wall.&lt;/p&gt;

&lt;p&gt;I raised &lt;code&gt;max_tokens&lt;/code&gt; to rule out a truncation issue — no change, which at least told me it wasn't simply running out of room. I added debug logging to print the raw error body directly from the API response, ready to finally see what Groq was actually rejecting.&lt;/p&gt;

&lt;p&gt;And that's where the day ended — mid-investigation, with the fix still unknown.&lt;/p&gt;




&lt;h2&gt;
  
  
  The twist: it wasn't the bug I thought it was
&lt;/h2&gt;

&lt;p&gt;Picking the debugging back up, the actual cause turned out to have nothing to do with malformed JSON at all. It was a &lt;strong&gt;token budget death spiral&lt;/strong&gt;: &lt;code&gt;reasoning_effort: "high"&lt;/code&gt; combined with strict JSON mode was quietly consuming the model's &lt;em&gt;entire&lt;/em&gt; output budget on hidden internal reasoning for this longer, more tangled input — leaving zero tokens left to actually write the answer. Not truncation in the way I'd assumed; raising &lt;code&gt;max_tokens&lt;/code&gt; on its own hadn't touched it, because the problem wasn't the ceiling, it was what was eating the budget underneath it. The real fix was dropping reasoning effort to &lt;code&gt;"medium"&lt;/code&gt; and raising the token ceiling together, at the same time.&lt;/p&gt;

&lt;p&gt;Fixing that immediately uncovered a second, completely different bug hiding behind it: my circular-reasoning detector only ever looked for cycles built from one specific relation type. Real circular arguments, it turns out, don't always stay consistent — a claim can "depend on" another, which in turn "supports" the first, and that's just as circular as two matching relations, but my detector was blind to the mix. A one-line fix — treating both relation types as cycle-relevant — and circular reasoning started showing up for the first time all night.&lt;/p&gt;

&lt;p&gt;Fixing &lt;em&gt;that&lt;/em&gt; revealed a third bug: to make extraction more reliable (since a single pass sometimes missed things), I started running it multiple times and merging the results — a deliberate reliability upgrade, not a bug fix. But that upgrade introduced its own bug: the same circular reasoning chain occasionally got reported twice, because the underlying graph search could rediscover an identical cycle starting from two different entry points. Fixed with a dedup pass that collapses cycles representing the same set of claims into one.&lt;/p&gt;

&lt;p&gt;Three real, distinct root causes, each one hiding behind the last, like debugging nesting dolls. None of them were the bug I originally thought I was chasing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where it stands now
&lt;/h2&gt;

&lt;p&gt;Full pipeline, verified end-to-end: paste an argument in, get contradictions and circular reasoning chains out, filtered through real policy logic, rendered live in the browser. On one real test case, the system now reliably catches multiple genuine contradictions and several distinct circular reasoning chains — with zero duplicates.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fk1037grjps95s87fobv9.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%2Fk1037grjps95s87fobv9.png" alt="Final UI — contradictions and circular reasoning, deduped (part 1)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmx2l85oghqv87a0cakmc.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%2Fmx2l85oghqv87a0cakmc.png" alt="Final UI — contradictions and circular reasoning, deduped (part 2)" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0grfl1wr55w13s6k3kzn.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%2F0grfl1wr55w13s6k3kzn.png" alt="Final pipeline run — consensus and dedup proof" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frcguzzayb1mtxd1w6wha.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%2Frcguzzayb1mtxd1w6wha.png" alt="Cedar policy gate smoke test" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tech stack, as it stands:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; — core pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq API&lt;/strong&gt; — model backend, free tier&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strands Agents SDK&lt;/strong&gt; — AWS-native model wrapper&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cedar policy language&lt;/strong&gt; — real policy spec, enforced faithfully&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flask&lt;/strong&gt; — UI, confirmed working live&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Four bugs today. Four different root causes. Only one of them was where I first thought to look.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;&lt;em&gt;Building in public for AWS First Commit.&lt;/em&gt;&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>wemakedevs</category>
      <category>aws</category>
      <category>hackathon</category>
      <category>buildinpublic</category>
    </item>
    <item>
      <title>I Hit My First Contradiction Before Building One for the First Commit Hackathon</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Fri, 18 Sep 2026 15:21:10 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/i-hit-my-first-contradiction-before-building-one-for-the-first-commit-hackathon-k2k</link>
      <guid>https://dev.to/sagarmaurya/i-hit-my-first-contradiction-before-building-one-for-the-first-commit-hackathon-k2k</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Building MindMap Debugger for AWS First Commit — Day 1&lt;/em&gt;  &lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;Three days ago I landed on an idea: a contradiction-detector that reads through a set of claims and flags where they don't add up. Then First Commit — AWS's "Bharat Builds Tour", run by WeMakeDevs, gave me a reason to finally deploy something on AWS — something I'd never actually done before.&lt;/p&gt;

&lt;p&gt;I had 4 days. I had an idea. I had zero AWS experience deploying anything real.&lt;/p&gt;

&lt;p&gt;Here's everything that went right, wrong, and sideways on Day 1 — before I'd written a single working line of code. &lt;strong&gt;The first contradiction I found had nothing to do with the project.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #1: Figuring out what "built on AWS" actually means for my idea
&lt;/h2&gt;

&lt;p&gt;First Commit judges on something specific: &lt;strong&gt;"Built on AWS" is worth real points, non-negotiable.&lt;/strong&gt; It's not enough to have a good idea sitting behind an API call — AWS has to be doing real work in the architecture, or it reads as generic.&lt;/p&gt;

&lt;p&gt;So the first real decision of this hackathon wasn't "what do I build" — it was &lt;strong&gt;how do I architect this so AWS is actually load-bearing, not decorative.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where I landed:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MindMap Debugger&lt;/strong&gt; (contradiction detection) — maps naturally onto structured extraction and a UI-first demo, with room to bring in AWS-native pieces like Strands and Cedar as the pipeline matures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Lesson one, before any code: &lt;strong&gt;know exactly what the judges are scoring, and architect toward that from the first decision — not as an afterthought.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #2: I didn't have a card. AWS didn't care.
&lt;/h2&gt;

&lt;p&gt;This is the part nobody warns you about.&lt;/p&gt;

&lt;p&gt;I went to set up a real AWS account for the &lt;strong&gt;Ship It&lt;/strong&gt; track — the one with live deployment on AWS. I verified on AWS Builder Center first, no problem. Then I went to create the actual AWS account for real cloud deployment, and hit this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffg4i16jmiqoio37ul7po.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%2Ffg4i16jmiqoio37ul7po.png" alt="AWS sign-in error" width="799" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Turns out &lt;strong&gt;AWS Builder Center and an actual AWS account are two completely separate systems.&lt;/strong&gt; I didn't know that going in. Fair enough — I went to create a real account.&lt;/p&gt;

&lt;p&gt;Then I hit the actual wall:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsu10886i70oy7lvvzzbx.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%2Fsu10886i70oy7lvvzzbx.png" alt="AWS account payment step" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;AWS account signup requires a debit or credit card. I only had UPI, and that wasn't accepted at signup. And here's where &lt;strong&gt;I hit my first contradiction of the hackathon.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;So I pivoted to Build It&lt;/strong&gt; — the track that explicitly promises &lt;em&gt;"no AWS account, no card, no bill."&lt;/em&gt; It's fully real, fully eligible.&lt;/p&gt;

&lt;p&gt;Lesson two: &lt;strong&gt;check your hard constraints before you pick your architecture, not after.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Problem #3: If no AWS account, then no Bedrock. Then what?
&lt;/h2&gt;

&lt;p&gt;Build It's tool list includes Strands Agents SDK, Cedar, PartyRock, SAM CLI + LocalStack, OpenSearch. None of them are the actual AI brain my project needed — something that reads text and reasons about whether two claims contradict each other.&lt;/p&gt;

&lt;p&gt;I posted in the WeMakeDevs Discord asking about local model options. A community member's reply pointed me toward &lt;strong&gt;Groq&lt;/strong&gt; — free tier, genuinely no card required, and fast.&lt;/p&gt;

&lt;p&gt;Day 1's actual stack was simpler than the final architecture: just Groq, called directly. &lt;strong&gt;Strands Agents SDK&lt;/strong&gt; — AWS's own, model-agnostic, so it wouldn't care that I wasn't using Bedrock — was the plan for later, once the core pipeline actually worked.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first real code, and the first real bug
&lt;/h2&gt;

&lt;p&gt;Wired up the extraction pipeline: paste text in, get structured claims and relationships out, in JSON.&lt;/p&gt;

&lt;p&gt;First run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fbak14203admfmldcr7s5.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%2Fbak14203admfmldcr7s5.png" alt="Deprecated model 404 error" width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A clean 404. The model name I'd used had been deprecated by Groq back in June — dead before I ever touched it. Not a mistake, just stale information colliding with a fast-moving API landscape.&lt;/p&gt;

&lt;p&gt;Swapped in the current recommended model. Ran it again:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F424fiaqktbn2sde3l8fl.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%2F424fiaqktbn2sde3l8fl.png" alt="First successful extraction" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Real structured output. Propositions extracted, typed, ready to reason over. This was the moment the project stopped being a plan and started being a thing that actually runs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where Day 1 ends
&lt;/h2&gt;

&lt;p&gt;By the end of today I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A clear, deliberate track decision (Build It, and why)&lt;/li&gt;
&lt;li&gt;A working extraction pipeline calling a real model, no AWS account required&lt;/li&gt;
&lt;li&gt;A first real bug, diagnosed and fixed within the hour&lt;/li&gt;
&lt;li&gt;A strong sense of exactly which AWS-native tools (Strands, Cedar) actually belong in this architecture — and which ones (PartyRock, SAM CLI/LocalStack, OpenSearch) don't, and why forcing them in would hurt more than help&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tomorrow:&lt;/strong&gt; the detection logic — teaching the system to catch contradictions that aren't stated explicitly, wiring the full pipeline end-to-end, and (I'm guessing, based on how Day 1 went) at least one more bug I don't know about yet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech stack so far
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python&lt;/strong&gt; — core pipeline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq API&lt;/strong&gt; — model backend, free tier, no card&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(Coming: Strands Agents SDK to wrap the Groq call, Cedar for policy-based filtering, Flask for the UI)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Also read:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Day 1 blog on &lt;a href="https://builder.aws.com/post/3JVQex5BZ86N73A8IQ6jCV1Lvj2_p/i-hit-my-first-contradiction-before-building-one-for-the-first-commit-hackathon" rel="noopener noreferrer"&gt;AWS Builder Center&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Building in public for AWS First Commit. Day 2 tomorrow — the detection logic, and probably another bug I haven't met yet.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>wemakedevs</category>
      <category>ai</category>
      <category>aws</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>Building a Voice-First AI Reflection Journal with 3D Mindset Constellations on Google Cloud Run</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sun, 06 Sep 2026 15:33:42 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/building-a-voice-first-ai-reflection-journal-with-3d-mindset-constellations-on-google-cloud-run-55ai</link>
      <guid>https://dev.to/sagarmaurya/building-a-voice-first-ai-reflection-journal-with-3d-mindset-constellations-on-google-cloud-run-55ai</guid>
      <description>&lt;p&gt;Most digital journals are fundamentally passive text storage. You type your thoughts into an empty text area, save the entry under a timestamp, and close the tab. Over time, that leaves you with an unindexed graveyard of text entries that are rarely revisited, organized, or connected.&lt;/p&gt;

&lt;p&gt;When developers attempt to introduce AI into journaling tools, the implementation often stops at generic chatbot wrappers or ungrounded motivational slogans. What is missing is &lt;strong&gt;structure, cognitive continuity, and active reflection&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Low-barrier capture&lt;/strong&gt;: When you are fatigued after hours of engineering, sitting down to type several paragraphs creates friction. Speaking aloud with natural voice dictation lowers the barrier to getting thoughts out of your head.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Socratic feedback&lt;/strong&gt;: Instead of generic positive reinforcement, a reflective partner should identify underlying assumptions, clarify trade-offs, and ask thoughtful follow-up questions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-session memory stitching&lt;/strong&gt;: Daily reflections shouldn't exist in silos. An intelligent journal should identify connections between today's friction and dilemmas you faced three weeks ago.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spatial pattern synthesis&lt;/strong&gt;: Rather than keeping thoughts locked in a flat chronological feed, visualizing recurring cognitive themes—breakthroughs, friction, growth, and decisions—as an interactive 3D universe makes mental habits immediately visible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To explore this, I built &lt;strong&gt;Gemini Reflection Journal&lt;/strong&gt;: an open, voice-first reflection application powered by &lt;strong&gt;Gemini 3.6 Flash&lt;/strong&gt;, an interactive &lt;strong&gt;Three.js WebGL 3D constellation galaxy&lt;/strong&gt;, and a unified full-stack architecture deployed to &lt;strong&gt;Google Cloud Run&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is what the interface looks like in practice:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8njhxl7e46k85pqv70d.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%2Fn8njhxl7e46k85pqv70d.png" alt="Gemini Reflection Journal Dashboard" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 1: The distraction-free reflection workspace featuring live voice dictation, reflection spark selection, Socratic multi-turn dialogue, and historical callbacks.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🏗️ System Architecture &amp;amp; Data Flow
&lt;/h2&gt;

&lt;p&gt;The application is structured as a unified full-stack service where a React single-page frontend and an Express API proxy are served from a single container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌────────────────────────────────────────────────────────────────────────────────────────┐
│                               React 18 + Vite Frontend                                 │
│       (Tailwind CSS • Lucide Icons • Web Speech API • Three.js 3D Cosmic Galaxy)       │
└───────────────────────────┬────────────────────────────────┬───────────────────────────┘
                            │                                │
                   (Firebase Auth SDK)               (Internal API Proxy)
                   (Owner-Bound Firestore Sync)      (/api/chat, /api/summarize,
                            │                         /api/socratic-callback,
                            │                         /api/mindset-constellation)
                            ▼                                │
               ┌────────────────────────┐                    ▼
               │ Google Cloud Firestore │      ┌─────────────────────────────┐
               │ (Data Isolation Rules) │      │ Node/Express on Cloud Run   │
               └────────────────────────┘      │ (GCP Secret Manager Ingest) │
                                               └──────────────┬──────────────┘
                                                              │
                                                   (@google/genai SDK)
                                                              ▼
                                               ┌─────────────────────────────┐
                                               │ Gemini Model Fallback Ladder│
                                               │ 1. gemini-3.6-flash         │
                                               │ 2. gemini-3.1-flash-lite    │
                                               │ 3. gemini-flash-latest      │
                                               │ 4. gemini-3.8-flash         │
                                               │ 5. gemini-3.7-flash         │
                                               └─────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Stack Components:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: React 18, TypeScript, Tailwind CSS, Three.js, Lucide Icons, and the browser Web Speech API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Node.js and Express, bundled into a single self-contained CommonJS artifact (&lt;code&gt;dist/server.cjs&lt;/code&gt;) via &lt;code&gt;esbuild&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI Engine&lt;/strong&gt;: &lt;code&gt;@google/genai&lt;/code&gt; TypeScript SDK with an automated circuit-breaker model fallback ladder.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database &amp;amp; Identity&lt;/strong&gt;: Google Cloud Firestore with owner-bound rules, plus Firebase Authentication (Google Sign-In with an automatic fallback to isolated guest sessions).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets Management&lt;/strong&gt;: Google Cloud Secret Manager.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting &amp;amp; Runtime&lt;/strong&gt;: &lt;strong&gt;Google Cloud Run&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🌟 Core System Capabilities
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Hands-Free Voice Dictation &amp;amp; Spoken Feedback
&lt;/h3&gt;

&lt;p&gt;Typing on a keyboard can interrupt quick trains of thought. The application integrates the browser's native &lt;strong&gt;Web Speech API&lt;/strong&gt; for real-time speech recognition:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuous Transcription&lt;/strong&gt;: Captures spoken thoughts and writes directly to the reflection buffer with silence handling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visual Audio Feedback&lt;/strong&gt;: Animated speech indicators provide immediate confirmation that your microphone is capturing input without lag.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Text-to-Speech Playback&lt;/strong&gt;: Follow-up questions generated by Gemini can be read aloud using SpeechSynthesis with synchronized play, pause, and stop controls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F314oswrees6i4pn6z6rf.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%2F314oswrees6i4pn6z6rf.png" alt="Voice Dictation" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fia474zloeqj8tpd7vbhv.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%2Fia474zloeqj8tpd7vbhv.png" alt="Audio Dialogue" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 2: Real-time voice dictation with animated speech waveform and audio playback of Socratic inquiries.&lt;/em&gt;&lt;/p&gt;


&lt;h3&gt;
  
  
  2. Socratic Reflection Engine
&lt;/h3&gt;

&lt;p&gt;When a reflection is submitted, the backend doesn't output generic advice. The system prompt instructs Gemini to act as a thoughtful Socratic partner:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Identifies core tensions, cognitive dissonance, or hidden assumptions.&lt;/li&gt;
&lt;li&gt;Formulates one or two focused clarifying questions.&lt;/li&gt;
&lt;li&gt;Suggests small, concrete next actions rather than abstract platitudes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Users can converse across multiple turns, digging deeper into a technical problem, team friction, or personal decision before concluding the session.&lt;/p&gt;


&lt;h3&gt;
  
  
  3. Socratic Callbacks &amp;amp; Cross-Session Memory Stitching
&lt;/h3&gt;

&lt;p&gt;Most journaling applications treat each entry as an isolated event. Our &lt;strong&gt;Socratic Callback Engine&lt;/strong&gt; (&lt;code&gt;POST /api/socratic-callback&lt;/code&gt;) queries historical entries to bridge the gap between past and present:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Analyzes previous reflections (e.g. from 3, 7, or 14 days ago) alongside today's entry.&lt;/li&gt;
&lt;li&gt;Detects recurring themes, unresolved tensions, and cognitive growth.&lt;/li&gt;
&lt;li&gt;Surfaces tailored callbacks: &lt;em&gt;"Three days ago, you noted feeling blocked by asynchronous review delays. In today's entry, you mentioned shipping the auth service. Did your strategy of small PRs resolve the friction, or did team dynamics shift?"&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This transforms passive logging into a continuous, active feedback loop.&lt;/p&gt;


&lt;h3&gt;
  
  
  4. Interactive 3D Mindset Constellations (Three.js WebGL)
&lt;/h3&gt;

&lt;p&gt;The centerpiece of the application is the &lt;strong&gt;3D Mindset Constellation&lt;/strong&gt; (&lt;code&gt;POST /api/mindset-constellation&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;When reflections are recorded, the backend prompts Gemini 3.6 Flash to analyze the text and extract cognitive nodes classified into five primary categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;💡 &lt;strong&gt;Breakthroughs&lt;/strong&gt; (moments of sudden clarity or insight)&lt;/li&gt;
&lt;li&gt;🌱 &lt;strong&gt;Growth&lt;/strong&gt; (positive momentum, new habits, skill acquisition)&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Friction&lt;/strong&gt; (blockers, fatigue, technical hurdles)&lt;/li&gt;
&lt;li&gt;🧭 &lt;strong&gt;Decision-Making&lt;/strong&gt; (trade-offs, architectural forks, prioritization)&lt;/li&gt;
&lt;li&gt;🧘 &lt;strong&gt;Mindset&lt;/strong&gt; (grounding, self-awareness, perspective shifts)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These nodes and their conceptual links are mapped into a custom &lt;strong&gt;Three.js WebGL scene&lt;/strong&gt;:&lt;/p&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqrzeie9yp77w2yuxzsl1.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%2Fqrzeie9yp77w2yuxzsl1.png" alt="3D Mindset Constellation Galaxy" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 3: Interactive Three.js WebGL 3D universe mapping recurring breakthroughs, friction points, and cognitive growth anchors.&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;
  
  
  🚀 Technical Breakthrough: Zero-Lag Hardware-Accelerated 2D HUD Projection
&lt;/h4&gt;

&lt;p&gt;In hybrid 3D WebGL applications, displaying 2D HTML labels over 3D coordinates often suffers from noticeable lag or jitter. If you pipe 3D coordinates through React state (&lt;code&gt;useState&lt;/code&gt;), React's asynchronous render batching creates a 1–2 frame delay behind the canvas. Furthermore, CSS transitions on &lt;code&gt;left&lt;/code&gt;/&lt;code&gt;top&lt;/code&gt; cause labels to drag like a trailing tail during rotations.&lt;/p&gt;

&lt;p&gt;To achieve fluid, 60 FPS synchronization, we engineered a direct DOM projection pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Direct DOM Refs&lt;/strong&gt;: Badges are mounted with standard React markup, but their coordinates are managed outside the virtual DOM via &lt;code&gt;badgeElementsRef&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synchronized Animation Loop&lt;/strong&gt;: Inside &lt;code&gt;requestAnimationFrame&lt;/code&gt;, right after &lt;code&gt;renderer.render(scene, camera)&lt;/code&gt;, coordinates are calculated via &lt;code&gt;tempV.project(cam)&lt;/code&gt; and immediately applied to element styles using hardware-accelerated GPU transforms:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;transform&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`translate3d(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenX&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;px, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;screenY&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;offsetY&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;px, 0) translate(-50%, &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Camera Forward Vector Dot-Product Check&lt;/strong&gt;: Prevents inverted projections when nodes rotate behind the camera lens:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;   &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;tempV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camPos&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
   &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;toNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;camDir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
     &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
     &lt;span class="k"&gt;return&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;ol&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic Boundary Flip Logic&lt;/strong&gt;: When a star orbits near the top edge of the canvas (&lt;code&gt;screenY &amp;lt; 54&lt;/code&gt;), the badge automatically flips below the star with an upward-pointing anchor stem, eliminating clipping.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Depth-Based Z-Indexing&lt;/strong&gt;: Badges dynamically scale their &lt;code&gt;z-index&lt;/code&gt; based on normalized camera distance so foreground stars naturally occlude background labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Smooth Camera Fly-To&lt;/strong&gt;: Clicking or searching any star triggers a spherical camera flight directly to the node, opening an inspection drawer with related reflection excerpts.&lt;/li&gt;
&lt;/ol&gt;


&lt;h3&gt;
  
  
  5. Structured Synthesis &amp;amp; Daily Sparks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Executive Synthesis&lt;/strong&gt;: Users can generate a clean summary of their journal entry containing an executive summary, key realizations, mood/energy tags, and micro-commitments (&lt;code&gt;POST /api/summarize&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily Reflection Sparks&lt;/strong&gt;: A built-in prompt generator offers targeted starters across five disciplines: Stoic Premeditatio Malorum, Evening Wind-Down Review, Decision Framing, Mindfulness, and Creative Problem Solving (&lt;code&gt;POST /api/daily-prompt&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl33geo4gu3929hgd4766.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%2Fl33geo4gu3929hgd4766.png" alt="Executive Synthesis and Insights" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Figure 4: Automated reflection synthesis generating concise executive summaries, sentiment tags, and actionable micro-commitments.&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  🛡️ Security Architecture &amp;amp; Threat Model
&lt;/h2&gt;

&lt;p&gt;Personal reflections require strict data privacy and isolation. Before building the application logic, we mapped the system against five primary threat zones:&lt;/p&gt;
&lt;h3&gt;
  
  
  Threat Summary Table
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threat Zone&lt;/th&gt;
&lt;th&gt;Identified Risk&lt;/th&gt;
&lt;th&gt;Countermeasure Implemented&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Input Surfaces&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Malicious injection or oversized payload structures in API endpoints.&lt;/td&gt;
&lt;td&gt;Strict schema validation, top-level JSON request deserialization, and defensive null-safe parameter guarding (&lt;code&gt;(req.body &amp;amp;&amp;amp; typeof req.body === 'object') ? req.body : {}&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Planning &amp;amp; Reasoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Prompt injection attempting to divert the Socratic persona.&lt;/td&gt;
&lt;td&gt;Strict system instruction boundaries; incoming reflections are treated strictly as plain text data, never executable instructions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool &amp;amp; Server Execution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API credential exposure or client-side token leakage.&lt;/td&gt;
&lt;td&gt;Complete backend API proxy architecture. The browser client never touches or stores the Gemini API key.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Memory &amp;amp; Database&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cross-user data access or unauthorized document read/write.&lt;/td&gt;
&lt;td&gt;Owner-bound Firestore security rules verify &lt;code&gt;request.auth.uid == userId&lt;/code&gt; on every path and sub-collection.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Secret Management&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Hardcoded secrets or keys leaked through version control.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;GEMINI_API_KEY&lt;/code&gt; is loaded at container runtime from Google Cloud Secret Manager. Zero secrets exist in code or repository commits.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Owner-Bound Firestore Security Rules
&lt;/h3&gt;

&lt;p&gt;To enforce complete user data isolation at the database level, &lt;code&gt;firestore.rules&lt;/code&gt; enforces that only the authenticated user can access their own document trees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;rules_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;service&lt;/span&gt; &lt;span class="nx"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;firestore&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;databases&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/documents &lt;/span&gt;&lt;span class="err"&gt;{
&lt;/span&gt;    &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;users&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;interactions&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;interactionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;entryId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;allow&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;write&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;auth&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;uid&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nx"&gt;userId&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="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;Even if client-side configuration parameters are inspected in the browser, Firestore directly rejects any query where &lt;code&gt;request.auth.uid&lt;/code&gt; fails to match the targeted document path.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Engineering Highlight: Resilient Gemini Model Fallback Ladder
&lt;/h2&gt;

&lt;p&gt;In production, external API endpoints can occasionally return transient &lt;code&gt;429 Resource Exhausted&lt;/code&gt;, &lt;code&gt;503 Service Unavailable&lt;/code&gt;, or capacity errors.&lt;/p&gt;

&lt;p&gt;Rather than letting an API hiccup interrupt a user's train of thought, our backend helper wraps &lt;code&gt;@google/genai&lt;/code&gt; in an automated multi-tier fallback ladder with &lt;strong&gt;circuit-breaker health tracking&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server.ts - Resilient Gemini Call with Circuit-Breaker Fallback Ladder&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;GoogleGenAI&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google/genai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;BASE_MODEL_LADDER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-3.6-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Primary: Low latency, high reasoning quality&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-3.1-flash-lite&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// High-availability lightweight fallback&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-flash-latest&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;// Dynamic alias fallback&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-3.8-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;// Next-gen reasoning engine&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;gemini-3.7-flash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;        &lt;span class="c1"&gt;// Deep reasoning fallback&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Track degraded models to avoid hammering exhausted quotas&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getPrioritizedModelList&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;BASE_MODEL_LADDER&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;degradedUntil&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;degradedUntil&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;healthy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;degraded&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generateContentWithFallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;config&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;any&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;GoogleGenAI&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GEMINI_API_KEY&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidateModels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getPrioritizedModelList&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;lastError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;candidateModels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isQuota&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; 
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;isQuota&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Skip models with active multi-hour quota exhaustion&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Allow 1 quick retry for transient demand surges&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;maxAttempts&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generateContent&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;systemInstruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;

        &lt;span class="c1"&gt;// Success: clear degraded state&lt;/span&gt;
        &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;modelUsed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;lastError&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Quota exhausted: cool down model for 15 minutes&lt;/span&gt;
          &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Cascade to next model immediately&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Transient server pressure: short 30-second cooldown&lt;/span&gt;
          &lt;span class="nx"&gt;modelDegradedUntil&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Try once more before cascading&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="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;lastError&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;If the primary model experiences transient capacity pressure, the call cascades to the next tier in milliseconds, returning a valid response without breaking the user experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Deploying to Google Cloud Run
&lt;/h2&gt;

&lt;p&gt;Deploying a full-stack container to Google Cloud Run provides several key operational advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unified Container&lt;/strong&gt;: Both the static client assets and the Node.js Express server run in one container on port 3000.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scale to Zero&lt;/strong&gt;: During idle hours, Cloud Run instances scale down to zero, minimizing resource consumption.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Secret Manager Binding&lt;/strong&gt;: Cloud Run mounts secrets directly into environment variables without requiring &lt;code&gt;.env&lt;/code&gt; files in production images.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Step-by-Step Deployment Walkthrough
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. Enable Required Google Cloud APIs
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcloud services &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  run.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  secretmanager.googleapis.com &lt;span class="se"&gt;\&lt;/span&gt;
  firestore.googleapis.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Configure Secret Manager for the Gemini API Key
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create the secret&lt;/span&gt;
gcloud secrets create GEMINI_API_KEY &lt;span class="nt"&gt;--replication-policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"automatic"&lt;/span&gt;

&lt;span class="c"&gt;# Set the secret value&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"YOUR_GEMINI_API_KEY"&lt;/span&gt; | gcloud secrets versions add GEMINI_API_KEY &lt;span class="nt"&gt;--data-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-

&lt;span class="c"&gt;# Grant Cloud Run's service account permission to access the secret&lt;/span&gt;
&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;gcloud projects describe &lt;span class="si"&gt;$(&lt;/span&gt;gcloud config get-value project&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"value(projectNumber)"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

gcloud secrets add-iam-policy-binding GEMINI_API_KEY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--member&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"serviceAccount:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROJECT_NUMBER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-compute@developer.gserviceaccount.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"roles/secretmanager.secretAccessor"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Build &amp;amp; Deploy to Cloud Run
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Deploy container directly from the application source&lt;/span&gt;
gcloud run deploy gemini-reflection-journal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--source&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; managed &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; us-central1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--allow-unauthenticated&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set-secrets&lt;/span&gt; &lt;span class="nv"&gt;GEMINI_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;GEMINI_API_KEY:latest

&lt;span class="c"&gt;# Attach the required Cloud Run AI Challenge label for automated verification&lt;/span&gt;
gcloud run services update gemini-reflection-journal &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--update-labels&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev-tutorial&lt;span class="o"&gt;=&lt;/span&gt;cloud-run-ai-challenge &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us-central1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once deployed, Cloud Run provides a production HTTPS URL with automatic TLS termination and scalable request routing.&lt;/p&gt;




&lt;h2&gt;
  
  
  📈 Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sub-second latency enables natural conversational flow&lt;/strong&gt;: Using &lt;strong&gt;Gemini 3.6 Flash&lt;/strong&gt; ensures that reflection feedback returns within seconds of finishing dictation, preventing awkward conversational pauses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct DOM synchronization solves hybrid 3D UI lag&lt;/strong&gt;: Decoupling 2D HTML labels from React's virtual DOM and projecting them directly in the WebGL &lt;code&gt;requestAnimationFrame&lt;/code&gt; loop delivers smooth 60 FPS performance without trailing or jitter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-session callbacks turn logs into insights&lt;/strong&gt;: Connecting historical reflections with current entries via Socratic callbacks bridges cognitive patterns that users would otherwise miss.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containerized serverless simplifies full-stack delivery&lt;/strong&gt;: Running a bundled React + Express service in Cloud Run eliminates the need to coordinate separate hosting environments, API gateways, or complex reverse proxies.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;Submitted as part of the Google Cloud Run AI Challenge.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#AccelerateAIwithCloudRun #GoogleCloud #CloudRun #Gemini #GeminiAI #GoogleGenAI #Firestore #Firebase&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

</description>
      <category>accelerateaiwithcloudrun</category>
      <category>googlecloud</category>
      <category>gemini</category>
      <category>firebase</category>
    </item>
    <item>
      <title>How I Built a Self-Healing AI Tracker That Fixes Its Own Scrapers</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sun, 23 Aug 2026 16:48:31 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/how-i-built-a-self-healing-ai-tracker-that-fixes-its-own-scrapers-1mi6</link>
      <guid>https://dev.to/sagarmaurya/how-i-built-a-self-healing-ai-tracker-that-fixes-its-own-scrapers-1mi6</guid>
      <description>&lt;p&gt;WeMakeDevs and Bright Data ran a week-long hackathon called &lt;strong&gt;Into the Scrape-Verse&lt;/strong&gt; — the challenge was to build a self-healing web scraper and turn live web data into something real.&lt;/p&gt;

&lt;p&gt;I had one week. I built &lt;strong&gt;Crawlr&lt;/strong&gt; — an autonomous AI model and dataset tracker that watches four research sources in real time, detects when a scraper breaks, heals it automatically, and logs every event to a live audit trail. No human in the loop.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fstaebbxiafhvzq21u6v6.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%2Fstaebbxiafhvzq21u6v6.png" alt="Crawlr landing page" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem I Wanted to Solve
&lt;/h2&gt;

&lt;p&gt;Scrapers break silently. A site changes a class name, a layout shifts, a field moves — and your scraper returns nothing. You find out days later when someone notices the data is stale.&lt;/p&gt;

&lt;p&gt;The usual fix is manual: inspect the HTML, find the new selector, redeploy. It works until the next time.&lt;/p&gt;

&lt;p&gt;I wanted to build something that detects the break, figures out what went wrong, and fixes itself — without any human involvement.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Crawlr Tracks
&lt;/h2&gt;

&lt;p&gt;The AI research world moves fast. New models drop on EleutherAI. Together AI publishes blog posts. Papers With Code indexes the latest benchmarks. OpenRouter lists every available model with real pricing data.&lt;/p&gt;

&lt;p&gt;Crawlr watches all four:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;What it scrapes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;EleutherAI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Model and dataset releases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Together AI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Blog posts and announcements&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Papers With Code&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Latest research papers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OpenRouter&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Live model catalog with context length and pricing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every pipeline run scrapes all four, normalises the data into a unified format, and serves it through a real-time dashboard. 78 records across 4 sources, updated on every run.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmcaf37yd4qe7qmht887p.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%2Fmcaf37yd4qe7qmht887p.png" alt="Dashboard" width="800" height="454"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How Bright Data Scraper Studio Powers Everything
&lt;/h2&gt;

&lt;p&gt;Every scraper in Crawlr was built using &lt;strong&gt;Bright Data's Scraper Studio&lt;/strong&gt; — an AI-powered platform that builds, runs, and self-heals custom web scrapers from your terminal.&lt;/p&gt;

&lt;p&gt;The entire workflow is four commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Login once&lt;/span&gt;
npx &lt;span class="nt"&gt;-p&lt;/span&gt; @brightdata/cli bdata login

&lt;span class="c"&gt;# Describe the data you want — AI builds the scraper&lt;/span&gt;
bdata scraper create https://eleuther.ai/releases &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"model name, description, release date, type (model/dataset/library)"&lt;/span&gt;

&lt;span class="c"&gt;# Run it — returns clean JSON&lt;/span&gt;
bdata scraper run c_mt4lee1f5n8ouckku https://eleuther.ai/releases &lt;span class="nt"&gt;--pretty&lt;/span&gt;

&lt;span class="c"&gt;# When the site changes — heal it with a plain-English description&lt;/span&gt;
bdata scraper heal c_mt4lee1f5n8ouckku &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"description field is empty in 6 out of 8 records, selector may have changed"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftvh379tqwam1t6hok24t.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%2Ftvh379tqwam1t6hok24t.png" alt="Terminal showing bdata scraper run — clean JSON output" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;c_*&lt;/code&gt; Collector ID you get back is a live production API endpoint. No deployment step. No proxy rotation, retries, or unblocking to manage — Bright Data handles all of that. You just describe what you want.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Moment That Made It Click
&lt;/h3&gt;

&lt;p&gt;While building the OpenRouter collector, the initial scraper was returning records but the &lt;code&gt;context_length&lt;/code&gt; and &lt;code&gt;pricing_per_token&lt;/code&gt; fields were all null. I ran:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bdata scraper heal c_mt4m2cql181bpcgals &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"context_length and pricing fields returning null. 
   Model names contain provider prefix like 'Meta: Llama 3.1'. 
   Extract provider from model name string."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two minutes later, the collector was returning this:&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;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Meta: Muse Spark 1.2 Contributor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Meta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"context_length"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.05M context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pricing_per_token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$0.10 /M input tokens"&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;Same Collector ID. Nothing downstream changed. The scraper just started working correctly. That's when I understood what made Scraper Studio genuinely different.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Crawlr has three components working together in a closed loop.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Pipeline
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scrapers/pipeline.js&lt;/code&gt; runs all four collectors in sequence, normalises the raw JSON into a unified schema, and saves the output to the &lt;code&gt;data/&lt;/code&gt; folder.&lt;/p&gt;

&lt;p&gt;Each source returns completely different data shapes — OpenRouter looks nothing like EleutherAI. Every source has its own &lt;code&gt;unify*&lt;/code&gt; function that maps it to the same schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eleuther-0-trlX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eleuther&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trlX&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A repo for distributed training of language models...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/CarperAI/trlx&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Library&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Dec 9, 2023&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Sentry — The Health Agent
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;scrapers/sentry.js&lt;/code&gt; is an autonomous health checker that runs after every pipeline and validates each source's output for three failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Schema Break&lt;/strong&gt; — payload is empty or not an array&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume Drop&lt;/strong&gt; — record count fell more than 50% vs the baseline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Null Creep&lt;/strong&gt; — required fields are empty in more than 20% of records&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When it detects a failure, it auto-generates a diagnosis from the actual failure data — field names, failure percentages, a sample broken record — and calls &lt;code&gt;bdata scraper heal&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;buildDiagnosis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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="nf"&gt;slice&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="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`Self-healing prompt for '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sourceId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;': 
            &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;issues&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; | &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; 
            Sample record: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
            Action: locate updated selectors for the listed fields.`&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;After healing, Sentry re-runs the collector to verify recovery. Every step is logged with timestamps. Here's what a normal healthy run looks like — no anomalies this time, all four sources verified clean. (The full heal cycle, triggered live, is a bit further down in Chaos Mode.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmuxg7kf2i5r4ch30qm8d.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%2Fmuxg7kf2i5r4ch30qm8d.png" alt="Sentry health check — all 4 sources verified healthy" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Dashboard
&lt;/h3&gt;

&lt;p&gt;A pure HTML/CSS/JS frontend — no framework, no build step. It reads directly from the JSON files the pipeline writes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F8lqermvqael7znf2au40.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%2F8lqermvqael7znf2au40.png" alt="Dashboard with search active, filtering cards" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;4 source tabs&lt;/strong&gt; with live record counts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant search&lt;/strong&gt; across title, description, and organization&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live Audit Log&lt;/strong&gt; — color-coded pills for each event type (HEALTHY, ANOMALY, HEALING, ERROR)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chaos Mode&lt;/strong&gt; — simulates a scraper break so you can see the full heal cycle live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6z8y8uxu0wznxn38nt4v.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%2F6z8y8uxu0wznxn38nt4v.png" alt="Chaos Mode — log showing the full simulated break and recovery" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What a Full Run Looks Like
&lt;/h2&gt;

&lt;p&gt;Pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ eleuther:   8 items saved
✅ togetherai: 25 items saved
✅ pwc:        25 items saved
✅ openrouter: 20 items saved
✅ Pipeline complete.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sentry health check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✅ [eleuther]   Healthy. 8 records returned.
✅ [togetherai] Healthy. 31 records returned.
✅ [pwc]        Healthy. 50 records returned.
✅ [openrouter] Healthy. 20 records returned.
✅ All sources checked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fevp0mdmsved89iyv043h.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%2Fevp0mdmsved89iyv043h.png" alt="Terminal showing full pipeline.js run — all 4 sources scraped and saved" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;&lt;code&gt;bdata scraper heal&lt;/code&gt; is the real unlock.&lt;/strong&gt; You describe what's broken in plain English, Bright Data's AI rewrites the extraction logic, and the same Collector ID starts returning clean data. I watched it fix broken selectors on live sources multiple times during the week.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bright Data handles the hard parts of scraping.&lt;/strong&gt; Proxy rotation, retries, unblocking, rate limiting — none of that is your problem. You describe what you want and get clean JSON back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Collector ID is a production API endpoint.&lt;/strong&gt; Trigger it with &lt;code&gt;POST /dca/trigger&lt;/code&gt; from any language or scheduler. No deployment step. This made wiring it into the pipeline and dashboard trivial.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-healing is not a gimmick.&lt;/strong&gt; Going into this I wasn't sure how well automated healing would actually work on real sites. After a week of watching it fix real extraction failures, I'm convinced it's the right direction for production scrapers.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/mauryasagar/crawlr" rel="noopener noreferrer"&gt;github.com/mauryasagar/crawlr&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone it, add your &lt;code&gt;.env&lt;/code&gt; with four Bright Data Collector IDs, and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run pipeline   &lt;span class="c"&gt;# Scrape all 4 sources&lt;/span&gt;
npm run sentry     &lt;span class="c"&gt;# Health check + auto-heal&lt;/span&gt;
npm run serve      &lt;span class="c"&gt;# Open the dashboard at localhost:3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Built for the Into the Scrape-Verse hackathon — WeMakeDevs × Bright Data, August 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;#webdev #javascript #hackathon #brightdata #scraping #wemakedevs #opensource&lt;/em&gt;&lt;/p&gt;

</description>
      <category>wemakedevs</category>
      <category>brightdatachallenge</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>How I Built a YouTube Trend Engine on Zerops</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sun, 09 Aug 2026 17:14:45 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/how-i-built-a-youtube-trend-engine-on-zerops-hka</link>
      <guid>https://dev.to/sagarmaurya/how-i-built-a-youtube-trend-engine-on-zerops-hka</guid>
      <description>&lt;h2&gt;
  
  
  The API Quota Problem
&lt;/h2&gt;

&lt;p&gt;If you've ever built applications on public APIs, you know how quickly limits can become a bottleneck. The YouTube Data API v3 gives you a free tier of 10,000 units/day. Each &lt;code&gt;search.list&lt;/code&gt; request costs &lt;strong&gt;100 units&lt;/strong&gt;. A single user scanning a niche across 6 keywords burns 600 units. A handful of test scans, and your daily allowance is practically gone.&lt;/p&gt;

&lt;p&gt;To make matters worse, Google Trends (via &lt;code&gt;pytrends&lt;/code&gt;) has no official API — if you hit it repeatedly without caching, Google silently rate-limits your IP address, returning empty data frames or hanging your server indefinitely.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Signal&lt;/strong&gt; for the &lt;strong&gt;Zerops Challenge by WeMakeDevs&lt;/strong&gt;: a tool that turns real-time search trends and YouTube view telemetry into ready-to-film video titles ranked by an objective demand score. &lt;/p&gt;

&lt;p&gt;Because of these aggressive limits, I didn't just need an app that worked — I needed an infrastructure that could &lt;strong&gt;fail soft, save API quota, and respond in under 1 millisecond on repeat scans&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here is how I built Signal, the architectural decisions behind it, the three sneaky deployment bugs I encountered, and how &lt;strong&gt;Zerops Valkey&lt;/strong&gt; saved my API quota.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://signal-2dc6-8000.prg1.zerops.app" rel="noopener noreferrer"&gt;https://signal-2dc6-8000.prg1.zerops.app&lt;/a&gt;&lt;br&gt;&lt;br&gt;
💻 &lt;strong&gt;GitHub repository:&lt;/strong&gt; &lt;a href="https://github.com/mauryasagar/signal" rel="noopener noreferrer"&gt;github.com/mauryasagar/signal&lt;/a&gt;  &lt;/p&gt;


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

&lt;p&gt;Most YouTube advice is generic. Signal changes that by backing up video ideas with real data.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;User inputs a niche&lt;/strong&gt; (e.g., &lt;code&gt;"home coffee brewing"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Trends Discovery&lt;/strong&gt;: Pulls rising and related search queries over the last 90 days.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YouTube Telemetry Check&lt;/strong&gt;: Queries YouTube Data API v3 in parallel to fetch recent video upload counts and average view metrics.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Groq LLM Synthesis&lt;/strong&gt;: Sends all signals to &lt;strong&gt;Llama 3.1 8B Instant&lt;/strong&gt; on Groq LPUs, generating concrete video titles and strategic creator angles in &lt;strong&gt;~280ms&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demand Scoring&lt;/strong&gt;: Ranks topics using a weighted logarithmic score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zerops Valkey Caching&lt;/strong&gt;: Caches raw signal telemetry for 1 hour. Repeat queries skip external APIs entirely, returning in &lt;strong&gt;&amp;lt;1ms&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsn7d0taf8bq6bkyjcs6x.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%2Fsn7d0taf8bq6bkyjcs6x.png" alt="Signal App Landing Page" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Live view of the Signal Dashboard generating topics for coffee brewing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h2&gt;
  
  
  System Architecture: The 4-Layer Engine
&lt;/h2&gt;

&lt;p&gt;Signal is designed as a micro-cached pipeline deployed on &lt;strong&gt;Zerops&lt;/strong&gt;, using Gunicorn workers, Valkey key-value storage, and external API connectors.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User Input: "home coffee brewing"
         │
         ▼
┌─────────────────────────────────────────────────────────────┐
│              Flask Web App (Gunicorn · Python 3.12)         │
│                    Deployed on Zerops                       │
└──────────────┬──────────────────────────────────────────────┘
               │
               ▼
┌─────────────────────────────────────────────────────────────┐
│                    Pipeline (report.py)                     │
│                                                             │
│  1. Check Zerops Valkey cache ──► HIT: Skip to step 4       │
│                                   MISS: Continue            │
│                                                             │
│  2. Google Trends (pytrends)                                │
│     └── Daemon thread → [{query, interest}]                 │
│                                                             │
│  3. YouTube Data API v3                                     │
│     └── Per query: video count + avg views                  │
│                                                             │
│  4. Write signals to Valkey cache (1 hour TTL)              │
│                                                             │
│  5. Groq LPU Inference (Llama 3.1 8B)                       │
│     └── Keyword + demand data → Title + Angle               │
│                                                             │
│  6. Demand score computed, topics sorted                    │
└──────────────┬──────────────────────────────────────────────┘
               │
               ▼
        Report page rendered
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Web server&lt;/td&gt;
&lt;td&gt;Flask + Gunicorn&lt;/td&gt;
&lt;td&gt;Lightweight, production-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trend data&lt;/td&gt;
&lt;td&gt;Google Trends via &lt;code&gt;pytrends&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No official API key needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;YouTube demand&lt;/td&gt;
&lt;td&gt;YouTube Data API v3&lt;/td&gt;
&lt;td&gt;Official, free 10K quota/day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM&lt;/td&gt;
&lt;td&gt;Groq — Llama 3.1 8B&lt;/td&gt;
&lt;td&gt;Sub-second inference&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cache&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zerops Valkey&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Cuts repeat response time from 25s → &amp;lt;1s&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zerops&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Production infra, private networking, auto-deploy&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;signal/
├── app.py              # Flask routes: /, /about, /generate, /health
├── report.py           # Pipeline orchestration + demand scoring
├── fetch_trends.py     # Google Trends + YouTube API calls
├── generate_topics.py  # Groq LLM call + JSON parsing
├── cache.py            # Zerops Valkey integration
├── zerops.yaml         # Zerops build + run config
├── templates/
│   ├── base.html       # Navbar, radar loading animation
│   ├── landing.html    # Hero + search form
│   └── report.html     # Ranked topic cards
└── static/
    ├── style.css       # Dark/light theme + custom properties
    └── main.js         # Theme toggle + loading overlay
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Technical Deep-Dive: Code &amp;amp; Logic
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Resilient Ingestion &amp;amp; Daemon Thread Caps (&lt;code&gt;fetch_trends.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pytrends&lt;/code&gt; is prone to silent hangs when Google rate-limits requests. Allowing &lt;code&gt;pytrends&lt;/code&gt; to block synchronously would exhaust Gunicorn worker threads.&lt;/p&gt;

&lt;p&gt;To guarantee sub-second bounds, Signal wraps &lt;code&gt;pytrends&lt;/code&gt; in a &lt;strong&gt;daemon thread with an explicit 0.8-second hard cutoff&lt;/strong&gt;. If the thread doesn't finish, we gracefully fall back to predefined query templates:&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="c1"&gt;# fetch_trends.py snippet
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_all_signals&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;youtube_api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_terms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;related&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_fetch_pytrends&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;related&lt;/span&gt;
        &lt;span class="n"&gt;related&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_related_queries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_terms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_terms&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Enforce strict 0.8s latency bound
&lt;/span&gt;    &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_fetch_pytrends&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Soft failure fallback if Google Trends rate-limits
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;related&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;related&lt;/span&gt; &lt;span class="o"&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;interest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;50&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tips&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;interest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&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;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;niche&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; for beginners&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;interest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;40&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;h3&gt;
  
  
  2. The Logarithmic Demand Scoring Model (&lt;code&gt;report.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Raw views can be misleading — a single viral video with 10 million views can distort averages and make a dead niche look hyper-attractive. Signal uses a normalized &lt;strong&gt;0–100 Demand Score&lt;/strong&gt; formula with &lt;strong&gt;log-scaled view counts&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DemandScore&lt;/strong&gt; = &lt;code&gt;(Trend Momentum × 60%)&lt;/code&gt; + &lt;code&gt;(Upload Velocity × 25%)&lt;/code&gt; + &lt;code&gt;(Audience Size × 15%)&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# report.py snippet
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_compute_demand_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# 60% — Google Trends search interest momentum (0-100)
&lt;/span&gt;    &lt;span class="n"&gt;trend_component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trend_interest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.6&lt;/span&gt;

    &lt;span class="c1"&gt;# 25% — Creator upload velocity (caps at 5 recent videos)
&lt;/span&gt;    &lt;span class="n"&gt;video_count_component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt;

    &lt;span class="c1"&gt;# 15% — Audience size (log10 scaled up to 10M views so outliers don't dominate)
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;views_component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log10&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_views&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;views_component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;trend_component&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;video_count_component&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;views_component&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Fail-Soft Caching Layer with Zerops Valkey (&lt;code&gt;cache.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Zerops provides a managed, Redis-compatible &lt;strong&gt;Valkey&lt;/strong&gt; database service. Signal uses Valkey to cache raw signals for 1 hour. But I wanted local development to work &lt;em&gt;without&lt;/em&gt; installing Redis. The solution? A &lt;strong&gt;thread-safe in-memory fallback&lt;/strong&gt;:&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="c1"&gt;# cache.py snippet
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="n"&gt;_MEMORY_CACHE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;_MEMORY_CACHE_LOCK&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_cache&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALKEY_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="c1"&gt;# Falls back to in-memory
&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALKEY_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;6379&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;VALKEY_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;decode_responses&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;socket_connect_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ping&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Valkey unreachable — using in-memory fallback.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The Frontend: Dynamic Report Cards
&lt;/h2&gt;

&lt;p&gt;To make the UI feel as fast and premium as the backend, I built a color-coded topic card system. Each rank gets a unique accent color (amber, green, purple, blue, rose). &lt;/p&gt;

&lt;p&gt;When a user hovers over a topic card, it reveals a gradient wash in the card's specific accent color. This is powered by per-card CSS custom properties:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.topic-card-1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e8a33d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;232&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;163&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;61&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.topic-card-2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#5fb88a&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;95&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="py"&gt;--accent-g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;184&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;138&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.topic-card-3&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;--accent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#a78bfa&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;167&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-g&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;139&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;--accent-b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;250&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.topic-card&lt;/span&gt;&lt;span class="nd"&gt;:hover&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;border-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;translateX&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;4px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;translateY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;-2px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;translateZ&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;box-shadow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;12px&lt;/span&gt; &lt;span class="m"&gt;32px&lt;/span&gt; &lt;span class="m"&gt;-12px&lt;/span&gt; &lt;span class="n"&gt;rgba&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-g&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--accent-b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;0.25&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;h2&gt;
  
  
  Three Sneaky Roadblocks (And How I Fixed Them)
&lt;/h2&gt;

&lt;p&gt;Building for a hackathon is never a straight path. Here are the three sneaky bugs that caught me off guard:&lt;/p&gt;

&lt;h3&gt;
  
  
  🐛 Roadblock #1: The Zerops Container Runtime Ghost
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Bug&lt;/strong&gt;: My local tests passed. I configured &lt;code&gt;zerops.yaml&lt;/code&gt; and deployed. The build succeeded, but the runtime container immediately crashed with:&lt;br&gt;
&lt;code&gt;bash: gunicorn: command not found&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Cause&lt;/strong&gt;: Zerops intelligently decouples the &lt;strong&gt;build container&lt;/strong&gt; from the &lt;strong&gt;runtime container&lt;/strong&gt;. Packages installed during &lt;code&gt;buildCommands&lt;/code&gt; do &lt;em&gt;not&lt;/em&gt; automatically persist into the slim runtime image.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;: Adding a &lt;code&gt;prepareCommands&lt;/code&gt; block inside &lt;code&gt;zerops.yaml&lt;/code&gt; to install production dependencies directly in the runtime environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# zerops.yaml snippet&lt;/span&gt;
&lt;span class="na"&gt;zerops&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;signal&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python@3.12&lt;/span&gt;
      &lt;span class="na"&gt;buildCommands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;base&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python@3.12&lt;/span&gt;
      &lt;span class="c1"&gt;# THE FIX: Install packages in the runtime container&lt;/span&gt;
      &lt;span class="na"&gt;prepareCommands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install Flask==3.0.3 python-dotenv==1.0.1 gunicorn==22.0.0 redis==5.2.0 ...&lt;/span&gt;
      &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
          &lt;span class="na"&gt;httpSupport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="na"&gt;start&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gunicorn app:app --bind 0.0.0.0:8000 --workers &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  🐛 Roadblock #2: The Missing Valkey Auto-Password
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Bug&lt;/strong&gt;: Zerops Valkey service was running, but &lt;code&gt;cache.py&lt;/code&gt; kept throwing &lt;code&gt;redis.exceptions.AuthenticationError: NOAUTH Authentication required&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Cause&lt;/strong&gt;: Zerops Valkey instances ship with authentication enabled by default, auto-generating a secure password. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;: Using Zerops' dynamic environment variable cross-referencing in the project dashboard! I didn't need to hardcode anything.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm537iji0apbsj01z24kc.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%2Fm537iji0apbsj01z24kc.png" alt="Zerops Environment Variables" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Using Zerops' dynamic variables to securely inject the auto-generated Valkey password at runtime.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;By setting &lt;code&gt;VALKEY_PASSWORD=${valkey_password}&lt;/code&gt;, Zerops dynamically injects the auto-generated Valkey credential at runtime. Magic.&lt;/p&gt;

&lt;h3&gt;
  
  
  🐛 Roadblock #3: Silent LLM JSON Output Formatting Drift
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The Bug&lt;/strong&gt;: Even with explicit prompting, Llama 3.1 would occasionally wrap JSON responses in conversational preambles or markdown block wrappers (&lt;code&gt;&lt;/code&gt;&lt;code&gt;json ...&lt;/code&gt;&lt;code&gt;&lt;/code&gt;), completely breaking Python's &lt;code&gt;json.loads()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Fix&lt;/strong&gt;: A robust regex extraction function (&lt;code&gt;_extract_json&lt;/code&gt;) in &lt;code&gt;generate_topics.py&lt;/code&gt; to strip out markdown and preambles safely before parsing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Telemetry &amp;amp; Verification: The &lt;code&gt;/health&lt;/code&gt; Endpoint
&lt;/h2&gt;

&lt;p&gt;To confirm that Valkey and Flask were communicating over Zerops' private network, I built a live &lt;code&gt;/health&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkbhjeliiv1g54nyvlbgx.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%2Fkbhjeliiv1g54nyvlbgx.png" alt="Signal Health Check" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Live telemetry from the /health endpoint confirming successful connection to the Zerops Valkey cache.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Hitting &lt;code&gt;https://signal-2dc6-8000.prg1.zerops.app/health&lt;/code&gt; returns live confirmation that caching is active, which instantly drops our response time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Production Performance Benchmarks
&lt;/h3&gt;

&lt;p&gt;Here is how Signal performs on Zerops under live production conditions:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Performance Metric&lt;/th&gt;
&lt;th&gt;Live Uncached Scan&lt;/th&gt;
&lt;th&gt;Zerops Valkey Cache Hit&lt;/th&gt;
&lt;th&gt;Net Savings / Gain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Response Latency&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1,420 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.85 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;99.94% Faster&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;YouTube API Quota&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;600 units / scan&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 units&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;100% Quota Saved&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Google Trends Calls&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1 HTTP request&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0 HTTP requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Zero Rate Limits&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Groq LPU Inference&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~280 ms&lt;/td&gt;
&lt;td&gt;~280 ms (Fresh Titles)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Sub-second AI&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Step-by-Step Zerops Deployment Walkthrough
&lt;/h2&gt;

&lt;p&gt;Deploying Signal on Zerops took under &lt;strong&gt;4 minutes&lt;/strong&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create Zerops Account&lt;/strong&gt;: Sign up at &lt;a href="https://zerops.io" rel="noopener noreferrer"&gt;zerops.io&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add Services&lt;/strong&gt;: I added a Python runtime service (&lt;code&gt;signal&lt;/code&gt;, Python 3.12) and a Valkey database service (&lt;code&gt;valkey&lt;/code&gt;, Valkey 7.2 Single mode).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Environment Variables&lt;/strong&gt;: Set &lt;code&gt;YOUTUBE_API_KEY&lt;/code&gt;, &lt;code&gt;GROQ_API_KEY&lt;/code&gt;, &lt;code&gt;VALKEY_HOST=valkey&lt;/code&gt;, and &lt;code&gt;VALKEY_PASSWORD=${valkey_password}&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connect GitHub Repo&lt;/strong&gt;: Triggered the build pipeline directly from my main branch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enable Public Subdomain&lt;/strong&gt;: Flipped the switch for public access on port 8000.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxlqs0t0qc541flcht04.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%2Fgxlqs0t0qc541flcht04.png" alt="Zerops Dashboard Projects" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Mission accomplished: both Signal and Valkey services running flawlessly in the Zerops production environment.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Known Limitations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Demand score is directional, not exact.&lt;/strong&gt; No free data source provides real search volume. The score combines relative trend interest and YouTube activity as a proxy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pytrends is unofficial.&lt;/strong&gt; Google Trends has no public API. The library can be rate-limited or break without warning. Signal falls back to seeded keywords automatically, so the app never fully fails.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Avg views can look inflated.&lt;/strong&gt; Results are sorted by view count, so the top videos tend to be outlier performers — it's a ceiling signal, not a guaranteed expectation for a new channel.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Takeaways &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never trust external APIs to fail loud&lt;/strong&gt;: Wrap unofficial endpoints (like &lt;code&gt;pytrends&lt;/code&gt;) in daemon threads with hard timeout limits and soft fallbacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple build vs runtime environments&lt;/strong&gt;: Always configure &lt;code&gt;prepareCommands&lt;/code&gt; in &lt;code&gt;zerops.yaml&lt;/code&gt; for runtime dependencies like &lt;code&gt;gunicorn&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail-soft caching is worth every line of code&lt;/strong&gt;: Adding a thread-safe in-memory cache fallback meant local dev worked without Redis, and production degraded gracefully during network blips.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zerops makes multi-service infra seamless&lt;/strong&gt;: Wiring Flask to Valkey over internal networking took literally one environment variable (&lt;code&gt;VALKEY_HOST=valkey&lt;/code&gt;).&lt;/li&gt;
&lt;/ol&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI Disclosure:&lt;/strong&gt; As per hackathon rules, tools like Claude and Antigravity were used as pair-programmers to speed up boilerplate code and draft this post. The core architecture, API logic, and Zerops deployment are my own work.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Try Signal Today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🚀 &lt;strong&gt;Live Demo&lt;/strong&gt;: &lt;a href="https://signal-2dc6-8000.prg1.zerops.app" rel="noopener noreferrer"&gt;https://signal-2dc6-8000.prg1.zerops.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;strong&gt;GitHub Repo&lt;/strong&gt;: &lt;a href="https://github.com/mauryasagar/signal" rel="noopener noreferrer"&gt;github.com/mauryasagar/signal&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Built for &lt;strong&gt;The Zerops Challenge by WeMakeDevs 2026&lt;/strong&gt;. Thanks to the Zerops team for building an exceptional developer platform, and a massive thank you to the &lt;a href="https://wemakedevs.org" rel="noopener noreferrer"&gt;WeMakeDevs&lt;/a&gt; community for hosting this incredible hackathon!&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>zerops</category>
      <category>wemakedevs</category>
      <category>hackathon</category>
      <category>python</category>
    </item>
    <item>
      <title>Ouroboros AI: We Made a Fake Agent Break Itself, Then Built Something That Fixes It</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sun, 26 Jul 2026 15:15:12 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/ouroboros-ai-we-made-a-fake-agent-break-itself-then-built-something-that-fixes-it-4akk</link>
      <guid>https://dev.to/sagarmaurya/ouroboros-ai-we-made-a-fake-agent-break-itself-then-built-something-that-fixes-it-4akk</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By Sagar Maurya &amp;amp; Disha Sonowal — WeMakeDevs SigNoz Hackathon&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're just landing on this series for the first time, here's the short version of where we've been.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/dishasonowal/we-traced-a-fake-ai-agent-before-building-a-real-one-heres-what-broke-1e7d"&gt;our first post&lt;/a&gt;, we installed a tool called SigNoz for the very first time, so we could watch what a piece of software is doing while it runs — which parts are slow, which parts fail, and why. We built a tiny fake AI agent, and just by looking at a picture of its actions laid out in order, we could instantly see that one single step was eating up 85% of the total time. That was the moment this whole thing clicked for us.&lt;/p&gt;

&lt;p&gt;In &lt;a href="https://dev.to/sagarmaurya/we-asked-signoz-a-question-and-it-actually-answered-p6o"&gt;our second post&lt;/a&gt;, we went further. We built a slightly smarter fake agent and asked SigNoz a real question: "does this get slower when it has more information to look through?" We didn't calculate the answer ourselves. We just told SigNoz what to group and average, and it handed us a table proving the pattern, clean as anything.&lt;/p&gt;

&lt;p&gt;Both of those were practice. Small, safe experiments before the real hackathon clock started ticking. This post is about what we actually built once it did — and honestly, it's the post we're proudest of, because it's the one where we stopped just &lt;em&gt;watching&lt;/em&gt; our fake agent and started building something that &lt;em&gt;does something about it&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;We're calling the project &lt;strong&gt;Ouroboros AI&lt;/strong&gt;. Repo's here if you want to poke around: &lt;a href="https://github.com/mauryasagar/ouroboros-ai" rel="noopener noreferrer"&gt;github.com/mauryasagar/ouroboros-ai&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem that was quietly bugging us
&lt;/h2&gt;

&lt;p&gt;Right after we published that second post, something about it kept nagging at us.&lt;/p&gt;

&lt;p&gt;We'd asked SigNoz a good question and gotten a good answer: yes, more documents means a slower response, and it climbs in an almost perfectly straight line. We felt clever about it for about a day. Then we looked at what we'd actually built and realized... nothing happened next. We saw the pattern, said "yep, makes sense," and closed the browser tab. If this had been a real product used by real people, someone would still have had to &lt;em&gt;notice&lt;/em&gt; that pattern themselves, write it up, wait for an engineer to have time, and fix it — probably days later, and only if they happened to be looking at the right dashboard at the right moment.&lt;/p&gt;

&lt;p&gt;That bothered us more the more we thought about it. Watching something break in slow motion isn't the same as fixing it. So a simple, slightly stubborn question became the whole idea for this project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if the system could notice its own problem and fix itself, without a person in the middle at all?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not "send someone a faster alert." Actually close the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few words before the story, so nothing feels confusing
&lt;/h2&gt;

&lt;p&gt;We're going to use a handful of words a lot in this post. If you already know them, skip ahead. If you don't, here they are in plain terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace&lt;/strong&gt; — a record of everything that happened during one single request, start to finish.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span&lt;/strong&gt; — one step inside that trace. Four things happened? Four spans.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert&lt;/strong&gt; — a rule you set up that says "if X happens, tell someone."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Webhook&lt;/strong&gt; — instead of "tell someone" meaning a text message or an email, it can mean "call this web address automatically." That's the piece that let a computer react instead of a human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP (Model Context Protocol)&lt;/strong&gt; — a standard way for an AI model to reach out and use real tools — in our case, letting an AI ask SigNoz real questions instead of us clicking around the dashboard ourselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's genuinely all the vocabulary you need. Everything else is just those five ideas, wired together.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we actually decided to build
&lt;/h2&gt;

&lt;p&gt;We sat down together and sketched this out on a shared doc before writing a single line of code. Five steps, in order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Chaos&lt;/strong&gt; — a fake AI agent runs constantly, and every so often, on purpose, it messes up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traced&lt;/strong&gt; — every single step it takes gets recorded and sent to SigNoz, live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert&lt;/strong&gt; — SigNoz watches all these traces coming in, notices the mess-up pattern, and automatically fires off a webhook — no human clicks anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heal&lt;/strong&gt; — a separate little program receives that webhook and retries the failed thing with better settings. Still no human.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explain&lt;/strong&gt; — an AI "Sidekick" you can literally chat with, which reads the real SigNoz data through MCP and tells you, in plain English, what just happened.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmejtgjzyagktsdls0thw.jpg" 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%2Fmejtgjzyagktsdls0thw.jpg" alt="Ouroboros AI architecture diagram showing Agent Workload, SigNoz Observability Engine, Auto-Healer Service, and AI Diagnostic Sidekick" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Steps 1 and 2 were a more serious version of what we'd already built in post two. Steps 3, 4, and 5 didn't exist anywhere yet. That's where basically the entire week went.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: giving our fake agent something real to fail at
&lt;/h2&gt;

&lt;p&gt;Our agent from post two never actually &lt;em&gt;failed&lt;/em&gt; — it just got randomly slower or faster each time. That's fine for a screenshot, but it's useless for an alert. You can't tell a computer "notice when things feel kind of slow sometimes." You need something crisp: a clear line that gets crossed.&lt;/p&gt;

&lt;p&gt;So we gave our agent one specific way to break: &lt;strong&gt;context overload&lt;/strong&gt;. In plain terms — our agent normally looks through somewhere between 1 and 8 "documents" to answer a question. But 20% of the time, on purpose, we let it get buried under 15 documents at once, which is way more than it can handle cleanly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;retrieve_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_chaos_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;tracer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_as_current_span&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;retrieve_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# CHAOS MODE: 15 docs, NORMAL: 1-8 docs
&lt;/span&gt;        &lt;span class="n"&gt;context_docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_chaos_mode&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;llm.context_docs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context_docs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.4&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mock context data (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context_docs&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; docs)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F896eycflh6tqrjthz9vb.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%2F896eycflh6tqrjthz9vb.png" alt="SigNoz traces explorer showing live spans from deep-agent-service including llm_call and retrieve_context" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We picked this specific failure on purpose, not randomly. Real AI agents that search through documents actually run into this exact problem — feed them too much information at once, and they get slower, cost more, and often don't even answer better for it. We wanted our fake failure to be one that real systems actually have, not something we made up just to have a demo.&lt;/p&gt;

&lt;p&gt;When the overload happens, we don't just log it as "a bit slow." We mark it as an actual error, with a clear message SigNoz can watch for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;context_docs&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CONTEXT_OVERLOAD: Agent failed. Used &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; tokens, cost $&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, took &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set_status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;StatusCode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Context Overload&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One small thing from post two turned out to matter a lot here: we'd mentioned back then that just printing something to your terminal doesn't automatically send it to SigNoz — you have to specifically wire up your logs to go there too, separately from your traces. This time, we made sure that was working properly from day one, because our whole alert depended on SigNoz actually being able to &lt;em&gt;see&lt;/em&gt; that error message, not just a slightly-slower-than-usual span.&lt;/p&gt;

&lt;p&gt;We let this fake agent run forever in the background, quietly sending a new trace every 200 milliseconds, so there was always something fresh happening while we built the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step three and four: teaching the system to fix itself
&lt;/h2&gt;

&lt;p&gt;This is the part that didn't exist in either of our earlier posts, and honestly, the part we're most excited about.&lt;/p&gt;

&lt;p&gt;The good news is, we didn't have to build anything fancy to make SigNoz &lt;em&gt;watch&lt;/em&gt; for the failure — it already does that with alert rules, the exact same feature we used in post one to watch for slow calls. The only difference is where the alert points. Instead of sending a Slack message or an email, we pointed it at a &lt;strong&gt;webhook&lt;/strong&gt; — a plain web address that SigNoz calls automatically the moment it sees the problem.&lt;/p&gt;

&lt;p&gt;On the other end of that web address, we built a tiny program whose only job is to sit there, wait, and fix things when it gets called:&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="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/webhook&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&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;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;alert_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;alertname&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;Unknown Alert&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;🚨 ALERT RECEIVED FROM SIGNOZ: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;alert_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;optimized_llm_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Auto-healed request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;span&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AUTO-HEAL SUCCESS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;attributes&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;message&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request auto-healed with optimized parameters&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;status&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;healed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;healed&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;result&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pjypzmweeq1qot275qu.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%2F3pjypzmweeq1qot275qu.png" alt="SigNoz service list showing deep-agent-service and auto-healer-service with P99 latency and error rate" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The actual fix is simple on purpose — it retries the request with a smaller, safer amount of information instead of the overload amount. The part we spent real time thinking about wasn't the fix itself, though. It was this question: &lt;strong&gt;should the fix be invisible, or should it leave a trace of its own?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We almost let it be invisible — quietly patch things up in the background and move on. But that felt like exactly the thing we were trying to get away from in the first place: a fix that only lives in someone's head, not in the system. So we made the healer trace its own retry too, tag it clearly as an auto-heal, and record an event that says, plainly, "this got fixed automatically." Now the healing itself shows up in SigNoz right next to the failure that caused it. Nobody has to remember it happened. The system remembers for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step five: teaching an AI to explain all of it in plain English
&lt;/h2&gt;

&lt;p&gt;Here's where post two's biggest lesson came back around. Back then, we discovered that if you tag your data with the right detail, SigNoz can answer real questions for you — no manual math required. The bigger question that kept nagging at us afterward was: what if you didn't even need to know which button to click, or which filter to type? What if you could just &lt;em&gt;ask&lt;/em&gt;, like you're talking to a person?&lt;/p&gt;

&lt;p&gt;That question is what led us into MCP — the protocol that lets an AI model reach out and actually use tools, instead of just talking. Neither of us had touched MCP before this hackathon, so before writing any code, we spent a real evening just reading about how it works. That reading turned out to save us a lot of pain later, even though it didn't save us from all of it.&lt;/p&gt;

&lt;p&gt;What we ended up building is a small chat app we call the &lt;strong&gt;Sidekick&lt;/strong&gt;. You type a question, and behind the scenes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You type a question
   → Groq (the AI model) decides which tool it needs
   → it asks SigNoz for the real data through MCP
   → the real numbers come back
   → the AI reads them and answers you in plain English
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;SigNoz's MCP connection offers over 40 different tools it can use — way more than we needed — so we limited our Sidekick to five: list services, get traces, get logs, get metrics, get one specific span. Simple, focused, and cheap to run.&lt;/p&gt;

&lt;p&gt;Getting this actually working took two separate evenings of head-scratching, and both problems were the sneaky kind — nothing crashed, things just quietly didn't work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The first problem:&lt;/strong&gt; our AI would just never use any tools. It would answer vaguely instead of actually checking real data, no matter how directly we asked it something like "what services are running right now." We spent a while assuming the AI itself was being stubborn, tweaking our instructions to it, before we finally printed out exactly what we were sending it — and found out the tool descriptions we were passing along were completely empty. Turns out SigNoz describes its tools using a label called &lt;code&gt;inputSchema&lt;/code&gt;, while the AI model we were using expects a label called &lt;code&gt;parameters&lt;/code&gt;. We were only checking for one of the two. The fix was a single line of code, but finding it meant working backwards through several layers of "this should be working" first:&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;schema&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inputSchema&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&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;object&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;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The second problem&lt;/strong&gt; was stranger. The very first request would work perfectly, and then the second one would get flatly rejected — every time. It took a while to notice that the server hands you an invisible little "session ID" the first time you connect, and expects you to send it back on every request after that, like a wristband at an event. Miss that, and it treats you like a stranger who just walked in:&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;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mcp-Session-Id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On top of that, some answers came back in a slightly unusual format made for streaming data, instead of a plain, simple response — so our little connector had to learn to handle both.&lt;/p&gt;

&lt;p&gt;None of this felt exciting while it was happening. It was two nights of adding print statements, staring at raw technical details, and slowly ruling things out one at a time. But the moment both fixes were in, the payoff was immediate. Post two's big win was clicking through a filter and a group-by to get a table. This time, the same kind of insight just... showed up, after typing a normal sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Which services are currently being monitored in SigNoz?"&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&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;"answer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The services currently being monitored are:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- auto-healer-service — 1 trace in the last hour (avg ~494ms)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;- deep-agent-service — 688 traces in the last hour (avg ~1.63s)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tools_used"&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;"signoz_list_services"&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="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0zya6vmsgnwq2byzbcnt.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%2F0zya6vmsgnwq2byzbcnt.png" alt="Ouroboros AI Sidekick chat interface answering a question about deep-agent-service traces using real SigNoz data" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We kept the AI's instructions short and strict on purpose — answer in a few sentences, use the real numbers you found, don't waste time asking for logins it already has. It's meant to feel like a fast, useful teammate answering a quick question, not a general chatbot rambling.&lt;/p&gt;

&lt;h2&gt;
  
  
  The moment it actually came together
&lt;/h2&gt;

&lt;p&gt;Here's the part that felt like more than just code working.&lt;/p&gt;

&lt;p&gt;We finally let all three pieces run at the same time and just watched. In one window, the fake agent quietly sending traces every 200 milliseconds, like it had been doing for days. In another, the Sidekick sitting there, waiting for a question. And then, in the third window — completely on its own, with nobody typing a single command — the healer suddenly printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🚨 ALERT RECEIVED FROM SIGNOZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We hadn't triggered that. SigNoz had caught the overload pattern by itself, in the live stream of data, and called the webhook on its own.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgpfcj7are1ruefhfg4as.jpg" 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%2Fgpfcj7are1ruefhfg4as.jpg" alt="Ouroboros AI landing page showing the self-healing incident timeline: context overload detected, span exported to SigNoz" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the moment this stopped being a demo we were operating and started being a system that was actually running itself.&lt;/p&gt;

&lt;p&gt;About thirty seconds later, we asked the Sidekick what had just happened, half-expecting to have to nudge it toward the right answer. We didn't need to. It described the overload, the automatic retry, and the corrected numbers, pulled straight from the same live data we'd watched land in SigNoz seconds earlier. That was the entire five-step loop, closing itself, start to finish, with nobody standing in the middle of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're taking away from all three posts
&lt;/h2&gt;

&lt;p&gt;Post one taught us that just &lt;em&gt;seeing&lt;/em&gt; where the time goes changes how you think about a problem. Post two taught us that tagging the right detail turns a dashboard into something you can actually have a conversation with. This project is where those two lessons stopped being separate little discoveries and turned into one real habit: instrument things not just to log what's easy right now, but to answer the question you'll actually want answered later — because that same detail is what lets an alert fire cleanly, and what lets an AI explain the whole story back to you without you writing a single extra line of code for it.&lt;/p&gt;

&lt;p&gt;If someone asked us what we'd tell ourselves before starting this hackathon, it's that one sentence. Everything else — the failures, the debugging nights, the late realization that our first agent needed to actually break instead of just wobble — all came from following that one idea seriously enough to build around it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Full code, the landing page, and everything else is in the repo: &lt;a href="https://github.com/mauryasagar/ouroboros-ai" rel="noopener noreferrer"&gt;github.com/mauryasagar/ouroboros-ai&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By Sagar Maurya &amp;amp; Disha Sonowal — WeMakeDevs SigNoz Hackathon&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>signoz</category>
      <category>wemakedevs</category>
      <category>agents</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>We Asked SigNoz a Question, and It Actually Answered</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sun, 19 Jul 2026 16:51:23 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/we-asked-signoz-a-question-and-it-actually-answered-p6o</link>
      <guid>https://dev.to/sagarmaurya/we-asked-signoz-a-question-and-it-actually-answered-p6o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By Sagar Maurya &amp;amp; Disha Sonowal — WeMakeDevs SigNoz Hackathon&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Before this, we wrote a &lt;a href="https://dev.to/dishasonowal/we-traced-a-fake-ai-agent-before-building-a-real-one-heres-what-broke-1e7d"&gt;first post&lt;/a&gt; about setting up SigNoz for the very first time — installing it, sending it some data, and figuring out what all this "tracing" stuff actually means. You can also check that out on &lt;a href="https://www.linkedin.com/posts/disha-sonowal-95831a29b_we-traced-a-fake-ai-agent-before-building-activity-7481685838620372992-OWeV" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. If you're new to any of this, that post explains the basics in a simple way, so it's worth a quick look first.&lt;/p&gt;

&lt;p&gt;This post is us going one step further as a team. We wanted to see if SigNoz could actually help us &lt;em&gt;discover&lt;/em&gt; something, not just show us pretty charts. It did, and here's exactly what happened, explained simply.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, two words you need to know
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trace&lt;/strong&gt; — a record of everything that happened during one single request, from the moment it starts to the moment it finishes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Span&lt;/strong&gt; — one step inside that trace. If a request does 4 things, it has 4 spans, one for each step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's really all you need to follow along.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built
&lt;/h2&gt;

&lt;p&gt;A small fake AI assistant. Instead of doing just one thing, it does four things every time someone asks it a question:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Look up some documents&lt;/strong&gt; — pretend to search for information related to the question (a random number, somewhere between 1 and 8 documents)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the AI&lt;/strong&gt; — pretend to send the question and those documents to an AI model and get an answer back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use a tool&lt;/strong&gt; — pretend to use something like a calculator, a calendar, or a search tool&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Clean up the answer&lt;/strong&gt; — tidy up the final response before sending it back&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We wrapped each of these four steps in code that tells SigNoz "hey, this step just started" and "hey, this step just finished." That's what creates the spans.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking at just one question it answered
&lt;/h2&gt;

&lt;p&gt;We ran our fake assistant 30 times, and then picked one single run to look at closely in SigNoz. Here's what it showed us:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fx1i4jvh7ghleiti3p3eq.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%2Fx1i4jvh7ghleiti3p3eq.png" alt="flame graph, handle_request 1.72s, llm_call 1.43s" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The whole thing took 1.72 seconds. Almost all of that — 1.43 seconds — was spent in the "ask the AI" step. Everything else (looking up documents, using a tool, cleaning up) barely took any time at all in comparison. You can literally see this with your eyes on the chart, no reading through code needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then we asked a bigger question
&lt;/h2&gt;

&lt;p&gt;Looking at one run is useful, but we wanted to know something bigger: &lt;strong&gt;does the AI step get slower when it has more documents to look through?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In real life, this makes sense — if you ask an AI to read through 8 documents instead of 1, it usually takes longer to answer, because there's more for it to process.&lt;/p&gt;

&lt;p&gt;So instead of checking each of our 30 runs one by one, we asked SigNoz to do the work for us:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We told it: "only show me the AI-call steps, ignore the other three"&lt;/li&gt;
&lt;li&gt;We told it: "now group these by how many documents each one used"&lt;/li&gt;
&lt;li&gt;We told it: "show me the average time for each group"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And within seconds, SigNoz gave us this table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Documents used&lt;/th&gt;
&lt;th&gt;Average time taken&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;568ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;658ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;825ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;999ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;1147ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;1333ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;1488ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;1614ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frahttpjgaufc3d2n3mt5.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%2Frahttpjgaufc3d2n3mt5.png" alt="grouped table" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Look at that — it climbs almost perfectly evenly. Every extra document adds roughly 150 milliseconds to the response time. We built this rule into our code, so finding it wasn't a total surprise, but what genuinely impressed us was &lt;em&gt;how easily&lt;/em&gt; SigNoz found and displayed it. We didn't write any extra code, we didn't open Excel, we didn't do any manual math. Just a couple of clicks, and the whole pattern was right there in a table.&lt;/p&gt;

&lt;h2&gt;
  
  
  We also checked the logs
&lt;/h2&gt;

&lt;p&gt;Traces are great for timing, but sometimes you just want to read a plain sentence about what happened. That's what logs are for. We turned those on too, and got results like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn5sldhfsjklh87lkpyrn.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%2Fn5sldhfsjklh87lkpyrn.png" alt="logs showing entries like " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now every request has two views: the trace shows &lt;em&gt;how long&lt;/em&gt; things took and &lt;em&gt;in what order&lt;/em&gt;, while the logs show &lt;em&gt;what actually happened&lt;/em&gt;, in plain readable sentences. Small tip if you try this yourself — just printing messages to your terminal doesn't automatically send them to SigNoz. You need a couple of extra lines of setup code to actually connect them, or nothing will show up in the Logs tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're taking away from this
&lt;/h2&gt;

&lt;p&gt;The biggest thing we learned: a trace waterfall is great for spotting &lt;em&gt;what's&lt;/em&gt; slow. But the real magic happened when we tagged our own extra detail onto a span — in our case, "how many documents did this use." That one small addition is what let us ask SigNoz a real question and get a real answer back, across all 30 runs at once, instead of guessing.&lt;/p&gt;

&lt;p&gt;For the actual hackathon build, this is exactly the habit we want to carry forward — don't just time your code, tag it with details that'll actually matter later when something's slow and you're trying to figure out why.&lt;/p&gt;

</description>
      <category>signoz</category>
      <category>wemakedevs</category>
      <category>agents</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>Data Isn't What Your Textbook Said It Was</title>
      <dc:creator>Sagar Maurya</dc:creator>
      <pubDate>Sat, 11 Jul 2026 02:13:37 +0000</pubDate>
      <link>https://dev.to/sagarmaurya/data-isnt-what-your-textbook-said-it-was-3df8</link>
      <guid>https://dev.to/sagarmaurya/data-isnt-what-your-textbook-said-it-was-3df8</guid>
      <description>&lt;h3&gt;
  
  
  &lt;em&gt;A beginner-friendly breakdown of what data really is, where it comes from, and why it matters more than you think.&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
You woke up this morning and checked your phone.&lt;/p&gt;

&lt;p&gt;You scrolled through Instagram. Opened WhatsApp. Maybe checked the weather. Ordered breakfast on Swiggy.&lt;/p&gt;

&lt;p&gt;By the time you finished your morning tea, you had already generated hundreds of data points — and that was before 9 AM.&lt;/p&gt;

&lt;p&gt;We live in a world that runs on data. But most people, including a lot of CS students, have never stopped to ask: &lt;strong&gt;what actually is data?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not the textbook definition. The real one.&lt;/p&gt;

&lt;p&gt;And that's exactly the problem — we all learned about data from a textbook. Which means we learned about it wrong.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Textbook Lie
&lt;/h2&gt;

&lt;p&gt;Ask anyone what data is and they'll say something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Data is raw, unprocessed facts and figures."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Technically correct. Completely useless.&lt;/p&gt;

&lt;p&gt;That definition tells you nothing about why data matters, where it comes from, or what makes it powerful. It's the kind of definition written for an exam, not for understanding.&lt;/p&gt;

&lt;p&gt;So let's close the textbook and start over.&lt;/p&gt;


&lt;h2&gt;
  
  
  Data is Just a Record of Something That Happened
&lt;/h2&gt;

&lt;p&gt;Every time something happens in the real world — a click, a purchase, a step you walked, a message you sent — there's a possibility of recording it.&lt;/p&gt;

&lt;p&gt;When you record it? That's data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You clicked on a product → &lt;strong&gt;data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Your phone counted 6,400 steps today → &lt;strong&gt;data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You paused a YouTube video at 2:34 → &lt;strong&gt;data&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You left a website after 3 seconds → &lt;strong&gt;data&lt;/strong&gt; (and a headache for that website's owner)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data is just a trace that something happened. That's it.&lt;/p&gt;

&lt;p&gt;The interesting part is what happens &lt;em&gt;after&lt;/em&gt; you collect those traces.&lt;/p&gt;


&lt;h2&gt;
  
  
  Raw Data is Almost Always Ugly
&lt;/h2&gt;

&lt;p&gt;Here's what nobody tells beginners: data in the real world is messy.&lt;/p&gt;

&lt;p&gt;Imagine a Google Form that asks for someone's phone number. You'll get responses like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;9876543210
+91-9876543210
98765 43210
9876543210 (call after 6pm)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All four people gave you the same number. But to a computer, these are four completely different values.&lt;/p&gt;

&lt;p&gt;This is what's called &lt;strong&gt;dirty data&lt;/strong&gt; — and cleaning it is genuinely one of the most important (and underrated) skills in the data field.&lt;/p&gt;

&lt;p&gt;A famous saying in data science is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"80% of data work is cleaning. The other 20% is complaining about cleaning."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It's a joke. But not really.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Journey of Data: From Noise to Decision
&lt;/h2&gt;

&lt;p&gt;Here's a simple way to think about how data travels:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Something happens
      ↓
It gets recorded (raw data)
      ↓
It gets cleaned (processed data)
      ↓
It gets analyzed (insights)
      ↓
Someone makes a decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's make this real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; Swiggy wants to know why orders drop on Tuesday evenings.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;They collect order timestamps → &lt;em&gt;raw data&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;They remove duplicates, fix timezone errors → &lt;em&gt;clean data&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;They spot that Tuesday 7–9 PM has 40% fewer orders → &lt;em&gt;insight&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;They run a Tuesday evening discount campaign → &lt;em&gt;decision&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That entire chain? It all started with someone pressing "Order" on their phone.&lt;/p&gt;




&lt;h2&gt;
  
  
  Not All Data is Numbers
&lt;/h2&gt;

&lt;p&gt;A common misconception is that data means spreadsheets full of numbers. But data comes in many forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured data&lt;/strong&gt; — rows and columns, like a CSV or a database table. Easy for computers to process.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unstructured data&lt;/strong&gt; — text, images, audio, video. Harder to process, but far more common in the real world.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semi-structured data&lt;/strong&gt; — like JSON or XML. Has some structure, but not rigid rows and columns.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you send a WhatsApp message, that's unstructured data. When your bank logs a transaction, that's structured data. When you fill a Google Form, that produces semi-structured data.&lt;/p&gt;

&lt;p&gt;Most of the world's data — over 80% of it — is unstructured. Which is why fields like NLP (Natural Language Processing) and Computer Vision exist: to make sense of data that doesn't fit neatly into a table.&lt;/p&gt;




&lt;h2&gt;
  
  
  Your Data is Someone's Product
&lt;/h2&gt;

&lt;p&gt;Here's the uncomfortable truth.&lt;/p&gt;

&lt;p&gt;Every free app you use — Instagram, Google, YouTube — is free because &lt;em&gt;you&lt;/em&gt; are not the customer. You're the product. More precisely, your data is.&lt;/p&gt;

&lt;p&gt;When you like a post, skip an ad, or spend 45 minutes on Reels instead of 5, that behavior is recorded, analyzed, and used to serve you more content that keeps you on the platform longer.&lt;/p&gt;

&lt;p&gt;This isn't conspiracy theory. It's just the data pipeline at scale.&lt;/p&gt;

&lt;p&gt;Understanding this doesn't mean you need to delete all your apps. But it does mean you should be aware of the trade you're making — your attention and behavior in exchange for a free service.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Everyone Should Care About Data (Not Just Data Scientists)
&lt;/h2&gt;

&lt;p&gt;You don't need to work in data to benefit from understanding it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;As a developer&lt;/strong&gt; — you'll build systems that generate data. Understanding data helps you design better databases and APIs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a product person&lt;/strong&gt; — decisions without data are just opinions. Data makes arguments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a user&lt;/strong&gt; — knowing how your data is used makes you a more informed digital citizen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;As a student preparing for placements&lt;/strong&gt; — almost every tech company today is a data company in some way. Interviews increasingly involve data thinking, even for SDE roles.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Data literacy is becoming as fundamental as being able to read and write.&lt;/p&gt;




&lt;h2&gt;
  
  
  So What Did Your Textbook Get Wrong?
&lt;/h2&gt;

&lt;p&gt;Nothing, technically. But everything, practically.&lt;/p&gt;

&lt;p&gt;Your textbook gave you a definition of data that would pass an exam. It didn't give you a mental model that would help you think about the world differently.&lt;/p&gt;

&lt;p&gt;The difference between a student who "knows data" and someone who &lt;em&gt;understands&lt;/em&gt; it is this: one memorized a definition, the other sees data everywhere they look — in the apps they use, the decisions companies make, and the systems they build.&lt;/p&gt;

&lt;p&gt;That shift in perspective is what this blog is about. Not definitions. Mental models.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where to Go From Here
&lt;/h2&gt;

&lt;p&gt;This post was just the surface. In future posts, I'll go deeper into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How databases store and retrieve data efficiently&lt;/li&gt;
&lt;li&gt;What actually happens when you run a SQL query&lt;/li&gt;
&lt;li&gt;The difference between a data analyst, data engineer, and data scientist&lt;/li&gt;
&lt;li&gt;And some hands-on projects that helped me understand all of this better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a student or someone just getting into tech — I hope this gave you a clearer mental model of what data actually is.&lt;/p&gt;

&lt;p&gt;Because before you can work with data, you need to understand what you're actually working with.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Thanks for reading! If you found this useful, drop a reaction or share it with someone who's just getting started in tech.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>data</category>
      <category>career</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
