| # Architecture |
|
|
| Small Shop Ledger is a small-model Gradio app that turns messy shop |
| notes into structured ledger rows, insights, and follow-up actions. |
|
|
| ## System Overview |
|
|
| ```text |
| User text/audio/document |
| | |
| v |
| Gradio UI (`shop_ledger/ui.py`) |
| | |
| +--> local mode: `LedgerProcessor` |
| | |
| +--> Modal mode: `LedgerAgent().process.remote(...)` |
| | |
| v |
| `LedgerProcessor` |
| | |
| +--> llama.cpp backend (`LlamaLedgerBackend`) |
| | |
| +--> heuristic fallback |
| | |
| v |
| `LedgerResult` |
| | |
| v |
| Dashboard, ledger table, automation queue, CSV export |
| ``` |
|
|
| ## Code Map |
|
|
| | File | Responsibility | |
| | --- | --- | |
| | `app.py` | Local Gradio entrypoint. | |
| | `modal_app.py` | Modal image, volume, GPU worker, ASGI app, model download, smoke tests. | |
| | `shop_ledger/ui.py` | Gradio Blocks UI, input selection, callbacks, CSV export. | |
| | `shop_ledger/schema.py` | Pydantic models for ledger entries and model results. | |
| | `shop_ledger/llama_backend.py` | llama.cpp prompt, model loading, JSON parsing. | |
| | `shop_ledger/processor.py` | Runtime mode selection and fallback handling. | |
| | `shop_ledger/heuristics.py` | Deterministic parser for mock/dev/fallback mode. | |
| | `shop_ledger/insights.py` | Dashboard metrics, risk flags, follow-up queue, breakdown tables. | |
| | `tests/` | Unit tests for extraction, processor fallback, input-choice behavior, and insights. | |
|
|
| ## Data Flow |
|
|
| 1. The user enters a written note, records/uploads a voice note, or uploads a |
| receipt/bill image or PDF. |
| 2. If multiple inputs are present and `Auto` is selected, the UI asks the user |
| to choose which input to analyze. |
| 3. Audio input is transcribed locally with `faster-whisper` when available. |
| 4. Documents are prepared locally: PDFs are rendered into page images with |
| PyMuPDF, uploaded images are resized with Pillow, and both become base64 |
| data URLs. |
| 5. The chosen note text is sent to `LedgerProcessor`. |
| 6. In Modal production, `LedgerProcessor` uses `LlamaLedgerBackend`. |
| 7. `LlamaLedgerBackend` asks Gemma through llama.cpp to return strict JSON, |
| using multimodal `image_url` message parts when document images are present. |
| 8. The result is validated by `LedgerResult` and `LedgerEntry`, then tagged |
| with the readable `LLAMA_MODEL_LABEL` when llama.cpp was used. |
| 9. Rows are appended to Gradio state. |
| 10. The app recomputes: |
| - ledger table |
| - dashboard metrics |
| - field intelligence |
| - dynamic insight graph plan |
| - daily shop-pulse brief |
| - local ledger question answer |
| - shop pulse timeline |
| - counterparty memory |
| - anomaly scan |
| - closing checklist |
| - Plotly insight figures |
| - automation queue |
| - review queue |
| - category and party tables |
| - CSV export |
| 11. The analyzed input is cleared so the next note starts cleanly. |
|
|
| ## Model Contract |
|
|
| The model must return JSON shaped like: |
|
|
| ```json |
| { |
| "entries": [ |
| { |
| "date": "YYYY-MM-DD or empty", |
| "direction": "expense|income|transfer|unknown", |
| "counterparty": "person or business", |
| "item": "what changed hands", |
| "quantity": "quantity if known", |
| "amount": 0, |
| "currency": "LKR", |
| "category": "inventory|utilities|rent|wages|transport|maintenance|sales|general expense|uncategorized", |
| "payment_status": "paid|due|partial|unknown", |
| "due_date": "", |
| "reminder": "short follow-up reminder or empty", |
| "confidence": 0.0, |
| "original_note": "source fragment" |
| } |
| ], |
| "reminders": ["short reminders"], |
| "questions": ["only ask if an amount, person, or due date is unclear"], |
| "cleaned_note": "normalized note" |
| } |
| ``` |
|
|
| The schema intentionally tolerates `null` for text fields by converting it to an |
| empty string. This prevents valid model intent from failing because of minor |
| JSON style differences. |
|
|
| ## Fallback Design |
|
|
| The app keeps a deterministic heuristic parser for three reasons: |
|
|
| 1. Local UI development should work without downloading a 12B model. |
| 2. The live demo should never go completely blank if model loading fails. |
| 3. Tests can verify app behavior quickly. |
|
|
| Fallback is visible. If llama.cpp fails, `model_used` becomes something like: |
|
|
| ```text |
| heuristic fallback (ValidationError) |
| ``` |
|
|
| The exception details are added to `questions` so the UI and smoke tests expose |
| the reason. |
|
|
| ## Insights Engine |
|
|
| `shop_ledger/insights.py` is pure Python and deterministic. It computes: |
|
|
| - net cash |
| - paid income |
| - paid expenses |
| - due income |
| - due expenses |
| - open follow-ups |
| - average extraction confidence |
| - top categories |
| - top parties |
| - high-value due risk flags |
| - low-confidence risk flags |
| - chart plan selection |
| - daily brief generation with Gemma or local fallback |
| - Ask My Ledger answers from structured rows with Gemma or local fallback |
| - voice questions for Ask My Ledger |
| - command palette actions |
| - counterparty memory cards |
| - anomaly detection |
| - daily closing checklist |
| - timeline events and pulse chart |
| - Plotly figures for due radar, spend pressure, cashflow, confidence review, |
| category mix, and party exposure |
| - follow-up queue with cadence and scripts |
| - reply studio variants for polite, friendly, and firm reminders |
| - review queue for low-confidence or incomplete rows |
| - daily field note |
|
|
| The chart planner is deterministic first. It asks what matters most right now: |
| unpaid money, expense pressure, cashflow timeline, review risk, or overall |
| category mix. Keeping insights separate from the Gradio UI makes the dashboard |
| testable and leaves room for a later local-LLM chart selector. |
|
|
| ## UI Structure |
|
|
| The UI is a dark, custom-styled Gradio Blocks app organized as a small-shop |
| operating cockpit. |
|
|
| Top status strip: |
|
|
| - model status |
| - row count |
|
|
| Shop OS Cockpit: |
|
|
| - `Capture` rail: written note, voice note, document upload, input selector, |
| currency, add/clear actions, conflict notice, and examples. |
| - `Shop Pulse` center: live KPIs, chart composer, chart director, themed Plotly |
| graph wall, pulse timeline, and field intelligence. |
| - `Ledger Assistant` rail: running totals, reminders, Gemma daily brief, full |
| Ask My Ledger chat, voice questions, prompt suggestions, command palette, and |
| daily closing ritual. |
|
|
| Action Inbox: |
|
|
| - follow-up automation cards |
| - review desk cards |
| - anomaly lantern cards |
| - reply/review/anomaly tables inside a secondary accordion |
|
|
| Workbenches: |
|
|
| - `People`: counterparty memory, trust pulse, party totals, and next-message |
| suggestions. |
| - `Ledger Archive`: raw ledger rows, CSV export, category heatmap, closing |
| checklist table, and timeline event table. |
|
|
| See `UI_DESIGN.md` for the layout rationale, CSS hooks, and demo flow. |
|
|
| ## Modal Production Path |
|
|
| The live Modal path is: |
|
|
| ```text |
| fastapi_app |
| -> build_demo(process_fn=process_remote) |
| -> LedgerAgent().process.remote(note, currency) |
| -> LedgerProcessor.from_env() |
| -> LlamaLedgerBackend(...) |
| -> llama_cpp.Llama.create_chat_completion(...) |
| ``` |
|
|
| The model worker is configured with: |
|
|
| ```text |
| gpu=A10 |
| cpu=8 |
| memory=32768 |
| timeout=1800 |
| LLAMA_N_GPU_LAYERS=-1 |
| LLAMA_N_CTX=2048 |
| LLAMA_MODEL_LABEL=unsloth/gemma-4-12b-it-GGUF / gemma-4-12b-it-UD-Q4_K_XL.gguf / llama.cpp |
| ``` |
|
|
| ## Testing Strategy |
|
|
| Current tests cover: |
|
|
| - heuristic parsing |
| - processor fallback behavior |
| - text/audio input-choice rules |
| - field-clearing callback behavior |
| - dashboard metrics |
| - chart-plan selection |
| - Plotly figure generation |
| - follow-up queue priority |
| - reply studio message variants |
| - review queue generation |
| - risk flags |
|
|
| Run: |
|
|
| ```bash |
| python3 -m unittest discover -s tests |
| python3 -m compileall shop_ledger app.py modal_app.py tests |
| ``` |
|
|
| ## Known Constraints |
|
|
| - Gradio state is session-local. This is enough for the hackathon demo but not a |
| multi-user accounting product. |
| - CSV export is generated per session. |
| - Voice transcription uses local `faster-whisper` only when available. |
| - The app is not a replacement for accounting, tax, legal, or financial advice. |
| - The app should not store sensitive customer data without adding auth and |
| persistence controls. |
|
|