Building Agro-Bot: An AI Assistant for Farmers
Agro-Bot started from a simple question: what if a farmer could get better crop guidance and disease detection without digging through scattered advice?
That idea stayed with me because the problem felt very real. A wrong crop decision can cost a season, and a missed disease diagnosis can damage a crop before anyone reacts.
So I built Agro-Bot, an AI-powered agricultural assistant with two parts that work together:
- a crop recommendation model for structured soil and weather data
- a plant disease classifier for leaf images
The goal was not to build a flashy demo. It was to build something that could actually help with everyday farming decisions.
The First Version
I started with the crop recommendation pipeline because it was the cleanest place to begin.
The model takes in tabular inputs like nitrogen, phosphorus, potassium, pH, temperature, humidity, and rainfall, then recommends the most suitable crop. I used XGBoost for this part because it fits structured data well and gives strong performance without needing heavy preprocessing — being tree-based, it’s scale-invariant, so there was no need for manual feature scaling.
On the preprocessing side, I kept things disciplined rather than fancy: dropped duplicates, coerced everything to numeric, dropped rows with missing values, capped potassium at a domain-informed upper bound of 120, truncated decimals to 3 places for reproducibility, and label-encoded the crop classes. The split was an 80/20 stratified train-test split (stratify=y, random_state=42), so every crop class is represented proportionally in both sets, and the test set gets touched exactly once — at final evaluation.
For the model itself, I settled on n_estimators=300, max_depth=6, learning_rate=0.08, with subsample=0.9 and colsample_bytree=0.9 to add a bit of variance reduction. The tuning approach was a coarse grid over learning rate × depth × estimators, then fine-tuning regularization (gamma, min_child_weight, reg_lambda) with early stopping.
When I saw the model reach 98.86% test accuracy, it felt like the project had crossed from idea into something real. I didn’t want to just take that number at face value though — the defense for it rests on three things: it’s measured on a genuinely held-out test set, the split is stratified so no class is over- or under-represented, and the train/test gap is small (a large gap, like 99.9% train vs. 70% test, would be the real overfitting red flag, not a high number by itself). Looking at feature importance by gain, rainfall, nitrogen, and humidity came out as the strongest signals — which tracks, since those are the variables that most sharply separate one crop’s requirements from another’s.
That accuracy number mattered, but what mattered more was that the model behaved like a practical decision aid instead of a random classifier.
Adding the Second Layer
Once the crop side was working, I wanted Agro-Bot to do more than recommend what to grow.
Farmers also need to know when a plant is sick.
That led me to build a second pipeline: a PyTorch CNN that classifies leaf diseases from images. The images are resized to 224×224, normalized with ImageNet mean/std ([0.485, 0.456, 0.406] / [0.229, 0.224, 0.225]), and augmented during training — but only mildly: a horizontal flip and a small ±10° rotation, since disease symptoms are subtle and heavier augmentation like color jitter risks erasing the actual lesion signal.
The architecture, which I called PlantDiseaseCNN, is a custom 4-stage network: four Conv2d → BatchNorm → ReLU → MaxPool blocks, doubling channels at each stage (3 → 32 → 64 → 128 → 256) while spatial dimensions halve. The classifier head uses AdaptiveAvgPool2d to collapse everything to a 256-element vector, then Flatten → Dropout(0.3) → Linear(256→128) → ReLU → Dropout(0.3) → Linear(128→num_classes). BatchNorm sits right after each conv to stabilize training and allow higher learning rates, while dropout only lives in the classifier head — the BatchNorm layers already do regularization work in the feature extractor.
Training used CrossEntropyLoss and the Adam optimizer at the default lr=1e-3 — forgiving enough to skip heavy tuning. I didn’t add a learning-rate scheduler in this version, though a CosineAnnealingLR or ReduceLROnPlateau would likely help the model settle into a sharper minimum. Best checkpoints were saved based on validation accuracy, with a fixed seed (42) for reproducibility, and final evaluation went beyond raw accuracy to precision, recall, and F1 per class plus a confusion matrix — because in disease detection, a model that just predicts “healthy” every time can look deceptively accurate while being useless, and a missed disease is a lot costlier than a false alarm.
This part changed the project from a single model into a more complete assistant.
Instead of only saying, “plant this crop,” Agro-Bot could also help answer, “is this leaf healthy?”
How I Put It Together
The architecture was kept straightforward so it would be easier to explain, test, and deploy.
- Backend: FastAPI
- Crop model: XGBoost
- Disease model: PyTorch CNN
- Deployment: Dockerized inference service
Both models load once at application startup via FastAPI’s lifespan event and get stored in app.state, so there’s no per-request loading overhead. Two endpoints handle the actual work: /predict/crop takes a JSON body of soil/weather readings and returns the recommended crop, and /predict/disease takes an uploaded leaf image and returns a disease label with a confidence score. The Dockerfile is a simple multi-stage build off python:3.11-slim, installing dependencies, copying the app, and launching uvicorn on port 8000.
I separated the pipelines instead of forcing them into one giant model. That made each one easier to improve on its own, and it matched the inputs better:
- structured sensor data for crop recommendation
- raw images for disease detection
That separation also made the system easier to reason about when debugging or updating the models — I can retrain or version one pipeline without touching the other. Artifacts are saved distinctly too: the XGBoost model as a .joblib bundle (model + label encoder together), and the CNN as a .pt checkpoint holding the state dict, class names, and expected image size.
What I Learned
Agro-Bot taught me that good ML projects are not just about model accuracy.
They are about the full chain:
- choosing the right model for the right data
- validating the result properly
- handling two different inference paths cleanly
- keeping the system usable outside of a notebook
I also learned that small engineering choices matter a lot. Versioning model artifacts, separating training from inference, and using a proper API layer make the project feel much more real. A high accuracy number on its own doesn’t mean much without the right validation strategy and deployment hygiene behind it — stratified splits, reproducible seeds, checkpointing the best model, keeping training and inference code apart.
What I Would Add Next
If I continue the project, the next steps are clear:
- stronger real-world field datasets — lab accuracy tends not to transfer cleanly to field conditions
- transfer learning for the CNN, likely a pretrained ResNet18/50 or EfficientNet fine-tuned on top of frozen early layers, which should generalize better than training from scratch
- a learning rate scheduler (
CosineAnnealingLRorReduceLROnPlateau) for sharper convergence - a feedback loop for user corrections — logging predictions, confidence scores, and farmer corrections to feed periodic retraining
- proper model monitoring and versioning after deployment (versioned artifact filenames, a metadata file per artifact, and a lightweight registry via MLflow, DVC, or even a manifest in S3)
- a friendlier frontend for farmers, with cloud inference for easy updates or on-device inference (ONNX / TensorFlow Lite) for low-connectivity use
I would also like to add clearer confidence handling so low-confidence predictions can be flagged instead of forced — ideally escalated to an agronomist rather than guessed on.
Final Thought
Agro-Bot is one of those projects that started with a technical idea but grew into something more practical.
It combines classical ML, deep learning, and backend engineering in a way that feels useful instead of theoretical.
And that is what made it worth building: a small system with a real-world purpose.