KrishnaGarg commited on
Commit
d179e5e
Β·
verified Β·
1 Parent(s): 7daaf82

Deploy BudgetBuddy update

Browse files
Files changed (1) hide show
  1. FIELD_NOTES.md +115 -90
FIELD_NOTES.md CHANGED
@@ -1,94 +1,119 @@
1
- # Field Notes β€” Building BudgetBuddy on small models + Modal
2
 
3
- *Build-story / report for the Build Small hackathon "Field Notes" badge. Edit the
4
- voice to your own, add a couple of screenshots/GIFs, and (ideally) publish it on
5
- the Hugging Face blog or your own site, then link it from the Space.*
6
 
7
  ---
8
 
9
- ## The itch
10
-
11
- My family doesn't use a budgeting app. Not because they don't care about money β€”
12
- because every app wants you to either link your bank, type each expense like a
13
- data-entry clerk, or hand your receipts to some cloud. For a homemaker running a
14
- household on cash, UPI, and a pile of crumpled bills, none of that fits.
15
-
16
- So for the **Build Small** hackathon (Backyard AI track) I built **BudgetBuddy**:
17
- snap a bill, it reads it, and you can literally *ask it* where your money went.
18
- It runs on two **MiniCPM** models and no third-party AI API.
19
-
20
- ## The shape of it
21
-
22
- - **MiniCPM-V-4.6 (1.3B, vision)** reads a receipt *or* a UPI/card screenshot
23
- into structured JSON: vendor, date, line items, tax/service/tip/discount,
24
- round-off, total. Runs on the Space's **ZeroGPU**.
25
- - **MiniCPM4.1-8B (8B, reasoning)** does two jobs: a *reason-and-repair* pass that
26
- cleans up the extraction and categorises it, and a *tool-using agent* that
27
- answers questions about your spending. Runs on **Modal**.
28
- - A custom dark dashboard (no stock Gradio) shows it all.
29
-
30
- ~9.3B params total, comfortably under the hackathon's 32B cap.
31
-
32
- ## Five things that bit me (so they don't bite you)
33
-
34
- **1. "4.6" is a version, not a parameter count.** I assumed MiniCPM-V-4.6 was an
35
- ~8B model like the older 2.6/4.5 line and warned about GPU memory. It's actually
36
- **1.3B**. Always check `safetensors` metadata, not the name.
37
-
38
- **2. The model dropped the API I coded against.** MiniCPM-V's older `.chat()`
39
- method is gone in 4.6 β€” it's now a native `transformers` model
40
- (`AutoModelForImageTextToText` + `AutoProcessor` + `apply_chat_template`). I only
41
- found this by reading the repo's `config.json` and chat template, not from
42
- memory. Lesson: when wiring a model, read its *actual* files first.
43
-
44
- **3. Two models, one impossible dependency.** The vision model needs a modern
45
- `transformers` (β‰ˆ5.7+); MiniCPM4.1-8B's `trust_remote_code` is written for
46
- `transformers` β‰ˆ4.56 and *RuntimeErrors* on 5.x. You cannot satisfy both in one
47
- environment. I burned hours trying to shim the 8B onto new `transformers` before
48
- admitting defeat. **The fix was Modal:** run the 8B in its *own* container with
49
- its own pinned `transformers`, expose a `generate()` method, and call it from the
50
- Space over the Modal SDK. One memory snapshot for fast cold starts, a warm pool
51
- so it stays hot, and the version war just… disappears. Isolating the awkward
52
- dependency on its own compute was the single highest-leverage decision.
53
-
54
- **4. Real dates aren't ISO, and literal OCR isn't "understanding."** My analytics
55
- silently dropped transactions because a bill said `01/07/17` and my parser only
56
- accepted `YYYY-MM-DD`. Separately, the vision model would extract "Subtotal",
57
- "Total" and "Amount Payable" *as line items*, so nothing reconciled. The fix was a
58
- tolerant date parser used everywhere, plus a deterministic post-processor that
59
- understands a literal extraction β€” dropping summary/total rows, moving tax/fee
60
- rows into charges (while keeping utility "energy charges" as items), and
61
- recovering the total. Instant, no model, far less manual fixing.
62
-
63
- **5. Make the agent *plan*, but never let it do the math.** For the assistant I
64
- gave the 8B a ReAct loop over **13 analytics tools** (`total_spend`,
65
- `category_spend`, `vendor_spend`, `biggest_expense` (rank-aware),
66
- `average_monthly`, `budget_status`, …). The model decides *which* tool to call and
67
- chains another when needed; the tools are deterministic Python, so the numbers
68
- can't be hallucinated. Crucially, an answer is only returned once it's **grounded
69
- by a real tool call** β€” if the model free-associates a number, it's rejected and a
70
- deterministic router answers instead. That split β€” *AI plans, code computes* β€” is
71
- what makes a small-model agent both genuinely agentic and trustworthy. (Open
72
- traces: [`AGENT_TRACES.md`](AGENT_TRACES.md).)
73
-
74
- ## The "Off-Brand" bit: a custom frontend on `gradio.Server`
75
-
76
- The hackathon rewards going past the default Gradio look. Gradio 6's
77
- **`gradio.Server`** is perfect for it: you write Python API endpoints
78
- (`@app.api`, still ZeroGPU-enabled) and serve *your own* HTML/CSS/JS on top. I
79
- validated it with a tiny ping/`cuda_available` build on the Space first β€” the
80
- single best de-risking decision, because it proved a major version bump before I
81
- wrote a line of frontend. The result is a dark single-page dashboard (Chart.js
82
- donut + line, a budget ring, a calendar heatmap, editable line items, and an
83
- agent chat that shows the tools it used) that talks to the models through the
84
- Gradio JS client.
85
-
86
- ## What "building small" taught me
87
-
88
- Constraints made the product *better*. Because the models are small and the data
89
- is mine, it can stay private (per-user, hashed PIN, a private dataset). Because
90
- the models are imperfect, I had to build the reconciliation, the reason-and-repair
91
- pass, and the grounded agent β€” which is exactly the "intelligence" that makes it
92
- trustworthy. Small didn't mean toy. It meant *honest*.
93
-
94
- *BudgetBuddy is open on its Space β€” snap a bill and ask it something.*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Field Notes β€” building BudgetBuddy
2
 
