| --- |
| title: Dukaan Saathi |
| emoji: π |
| colorFrom: indigo |
| colorTo: green |
| sdk: docker |
| app_port: 7860 |
| pinned: false |
| license: mit |
| tags: |
| - inventory |
| - kirana |
| - telugu |
| - fastapi |
| - minicpm-v |
| - modal |
| - speech-to-text |
| - sqlite |
| - human-in-the-loop |
| - small-business |
| - receipt-parsing |
| - react-agent |
| - approval-gated |
| - llm-finetuning |
| --- |
| |
| # Dukaan Saathi Β· Small-Model Inventory Copilot for Kirana Stores |
|
|
| Dukaan Saathi is a phone-friendly inventory copilot for a small Indian convenience store. |
|
|
| The store owner uses Telugu/code-mixed commands during the day, sells products with English names, and receives messy supplier receipts on paper. The app helps turn those messy inputs into safe, reviewable inventory updates. |
|
|
| The goal is **not perfect OCR**. Supplier receipts can be noisy, handwritten, folded, and inconsistent. Dukaan Saathi uses a small vision model to create a draft, then lets the owner quickly correct it before anything touches inventory. |
|
|
| ## Core workflow |
|
|
| ```text |
| receipt photo / text command |
| β AI draft |
| β owner correction |
| β owner approval |
| β inventory update |
| β reorder suggestion |
| ``` |
|
|
| Inventory is never updated directly from model output. Every write is approval-gated. |
|
|
| ## Using this Space |
|
|
| The app opens on the **Dashboard** tab. Navigate using the top menu: |
|
|
| - **Dashboard** β current stock levels, expiry status, and AI-generated insights |
| - **Inventory** β full product list; add or edit items |
| - **Bill Desk** β upload a supplier receipt photo or paste receipt text, correct extracted rows, then approve to update stock |
| - **Voice** β record or upload a stock command, transcribe it, then approve the proposed change |
| - **Orders** β pending reorder suggestions; mark as received after stock arrives |
| - **Analytics** β sales window (7 d / 30 d / 90 d) |
|
|
| **Note on state:** This Space uses SQLite with in-container storage. Inventory changes are visible during your session but may reset when the Space rebuilds. The seeded catalog (Bun, OBM, Happy Happy, Bingo (C), Parle (bulk)) is always restored on restart. |
|
|
| **Note on Modal services:** Receipt image OCR and speech transcription use Modal-hosted endpoints that may have a cold start of 10β30 seconds on first use. A warm-up call runs automatically on page load. |
|
|
| ## What it does |
|
|
| ### Stock commands |
|
|
| Example: |
|
|
| ```text |
| add Bun 12 |
| ``` |
|
|
| The app detects that 12 buns arrived and proposes an inventory update. The owner must approve before the stock value changes. |
|
|
| ### Receipt photo extraction |
|
|
| The owner uploads a supplier receipt photo. MiniCPM-V extracts likely product rows, quantities, and amounts into a review table. |
|
|
| The extraction can be imperfect. That is expected. |
|
|
| Example noisy draft: |
|
|
| ```text |
| 1. Port Ranges (c), qty 1, amount 2450 |
| 2. Chocoly, qty 1, amount 8702 |
| ``` |
|
|
| ### Phone-friendly correction commands |
|
|
| Instead of forcing spreadsheet-style editing on a phone, the owner can type a simple correction: |
|
|
| ```text |
| first one Parle bulk, second one Bingo |
| ``` |
|
|
| The owner can also record or upload correction audio. The app sends the audio to the Modal speech ASR endpoint, fills the correction command textbox with the transcript, and still waits for the owner to apply the correction and approve rows. |
|
|
| The app remaps the rows to known inventory products: |
|
|
| ```text |
| row 1 β Parle (bulk) |
| row 2 β Bingo (C) |
| ``` |
|
|
| Matched rows become candidates for approval. |
|
|
| Supported correction examples: |
|
|
| ```text |
| first one Parle bulk |
| second one Bingo |
| row 1 Parle bulk |
| row 2 Bingo |
| skip row 2 |
| quantity row 1 is 4 |
| ``` |
|
|
| ### Approval-gated inventory updates |
|
|
| The owner must explicitly approve stock commands and receipt rows before SQLite inventory is updated. |
|
|
| ### Reorder suggestions |
|
|
| When stock falls below threshold, the app drafts reorder suggestions grouped by supplier. Nothing is sent or purchased automatically. |
|
|
| ## Quick demo (text-only, no Modal needed) |
|
|
| Paste this into the **Bill Desk** receipt text box: |
|
|
| ```text |
| Mahalakshmi Marketing |
| |
| | S.No | Particulars | Qty | Rate | Amount | |
| | 5/ | Port | 1 | X2450 | 2450 | |
| | 10/ | Rs.g/c | 4 | X8702 | 3480 | |
| ``` |
|
|
| Click **Parse receipt text**, then type this correction: |
|
|
| ```text |
| first one Parle bulk, second one Bingo |
| ``` |
|
|
| Click **Apply correction** β rows map to known products β click **Approve receipt rows** β inventory updates. |
|
|
| See the full [demo flow](#demo-flow) section below for the complete step-by-step walkthrough including voice and photo paths. |
|
|
| ## Why small models fit this problem |
|
|
| Small models are good enough to turn messy receipts and natural commands into useful drafts, but they should not be trusted to update business records directly. |
|
|
| Dukaan Saathi uses the model for interpretation and deterministic Python for safety-critical inventory logic: |
|
|
| ```text |
| MiniCPM-V output |
| β parsed candidate rows |
| β product matching |
| β owner correction |
| β owner approval |
| β SQLite write |
| ``` |
|
|
| This keeps the workflow useful even when the model makes mistakes. |
|
|
| ## Model lifecycle |
|
|
| The receipt model is trained on Modal, then pushed to Hugging Face Hub for the |
| public Space runtime. |
|
|
| ```text |
| Modal synthetic data generation |
| β Modal LoRA fine-tuning |
| β LoRA adapter stored in a Modal Volume |
| β Modal push job merges adapter into the base model |
| β merged model pushed to Hugging Face Hub |
| β HF Space uses hf_inference to call that Hub model |
| β parsed receipt rows populate an editable table |
| β owner approval updates inventory |
| ``` |
|
|
| Modal is the training and optional serving environment. Hugging Face Hub is the |
| public model artifact store. Hugging Face Inference is the public Space inference |
| path. |
|
|
| ### Fine-tuned receipt model |
|
|
| A LoRA adapter trained on Llama-3.2-3B-Instruct, stored in a Modal Volume: |
|
|
| ```text |
| Modal app: dukaan-saathi-receipt-llm |
| Modal Volume: dukaan-saathi-receipt-lora |
| Adapter path: /adapters/receipt-lora |
| Base model: unsloth/Llama-3.2-3B-Instruct-bnb-4bit |
| ``` |
|
|
| After training, push the merged model to Hugging Face Hub: |
|
|
| ```bash |
| uv run modal run modal_apps/receipt_llm_service.py::push |
| ``` |
|
|
| That push reads these values from `.env`: |
|
|
| ```text |
| HF_TOKEN=... |
| HF_RECEIPT_MODEL_REPO=summerdevlin46/dukaan-saathi-receipt-lora |
| ``` |
|
|
| The public HF Space uses the pushed model through: |
|
|
| ```text |
| RECEIPT_BACKEND=hf_inference |
| HF_RECEIPT_MODEL_REPO=summerdevlin46/dukaan-saathi-receipt-lora |
| ``` |
|
|
| The same adapter can also be served directly through a Modal receipt parser |
| endpoint for local or fallback runs. Deploying that endpoint writes this to |
| `.env`: |
|
|
| ```text |
| MODAL_RECEIPT_LLM_ENDPOINT=https://summerdevlin46--dukaan-saathi-receipt-llm-api.modal.run/parse |
| ``` |
|
|
| This is not a local GGUF file. Local llama.cpp use is a separate optional path. |
|
|
| ### Training data |
|
|
| | File | Examples | Source | |
| |------|----------|--------| |
| | `data/finetune/receipt_examples.jsonl` | 6 | Hand-authored | |
| | `data/finetune/generated/receipt_examples_modal_synthetic.jsonl` | 22 | Modal LLM-generated | |
|
|
| To regenerate synthetic examples: |
|
|
| ```bash |
| scripts/modal_generate_receipt_examples.sh \ |
| --count 48 \ |
| --output data/finetune/generated/receipt_examples_modal_synthetic.jsonl |
| ``` |
|
|
| To retrain the LoRA adapter on Modal: |
|
|
| ```bash |
| scripts/modal_finetune_receipt.sh --modal-synthetic-count 48 --max-steps 60 --epochs 8 |
| ``` |
|
|
| To redeploy the inference endpoint after retraining: |
|
|
| ```bash |
| scripts/modal_deploy.sh modal_apps/receipt_llm_service.py |
| ``` |
|
|
| To update the public HF Space model after retraining, push again: |
|
|
| ```bash |
| uv run modal run modal_apps/receipt_llm_service.py::push |
| ``` |
|
|
| ## Current stack |
|
|
| * **Gradio / Hugging Face Space** for the demo UI |
| * **Lean ReAct tool router** for selecting the small set of inventory/receipt tools |
| * **HF Inference API** for the public Hugging Face Space receipt parser path, using the model fine-tuned on Modal and pushed to HF Hub |
| * **llama.cpp + smolagents tools** for the local model-backed receipt parser path |
| * **MiniCPM-V 4.6** for receipt image extraction |
| * **Distil-Whisper small English** for correction-command speech transcription |
| * **Qwen2.5-1.5B-Instruct** for voice command NLU β semantic slot extraction (intent, product name, quantity, unit) from free-form and Telugu/English mixed commands |
| * **Modal** for hosting model endpoints |
| * **SQLite** for local inventory state |
| * **uv** for Python environment and commands |
| * **Deterministic Python services and fallback parsers** for: |
|
|
| * stock command parsing |
| * receipt text parsing |
| * receipt correction commands |
| * product matching |
| * inventory updates |
| * reorder drafts |
|
|
| Modal integrations are optional remote model services. The app-side Modal code |
| stays as thin HTTP clients; model serving code lives in `modal_apps/`. |
|
|
| For the public Hugging Face Space, use `RECEIPT_BACKEND=hf_inference` with |
| `HF_RECEIPT_MODEL_REPO` pointing at the published fine-tuned model. The |
| deterministic parser path exists for smoke tests, offline debugging, and safety |
| fallbacks; it is not the primary demo experience. |
|
|
| Modal can also host the receipt parser model. This is useful when local or |
| Hugging Face environments hit GPU/storage/runtime limits. With the current tiny |
| fine-tuning set, treat Modal fine-tuning as a demo-oriented adapter that improves |
| format following on known receipt styles, not as a generally reliable parser. |
|
|
| ## Runtime pipeline |
|
|
| The local orchestrator is: |
|
|
| ```bash |
| scripts/dev.sh |
| ``` |
|
|
| It selects one of four staged runtime paths: |
|
|
| ```text |
| scripts/dev.sh --hf-inference |
| β scripts/run_app.sh --backend hf_inference |
| β uv run python app.py |
| β receipt text parsing calls the HF Inference API model in HF_RECEIPT_MODEL_REPO |
| ``` |
|
|
| ```text |
| scripts/dev.sh --llamacpp |
| β scripts/start_llamacpp.sh |
| β uv run python scripts/download_models.py |
| β uv run python -m llama_cpp.server on port 8080 |
| β uv run python -m llama_cpp.server on port 8082 |
| β scripts/run_app.sh --backend llamacpp |
| β uv run python app.py |
| ``` |
|
|
| ```text |
| scripts/dev.sh --modal-llm |
| β scripts/run_app.sh --backend modal_llm |
| β uv run python app.py |
| β receipt text parsing calls MODAL_RECEIPT_LLM_ENDPOINT |
| ``` |
|
|
| ```text |
| scripts/dev.sh --deterministic |
| β scripts/run_app.sh --backend deterministic |
| β uv run python app.py |
| β receipt text parsing uses dukaan_saathi/parsers/receipt_text.py |
| ``` |
|
|
| Receipt image and speech are separate optional Modal services: |
|
|
| ```text |
| receipt image |
| β ReAct router |
| β extract_text_from_receipt_image tool |
| β dukaan_saathi/integrations/modal_receipt.py |
| β MODAL_RECEIPT_ENDPOINT |
| β modal_apps/receipt_vlm_service.py |
| β raw receipt text |
| β parse_receipt_text_tool |
| β configured receipt parser backend |
| β editable receipt table |
| β owner approval |
| β dukaan_saathi/services/inventory.py |
| ``` |
|
|
| ```text |
| voice or correction audio |
| β dukaan_saathi/integrations/speech.py |
| β MODAL_SPEECH_ENDPOINT |
| β transcript |
| β ReAct router for stock commands, or correction parser for receipt rows |
| β pending action / corrected editable rows |
| β owner approval |
| ``` |
|
|
| Inventory writes only happen after approval: |
|
|
| ```text |
| approve command / approve receipt rows |
| β dukaan_saathi/services/inventory.py |
| β dukaan_saathi/storage.py |
| β SQLite stock ledger |
| ``` |
|
|
| ## Agent status |
|
|
| The active Gradio path uses a lean ReAct-style router in |
| `dukaan_saathi/agent/react_agent.py`. It records `Thought`, `Action`, and |
| `Observation` trace lines, chooses the correct existing tool for the small task |
| set, and never writes inventory directly. |
|
|
| ReAct is the orchestrator, not the model. It calls tools; some tools call remote |
| models. For example, receipt-photo ReAct chooses the OCR tool, that tool calls |
| the Modal MiniCPM-V endpoint, then ReAct chooses the receipt parser tool, which |
| uses the configured backend: |
|
|
| ```text |
| Receipt photo |
| β ReAct |
| β Modal OCR tool |
| β receipt parser tool |
| β HF Inference / Modal LLM / llama.cpp / deterministic parser |
| β editable rows |
| β owner approval |
| β inventory write |
| ``` |
|
|
| Voice follows the same approval-gated shape: |
|
|
| ```text |
| Audio |
| β Modal ASR |
| β transcript |
| β ReAct stock-command tool |
| β pending stock action |
| β owner approval |
| β inventory write |
| ``` |
|
|
| This separation is intentional: Modal/HF/llama.cpp do expensive inference, |
| ReAct sequences safe tools and exposes a trace, and deterministic inventory |
| code performs approved writes. |
|
|
| The heavier `smolagents.ToolCallingAgent` implementation remains in |
| `dukaan_saathi/agent/agent.py`, but it is no longer the primary Gradio path. The |
| ReAct router calls the existing tool layer directly, which keeps the app simpler |
| while preserving the same approval gates. |
|
|
| ## Main files |
|
|
| ```text |
| app.py β FastAPI/Server entry point; routes dispatches and approval handlers |
| frontend_backend.py β adapter between custom HTML frontend and dukaan_saathi backend |
| dukaan_saathi/agent/react_agent.py β lean ReAct tool router; records Thought/Action/Observation traces |
| dukaan_saathi/agent/tools.py β parser, integration, and service tools called by the ReAct router |
| dukaan_saathi/parsers/stock_command.py β deterministic stock command parser (keyword + fuzzy catalog match) |
| dukaan_saathi/parsers/receipt_text.py β deterministic receipt text parser |
| dukaan_saathi/parsers/receipt_correction.py β row correction command parser |
| dukaan_saathi/services/inventory.py β canonical inventory write boundary (all stock writes go here) |
| dukaan_saathi/services/reorder.py β reorder suggestion generator |
| dukaan_saathi/storage.py β SQLite access, seed data, find_product |
| dukaan_saathi/integrations/command_nlu.py β Qwen2.5-1.5B NLU HTTP client (MODAL_NLU_ENDPOINT) |
| dukaan_saathi/integrations/modal_receipt.py β MiniCPM-V OCR HTTP client (MODAL_RECEIPT_ENDPOINT) |
| dukaan_saathi/integrations/speech.py β Distil-Whisper ASR HTTP client (MODAL_SPEECH_ENDPOINT) |
| modal_apps/command_nlu_service.py β Qwen2.5-1.5B slot extraction endpoint |
| modal_apps/receipt_vlm_service.py β MiniCPM-V receipt OCR endpoint |
| modal_apps/speech_asr_service.py β Distil-Whisper ASR endpoint |
| modal_apps/receipt_llm_service.py β receipt LLM train/serve/push |
| modal_apps/receipt_data_generator.py β synthetic receipt example generator |
| scripts/dev.sh β local run entrypoint |
| scripts/modal_deploy.sh β deploy a Modal service and write its URL to .env |
| smoke_tests/test_custom_app_safety.py β approval gate, NLU, and Modal integration tests |
| smoke_tests/test_receipt_parser_regression.py |
| smoke_tests/test_receipt_correction.py |
| ``` |
|
|
| ## Run locally |
|
|
| ### Prerequisites |
|
|
| - Python 3.13+ |
| - [uv](https://docs.astral.sh/uv/getting-started/installation/) (`pip install uv` or `curl -LsSf https://astral.sh/uv/install.sh | sh`) |
|
|
| ### 1 β Install dependencies |
|
|
| ```bash |
| uv sync |
| ``` |
|
|
| ### 2 β Configure environment |
|
|
| ```bash |
| cp .env.example .env |
| ``` |
|
|
| Then edit `.env`. The minimum required value depends on which backend you run: |
|
|
| | Backend | Required in `.env` | |
| |---------|-------------------| |
| | `hf_inference` (recommended) | `HF_RECEIPT_MODEL_REPO=summerdevlin46/dukaan-saathi-receipt-lora` | |
| | `modal_llm` | `MODAL_RECEIPT_LLM_ENDPOINT=<url from modal_deploy.sh>` | |
| | `deterministic` | nothing β no model calls | |
| | `llamacpp` | nothing extra β models downloaded automatically | |
|
|
| Optional Modal services (add when you have them; app runs without them): |
|
|
| ```text |
| MODAL_RECEIPT_ENDPOINT=... # receipt image OCR (MiniCPM-V) |
| MODAL_SPEECH_ENDPOINT=... # speech transcription (Distil-Whisper) |
| MODAL_NLU_ENDPOINT=... # voice command slot extraction (Qwen2.5-1.5B) |
| ``` |
|
|
| `HF_TOKEN` is only needed if `HF_RECEIPT_MODEL_REPO` is a private repo. |
|
|
| **Running on the public HF Space?** Modal endpoints must be added as Space secrets in the HF UI β see [docs/deployment_setup.md](docs/deployment_setup.md) for the full walkthrough. |
|
|
| ### 3 β Run |
|
|
| Pick one backend and start the app. It opens at **http://127.0.0.1:7860**. |
|
|
| **HF Inference (recommended for full demo)** |
|
|
| Calls the fine-tuned receipt model hosted on Hugging Face Hub. Requires |
| `HF_RECEIPT_MODEL_REPO` in `.env`. |
|
|
| ```bash |
| scripts/dev.sh --hf-inference |
| ``` |
|
|
| **Deterministic (fastest, no model needed)** |
|
|
| Uses rule-based parsers only. Stock commands, receipt text, corrections, and |
| approval all work. Use this to verify UI and approval flows without any model |
| calls. |
|
|
| ```bash |
| scripts/dev.sh --deterministic |
| ``` |
|
|
| **Modal LLM (fine-tuned model served on Modal)** |
|
|
| Calls the LoRA-fine-tuned endpoint you deployed on Modal. Requires |
| `MODAL_RECEIPT_LLM_ENDPOINT` in `.env`. |
|
|
| ```bash |
| scripts/dev.sh --modal-llm |
| ``` |
|
|
| **Local llama.cpp (fully offline fallback)** |
|
|
| Downloads GGUF models and starts two llama.cpp servers on ports 8080 and 8082, |
| then starts the app. Slow first start; receipt quality depends on whether a |
| fine-tuned GGUF is available via `HF_RECEIPT_GGUF_REPO`. |
|
|
| ```bash |
| scripts/dev.sh --llamacpp |
| ``` |
|
|
| ### 4 β Run tests |
|
|
| ```bash |
| uv run scripts/smoke_test.sh |
| ``` |
|
|
| Focused test runs: |
|
|
| ```bash |
| uv run python -m pytest smoke_tests/test_custom_app_safety.py -v |
| uv run python -m pytest smoke_tests/test_receipt_parser_regression.py smoke_tests/test_receipt_correction.py -q |
| ``` |
|
|
| ## Modal endpoints |
|
|
| Deploy the MiniCPM-V receipt endpoint: |
|
|
| ```bash |
| scripts/modal_deploy.sh modal_apps/receipt_vlm_service.py |
| ``` |
|
|
| Deploy the speech ASR endpoint: |
|
|
| ```bash |
| scripts/modal_deploy.sh modal_apps/speech_asr_service.py |
| ``` |
|
|
| Deploy the voice command NLU endpoint: |
|
|
| ```bash |
| scripts/modal_deploy.sh modal_apps/command_nlu_service.py |
| ``` |
|
|
| All three commands deploy the Modal app and write the generated endpoint URL to `.env`. |
| `MODAL_RECEIPT_ENDPOINT`, `MODAL_SPEECH_ENDPOINT`, and `MODAL_NLU_ENDPOINT` are written automatically. |
|
|
| Load the endpoint environment: |
|
|
| ```bash |
| source scripts/_env.sh |
| ``` |
|
|
| Health check: |
|
|
| ```bash |
| BASE_URL="${MODAL_RECEIPT_ENDPOINT%/extract}" |
| curl "$BASE_URL/health" |
| ``` |
|
|
| Speech health check: |
|
|
| ```bash |
| SPEECH_HEALTH_URL="${MODAL_SPEECH_ENDPOINT/speech-transcribe/speech-health}" |
| curl "$SPEECH_HEALTH_URL" |
| ``` |
|
|
| Test receipt extraction directly (replace with your own receipt image): |
|
|
| ```bash |
| curl -sS -X POST "$MODAL_RECEIPT_ENDPOINT" \ |
| -F "image=@/path/to/receipt.jpeg" |
| ``` |
|
|
| Test speech transcription directly: |
|
|
| ```bash |
| curl -sS -X POST "$MODAL_SPEECH_ENDPOINT" \ |
| -F "audio=@path/to/audio.wav" |
| ``` |
|
|
| NLU health check: |
|
|
| ```bash |
| curl "${MODAL_NLU_ENDPOINT}" \ |
| -X POST -H "Content-Type: application/json" \ |
| -d '{"command": "add Bun 12"}' |
| ``` |
|
|
| Stop Modal to save cost: |
|
|
| ```bash |
| uv run modal app stop dukaan-saathi-receipt-vlm || true |
| uv run modal app stop dukaan-saathi-speech-asr || true |
| uv run modal app stop dukaan-saathi-command-nlu || true |
| uv run modal app list |
| ``` |
|
|
| Look for: |
|
|
| ```text |
| Tasks 0 |
| ``` |
|
|
| ## Demo flow |
|
|
| Full walkthrough covering stock commands, receipt photo, and voice correction: |
|
|
| ```text |
| 1. Open Dukaan Saathi. |
| 2. Show current inventory. |
| 3. Enter: add Bun 12 |
| 4. Click Parse command. |
| 5. Approve the proposed stock update. |
| 6. Show the updated inventory and reorder draft. |
| 7. Upload a supplier receipt photo. |
| 8. MiniCPM-V extracts imperfect rows. |
| 9. Type or record this correction: first one Parle bulk, second one Bingo |
| 10. If using audio, click Transcribe correction audio. |
| 11. Click Apply correction. |
| 12. Show rows mapped to known inventory products. |
| 13. Click Approve receipt rows. |
| 14. Show inventory updated. |
| 15. Show reorder draft updated. |
| ``` |
|
|
| ## Safety rule |
|
|
| Model output never writes inventory directly. |
|
|
| The app always follows this flow: |
|
|
| ```text |
| model output |
| β parsed draft |
| β owner review/correction |
| β owner approval |
| β inventory write |
| ``` |
|
|
| This is the core design principle of Dukaan Saathi. |
|
|