DEV Community

Siesta - building a local-first agent that naps until something changes

Gemini CLI 39 messages
by faijaleaqbal
Siesta - building a local-first agent that naps until something changes
You

Hackathon prompt: build something with open-source AI at its core for the Hacktoberfest Open-Source AI Challenge. Theme: 'Slow and Steady' - build something that rewards laziness, that does the work so you don't have to. Open innovation matters: offline, private, swappable, free. Writing quality is weighted most heavily in judging.

Agent

First I want the real rules, not the summary. The DevRelay gateway can read the live event, including the unabridged full_details dump the organizers publish for agents.

Agent
Agent

Scope confirmed. Now the machine: I need to know whether local inference is even viable here, and how much disk it costs, before I commit to a design.

Agent
You

total how much gb ita consume for three models

Agent
Agent

CPU-only inference, 6.55GB, approved. Now the design.

The theme is laziness, and the interesting technical question is not 'can a small model do vision' - it is 'how do you build an agent whose default posture is to do nothing'. So the spine of the project is a cascade that stops at the cheapest stage it can be confident at:

stage 0 hash check free, and it means the agent sleeps when nothing changed
stage 1 metadata gate free, 0 tokens, filename and folder decide most files
stage 2 cheap vision gate a tiny VLM, optional
stage 3 escalate the reasoning model, only when genuinely unsure
stage 4 one digest one page for the whole batch, never one per file

Plus semantic duplicate suppression: embed the draft digest, compare with the last one, stay silent if the cosine is above 0.965. Re-photographing the same fridge must not produce a second shopping list.

Agent

Writing the core now. Zero third-party runtime dependencies - it talks to Ollama over plain HTTP with urllib, so it runs on a bare Python 3.10+ install.

Agent
Agent
Agent

Both fixed. The energy gate becomes opt-in rather than the default, because an assistant that never runs is not helpful.

Agent
Agent

Now the part that matters - testing against the real models rather than the stub. First call to the live vision model failed:

Agent
Agent

A real bug the stub could never have caught: Ollama's native /api/chat wants content as a plain string. The OpenAI-compatible endpoint accepts an array of content parts, but the native one does not. My message builder was OpenAI-shaped. Fixed - images travel in the request's images array, not inside the message content.

Agent
Agent

Structurally valid and completely wrong. A hallucinating fridge inventory is worse than no inventory at all, so before building anything on top of this I ran a prompt experiment with four strategies and measured precision and recall against ground truth I annotated by hand.

Agent
Agent

Strategy B wins and the reason is interesting. Asking a 3B vision model for full structured records in one shot invites it to fill in a plausible fridge. Asking it to enumerate what it can see, shelf by shelf, with a confidence for each object, killed the hallucinations outright.

So the pantry chore becomes three steps: scan with the vision model (short output, so it is also fast on a CPU), filter on confidence, then let the cheap text model fill in category/quantity/expiry for the confirmed names only. The text model cannot introduce an item that was never confirmed.

Agent
Agent
Agent

Eight of eight, zero invented, and I was about to write that up as a success. Then the receipt came back wrong - 'The Store, 100.00 USD, 3 items' for a receipt whose printed total is $32.24. Same root cause, same fix: transcribe with the vision model, parse with the text model.

Agent
Agent
Agent

And then the giveaway. Four different preprocessed images came back with byte-identical answers. Four different images cannot produce the same output, so the image was not being processed at all - the model was answering from the prompt alone.

Agent
Agent

That was the whole bug, and it had been there from the start.

Ollama's /api/chat accepts a request-level images field and silently ignores it. The model then answers from the text of the prompt alone. And because my prompt listed plausible example items - 'milk, eggs, cheddar cheese, spinach' - the blind model parroted my own prompt back at me, which looked exactly like a perfect reading of a fridge I had photographed.

The eight-of-eight score was an illusion. The model had never seen the picture; it had copied my instructions, and my instructions happened to mention the right things because I had written them after looking at the photo myself.

Two things came out of that. First, the client now puts the images inside the message, where Ollama actually reads them, and the real prompt token count jumped from 198 to 1587 - the picture is now genuinely in the context. Second, and more important, the project now verifies that the image arrived: an attached image costs about a thousand prompt tokens, so every vision call asserts on that, and siesta doctor sends a red-and-green test picture and checks the model can describe it.

Agent
Agent
Agent
Agent

That is the real number, and it is a real number rather than a flattering one. A 3B open-weight vision model on a CPU-only laptop reads a fridge photo with high recall and occasional duplicates, and it does not read a crumpled receipt reliably - which is why the receipt chore now verifies the printed total against the sum of the lines and says so when they disagree, instead of writing a fake expense into the ledger.

And this is the part that makes the open-weights argument concrete rather than sentimental: the fix for a weak model is a file. If a 7B vision model reads receipts better, that is one flag. With a hosted API the only remedy for 'the small model cannot do this' is to wait for the vendor to ship a better one.

Agent

The receipt took one more round. Asking the vision model for an array of strings - one entry per line of the receipt - made it degenerate: it echoed my instructions and then produced 100.00, 200.00, 300.00... until it ran out of tokens. The same trick that fixed the fridge fixed this: ask for one plain string, and let the text model do the structuring.

Agent
Agent

$32.24, and every item name and price correct. The printed total and the line items agree, so the expense lands in the ledger as a verified fact rather than a guess.

Two smaller bugs fell out of the same run. The shopping list was telling me to buy milk while the fridge photo showed a milk carton, because the staple list said 'milk' and the model said 'milk carton' and the comparison was a raw string match. Now there is a canonical-name function that drops packaging words and plurals. And the plan step was inventing recipes - 'preheat oven to 350F, slice bread and spread with butter' - for a fridge that contained no bread, so its prompt now says every step must refer to a named item and nothing else.

Agent
Agent

One more fix along the way, worth recording: the plan prompt was asking for 'the smallest set of actions', and the model helpfully invented a recipe - 'preheat oven to 350F, slice bread and spread with butter' - for a fridge that contained no bread. The prompt now says every step must refer to a named item, and nothing else.

Agent
Agent

Shipped. The repo is at github.com/faijaleaqbal/siesta, with 61 tests that run against a deterministic stub backend so a judge can clone it and run pytest with no model download at all.

The closing thought, which is really the whole submission: the theme was 'Slow and Steady', and the honest reading of it turned out to be about restraint rather than speed. Hosted AI pushes hard the other way - every call costs money, so the systems get louder and chattier. Running the weights locally is what made the quiet version possible.

And the second thing, which I did not expect: in a system that reads the world, you have to prove that it looked. My best-looking result was the model copying my own prompt back at me. The check that caught it - count the tokens, and test the model against an image whose answer you already know - is three lines long, and it is now the part of this project I am proudest of.