NeoFix-API / docs /ADR.md
rairo's picture
docs: add ADR log for cross-session continuity
ba74ad7
|
Raw
History Blame Contribute Delete
5.45 kB

Architecture Decision Records — NeoFix-API (SozoFix backend)

This log records the "why" behind non-obvious backend decisions so future sessions keep continuity. Append new records at the bottom. Newest decisions supersede older ones only when explicitly marked. Status: Accepted | Superseded | Deprecated.

Stack recap: single-file Flask (main.py) on a Hugging Face Space (git remote is the Space itself; pushing main redeploys). Firebase Realtime DB + Storage via Admin SDK. Google GenAI (Gemini) for text/image, Deepgram for TTS, Stripe, Resend.


ADR-001 — Asynchronous background generation for repair guides

Date: 2026-07-04 · Status: Accepted

Context. PUT /api/projects/<id>/approve was one synchronous Gemini call asking for 5 steps with interleaved images, then uploads + TTS, all inside the request. Two failures: (1) the newer image model returns fewer inline images than text steps, and the old code truncated steps to min(steps, images), so users got fewer than 5 steps; (2) the whole job risked the Hugging Face Space HTTP timeout (~60 to 90s). The frontend meanwhile showed a fake client-side progress bar that hit 100% regardless of backend state.

Decision. Split validation from work. /approve now validates (auth, credits, ownership, re-entrancy guard), sets project status=generating, seeds a projects/{id}/generationProgress node, spawns a threading.Thread(daemon=True) running _run_guide_generation, and returns 202 immediately. A new lightweight GET /api/projects/<id>/progress is polled by the client. A staleness guard in that endpoint flips a stuck generating project to generation_failed once its heartbeat is older than GENERATION_STALE_S (default 240s), covering threads killed by a Space restart.

Consequences. No request-timeout risk. Real, pollable progress. Credits are still deducted only on success (moved into the job's finish path). Background threads do not survive a Space restart; the staleness guard plus the client retry panel recover the user. debug/reloader is left on; threads still run.


ADR-002 — True iterative image refinement with model fallback

Date: 2026-07-04 · Status: Accepted

Context. A single multimodal call is unreliable for producing exactly 5 illustrated steps. The primary image model (GENERATION_MODEL, "Nano Banana 2" = gemini-3.1-flash-image-preview) is higher latency and sometimes returns text-only turns.

Decision. Generate in phases inside _run_guide_generation:

  • Phase A (plan): a fast text-only call on PLAN_TEXT_MODEL (gemini-3.1-flash-lite) returns the tools list and exactly 5 numbered steps. Reuses the existing regex extraction and parse_numbered_steps.
  • Phase B (images): one chat session on GENERATION_MODEL, one send_message per step so each turn yields exactly one image with visual continuity. Each call runs under a timeout (IMAGE_CALL_TIMEOUT_S, default 28s) with one retry, then falls back to FALLBACK_IMAGE_MODEL (gemini-2.5-flash-image, "nano banana original") for the remaining steps. A step that still yields no image keeps its text with an empty imageUrl; steps are never truncated.
  • Phase C (upload): reuses the existing per-step image-upload + Deepgram-TTS parallelism.

All model ids and the timeout are environment variables, tunable without a redeploy.

Consequences. Guaranteed 5 text steps, with a latency safety net. Cost per guide is higher than a single call (multiple model turns) but predictable; track it. Fixes the min-truncation bug.


ADR-003 — Country-scoped supplier catalogue grounding

Date: 2026-07-04 (country scoping added 2026-07-05) · Status: Accepted

Context. The tools list was plain strings. We want real supplier prices (anchor partner: Electrosales, Harare) shown to users, but only to users in that supplier's market, the way Home Depot shows to US users and Builders to SA users. A user in a country with no partner should see the generic tools list.

Decision. New catalogue/ RTDB root. Each item carries a 2-letter country ISO code (default ZW via DEFAULT_CATALOGUE_COUNTRY) plus supplier, city, keywords, price, etc. Admin-only endpoints: CRUD, AI extract from an uploaded image or PDF (Gemini with response_mime_type=application/json; PDFs go through types.Part.from_bytes, no new deps), and bulk save after review. match_tools_to_catalogue is a cheap token-overlap scorer that returns the best in-stock item per tool (carrying its country), cached on the project as toolMatches at generation time and recomputed on demand via GET /api/projects/<id>/tool-matches. The client detects the viewer's country and filters matches; the backend stays country-agnostic.

Rejected alternative. A per-user city field on the user profile (built, then reverted). The user wanted automatic locale detection, not a manual setting. PUT /api/user/profile is back to display-name only.

Consequences. Onboarding a new market is a data operation: load that supplier's catalogue tagged with its country code. No server change per user. The matching scan is O(tools × items), fine to roughly 1–2k items; add a keyword index if the catalogue grows large.