3
+ *A build log for the Build Small hackathon. This is the honest version β€” the wrong
4
+ turns included. Publish-ready; edit the voice to your own and add a screenshot or
5
+ two before posting.*
6
 
7
  ---
8
 
9
+ ## Why I built it
10
+
11
+ My mum keeps a household running on cash, UPI, and a drawer full of crumpled
12
+ bills. She writes the day's spends in a notebook and tries to hold a budget β€” and
13
+ by night she genuinely can't remember where β‚Ή350 went. Every "real" budgeting app
14
+ wants her to link a bank account or type every line like a clerk, or it ships her
15
+ receipts off to some cloud. None of that was ever going to happen in my house.
16
+
17
+ So I built **BudgetBuddy** for the Build Small hackathon (Backyard AI track): take
18
+ a photo of a bill or a payment screenshot, a small model reads it, and you can
19
+ just *ask* where your money went. Two MiniCPM models do everything; no third-party
20
+ AI API is ever called.
21
+
22
+ It works now. Getting here was not a straight line.
23
+
24
+ ## I got the very first fact wrong
25
+
26
+ I started by assuming MiniCPM-V-4.6 was an ~8B model like the older 2.6/4.5 line,
27
+ and spent an evening worrying about GPU memory I didn't need to worry about. It's
28
+ **1.3B**. I'd anchored on the version number as if it were a size. Then I tried to
29
+ call it the way I "knew" the model worked β€” `model.chat(...)` β€” and it didn't
30
+ exist anymore. In 4.6 it's a plain `transformers` model you drive with
31
+ `AutoModelForImageTextToText` + `apply_chat_template`. I only sorted both out by
32
+ actually opening the repo's `config.json` and chat template instead of trusting my
33
+ memory. Lesson I keep re-learning: read the model's *actual* files first.
34
+
35
+ ## Then the dependencies tried to kill it
36
+
37
+ Putting a current stack on a Hugging Face Space turned into a cascade. An older
38
+ Gradio imported `HfFolder`, which `huggingface_hub` 1.0 had removed, so the Space
39
+ 500'd on boot. Fix that and the `gradio_client` schema walker choked on a boolean
40
+ in my API signature. Fix that and Starlette and Jinja disagreed about something
41
+ else. I kept patching symptoms until I gave up and moved onto a single,
42
+ self-consistent modern Gradio instead of pinning a museum piece. Hours, gone, on
43
+ nothing to do with the actual product.
44
+
45
+ ## A 401 that turned into an auth system
46
+
47
+ First time I tried to save a parsed bill: **"Save failed: 401 Unauthorized."** Of
48
+ course β€” writing to a Dataset needs a token. I wired that up, felt clever, and
49
+ then it hit me: if every tester and judge writes to the *same* dataset, they'd all
50
+ see each other's receipts. That's not a budgeting app, that's a group chat. So a
51
+ "quick" capture demo grew a real spine: username + PIN (PBKDF2-hashed, salted),
52
+ signed session tokens, and one private file per user. Not bank-grade, and I say so
53
+ in the README β€” but honest about what it is.
54
+
55
+ ## Two models that refused to share a room
56
+
57
+ This one cost me the most. The vision model wants a modern `transformers` (β‰ˆ5.7+).
58
+ MiniCPM4.1-8B β€” the reasoning model I wanted for understanding and the agent β€” has
59
+ `trust_remote_code` written for `transformers` β‰ˆ4.56 and throws `RuntimeError`s on
60
+ 5.x. You cannot have both in one environment. I tried to shim the gaps
61
+ (`is_torch_greater_or_equal_than_1_13`, `ALL_LAYERNORM_LAYERS`,
62
+ `is_torch_fx_available` …) and got it to *load*, but it was a house of cards.
63
+
64
+ The fix that actually worked was to stop fighting: run the 8B on **Modal**, in its
65
+ own container with its own pinned `transformers`, behind a `generate()` method the
66
+ Space calls over the SDK. A memory snapshot makes the cold start bearable and a
67
+ warm window keeps it hot. The moment I stopped trying to make one environment do
68
+ two contradictory things, the whole problem evaporated. That's also what made the
69
+ 8B affordable to use at all.
70
+
71
+ ## It worked, and it looked like everything else
72
+
73
+ At one point the pipeline ran end-to-end and I was almost proud β€” until I looked
74
+ at it. It was the default Gradio look, indistinguishable from a thousand other
75
+ demos. So I rebuilt the front of it on Gradio 6's **`gradio.Server`**: Python API
76
+ endpoints (still ZeroGPU-enabled) with my own HTML/CSS/JS served on top β€” a dark
77
+ dashboard with Chart.js, a budget ring, a calendar heatmap, editable line items.
78
+ I de-risked the version jump with a throwaway ping build before writing any UI,
79
+ which saved me from discovering a breaking change at the worst possible time.
80
+
81
+ ## The assistant confidently lied to me
82
+
83
+ The chat was the part I was proudest of and the part that embarrassed me most. I
84
+ asked "how much did I spend in July 2026?" and it answered **β‚Ή0.00** β€” while a β‚Ή419
85
+ July bill sat right there in the data. The model wasn't hallucinating; my *tools*
86
+ only understood `this_month` / `last_month` / `all`, so "July 2026" silently
87
+ collapsed to the current month. I taught the tools real periods (a specific
88
+ `YYYY-MM`, a year, this/last year) and started handing the agent today's date.
89
+
90
+ Then I asked for my "second most expensive" spend and it cheerfully returned the
91
+ *first* β€” my regex had matched "most expensive" inside "second", and there was no
92
+ notion of rank at all. Little failures like that taught me the real rule for a
93
+ small-model agent: let it *plan* which tool to call, but never let it do the
94
+ arithmetic. The 13 tools are deterministic Python; the model chooses and chains
95
+ them; an answer is only returned once it's grounded in an actual tool result, and
96
+ ungrounded output is thrown away. *AI plans, code computes.* (Open traces:
97
+ [`AGENT_TRACES.md`](AGENT_TRACES.md).)
98
+
99
+ ## Bills are messier than any schema
100
+
101
+ Real bills don't respect your data model. Dates showed up as `01/07/17` and my
102
+ ISO-only parser dropped them, so the charts looked empty while the data was fine.
103
+ The vision model would list "Subtotal", "Total" and "Amount Payable" as if they
104
+ were *items*, so nothing reconciled. An electricity bill's "energy charges" got
105
+ mixed in with its taxes. None of that is solved by a bigger prompt β€” it's solved
106
+ by a small, boring, deterministic pass that knows a summary row from a line item
107
+ and a tax from a charge, runs in microseconds, and means my mum almost never has
108
+ to fix anything by hand.
109
+
110
+ ## What building small actually taught me
111
+
112
+ I expected "small models" to mean "compromise." It didn't. Because the models are
113
+ small and local, the data can stay private and the thing is cheap to run. Because
114
+ the models are imperfect, I *had* to build the reconciliation, the repair pass,
115
+ and the grounded agent β€” and that work is exactly what makes the app trustworthy
116
+ instead of a toy. The constraints didn't shrink the product. They made it honest.
117
+
118
+ *BudgetBuddy is live β€” snap a bill and ask it something:*
119
+ *https://huggingface.co/spaces/build-small-hackathon/BudgetBuddy*