DEV Community

Cover image for Déjà vu, but billable
Lewis Sawe
Lewis Sawe Subscriber

Posted on AI-assisted

Déjà vu, but billable

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle

What I Built

CaseJog is a local semantic memory for an independent appliance repair tech. You type the symptom in front of you, optionally pick the appliance you're actually holding, and it ranks your own past jobs that match: root cause, parts, labor time, price, and a source citation on every card. The whole thing runs on the bench. The case history never leaves the machine.

Who pays for this? The people who already pay for the adjacent thing. A one-person repair shop spends $29 to $80+ a month on tools like RepairDesk or an iFixit Pro subscription. Those are CRMs and manual libraries. None of them does the one trick that matters most once you've been on the bench a few years: finding your own past fix when the new customer describes it with completely different words.

That gap is the product. Picture a composite tech, call him Marcus, eleven years in, a notebook instead of a CRM. A microwave comes in, "door won't lock, it just clicks." Marcus burns forty minutes on the latch before he remembers he solved this exact fault a year ago on another brand, written down as "won't start with door shut." Same dead door-switch interlock. Different words. He'd already earned that fix once and paid for it again in lost time. (Marcus is a stand-in, not a real quoted user. I didn't have a technician to hand this to inside the window, so I'm naming that instead of inventing a testimonial.)

Keyword search can't rescue him, because the words never line up. "Won't spin," "drum stuck," and "no agitation" are one fault written three ways. What a tech needs is not a smarter diagnosis. It's his own past cases, ranked by how close they sit to the thing on the bench, and nothing more clever than that.

Demo

Live demo: casejog.onrender.com. Type a symptom, pick the appliance, and it ranks the real corpus live. MongoDB Atlas vector search runs on the backend; the open embedding model runs in the container. The first query after the service has been idle pays a one-time model-load cost. After that it is a couple of seconds against the free shared cluster.

The hero query is the one that started it. The tech is holding a microwave and types the door-lock symptom. Here is CaseJog ranking his own past jobs for it, each card showing the root cause, parts, labor, price, and a source case:

CaseJog ranking past microwave door-switch cases for the symptom

Here is what the dumb baseline (pure cosine, no idea what appliance is in his hand) actually returns:

== BASELINE (pure cosine, no filter) ==
  0.805  JOB-0038  washing machine   Door lock mechanism failed
  0.800  JOB-0028  washing machine   Door lock mechanism failed
  0.799  JOB-0029  washing machine   Door lock mechanism failed
  0.794  JOB-0033  washing machine   Door lock mechanism failed
  0.788  JOB-0032  washing machine   Door lock mechanism failed
Enter fullscreen mode Exit fullscreen mode

Five washing machines. All wrong. "Door won't lock" is a louder phrase in the washer cases, so cosine drowns in them. Now the same query with the appliance filter fused into the search, the real API response, trimmed:

engine: filtered-hybrid   count: 5
  0.864  JOB-0218  microwave  Door switch interlock faulty  [cite JOB-0218 2025-02-23 $75]
  0.863  JOB-0212  microwave  Door switch interlock faulty  [cite JOB-0212 2024-05-23 $75]
  0.855  JOB-0215  microwave  Door switch interlock faulty  [cite JOB-0215 2025-08-04 $75]
  0.852  JOB-0223  microwave  Door switch interlock faulty  [cite JOB-0223 2023-04-08 $75]
  0.851  JOB-0219  microwave  Door switch interlock faulty  [cite JOB-0219 2024-02-01 $75]
Enter fullscreen mode Exit fullscreen mode

Five microwave door-switch cases, each pointing at a real job with the price charged. This is the top-k the flat cosine loop cannot produce, because it has no notion of the appliance on the bench. Steady-state retrieval is 72ms (the first request pays a one-time model-load cost). The filter is the difference between the right shelf and the wrong one.

The exact-signal half of the hybrid shows up when an error code is in play. "fridge not cold, code 5E," filtered to fridge:

  final=1.024 vec=0.874 boost=0.150  JOB-0098 code=5E  Defrost heater failure, evaporator iced over
  final=1.022 vec=0.872 boost=0.150  JOB-0101 code=5E  Defrost heater failure, evaporator iced over
  final=1.018 vec=0.868 boost=0.150  JOB-0102 code=5E  Defrost heater failure, evaporator iced over
Enter fullscreen mode Exit fullscreen mode

The boost=0.150 column is an exact 5E match firing on top of vector similarity. Meaning plus exact signal, which is what keeps the right case on top when a near-but-wrong case is just as close in plain language.

Did it beat the dumb baseline? (held-out, run once)

I built the pure-cosine baseline first and only kept the heavy path because it won. precision@k over 28 held-out symptoms, worded so they do not appear in the corpus, run once, never tuned:

  k   filtered-hybrid   baseline-cosine
---  ----------------  ----------------
  1             0.893             0.857
  3             0.869             0.738
  5             0.814             0.657
Enter fullscreen mode Exit fullscreen mode

Filtered-hybrid wins at every k, and the gap grows as it reaches deeper: by k=5 the baseline has drifted onto cases close in meaning but wrong in cause, while the filter and the error-code boost hold the line. With n=28 the margins are suggestive, not a p-value, and I won't pretend otherwise. The direction is consistent and the reason is legible.

