π Production: https://plantaeai.tech
- Overview
- Screenshots
- Features
- AI Agent & Workflow
- Human-in-the-Loop (Slack)
- Price-Match Coupons
- App Structure
- Tech Stack
- Getting Started
- Load Test Analysis
- Deployment
- License
- Contact
Plantae is a modern, AI-powered e-commerce platform for plant and gardening products. It features a conversational agent for plant care, product recommendations, and order support. The project is built with Django and deployed on an Azure VM at plantaeai.tech.
| Home Page | Admin |
|---|---|
![]() |
![]() |
| Store | Product Detail |
|---|---|
![]() |
![]() |
| Cart | Payment Success |
|---|---|
![]() |
![]() |
| Dashboard | Coupon at Checkout |
|---|---|
![]() |
![]() |
| Chat Widget | Chat Suggestions |
|---|---|
![]() |
![]() |
Slack screenshots are in the Human-in-the-Loop section.
- User Authentication: Register, login, logout, email verification, password reset, profile management.
- Product Catalog: Browse, search, and filter products by category, price, and keyword.
- Product Details: Detailed product pages with images, plant care info, reviews, and variations.
- Cart & Checkout: Add/remove products, manage variations, view cart, apply user-specific coupon codes, checkout with tax calculation.
- Order Management: Place orders, view order history, order details, payment via Razorpay, order confirmation emails.
- Reviews: Submit and update product reviews.
- Admin Panel: Manage users, products, categories, orders, and chat limits.
- Modern UI: Responsive design with Bootstrap and custom CSS.
- AI Assistant: Chatbot for plant care, product help, and order support (text, image, and voice), with starter suggestions in the chat.
- Human-in-the-Loop: Requests the AI can't handle (price matches, cancellations, refunds, damaged items, bulk orders, complaints) are escalated to the team on Slack, where staff approve, edit, reject, or take over the chat live.
- Price-Match Coupons: An approved price match creates a coupon code that only works for that customer and product.
- Conversational Assistant:
- Handles plant care queries, product recommendations, order status, and cart management (including quantities, e.g. "add 2 marigolds")
- Supports text and image-based queries (can identify plants from images)
- Understands casual and mixed-language messages (English/Hindi/Hinglish, typos)
- Politely declines off-topic requests and attempts to override its instructions
- Remembers the conversation across sessions (stored in PostgreSQL)
- Rate-limited (max 10 AI messages per user; messages to staff during a takeover don't count)
- Voice Features:
- Text-to-Speech (TTS) and Speech-to-Text (STT) using ElevenLabs
- Agent Modules:
- Supervisor Agent: Routes each message to one sub-agent using LLM-based classification (with recent conversation as context)
- Cart Agent: Add/view/remove items in cart
- Order Agent: Fetch order details, order history, redirect to checkout and my orders links
- Research Agent: Plant care, diseases, watering, sunlight, etc.
- Recommendation Agent: Suggests products based on user needs and catalog
- General Agent: Greetings, thanks, and questions about the conversation itself ("what did the team say?")
- Escalation Intake: Collects the details staff need and hands the request to a human (see below)
flowchart TD
User[User Query/Input] --> Supervisor[Supervisor Agent]
Supervisor -->|Cart| Cart[Cart Agent]
Supervisor -->|Order| Order[Order Agent]
Supervisor -->|Recommendation| Recommendation[Recommendation Agent]
Supervisor -->|Research| Research[Research Agent]
Supervisor -->|Small talk / follow-up| General[General Agent]
Supervisor -->|Needs a human| Intake[Escalation Intake]
Cart -- Needs Variation? --> Variation[Variation Selection]
Variation -- After Selection --> Cart
Intake -->|Ticket| Slack[(Slack: Plantae team)]
Cart --> Response[Response Node]
Order --> Response
Recommendation --> Response
Research --> Response
General --> Response
Intake --> Response
Response --> UserResp[Final Response to User]
User -- Image Uploaded --> PlantID[Plant Identification]
PlantID --> Supervisor
%% Notes:
%% - Supervisor routes to one agent per query
%% - Variation selection only for cart
%% - Plant identification augments user input if image is uploaded
See agent/README.md for the detailed agent and escalation architecture.
When a request needs a person, the AI hands it to the Plantae team in a Slack channel instead of guessing.
- Price match / better deal, order cancellation, delivery issues, payment issues: the AI collects the details (product, price, order number, ...), checks them against the database, and proposes an action for staff to approve.
- Damaged or wrong item, bulk orders, complaints, or when the customer asks for a human (or is clearly upset): the conversation is handed over so staff can chat with the customer directly.
- Customers can also use the π€ Talk to a human button or suggestion in the chat at any time.
1. Approve the AI's proposal (staff β AI). A ticket card is posted to Slack with a summary, the customer's details, facts from the database, and the AI's proposal. Staff click:
| Button | What happens |
|---|---|
| Approve | The action is carried out (e.g. the order is cancelled) and the AI tells the customer. |
| Edit & approve | Staff change the proposal (e.g. 15% β 10%) and add a note; the AI tells the customer the edited outcome. |
| Reject | Staff give a reason; the AI explains it to the customer. Nothing is changed. |
| Take over chat | Switches to a live conversation (below). |
2. Talk to the customer (staff β customer). The AI goes quiet. The customer's messages appear in the ticket's Slack thread, and staff replies in that thread appear in the customer's chat (labelled with the staff member's name). Hand back to AI closes the ticket; the AI then remembers what staff said.
Ticket card with the AI proposal and the final price
Edit & approve: the final price updates as the discount changes
Resolved ticket
Live chat with the customer in the ticket thread
| Customer view during takeover | Customer view after approval |
|---|---|
![]() |
![]() |
- Tickets wait in Slack until staff act; a reminder is posted if a ticket waits longer than
HITL_SLA_MINUTES, and afterHITL_EXPIRE_HOURSthe customer is told the team will follow up by email. - If Slack is not configured, staff are emailed and can decide everything from the Django admin (Escalation Tickets), which has the same Approve / Edit / Reject / Take over / Reply actions.
- Every action is recorded on the ticket (who, when, what), visible in the admin.
When staff approve a price match, the customer gets a coupon code such as ROSE10:
- Only for that customer: codes are looked up together with the logged-in account, so another customer entering
ROSE10gets "not valid for your account". - Only for that product, for up to 2 units per order (
PRICE_MATCH_MAX_UNITS). - Valid for 7 days (
PRICE_MATCH_VALID_DAYS) and single use: it is marked used only after Razorpay confirms the payment. - The customer enters it in the coupon box on the cart or checkout page; the discount is shown before tax (18% GST is charged on the discounted amount) and saved on the order.
- Coupons are listed in the admin (owner, product, %, expiry, when used, order, and the ticket that created it).
Handles user authentication, registration, profile management, and user dashboard.
- Custom user model (
Account) with email as the username. - User registration, login, logout, and email activation.
- User profile management (
UserProfile), including address and profile picture. - Password reset and change, user dashboard, and context processor for user info.
AI-powered chat assistant for plant care, shopping, and order support.
- Chat interface for users to interact with the AI assistant.
- Handles plant identification from images using LLMs.
- Supports cart, order, product recommendation, and plant care queries.
- Voice integration (STT/TTS), conversation memory (PostgreSQL), and rate limiting.
- Human-in-the-loop escalations to Slack, with a background worker for notifications and follow-ups.
- Admin tools for chat history, agent errors, and escalation tickets.
- See Detailed Agent Workflow Diagram for advanced logic.
Manages shopping cart functionality for users (both authenticated and guests).
- Add, remove, and update products in the cart.
- Handles product variations (color, size, etc.).
- Calculates cart totals, coupon discount, tax, and grand total in one place (
carts/pricing.py). - User-specific coupon codes (
Coupon), created from approved price matches. - Checkout process integration and cart item count context processor.
Manages product categories for the store.
- CRUD for product categories (name, slug, description, image).
- Used for filtering and organizing products in the store.
- Context processor for category navigation.
Handles order placement, payment, and order history.
- Place orders from cart items, payment via Razorpay.
- Stores order details, shipping address, payment info, and any coupon discount.
- Order status tracking, order history, and order detail views.
- Sends order confirmation emails.
Manages products, product variations, reviews, and the main store interface.
- Product listing, detail, and search.
- Product variations (color, size, pack), reviews, and gallery images.
- Plant care information for products.
- Pagination and price filtering.
- Backend: Django 4.2 (Python)
- Frontend: Django Templates, Bootstrap, jQuery, FontAwesome
- AI/Agent: LangChain, OpenAI, LangGraph (PostgreSQL checkpointer), ElevenLabs (TTS/STT)
- Human-in-the-Loop: Slack (Block Kit buttons, threads, signed webhooks), django-q2 background worker
- LLM: OpenAI's gpt-5.6-luna
- Database: Azure Database for PostgreSQL Flexible Server
- Payments: Razorpay
- Other: dotenv, crispy-forms, admin-thumbnails, etc.
See requirements.txt for full dependency list.
- Clone the repo:
git clone https://github.com/ayushraj09/plantae.git cd plantae - Install dependencies:
pip install -r requirements.txt
- Set up environment variables:
- Copy
.env-sampleto.envand fill in your secrets.
- Copy
- Run migrations and set up agent memory tables:
python manage.py migrate python manage.py setup_checkpointer
- Create superuser:
python manage.py createsuperuser
- Run the server and the background worker (two terminals):
The worker posts tickets to Slack, writes the AI's reply after staff decide, and sends reminders. Slack is optional for local development; without it, tickets are handled from the admin.
python manage.py runserver python manage.py qcluster
- Access:
- Website: http://127.0.0.1:8000/
- Admin: http://127.0.0.1:8000/admin/
Load testing was performed with Locust using the scenario in locust_loadtest/locustfile.py. The suite mixes three weighted user types against production: anonymous browsers (weight 4), authenticated customers who log in and exercise cart/checkout paths (weight 3), and a cart-profiling user that hammers add_cart (weight 1). Each page load also fetches its referenced static and media assets, mirroring real browser traffic.
- Anonymous browse:
GET /,/store/,/store/category/plants/,/store/search/, product detail pages - Authenticated journey: login, dashboard, my orders, browse,
POST /cart/add_cart/{id}/, cart view, checkout page - Cart profiling: repeated
POST /cart/add_cart/{id}/, cart and checkout reads - Static/media: up to 12 referenced assets per page (
/static/β¦,/media/β¦)
Order-placement POSTs were left disabled in both runs so no test orders were written to the production database.
50 concurrent users, spawn rate 5/s, 3-minute run.
| Metric | Result |
|---|---|
| Peak users | 50 |
| Total requests | 8,351 |
| Total failures | 5 |
| Failure rate | 0.06% |
| Average throughput | 46.7 requests/second |
| Aggregate median response time | 44 ms |
| Aggregate 95th percentile | 3,300 ms |
| Aggregate 99th percentile | 6,200 ms |
| Maximum response time | 7,597 ms |
The aggregate median is dominated by the ~7,400 static/media requests, which Apache served in 42-43 ms. Dynamic Django views told a very different story:
- Static and media assets held up well: 42-43 ms median across all asset types, 95th percentile between 55 and 360 ms, zero failures.
- Dynamic pages degraded: home, store, category, product detail, and search all sat at a 2,800-3,000 ms median. Authenticated dashboard and my-orders pages were 2,600-2,700 ms. Login POST was 2,000 ms.
- Cart writes were the worst path:
POST /cart/add_cart/{id}/had a 6,000-6,200 ms median, roughly 2x any read path. - 5 failures: four
RemoteDisconnectederrors and one403from a stale CSRF token on a retried cart-add POST.
200 concurrent users, spawn rate 10/s, 3-minute run.
| Metric | Result |
|---|---|
| Peak users | 200 |
| Total requests | 11,209 |
| Total failures | 591 |
| Failure rate | 5.27% |
| Average throughput | 62.6 requests/second |
| Aggregate median response time | 52 ms |
| Aggregate 90th percentile | 12,000 ms |
| Aggregate 99th percentile | 19,000 ms |
| Maximum response time | 30,880 ms |
Quadrupling the user count barely moved throughput (46.7 β 62.6 req/s) while latency and errors collapsed:
- Dynamic pages: ~13,000 ms median across every browse and account view.
- Cart writes: 25,000-31,000 ms median on
add_cartPOSTs. - Responses capped near 31 s, indicating a proxy/Gunicorn timeout ceiling rather than eventual completion.
- 591 failures (5.27%): mostly
RemoteDisconnectedas the server dropped connections; the auth path itself began failing (23 failed logins, 10 unreachable dashboards), and CSRF-dependent cart POSTs 403'd because the page that carries the token never loaded.
Anonymous browse journey only (no login), 90 s per level, spawn rate 20/s.
| Concurrent users | Failure rate | Successful requests |
|---|---|---|
| 100 | 0.43% | 99.57% |
| 150 | 0.63% | 99.37% |
| 200 | 17.24% | 82.76% |
| 250 | 23.03% | 76.97% |
| 500 | 27.28% | 72.72% |
~150 concurrent unauthenticated users is the ceiling for a healthy service (>99% success). Between 150 and 200 the site falls off a cliff as Gunicorn's queue overflows and connections are dropped; past 250, throughput drops while errors climb.
The server handles ~150 concurrent unauthenticated browsers or ~50 concurrent authenticated customers before failures climb. Dynamic Django throughput is capped at ~15-20 requests/second regardless of load β consistent with a small Gunicorn worker pool where each database-writing request holds a worker for seconds. Beyond those limits, added concurrency produces queueing and timeouts, not more work done. The bottleneck is not the network: static assets from the same host returned in ~43 ms across every run.
Raw Locust result files are in locust_loadtest/ (plantae_loadtest_* for the baseline, plantae_stress_* for the stress run), with full HTML reports at plantae_loadtest_report.html and plantae_stress_report.html.
- Production: Azure VM with Docker Compose, deployed by GitHub Actions on every push to
main(deploy.sh). - Domain: https://plantaeai.tech
- Containers:
web(plantae): runs migrations,setup_checkpointer,collectstatic, then Gunicorn.worker(plantae-worker):python manage.py qclusterfor Slack notifications, AI replies after staff decisions, and SLA reminders.
- Static & Media: Collected into
static/and served by Apache; the chat widget's CSS/JS URLs carryCHAT_WIDGET_VERSIONso browsers fetch new versions after a deploy. - Database: Azure Database for PostgreSQL (also stores agent memory and the background task queue).
Copy .env-sample to .env. Settings added for the agent and human-in-the-loop:
| Variable | Purpose |
|---|---|
SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET, SLACK_ESCALATION_CHANNEL |
Slack app credentials and the channel for tickets |
HITL_STAFF_EMAILS |
Email fallback and overdue reminders (comma-separated) |
HITL_SLA_MINUTES, HITL_EXPIRE_HOURS |
When to remind staff, and when to close unanswered tickets |
PRICE_MATCH_MAX_UNITS, PRICE_MATCH_VALID_DAYS |
Coupon limits (default 2 units, 7 days) |
SITE_URL |
Public URL used in links from Slack/email |
CHAT_WIDGET_VERSION |
Bump when the chat widget's CSS/JS change |
DB_SSLMODE |
Database SSL mode (default require) |
- Create a Slack app with bot scopes
chat:write,users:read,users:read.email,channels:history(andgroups:historyfor a private channel), install it, and invite it to the escalation channel. - Interactivity request URL:
https://<your-domain>/agent/slack/interactions/ - Event Subscriptions request URL:
https://<your-domain>/agent/slack/events/, subscribed tomessage.channels(ormessage.groups). - Staff are matched to site accounts by email; give their accounts
is_staffso decisions are recorded under their name.
This project is licensed under the MIT License.
- Email: ayush.ttps@gmail.com
Pull requests are welcome! Please open an issue first to discuss changes.
















