Spaces:
Sleeping
Sleeping
docs: add ADR log for cross-session continuity
Browse filesRecords the async generation pipeline, iterative image refinement with
model fallback, and country-scoped catalogue grounding decisions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- docs/ADR.md +96 -0
docs/ADR.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Architecture Decision Records — NeoFix-API (SozoFix backend)
|
| 2 |
+
|
| 3 |
+
This log records the "why" behind non-obvious backend decisions so future sessions
|
| 4 |
+
keep continuity. Append new records at the bottom. Newest decisions supersede older
|
| 5 |
+
ones only when explicitly marked. Status: Accepted | Superseded | Deprecated.
|
| 6 |
+
|
| 7 |
+
Stack recap: single-file Flask (`main.py`) on a Hugging Face Space (git remote is the
|
| 8 |
+
Space itself; pushing `main` redeploys). Firebase Realtime DB + Storage via Admin SDK.
|
| 9 |
+
Google GenAI (Gemini) for text/image, Deepgram for TTS, Stripe, Resend.
|
| 10 |
+
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
## ADR-001 — Asynchronous background generation for repair guides
|
| 14 |
+
**Date:** 2026-07-04 · **Status:** Accepted
|
| 15 |
+
|
| 16 |
+
**Context.** `PUT /api/projects/<id>/approve` was one synchronous Gemini call asking for
|
| 17 |
+
5 steps with interleaved images, then uploads + TTS, all inside the request. Two failures:
|
| 18 |
+
(1) the newer image model returns fewer inline images than text steps, and the old code
|
| 19 |
+
truncated steps to `min(steps, images)`, so users got fewer than 5 steps; (2) the whole
|
| 20 |
+
job risked the Hugging Face Space HTTP timeout (~60 to 90s). The frontend meanwhile showed
|
| 21 |
+
a fake client-side progress bar that hit 100% regardless of backend state.
|
| 22 |
+
|
| 23 |
+
**Decision.** Split validation from work. `/approve` now validates (auth, credits, ownership,
|
| 24 |
+
re-entrancy guard), sets project `status=generating`, seeds a `projects/{id}/generationProgress`
|
| 25 |
+
node, spawns a `threading.Thread(daemon=True)` running `_run_guide_generation`, and returns
|
| 26 |
+
**202** immediately. A new lightweight `GET /api/projects/<id>/progress` is polled by the client.
|
| 27 |
+
A staleness guard in that endpoint flips a stuck `generating` project to `generation_failed`
|
| 28 |
+
once its heartbeat is older than `GENERATION_STALE_S` (default 240s), covering threads killed
|
| 29 |
+
by a Space restart.
|
| 30 |
+
|
| 31 |
+
**Consequences.** No request-timeout risk. Real, pollable progress. Credits are still deducted
|
| 32 |
+
only on success (moved into the job's finish path). Background threads do not survive a Space
|
| 33 |
+
restart; the staleness guard plus the client retry panel recover the user. `debug`/reloader is
|
| 34 |
+
left on; threads still run.
|
| 35 |
+
|
| 36 |
+
---
|
| 37 |
+
|
| 38 |
+
## ADR-002 — True iterative image refinement with model fallback
|
| 39 |
+
**Date:** 2026-07-04 · **Status:** Accepted
|
| 40 |
+
|
| 41 |
+
**Context.** A single multimodal call is unreliable for producing exactly 5 illustrated steps.
|
| 42 |
+
The primary image model (`GENERATION_MODEL`, "Nano Banana 2" = `gemini-3.1-flash-image-preview`)
|
| 43 |
+
is higher latency and sometimes returns text-only turns.
|
| 44 |
+
|
| 45 |
+
**Decision.** Generate in phases inside `_run_guide_generation`:
|
| 46 |
+
- **Phase A (plan):** a fast text-only call on `PLAN_TEXT_MODEL` (`gemini-3.1-flash-lite`) returns
|
| 47 |
+
the tools list and exactly 5 numbered steps. Reuses the existing regex extraction and
|
| 48 |
+
`parse_numbered_steps`.
|
| 49 |
+
- **Phase B (images):** one chat session on `GENERATION_MODEL`, one `send_message` per step so each
|
| 50 |
+
turn yields exactly one image with visual continuity. Each call runs under a timeout
|
| 51 |
+
(`IMAGE_CALL_TIMEOUT_S`, default 28s) with one retry, then falls back to `FALLBACK_IMAGE_MODEL`
|
| 52 |
+
(`gemini-2.5-flash-image`, "nano banana original") for the remaining steps. A step that still
|
| 53 |
+
yields no image keeps its text with an empty `imageUrl`; **steps are never truncated**.
|
| 54 |
+
- **Phase C (upload):** reuses the existing per-step image-upload + Deepgram-TTS parallelism.
|
| 55 |
+
|
| 56 |
+
All model ids and the timeout are environment variables, tunable without a redeploy.
|
| 57 |
+
|
| 58 |
+
**Consequences.** Guaranteed 5 text steps, with a latency safety net. Cost per guide is higher
|
| 59 |
+
than a single call (multiple model turns) but predictable; track it. Fixes the min-truncation bug.
|
| 60 |
+
|
| 61 |
+
---
|
| 62 |
+
|
| 63 |
+
## ADR-003 — Country-scoped supplier catalogue grounding
|
| 64 |
+
**Date:** 2026-07-04 (country scoping added 2026-07-05) · **Status:** Accepted
|
| 65 |
+
|
| 66 |
+
**Context.** The tools list was plain strings. We want real supplier prices (anchor partner:
|
| 67 |
+
Electrosales, Harare) shown to users, but only to users in that supplier's market, the way
|
| 68 |
+
Home Depot shows to US users and Builders to SA users. A user in a country with no partner
|
| 69 |
+
should see the generic tools list.
|
| 70 |
+
|
| 71 |
+
**Decision.** New `catalogue/` RTDB root. Each item carries a 2-letter `country` ISO code
|
| 72 |
+
(default `ZW` via `DEFAULT_CATALOGUE_COUNTRY`) plus `supplier`, `city`, keywords, price, etc.
|
| 73 |
+
Admin-only endpoints: CRUD, AI extract from an uploaded image or PDF (Gemini with
|
| 74 |
+
`response_mime_type=application/json`; PDFs go through `types.Part.from_bytes`, no new deps),
|
| 75 |
+
and bulk save after review. `match_tools_to_catalogue` is a cheap token-overlap scorer that
|
| 76 |
+
returns the best in-stock item per tool (carrying its `country`), cached on the project as
|
| 77 |
+
`toolMatches` at generation time and recomputed on demand via `GET /api/projects/<id>/tool-matches`.
|
| 78 |
+
The **client** detects the viewer's country and filters matches; the backend stays country-agnostic.
|
| 79 |
+
|
| 80 |
+
**Rejected alternative.** A per-user `city` field on the user profile (built, then reverted).
|
| 81 |
+
The user wanted automatic locale detection, not a manual setting. `PUT /api/user/profile` is back
|
| 82 |
+
to display-name only.
|
| 83 |
+
|
| 84 |
+
**Consequences.** Onboarding a new market is a data operation: load that supplier's catalogue
|
| 85 |
+
tagged with its country code. No server change per user. The matching scan is O(tools × items),
|
| 86 |
+
fine to roughly 1–2k items; add a keyword index if the catalogue grows large.
|
| 87 |
+
|
| 88 |
+
---
|
| 89 |
+
|
| 90 |
+
<!-- Template for new records:
|
| 91 |
+
## ADR-00N — Title
|
| 92 |
+
**Date:** YYYY-MM-DD · **Status:** Accepted
|
| 93 |
+
**Context.** ...
|
| 94 |
+
**Decision.** ...
|
| 95 |
+
**Consequences.** ...
|
| 96 |
+
-->
|