Code

CaseJog

A 100% local semantic memory for an independent appliance / small-electronics repair technician. Type a new symptom and instantly get a ranked list of your own past jobs that match — each with root cause, parts, time, and price — so you recall a fix you have seen before instead of re-diagnosing from scratch.

Output is always framed as your past cases, ranked — a recall aid a human verifies, never an autonomous diagnosis. Every result cites its source case (id, date, price): grounded recall.

Why local / open (the open-vs-closed thesis)

The private case corpus is the technician's hard-won edge and never leaves the bench. Local open embeddings (fastembed, ONNX, CPU) make every query $0, offline, and private. The embedding model is swappable in one place (embed.py / the EMBED_MODEL env var) — the open-weight model is the mechanic and it is replaceable…

How I Built It

symptom -> embed.py (fastembed, local) -> query vector
                                            |
        +-----------------------------------+
        v                                   v
 baseline.py                          search.py
 pure cosine, no filter               Atlas $vectorSearch (store.py)
 the dumb baseline + fallback         + metadata filter inside the stage
                                      + hybrid re-rank (error_code + recency)
                                      + a source citation on every result
        +----------------+------------------+
                         v
                   app.py / static/index.html
Enter fullscreen mode Exit fullscreen mode

The open-source AI at the core is an open-weight embedding model, BAAI/bge-small-en-v1.5, run locally on CPU through fastembed on ONNX. The retrieval engine is MongoDB Atlas vector search. The discipline I kept throughout: embeddings and Atlas surface the candidates, code owns the ranking, and no model ever writes a fix. There is no generative model in the answer path. A card is always a real past job you can open and check.

Two pieces are where the open-weight argument stops being decorative:

  • The swappable embed() seam. All embedding goes through one function, selected by a single env var (EMBED_MODEL). Change the model in one line and nothing else in the pipeline moves. The open model is the mechanic, and the mechanic is replaceable.
  • The offline cosine fallback. app.py runs the Atlas retriever by default, but if Mongo is unreachable it drops to the in-memory cosine baseline and says so in the response. Pull the plug on the database and CaseJog still answers:

CaseJog still answering with MongoDB down: an OFFLINE FALLBACK banner and a

I scoped that fallback to Mongo connection errors on purpose, so a real bug surfaces instead of hiding behind the word "fallback."

The store runs the filter inside the $vectorSearch stage, not as a post-filter. That is what makes the microwave query correct, instead of correct only after discarding four wrong washers.

Why Does Open Innovation Matter?

The corpus is the business. A tech's pile of past jobs is his edge, and the moment it has to be uploaded to a cloud service to be searchable, that edge is now someone else's asset and every query is a line on a bill. CaseJog inverts all four of those: the embeddings run locally through an open model, the case data stays on the bench, every query is free and offline, and the ranking is a few hundred lines of Python you can read. The model is open and swappable. The data is private and stays home. A closed cloud API gives you the opposite on every count, and for a one-person shop whose whole advantage is the notebook, that is not a tradeoff worth making.

Two honest limits, because they are part of the design, not footnotes. First, I caught myself cheating the eval: my initial held-out set shared phrasings with the corpus, so both retrievers scored an inflated ~0.96, the retriever grading its own homework. I reserved held-out phrasings, regenerated, and re-ran once, and the numbers dropped to the honest figures above. Lower and real, and the head-to-head got clearer after the fix. Second, this is recall, not diagnosis, as a hard cap. CaseJog never says what's wrong. It shows which of your past jobs look closest and makes you open them. A fault genuinely new to your history returns the nearest old thing, which may be nothing like the answer, so "a human verifies" is the design, not a disclaimer.

I also tried a 2B decision model as a per-candidate confidence gate and deferred it. Smoke-tested locally first, it confidently mislabeled the near-but-wrong case the error-code boost already gets right, emitted two values dressed as probabilities, and cost about 20 seconds per pair on CPU, which would turn a sub-second search into a 100-second one. It would have made the demo slower and less correct, so it isn't in the code. I'd rather tell you that than claim a feature I don't have.

My Agent Session

The whole build ran through an AI coding agent over SSH on a small cloud box: scaffold, synthetic corpus, the Atlas-local vector index, the baseline-versus-hybrid comparison, and the eval, each step run for real with output captured in the repo's NOTES.md. The run that mattered most was the eval review, where the agent caught the phrasing leak described above and forced the honest re-run.

Prize Categories

  • Best Use of MongoDB Atlas: the appliance filter fused into $vectorSearch, plus an exact error-code boost, returns the correct microwave door-switch cases, each citing its source job, on a query where pure cosine returns five wrong washing machines. The warrant is filter plus exact signal plus grounded citations, not corpus scale.
  • Best Use of Render: the live demo at casejog.onrender.com is a Render web service. It runs the open embedding model and talks to Atlas, with the model warmed at build time so the first query is not a cold load. The public endpoint is rate-limited per IP and caps query size, so an open, unauthenticated demo cannot be turned into a compute sink.

The product is built to run on the bench: one machine, open weights, the case data never leaving. The hosted demo exists so you can click it. Local and free is the version a one-person shop actually runs.

Top comments (0)