| title | Plotwise |
|---|---|
| emoji | 🌾 |
| colorFrom | green |
| colorTo | gray |
| sdk | docker |
| app_port | 8080 |
| pinned | false |
Agricultural intelligence platform for Nagaland, India: on-device crop-disease detection, market prices with a live mandi feed, planting calendars, weather, and a government-scheme finder — built on the state's real 2023-24 production records. FastAPI + TensorFlow backend, framework-free HTML/JS frontends, Capacitor Android app.
Live demo: https://limajmr-plotwise.hf.space · Author: Limawapang L Jamir
I audited my own test set and found 427 images I had already trained on — how a contaminated split inflated this project's headline accuracy, why the corrected number matters less than the 43.6% it led me to measure, and what the confidence system does not protect against.
1. Inference ships to the phone. The trained CNN (EfficientNetB0, 24
classes) is converted to a 9 MB TensorFlow.js graph model
(frontend/src/tfjs-model/, conversion metadata
recorded in model.json: TensorFlow.js Converter 4.22.0 from TF 2.17.0) and
bundled inside the Android APK — capacitor.config.ts has no remote
server.url; scripts/build_bundle.sh packs the
UI, model, and reference data into the app. In the APK, leaf diagnosis
therefore runs with zero connectivity, behind a bilingual English/Nagamese
UI (the diagnosis and treatment text itself is currently English-only);
offline-ai.js is a JS port of the server's
crop-aware decision layer, and offline detections queue locally and sync to
the server's surveillance heatmap when a connection returns.
In the browser, that same code path is deliberately switched off: running
TensorFlow.js requires 'unsafe-eval', which this app's Content-Security-Policy
(main.py CONTENT_SECURITY_POLICY) does not grant, so
the web PWA sends photos to the server endpoint instead. The APK is unaffected
because it serves no CSP header. The trade — a hardened header on the public
web, full on-device inference in the app — is deliberate, and
offline-ai.js ready() detects the disabled
runtime and falls back rather than failing. Measured, with the reasoning, in
CHANGELOG 2.5.2.
2. The accuracy claims are measured — including the bad ones. Models
trained on lab datasets are known to collapse on real field photos. Instead of
quoting the lab number, eval/ measures the deployed inference path
on in-the-wild images and reports the collapse, what the decision layer
recovers, and where the confidence system fails. See the table below.
3. An evaluated agent layer, not a chatbot wrapper. The assistant can run
as a tool-use loop over the backend's data functions, with local-model routing
(local 7B via Ollama, no API cost) and escalation to claude-opus-5 — the
routing is built, though the measured numbers so far cover the local backend
only. Graded by a committed 40-question eval with ground truth derived from
the dataset. See below.
The same deployed inference code (imported from backend/src/main.py, not
reimplemented) evaluated on held-out lab images (PlantVillage) and on
real field photographs (PlantDoc):
| PlantVillage held-out, clean (n=7,654)³ | PlantDoc field images (n=1,642) | |
|---|---|---|
| A — raw model argmax, top-1 | 87.7% | 27.3% |
| B — + crop-aware pass, top-1 | 91.9%¹ | 43.6% |
| C — + abstention: coverage | 89.9% | 48.4% |
| C — selective accuracy (when it answers) | 96.4% | 45.7% |
| OOD images routed to "uncertain"² | — | 75.7% same-crop · 90.6% foreign-species |
¹ n=7,588 (66 images with no crop label excluded from the crop-aware arm), so
the lab delta below compares across the two denominators.
² Computed on the 936 unmapped OOD images (305 same-crop, 631 foreign-species),
not the 1,642 in the column header.
³ A 2026-08 content-hash audit found 427 of the 8,081 test images
byte-identical to training files (residue of an earlier dataset prep); they
are excluded from every number here, the runner re-excludes them on every
run, and the leaked subset's 98.7% score is reported in
eval/RESULTS.md §1a as the memorization signature it is.
python eval/plantvillage/run_baseline.py # left column
python eval/plantdoc/download.py # fetch PlantDoc (CC BY 4.0)
python eval/plantdoc/run_eval.py # right columnHonest reading: the crop-aware pass genuinely helps everywhere (+4.2 pts lab,
+16.3 pts field), but field accuracy is 43.6%, not 91.9% — and under
domain shift the confidence system stops working as a safety net (on answered
field images the error rate is 54.3%; in-domain it is 3.6%). The part that
does survive is OOD routing: photos of untrained diseases and foreign species
are mostly sent to "uncertain" instead of answered confidently. Full analysis,
per-class breakdowns, risk–coverage curves, and dataset caveats:
eval/RESULTS.md.
flowchart TB
subgraph train["Model pipeline (offline, GPU)"]
PV["PlantVillage dataset"] --> TRAIN["ml/train_disease_model.py<br/>EfficientNetB0 · 24 classes"]
TRAIN --> H5["disease_model.h5"]
H5 -->|"tensorflowjs_converter 4.22.0"| TFJS["frontend/src/tfjs-model/<br/>9 MB graph model"]
end
subgraph server["FastAPI backend (backend/src/main.py)"]
CSV["576-record CSV<br/>Director of Agriculture, Nagaland 2023-24"] --> API["REST API · PDF/CSV export · chatbot"]
MANDI["data.gov.in Agmarknet feed<br/>12 h cache · 7-day stale guard"] --> API
METEO["Open-Meteo weather"] --> API
API --- AGENT["backend/src/agent.py<br/>tool-use loop (PLOTWISE_AGENT=1)"]
AGENT -->|"simple lookups"| OLLAMA["qwen2.5:7b via Ollama<br/>(local, $0)"]
AGENT -->|"multi-step escalation"| CLAUDE["claude-opus-5"]
end
subgraph clients["Clients"]
WEB["Desktop SPA<br/>index.html"]
PWA["Mobile PWA<br/>mobile.html + sw.js app-shell cache"]
APK["Android APK (Capacitor)<br/>UI + model + data bundled"]
end
H5 --> API
API <--> WEB
API <--> PWA
TFJS -->|"scripts/build_bundle.sh"| APK
APK -.->|"offline queue → sync on reconnect"| API
- Crop records — 576 records: "District-wise Area, Production and Yield (Achievement) 2023-24", Director of Agriculture, Nagaland, Kohima (verified 09-May-2024, checked 30-Jul-2024). 16 districts, 44 crops. This is the ground truth for the dashboard, the yield API, and most of the agent-eval questions.
- Market prices — live daily mandi rates for Nagaland APMCs from the official data.gov.in Agmarknet feed (12-hour cache, 7-day stale guard) where mandis report; MSP 2023-24 (CACP) + Agmarknet anchors as clearly-labelled indicative fallback. A live rate is never invented — no key, no feed, or stale data means the row says "Indicative".
- Planting calendar — sow/harvest windows checked against the official
Nagaland Crop Calendar (Department of Agriculture, Government of
Nagaland, web-published Jan 2025); zone lists derived from the 2023-24 CSV
by
scripts/audit_calendar_vs_csv.pyand drift-guarded by the test suite. - Weather — Open-Meteo (no API key), GPS coordinates for all 16 district HQs: Kohima, Tseminyu, Phek, Mokokchung, Tuensang, Noklak, Shamator, Mon, Dimapur, Niuland, Chumoukedima, Wokha, Zunheboto, Peren, Kiphire, Longleng.
- Disease model — PlantVillage dataset (Penn State). Field eval: PlantDoc — Singh et al., CoDS-COMAD 2020, DOI 10.1145/3371158.3371196, CC BY 4.0.
backend/src/agent.py replaces keyword intent-detection with a model-driven
tool-use loop over six backend functions (get_yield, get_price,
get_weather, compare_districts, get_planting_calendar, find_schemes).
Feature-flagged (PLOTWISE_AGENT=1); the legacy intent path stays as the
default and as the fallback when no backend is reachable. A rule-based router
sends simple lookups to a local model and escalates comparisons and multi-step
questions to claude-opus-5; every turn is logged as JSONL (tools, arguments,
latency, token cost). Numeric answers must trace to tool results — no data
means a refusal, not a guess.
Graded by a 40-question eval: 25 answerable, with ground truth derived from committed sources — the 576-record CSV plus the backend's price, calendar, and scheme tables (one live-weather question is graded on tool selection only, hence 24 graded below); 10 unanswerable, where refusal is correct; and 5 ambiguous, where a clarifying question is correct:
| Metric | local backend (qwen2.5:7b) |
|---|---|
| Tool-selection accuracy | 100% (25/25) |
| Answer correctness (24 graded) | 70.8% |
| Refusal accuracy (10 unanswerable) | 90% |
| Clarification rate (5 ambiguous) | 20% |
| Mean latency | 1.7 s |
| API cost | $0 |
python eval/agent/run_eval.py --backend localThe local 7B model picks the right tools and refuses well, but tends to answer
one interpretation of an ambiguous question instead of asking, and fumbles
multi-step arithmetic — exactly the traffic the router escalates. Claude and
routed columns are pending an ANTHROPIC_API_KEY. Grader design, iteration
history (recorded honestly, including the bugs that inflated early numbers),
and per-question outputs: eval/README.md.
- Disease detection — photograph a leaf → prediction with three-tier confidence (confident ≥70% / low 55–70% / uncertain — the app says "not sure" instead of guessing) and treatment guidance; works offline on the phone. Crops outside the model fall back to a curated knowledge base.
- Market prices — live vs. indicative labelling on every surface (UI, CSV export, PDF report, chatbot).
- Planting calendar — statewide sow/harvest windows checked against the official Nagaland Crop Calendar, filtered per district to the crops its 2023-24 records support, with live seasonal status.
- Scheme finder — crop → matching government schemes with eligibility notes.
- Weather — current + 7-day forecast with farming advisories, all 16 district HQs.
- Analytics dashboard — district-wise area/production/yield, disease surveillance heatmap with audit trail, one-click district PDF report.
- Assistant — English replies behind a bilingual EN/Nagamese chat UI; agent mode behind a flag.
Requires Python 3.11 — TensorFlow 2.17 has no wheels for newer Pythons.
git clone https://github.com/Limajmr8/plotwise.git
cd plotwise
python3.11 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn backend.src.main:app --port 8000Open http://127.0.0.1:8000 (desktop UI) or /mobile (mobile UI).
⚠️ Do not use--reloadon Windows — it crashes the TensorFlow subprocess. Keep the pinned versions inrequirements.txt(TF 2.17.0 / Keras 3.10.0) — newer versions cannot load the model.
Optional configuration via .env (see .env.example):
database path, CORS origins, live-price API key, agent flag and backends.
docker build -t plotwise .
docker run -p 8080:8080 -e PLOTWISE_SEED_ON_EMPTY=1 plotwise| Method | Endpoint | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /api/status |
App status, version, data summary |
| GET | /districts · /crops |
Reference data (16 districts, 44 crops) |
| GET | /prices |
Market prices (filters: crop, district) |
| GET | /calendar |
Planting calendar with live status (filters: district, crop) |
| POST | /schemes |
Scheme matching by crop |
| POST | /disease/detect |
Leaf image upload → ML prediction (multipart) |
| GET | /api/weather |
Live weather + 7-day forecast per district |
| POST | /api/chat |
Assistant — agent loop when PLOTWISE_AGENT=1, keyword intents otherwise |
| GET | /dashboard/yield · /dashboard/disease-heatmap |
Analytics |
| GET | /api/export/report |
District PDF report |
| GET | /api/export/prices · /api/export/yield |
CSV exports |
Interactive docs at /docs when running locally.
curl -X POST http://localhost:8000/disease/detect \
-F "file=@leaf.jpg" \
-F "crop=Potato" -F "district=Kohima"Capacitor bundles the mobile UI, the TF.js model, and the reference data into
the APK (npx cap sync android after scripts/build_bundle.sh), so detection
and the data tools work with no network; live features call the hosted
backend. GitHub Actions builds a debug APK on every push to main.
pytest tests/ -q # also runs in CI on every pushCovers every endpoint, the ML confidence tiers, the live-mandi cache (hit/miss/stale/failure-backoff), the weather proxy (mocked Open-Meteo), the agent loop (scripted fake backend — no network), a security-regression suite (XSS sanitisation, CSV-injection guards, filename injection, input validation), exports, edge cases, and data-integrity guards (the offline bundle must match the backend constants; calendar zones must match the CSV). Tests run against an isolated temporary database with the live feed disabled.
The backend is security-hardened from a whitebox audit with live exploitation: server-side input sanitisation + output escaping, a Content-Security-Policy, and injection guards — all under regression tests — plus per-client rate limiting and CVE-patched dependency pins.
- Field accuracy is 43.6%, and abstention is not a reliable safety net
under domain shift — on field images the system still answers 48% of the
time with a 54.3% error rate on those answers. The eval exists precisely to
keep this number visible. Mitigations that would actually move it
(fine-tuning on field imagery, adding the missing healthy/mosaic classes,
confidence calibration on field data, background-robustness augmentation or
leaf segmentation) are discussed in
eval/RESULTS.md. - Same-crop OOD is the dangerous case: a photo of an untrained disease on a covered crop is routed to "uncertain" only 75.7% of the time (worst: tomato mosaic virus, 13% routed — it usually gets a confident wrong answer).
- Class coverage: 24 classes over 9 crops. Nagaland staples such as paddy and ginger have no model coverage and fall back to a knowledge base.
- Mandi coverage is sparse: Nagaland APMCs report irregularly and mostly horticulture/perishables, so many price rows remain MSP-anchored "Indicative" even with a live key configured.
- Single data year: all yield data is 2023-24; no trends across years.
- Planting windows are statewide simplifications: the official Nagaland Crop Calendar documents different windows per district and farming system (jhum / terrace / plains); the app carries one modal [start, end] window per crop. Zone lists are derived from the 2023-24 CSV and drift-guarded by tests, but documented per-district exceptions (plains potato sown Oct–Nov, Tuensang's autumn potato, Mon's Aug–Sep rabi-maize cycle) are not modelled — see CHANGELOG 2.3.1 for the full audit trail.
- PlantDoc caveats: label noise, a known apple train/test inconsistency,
and some very small classes — documented per-decision in
eval/plantdoc/class_map.yaml. - Agent eval grading is heuristic (deterministic keyword/numeric matching, no LLM judge) — cheap and reproducible, but crude; audit any grade from the committed per-question JSONL.
Limawapang Jamir | B.Tech CSE, Bennett University (2020–2024) From Mokokchung, Nagaland 📧 limawapang8@gmail.com | LinkedIn | GitHub