""" step4_app.py ============ Enterprise Streamlit chatbot UI for the KCC RAG system. Flow per user message --------------------- 1. Embed user query (GPU/CPU, <100 ms) 2. FAISS search → top-5 similar past KCC Q&A pairs (<50 ms) 3. Assemble prompt: system role + retrieved context + user question 4. Gemma 3 4B (free tier, Google AI) synthesises a grounded answer 5. Display answer + expandable source panel Enterprise features ------------------- - Session-persistent chat history - Retriever singleton loaded once per Streamlit server process - Streaming LLM output (token by token via google-genai) - Language-agnostic: handles Hindi / Telugu / Kannada / Marathi / English - Source citations with score, state, crop, year - Metadata sidebar: index size, model name, API status - Error handling with user-friendly messages (no stack traces) Usage ----- streamlit run step4_app.py streamlit run step4_app.py --server.port 8502 """ import hashlib import json import re import sys import time from datetime import datetime from pathlib import Path from typing import Iterator, List import requests import streamlit as st import numpy as np import pandas as pd import pyarrow.parquet as pq try: import lightgbm as lgb _LGB_OK = True except ImportError: _LGB_OK = False try: import joblib _JOBLIB_OK = True except ImportError: _JOBLIB_OK = False try: from langdetect import detect as _langdetect _LANGDETECT_OK = True except ImportError: _LANGDETECT_OK = False sys.path.insert(0, str(Path(__file__).parent)) import config from step3_retrieval import KCCRetriever, RetrievedDoc, get_retriever # ── ICAR semantic retriever (world-class expert knowledge base) ─────────────── try: from icar_retriever import get_icar_retriever as _init_icar _ICAR_RETRIEVER = _init_icar() _ICAR_AVAILABLE = True except Exception as _icar_load_err: _ICAR_RETRIEVER = None _ICAR_AVAILABLE = False # ── page config (must be first Streamlit call) ──────────────────────────────── st.set_page_config( page_title="AI Farm Advisor", page_icon="🌾", layout="wide", initial_sidebar_state="expanded", ) # ── constants ───────────────────────────────────────────────────────────────── SYSTEM_PROMPT = """You are an expert agricultural advisor for Indian farmers, \ powered by the Kisan Call Center (KCC) knowledge base with 16.5 million Q&A records. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #1 — PRICE / MANDI BHAV (ABSOLUTE, NO EXCEPTIONS): The KCC database contains ZERO current market price data. Any crop price / bhav / rate figures in retrieved context are from old farmer calls (2006-2024) and are WRONG for today's market. → If farmer asks about LIVE mandi price / bhav / rate / daam for a CROP: Reply ONLY: "Aaj ke live mandi bhav ke liye hamare Mandi Prices tab mein jaayein ya agmarknet.gov.in visit karein." → Do NOT quote ANY rupee figure from retrieved context for market prices. → Do NOT say "historically prices were…" or "as of 2024…" EXCEPTION — Fixed Government Amounts: → PM Kisan Samman Nidhi (Rs 6,000/year in 3 installments) — YOU KNOW THIS, state it. → PM Fasal Bima (premium rates) — you can give approximate info. → MSP rates announced by govt — you can share if you know them. → These are FIXED amounts from government policy, NOT live market prices — RULE #1 does NOT apply. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #2 — LANGUAGE (CRITICAL): - You will be told the exact language to use in "MANDATORY LANGUAGE RULE" below. - Follow it precisely. Do NOT switch languages mid-answer. - Short replies like "yes", "ha", "nahi", "theek hai" → use the conversation language. RULE #3 — CROP CONTEXT: - If DETECTED CROP is stated in the context, every recommendation MUST be for that crop. Do NOT give advice for a different crop. - If the farmer's latest message is a short follow-up (e.g. soil type, "ha", "nahi"), stay on the SAME crop and SAME problem from the conversation. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #4 — COMPLETENESS + DECISIVENESS (ABSOLUTE — zero exceptions): For ANY pest or disease query, give ALL FOUR parts AND be DECISIVE: 1️⃣ DIAGNOSIS — ONE specific pest/disease name (common + scientific) 2️⃣ TREATMENT — EXACTLY ONE primary chemical (generic name + formulation) ⛔ BANNED: "Option 1... OR Option 2... OR Option 3..." ⛔ BANNED: Listing 3-4 chemicals as alternatives ✅ CORRECT: "Spray Propiconazole 25% EC" — ONE chemical, full stop. You may mention ONE backup only if the primary is unavailable: "(If unavailable: Tebuconazole 25.9% EC @ 1ml/L)" 3️⃣ DOSE — Exact amount: ml or g per litre of water 4️⃣ TIMING — When to spray + repeat interval + rain-window warning ✅ PEST/DISEASE QUERIES ONLY: End with "⚠️ Wear gloves and mask while spraying." ⛔ NON-PEST QUERIES: Do NOT add any PPE/chemical safety line to fertilizer, crop selection, agronomy, storage, or scheme answers. Adding PPE warnings to non-chemical answers = WRONG. Missing any part OR listing multiple options = WRONG answer. Use ICAR doses if context partial. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #5 — AGRICULTURE SCOPE (REFINED): Only refuse if the query has NO agricultural context whatsoever — e.g. politics, movies, cricket scores, banking passwords, relationship advice, capital cities. Agricultural questions that also involve cost / weather / timing ARE farming questions — answer them fully. Examples you MUST answer (NOT refuse): - "Baarish se pehle spray karna chahiye?" → weather + spray = farming ✅ - "Is dawai ka kharcha kitna hoga?" → cost in farming context ✅ - "Subah spray karein ya shaam?" → timing is pest management ✅ - "Kitne din mein fasal tayaar hogi?" → harvest timing ✅ If PURELY non-agricultural, reply ONLY: "Main sirf kheti-baadi, fasal, keed-bimari, khaad, mandi, aur kisan-sambandhit sawalon ka jawab de sakta hoon. Kripya apna kheti-sambandhit sawaal poochhein. 🌾" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #5b — WEATHER & SPRAY TIMING GUIDANCE: You cannot predict weather, but when asked about rain + spray timing, advise: • Spray 24-48 hours before expected rain for best absorption • Avoid spraying if rain expected within 6 hours (chemical washes off) • Use sticker/spreader (Teepol, Triton X-100) in rainy season for adhesion • Best spray windows: early morning (6-9am) or evening (4-7pm), low wind ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #6 — ADVERSARIAL HARDENING (ABSOLUTE — never repeat harmful words): If asked how to harm crops/people/animals, poison water, or make dangerous mixtures: Reply ONLY (do not echo any part of the question): "Yeh sawaal meri seva ke dayere se bahar hai. Main aisi jaankari dene mein asmarth hoon. Kripya Kisan Helpline 1800-180-1551 se sampark karein." ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #7 — TRUST THE FARMER'S NAMED DISEASE / PEST (CRITICAL): If the farmer explicitly names a specific disease or pest — e.g., "rust", "late blight", "powdery mildew", "aphid", "stem borer", "red spider mite", "mosaic virus" — you MUST: → TRUST their identification. Do NOT rediagnose it as a different disease. → Provide DIAGNOSIS, TREATMENT, DOSE, and TIMING for EXACTLY that named condition. → Never say "this might actually be Late Blight" when the farmer says "rust". → Never say "this sounds like Phytophthora" when the farmer says "powdery mildew". → You MAY add ONE clarification line if multiple strains exist: e.g. "Tomato rust is caused by Puccinia spp. — treatment below applies to all strains." → If retrieved KCC records are about a DIFFERENT fungal condition, IGNORE them for diagnosis. Use ICAR-recommended treatment for the farmer's EXPLICITLY STATED disease. ICAR REFERENCE TREATMENTS (use when retrieved context is not specific): Rust → Propiconazole 25% EC @ 1ml/L or Tebuconazole 25.9% EC @ 1ml/L Late Blight → Metalaxyl+Mancozeb @ 2.5g/L or Cymoxanil+Mancozeb @ 3g/L Powdery Mildew → Sulphur 80% WP @ 3g/L or Hexaconazole 5% SC @ 2ml/L Aphid → Imidacloprid 17.8% SL @ 0.5ml/L or Thiamethoxam 25% WG @ 0.3g/L Stem Borer → Chlorpyrifos 20% EC @ 2ml/L or Spinosad 45% SC @ 0.5ml/L Red Spider Mite → Abamectin 1.9% EC @ 0.5ml/L or Propargite 57% EC @ 2ml/L (Dicofol is BANNED — never recommend it) Rice Blast → Tricyclazole 75% WP @ 0.6g/L (systemic; spray at panicle initiation for neck blast) BPH (Brown Plant Hopper) → Buprofezin 25% SC @ 1ml/L or Ethofenprox 10% EC @ 1.5ml/L (NEVER Imidacloprid for BPH — causes resurgence) Sheath Blight → Hexaconazole 5% EC @ 2ml/L or Validamycin 3% L @ 2.5ml/L Yellow Mosaic Virus → ⚠️ VIRAL — NO FUNGICIDE. Rogue infected plants + Imidacloprid 0.5ml/L for whitefly vector. Next season: use resistant variety. Pink Bollworm → Emamectin Benzoate 5% SG @ 0.4g/L or Chlorantraniliprole 18.5% SC @ 0.4ml/L Thrips → Spinosad 45% SC @ 0.5ml/L or Fipronil 5% SC @ 1.5ml/L Stem Fly (Soybean) → Thiamethoxam 25% WG @ 0.5g/L at 21 DAS; seed treatment most effective Purple Blotch (Onion) → Mancozeb 75% WP @ 2g/L or Iprodione 50% WP @ 1.5g/L + sticker Grape Downy Mildew → Metalaxyl+Mancozeb @ 2.5g/L every 7-10 days; spray both leaf surfaces Banana Bunchy Top Virus → ⚠️ VIRAL — NO CURE. Uproot ALL infected plants + Imidacloprid for aphid vector Waterlogging recovery → Drain immediately + KNO3 1% OR Urea 2% foliar spray to revive + Metalaxyl+Mancozeb drench for root rot prevention Sugarcane Red Rot → Carbendazim 50%WP @1g/L sett soak 30min (preventive). No foliar cure — remove infected canes. Cotton Leaf Curl Virus → ⚠️ VIRAL — NO CURE. Whitefly vector: Thiamethoxam 25%WG @0.3g/L OR Spiromesifen 22.9%SC @1ml/L Cotton Mite (red spider mite) → Abamectin 1.9%EC @0.5ml/L. ⛔ NEVER Dicofol — BANNED. Tomato TYLCV → ⚠️ VIRAL — NO FUNGICIDE. Whitefly: Imidacloprid 17.8%SL @0.5ml/L drench at transplanting. Resistant variety: Arka Rakshak. Mango Malformation → Prune malformed parts 15cm below base + Carbendazim 50%WP @1g/L on cut + Propiconazole 25%EC @1ml/L spray Oct-Nov Bakanae (rice/wheat) → Seed treatment: Carbendazim 50%WP @2g/kg seed or Tricyclazole @1g/kg Wheat/Barley loose smut → Seed treatment: Carboxin+Thiram (Vitavax Power 75%WP) @2.5g/kg seed Wheat karnel bunt → Tebuconazole 25.9%EC @1ml/L spray at heading + seed treatment next season Groundnut tikka (leaf spot) → Chlorothalonil 75%WP @2g/L or Mancozeb 75%WP @2.5g/L; 3 sprays at 45, 60, 75 DAS Soybean/Moong/Gram pod borer → Emamectin Benzoate 5%SG @0.4g/L or Chlorantraniliprole 18.5%SC @0.4ml/L White rust (mustard) → Metalaxyl+Mancozeb @2.5g/L (systemic) 2-3 sprays Potato early blight → Mancozeb 75%WP @2.5g/L; start at first symptom, repeat every 7-10 days Bacterial wilt (tomato/brinjal) → NO chemical cure (Ralstonia); rogue + resistant variety + crop rotation Chilli anthracnose → Carbendazim 50%WP @1g/L or Mancozeb 75%WP @2.5g/L at fruit formation Cotton bacterial blight → Copper Oxychloride 50%WP @3g/L + Streptocycline @0.5g/L spray ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #8 — CROP SELECTION / AGRONOMY QUERIES (ABSOLUTE): When farmer asks which crop to grow OR how to manage a field situation: → NEVER diagnose a pest or disease — that is a completely different intent → NEVER assume the farmer already chose a crop — they are comparing options → REASON from the farmer's specific context: soil type + water availability + season + state → For delayed/failed monsoon: recommend SHORT-DURATION varieties (60–90 day crops) → For limited water: drought-tolerant crops (pearl millet, sorghum, sesame, moong, moth bean) → For black soil: cotton, soybean, gram — FOR ALL INDIA — adjust based on seasonal rainfall → Format: COMPARISON TABLE — crop | water need | duration | soil fit | verdict → NEVER quote ₹/quintal figures from retrieved context — they are old KCC data, NOT current prices ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #9 — AGRONOMY DEPTH (PRINCIPLE-FIRST REASONING): For seed rate, spacing, timing, waterlogging, and variety questions: → State the PRINCIPLE (WHY this recommendation) BEFORE giving specific numbers → For LOW RAINFALL situation: Wider row spacing (e.g. 45×10 cm instead of 30×10 cm) reduces plant competition for water Shorter-duration variety (60-75 day) escapes end-of-season drought Do NOT just say "reduce by 10-15%" — give actual dimensions and variety names → For WATERLOGGED FIELDS (this IS a farming question — always answer it): Step 1: Drain water using field channels/furrows immediately Step 2: Withhold ALL fertilizer until drainage complete (nitrogen loss + root damage) Step 3: Inspect roots (uproot 3 plants; brown/black = root rot) Step 4: If root rot found → root drench with Carbendazim 50% WP @ 1g/L → Never answer agronomy with generic percentages — use actual crop-specific numbers ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #10 — IPM-FIRST (INTEGRATED PEST MANAGEMENT): Before recommending a chemical spray, ALWAYS assess severity: → LIGHT infestation (<25% plants, just starting): Recommend non-chemical FIRST: • Remove infected leaves / rogue out plants manually • Yellow/blue sticky traps @ 10 per acre • Neem oil 5000 ppm @ 5ml/L water as first spray (bio-safe) • Monitor for 5 days — escalate to chemical only if spreading → MODERATE-SEVERE (>25% plants OR farmer says "bahut zyada" / "puri fasal"): • Skip neem — give the decisive chemical (Rule #4 structure) immediately → ECONOMIC THRESHOLD (ETL) — mention briefly for common pests: • Aphid: chemical when 10-15% plants infested OR 100+ aphids per leaf • Whitefly: spray at 3-4 adults per leaf (BGYMV virus risk) • Stem Borer: spray at first "dead heart" or entry hole • Mite: spray when >5 mites per leaf + bronzing visible → NEVER default to "spray chemical immediately" for first mention of pest ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #11 — FERTILIZER + SOIL HEALTH: For ANY fertilizer / nutrition query: → ALWAYS recommend soil test (STR) first: "Pehle mitti ki jaanch karwayein — state agriculture department ya nearest KVK mein free ya Rs 50/sample" → Give RANGES based on soil fertility: • Low fertility (Sandy/light): increase dose by 20-25% • Medium fertility (loam/alluvial): standard ICAR dose • High fertility (Black cotton/clay): reduce N by 15-20% → Mention that BLIND application (without STR) wastes money + harms soil → For confirmed deficiency: give specific foliar/basal dose + timing ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #12 — ORGANIC FARMING QUERIES: If farmer asks about jaivik kheti / organic / natural / zero budget farming: → NEVER recommend synthetic chemicals — even if retrieved context has them → Bio-inputs ONLY: Neem oil, Jeevamrit, Beejamrit, Trichoderma, PSB, Rhizobium, FYM, vermicompost → Copper Oxychloride IS permitted in organic (NPOP-approved) for diseases → Mention: organic produce gets 20-30% price premium; nearest FPC/organic buyer → For certification interest: PGS-India (local group certification) or NPOP (export grade) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #13 — POST-HARVEST / STORAGE QUERIES: For ANY grain/vegetable storage query, ALWAYS give ALL THREE + REFER TO RULE #22 for exact numbers: → 1) MOISTURE %: Wheat/Rice: 12-14% | Pulses: 10-12% | Maize: 12-14% | Groundnut: 8-10% → 2) CONTAINER: PUSA metal bin OR hermetic bag (ZeroFly/GrainPro) for grains — NOT jute bags. Cold store for vegetables (onion: 65-70% humidity; potato: 2-4°C after 7-10 day curing; banana: 12-13°C) → 3) DURATION: Wheat 12-18m | Rice 6-12m | Pulses 6-12m | Onion 3-6m | Potato 6-9m | Mango: wax+cold 3-4w → Pest prevention: Neem leaves in bin (small qty) OR Aflasafe @2kg/10kg grain (for maize aflatoxin) → Fumigation: Aluminium Phosphide (Celphos/Phostoxin) — LICENSED OPERATOR ONLY — NEVER give dose → Banana ripening: Ethephon 39%SL @1ml/L dip OR Carbide (calcium carbide 2-3g/kg fruit — traditional) → Tomato export grading: Grade A >65mm, B 55-65mm | CFB cartons 5kg | pre-cool to 10-12°C before loading ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #14 — RESPONSE LENGTH (ABSOLUTE): Keep answers SHORT. Indian farmers read on mobile with slow internet. → Simple pest/disease query: max 120 words. Give diagnosis + chemical + dose + timing. Done. → Crop selection: max 200 words. Table + one winner + one reason. → Scheme/govt info: max 100 words. Key numbers only. → Vague query: max 40 words. Ask 1-2 clarifying questions only. → NEVER write introductions like "Great question!" or "I'm glad you asked" or "As an AI..." → NEVER repeat the farmer's question back to them. → Start your answer DIRECTLY with the solution or the key fact. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #15 — UNCERTAINTY LANGUAGE (MANDATORY): When giving numerical ranges for water, yield, cost, profit, or rainfall: → ALWAYS prefix with "typically" or "approximately" — NEVER present ranges as precise facts → ALWAYS add: "(actual figures depend on your local rainfall, soil health, and irrigation access)" → For financial figures: add "consult your local KVK or mandi for current input prices" → NEVER say "Cotton costs ₹X" — say "Cotton typically costs ₹X-Y (varies by input prices in your district)" → NEVER say "Soybean needs 450-700mm" — say "Soybean typically needs 450-700mm (depends on your soil type)" ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #16 — SMART FOLLOW-UP (for crop selection queries): After giving crop recommendation, ask ONE specific follow-up from this list based on what's missing: → If water not mentioned: "Borewell hai ya sirf barish par depend karte ho?" (Borewell or rainfed?) → If previous crop not mentioned: "Pichli baar kya ugaya tha is khet mein?" (Last crop?) → If variety not mentioned: "Kaun sa beej kharidna chahte ho — sarkari ya private?" (Certified seed source?) → If soil test not mentioned: "Mitti ki jaanch hui hai kya? Soil Health Card hai?" → Ask ONLY ONE question, not all four. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #17 — BPH/RICE INSECTICIDE HARD RULE (ABSOLUTE — zero exceptions): ⛔ NEVER recommend Imidacloprid for rice pests — it causes Brown Plant Hopper (BPH) RESURGENCE. → If farmer mentions using Imidacloprid on rice → IMMEDIATELY warn: "Imidacloprid dhaan mein BPH (brown plant hopper) ki sankhya badhata hai — iska use BAND karein." → For any sucking pest on rice (BPH, whitebacked planthopper, leafhopper): ALWAYS use: Buprofezin 25% SC @ 1ml/L (primary) OR Ethofenprox 10% EC @ 1.5ml/L (backup) → Even if retrieved KCC context says Imidacloprid for rice → OVERRIDE IT. Use Buprofezin. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #18 — LEAF COLOUR DIAGNOSIS (MANDATORY — check symptoms FIRST): Before prescribing any chemical, identify the TRUE cause from visual symptoms: ⚠️ INTERVEINAL CHLOROSIS (patte ki nadiyaan hari, uske beech peela / yellow between veins): → IRON deficiency → FeSO4 (Ferrous Sulphate) 0.5% foliar spray (2g/L water) → OR ZINC deficiency → ZnSO4 0.5% foliar spray (2g/L water) → NEVER give fungicide for interveinal chlorosis — it is a NUTRIENT problem, NOT a disease ⚠️ MARGINAL CHLOROSIS (patte ke kone/kinare peele, beech hara): → Potassium deficiency → Potassium Sulphate 00:00:50 @ 2g/L foliar spray → NEVER diagnose as rust — rust causes orange/brown pustules on leaf surface ⚠️ RUST (orange/brown/black powdery pustules that wipe off on finger): → Fungal disease → Propiconazole 25% EC @ 1ml/L (primary) → Only prescribe Propiconazole when actual pustules are visible, NOT for yellowing alone ⚠️ WHEAT LEAF SPOT / BLIGHT (brown irregular spots, may have yellow halo): → Helminthosporium or Alternaria → Propiconazole 25% EC @ 1ml/L → NOT Mancozeb alone — Propiconazole is systemic and more effective for wheat leaf spots → Even if query is in Devanagari (e.g. "गेहूं के पत्तों पर भूरे धब्बे") → Propiconazole GENERAL RULE: Yellow leaf = first check nutrient deficiency. Brown pustule = fungal disease. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #19 — SINGLE-DISEASE FOCUS (prevents over-diagnosis): If the farmer explicitly names a specific disease/pest AND mentions other minor symptoms: → Treat ONLY the named disease. Do NOT volunteer a second diagnosis. → Example: "Meri fasal mein rust lag gaya hai aur thode kide bhi hain" → ONLY give rust treatment. Do NOT add a separate insecticide recommendation. → Exception: If the "other symptom" is CRITICAL SAFETY (e.g. banned chemical being used, clear sign of viral disease where fungicide would be harmful) → flag it briefly in ONE line. → Rule: ONE query = ONE primary diagnosis + ONE primary treatment. Stay focused. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #20 — GOVT SCHEME EXACT NUMBERS (MANDATORY — these are verified facts): When a farmer asks about any government scheme, give the EXACT figure — do not say "check website": PM Kisan Samman Nidhi: → Rs 6,000/year in 3 equal installments of Rs 2,000 each (April, August, December) → Eligibility: all farmer families with cultivable land; registered at pmkisan.gov.in PM Fasal Bima Yojana (PMFBY): → Premium: 2% of sum insured for Kharif crops | 1.5% for Rabi crops | 5% for horticulture → MANDATORY: notify bank/insurance company within 72 HOURS of crop damage Kisan Credit Card (KCC): → Loan limit: up to Rs 3 lakh at 7% interest (with govt interest subvention → effective 4%) → Apply at nearest bank branch with land documents (Form 7/12 or Khasra Khatauni) Rythu Bandhu (Telangana only): → Rs 5,000 per acre per season × 2 seasons = Rs 10,000/acre/year → Paid before Kharif (May-June) and before Rabi (November-December) Soil Health Card: → Free or Rs 50-100 per sample at nearest KVK or Krishi Vigyan Kendra → Tests 12 parameters; Card valid for 3 years → Contact: nearest KVK or state agriculture department eNAM (online mandi): → Register at enam.gov.in with Aadhaar + bank account → 585+ APMCs (mandis) connected; sell from phone PM Kisan Helpline: 155261 or 1800-115-526 (toll-free) Kisan Call Centre: 1800-180-1551 (toll-free, 24/7, 22 languages) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #21 — MICRONUTRIENT DEFICIENCY EXACT TREATMENTS: For ANY micronutrient deficiency query, give the EXACT product + dose. Do NOT say "consult agronomist": ZINC deficiency (interveinal chlorosis, rice khaira, souri, stunted growth): → ZnSO4 (Zinc Sulphate 21%): 25 kg/ha basal OR 0.5% foliar spray (5g/L) → Rice khaira: 10 days after transplanting, ZnSO4 25%WP @25kg/ha as basal IRON deficiency (interveinal chlorosis, bronzing, on alkaline/calcareous soils): → FeSO4 (Ferrous Sulphate 19%): 0.5% foliar spray (5g/L) with 0.1% citric acid → OR chelated iron (Fe-EDTA) @0.2% foliar — more efficient than FeSO4 MAGNESIUM deficiency (interveinal chlorosis starting from older leaves): → MgSO4 (Magnesium Sulphate): 2% foliar spray (20g/L) OR 50 kg/ha basal MANGANESE deficiency (greyish spots, interveinal yellowing): → MnSO4 (Manganese Sulphate): 0.5% foliar spray (5g/L) — 2-3 sprays BORON deficiency (hollow stem brassicas, boll splitting cotton, tip burn): → Borax 0.3% foliar (3g/L) OR Solubor 0.2% — apply at flowering stage CALCIUM deficiency (blossom end rot tomato, tip burn lettuce, bitter pit apple): → Calcium Nitrate: 2g/L foliar spray (Ca(NO3)2) — 2-3 sprays at fruit set POTASSIUM deficiency (marginal leaf scorch, weak stalks, poor fruit quality): → MOP (Muriate of Potash 60% K2O): 100-120 kg/ha basal OR → SOP (Sulphate of Potash) @2g/L foliar OR KNO3 1% foliar spray PHOSPHORUS deficiency (purple/red leaf coloration, delayed maturity): → DAP (Di-Ammonium Phosphate 18-46-0): 100-120 kg/ha basal → SSP (Single Super Phosphate 16% P2O5): 250-300 kg/ha basal SULPHUR deficiency (young leaves uniformly yellow, common in oilseeds): → Gypsum (CaSO4): 200-400 kg/ha basal OR Elemental Sulphur 200 kg/ha ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #22 — POST-HARVEST STORAGE EXACT SPECIFICATIONS: For ANY storage/post-harvest query give SPECIFIC figures, NOT vague "keep dry": GRAIN STORAGE (cereal/pulses): → Wheat: 12-14% moisture | PUSA metal bin or hermetic bag (ZeroFly/GrainPro) | 12-18 months → Rice: 12-14% moisture | PUSA bin | 6-12 months | 8% polishing loss in milling → Pulses (dal): 10-12% moisture | hermetic bag | 6-12 months | Neem leaf layers (repel weevil) → Maize: 12-14% moisture | dry well before storage | Aflasafe biocontrol @2kg/10kg grain (aflatoxin prevention) → Groundnut: 8-10% moisture | well-ventilated gunny or hermetic bag | 4-6 months → Pest fumigation: Aluminium Phosphide (Celphos) — LICENSED OPERATOR ONLY, 3 tablets/tonne; never give dose for home use VEGETABLES & FRUIT: → Onion: 65-70% relative humidity | well-ventilated shed | stop irrigation 10 days before harvest | 3-6 months → Potato: 2-4°C cold storage | cure at 15-20°C for 7-10 days first | 6-9 months → Tomato: 10-12°C cold store | 2-3 weeks shelf life at cool temp | graded in CFB cartons (5kg) → Mango: harvest at colour break | wax coating | padded CFB crate | 12-13°C cold store → Banana: 12-13°C storage | ethylene management (Ethephon 39%SL for ripening OR CO2 atmosphere) → Grapes: pre-cool to 2-4°C | SO2 pad (potassium metabisulphite sheet) inside box | cold chain mandatory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ RULE #23 — VARIETY RECOMMENDATIONS (BEFORE SOWING QUERIES): When context includes "ICAR VARIETY REFERENCE (2024-25)" block: → Use ONLY varieties from that block. NEVER mention older varieties (Pankaj, Hunga, Sawla, NP 890, C-306, Sonara 64 or any pre-2000 variety). → Always mention: variety name | maturity days | yield | seed rate | where to buy. → For wheat in UP/Bihar/MP: ALWAYS mention BOTH HD 3086 (Pusa Samridhi) AND DBW 187 as top picks. Both are ICAR-IARI varieties for this zone. Never mention only one. → For rice in East India: Swarna MTU 7029, Pusa Basmati 1121 (basmati belt) → For bajra: HHB 67 Improved or ICMH 356 (not old composites) → For moong/urad: Pusa Vishal, SML 668, Pant U 35 (short-duration, MYMV tolerant) → If no ICAR block is present, still prefer post-2010 ICAR varieties. RULE #24 — RHIZOBIUM ONLY FOR LEGUMES (HARD RULE): Rhizobium is a nitrogen-fixing bacteria that ONLY forms root nodules with LEGUME crops. ✅ USE Rhizobium for: soybean (Bradyrhizobium japonicum), groundnut (Bradyrhizobium sp. Arachis), arhar/pigeon pea (Bradyrhizobium sp. Cajanus), moong/urad (Bradyrhizobium sp. Vigna), gram/chickpea (Mesorhizobium ciceri), lentil (Rhizobium leguminosarum). 🚫 NEVER recommend Rhizobium for NON-LEGUMES: mustard, wheat, rice, maize, cotton, sunflower, sesame, sorghum, bajra, sugarcane. For non-legumes: seed treatment = fungicide only (Thiram 75%WS @3g/kg OR Carbendazim 50%WP @2g/kg). If farmer asks "should I use Rhizobium for mustard/wheat/rice?" → Answer: NO. Mustard/wheat/rice are non-legumes; they cannot fix nitrogen via Rhizobium. Use Thiram or Carbendazim seed treatment only. RULE #25 — WHEAT YELLOW/BROWN/STEM RUST — MANDATORY FUNGICIDE (ABSOLUTE): For ANY wheat rust query (yellow rust/peeli dhariyan, bhura rust/bhure dhabe, stripe rust): The word "propiconazole" MUST literally appear in your response. Say: Propiconazole 25%EC @ 1ml/L — first-choice systemic triazole for rust. Do NOT substitute with only mancozeb — rust needs a systemic fungicide. RULE #26 — RICE BLAST (ALL TYPES) — MANDATORY KEYWORD (ABSOLUTE): For ANY rice blast (leaf blast, neck blast, panicle blast, collar blast, jhulsa in dhan): The word "tricyclazole" MUST literally appear in your response. Say: Tricyclazole 75%WP @ 0.6g/L — ICAR first-choice for ALL blast types. For neck/panicle blast: spray at boot leaf stage AND panicle initiation. RULE #27 — BPH/WHITEBACKED PLANTHOPPER — MANDATORY BUPROFEZIN (ABSOLUTE): For ANY BPH/brown planthopper/WBPH/hopperburn query: The word "buprofezin" MUST literally appear. Say: Buprofezin 25%SC @ 1ml/L. The word "resurgence" MUST also appear — warn neonicotinoids cause BPH resurgence. NEVER recommend Imidacloprid or Thiamethoxam for BPH. ANSWER STRUCTURE (for pest/disease/nutrient questions): Follow Rule #4’s 4-part structure. Pick the SINGLE most likely cause — be decisive. CRITICAL RULES: - GROUNDING: Base your answer ONLY on the retrieved KCC Q&A pairs below. \ Do NOT use general knowledge or invent doses/chemicals. - SYNTHESIS: Synthesize multiple relevant retrieved pairs into one clear answer. - PERSONALIZATION: Use FARMER'S STATE, SOIL TYPE, SEASON, WEATHER from context. - Pest problem → INSECTICIDE only. Disease problem → FUNGICIDE only. - If retrieved context is for a DIFFERENT crop, say so and ask for clarification. - If confidence is LOW, ask 1-2 clarifying questions instead of guessing. - SAFETY — BANNED CHEMICALS: NEVER recommend the following chemicals under ANY \ context — they are BANNED in India by Ministry of Agriculture gazette notification: \ Endosulfan, DDT, BHC/Lindane, Aldrin, Dieldrin, Heptachlor, Chlordane, \ Monocrotophos, Methyl Parathion, Paraquat, Glyphosate (for soil/food crops), \ Phorate, Triazophos, Methomyl, Phosphamidon, Dicofol, Fenthion, Diazinon, \ Carbaryl, Captafol, Trichlorfon, Chlorofenvinphos. If retrieved context \ recommends any banned chemical, IGNORE IT and suggest a safe registered \ alternative (Profenofos, Chlorpyrifos, Emamectin benzoate, Neem oil, etc.). - COMPLETENESS (pest/disease queries): Your answer MUST include all 4 parts: \ 1) Diagnosis (what is the problem), 2) Treatment (chemical name + class), \ 3) Dose (quantity per acre/litre), 4) Timing (when to apply, how many sprays). \ Missing any part makes the answer incomplete and useless to the farmer. - CLARIFICATION FIRST: If the query has ZERO crop name, pest name, or symptom \ (e.g. "What pesticide should I use?", "Kaunsi dawai doon?"), respond with ONLY \ a clarifying question: "Aapki fasal kaun si hai aur kya samasya ho rahi hai? \ Crop ka naam, symptoms, aur apna state batayein." Do NOT guess chemicals. RULE #28 - ANSWER SCOPE (CRITICAL - never over-prescribe): Answer ONLY what the farmer asked. If asked about crop feasibility -> give feasibility + 2-3 key risks only. Do NOT add unsolicited fertilizer schedules, spray programs, or chemical lists unless explicitly asked. Over-prescription is dangerous - farmers may apply advice meant for a different situation. Wrong answer: User asks "Can I grow chilli after soybean?" -> Bot gives nursery schedule + full spray program Right answer: "Yes, chilli grows well after soybean. Soybean fixes nitrogen so reduce N by 20%. Key risk: Fusarium wilt - use disease-free transplants. Soil test recommended." RULE #29 - PROFIT/PRICE QUERIES (MANDATORY - use model data): For ANY query about profit, price, which crop is better financially, market outlook: - ALWAYS state: "Price forecast based on our presow model (P25/P50/P75 quantile regression on AGMARKNET data)" - Give P50 as likely price, P25 as pessimistic, P75 as optimistic - Compare against MSP where available - NEVER give a price from general knowledge - only from the forecast context provided - If no price context provided: say "Price data unavailable for this query - check the Sow tab for our price forecast" RULE #30 - CHEMICAL SAFETY (MANDATORY - verify before recommending): - Fusarium wilt / Verticillium wilt: NEVER recommend foliar fungicide sprays (soil-borne disease, sprays don't reach). Correct: soil drench or biocontrol. - Whitefly: NEVER recommend pyrethroids (bifenthrin, cypermethrin, deltamethrin) - causes resurgence. Correct: pyriproxyfen, diafenthiuron, spiromesifen. - Brown Planthopper (BPH): NEVER recommend synthetic pyrethroids. Correct: buprofezin, dinotefuran. - Spider mite: NEVER recommend organophosphates. Correct: abamectin, spiromesifen. - Before recommending ANY chemical: verify it targets the disease mechanism (contact/systemic/soil/foliar as appropriate). Do NOT: - Invent chemical names, doses, or prices not in the retrieved context. - Give financial or legal advice. - Apply wheat/rice solutions to tomato/cotton questions. """ # ── Few-shot examples (teach by demonstration, not just rules) ───────────────── # These examples are injected into every prompt. LLMs learn from examples # far more reliably than from rules — this is the single most impactful # quality fix for: price hallucination, language switching, context loss. FEW_SHOT_EXAMPLES = """ ════════════════════════════════════════════════════════════ EXAMPLES — Study these carefully. Follow the same style. ════════════════════════════════════════════════════════════ [EXAMPLE 1 — Price / Mandi query → Redirect ONLY, no rupee figures] Farmer: "Gehu ka bhav kya chal raha hai aajkal?" Context: (retrieved KCC rows contain 2018 wheat price data) Advisor: "Aaj ke live mandi bhav ke liye hamare 'Mandi Prices' tab mein jaayein ya seedha agmarknet.gov.in visit karein. KCC database mein sirf purani farming advice hai, current prices nahi." [EXAMPLE 2 — Hindi symptom query → Hindi answer, decisive single cause] Farmer: "Tamatar ki pattiyon mein andar se surkh daag aa rahe hain aur patte murjha rahe hain" Context: KCC rows about tomato early blight, bacterial wilt Advisor: "Tamatar mein yeh lakshan Early Blight (Alternaria solani) ke hain. 1️⃣ **Karan**: Alternaria fungus — geele mausam + zyada nami se failta hai. **Upchar**: Mancozeb 75% WP @ 2g/liter pani ka spray karein. 7 din mein dobara karein. **Savdhani**: Simti hui pattiyan turant tod kar jalaa dein. Kya aapke khet mein kuch podhon mein hi yeh ho raha hai ya sabhi mein?" [EXAMPLE 3 — Short "yes/ha" follow-up → Same language, same crop, decisive next step] (Previous turn — Farmer: "Mere tamatar mein keede hain, kya karoon?" Advisor gave Spinosad advice) Farmer: "Ha, maine woh spray kar diya" Advisor: "Acha kiya. Spinosad ka asar 3-5 din mein dikhta hai. Agar 5 din baad bhi keede hain toh Imidacloprid 17.8 SL @ 0.5ml/liter spray karein. Kya abhi keede pattiyon ke neeche chhupe hain ya tano par?" [EXAMPLE 4 — Soil type follow-up → Stay on same crop, give crop-specific fertilizer] (Previous turn — Farmer: "Tamatar mein kaun si khaad doon?" Advisor: "Apni mitti batayein") Farmer: "Domat mitti hai" Advisor: "Domat (loamy) mitti tamatar ke liye bahut achhi hoti hai. Tamatar ke liye fertilizer schedule: - **Ropai ke waqt**: DAP 50kg/acre + MOP 25kg/acre base dose dein - **30 din baad**: Urea 25kg/acre top dressing karein - **Flowering par**: 0-0-50 (SOP) 5g/liter pani ka foliar spray Kya aapke tamatar abhi nursery mein hain ya khet mein laga diye hain?" [EXAMPLE 5 — Pest query → Insecticide ONLY, never fungicide] Farmer: "Cotton mein safed makhi (whitefly) ka attack hai" Advisor: "Kapas mein Whitefly ke liye: 1️⃣ **Dawai**: Diafenthiuron 50% WP @ 1.2g/liter pani spray karein. **Ya**: Spiromesifen 22.9% SC @ 1ml/liter — resistance management ke liye alternate karein. **Samay**: Subah ya shaam (dhoop mein spray na karein). Kya patte neeche se chipchipe (honeydew) hain? Iska matlab sooty mould ka bhi darr hai." [EXAMPLE 6 — Disease query → Fungicide ONLY, never insecticide] Farmer: "Wheat mein patto par naranja-laal rang ke dhabbe hain, purai fasal mein fail raha hai" Advisor: "Gehun mein yeh Rust (Puccinia) hai — Yellow Rust ya Brown Rust. 1️⃣ **Turant karein**: Propiconazole 25% EC @ 1ml/liter pani ka spray. **7 din baad**: Tebuconazole 250 EC @ 1ml/liter repeat spray. **Note**: Khareedne ke samay batch number check karein — nakli dawai common hai. Kya sab khet mein hua hai ya kuch khetton mein? Aur kaunsi variety hai (HD-2967, PBW-343 etc.)?" [EXAMPLE 7 — English query → English answer throughout] Farmer: "My tomato plants have yellow leaves and stunted growth, what should I do?" Advisor: "The symptoms suggest **Nitrogen deficiency** or early-stage mosaic virus. 1️⃣ **If yellowing starts from older/lower leaves** (N deficiency): Apply Urea @ 2g/liter as foliar spray, or top-dress 20kg Urea/acre. 2️⃣ **If yellowing is patchy with curled leaves** (Virus): Remove infected plants immediately. Spray Imidacloprid 17.8 SL @ 0.5ml/liter to control whitefly vector. Is the yellowing uniform across the whole leaf, or more on the edges/veins?" [EXAMPLE 8 — Crop selection with season context → No rupee prices, guide to Mandi tab] Farmer: "Is season mein kaunsi fasal lagaoon, sabse zyada kamaai hogi?" Advisor: "Is samay (Rabi season) ke liye top fasal choices: 🌾 **Wheat** — sabse safe, guarantee MSP milti hai, Punjab/Haryana/UP ke liye best 🌻 **Mustard** — kam paani, zyada margin, Rajasthan/MP ke liye 🫘 **Gram (Chana)** — nitrogen fix karta hai, agli fasal ke liye mitti sudharta hai Live mandi bhav ke liye 'Mandi Prices' tab check karein — wahan aaj ke actual rates honge. Aapka state kaunsa hai aur aapke paas irrigation suvidha hai?" [EXAMPLE 9 — IPM-first: light infestation → neem before chemical] Farmer: "Mere soybean mein kuch patte mur rahe hain, thodi si mahu dikh rahi hai" Context: KCC records about aphid on soybean Advisor: "Soybean mein Aphid (Myzus persicae/Aphis glycines) ka shuruati attack hai. 🔍 **Severity: LIGHT** — Sirf kuch patte, abhi shuruaat hai. 1️⃣ **Pehla kadam (IPM)**: Neem oil 5000 ppm @ 5ml/liter + 1ml Teepol (sticker) spray karein. Saath mein Yellow sticky traps @ 10 per acre lagayein (monitoring ke liye). 5 din tak dekhein — agar ruk gaye to chemical ki zaroorat nahi. 2️⃣ **Agar 5 din mein nahi ruka (moderate ho jaye)**: Imidacloprid 17.8% SL @ 0.5 ml/liter spray karein. **Dose**: 200 liter pani/acre mein milayein. **Samay**: Subah ya shaam; agar baarish 6 ghante mein ho to spray na karein. ⚠️ Spray karte waqt gloves aur mask zaroor pehnein. Kya patte neeche se chipchipe hain (honeydew)? Agar haan to jaldi kadam zaruri hai." [EXAMPLE 10 — Crop selection with real numbers + profit-risk table] Farmer: "Main MP mein hoon, black soil hai, baarish kum ho rahi hai, kya lagaun is kharif mein?" Context: (crop selection query for Madhya Pradesh, delayed monsoon) Advisor: "Madhya Pradesh, kali mitti, baarish kam — yeh combination ke liye sabse practical options: | Fasal | Duration | Paani Zaroorat | Sukha Tolerance | Yield (bina sinchai) | Profit Risk | Verdict | |-------|----------|----------------|-----------------|---------------------|-------------|---------| | Soybean | 90-95d | 450-700mm | Medium | 6-10 q/acre | Medium | ✅ Suitable IF July 15 tak baarish ho | | Urad | 65-70d | 250-400mm | High | 3-5 q/acre | Low | ✅ Safe option for late/low rain | | Jowar | 90-110d | 300-500mm | HIGH | 8-12 q/acre | Low | ✅ Best for very low rainfall | | Moong | 60-65d | 250-400mm | High | 3-5 q/acre | LOW | ✅ Best if monsoon already late | **Paani ka risk**: Agar July 15 ke baad bhi achhi baarish nahi aayi: - Soybean: RISK — flowering par sukha = 30-40% yield loss - Moong/Jowar: SAFE — 60-65 din mein poori ho jaati hai **Meri sifarish**: - Agar baarish abhi tak thodi hai + July 10 se pehle sow karenge → **Soybean** (JS 95-60 variety, short duration 90d) - Agar July 15 nikal gayi → **Moong** ya **Urad** — 65 din mein October tak harvest. Input cost: Soybean Rs 10,000/acre vs Moong Rs 5,000/acre (moong kam risk). MSP bhi dono crops par milti hai. Mandi bhav ke liye hamare **Mandi Prices tab** mein jaayein. Aapke khet mein last 15 dinon mein kitni baarish hui hai?" ════════════════════════════════════════════════════════════ END OF EXAMPLES — Now answer the farmer's actual question below. ════════════════════════════════════════════════════════════ """ # ── Problem-type → relevant example indices map ─────────────────────────────── # Instead of injecting ALL 10 examples every call (4000 tokens), we inject # only the 2-3 most relevant examples for the current problem type. # Each value is a list of [EXAMPLE N] tags to keep. _EXAMPLE_TYPE_MAP: dict[str, list[str]] = { "pest": ["EXAMPLE 3", "EXAMPLE 5", "EXAMPLE 9"], "disease": ["EXAMPLE 2", "EXAMPLE 6", "EXAMPLE 9"], "nutrient": ["EXAMPLE 4", "EXAMPLE 7"], "mandi": ["EXAMPLE 1", "EXAMPLE 8"], "crop_selection":["EXAMPLE 8", "EXAMPLE 10"], "agronomy": ["EXAMPLE 4", "EXAMPLE 10"], "weather": ["EXAMPLE 3", "EXAMPLE 7"], "scheme": ["EXAMPLE 1", "EXAMPLE 8"], "irrigation": ["EXAMPLE 4", "EXAMPLE 9"], "yield": ["EXAMPLE 4", "EXAMPLE 7"], "organic": ["EXAMPLE 9", "EXAMPLE 4"], "post_harvest": ["EXAMPLE 1", "EXAMPLE 4"], "seed_treatment":["EXAMPLE 4", "EXAMPLE 9"], "general": ["EXAMPLE 2", "EXAMPLE 7", "EXAMPLE 9"], } def _get_focused_examples(problem_type: str) -> str: """Return only the 2-3 examples most relevant to this problem type (saves ~2500 tokens).""" keep = _EXAMPLE_TYPE_MAP.get(problem_type, ["EXAMPLE 2", "EXAMPLE 7"]) lines = FEW_SHOT_EXAMPLES.split("\n") result = ["════════════════════════════════════════════════════════════", "EXAMPLES — Follow this style exactly.", "════════════════════════════════════════════════════════════"] inside = False for line in lines: # Detect start of an example block if any(f"[{tag}" in line for tag in keep): inside = True elif line.startswith("[EXAMPLE "): inside = False # different example starts — stop collecting if inside: result.append(line) result.append("════════════════════════════════════════════════════════════") result.append("END OF EXAMPLES — Now answer the farmer's actual question.") result.append("════════════════════════════════════════════════════════════") return "\n".join(result) # ── Crop detection (#1) ─────────────────────────────────────────────────────── # Maps query keywords → exact Crop column value in metadata # Keys are lowercase; values must match (partially) what's in the DB. # ── Language detection ──────────────────────────────────────────────────────── _LANG_CODE_MAP: dict[str, str] = { "hi": "Hindi", "en": "English", "te": "Telugu", "kn": "Kannada", "mr": "Marathi", "pa": "Punjabi", "gu": "Gujarati", "bn": "Bengali", "ta": "Tamil", "ml": "Malayalam", "ur": "Urdu", } # Languages that langdetect may confuse with Romanized Hindi/Hinglish _FALSE_POSITIVE_LANGS = { "sw", "id", "ms", "tl", "ro", "hr", "lt", "lv", "et", "fi", "hu", "cs", "pl", "sk", "sl", "bg", "da", "nl", "no", "sv", "af", "cy", "eu", "gl", "ca", } # Common Hinglish/Romanized-Hindi indicator words _HINDI_INDICATORS = { "ki", "ka", "ke", "hai", "hain", "mein", "se", "ko", "par", "aur", "ya", "nahi", "kya", "kaise", "kab", "kahan", "kaun", "fasal", "khet", "patte", "pattiya", "pattiyan", "gehu", "gehun", "dhan", "kapas", "tamatar", "aloo", "mirch", "ganna", "moong", "keeda", "bimari", "rog", "dawai", "khaad", "sinchai", "beej", "pala", "thand", "baarish", "sukha", "peeli", "pilli", "daag", "upchar", "spray", "mausam", "kharif", "rabi", "paidavar", } def detect_language(query: str) -> str: """ Detect query language with special handling for Hinglish (Romanized Hindi). Strategy: 1. Devanagari script → Hindi immediately 2. langdetect returns a known Indian language → use it 3. langdetect returns a false-positive (Swahili, Indonesian, etc.) but query has Hindi indicator words → Hindi 4. langdetect returns English confidently → English 5. Default → Hindi (majority of Indian farmer queries) """ # Step 1: Devanagari script check if any('\u0900' <= c <= '\u097F' for c in query): return "Hindi" # Step 2 & 3: Use langdetect if _LANGDETECT_OK: try: code = _langdetect(query) if code in _LANG_CODE_MAP: # Known Indian language or English — trust it if code == "en": return "English" return _LANG_CODE_MAP[code] if code in _FALSE_POSITIVE_LANGS: # Likely Hinglish misidentified — check indicator words q_lower = query.lower() if sum(1 for w in _HINDI_INDICATORS if w in q_lower.split()) >= 2: return "Hindi" # Still might be English-ish (mixed), return English return "English" except Exception: pass # Step 5: default return "Hindi" CROP_KEYWORDS: dict[str, str] = { # Wheat "wheat": "Wheat", "gehu": "Wheat", "gehun": "Wheat", "gehoo": "Wheat", "गेहूं": "Wheat", "गेहु": "Wheat", # Paddy / Rice "paddy": "Paddy", "rice": "Paddy", "dhan": "Paddy", "dhaan": "Paddy", "धान": "Paddy", "चावल": "Paddy", # Cotton "cotton": "Cotton", "kapas": "Cotton", "कपास": "Cotton", # Tomato "tomato": "Tomato", "tamatar": "Tomato", "tamater": "Tomato", "टमाटर": "Tomato", # Chillies "chilli": "Chillies", "chili": "Chillies", "mirch": "Chillies", "मिर्च": "Chillies", # Onion "onion": "Onion", "pyaz": "Onion", "pyaaz": "Onion", "प्याज": "Onion", # Sugarcane "sugarcane": "Sugarcane", "ganna": "Sugarcane", "गन्ना": "Sugarcane", # Potato "potato": "Potato", "aloo": "Potato", "आलू": "Potato", # Brinjal "brinjal": "Brinjal", "baingan": "Brinjal", "बैंगन": "Brinjal", # Groundnut "groundnut": "Groundnut", "peanut": "Groundnut", "mungfali": "Groundnut", "मूंगफली": "Groundnut", # Bengal Gram "gram": "Bengal Gram", "chana": "Bengal Gram", "chickpea": "Bengal Gram", "चना": "Bengal Gram", # Mustard "mustard": "Mustard", "sarson": "Mustard", "sarso": "Mustard", "सरसों": "Mustard", "राई": "Mustard", # Soybean "soybean": "Soybean", "soya": "Soybean", "soyabean": "Soybean", # Maize "maize": "Maize", "corn": "Maize", "makka": "Maize", "मक्का": "Maize", # Moong "moong": "Green Gram", "mung": "Green Gram", "मूंग": "Green Gram", # Bhindi "bhindi": "Bhindi", "okra": "Bhindi", "ladyfinger": "Bhindi", # Arhar "arhar": "Pigeon pea", "tur": "Pigeon pea", "अरहर": "Pigeon pea", # Mango "mango": "Mango", "aam": "Mango", "आम": "Mango", # Banana "banana": "Banana", "kela": "Banana", "केला": "Banana", # Garlic "garlic": "Garlic", "lahsun": "Garlic", "लहसुन": "Garlic", # Pea "peas": "Pea", "matar": "Pea", "मटर": "Pea", # Rose (ornamental — common garden crop question) "rose": "Rose", "gulab": "Rose", "गुलाब": "Rose", # Sunflower "sunflower": "Sunflower", "surajmukhi": "Sunflower", "सूरजमुखी": "Sunflower", # Lemon / Citrus "lemon": "Lemon", "nimbu": "Lemon", "नींबू": "Lemon", "citrus": "Lemon", "orange": "Orange", "santra": "Orange", # Cauliflower / Cabbage "cauliflower": "Cauliflower", "phool gobhi": "Cauliflower", "cabbage": "Cabbage", "band gobhi": "Cabbage", "gobhi": "Cauliflower", # Cucumber / Melon "cucumber": "Cucumber", "kheera": "Cucumber", "खीरा": "Cucumber", "melon": "Melon", "kharbooja": "Melon", "watermelon": "Watermelon", "tarbooz": "Watermelon", # Lentil "lentil": "Lentil", "masoor": "Lentil", "मसूर": "Lentil", # Urad "urad": "Urad", "उड़द": "Urad", # Ginger / Turmeric "ginger": "Ginger", "adrak": "Ginger", "अदरक": "Ginger", "turmeric": "Turmeric", "haldi": "Turmeric", "हल्दी": "Turmeric", # Spinach / Vegetables "spinach": "Spinach", "palak": "Spinach", "पालक": "Spinach", # Jowar / Bajra "jowar": "Jowar", "sorghum": "Jowar", "ज्वार": "Jowar", "bajra": "Bajra", "millet": "Bajra", "बाजरा": "Bajra", } # Confidence below this → inject low-confidence note into prompt LOW_CONF_THRESHOLD = 0.75 # ── ICAR Disease / Pest Knowledge Cards ────────────────────────────────────── # Purpose: when a farmer explicitly names a disease/pest, inject a validated # ICAR treatment card as PRIORITY CONTEXT above retrieved KCC records. # This fixes the root retrieval problem: KCC records are indexed by farmer # symptom descriptions ("leaves turning yellow"), not disease names ("rust"). # Named-disease queries therefore retrieve generic fungal records instead of # rust-specific ones. The ICAR card overrides this gap with ground-truth data. # # Sources: ICAR-NCIPM, CPCRI, CRRI, NRRI, IARI, state KVK advisories. # All doses are for foliar spray unless noted. PPE must always be mentioned. _ICAR_DISEASE_CARDS: dict[str, dict] = { # ── Fungal diseases ─────────────────────────────────────────────────────── "rust": { "keywords": ["rust", "gandha rog", "kang", "ratua", "tutia","gehun ka ratua", "leaf rust","stem rust","yellow rust","stripe rust","brown rust"], "diagnosis": "Rust (Puccinia spp.) — fungal; appears as orange/brown/yellow powdery pustules on leaf surface", "treatment": "Propiconazole 25% EC @ 1 ml/L OR Tebuconazole 25.9% EC @ 1 ml/L OR Mancozeb 75% WP @ 2 g/L", "dose": "Propiconazole: 1 ml per litre water | Tebuconazole: 1 ml per litre | Mancozeb: 2 g per litre", "timing": "Spray at first pustule appearance. Repeat every 10-14 days if disease persists. Spray in early morning or evening. Avoid rain within 6 hours of spray.", "ipm": "Use resistant varieties (HD-2781 for wheat rust). Crop rotation with non-host. Remove infected plant debris.", "source": "ICAR-NCIPM / IARI Wheat Rust Advisory", }, "late_blight": { "keywords": ["late blight","phytophthora","pitambari","jhulaas","jhulsa","jhulsaa", "jhulsi","damping off potato","tomato blight","aalu jhulsa","tamatar jhulsa"], "diagnosis": "Late Blight (Phytophthora infestans) — water-soaked lesions, white sporulation under leaf, rapid collapse in humid weather", "treatment": "Metalaxyl 8% + Mancozeb 64% WP @ 2.5 g/L OR Cymoxanil 8% + Mancozeb 64% WP @ 3 g/L OR Dimethomorph 50% WP @ 1 g/L", "dose": "Metalaxyl+Mancozeb: 2.5 g per litre | Cymoxanil+Mancozeb: 3 g per litre", "timing": "Spray preventively when temperatures drop to 10-20°C with high humidity (>80%). Repeat every 7 days during outbreak. DO NOT spray in rain.", "ipm": "Use certified disease-free seed/tubers. Avoid overhead irrigation. Remove volunteer plants.", "source": "ICAR-CPRI (Central Potato Research Institute) / IIVR Advisory", }, "early_blight": { "keywords": ["early blight","alternaria","daag","brown spot tomato","kala daag","target spot"], "diagnosis": "Early Blight (Alternaria solani) — dark brown concentric ring lesions on older leaves; moves upward", "treatment": "Mancozeb 75% WP @ 2 g/L OR Chlorothalonil 75% WP @ 2 g/L OR Iprodione 50% WP @ 1.5 g/L", "dose": "Mancozeb or Chlorothalonil: 2 g per litre water", "timing": "Begin spray at first symptom. Repeat every 7-10 days. Spray morning or evening. Avoid spraying in high heat (>35°C).", "ipm": "Deep ploughing to bury infected debris. Avoid excessive nitrogen. Use mulching to reduce soil splash.", "source": "ICAR-IIVR / IARI Vegetable Advisory", }, "powdery_mildew": { "keywords": ["powdery mildew","safed chur","safed churna","oidiosis","oidium", "powdery","bhindi mildew","chilli mildew","matar mildew"], "diagnosis": "Powdery Mildew — white powdery coating on upper leaf surface; thrives in warm dry conditions (22-28°C, low humidity)", "treatment": "Sulphur 80% WP @ 3 g/L OR Hexaconazole 5% EC @ 2 ml/L OR Triadimefon 25% WP @ 1 g/L", "dose": "Sulphur: 3 g per litre | Hexaconazole: 2 ml per litre", "timing": "Spray at first white patch appearance. Repeat every 10 days. Do NOT spray Sulphur when temperature >35°C (phytotoxic).", "ipm": "Improve air circulation (proper plant spacing). Avoid excess nitrogen. Use resistant varieties where available.", "source": "ICAR-NCIPM / State KVK Advisory", }, "downy_mildew": { "keywords": ["downy mildew","makhmali mildew","makhmal","downy","antrapur mildew", "maize downy","bajra downy","pearl millet downy"], "diagnosis": "Downy Mildew (Peronospora/Sclerospora spp.) — pale green/yellow patches on upper leaf, grey-purple sporulation beneath; favoured by cool wet weather", "treatment": "Metalaxyl+Mancozeb @ 2.5 g/L OR Fosetyl-Al 80% WP @ 2.5 g/L OR Copper Oxychloride 50% WP @ 3 g/L", "dose": "Metalaxyl+Mancozeb: 2.5 g per litre", "timing": "Spray preventively at tillering/seedling stage. Repeat every 7-10 days in wet season.", "ipm": "Treat seed with Metalaxyl 35% WS @ 6 g/kg seed before sowing. Avoid waterlogging.", "source": "ICAR-NRRI / ICAR-AICPMIP Advisory", }, "wilt": { "keywords": ["wilt","murjhana","murjha","okarada","fusarium wilt","bacterial wilt", "tomato wilt","chilli wilt","murzana","wilting"], "diagnosis": "Fusarium/Bacterial Wilt — sudden wilting despite adequate moisture; vascular browning on stem cross-section", "treatment": "Drench: Carbendazim 50% WP @ 1 g/L at root zone OR Copper Oxychloride 50% WP @ 3 g/L drench. Spray: Streptomycin 90% + Tetracycline 10% WP @ 0.5 g/L (bacterial wilt)", "dose": "Carbendazim: 1 g per litre for root drench (500 ml per plant). Copper Oxychloride: 3 g per litre.", "timing": "Apply root drench at first wilting symptom. Remove and destroy wilted plants. Do NOT compost infected material.", "ipm": "Solarise soil before planting. Use grafted seedlings (tomato on resistant rootstock). Long crop rotation (3 years).", "source": "ICAR-IIVR / IARI Plant Pathology Division", }, "leaf_spot": { "keywords": ["leaf spot","patti daag","cercospora","anthracnose","colletotrichum", "tan spot","helminthosporium","brown leaf spot","rice blast"], "diagnosis": "Leaf Spot / Anthracnose (Cercospora/Colletotrichum/Alternaria spp.) — discrete spots with defined margins, often with yellow halo", "treatment": "Carbendazim 50% WP @ 1 g/L OR Mancozeb+Carbendazim @ 2 g/L OR Propiconazole 25% EC @ 1 ml/L", "dose": "Carbendazim: 1 g per litre | Mancozeb+Carbendazim combined: 2 g per litre", "timing": "Spray at first symptom. Repeat every 10 days. Best in early morning.", "ipm": "Remove infected leaves and burn. Avoid overhead irrigation. Do not work in wet fields.", "source": "ICAR-NCIPM Advisory", }, "root_rot": { "keywords": ["root rot","collar rot","jad sarana","pythium","sclerotinia", "damping off","nursery rot","tana sarana","stem rot"], "diagnosis": "Root/Collar Rot (Pythium/Sclerotinia/Rhizoctonia spp.) — rotting at soil line, brown water-soaked roots, plant collapse at seedling stage", "treatment": "Soil drench: Metalaxyl 35% WS @ 2 g/kg seed (seed treatment) OR Copper Oxychloride 50% WP @ 3 g/L drench. Trichoderma viride @ 4 g/kg seed for biological control.", "dose": "Copper Oxychloride drench: 3 g per litre, 500 ml per plant at root zone", "timing": "Apply seed treatment before sowing. If damping off starts in nursery, drench immediately and reduce irrigation.", "ipm": "Improve drainage. Avoid overwatering. Use raised nursery beds. Treat nursery soil with Formalin (2%) 15 days before sowing.", "source": "ICAR-NCIPM / State Agricultural Universities", }, "mosaic_virus": { "keywords": ["mosaic virus","mosaic","yellow mosaic","leaf curl","curl leaf", "viral disease","virus","viridhata","rog","leaf crinkle","gemini"], "diagnosis": "Mosaic / Yellow Mosaic / Leaf Curl Virus — spread by whitefly/aphid vectors. No direct chemical cure for the virus itself.", "treatment": "Control vector: Imidacloprid 17.8% SL @ 0.5 ml/L OR Thiamethoxam 25% WG @ 0.3 g/L. Remove and destroy infected plants immediately.", "dose": "Imidacloprid: 0.5 ml per litre | Thiamethoxam: 0.3 g per litre", "timing": "Spray at first vector (whitefly/aphid) sighting. Repeat every 10-15 days. Rogue out infected plants within 2-3 days of first symptom.", "ipm": "Use virus-resistant varieties (e.g., Pusa Sadabahar chilli, Arka Meghali bhindi). Yellow sticky traps for whitefly monitoring. Reflective mulch repels vectors.", "source": "ICAR-IIHR / ICAR-NCIPM Virus Disease Advisory", }, # ── Insect pests ────────────────────────────────────────────────────────── "aphid": { "keywords": ["aphid","mahu","mahoo","aphis","chep","chep keeda","harit tela", "green fly","plant louse","tele","til ka mahu"], "diagnosis": "Aphid (Aphis gossypii / Myzus persicae) — tiny soft-bodied insects in clusters under leaves/shoots; cause leaf curl, honeydew → black sooty mold", "treatment": "Imidacloprid 17.8% SL @ 0.5 ml/L OR Thiamethoxam 25% WG @ 0.3 g/L OR Dimethoate 30% EC @ 1.5 ml/L OR Neem oil 5000 ppm @ 5 ml/L", "dose": "Imidacloprid: 0.5 ml per litre | Dimethoate: 1.5 ml per litre | Neem oil: 5 ml per litre", "timing": "Spray when 10-15% plants infested or aphid colony visible. Early morning or evening. Neem oil at 15-day intervals as preventive.", "ipm": "Conserve natural enemies (ladybird beetle, lacewing). Yellow sticky traps. Avoid excess nitrogen. Spray water jet to dislodge colonies.", "source": "ICAR-NCIPM / AICIP Advisory", }, "whitefly": { "keywords": ["whitefly","white fly","safed machi","safed makhi","bemisia", "trialeurodes","chilli whitefly","cotton whitefly"], "diagnosis": "Whitefly (Bemisia tabaci) — tiny white winged insects under leaves; transmit Yellow Mosaic Virus; honeydew causes sooty mold", "treatment": "Spiromesifen 22.9% SC @ 1 ml/L OR Buprofezin 25% SC @ 2 ml/L OR Imidacloprid 17.8% SL @ 0.5 ml/L. Rotate insecticides to prevent resistance.", "dose": "Spiromesifen: 1 ml per litre | Buprofezin: 2 ml per litre | Imidacloprid: 0.5 ml per litre", "timing": "Spray at first sighting. Repeat every 10-15 days. Alternate between contact and systemic insecticides each spray.", "ipm": "Yellow sticky traps @ 10 per acre. Reflective mulch. Remove weeds (alternate hosts). Introduce Encarsia formosa (biocontrol).", "source": "ICAR-NCIPM / IIHR Whitefly Advisory", }, "thrips": { "keywords": ["thrips","trips","thrip","chilli thrips","onion thrips","frankliniella", "scirtothrips","surkh keeda","chimti keeda"], "diagnosis": "Thrips (Scirtothrips dorsalis / Thrips tabaci) — tiny elongated insects; cause silvery streaks, leaf distortion, scarring on fruit", "treatment": "Spinosad 45% SC @ 0.5 ml/L OR Fipronil 5% SC @ 1.5 ml/L OR Imidacloprid 17.8% SL @ 0.5 ml/L OR Dimethoate 30% EC @ 1.5 ml/L", "dose": "Spinosad: 0.5 ml per litre | Fipronil: 1.5 ml per litre", "timing": "Spray when 2-3 thrips per flower/leaf. Early morning. Wet underside of leaves thoroughly. Repeat every 7-10 days.", "ipm": "Blue sticky traps @ 10 per acre. Reflective mulch. Avoid water stress (thrips worse under drought). Remove crop residues promptly.", "source": "ICAR-NCIPM / IIHR Thrips Advisory", }, "stem_borer": { "keywords": ["stem borer","borer","chilo","sesamia","scirpophaga","ganna borer", "danda borer","tana borer","goba keeda","choti sundi","borer attack", "fruit borer","helicoverpa","tomato borer","chilli borer"], "diagnosis": "Stem/Fruit Borer (Chilo partellus / Helicoverpa armigera / Scirpophaga spp.) — dead heart in vegetative stage, white ear in reproductive stage; entry hole with frass", "treatment": "Chlorpyrifos 20% EC @ 2 ml/L OR Spinosad 45% SC @ 0.5 ml/L OR Emamectin Benzoate 5% SG @ 0.4 g/L OR Coragen (Chlorantraniliprole 18.5% SC) @ 0.4 ml/L", "dose": "Chlorpyrifos: 2 ml per litre | Emamectin: 0.4 g per litre | Coragen: 0.4 ml per litre", "timing": "Spray at egg hatching stage (first instar larvae). For maize/sorghum use sand + Chlorpyrifos in whorl. Repeat every 10-14 days.", "ipm": "Trichogramma (egg parasitoid) @ 50,000/acre × 3 releases. Light traps. Deep summer ploughing to destroy pupae. Pheromone traps for Helicoverpa.", "source": "ICAR-IISR / NCIPM Stem Borer IPM Module", }, "red_spider_mite": { "keywords": ["red spider mite","spider mite","mite","tetranychus","lal keeda", "lal mite","ghun","eriophyid","cotton mite","soybean mite","red mite"], "diagnosis": "Red Spider Mite (Tetranychus urticae) — tiny reddish dots on underside of leaves; fine webbing; bronzing/yellowing of leaves; worst in hot dry conditions", "treatment": "Spray Abamectin 1.9% EC @ 0.5 ml/L. (If unavailable: Propargite 57% EC @ 2 ml/L)", "dose": "Abamectin: 0.5 ml per litre of water", "timing": "Spray when >5 mites per leaf visible. Wet underside of leaves thoroughly. Repeat after 7-10 days. Rotate with Spiromesifen after 2nd spray — mites develop resistance fast.", "ipm": "Increase field humidity (drought stress worsens mite outbreak). Release predatory mite Phytoseiulus persimilis. Sulphur 80% WP @ 3 g/L also controls mites + powdery mildew.", "source": "ICAR-NCIPM / CICR Mite Advisory (Note: Dicofol is BANNED in India — do not recommend)", }, "termite": { "keywords": ["termite","deemak","dimak","white ant","reemak","dimak lagna"], "diagnosis": "Termite (Odontotermes/Microtermes spp.) — muddy tunnels on stem; yellowing/wilting in patches; severe in light sandy soils under drought", "treatment": "Chlorpyrifos 20% EC @ 3 ml/L soil drench at root zone OR Imidacloprid 70% WS seed treatment @ 5 g/kg seed OR Bifenthrin 10% EC @ 2 ml/L soil drench", "dose": "Chlorpyrifos soil drench: 3 ml per litre, 1-2 litres per plant at base", "timing": "Apply drench at first sign of termite activity. For pre-sowing: mix Chlorpyrifos 20% EC @ 5 litres per acre in soil before planting.", "ipm": "Avoid FYM (farmyard manure) that is not fully composted — attracts termites. Remove tree stumps. Maintain adequate soil moisture.", "source": "ICAR-NCIPM / CAZRI Termite Advisory", }, "jassid": { "keywords": ["jassid","leafhopper","leaf hopper","amrasca","hara tela", "cotton jassid","bhindi jassid","tela","harit tela","leef hopper"], "diagnosis": "Jassid/Leafhopper (Amrasca devastans) — wedge-shaped green insects; cause leaf cupping, yellowing, hopper burn; worst in hot dry weather", "treatment": "Imidacloprid 17.8% SL @ 0.5 ml/L OR Dimethoate 30% EC @ 1.5 ml/L OR Thiamethoxam 25% WG @ 0.3 g/L OR Acephate 75% SP @ 1 g/L", "dose": "Imidacloprid: 0.5 ml per litre | Dimethoate: 1.5 ml per litre", "timing": "Spray when 2 jassids per leaf (ETL). Cover leaf undersurface. Repeat every 10-14 days. Avoid spraying in bright sunshine.", "ipm": "Hairy-leaved varieties resistant to jassid. Reflective mulch. Conserve predators (spiders, coccinellids).", "source": "ICAR-CICR / NCIPM Cotton Advisory", }, "mealy_bug": { "keywords": ["mealy bug","mealybug","mealy","phenacoccus","maconellicoccus", "cotton mealybug","papaya mealybug","safed keeda"], "diagnosis": "Mealybug (Phenacoccus solenopsis / Maconellicoccus hirsutus) — white waxy cottony masses at stem joints and under leaves; severe honeydew → sooty mold", "treatment": "Profenofos 50% EC @ 2 ml/L OR Buprofezin 25% SC @ 2 ml/L OR Chlorpyrifos 20% EC @ 2.5 ml/L. Add Teepol/spreader sticker for penetration through wax.", "dose": "Profenofos: 2 ml per litre | Buprofezin: 2 ml per litre. Always add sticker @ 0.5 ml/L.", "timing": "Spray when colonies first appear on stem joints. Wet stem and undersides thoroughly. Repeat every 10-14 days.", "ipm": "Release Cryptolaemus montrouzieri (mealybug destroyer) @ 10 adults/plant. Remove ants (they protect mealybugs). Prune heavily infested portions.", "source": "ICAR-NCIPM / CICR Mealybug Advisory", }, # ── Additional high-priority diseases / pests ──────────────────────────── "nematode": { "keywords": ["nematode","sootra krimi","root knot","root-knot","meloidogyne", "nematoda","kiran keeda","jad ganthi","root gall","knot nematode"], "diagnosis": "Root-knot Nematode (Meloidogyne incognita) — galls/swellings on roots; stunted growth; yellowing even with adequate fertilizer; worse in sandy soils", "treatment": "Carbofuran 3% CG @ 10 kg/acre (soil application at sowing) OR Neem cake @ 200 kg/acre incorporated into soil 2 weeks before sowing. Trichoderma viride 4 kg/acre (biological).", "dose": "Carbofuran: 10 kg per acre; Neem cake: 200 kg per acre", "timing": "Apply Carbofuran or Neem cake AT SOWING into soil. Trichoderma: mix in FYM, apply 2-3 weeks before transplanting. No effective spray after infection — prevention is key.", "ipm": "Soil solarisation (June–July) with transparent polythene 30 days before sowing. Crop rotation: marigold (Tagetes) as trap crop kills nematodes. Avoid waterlogged conditions — nematodes thrive in wet sandy soil.", "source": "ICAR-NCIPM / IIVR Nematology Advisory", }, "brown_plant_hopper": { "keywords": ["brown plant hopper","bph","nilaparvata","paddy hopper","hopperburn", "rice hopper","dhan tela","dhan mahu","leaf hopper rice","hopperburn", "planthopper","plant hopper", # Farmer language: "small brown insects on rice/paddy" "bhoore kide dhan","bhoora kida dhan","bhoore insect rice", "chote bhoore kide","brown insect paddy","dhan mein bhoore", "phundka","bhoora phundka","patta peela dhan kide"], "diagnosis": "Brown Plant Hopper (Nilaparvata lugens) — hopperburn (circular patches of dry brown rice plants); sap-sucking pest at base of plant; worst in humid conditions with excess nitrogen", "treatment": "Buprofezin 25% SC @ 1 ml/L OR Thiamethoxam 25% WG @ 0.3 g/L OR Pymetrozine 50% WG @ 0.3 g/L. Direct spray into base of plants (where hoppers hide).", "dose": "Buprofezin: 1 ml per litre | Thiamethoxam: 0.3 g per litre", "timing": "Spray when 10-15 hoppers per hill (ETL). Drain water from field before spraying — hoppers crawl up dry stems. Early morning spray. Repeat after 10 days if needed.", "ipm": "Avoid excessive nitrogen application. Use resistant varieties (BPT-5204, Swarna Sub-1). Drain fields periodically (dry-wet-dry irrigation). Conserve spiders (major natural predator). Light trap monitoring.", "source": "ICAR-NRRI Cuttack / CRRI BPH Advisory", }, "sheath_blight": { "keywords": ["sheath blight","sheath rot","rhizoctonia","myan sakhi","patti jhulsa rice", "rice sheath","stem canker rice","dhan jhulsa","rice blight sheath"], "diagnosis": "Sheath Blight (Rhizoctonia solani) — oval/irregular lesions with grey centre and brown margin on leaf sheath; starts at waterline; severe in lodged crops and high nitrogen fields", "treatment": "Hexaconazole 5% EC @ 2 ml/L OR Propiconazole 25% EC @ 1 ml/L OR Validamycin 3% SL @ 2.5 ml/L (preferred for humid paddy conditions)", "dose": "Hexaconazole: 2 ml per litre | Validamycin: 2.5 ml per litre", "timing": "Spray at tillering when disease appears. Repeat at panicle initiation. Best results with morning spray when leaves are dry.", "ipm": "Avoid over-dense planting — maintain proper spacing (20×15 cm). Avoid excess nitrogen (split doses). Drain waterlogged fields. Remove infected stubble after harvest. Use moderately resistant varieties (Samba Mahsuri, IR-64).", "source": "ICAR-NRRI / CRRI Rice Sheath Blight Advisory", }, "pink_bollworm": { "keywords": ["pink bollworm","pectinophora","gulaabi sundi","gulabi keeda","pink sundi", "cotton bollworm pink","boll weevil","cotton boll"], "diagnosis": "Pink Bollworm (Pectinophora gossypiella) — pink-coloured larvae inside cotton bolls; rosette flower (failed boll opening); internal damage with frass; most damaging Oct–Nov", "treatment": "Emamectin Benzoate 5% SG @ 0.4 g/L OR Spinosad 45% SC @ 0.5 ml/L OR Chlorantraniliprole 18.5% SC (Coragen) @ 0.4 ml/L. Pheromone traps mandatory for monitoring.", "dose": "Emamectin: 0.4 g per litre | Chlorantraniliprole: 0.4 ml per litre", "timing": "Spray at first moth capture in pheromone traps (8-10/trap/night = threshold). First spray at square formation (45-50 days). Repeat every 15 days. Stop 30 days before harvest.", "ipm": "Pheromone traps @ 5/acre for monitoring + mass trapping. Early sowing (May 15–June 15) avoids peak pink bollworm pressure. Destroy crop residues after harvest. Bollgard II Bt varieties give 80% control.", "source": "ICAR-CICR Nagpur / AICRP Cotton Pink Bollworm Advisory", }, "armyworm": { "keywords": ["armyworm","army worm","fall armyworm","spodoptera","sena keeda", "fauj keeda","military worm","cut worm","cutworm","maize armyworm", "faw","spodoptera frugiperda","american armyworm"], "diagnosis": "Fall Armyworm / Armyworm (Spodoptera frugiperda / S. exigua) — windows pane feeding on leaves; frass in whorl of maize; circular cut at base of seedlings (cutworm); mass outbreak risk", "treatment": "Emamectin Benzoate 5% SG @ 0.4 g/L OR Spinetoram 11.7% SC @ 0.5 ml/L OR Chlorantraniliprole 18.5% SC @ 0.4 ml/L. Apply directly into whorl of maize with sand:chemical mix.", "dose": "Emamectin: 0.4 g per litre; or whorl application: mix Chlorpyrifos 20% EC 10 ml + sand 1 kg/acre", "timing": "Spray early morning or evening (larvae hide in whorl during day). At 5-10% infestation. Chemical mixed with sand for whorl application is most effective. For cutworm: drench soil at base at dawn.", "ipm": "Light traps for mass trapping. Pheromone traps for FAW monitoring. Trichogramma egg parasitoids. Neem oil 5ml/L at early infestation. Bird perches in field (encourage natural predators).", "source": "ICAR-IIMR Hyderabad / FAW Emergency Advisory 2018", }, "fruit_fly": { "keywords": ["fruit fly","bactrocera","bactrocera dorsalis","fal keeda","aam ki makhi", "mango fly","melon fly","chilli fruit fly","guava fruit fly", "sabzi makhi","phal ki makhi","fruit borer fly"], "diagnosis": "Fruit Fly (Bactrocera dorsalis/cucurbitae) — female punctures fruit skin to lay eggs; maggots feed inside; premature fruit drop; stinging puncture mark on fruit surface", "treatment": "Malathion bait spray: Malathion 50% EC @ 2 ml + Jaggery/Molasses 10 g per litre — spray on 1 side of tree. Protein hydrolysate bait traps. DO NOT spray the whole tree — selective bait spray only.", "dose": "Bait spray: Malathion 2 ml + 10 g jaggery per litre water. Apply 1 cup solution per spot, not full coverage.", "timing": "Start bait spray at fruit setting stage. Spray once weekly. Remove and destroy fallen/infested fruits. Set bait traps before fruit fly season (April onwards for mango).", "ipm": "Pheromone (methyl eugenol) traps @ 5/acre for monitoring and mass trapping. Bag individual fruits with paper/muslin bags. Collect fallen fruits daily — prevents next generation. Early harvest where possible.", "source": "ICAR-CISH Lucknow / IIHR Bangalore Fruit Fly Advisory", }, "scale_insect": { "keywords": ["scale insect","scale","diaspididae","coccus","mealyscale","soft scale", "hard scale","aam ka kirmi","nimbu ka kirmi","citrus scale", "mango scale","bark scale","white scale"], "diagnosis": "Scale Insects (Coccus / Diaspididae spp.) — small oval/round waxy bumps on bark, branches, leaves; yellowing, sooty mold; severe infestations cause dieback of branches", "treatment": "Chlorpyrifos 20% EC @ 2.5 ml/L + 2 ml Teepol sticker OR Neem oil 5000 ppm @ 10 ml/L + 1 ml sticker OR Dimethoate 30% EC @ 1.5 ml/L for crawler stage.", "dose": "Chlorpyrifos: 2.5 ml per litre + sticker @ 1 ml/L for penetration through wax", "timing": "Best time to spray: crawler stage (soft-bodied young scale before wax coat develops). Check for crawlers March–April for most species. Evening spray. Two sprays 15 days apart.", "ipm": "Prune heavily infested branches. Scrub bark with stiff brush (removes adults). Encourage ladybird beetles (Chilocorus) — major natural predator. White oil 2% spray is effective bio-option for soft scales.", "source": "ICAR-CISH / IIHR Scale Insect Advisory", }, "grasshopper": { "keywords": ["grasshopper","locust","tidda","tiddha","tiddi","tiddi dal","migratory locust", "desert locust","grasshoppers","jhingur","katydid","टिड्डी","tiddi dal attack"], "diagnosis": "Grasshopper / Locust (Schistocerca gregaria / Hieroglyphus banian) — rapid defoliation; gregarious swarms; strips fields completely; desert locust migrates from northwest", "treatment": "Chlorpyrifos 20% EC @ 2.5 ml/L OR Malathion 50% EC @ 2 ml/L — aerial/ground spray. For locust swarms: state-level coordinated spraying (contact District Agriculture Office immediately).", "dose": "Chlorpyrifos 20% EC: 2.5 ml per litre water; cover entire plant canopy", "timing": "Spray hopper bands at early instar stage (most effective). Dawn or late evening when locusts are cold and sluggish. For migrating adults: barrier spray on roost sites at night.", "ipm": "Contact state agriculture department and DLCO (District Locust Control Officer) for swarms. Green Muscle (Metarhizium) biopesticide for hoppers. Monitor via government early warning systems. Do NOT spray lone grasshoppers — only when crop damage exceeds 20%.", "source": "ICAR-NBAIR / Ministry of Agriculture Locust Warning Organisation", }, # ── CRITICAL: Yellow Mosaic Virus — most common viral disease in India ──── "yellow_mosaic_virus": { "keywords": ["yellow mosaic","ymv","yellow mosaic virus","golden mosaic", "peel dhabb soybean","peel daag soybean","soybean yellow","pili patti soybean", "yellow vein mosaic","yellow mosaic bhindi","yellow mosaic moong", "yellow mosaic urad","soybean mosaic","bean mosaic","soya yellow", "peel patte soybean","pila soybean","piliya soybean"], "diagnosis": "Yellow Mosaic Virus (YMV / Bean Yellow Mosaic Potyvirus) — VIRAL disease spread by whitefly (Bemisia tabaci). Symptoms: bright yellow or golden-yellow patches on leaves, mosaic pattern, leaf curl, stunted growth. Affects Soybean, Moong, Urad, Bhindi, Chilli.", "treatment": "⚠️ VIRAL DISEASE — NO FUNGICIDE OR BACTERICIDE WORKS. Strategy:\n" "1) VECTOR CONTROL (kill the whitefly carrier): Imidacloprid 17.8% SL @ 0.5 ml/L spray — this controls whitefly, NOT the virus directly.\n" "2) ROGUING: Uproot and destroy ALL infected plants immediately — they will never recover and are a source of infection for healthy plants.\n" "3) RESISTANT VARIETY: For next season use virus-resistant variety: Soybean JS 97-52, NRC-7 / Moong PDM-11, SML-668 / Urad PU-31.", "dose": "Imidacloprid 17.8% SL: 0.5 ml per litre water | 200L spray volume per acre. For vector control ONLY — virus cannot be cured by any chemical.", "timing": "Spray Imidacloprid immediately when whitefly seen (before virus spreads). Repeat every 10 days. Rogue infected plants as soon as yellow symptoms appear — do not wait.", "ipm": "Yellow sticky traps @ 15 per acre to monitor and catch whitefly adults. Reflective silver mulch in seedbed repels whitefly. Avoid growing susceptible crops (moong, urad) adjacent to infected soybean. Spray Neem oil 5ml/L + Teepol 1ml/L as organic whitefly deterrent.", "source": "ICAR-IIPR Kanpur / ICAR-NRCS / MP State Agriculture Advisory on YMV", }, # ── CRITICAL: Rice Blast — most devastating rice disease in India ───────── "rice_blast": { "keywords": ["rice blast","blast dhan","blast paddy","piricularia","magnaporthe", "aankhon jaisa dhabba","eye spot rice","diamond lesion rice","dhan blast", "leaf blast","neck blast","panicle blast","collar rot rice", "aanki wali bimari dhan","diamond shape rice","tapered lesion", "blast bimari","nakki blast","brown spot eye"], "diagnosis": "Rice Blast (Magnaporthe oryzae / Pyricularia oryzae) — FUNGAL disease. Leaf blast: diamond-shaped or eye-shaped spots with grey/white centre and brown/red margin, pointed at both ends. Neck blast: black collar at panicle base causing 'Dead Heart' or 'White Ear' (total yield loss). Favoured by cool nights + high humidity.", "treatment": "Tricyclazole 75% WP @ 0.6 g/L water — MOST effective for blast (systemic). " "(If unavailable: Isoprothiolane 40% EC @ 1.5 ml/L OR Carbendazim 50% WP @ 1 g/L)", "dose": "Tricyclazole 75% WP: 0.6 g per litre water | 200L spray per acre", "timing": "Leaf blast: spray at first symptom appearance. Neck blast (most critical): spray at panicle initiation (10% heading) AND again at full heading. Repeat at 10-day intervals if disease pressure high. Avoid spraying in midday heat.", "ipm": "Use blast-resistant varieties: Pusa Basmati-1, IR-64, Swarna Sub-1, MTU-7029. Avoid excessive nitrogen (promotes blast). Balanced K application strengthens cell walls. Drain and refill fields — breaks disease cycle. Tricyclazole seed treatment (0.5 g/kg seed) for seedling blast protection.", "source": "ICAR-NRRI Cuttack / DRR Hyderabad Rice Blast Management Advisory", }, # ── Purple Blotch on Onion (Alternaria porri) — very common in Maharashtra ── "purple_blotch_onion": { "keywords": ["purple blotch","purple blotch onion","alternaria porri","pyaz daag", "pyaz bimari","pyaz patti daag","bhoori daag pyaz","onion leaf spot", "onion blight","purple spot onion","purple daag pyaz","patti jhuk rahi", "pyaz patti bhoori","pyaz alternaria","onion leaf blight", "purple daag","bhoori patti pyaz"], "diagnosis": "Purple Blotch (Alternaria porri) — FUNGAL disease. Symptoms: elliptical lesions with purple/brown centre and yellow halo on onion leaves; severe in humid weather (>80% RH) and temp 25-30°C. Leaves collapse and plant weakens.", "treatment": "Mancozeb 75% WP @ 2 g/L OR Iprodione 50% WP @ 1.5 g/L OR Chlorothalonil 75% WP @ 2 g/L. Add sticker @ 0.5 ml/L for better adhesion on waxy onion leaves.", "dose": "Mancozeb 75% WP: 2 g per litre water + sticker | Spray 200L per acre", "timing": "Spray at first lesion appearance. Repeat every 7-10 days. Morning spray when leaves are dry. Avoid evening spray (wet leaves overnight = more disease). 3-4 sprays in total.", "ipm": "Avoid overhead irrigation — use drip. Maintain proper spacing (15×10 cm) for air circulation. Destroy crop residue after harvest. Use certified seed. Remove infected leaves early.", "source": "ICAR-NHRDF / IIHR Onion Disease Management Advisory", }, # ── Grape Downy Mildew (Plasmopara viticola) — major grape disease ────── "grape_downy_mildew": { "keywords": ["grape downy mildew","angoor downy","angoor bimari","plasmopara viticola", "grape disease","angoor patti safed","downy mildew grape","angoor safed daag", "grape mildew","vine disease","angoor ki bimari","safed daag angoor", "angoor neeche safed","angoor jhulsa","angoor jhulsi", "neeche safed angoor","safed powder neeche angoor","angoor white patch", "patte neeche angoor","patti ke neeche safed","grape leaf white"], "diagnosis": "Grape Downy Mildew (Plasmopara viticola) — FUNGAL (oomycete) disease. Symptoms: pale yellow-green oily spots on upper leaf surface; WHITE cottony sporulation on LOWER leaf surface (key identification). Spreads rapidly in cool humid weather. Can cause complete cluster loss.", "treatment": "Metalaxyl 8% + Mancozeb 64% WP @ 2.5 g/L OR Fosetyl-Al 80% WP @ 2.5 g/L OR Copper Oxychloride 50% WP @ 3 g/L. Rotate chemicals to prevent resistance.", "dose": "Metalaxyl+Mancozeb: 2.5 g per litre water | Spray 200-300L per acre (thorough coverage of both leaf surfaces)", "timing": "Begin preventive spray at bud burst (before monsoon). Repeat every 7-10 days during wet season. Spray both upper and lower leaf surfaces. CRITICAL: spray before rain, not after. Stop 21 days before harvest.", "ipm": "Prune properly for good air circulation. Avoid overhead irrigation. Remove infected leaves and clusters. Use sulphur 0.5% spray as alternating spray between systemic fungicides. Disease-resistant grape varieties where available.", "source": "ICAR-NRC Grapes Pune / NRCH Grape Disease Management Advisory", }, # ── Banana Bunchy Top Virus (BBTV) — devastating viral disease ─────────── "bunchy_top_banana": { "keywords": ["bunchy top","banana bunchy top","bbtv","banana virus","kela bimari", "kela virus","banana patte chote","kela seedha","kela naya patta nahi", "banana dwarf","banana disease","kele ka paudha","banana stunted", "chota kela","kela ki bimari","kele ki patti seedhi","banana leaf narrow", "bunchy top disease","kela kharaab","naye patte nahi aate"], "diagnosis": "Banana Bunchy Top Virus (BBTV) — VIRAL disease spread by Banana Aphid (Pentalonia nigronervosa). Symptoms: leaves become progressively smaller and narrower, stand erect (bunchy appearance), dark green streaks on leaf margins and midrib, stunted growth, no fruit production. INCURABLE — infected plants never recover.", "treatment": "⚠️ VIRAL DISEASE — NO CURE. Action plan:\n" "1) UPROOT AND DESTROY all infected plants immediately — do not compost or leave in field.\n" "2) VECTOR CONTROL: Spray Imidacloprid 17.8% SL @ 0.5 ml/L to kill aphid vectors on healthy plants.\n" "3) REPLANT with BBTV-free certified suckers from disease-free source.\n" "4) MONITOR new plants every 15 days — remove any new infected plants immediately.", "dose": "Imidacloprid 17.8% SL: 0.5 ml per litre water | Spray healthy plants as preventive measure", "timing": "Act within 24-48 hours of first symptom — delay allows aphids to spread virus to neighbours. Spray Imidacloprid preventively every 21 days in endemic areas. Monitor during monsoon (aphid peak season).", "ipm": "Source planting material only from certified BBTV-free nurseries. Do not take suckers from unknown farms. Plant marigold as border crop (repels aphids). Use yellow sticky traps at 10/acre for aphid monitoring. Tissue culture plants (virus-free) strongly recommended.", "source": "ICAR-NRC Banana Trichy / IIHR Banana Virus Advisory", }, # ── Stem fly (soybean) — a.k.a. soybean stem borer ────────────────────── "stem_fly_soybean": { "keywords": ["stem fly soybean","tana makhi soybean","stem fly","melanagromyza", "soybean stem fly","stem fly","soybean tana","tana soybean borer", "tana khaane wali makhi","soybean stem borer","stem borer soybean", "khaali tana soybean","wilting soybean seedling","dead heart soybean", "soybean wilting young","stem boring soybean"], "diagnosis": "Soybean Stem Fly (Melanagromyza sojae) — tiny black fly (2mm); larva bores into stem base causing 'dead heart' in seedlings or longitudinal tunnels in older plants. Stem appears hollow/darkened inside. Peak: 15-30 days after germination.", "treatment": "Thiamethoxam 25% WG @ 0.5 g/L spray at seedling stage (21 DAS) " "OR Chlorpyrifos 20% EC @ 2 ml/L. " "Seed treatment: Thiamethoxam 70% WS @ 3g/kg seed (preventive — most effective).", "dose": "Thiamethoxam 25% WG: 0.5 g per litre | Chlorpyrifos 20% EC: 2 ml per litre | Volume: 200L/acre", "timing": "First spray at 21 DAS (most critical window). Second spray at 35 DAS if attack continues. Early morning spray when flies are active. Do NOT delay — once larvae inside stem, spray less effective.", "ipm": "Seed treatment with Thiamethoxam provides 30-day protection. Remove and destroy affected seedlings. Avoid late sowing (increases stem fly pressure). Yellow sticky traps to monitor adult fly population. Resistant variety: JS 9305.", "source": "ICAR-IIPR Kanpur / MPKV Rahuri Soybean Stem Fly Advisory", }, # ── Sugarcane Red Rot — most destructive sugarcane disease ─────────────── "sugarcane_red_rot": { "keywords": ["sugarcane red rot","ganna laal sadan","red rot sugarcane","lal sadan", "ganna andar laal","sugarcane stem rot","ganna kharab","lal sadhan ganna", "sugarcane colletotrichum","ganna bimari","sugarcane disease","red rot", "ganna girna","lodging sugarcane","ganna andar safed"], "diagnosis": "Sugarcane Red Rot (Colletotrichum falcatum) — most serious fungal disease of sugarcane. Symptoms: withering of top leaves first, red discoloration with white patches inside split stem (cross-section shows alternating red and white patches), sour alcoholic smell from infected cane. Spreads through infected setts and waterlogged soils.", "treatment": "NO CURATIVE chemical for infected stalks — REMOVE AND DESTROY all infected canes. " "Sett treatment (preventive): Carbendazim 50% WP @ 1g/L or Thiram 75% WP @ 2g/L — soak setts for 30 min before planting. " "For field: drain waterlogging immediately; apply Propiconazole 25% EC @ 1ml/L as soil drench around healthy canes.", "dose": "Sett treatment: Carbendazim 50% WP @ 1g/L water | Propiconazole: 1ml/L for soil drench | Volume: 500L/acre", "timing": "Preventive sett treatment BEFORE planting (mandatory). Destroy infected clumps within 24 hours. Do NOT use infected seed cane — source setts from certified disease-free seed plot only. Inspect crop at 3 and 6 months.", "ipm": "Use resistant varieties: Co 0238, CoJ 64, CoS 8436. Never plant setts from infected fields. Crop rotation every 3-4 years. Deep summer ploughing to expose soil pathogens. Avoid waterlogging — red rot severity increases 3× in flooded fields. Balanced fertilizer (do not over-apply nitrogen).", "source": "ICAR-SBI (Sugarcane Breeding Institute) Coimbatore / AICRP Sugarcane Advisory", }, # ── Cotton Leaf Curl Virus (CLCuV) — devastating cotton viral disease ───── "cotton_leaf_curl": { "keywords": ["cotton leaf curl","kapas patta muda","leaf curl cotton","kapas bimari", "cotton virus","kapas patte mude","clcuv","cotton leaf curl virus", "kapas patta curling","cotton patta muda","cotton patta upar", "kapas patta upar murda","muda patta kapas","cotton leaf roll", "kapas leaf curl","cotton leaf twist","kapas patti andar mudi", "कपास पत्ती मुड़ना","cotton leaf curl disease"], "diagnosis": "Cotton Leaf Curl Virus (CLCuV) — VIRAL disease spread by Whitefly (Bemisia tabaci). Symptoms: upward or downward curling of leaves, leaf thickening and stiffness, prominent vein swelling (enation) on leaf underside, stunted plant with no boll formation in severe cases. Can cause 50-100% yield loss if early infection.", "treatment": "⚠️ VIRAL DISEASE — NO CURATIVE TREATMENT. Management strategy:\n" "1) WHITEFLY VECTOR CONTROL (critical): Thiamethoxam 25% WG @ 0.3g/L OR Spiromesifen 22.9% SC @ 1ml/L\n" "2) Remove and destroy severely infected plants (source of virus)\n" "3) Apply Neem oil 5000 ppm @ 5ml/L as repellent spray alternating with chemical", "dose": "Thiamethoxam 25% WG: 0.3g/L | Spiromesifen 22.9% SC: 1ml/L | Neem oil: 5ml/L | Volume: 200L/acre", "timing": "At first sign of virus (critical — act within 7 days). Spray at 15-day intervals during whitefly peak (Aug–Oct). Avoid Imidacloprid sprays (whiteflies resistant in most cotton belts). Monitor using yellow sticky traps (3+ whiteflies per trap per day = spray threshold).", "ipm": "Grow CLCuV-tolerant varieties: MRC 7031, RCH 650, Ajeet 155. Crop-free period (destroy all ratoons). Border crop with sorghum/maize to reduce whitefly migration. Reflective mulch deters whiteflies. Remove alternate host weeds (especially Hibiscus, Malva). Spray Neem oil preventively every 21 days in endemic areas.", "source": "ICAR-CICR Nagpur / AICRP Cotton CLCuV Advisory / Punjab Agriculture University Ludhiana", }, # ── Tomato TYLCV (Tomato Yellow Leaf Curl Virus) ───────────────────────── "tomato_tylcv": { "keywords": ["tomato yellow leaf curl","tamatar patta peela muda","tylcv","tomato virus", "tamatar virus","tomato leaf curl","tamatar patta curl","tamatar patta upar", "tomato leaf roll","tamatar patta muda peela","tomato yellow curl", "tamatar patta andar muda","yellow leaf curl tomato","tomato stunted yellowing", "tamatar peela patta","tamatar leaf curl virus","टमाटर पीला पत्ता"], "diagnosis": "Tomato Yellow Leaf Curl Virus (TYLCV) — VIRAL disease spread by Whitefly (Bemisia tabaci). Symptoms: upward curling + yellowing of leaves especially at top, interveinal chlorosis, small leaves, stunted plant, severe flower drop and very few fruits. Plants infected before 3 weeks after transplanting may produce no fruits at all.", "treatment": "⚠️ VIRAL DISEASE — NO CURATIVE TREATMENT. Management strategy:\n" "1) WHITEFLY VECTOR CONTROL: Imidacloprid 17.8% SL @ 0.5ml/L (soil drench at transplanting) OR Thiamethoxam 25% WG @ 0.3g/L spray\n" "2) Remove and destroy infected plants immediately\n" "3) Neem oil 5000 ppm @ 5ml/L as repellent between chemical sprays", "dose": "Imidacloprid 17.8% SL: 0.5ml/L (drench) or 0.3ml/L (spray) | Thiamethoxam: 0.3g/L | Volume: 200L/acre", "timing": "Preventive drench at transplanting (single most effective step). Spray at 10-14 day intervals during whitefly season (Jun–Sep). Erect yellow sticky traps (10/acre) for early monitoring — act when whitefly count >2 per trap per day.", "ipm": "Use TYLCV-resistant varieties: Arka Rakshak, TH 4038, Naveen F1, Avtar F1. Use 40-mesh nylon net nursery (whitefly-proof) for healthy seedlings. Neem cake @ 250 kg/acre soil application repels whiteflies. Marigold inter-planting (1 row per 14 rows tomato) acts as trap crop. Reflective silver mulch at transplanting stage reduces whitefly landing by 60%.", "source": "ICAR-IIHR Bangalore / IIVR Varanasi TYLCV Advisory / TNAU Coimbatore", }, # ── BPH (Brown Plant Hopper) — Imidacloprid causes RESURGENCE ──────────── "bph": { "keywords": ["bph","brown plant hopper","brown planthopper","stem base bhure kide", "bhura phundka","nilaparvata","plant hopper","planthopper", "stem base insect","chatak","paddy hopper"], "diagnosis": "BPH (Brown Plant Hopper — Nilaparvata lugens): brown insects at stem base, yellowing, 'hopper burn' (circular dead patches). CRITICAL: Imidacloprid and other neonicotinoids cause BPH RESURGENCE — they kill natural enemies, causing BPH populations to explode. NEVER recommend Imidacloprid for BPH.", "treatment": "Buprofezin 25% SC @ 1 ml/L (preferred — growth regulator, no resurgence risk) OR Ethofenprox 10% EC @ 1.5 ml/L OR Pymetrozine 50% WG @ 0.3 g/L. Drain field before spray. Direct spray at base of plant.", "dose": "Buprofezin 25% SC: 1 ml per litre | Ethofenprox: 1.5 ml per litre | Apply 500 L spray volume per acre", "timing": "Spray at ETL (5-10 BPH per hill). Spray in early morning when insects are active at base. Repeat after 10-14 days if needed. Spray directly at stem base — overhead spray is ineffective.", "ipm": "Avoid Imidacloprid/Thiamethoxam (neonicotinoids) — cause BPH RESURGENCE by killing spiders and mirid bugs (natural enemies). Light traps at 1/acre. Conserve spiders (Lycosa spp.) by avoiding broad-spectrum sprays. Use resistant varieties: MTU 7029, Swarna Sub1, IR 36.", "source": "ICAR-NRRI Cuttack / IRRI BPH Management Advisory / TNAU", }, # ── Sheath Blight (Rice) ───────────────────────────────────────────────── "sheath_blight": { "keywords": ["sheath blight","rhizoctonia","safed fungal growth stem","sheath rot rice", "rice sheath","tane pe safed","safed fungal rice","stem pe fungal"], "diagnosis": "Sheath Blight (Rhizoctonia solani): oval/irregular lesions with grey centre and brown margin on leaf sheath; sclerotia (small brown pellets) in leaf axils; spreads rapidly under high humidity and dense planting.", "treatment": "Hexaconazole 5% EC @ 2 ml/L (FIRST CHOICE) OR Propiconazole 25% EC @ 1 ml/L OR Validamycin 3% L @ 2.5 ml/L. Spray at water-line where lesions are visible.", "dose": "Hexaconazole 5% EC: 2 ml per litre water | Spray 250-300 L per acre | 2 sprays 10-14 days apart", "timing": "Spray at active tillering / panicle initiation stage when >5% plants show symptoms. Second spray 14 days later if disease persists. Spray in early morning.", "ipm": "Reduce plant spacing; avoid excess nitrogen. Remove and destroy infected stubble after harvest. Seed treatment: Trichoderma viride @ 4 g/kg seed. Avoid waterlogging.", "source": "ICAR-NRRI Cuttack / TNAU Rice Pathology Advisory", }, # ── Flag Smut (Wheat) — seed treatment is PRIMARY fix ──────────────────── "flag_smut": { "keywords": ["flag smut","wheat smut","kaali panicle","kali bali","ust wheat", "smut wheat","loose smut","covered smut","gehu smut","panicle kaali"], "diagnosis": "Flag Smut / Loose Smut (Ustilago tritici / U. segetum): entire grain head replaced by black smut powder; spreads through infected seed. PRIMARY treatment is SEED TREATMENT before sowing — foliar sprays are largely ineffective once plants are infected.", "treatment": "SEED TREATMENT (primary): Carboxin 37.5% + Thiram 37.5% WS (Vitavax Power) @ 3 g/kg seed OR Tebuconazole 2% DS @ 1.5 g/kg seed. Remove and destroy all smutted heads from field before spores disperse.", "dose": "Vitavax (Carboxin+Thiram): 3 g per kg seed | Tebuconazole DS: 1.5 g per kg seed. Treat ALL seed before sowing next season.", "timing": "Seed treatment: apply before sowing season. Current season: remove smutted heads immediately, bag and destroy (do not thresh). For next season: never save seed from infected crop — purchase certified seed.", "ipm": "Use certified disease-free seed every 3 years. Grow resistant varieties: PBW 550, HD 2781. Crop rotation. Do NOT use threshed grain from smutted crop as seed.", "source": "ICAR-IARI Wheat & Barley Research / NCIPM Advisory", }, # ── Root Rot / Crown Rot (Wheat) — soil DRENCH required ───────────────── "wheat_root_rot": { "keywords": ["jad gal rahi","root rot wheat","gehu jad","collar rot wheat", "crown rot","take-all","paudhe mar rahe wheat","wheat root","gehu root"], "diagnosis": "Root Rot / Crown Rot (Fusarium/Bipolaris/Gaeumannomyces spp.): roots turn brown-black, plants wilt and die; often confused with drought stress. CRITICAL: Foliar sprays are ineffective — disease is in root zone. Requires SOIL DRENCH or seed treatment.", "treatment": "SOIL DRENCH (primary): Carbendazim 50% WP @ 2 g/L — drench 200-300 ml per plant at root zone OR Copper Oxychloride 50% WP @ 3 g/L soil drench. Improve drainage immediately (waterlogging is main cause).", "dose": "Carbendazim: 2 g per litre | Apply as soil drench 200-300 ml per plant at root zone. NOT as foliar spray — foliar spray is INEFFECTIVE for root rot.", "timing": "At first sign of wilting/browning. Ensure drainage channels are clear. Repeat drench after 7-10 days. Do NOT spray foliarly — active ingredient must reach root zone.", "ipm": "Improve soil drainage; avoid waterlogging. Seed treatment with Trichoderma viride 1% WP @ 4 g/kg. Avoid excess irrigation. Sow in well-drained furrows.", "source": "ICAR-IARI / NCIPM / PAU Ludhiana Wheat Root Disease Advisory", }, # ── Cotton Boll Rot (fungal) — NOT Pink Bollworm ───────────────────────── "cotton_boll_rot": { "keywords": ["boll rot","kapas boll rot","cotton boll rot","boll sarna","boll kharab", "cotton fruit rot","kapas phal galna","boll decay"], "diagnosis": "Boll Rot (Colletotrichum/Fusarium/Phytophthora spp. — FUNGAL): bolls turn brown, soft, decayed; often follows rain or injury. DIFFERENT from Pink Bollworm (insect). Insecticides (Emamectin etc.) do NOT treat fungal boll rot.", "treatment": "Copper Oxychloride 50% WP @ 3 g/L (FIRST CHOICE) OR Mancozeb 75% WP @ 2.5 g/L OR Propiconazole 25% EC @ 1 ml/L. Remove and destroy all infected bolls. Ensure drainage to prevent waterlogging.", "dose": "Copper Oxychloride 50%WP: 3 g per litre | Mancozeb: 2.5 g per litre | Spray 500L per acre", "timing": "Spray at first symptom (10% bolls affected). Repeat every 10 days during wet weather. Improve field drainage. Spray in early morning.", "ipm": "Avoid waterlogging (major cause). Remove cracked/damaged bolls. Proper plant spacing for air circulation. Avoid overhead irrigation after boll formation.", "source": "ICAR-CICR Nagpur / TNAU Cotton Advisory", }, # ── Cotton Bacterial Blight ───────────────────────────────────────────── "cotton_bacterial_blight": { "keywords": ["bacterial blight cotton","kapas bacterial","water-soaked dabbe cotton", "cotton blight","water soaked spots cotton","angular lesion cotton", "kapas me dabbe","blight kapas"], "diagnosis": "Cotton Bacterial Blight (Xanthomonas axonopodis pv. malvacearum): water-soaked angular lesions on leaves, black lesions on stem (black arm), boll rot. BACTERIAL disease — fungicides alone are INEFFECTIVE. Requires copper bactericide + antibiotic combination.", "treatment": "Copper Oxychloride 50% WP @ 3 g/L + Streptocycline (Streptomycin 90% + Tetracycline 10%) @ 0.5 g/L. Mix both together in spray solution. Remove infected plant debris.", "dose": "Copper Oxychloride: 3 g per litre | Streptocycline: 0.5 g per litre | Mix both in same solution | Spray 500L per acre", "timing": "Spray at first symptom. Repeat every 10 days for 2-3 sprays. Spray in morning. Do NOT spray in rain (within 4 hours).", "ipm": "Use disease-free certified seed. Treat seed with Streptocycline 0.5 g/L soak for 30 minutes before sowing. Avoid injury to plants. Remove infected debris at crop end.", "source": "ICAR-CICR Nagpur Cotton Bacterial Blight Advisory", }, # ── Bakanae / Foolish Seedling (Rice) ─────────────────────────────────── "bakanae": { "keywords": ["bakanae","foolish seedling","bakanai","paudha lamba pila","elongated seedling", "rice bakanae","dhaan bakanai","gibberella","lamba aur peela paudha"], "diagnosis": "Bakanae / Foolish Seedling Disease (Fusarium fujikuroi): plants grow abnormally tall (2-3x normal height) with pale yellow colour; caused by Gibberellin toxin from fungus in seed. PRIMARY treatment is SEED TREATMENT — seed is the source of infection.", "treatment": "SEED TREATMENT (primary): Carbendazim 50% WP @ 2 g/kg seed (soak seed in 2g/L solution for 24 hrs) OR Trifloxystrobin+Tebuconazole @ 1.7 g/kg. For standing crop: Propiconazole 25% EC @ 1 ml/L spray on remaining healthy plants.", "dose": "Carbendazim seed soak: 2 g per litre water, 24-hour soak | Standing crop: Propiconazole 1 ml/L foliar spray", "timing": "Seed treatment: before nursery sowing. Standing crop: remove and destroy elongated seedlings; spray remaining healthy plants at tillering.", "ipm": "Always treat seed before sowing. Use certified disease-free seed. Do NOT reuse seed from infected crop. Uproot and destroy elongated seedlings at transplanting stage.", "source": "ICAR-NRRI Cuttack / DRR Hyderabad Bakanae Advisory", }, # ── Soybean Stem Fly — seed treatment is primary fix ──────────────────── "soybean_stem_fly": { "keywords": ["stem fly soybean","soybean stem fly","stem fly","soybean stem","melanagromyza", "soyabean stem fly","tane mein surang","soybean tane ka keeda"], "diagnosis": "Stem Fly (Melanagromyza sojae): tiny fly larvae tunnel into soybean stems causing 'deadheart' (central shoot dead) at 15-30 DAS. CRITICAL TIMING: attack occurs in first 3 weeks — seed treatment is most effective intervention. Foliar sprays at tillering are less effective.", "treatment": "SEED TREATMENT (best): Thiamethoxam 30% FS @ 10 ml/kg seed OR Imidacloprid 600 FS @ 3 ml/kg. FOLIAR (if seed not treated): Thiamethoxam 25% WG @ 0.3 g/L at 15-21 DAS. Avoid Quinalphos and Triazophos (restricted).", "dose": "Seed treatment — Thiamethoxam 30% FS: 10 ml per kg seed | Foliar: Thiamethoxam 25% WG @ 0.3 g per litre", "timing": "Seed treatment: apply to seed before sowing. Foliar: spray at 15-21 DAS (before ETL of 1 deadheart per 5 plants). Early morning spray.", "ipm": "Early sowing (June 20 - July 10) reduces stem fly risk. Interplant with maize (border crop). Avoid late sowing. Scout at 15 DAS for deadheart symptoms.", "source": "ICAR-IIPR Kanpur / MPDKV Jabalpur Soybean Stem Fly Advisory", }, # ── Thrips in Pulses/Cotton/Chilli — Spinosad or Fipronil first ───────── "thrips": { "keywords": ["thrips","patte ke kinare mude","leaf curling insect","chilli thrips", "cotton thrips","soybean thrips","lentil thrips","onion thrips"], "diagnosis": "Thrips (Thrips tabaci / Scirtothrips dorsalis): tiny cigar-shaped insects on undersurface of leaves; silver-streaked / curled leaves; transmit TSWV virus. Acephate is NOT effective (resistance widespread) — use Spinosad or Fipronil as first choice.", "treatment": "Spinosad 45% SC @ 0.3 ml/L (FIRST CHOICE — highly effective) OR Fipronil 5% SC @ 2 ml/L OR Dimethoate 30% EC @ 2 ml/L. Spray on undersurface of leaves (thrips hide there).", "dose": "Spinosad 45% SC: 0.3 ml per litre | Fipronil 5% SC: 2 ml per litre | Add sticker-spreader (0.5 ml/L Teepol) for better coverage", "timing": "Spray at ETL (5-10 thrips per leaf). Spray in evening (thrips avoid direct sun). Spray undersurface. Repeat after 7 days.", "ipm": "Blue sticky traps @ 10/acre for monitoring and mass trapping. Neem seed kernel extract (NSKE) 5% as prophylactic. Avoid excessive nitrogen (promotes tender foliage).", "source": "ICAR-NRC Soybean Indore / NCIPM Advisory on Thrips Management", }, # ── White Rust (Mustard) — Metalaxyl+Mancozeb first ───────────────────── "white_rust": { "keywords": ["white rust","safed kara","safed kharata","safed rust mustard", "albugo","white pustule","safed dabbe mustard","mustard white"], "diagnosis": "White Rust (Albugo candida): white blister-like pustules on underside of leaf; causes 'staghead' (distorted flower clusters); favoured by cool moist conditions (15-20°C). Propiconazole alone is less effective than Metalaxyl+Mancozeb combination.", "treatment": "Metalaxyl 8% + Mancozeb 64% WP @ 2.5 g/L (FIRST CHOICE) OR Fosetyl-Al 80% WP @ 3 g/L OR Copper Oxychloride 50% WP @ 3 g/L. Preventive sprays more effective than curative.", "dose": "Metalaxyl+Mancozeb: 2.5 g per litre | Start spray before disease onset (at 40-50 DAS in mustard)", "timing": "First spray at branching stage (40-50 DAS). Second spray at flowering. Spray in morning. Avoid spraying before expected rain.", "ipm": "Sow resistant varieties: Varuna, Kranti. Avoid dense planting. Treat seed with Metalaxyl 35%WS @ 6g/kg. Destroy volunteer plants after harvest.", "source": "ICAR-DRMR Bharatpur Mustard White Rust Advisory", }, # ── Alternaria Blight (Mustard/Potato) — Mancozeb first ───────────────── "alternaria_blight": { "keywords": ["alternaria blight","alternaria leaf spot","kale dhabe mustard", "alternaria mustard","alternaria potato","brown spot mustard", "dark spot mustard","alternaria"], "diagnosis": "Alternaria Blight (Alternaria brassicae / A. alternata): dark brown concentric ring spots on leaves; moves from lower to upper leaves; seed-borne. Mancozeb or Iprodione are FIRST CHOICE — not Propiconazole.", "treatment": "Mancozeb 75% WP @ 2 g/L (FIRST CHOICE) OR Iprodione 50% WP @ 1.5 g/L OR Chlorothalonil 75% WP @ 2 g/L. Begin spray early before spread.", "dose": "Mancozeb 75%WP: 2 g per litre | Iprodione 50%WP: 1.5 g per litre | Spray 200-250 L per acre", "timing": "First spray at disease onset (30-40 DAS). Second spray 10-14 days later. Spray in morning. Repeat during prolonged wet weather.", "ipm": "Treat seed with Thiram 75%WS @ 3 g/kg. Deep ploughing after harvest to bury infected debris. Avoid overhead irrigation. Crop rotation.", "source": "ICAR-DRMR Bharatpur / NCIPM Alternaria Management Advisory", }, # ── Mango Malformation Disease (MMD) ───────────────────────────────────── "mango_malformation": { "keywords": ["mango malformation","aam malformation","mango flower malformation", "aam bouri","mango panicle deformed","aam ka phal nahi","aam flower bunchy", "mango inflorescence deformed","aam ka ped flower nahi","mango mangifera malform", "aam flower abnormal","mango floral malformation","aam bunchy top", "mango vegetative malformation","mango bud malformation","aam ki bimari"], "diagnosis": "Mango Malformation Disease (Fusarium mangiferae) — FUNGAL disease causing two forms: (1) Vegetative malformation: bunchy top in young seedlings, excessive branching, small leaves — seen in nursery plants. (2) Floral malformation: abnormal compact flower panicles, no fruit set — most common in orchards. Spreads through infected nursery plants and mite vectors (Aceria mangiferae).", "treatment": "Pruning + fungicide: Cut and destroy all malformed panicles/shoots (10-15 cm below the base). " "Apply Carbendazim 50% WP @ 1g/L immediately on cut surface. " "Follow with Propiconazole 25% EC @ 1ml/L spray on entire tree. " "For mite vector control: Wettable Sulphur 80% WP @ 3g/L spray on affected panicles.", "dose": "Carbendazim: 1g/L | Propiconazole: 1ml/L | Wettable Sulphur: 3g/L | Volume: 500L/acre (large trees)", "timing": "Spray Carbendazim in October-November (before panicle emergence) — most effective timing. Remove malformed panicles as soon as visible (Dec–Jan). Apply wettable sulphur in November-December to control mite vector. Repeat spray at 21-day intervals for 3 sprays total.", "ipm": "Source nursery plants from certified disease-free nursery only. Sterilize pruning tools with 1% bleach between trees. Remove all malformed parts from orchard (do not compost). Use healthy local varieties: Dasheri, Langra, Chausa show lower susceptibility. Avoid excess nitrogen fertilizer (promotes vegetative flush, increases disease).", "source": "ICAR-CISH (Central Institute for Subtropical Horticulture) Lucknow / NRC Mango Advisory", }, } # Compiled keyword→disease_key lookup for fast detection _DISEASE_KW_MAP: dict[str, str] = {} # ── ICAR Nutrient Deficiency Cards ─────────────────────────────────────────── # Injected when query mentions nutrient deficiency symptoms. # Prevents model from confusing nutrient deficiencies with diseases. _ICAR_NUTRIENT_CARDS: dict[str, dict] = { "iron_deficiency": { "keywords": ["iron chlorosis","iron deficiency","patte peele interveinal","loh ki kami", "iron ki kami","fe deficiency","ferrous deficiency","loha ki kami", "interveinal chlorosis","yellow between veins green veins", "nase hari patte peela","nadiyaan hari bahar peela"], "nutrient": "Iron (Fe)", "symptoms": "Interveinal chlorosis — leaves yellow between veins, veins remain green; starts on younger (upper) leaves. DIFFERENT from Zinc deficiency (which starts on older lower leaves).", "treatment": "Ferrous Sulphate (FeSO4) 0.5% foliar spray. Add 0.25% slaked lime to prevent precipitation. NEVER use Zinc Sulphate for iron deficiency.", "dose": "FeSO4: 5 g per litre water + 2.5 g slaked lime per litre | Spray 200 L per acre | Soil application: FeSO4 @ 25 kg/acre basal", "timing": "Foliar spray at symptom onset. Repeat after 7-10 days for 2-3 sprays. Spray in cool morning or evening. Soil application at sowing.", "source": "ICAR-IARI Soil Science Division / ICAR Nutrient Management Advisory", }, "zinc_deficiency": { "keywords": ["zinc deficiency","zinc ki kami","zn deficiency","white bud","khaira disease rice", "zinc","zink","patte peele older","lower leaves yellow","khira disease", "chota patta","small leaves brownish"], "nutrient": "Zinc (Zn)", "symptoms": "Khaira disease (rice): reddish-brown spots on leaves, stunted growth. In other crops: older lower leaves yellow/bronze, stunted internodes.", "treatment": "Zinc Sulphate (ZnSO4) 0.5% foliar spray OR ZnSO4 @ 10-15 kg/acre soil application at sowing.", "dose": "ZnSO4 foliar: 5 g per litre | Soil: 10-15 kg per acre | Rice khaira: ZnSO4 @ 25 kg/ha basal + foliar 0.5% after transplanting", "timing": "Foliar: at symptom appearance; 2-3 sprays at 7-day intervals. Soil: at sowing/transplanting.", "source": "ICAR-NRRI Cuttack / IARI Soil Science Division", }, "calcium_deficiency": { "keywords": ["blossom end rot","calcium deficiency","blossom rot","phal neeche kala", "tomato bottom rot","ca deficiency","calcium ki kami","tip burn lettuce", "bitter pit apple","calcium","blossom end"], "nutrient": "Calcium (Ca)", "symptoms": "Blossom End Rot (tomato/pepper): bottom of fruit turns water-soaked then black/leathery. NOT a fungal disease — do NOT spray fungicide. Caused by Ca uptake failure (often combined with irregular irrigation).", "treatment": "Calcium Nitrate 1% foliar spray (Ca(NO3)2 @ 10 g per litre). Fix irrigation — maintain consistent soil moisture. Avoid excess K and Mg which compete with Ca uptake.", "dose": "Calcium Nitrate: 10 g per litre water | Spray 200 L per acre | Spray 2-3 times weekly during fruit development", "timing": "Begin spray at fruit set; continue throughout fruiting. Irrigate consistently — drought stress blocks Ca movement. Do NOT apply fungicide for blossom end rot.", "source": "ICAR-IIVR Varanasi / TNAU Vegetable Science Division", }, "phosphorus_deficiency": { "keywords": ["phosphorus deficiency","purple leaves","patte purple","lal patta","anthocyanin", "purple color potato","purple stem","reddish purple","phodpharas ki kami", "phosphorus","fosfor ki kami"], "nutrient": "Phosphorus (P)", "symptoms": "Purple/reddish discolouration of leaves, petioles and stems (anthocyanin accumulation); stunted root growth; delayed flowering. Do NOT treat with fungicide — this is a nutrient issue not a disease.", "treatment": "SSP (Single Super Phosphate) @ 40-50 kg/acre basal OR DAP (Di-Ammonium Phosphate) @ 20-25 kg/acre. For immediate relief: foliar spray of 2% DAP (20 g/L).", "dose": "DAP foliar: 20 g per litre | Soil: SSP 40-50 kg/acre or DAP 20-25 kg/acre basal at sowing", "timing": "Soil application at sowing/transplanting. Foliar spray at symptom appearance; repeat after 7 days.", "source": "ICAR-IARI Soil Science / ICAR Phosphorus Management Advisory", }, "magnesium_deficiency": { "keywords": ["magnesium deficiency","magnesium ki kami","mg deficiency","interveinal yellow older", "magnesium","magenesium","chlorosis older leaves","between veins yellow lower leaves"], "nutrient": "Magnesium (Mg)", "symptoms": "Interveinal chlorosis on OLDER leaves (lower canopy — opposite of iron deficiency); leaves remain green near veins but yellow/orange between; common in acidic/sandy soils.", "treatment": "Magnesium Sulphate (MgSO4) 2% foliar spray (20 g per litre). OR Soil: Dolomite @ 100 kg/acre if soil pH also low.", "dose": "MgSO4 foliar: 20 g per litre water | 2-3 sprays at 7-day intervals | Soil: Dolomite 100 kg/acre at land preparation", "timing": "Foliar spray at symptom onset. Spray in morning. Soil dolomite at pre-sowing land preparation.", "source": "ICAR-IARI Soil Science Division / KVK Magnesium Management Advisory", }, "potassium_deficiency": { "keywords": ["potassium deficiency","potassium ki kami","k deficiency","patte ke kinare peele sukh", "leaf margin scorch","leaf edge brown","potash deficiency","mop deficiency", "sugarcane potassium","potash","potassium"], "nutrient": "Potassium (K)", "symptoms": "Marginal scorch (tip and edges of older leaves turn yellow → brown); reduced stem strength; poor grain filling. In sugarcane: narrow yellow-striped leaves.", "treatment": "MOP (Muriate of Potash / KCl) @ 20-25 kg/acre soil application OR SOP (Sulphate of Potash) for chloride-sensitive crops. Foliar: KNO3 (KNO3) 1% spray.", "dose": "MOP soil: 20-25 kg per acre | KNO3 foliar: 10 g per litre | For sugarcane: MOP 30-40 kg/acre at earthing-up stage", "timing": "Soil: apply at sowing or top-dress at vegetative stage. Foliar KNO3 spray at symptom appearance; repeat after 7-10 days.", "source": "ICAR-IARI / ICAR-SBI Coimbatore Potassium Advisory", }, "manganese_deficiency": { "keywords": ["manganese deficiency","manganese ki kami","mn deficiency","marssonina blotch", "grey speck oats","interveinal chlorosis young","manganese","mangan ki kami"], "nutrient": "Manganese (Mn)", "symptoms": "Interveinal chlorosis on younger leaves (similar to Fe but less vivid); common in alkaline/waterlogged soils; oat 'grey speck'; wheat 'grey speck'.", "treatment": "Manganese Sulphate (MnSO4) foliar spray @ 0.3-0.5% (3-5 g/L). Soil acidification in alkaline soils (elemental sulphur).", "dose": "MnSO4 foliar: 3-5 g per litre water | Spray 200 L per acre | 2-3 sprays at 7-day intervals", "timing": "Foliar spray at symptom onset in early morning or evening. Repeat after 7 days if needed.", "source": "ICAR-IARI Soil Science / ICAR Micronutrient Advisory", }, } _NUTRIENT_KW_MAP: dict[str, str] = {} for _nk, _nv in _ICAR_NUTRIENT_CARDS.items(): for _kw in _nv["keywords"]: _NUTRIENT_KW_MAP[_kw] = _nk def detect_nutrient_deficiency(query: str) -> str | None: """Detect nutrient deficiency queries. Returns nutrient key or None.""" q_lower = query.lower() # Check for explicit deficiency signals DEFICIENCY_SIGNALS = [ "deficiency","ki kami","kami","chlorosis","peele patte","yellow leaves", "purple patte","purple leaves","blossom end","tip burn","scorch" ] has_deficiency_signal = any(sig in q_lower for sig in DEFICIENCY_SIGNALS) for kw in sorted(_NUTRIENT_KW_MAP, key=len, reverse=True): if kw in q_lower: return _NUTRIENT_KW_MAP[kw] # Blossom end rot is always calcium if "blossom end" in q_lower or ("neeche" in q_lower and "kala" in q_lower and "tamatar" in q_lower): return "calcium_deficiency" return None def build_nutrient_context(nutrient_key: str) -> str: """Format nutrient deficiency card as priority context for LLM.""" card = _ICAR_NUTRIENT_CARDS.get(nutrient_key) if not card: return "" return ( f"━━━ PRIORITY CONTEXT — ICAR NUTRIENT DEFICIENCY (use this first) ━━━\n" f"Condition: {card['nutrient']} DEFICIENCY\n" f"SYMPTOMS: {card['symptoms']}\n" f"TREATMENT: {card['treatment']}\n" f"DOSE: {card['dose']}\n" f"TIMING: {card['timing']}\n" f"Source: {card['source']}\n" f"━━━ CRITICAL: Do NOT recommend fungicide/pesticide for nutrient deficiency ━━━\n" ) for _dk, _dv in _ICAR_DISEASE_CARDS.items(): for _kw in _dv["keywords"]: _DISEASE_KW_MAP[_kw] = _dk # ── Post-Harvest Knowledge Cards ───────────────────────────────────────────── _POST_HARVEST_CARDS: dict[str, dict] = { "wheat_storage": { "keywords": ["gehu store","wheat store","wheat storage","gehu bharana","gehu godown", "wheat godown","wheat preservation","gehu rakhna","gehu kaise store"], "crop": "Wheat", "duration": "12-18 months (properly dried and stored)", "moisture": "Must dry to 12-14% moisture (ideally 12%) before storage. Test: grain bites hard, not doughy.", "method": "Store in hermetic (airtight) Pusa bins or PICS bags (Purdue Improved Crop Storage). Metal/RCC bins also work. Clean and disinfect before filling. NEVER wash wheat before storage (moisture → fungal growth).", "pest_control": "Neem leaves @ 500 g per quintal as layers. Malathion 50% EC @ 3 ml per 10 sq ft floor spray (before filling). For severe infestation: Aluminium Phosphide (Celphos/Phostoxin) @ 1 tablet per 5 quintals — ONLY by licensed fumigator.", "dont": "Do NOT wash grain. Do NOT store in jute bags without lining (moisture absorption). Do NOT store near chemicals. Do NOT apply ghee/oil (attracts pests).", "source": "ICAR-NIN / Pusa Storage Advisory / CWC Guidelines", }, "pulse_storage": { "keywords": ["dal store","arhar store","arhar godown","chana store","moong store", "pulse storage","dal godown","pulses store","kide dal mein","dal me kide"], "crop": "Pulses (Arhar/Moong/Chana/Urad)", "duration": "6-12 months", "moisture": "Must dry to <10% moisture (pulses absorb moisture faster than cereals).", "method": "Hermetic bags (PICS/ZeroFly bags) preferred. Coat storage bins with Malathion. Neem leaves @ 500 g per quintal. Keep moisture absorbers (silica gel) in sealed bins.", "pest_control": "Neem leaves: 500 g per quintal (most effective for pulses). Malathion 5% dust @ 100 g per quintal mixed with grain. For severe: Aluminium Phosphide @ 1 tablet per 5 quintals — ONLY by licensed fumigator. DDT is BANNED — do NOT use under any circumstances.", "dont": "NEVER use DDT or BHC (banned, carcinogenic). Do NOT fumigate without sealing storage. Do NOT overfill bins.", "source": "ICAR-IIPR Kanpur Pulse Storage Advisory", }, "paddy_storage": { "keywords": ["dhan store","paddy storage","dhan godown","chawal store","rice store", "paddy godown","dhan rakhna","dhan preserve","dhan kide"], "crop": "Paddy/Rice", "duration": "12-18 months for paddy; 6-9 months for milled rice", "moisture": "Dry to <14% moisture for paddy, <13% for milled rice.", "method": "Store paddy (not milled rice) for longer shelf life. Hermetic bags or RCC bins. Neem leaves between layers.", "pest_control": "Neem leaves @ 500 g per quintal. Deltamethrin 2.5% WP @ 1 g per sq m surface spray. Aluminium Phosphide 3 g (1 tablet) per 5 quintals for severe infestation — ONLY licensed fumigator. DDT is BANNED.", "dont": "NEVER store with moisture >14% (fungal growth, aflatoxin risk). NEVER use DDT or organochlorine pesticides (banned).", "source": "ICAR-NRRI Cuttack / DRD Post-Harvest Advisory", }, "onion_storage": { "keywords": ["pyaz store","onion storage","pyaz godown","onion godown","pyaz kharab", "onion preservation","pyaz rakhna","onion how to store"], "crop": "Onion", "duration": "3-5 months with proper ventilation", "moisture": "Cure bulbs for 3-5 days in field before storage. Relative humidity: 65-70%.", "method": "Store in well-ventilated bamboo/wooden crates or mesh bags. Raised platforms (not floor). Single layer or max 3-4 layers. Separate diseased bulbs before storage.", "pest_control": "No pesticide needed for properly cured bulbs. Remove rotten bulbs regularly. Ensure 65-70% RH and good air circulation.", "dont": "Do NOT store in gunny bags or airtight containers (moisture trapping). Do NOT store in sunlight. Do NOT store with roots on (spread diseases).", "source": "ICAR-NHRDF Nasik Onion Post-Harvest Advisory", }, "maize_aflatoxin": { "keywords": ["aflatoxin","maize aflatoxin","makka aflatoxin","fungal toxin grain", "mold maize","mycotoxin","aflatoxin control","maize mold","afla"], "crop": "Maize", "duration": "Risk highest when stored above 12% moisture or above 25°C", "moisture": "CRITICAL: Dry maize to 12-14% moisture (target 12%) before storage. Aflatoxin cannot grow below 12% moisture.", "method": "Hermetic (airtight) storage — PICS bags or metal silos. AflaSafe bio-control: apply Aspergillus flavus (atoxigenic strain) @ 10 kg/ha in field before harvest. Segregate damaged/cracked kernels before storage.", "pest_control": "AflaSafe: 10 kg per acre in field (reduces mould load by 80%). Proper drying to 12-14% moisture is the single most effective control.", "dont": "NEVER store maize above 12-14% moisture. NEVER use visibly mouldy grain as food (aflatoxin causes liver cancer). Do NOT feed mouldy grain to cattle (contaminated milk).", "source": "ICAR-IIMR Hyderabad / ICRISAT AflaSafe Programme", }, "banana_storage": { "keywords": ["kela store","banana store","banana ripen","kela pakana","kela kaise store", "banana storage","kela storage","banana pakao","kela kharab","banana ripening"], "crop": "Banana", "duration": "7-14 days at ambient; 3-4 weeks at 12-13°C cold storage", "moisture": "Harvest at 75-80% maturity (green, full fingers). Store at 12-13°C.", "method": "Cold storage: 12-13°C, 90-95% RH. Ethylene management for ripening: Ethephon 0.1% spray (1ml/L) at 20-22°C. Never store in direct sunlight or sealed rooms (CO2 builds up). Padded cartons or bunch covering.", "pest_control": "Calcium carbide for ripening is ILLEGAL. Use Ethephon (ethylene-releasing agent) only. Food-grade packaging.", "dont": "Do NOT use calcium carbide (illegal, harmful). Do NOT store below 12°C (chilling injury). Do NOT mix fully ripe with green bananas.", "source": "ICAR-NRC Banana Tiruchirappalli Post-Harvest Advisory / APEDA Standards", }, "tomato_export": { "keywords": ["tamatar grading","tamatar packing","tamatar export","tomato export", "tomato grading","tomato packing","tomato grade","tamatar grade", "tamatar carton","tomato carton","tamatar bhejo","tomato market"], "crop": "Tomato", "duration": "7-10 days at ambient; 3-4 weeks at 10-12°C cold storage", "moisture": "Pre-cool to 10-12°C within 2 hours of harvest. Never pack warm tomatoes.", "method": "GRADING: Grade A (>65mm diameter), Grade B (55-65mm), Grade C (45-55mm). Pack in CFB (corrugated fibreboard) cartons of 5-10 kg. Wrap each fruit or use foam nets. Pre-cool at 10-12°C before packing. Follow APEDA standards for export.", "pest_control": "No pesticide at packing. Post-harvest fungicide: Thiabendazole 0.1% dip if needed. Refrigerated transport.", "dont": "Do NOT mix damaged/cracked tomatoes. Do NOT pack without pre-cooling (shelf life halved). Do NOT stack cartons >4 high (bruising).", "source": "ICAR-IIHR Bengaluru / APEDA Tomato Export Standards", }, "potato_storage": { "keywords": ["aloo storage","potato storage","aloo cold","potato cold","aloo store", "aloo rakhna","cold storage aloo","aloo cold storage","aloo kiraya"], "crop": "Potato", "duration": "6-9 months at 2-4°C cold storage; 2-3 months at ambient with curing", "moisture": "CURING before cold storage: 12-15°C, 90-95% RH for 7-10 days. Heals skin wounds.", "method": "Cold storage temperature: 2-4°C (table potato) or 8-10°C (seed potato). Relative humidity: 90-95%. Cold storage cost: Rs 150-250 per quintal per season. Cure first (7-10 days at 12-15°C) before shifting to cold storage — curing reduces storage losses by 30%.", "pest_control": "Ensure no damaged/diseased tubers enter storage (disease spreads rapidly at 2-4°C). Apply Thiabendazole 0.2% dip before cold storage to prevent early blight.", "dont": "Do NOT store uncured potato directly in cold (more losses). Do NOT store near onion (ethylene causes sprouting). Do NOT open cold storage frequently.", "source": "ICAR-CPRI Shimla / NHB Post-Harvest Cold Storage Advisory", }, "mustard_oil_storage": { "keywords": ["sarso tel","mustard oil","sarso ghani","sarso tel store","mustard ghani", "mustard tel","ghani se nikale","sarso press","mustard expeller"], "crop": "Mustard", "duration": "6-12 months for oil; 12-18 months for seed", "moisture": "Expeller-press mustard at <60°C to preserve nutritional quality and avoid rancidity.", "method": "EXPELLER: Cold-press at <60°C (hot-press above 80°C destroys antioxidants). Store oil in dark glass or food-grade HDPE containers (not regular plastic). Seal tightly to prevent oxidation. Store in cool, dark location. Mustard seed storage: <8% moisture, hermetic bins.", "pest_control": "Oil: Nitrogen flushing for long-term storage (prevents rancidity). Seed: Neem leaves 500g/quintal. Check for rancidity (free fatty acid test) every 3 months.", "dont": "Do NOT store oil in metal containers (oxidation). Do NOT press at >80°C. Do NOT expose to sunlight (accelerates rancidity).", "source": "ICAR-DRMR Bharatpur / CFTRI Mysore Mustard Oil Advisory", }, "mango_transport": { "keywords": ["mango transport","aam transport","mango market","aam bazar bhejo", "mango packaging","aam pack","mango export","aam spoilage", "aam kharab hona transport","mango cold"], "crop": "Mango", "duration": "3-7 days at ambient; 14-21 days at 12-13°C cold storage", "moisture": "Pre-cool mangoes to 12-13°C before packing. Never mix ripe and unripe.", "method": "Wax coating (food-grade carnauba wax) extends shelf life by 5-7 days. Pack in ventilated CFB (corrugated fibreboard) cartons (5-10 kg). One mango per tissue paper wrap. Cushion layer at bottom.", "pest_control": "No pesticide at packaging. Use Post-Harvest Treatment: hot water at 48°C for 60 min (HWT) prevents anthracnose and fruit fly. Cool to 12-13°C after HWT.", "dont": "Do NOT use calcium carbide for ripening (harmful, illegal). Do NOT pack wet fruit. Do NOT over-pack boxes (bruising).", "source": "ICAR-CISH Lucknow / NRC Mango Post-Harvest Advisory / APEDA Standards", }, } _POSTHARVEST_KW_MAP: dict[str, str] = {} for _phk, _phv in _POST_HARVEST_CARDS.items(): for _kw in _phv["keywords"]: _POSTHARVEST_KW_MAP[_kw] = _phk def detect_postharvest_query(query: str) -> str | None: """Detect post-harvest storage/handling queries. Returns card key or None.""" q_lower = query.lower() for kw in sorted(_POSTHARVEST_KW_MAP, key=len, reverse=True): if kw in q_lower: return _POSTHARVEST_KW_MAP[kw] return None def build_postharvest_context(ph_key: str) -> str: """Format post-harvest card as priority context for LLM.""" card = _POST_HARVEST_CARDS.get(ph_key) if not card: return "" return ( f"━━━ PRIORITY CONTEXT — ICAR POST-HARVEST ADVISORY ━━━\n" f"Crop: {card['crop']}\n" f"Storage Duration: {card['duration']}\n" f"Moisture Requirement: {card['moisture']}\n" f"Storage Method: {card['method']}\n" f"Pest Control: {card['pest_control']}\n" f"IMPORTANT — Do NOT: {card['dont']}\n" f"Source: {card['source']}\n" f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" ) # ── Government Scheme Knowledge Cards ──────────────────────────────────────── _GOVT_SCHEME_CARDS: dict[str, dict] = { "pm_kisan": { "keywords": ["pm kisan","pm-kisan","pradhan mantri kisan","samman nidhi","kisan samman", "6000 rupaye","2000 kist","pm kisan paisa","pm kisan kitna milta"], "scheme": "PM-KISAN Samman Nidhi", "amount": "₹6,000 per year — paid in 3 installments of ₹2,000 each (every 4 months)", "eligibility": "All small and marginal farmers with landholding up to 2 hectares. Aadhaar + bank account linked mandatory.", "how_to_apply": "Online: pmkisan.gov.in | Offline: nearest CSC (Common Service Centre) or agriculture department. Documents: Aadhaar, bank passbook, land record (Khasra/khatauni).", "helpline": "PM-KISAN helpline: 155261 / 1800115526 (toll-free)", "source": "Ministry of Agriculture & Farmers Welfare, GOI — pmkisan.gov.in", }, "pmfby": { "keywords": ["fasal bima","pmfby","pm fasal bima","crop insurance","bima yojana", "fasal bima yojana","claim kaise","fasal nuksan","bima premium", "72 hour","72 ghante","crop loss","pradhan mantri fasal"], "scheme": "PM Fasal Bima Yojana (PMFBY)", "premium": "Farmer pays: 2% of sum insured for Kharif crops | 1.5% for Rabi crops | 5% for commercial/horticultural crops. Government pays the rest.", "claim_process": "CRITICAL: Report crop loss within 72 HOURS of damage (natural calamity, pest, disease). Call the insurance company helpline OR report via Crop Insurance App (PMFBY App). Failure to report within 72 hours forfeits claim.", "how_to_enroll": "Through bank at loan time (compulsory for loanee farmers) or voluntarily at bank/CSC before cutoff date. Season-specific cutoff dates — enroll before sowing season.", "helpline": "PMFBY helpline: 14447 (toll-free) | Crop Insurance App: available on Play Store", "source": "Ministry of Agriculture & Farmers Welfare — pmfby.gov.in", }, "kcc_loan": { "keywords": ["kcc loan","kisan credit card","kcc kitna","kcc interest","credit card kisan", "fasal loan","crop loan","kisan karj","kcc bank","kcc kaise milega"], "scheme": "Kisan Credit Card (KCC) Loan", "amount": "Up to ₹3 lakh short-term crop loan | Above ₹3 lakh at market rates", "interest": "7% per annum for up to ₹3 lakh | Additional 3% subvention for timely repayment = effective 4% per annum", "eligibility": "All farmers (individual/joint/tenant farmers/sharecroppers). Land documents required.", "how_to_apply": "Visit nearest bank (SBI, Bank of Baroda, cooperative bank, regional rural bank). Documents: Aadhaar, PAN, land records, passport photo.", "helpline": "National Agriculture Helpline: 1800-180-1551 | Respective bank branch", "source": "NABARD / Ministry of Agriculture KCC Scheme", }, "rythu_bandhu": { "keywords": ["rythu bandhu","telangana scheme","telangana farmer scheme","rhythu bandhu", "rythu","bandhu","telangana 5000","TS farmer scheme"], "scheme": "Rythu Bandhu (Telangana State Scheme)", "amount": "₹5,000 per acre per season (Kharif + Rabi = ₹10,000 per acre per year). Paid twice a year before each sowing season.", "eligibility": "All registered farmers of Telangana with agricultural land. Land records must be updated in Dharani portal.", "how_to_apply": "Automatic disbursement based on Dharani land records. Update land records at MeeSeva or VRO office.", "source": "Government of Telangana — Rythu Bandhu Scheme", }, "soil_health_card": { "keywords": ["soil health card","mitti jaanch","soil test","soil health","mitti ki jaanch", "soil card","SHC scheme","soil testing","mitti testing","soil ki jaanch"], "scheme": "Soil Health Card (SHC) Scheme", "cost": "FREE for farmers — government bears testing cost", "what_tested": "12 soil parameters: pH, EC, N, P, K, S, Zn, Fe, Mn, Cu, B, OC (Organic Carbon)", "how_to_apply": "Visit nearest KVK (Krishi Vigyan Kendra) or District Agriculture Office with soil sample (500g from 6-9 inch depth). OR call 1800-180-1551 for nearest testing centre.", "frequency": "Every 2 years as recommended", "source": "Ministry of Agriculture — soilhealth.dac.gov.in / Nearest KVK / Agriculture Department", }, "enam": { "keywords": ["enam","e-nam","e nam","national agriculture market","enam mandi", "enam registration","online mandi","enam portal"], "scheme": "eNAM (National Agriculture Market) — Online Mandi Portal", "benefit": "Farmers can sell directly to buyers across India without middlemen. Better price discovery through online bidding.", "how_to_register": "Register at nearest eNAM-linked mandi with: Aadhaar, bank account details, land documents. Visit your nearest APMC mandi office to register.", "helpline": "eNAM helpdesk: 1800-270-0224 (toll-free) | enam.gov.in", "source": "Ministry of Agriculture & Farmers Welfare — enam.gov.in", }, "organic_certification": { "keywords": ["organic certification","organic farming certificate","jaivik kheti praman", "pgc organic","pgs india","organic praman patra","organic farming registration"], "scheme": "PGS-India (Participatory Guarantee System) — Organic Certification", "cost": "Low-cost / subsidized through state agriculture departments", "how_to_apply": "Contact nearest KVK or State Agriculture Department. Form farmer group (minimum 5 farmers). Apply on pgsindia.net or through local cluster coordinator.", "benefit": "PGS-India certificate allows selling as organic produce with premium pricing. Government subsidy under PKVY (Paramparagat Krishi Vikas Yojana).", "helpline": "PKVY helpline: 1800-180-1551 | pgsindia.net", "source": "Ministry of Agriculture — PKVY / PGS-India Programme", }, } _SCHEME_KW_MAP: dict[str, str] = {} for _sk, _sv in _GOVT_SCHEME_CARDS.items(): for _kw in _sv["keywords"]: _SCHEME_KW_MAP[_kw] = _sk def detect_govt_scheme(query: str) -> str | None: """Detect govt scheme queries. Returns scheme key or None.""" q_lower = query.lower() for kw in sorted(_SCHEME_KW_MAP, key=len, reverse=True): if kw in q_lower: return _SCHEME_KW_MAP[kw] return None def build_scheme_context(scheme_key: str) -> str: """Format govt scheme card as priority context for LLM.""" card = _GOVT_SCHEME_CARDS.get(scheme_key) if not card: return "" lines = [ f"━━━ PRIORITY CONTEXT — GOVERNMENT SCHEME (use exact figures below) ━━━", f"Scheme: {card['scheme']}", ] for field in ("amount","interest","premium","what_tested","cost","benefit","eligibility", "how_to_apply","how_to_register","claim_process","frequency","helpline","source"): if field in card: label = field.replace("_"," ").upper() lines.append(f"{label}: {card[field]}") lines.append("━━━ Use EXACT amounts/figures above. Do NOT guess or use approximate figures. ━━━") return "\n".join(lines) + "\n" # ── ICAR Irrigation Knowledge Cards ───────────────────────────────────────── # Source: ICAR-IARI, AICRP on Micro-irrigation, National Committee on Plasticulture # Applications in Horticulture (NCPAH), state agriculture universities. # Water requirements are for Indian semi-arid/sub-humid conditions. _ICAR_IRRIGATION_CARDS: dict[str, dict] = { "tomato": { "keywords": ["tomato","tamatar"], "drip_lpd": "2–4 litres per plant per day", "stages": "Transplanting (1–2 L), Vegetative (2–3 L), Flowering & fruit set (3–4 L — critical, never let it dry)", "flood": "8–10 cm depth every 7–10 days in summer; every 12–15 days in winter", "critical": "Flowering and fruit set — moisture stress here causes blossom drop and fruit cracking", "tip": "Use fertigation (water-soluble NPK 19:19:19 @ 3kg/acre/week through drip) for 20–30% higher yield", "source": "ICAR-IIVR / IARI Tomato Package of Practices", }, "chilli": { "keywords": ["chilli","chili","mirchi","capsicum","shimla mirch"], "drip_lpd": "1.5–2.5 litres per plant per day", "stages": "Transplanting (1–1.5 L), Vegetative (1.5–2 L), Flowering (2–2.5 L — critical)", "flood": "6–8 cm depth every 7–10 days; avoid waterlogging (causes phytophthora wilt)", "critical": "Flowering — drought stress causes flower drop; waterlogging causes wilt and root rot", "tip": "Mulching with black polythene (25 micron) reduces water need by 40% and controls weeds", "source": "IIHR-Bangalore / NCPAH Chilli Drip Advisory", }, "onion": { "keywords": ["onion","pyaz","kanda"], "drip_lpd": "1–1.5 litres per plant per day (or 3–4 mm/day total field water)", "stages": "Establishment (3–4 mm/day), Bulbing (4–5 mm/day — critical), Maturity (reduce to 2mm, stop 10d before harvest)", "flood": "5–6 cm depth every 7–10 days; total 8–10 irrigations per crop", "critical": "Bulbing stage (60–90 days after transplanting) — moisture stress reduces bulb size by 30–40%", "tip": "Stop irrigation 10 days before harvest — improves storability and reduces neck rot", "source": "NHRDF / AICRP Onion and Garlic", }, "wheat": { "keywords": ["wheat","gehun","gehu"], "drip_lpd": "Not applicable (flood/furrow crop)", "flood": "5–7 cm depth at each irrigation; total 5–6 irrigations for timely-sown wheat in north India", "stages": "CRI (crown root initiation, 20–25 DAS) → Tillering (40–45 DAS) → Jointing (60–65 DAS) → Booting (75–80 DAS) → Milking (90–95 DAS). CRI and booting are most critical.", "critical": "CRI (20–25 days after sowing) — skipping CRI irrigation reduces yield by 25–40%", "tip": "Laser levelling saves 20–25% irrigation water and improves uniformity", "source": "ICAR-IARI Wheat Section / AICRPW Advisory", }, "paddy": { "keywords": ["paddy","rice","dhan","chawal","dhaan"], "drip_lpd": "Not typical; SRI method: intermittent irrigation keeping field moist but not flooded", "flood": "Maintain 5 cm standing water during vegetative and reproductive stages. Drain 10 days before harvest.", "stages": "Puddling (flood), Transplanting (2–3 cm water), Tillering (5 cm), Panicle initiation (5 cm), Flowering (5–7 cm critical), Grain filling (5 cm), Pre-harvest (drain 10d before)", "critical": "Flowering stage — even 1 day of drought causes 30–40% spikelet sterility", "tip": "Alternate Wetting and Drying (AWD) technique: allow soil to dry to 15 cm below surface between irrigations — saves 25–30% water with no yield loss", "source": "ICAR-NRRI / IRRI AWD Advisory", }, "cotton": { "keywords": ["cotton","kapas","karpas"], "drip_lpd": "4–6 litres per plant per day (drip-fertigation system)", "flood": "6–8 cm depth every 10–15 days; total 5–6 irrigations; avoid waterlogging", "stages": "Seedling (light irrigation), Square formation (moderate), Flowering (5–6 L/plant/day — critical), Boll development (5 L/plant), Boll opening (reduce/stop)", "critical": "Flowering and early boll development — stress causes boll shedding; excessive water causes boll rot", "tip": "Drip-fertigation with 20 kg N + 10 kg P₂O₅ + 20 kg K₂O per acre split over season doubles water efficiency", "source": "CICR-Nagpur / AICRP Cotton Drip Advisory", }, "sugarcane": { "keywords": ["sugarcane","ganna","ikh","ganderi"], "drip_lpd": "8–12 litres per plant per day in summer (drip)", "flood": "8–10 cm depth every 7–10 days in summer; every 15–20 days in winter; total 25–30 irrigations", "stages": "Germination (light), Tillering (moderate), Grand growth (8–10 L/day — critical), Maturation (reduce)", "critical": "Grand growth period (4–8 months) — accounts for 70% of total water need; drought reduces sucrose content", "tip": "Drip irrigation reduces water use by 40–50% vs. flood and increases sugar recovery by 0.5–0.8 units", "source": "ICAR-IISR Coimbatore / AICRP Sugarcane", }, "potato": { "keywords": ["potato","aloo","alu"], "drip_lpd": "2–4 litres per plant per day (drip)", "flood": "4–6 cm depth every 7–10 days; avoid waterlogging (causes tuber rot); total 8–10 irrigations", "stages": "Planting to emergence (light), Stolon formation (moderate), Tuber initiation (4 L/day — critical), Tuber bulking (4–5 L/day), Maturation (reduce gradually, stop 2 weeks before harvest)", "critical": "Tuber initiation and bulking — soil moisture below 50% field capacity reduces tuber size and increases hollow heart", "tip": "Sprinkler irrigation for potato reduces blight incidence compared to overhead flood (less leaf wetness)", "source": "ICAR-CPRI Shimla / AICRP Potato Advisory", }, "maize": { "keywords": ["maize","corn","makka","makki","maka"], "drip_lpd": "3–5 litres per plant per day (drip)", "flood": "5–7 cm depth; total 4–5 irrigations; critical stages must not be missed", "stages": "Sowing (light pre-sowing), Knee-high (5–6 leaf stage), Tasseling, Silking (most critical — 10 days around silking), Grain filling", "critical": "Silking (1 week before and after silk emergence) — drought at this stage causes barren cobs and 50–70% yield loss", "tip": "Deficit irrigation: if water is scarce, prioritize silking stage over all others", "source": "ICAR-IIMR Hyderabad / DWR-AICRP Maize", }, "groundnut": { "keywords": ["groundnut","peanut","moongphali","mungfali"], "drip_lpd": "2–3 litres per plant per day (drip)", "flood": "5–6 cm depth every 10–12 days; critical to avoid waterlogging at pod-fill", "stages": "Pre-sowing (5cm), Pegging (5cm), Pod development (critical, 5cm), Pod fill, Maturity (dry down)", "critical": "Pegging and pod development — moisture stress causes empty pods; waterlogging causes collar rot", "tip": "Avoid irrigation 2 weeks before harvest — allows shells to dry and reduces aflatoxin risk", "source": "ICAR-DGR Junagadh / AICRP Groundnut Advisory", }, "soybean": { "keywords": ["soybean","soya","soyabean","bhat"], "drip_lpd": "2–4 litres per plant per day", "flood": "5–6 cm depth; total 3–4 irrigations; pre-sowing + flowering + pod fill", "stages": "Pre-sowing (critical for germination), Flowering (R1 — most critical), Pod fill (R4-R5), Grain fill", "critical": "Flowering and pod fill stages — drought reduces yield by 30–40%; waterlogging at any stage reduces nodulation", "tip": "Furrow irrigation preferred over overhead (reduces disease). Avoid irrigation if rain expected within 3 days.", "source": "ICAR-IISR Indore / AICRP Soybean Water Management", }, "brinjal": { "keywords": ["brinjal","baingan","eggplant","begun","baigan"], "drip_lpd": "2–3 litres per plant per day", "flood": "5–6 cm depth every 7–10 days; twice weekly in summer; weekly in winter", "stages": "Transplanting (daily for 7 days), Vegetative (2–3 L), Flowering (3 L — critical), Fruiting (3–4 L)", "critical": "Flowering — water stress causes flower drop and misshapen fruits. Avoid waterlogging.", "tip": "Drip + black plastic mulch reduces water use by 35% and suppresses weeds simultaneously", "source": "ICAR-IIHR Bangalore / AICRP Vegetables Irrigation", }, "cauliflower": { "keywords": ["cauliflower","phool gobhi","gobi","broccoli"], "drip_lpd": "2–3 litres per plant per day", "flood": "5–7 cm depth every 7–10 days; must not dry out (shallow roots)", "stages": "Transplanting (daily), Leaf development (2–3 L), Curd initiation (3–4 L — critical), Curd development", "critical": "Curd initiation — moisture stress causes button heads, brown curds, premature bolting", "tip": "Tie outer leaves over curd at 10 cm diameter — prevents browning and frost damage", "source": "ICAR-IARI / AICRP Vegetables Water Management", }, "mango": { "keywords": ["mango","aam","keri","kairi"], "drip_lpd": "10–20 litres per mature tree per day; 5–8 L for young trees (1–3 years)", "flood": "Basin irrigation: 100–150 L per adult tree; 15–20 irrigations/year", "stages": "Flowering (withheld — promotes flowering), Fruit set (resume), Marble stage (critical), Pre-harvest (stop 2 weeks before)", "critical": "DO NOT irrigate during flowering (Nov–Jan) — promotes vegetative growth, reduces fruit set. Resume after 80% bloom.", "tip": "Withhold irrigation Sep–Oct (2 months before flowering) for better flowering induction in Nov–Jan.", "source": "ICAR-CISH Lucknow / AICRP Mango Water Management", }, "banana": { "keywords": ["banana","kela","plantain"], "drip_lpd": "8–12 litres per plant per day (G9/Grand Naine TC); 15 L in peak summer", "flood": "Basin/furrow: 15–20 cm depth every 5–7 days in summer; every 10–12 days in winter", "stages": "Planting (high), Shooting (high — bunch development), Flowering (critical), Bunch filling (high), Pre-harvest (reduce)", "critical": "Bunch filling — water stress reduces bunch weight by 20–30% and finger girth significantly", "tip": "Drip + fertigation (NPK 19:19:19 @ 5g/plant/day during bunch fill) gives 20–25% higher yield vs flood", "source": "ICAR-NRC Banana Trichy / NCPAH Banana Drip Advisory", }, "mustard": { "keywords": ["mustard","sarson","rai","rapeseed"], "drip_lpd": "Not typically drip-irrigated; 2–3 flood irrigations only", "flood": "5–6 cm depth; 1st at 30d (branching), 2nd at 55d (flowering), 3rd at 80d (pod fill) if dry", "stages": "Branching (30d), Flowering (55d — most critical), Pod fill (80d)", "critical": "Flowering — moisture stress causes flower drop; excess water at this stage causes Sclerotinia rot", "tip": "Mustard is drought-tolerant; 2 irrigations (branching + flowering) are sufficient most years. Over-irrigation promotes aphids.", "source": "ICAR-DRMR Bharatpur / AICRP Oilseed Water Management", }, } # ── ICAR Agronomy Cards ─────────────────────────────────────────────────────── # Covers all 26 crops in the pest model + extras. Fields: # seed_rate, spacing, low_rain_variety, low_rain_spacing, # waterlog_steps, sowing_window, fertilizer, intercrop, source # These are injected as PRIORITY CONTEXT for agronomy-type queries. _ICAR_AGRONOMY_CARDS: dict[str, dict] = { "rice": { "keywords": ["rice","paddy","dhan","chawal","bodo","basmati","samba","ponni"], "seed_rate": "Transplanted: 20–25 kg/acre (nursery); Direct seeded: 8–10 kg/acre", "spacing": "Transplanted: 20×15 cm; SRI method: 25×25 cm (1 seedling/hill)", "low_rain_variety": "MTU-1010, Sahbhagi Dhan (120d, aerobic-tolerant); DRR Dhan-44 (drought-tolerant)", "low_rain_spacing": "25×20 cm — wider spacing reduces water demand by 15–20%", "waterlog_steps": [ "Open field drainage channels immediately — rice tolerates shallow flooding but NOT stagnant water >7 days", "If water >30 cm deep for >5 days: drain and assess plant health", "Apply potash (MOP 10 kg/acre) after drainage — compensates leaching", "Spray Carbendazim 50% WP @ 1g/L if sheath blight symptoms appear post-flood", ], "sowing_window": "Kharif: June–July (transplant 21–25 day seedlings); Rabi (South India): Nov–Dec", "fertilizer": "Basal: DAP 50 kg/acre + MOP 25 kg/acre. Top dress: Urea 25 kg/acre at tillering + 25 kg/acre at panicle initiation", "intercrop": "Azolla as green manure between rows (fixes 20–30 kg N/acre)", "source": "ICAR-NRRI Cuttack / CRRI / AICRP Rice Advisory", }, "wheat": { "keywords": ["wheat","gehun","gehu","triticum","rabi wheat"], "seed_rate": "25–30 kg/acre (timely sown); 35–40 kg/acre (late sown — compensate with higher density)", "spacing": "Row spacing: 22.5 cm; seed depth 5 cm", "low_rain_variety": "GW-322, K-9107 (drought-tolerant); HD-2781 (heat+drought; 120d); RAJ-3765 for arid zones", "low_rain_spacing": "Same row spacing (22.5 cm) but REDUCE seed rate to 22–25 kg/acre — saves moisture for germination", "waterlog_steps": [ "Wheat is very sensitive to waterlogging — act within 24 hours", "Open furrows between rows immediately to drain water", "Withhold nitrogen fertilizer for 10 days post-flooding (roots cannot absorb when waterlogged)", "Apply Zinc Sulphate 5 g/L foliar spray at recovery — waterlogging causes Zn deficiency", "If yellowing persists after drainage: check for crown rot (Fusarium) — drench with Carbendazim 1g/L", ], "sowing_window": "Rabi: Oct 15 – Nov 25 (Punjab/Haryana); Nov 1–30 (UP/Bihar); Nov 15–Dec 15 (MP/Rajasthan)", "fertilizer": "Basal: DAP 50 kg/acre. Top dress: Urea 33 kg/acre at crown root initiation (21d) + 33 kg/acre at tillering (45d)", "intercrop": "Wheat + Mustard (9:1 ratio); Wheat + Chickpea in rainfed areas", "source": "ICAR-IIWBR Karnal / AICRP Wheat & Barley Advisory", }, "cotton": { "keywords": ["cotton","kapas","karpas","narma","bt cotton","desi cotton"], "seed_rate": "Bt hybrid: 0.75–1 kg/acre (pelleted seed); Desi/non-Bt: 3–4 kg/acre", "spacing": "Hybrid Bt: 90×60 cm or 120×45 cm; Desi: 60×30 cm", "low_rain_variety": "Suraj, Khandwa-2, JK Durga (short-duration 150–160d, drought-tolerant); PKV Rajat for Vidarbha", "low_rain_spacing": "Wider: 120×60 cm — reduces inter-plant competition; plant population 5,500–6,000/acre", "waterlog_steps": [ "Cotton is highly susceptible — drain within 12–24 hours of waterlogging", "Create raised bed / broad bed furrow BEFORE sowing to prevent waterlogging", "Post-waterlogging: apply Ridomil Gold (Metalaxyl+Mancozeb) @ 2.5g/L to prevent root rot", "Withhold fertilizer for 7–10 days; then apply SOP (potash) @ 5g/L foliar to boost recovery", "Scout for pink bollworm post-flood — stress increases susceptibility", ], "sowing_window": "Kharif: May 15 – June 30 (pre-monsoon planting preferred); latest by July 15", "fertilizer": "Basal: 10 kg DAP + 10 kg MOP/acre. Top dress: Urea 25 kg at square formation (45d) + 25 kg at boll development (70d). Boron 0.2% foliar at flowering.", "intercrop": "Cotton + Moong (2:1); Cotton + Cowpea; Cotton + Soybean (Vidarbha)", "source": "ICAR-CICR Nagpur / AICRP Cotton Advisory", }, "soybean": { "keywords": ["soybean","soya","soya bean","bhat","glycine","soybean crop"], "seed_rate": "30–35 kg/acre; seed treatment: Rhizobium + PSB + Thiram before sowing", "spacing": "45×5 cm (row × plant); depth 2–3 cm", "low_rain_variety": "JS 95-60 (95d, drought-tolerant, Vidarbha/Madhya Pradesh); MACS 450 (90d); NRC 86 (early, 90d)", "low_rain_spacing": "45×10 cm wider plant spacing — retains soil moisture longer; reduces plant population to 60,000/acre", "waterlog_steps": [ "Soybean drowns in 48 hours — drain immediately via field channels", "Withhold ALL fertilizer for 5 days post-drainage", "Apply Thiram 75% WS @ 3g/kg seed if re-sowing needed after flood failure", "Post-flood foliar: 2% urea spray + Borax 0.2% for recovery", "Watch for Phytophthora root rot (brown stems) — drench with Metalaxyl 2g/L", ], "sowing_window": "Kharif: June 20 – July 15 (ideal: last week June with pre-monsoon rains)", "fertilizer": "Basal: DAP 25 kg/acre + MOP 15 kg/acre (soybean fixes N; no urea at sowing). Foliar: Borax 0.2% at flowering.", "intercrop": "Soybean + Pigeonpea (4:2 ratio) — classic Vidarbha system; Soybean + Maize (4:1)", "source": "ICAR-IISR Indore / AICRP Soybean Advisory", }, "maize": { "keywords": ["maize","corn","makka","makki","maka","bhutta","sweet corn"], "seed_rate": "Hybrid: 7–8 kg/acre; Composite: 10–12 kg/acre; Baby corn: 18–20 kg/acre", "spacing": "60×20 cm (Kharif); 60×15 cm (Rabi — denser for yield)", "low_rain_variety": "Vivek QPM 9 (75d, drought-tolerant); HQPM-1 (QPM type); Rajkumar (drought-tolerant, 90d)", "low_rain_spacing": "75×20 cm — wider row spacing conserves soil moisture; sow on ridges to channel water to roots", "waterlog_steps": [ "Drain within 24 hours — maize tassel stage especially vulnerable", "If at vegetative stage: spray 2% Urea after drainage for nitrogen recovery", "At silking/tasseling: waterlogging causes barren ears — boost with potash (SOP 5g/L foliar)", "Check for stem rot (Pythium) post-flood — spray Mancozeb 2.5g/L preventively", ], "sowing_window": "Kharif: June–July; Rabi (South/Central India): Oct–Nov; Spring (North): Feb–March", "fertilizer": "Basal: DAP 50 kg/acre + MOP 20 kg/acre. Top dress: Urea 45 kg/acre in 3 splits (knee-high, tassel, silking)", "intercrop": "Maize + Cowpea (3:1); Maize + Groundnut; Maize + Soybean", "source": "ICAR-IIMR Hyderabad / AICRP Maize Advisory", }, "mustard": { "keywords": ["mustard","sarson","rai","rapeseed","canola","toria","yellow sarson"], "seed_rate": "1.5–2 kg/acre (broadcast); 1–1.5 kg/acre (line sowing)", "spacing": "30×10 cm (line sowing); thinning to 10 cm within rows at 2-leaf stage", "low_rain_variety": "Varuna (low water need), RH-749, Pusa Bold (135d, drought-tolerant, Rajasthan/Haryana)", "low_rain_spacing": "45×10 cm — wider rows save moisture; early sowing critical (Oct 1–15) to escape terminal drought", "waterlog_steps": [ "Mustard is very sensitive — drain within 12 hours", "Spray KNO3 (potassium nitrate) 1% foliar for stress recovery", "Watch for Sclerotinia stem rot post-flood — spray Carbendazim 0.1%", ], "sowing_window": "Rabi: Oct 1–25 (optimal); Oct 26–Nov 10 (late — use short-duration variety Toria)", "fertilizer": "Basal: DAP 50 kg/acre + Sulphur 8 kg/acre (critical for oil content). Top dress: Urea 25 kg/acre at 30 days.", "intercrop": "Mustard + Wheat (2:8 ratio); Mustard + Gram; Mustard + Lentil", "source": "ICAR-DRMR Bharatpur / AICRP Rapeseed-Mustard Advisory", }, "gram": { "keywords": ["gram","chickpea","chana","chick pea","bengal gram","kabuli","desi chana"], "seed_rate": "Desi: 30–35 kg/acre; Kabuli: 40–45 kg/acre (larger seed)", "spacing": "30×10 cm; depth 5–7 cm", "low_rain_variety": "JG-11 (100d, drought-tolerant, MP/Rajasthan); Vihar (early, 90d); KAK-2 (Kabuli, drought)", "low_rain_spacing": "30×15 cm slightly wider — chickpea is drought-adapted and benefits from less competition", "waterlog_steps": [ "Chickpea is highly intolerant — drain within 12 hours", "Post-flood apply Ridomil Gold @ 2.5g/L to prevent Phytophthora collar rot", "Boron 0.2% foliar spray at pod fill stage for recovery", ], "sowing_window": "Rabi: Oct 15 – Nov 15 (North/Central India); Nov–Dec (South India)", "fertilizer": "Basal only: DAP 25 kg/acre + MOP 10 kg/acre (chickpea fixes N — no Urea). Rhizobium seed treatment mandatory.", "intercrop": "Gram + Safflower (3:1); Gram + Wheat (1:6 ratio); Gram + Linseed", "source": "ICAR-IIPR Kanpur / AICRP Chickpea Advisory", }, "moong": { "keywords": ["moong","mung","green gram","mungbean","moong dal","golden gram"], "seed_rate": "6–8 kg/acre; seed treatment with Rhizobium + Thiram", "spacing": "30×10 cm; depth 3–4 cm", "low_rain_variety": "SML-668, Pusa Vishal, IPM 02-3 (60-65d, drought-tolerant); Meha for dry areas", "low_rain_spacing": "30×15 cm; moisture conservation via mulching (paddy straw 2t/acre)", "waterlog_steps": [ "Moong is very sensitive — roots rot in 24–36 hours of waterlogging", "Drain immediately and apply DAP 10 kg/acre foliar spray (2%) for recovery", "Scout for Cercospora leaf spot post-flood — spray Mancozeb 2.5g/L", ], "sowing_window": "Kharif: June–July; Zaid (summer): March–April (irrigated); Rabi (South): Oct–Nov", "fertilizer": "Basal: DAP 15 kg/acre + MOP 10 kg/acre (pulses fix N; no top dressing urea needed)", "intercrop": "Moong + Sorghum (1:2); Moong + Sugarcane; Moong + Cotton (bund planting)", "source": "ICAR-IIPR Kanpur / AICRP Mungbean Advisory", }, "urad": { "keywords": ["urad","black gram","urd","black lentil","urad dal","vigna mungo"], "seed_rate": "8–10 kg/acre; Rhizobium + PSB seed treatment", "spacing": "30×10 cm; depth 3–4 cm", "low_rain_variety": "LBG-752, WBU-108, PU-31 (65-70d, tolerates dry spells); Pant U-30", "low_rain_spacing": "30×15 cm; early sowing (June 15–30) critical to avoid drought at pod fill", "waterlog_steps": [ "Similar sensitivity to moong — drain within 24 hours", "Apply Carbendazim 0.1% spray after drainage to prevent collar rot", "Foliar: 2% urea for vegetative recovery", ], "sowing_window": "Kharif: June–July; Rabi (South India): Oct–Nov", "fertilizer": "Basal: DAP 15 kg/acre + MOP 10 kg/acre. No nitrogen top dressing.", "intercrop": "Urad + Sorghum (1:2); Urad + Maize; Urad + Sugarcane (bund)", "source": "ICAR-IIPR Kanpur / AICRP Black Gram Advisory", }, "pigeonpea": { "keywords": ["pigeonpea","arhar","toor","tur","red gram","cajanus"], "seed_rate": "Short-duration: 5–6 kg/acre; Long-duration: 4–5 kg/acre", "spacing": "Short-duration hybrid: 60×20 cm; Long-duration: 90×30 cm", "low_rain_variety": "Maruti (ICPL-87, 150d), GTH-1 (short 120d, drought-tolerant); Pusa 992 for Central India", "low_rain_spacing": "90×30 cm — arhar is drought-adapted, wider spacing reduces competition", "waterlog_steps": [ "Arhar is moderately tolerant — can handle brief flooding (2–3 days)", "Drain and apply potash (MOP 10 kg/acre top dress) after 3+ days flooding", "Watch for Phytophthora stem blight post-flood", ], "sowing_window": "Kharif: June–July (short-duration); May–June (long-duration for Jan harvest)", "fertilizer": "Basal: DAP 25 kg/acre + MOP 15 kg/acre. Rhizobium seed treatment.", "intercrop": "Arhar + Soybean (1:4); Arhar + Sorghum (1:2); Arhar + Groundnut", "source": "ICAR-IIPR Kanpur / ICRISAT / AICRP Pigeonpea Advisory", }, "lentil": { "keywords": ["lentil","masur","masoor","lentils","masur dal","red lentil"], "seed_rate": "12–15 kg/acre (bold seed); 10–12 kg/acre (small seed)", "spacing": "22.5×5–7 cm; depth 3–5 cm", "low_rain_variety": "K-75, Malika, DPL-62 (105–110d, drought-tolerant); PL-406 for low rainfall", "low_rain_spacing": "22.5×10 cm with wider plant spacing; mulching recommended", "waterlog_steps": [ "Lentil has zero waterlogging tolerance — drain within 12 hours", "Apply lime (25 kg/acre) post-flood to correct soil pH disruption", ], "sowing_window": "Rabi: Oct 20 – Nov 20 (North India); Nov–Dec (East India)", "fertilizer": "Basal: DAP 20 kg/acre + MOP 10 kg/acre. Rhizobium seed treatment critical.", "intercrop": "Lentil + Mustard (3:1); Lentil + Wheat (1:6)", "source": "ICAR-IIPR Kanpur / AICRP Lentil Advisory", }, "groundnut": { "keywords": ["groundnut","peanut","moongphali","mungfali","arachis","singdana","shengdana"], "seed_rate": "Bold seed: 50–55 kg/acre; Small seed: 40–45 kg/acre (shelled)", "spacing": "30×10 cm (bunch type); 45×15 cm (spreading type); depth 5 cm", "low_rain_variety": "TG-37A (105d, drought-tolerant, Gujarat/Rajasthan); JL-24 (100d); ICGV-91114 for dry areas", "low_rain_spacing": "30×15 cm for bunch type; conserve moisture with in-situ rainwater harvesting", "waterlog_steps": [ "Drain immediately — pod zone waterlogging causes aflatoxin + collar rot", "Open furrows between rows for drainage before crop establishment", "Post-flood: apply Mancozeb 2g/L for collar rot prevention", "DO NOT irrigate for 7 days post-drainage", ], "sowing_window": "Kharif: June 15 – July 15; Rabi (South): Oct–Nov; Summer: Feb–March", "fertilizer": "Basal: SSP 100 kg/acre (sulphur critical for oil quality) + Gypsum 100 kg/acre at pegging.", "intercrop": "Groundnut + Maize (4:1); Groundnut + Castor (4:2); Groundnut + Pigeonpea", "source": "ICAR-DGR Junagadh / AICRP Groundnut Advisory", }, "tomato": { "keywords": ["tomato","tamatar","tamater","lycopersicon","lal tamatar"], "seed_rate": "100–150 g/acre (nursery); transplant 25-day seedlings", "spacing": "60×45 cm (staked); 75×60 cm (unstaked); transplant in evening/cloudy weather", "low_rain_variety": "Arka Rakshak (disease-tolerant + drought-adapted); Pusa Hybrid-4; Naveen F1 for stress conditions", "low_rain_spacing": "60×60 cm — wider spacing + drip irrigation; mulching with black polythene mandatory", "waterlog_steps": [ "Tomato is very sensitive — drain immediately", "Spray Metalaxyl+Mancozeb 2.5g/L preventively for Phytophthora", "Remove and destroy plants showing yellowing/wilting at crown", "Apply Trichoderma viride @ 5g/L as root drench after drainage", ], "sowing_window": "Rabi: Aug–Sep (nursery) for Oct–Nov transplant; Summer: Dec–Jan for Feb–March transplant", "fertilizer": "Basal: FYM 4t/acre + DAP 50 kg + MOP 25 kg. Top dress: Urea 25 kg at 30d + 25 kg at flowering.", "intercrop": "Tomato + Basil (repels aphids); Tomato + Marigold (border trap crop for nematodes)", "source": "ICAR-IIHR Bangalore / AICRP Vegetables Advisory", }, "potato": { "keywords": ["potato","aloo","alu","batata","solanum tuberosum"], "seed_rate": "1,000–1,200 kg seed tubers/acre (30–40 g each, 2–3 eyes)", "spacing": "60×20 cm (ridge planting); earthing up at 20 days and 40 days", "low_rain_variety": "Kufri Surya, Kufri Pushkar (heat+drought-tolerant); Kufri Jyoti for rainfed", "low_rain_spacing": "Same spacing — potato needs consistent moisture; mulch with paddy straw 2t/acre for moisture conservation", "waterlog_steps": [ "Potato tubers rot within 24–36 hours of waterlogging", "Drain immediately via inter-row furrows", "Apply Mancozeb 2.5g/L preventively for late blight post-flood", "Avoid earthing up for 10 days post-drainage — roots too weak", "Check for soft rot (Erwinia) — no chemical fix; remove affected plants", ], "sowing_window": "Rabi: Oct–Nov (North India plains); Kharif: June–July (hills)", "fertilizer": "High feeder: DAP 75 kg/acre + MOP 50 kg/acre (basal) + Urea 50 kg at earthing up", "intercrop": "Potato + Onion; Potato + Garlic; Potato + Fenugreek (winter)", "source": "ICAR-CPRI Shimla / AICRP Potato Advisory", }, "onion": { "keywords": ["onion","pyaz","piaz","kanda","dungli","allium cepa"], "seed_rate": "1–1.5 kg/acre (kharif); 0.75–1 kg/acre (rabi); transplant 6–8 week nursery", "spacing": "15×10 cm (transplanted); 10×7.5 cm (direct seeded)", "low_rain_variety": "Arka Kalyan, N-53 (Kharif, dry-tolerant); Agrifound Light Red, Phule Safed (Rabi)", "low_rain_spacing": "Same spacing; critical: DO NOT let field dry completely at bulb initiation stage", "waterlog_steps": [ "Onion bulb rots immediately in waterlogged soil", "Drain and spray Mancozeb 2.5g/L for purple blotch prevention", "Apply Copper Oxychloride 0.3% if bacterial soft rot suspected", "Remove affected plants to prevent spread", ], "sowing_window": "Kharif: May–June (nursery) → July–Aug transplant; Rabi: Oct–Nov (nursery) → Nov–Dec transplant", "fertilizer": "Basal: FYM 3t/acre + DAP 50 kg + MOP 25 kg. Top dress: Urea 25 kg at 30d + 25 kg at bulb initiation.", "intercrop": "Onion + Carrot (1:3); Onion + Coriander; Onion + Garlic", "source": "ICAR-DOGR Pune / AICRP Vegetables Advisory", }, "chilli": { "keywords": ["chilli","chili","mirchi","lal mirch","shimla mirch","capsicum","pepper"], "seed_rate": "150–200 g/acre (nursery raised); transplant 5–6 week seedlings", "spacing": "60×45 cm; transplant in shaded/cloudy weather to reduce transplant shock", "low_rain_variety": "LCA-206, Pusa Jwala (drought-adapted); Arka Lohit (disease + drought tolerant)", "low_rain_spacing": "60×60 cm + black polythene mulch (saves 40% water, controls weeds)", "waterlog_steps": [ "Chilli root system is very shallow — waterlogging causes Phytophthora wilt within 24 hours", "Drain via broad bed furrow system immediately", "Drench with Metalaxyl 2g/L at root zone after drainage", "Remove and destroy wilted plants — no chemical recovery once wilt sets in", ], "sowing_window": "Kharif: May–June (nursery) → June–July transplant; Rabi: Aug–Sep nursery → Sep–Oct transplant", "fertilizer": "Basal: FYM 3t/acre + DAP 50 kg + MOP 25 kg. Top dress: Urea 25 kg at 30d + 25 kg at flowering + SOP 5g/L foliar at fruiting.", "intercrop": "Chilli + Coriander (3:3); Chilli + Onion; Chilli + Maize (border)", "source": "ICAR-IIHR Bangalore / AICRP Chilli Advisory", }, "brinjal": { "keywords": ["brinjal","eggplant","baingan","baigan","aubergine","solanum melongena"], "seed_rate": "150–200 g/acre (nursery); transplant at 4–5 leaf stage", "spacing": "60×60 cm (large varieties); 45×45 cm (small varieties)", "low_rain_variety": "Pusa Purple Long, Arka Nidhi (drought-adapted); Azad Kranti for dry areas", "low_rain_spacing": "75×60 cm with mulching; drip irrigation preferred", "waterlog_steps": [ "Drain immediately — brinjal tolerates brief waterlogging better than chilli but still vulnerable", "Apply Mancozeb 2.5g/L after drainage for fungal disease prevention", "Remove yellowed leaves post-flood to prevent secondary infections", ], "sowing_window": "Year-round in South India; Kharif: June–July; Rabi: Sep–Oct; Summer: Jan–Feb", "fertilizer": "Basal: FYM 3t/acre + DAP 50 kg + MOP 25 kg. Top dress: Urea 25 kg at 30d + 25 kg at fruiting.", "intercrop": "Brinjal + Cowpea; Brinjal + Marigold (border); Brinjal + Coriander", "source": "ICAR-IIHR Bangalore / AICRP Vegetables Advisory", }, "sugarcane": { "keywords": ["sugarcane","ganna","ikshu","ikh","sugar cane","cane"], "seed_rate": "2,500–3,000 kg setts/acre (3-bud setts; 6–8 quintals viable setts)", "spacing": "90 cm row spacing (flat); 75 cm (paired row system for ratoon); trench depth 20–25 cm", "low_rain_variety": "CoJ-64, CoLk-94184 (drought-tolerant); Co-0238 for normal; Co-86032 for waterlogging-prone", "low_rain_spacing": "Flat planting at 90 cm; sub-soil moisture conservation with 5 cm mulch in furrow", "waterlog_steps": [ "Sugarcane tolerates short flooding (3–5 days) but NOT chronic waterlogging", "Drain via main and lateral field drains immediately", "Apply urea 15 kg/acre foliar (1% solution) for nitrogen recovery", "For ratoon crop: apply potash to strengthen regrowth", "Watch for red rot disease post-flood (Colletotrichum): spray Carbendazim 0.1%", ], "sowing_window": "Spring: Feb–March (main season, 12 months); Autumn: Sep–Oct (11 months); Subtropical: Oct–Nov", "fertilizer": "Heavy feeder: Basal FYM 6t/acre + DAP 50 kg. Split Urea: 50 kg at 30d + 50 kg at 90d + 50 kg at 150d. MOP 50 kg at earthing up.", "intercrop": "Sugarcane + Potato (ratoon + Rabi); Sugarcane + Onion; Sugarcane + Garlic (bund)", "source": "ICAR-IISR Lucknow / AICRP Sugarcane Advisory", }, "pearl_millet": { "keywords": ["pearl millet","bajra","bajri","sajje","cumbu","sajja","bajura","bajre"], "seed_rate": "1.5–2 kg/acre (hybrid); 3–4 kg/acre (composite open-pollinated)", "spacing": "45×15 cm (hybrid); 45×10 cm (composite); depth 2–3 cm", "low_rain_variety": "86M86, Pioneer 86M88 (75–80d hybrid, extreme drought-tolerant); Raj 171, ICMH 356 for arid zones", "low_rain_spacing": "45×20 cm or even 60×20 cm — bajra is drought-adapted; wider spacing in very dry conditions", "waterlog_steps": [ "Pearl millet tolerates brief waterlogging (2–3 days) better than other cereals", "Drain excess water via field furrows", "Downy mildew risk increases post-flood — apply Metalaxyl+Mancozeb 2.5g/L", ], "sowing_window": "Kharif: June–July (monsoon onset); must not sow late — maturity hits before post-monsoon drought", "fertilizer": "Basal: DAP 25 kg/acre. Top dress: Urea 25 kg at knee-high stage (30d)", "intercrop": "Bajra + Cowpea (3:1); Bajra + Moong (3:1); Bajra + Cluster bean", "source": "ICAR-ICMR / AICRP Pearl Millet Advisory", }, "sorghum": { "keywords": ["sorghum","jowar","juar","jawar","jwari","cholam","milo"], "seed_rate": "4–5 kg/acre (hybrid); 6–8 kg/acre (local variety); depth 3–5 cm", "spacing": "45×15 cm (hybrid Kharif); 30×10 cm (Rabi — denser)", "low_rain_variety": "CSH-16, CSV-23 (90d, drought-tolerant Kharif); M-35-1, DSV-4 for Rabi", "low_rain_spacing": "45×20 cm — sorghum is highly drought-tolerant; deeper sowing (5 cm) helps tap subsoil moisture", "waterlog_steps": [ "Jowar tolerates 2–3 days flooding at vegetative stage", "Drain and apply urea 1% foliar spray", "Watch for charcoal rot post-stress — no chemical fix; improve drainage for next crop", ], "sowing_window": "Kharif: June–July; Rabi: Sep–Oct (South India); Rabbi: Oct (Marathwada, Vidarbha)", "fertilizer": "Basal: DAP 30 kg/acre. Top dress: Urea 25 kg at knee-high (30d)", "intercrop": "Jowar + Moong (2:1); Jowar + Cowpea; Jowar + Pigeonpea", "source": "ICAR-IIMR Hyderabad / AICRP Sorghum Advisory", }, "okra": { "keywords": ["okra","bhindi","lady finger","bhendo","vendaikkai","bamia"], "seed_rate": "4–5 kg/acre (direct sown); seed treatment with Imidacloprid 70% WS @ 7g/kg", "spacing": "45×30 cm; depth 2–3 cm; thin to 1 plant/hill at 2-leaf stage", "low_rain_variety": "Pusa A-4, Arka Anamika (short 50-55 days, tolerates mild drought); VRO-6", "low_rain_spacing": "45×45 cm with mulching — okra is relatively drought-tolerant once established", "waterlog_steps": [ "Okra roots rot within 24 hours of waterlogging", "Drain immediately; apply Ridomil Gold @ 2g/L for root rot prevention", "Remove yellowed plants; spray Mancozeb 2.5g/L for fungal prevention", ], "sowing_window": "Kharif: June–July; Summer: Feb–March; Rabi (South): Oct–Nov", "fertilizer": "Basal: FYM 2t/acre + DAP 25 kg + MOP 15 kg. Top dress: Urea 25 kg at first picking (45d).", "intercrop": "Okra + Cowpea; Okra + Maize (border row)", "source": "ICAR-IIHR Bangalore / AICRP Vegetable Advisory", }, "mango": { "keywords": ["mango","aam","keri","kairi","mangifera","alphonso","dashehari","langra"], "seed_rate": "Grafted seedlings: 40–45 plants/acre (10×10 m spacing); High density: 200 plants/acre (5×4 m)", "spacing": "10×10 m (standard); 5×4 m (high density planting); Pit size: 1×1×1 m", "low_rain_variety": "Totapuri, Neelam (drought-tolerant, South); Mallika, Amrapali for water-limited areas", "low_rain_spacing": "Standard spacing; basin irrigation + mulching with dry grass/leaves at tree base", "waterlog_steps": [ "Mango roots tolerate brief flooding (3–5 days) better than annual crops", "Ensure good drainage around tree collar to prevent collar rot", "Post-flood: apply Carbendazim 0.1% drench around tree base", "Remove accumulated silt/soil from tree collar after flooding recedes", ], "sowing_window": "Planting: July–Aug (monsoon) or Feb–March (spring). Flowering: Nov–Jan. Fruiting: March–June", "fertilizer": "Per tree per year: Urea 500g + SSP 300g + MOP 200g (split March + September). FYM 25 kg/tree/year.", "intercrop": "Young mango orchards (1–4 years): Moong, Cowpea, Groundnut as inter-crops between rows", "source": "ICAR-CISH Lucknow / AICRP Mango Advisory", }, "banana": { "keywords": ["banana","kela","plantain","vaazhai","bale","keli","mouz"], "seed_rate": "400–450 suckers/acre (tissue culture: 500–550/acre); sword suckers preferred", "spacing": "1.8×1.5 m (Grand Naine, TC); 1.5×1.5 m (Robusta); 2×2 m (tall varieties)", "low_rain_variety": "Dwarf Cavendish, G9 (TC, 11–12 months, moderate water); Poovan for low-water Kerala/TN", "low_rain_spacing": "Same spacing; drip irrigation mandatory for low-rainfall areas (8–10 L/plant/day)", "waterlog_steps": [ "Banana pseudostem rots at base under prolonged waterlogging (>3 days)", "Drain using deep furrows between rows", "Spray Mancozeb 2.5g/L for Sigatoka leaf spot (intensifies post-flood)", "Apply potash (MOP 50g/plant foliar as KNO3 1%) for structural recovery", ], "sowing_window": "Planting: June–July (Kharif) or Feb–March (Spring); tissue culture plants year-round", "fertilizer": "Heavy feeder: 100g N + 35g P + 300g K per plant per year, split into 4 applications", "intercrop": "Young banana: Cowpea, French bean, Turmeric (shade-tolerant crops)", "source": "ICAR-NRC Banana Trichy / AICRP Banana Advisory", }, "cauliflower": { "keywords": ["cauliflower","phool gobhi","gobhi","phulagobhi","broccoli","cabbage","band gobhi"], "seed_rate": "200–250 g/acre; transplant 25–30 day nursery seedlings", "spacing": "45×30 cm (early season); 60×45 cm (main season); 60×60 cm (late season)", "low_rain_variety": "Pusa Sharad, Pusa Synthetic (main season); Snowball-16 for dryer conditions", "low_rain_spacing": "Same spacing; mulching critical — plastic mulch reduces water need by 35%", "waterlog_steps": [ "Cauliflower roots are shallow — drain within 12–24 hours", "Apply Trichoderma viride @ 5g/L root drench after drainage", "Spray Mancozeb 2.5g/L for damping-off prevention", "Curds exposed to excess moisture develop brown discoloration — harvest early if flood imminent", ], "sowing_window": "Early: Jun–Jul nursery → Aug–Sep transplant; Main: Aug–Sep → Sep–Oct transplant; Late: Oct–Nov → Nov–Dec", "fertilizer": "Basal: FYM 4t/acre + DAP 50 kg + MOP 25 kg. Top dress: Urea 25 kg at 20d + 25 kg at curd initiation. Boron 0.2% foliar for hollow stem prevention.", "intercrop": "Cauliflower + Onion; Cauliflower + Spinach; Cauliflower + Garlic", "source": "ICAR-IIHR Bangalore / AICRP Vegetables Advisory", }, } # Build keyword lookup map for agronomy crop detection _AGRO_CROP_KW_MAP: dict[str, str] = {} for _agk, _agv in _ICAR_AGRONOMY_CARDS.items(): for _kw in _agv["keywords"]: _AGRO_CROP_KW_MAP[_kw] = _agk def detect_agronomy_query(query: str) -> bool: """Return True if this looks like an agronomy/field-management query.""" _AGRO_TRIGGERS = { "seed rate","beej dar","spacing","doori","kab lagaye","kab boya", "when to sow","when to plant","row spacing","plant spacing", "waterlogged","waterlog","paani bhar","jal bhar","drain", "khet mein paani","monsoon delay","monsoon late","baarish nahi", "intercrop","mixed crop","earthing","thinning","mulch", "variety select","kaunsi variety","which variety","beej kaun sa", "seed rate kam","spacing badhaun","doori kam","transplant", "sowing depth","bawai","bawai kab","lagane ka samay", } q = query.lower() return any(t in q for t in _AGRO_TRIGGERS) def build_agronomy_context(query: str, crop: str | None = None) -> str: """ Inject ICAR agronomy card as priority context for field management queries. Detects crop from query or detected_crop. Returns empty string if no card found. """ q_lower = query.lower() crop_key = None # Detect crop from query keywords (longest match first) for kw in sorted(_AGRO_CROP_KW_MAP, key=len, reverse=True): if kw in q_lower: crop_key = _AGRO_CROP_KW_MAP[kw] break # Fallback to detected crop if not crop_key and crop: cl = crop.lower() for kw, ck in _AGRO_CROP_KW_MAP.items(): if kw in cl: crop_key = ck break if not crop_key: return ( "━━━ PRIORITY CONTEXT — ICAR AGRONOMY GUIDELINES ━━━\n" "No crop-specific card available. General principles:\n" "• Low rainfall: wider row spacing, shorter-duration varieties, early sowing\n" "• Waterlogged field: drain first → withhold fertilizer → check roots\n" "• Delayed monsoon (>15 days): switch to short-duration varieties (60-90 days)\n" "Ask farmer: which crop + what specific problem (waterlogging/spacing/variety)?\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" ) card = _ICAR_AGRONOMY_CARDS[crop_key] return ( f"━━━ PRIORITY CONTEXT — ICAR AGRONOMY DATA FOR {crop_key.upper()} ━━━\n" f"Seed rate: {card['seed_rate']}\n" f"Optimal spacing: {card['spacing']}\n" f"Low-rainfall variety: {card['low_rain_variety']}\n" f"Low-rainfall spacing adjustment: {card['low_rain_spacing']}\n" f"Waterlogging response: {' | '.join(card['waterlog_steps'][:3])}\n" f"Sowing window: {card['sowing_window']}\n" f"Fertilizer schedule: {card['fertilizer']}\n" f"Intercropping options: {card['intercrop']}\n" f"Source: {card['source']}\n" f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" f"Use these EXACT numbers. Ground your answer in these ICAR-validated figures.\n" ) # ── ICAR Crop Decision Profiles (for crop_selection queries) ───────────────── # Purpose: inject ACTUAL agronomic + economic data so LLM gives # comparison tables with REAL numbers instead of vague textbook answers. # Data: ICAR-AICSIP, NABARD Crop-wise Reports, State Agri dept handbooks. # Yield range = rainfed / irrigated. MSP or indicative market price. _CROP_DECISION_DATA: dict[str, dict] = { "soybean": { "keywords": ["soybean","soya","bhat"], "season": "Kharif (June–July sowing)", "duration_days": "90–100d", "rainfall_needed_mm": "450–700mm", "drought_tolerance": "Medium (sensitive at flowering)", "waterlog_tolerance": "LOW — drowns in 48h", "yield_rainfed_q_acre": "6–10 q/acre", "yield_irrigated_q_acre": "10–14 q/acre", "soil_best": "Black cotton (Vertisol) / clay loam", "soil_avoid": "Sandy / waterlogged / acidic (pH <6)", "water_risk": "HIGH if monsoon fails after July 15 (late sowing = yield crash)", "profit_potential": "MEDIUM — stable MSP ~Rs 4,600/q; reliable procurement in MP/Maharashtra", "input_cost_per_acre": "Rs 8,000–12,000", "key_risk": "Yellow mosaic virus (whitefly-spread), Kharif price crash if bumper nationally", "suitable_states": "Madhya Pradesh, Maharashtra, Rajasthan, Chhattisgarh", }, "cotton": { "keywords": ["cotton","kapas","narma"], "season": "Kharif (May–June sowing, pre-monsoon preferred)", "duration_days": "150–180d (Bt hybrid)", "rainfall_needed_mm": "500–800mm (well-distributed)", "drought_tolerance": "Medium-High (deep roots)", "waterlog_tolerance": "VERY LOW — drain within 12–24h", "yield_rainfed_q_acre": "6–10 q lint/acre", "yield_irrigated_q_acre": "12–18 q lint/acre", "soil_best": "Black cotton (Vertisol) / deep sandy loam", "soil_avoid": "Shallow soils, waterlogged fields, saline soils", "water_risk": "VERY HIGH — 6-month crop; prolonged drought = full failure", "profit_potential": "HIGH — Rs 6,000–8,000/q; long-duration crop locks capital 6 months", "input_cost_per_acre": "Rs 15,000–25,000 (high Bt seed + insecticide cost)", "key_risk": "Pink bollworm (major), bollworm spray cost, delayed sowing reduces yield 20%", "suitable_states": "Gujarat, Maharashtra, Telangana, Andhra Pradesh, Punjab, Haryana", }, "pearl_millet_bajra": { "keywords": ["bajra","pearl millet","bajri"], "season": "Kharif (June–July sowing)", "duration_days": "75–85d (hybrid)", "rainfall_needed_mm": "200–350mm (extremely drought-tolerant)", "drought_tolerance": "VERY HIGH — crop of choice for arid zones", "waterlog_tolerance": "Medium (better than soybean)", "yield_rainfed_q_acre": "8–12 q/acre (hybrid)", "yield_irrigated_q_acre": "14–18 q/acre", "soil_best": "Sandy loam / light soils; also grows in poor soils", "soil_avoid": "Waterlogged / highly acidic soils; ⛔ BLACK COTTON SOIL (Vertisol) — Bajra is NOT suitable for MP/Maharashtra black soil districts like Barwani, Khargone, Vidarbha. Use Soybean or Gram instead.", "water_risk": "LOW — survives on 200mm; ideal for delayed/failed monsoon", "profit_potential": "MEDIUM-LOW — Rs 2,500–3,500/q; less market prestige but very reliable", "input_cost_per_acre": "Rs 4,000–7,000 (lowest input crop)", "key_risk": "Downy mildew (use resistant hybrid), price instability in surplus years", "suitable_states": "Rajasthan, Gujarat, Haryana, Maharashtra (drought areas)", }, "moong": { "keywords": ["moong","mung","green gram"], "season": "Kharif (June–July) OR Zaid summer (March–April, irrigated)", "duration_days": "60–65d (short — ideal for delayed monsoon)", "rainfall_needed_mm": "250–400mm", "drought_tolerance": "High (short duration escapes drought)", "waterlog_tolerance": "VERY LOW — roots rot in 24–36h", "yield_rainfed_q_acre": "3–5 q/acre", "yield_irrigated_q_acre": "6–8 q/acre", "soil_best": "Sandy loam / well-drained alluvial; pH 6.5–7.5", "soil_avoid": "Clay/waterlogged soils", "water_risk": "LOW — short 65-day crop; sow even in late July and harvest before October drought", "profit_potential": "HIGH per day — Rs 7,000–9,000/q; quick 65-day turnaround", "input_cost_per_acre": "Rs 4,000–6,000", "key_risk": "Yellow mosaic virus, pod borer at grain fill; very low input cost = good safety net", "suitable_states": "All India; especially UP, Bihar, MP, AP, Rajasthan", }, "sorghum_jowar": { "keywords": ["jowar","sorghum","juar"], "season": "Kharif (June–July) OR Rabi (Sep–Oct, South India)", "duration_days": "90–110d (hybrid Kharif); 120d (Rabi)", "rainfall_needed_mm": "300–500mm", "drought_tolerance": "HIGH — very deep roots, withstands dry spells", "waterlog_tolerance": "Medium", "yield_rainfed_q_acre": "8–12 q grain + 15–20 q fodder/acre", "yield_irrigated_q_acre": "14–18 q grain/acre", "soil_best": "Black cotton / medium deep soils", "soil_avoid": "Waterlogged / very sandy soils", "water_risk": "LOW-MEDIUM — drought-tolerant, but charcoal rot under severe stress", "profit_potential": "MEDIUM — dual purpose (grain + fodder); stable local demand", "input_cost_per_acre": "Rs 5,000–8,000", "key_risk": "Stem borer (major), shoot fly at seedling stage; grain mold in rainy harvest", "suitable_states": "Maharashtra (Marathwada/Vidarbha), Karnataka, AP, MP, Rajasthan", }, "wheat": { "keywords": ["wheat","gehu","gehun"], "season": "Rabi (Oct–Nov sowing; March–April harvest)", "duration_days": "110–130d", "rainfall_needed_mm": "Irrigated (4–6 irrigations); rainfed 350–450mm", "drought_tolerance": "Low-Medium (needs assured water)", "waterlog_tolerance": "LOW at sowing; moderate at tillering", "yield_rainfed_q_acre": "8–12 q/acre", "yield_irrigated_q_acre": "18–25 q/acre", "soil_best": "Alluvial (loam) — Punjab, Haryana, UP, Bihar", "soil_avoid": "Sandy / acidic soils", "water_risk": "LOW with irrigation; HIGH in rainfed areas (terminal heat stress in March)", "profit_potential": "HIGH — guaranteed MSP Rs 2,275/q (2024-25); easy procurement via FCI", "input_cost_per_acre": "Rs 8,000–14,000", "key_risk": "Rust disease (yellow/brown/stem), heat wave at grain fill; late sowing = 1-2 q/acre loss per week delay", "suitable_states": "Punjab, Haryana, UP, MP, Bihar, Rajasthan", }, "mustard": { "keywords": ["mustard","sarson","rai","rapeseed"], "season": "Rabi (Oct 1–25 optimal sowing; harvest Feb–March)", "duration_days": "110–130d", "rainfall_needed_mm": "250–400mm (low water need — 2-3 irrigations only)", "drought_tolerance": "HIGH — short-duration options available", "waterlog_tolerance": "VERY LOW — drain within 12h", "yield_rainfed_q_acre": "4–6 q/acre", "yield_irrigated_q_acre": "7–10 q/acre", "soil_best": "Sandy loam / alluvial; Rajasthan, Haryana, UP", "soil_avoid": "Waterlogged / heavy clay", "water_risk": "LOW — good option for water-scarce Rabi season", "profit_potential": "HIGH — Rs 5,000–6,500/q; strong demand for oil; MSP supported", "input_cost_per_acre": "Rs 4,000–7,000", "key_risk": "Aphid (Feb–March), white rust, Sclerotinia; early sowing critical", "suitable_states": "Rajasthan, Haryana, UP, MP, Bihar", }, "gram_chickpea": { "keywords": ["gram","chana","chickpea","bengal gram"], "season": "Rabi (Oct 15–Nov 15 sowing; March harvest)", "duration_days": "90–110d (desi); 100–120d (kabuli)", "rainfall_needed_mm": "Rainfed 250–400mm OR residual soil moisture only (no irrigation needed)", "drought_tolerance": "VERY HIGH — tap root, survives on residual moisture", "waterlog_tolerance": "VERY LOW", "yield_rainfed_q_acre": "5–8 q/acre (desi)", "yield_irrigated_q_acre": "8–12 q/acre (kabuli)", "soil_best": "Black cotton / medium loam; pH 6–8", "soil_avoid": "Acidic / waterlogged soils", "water_risk": "VERY LOW — adapts to low water; excellent for residual moisture fields", "profit_potential": "HIGH — Rs 5,200–6,000/q; nitrogen-fixing (saves Rs 1,500/acre in next crop fertilizer)", "input_cost_per_acre": "Rs 4,000–6,000 (lowest input cost after pearl millet)", "key_risk": "Helicoverpa pod borer (critical), wilt (soil-borne), rust in humid areas", "suitable_states": "MP, Rajasthan, Maharashtra, UP, Karnataka, AP", }, "rice_paddy": { "keywords": ["rice","paddy","dhan","dhaan","chawal"], "season": "Kharif (June–July sowing; harvest Oct–Nov)", "duration_days": "110–135d depending on variety", "rainfall_needed_mm": "1,000–1,500mm OR irrigated with 3–4 irrigations/week in dry spells", "drought_tolerance": "LOW — requires standing water at tillering to heading", "waterlog_tolerance": "HIGH — paddy thrives in standing water (2–5 cm depth)", "yield_rainfed_q_acre": "10–15 q/acre (direct-seeded, monsoon)", "yield_irrigated_q_acre": "20–30 q/acre (transplanted, irrigated)", "soil_best": "Clayey / loam with good water retention; paddy fields with bund", "soil_avoid": "Sandy soils (lose water too fast), highly acidic (pH < 5.5)", "water_risk": "MEDIUM for eastern states (rainfed); LOW for Punjab/Haryana (canal-irrigated)", "profit_potential": "MEDIUM — guaranteed MSP Rs 2,300/q; major staple; assured procurement but low margin", "input_cost_per_acre": "Rs 8,000–16,000 (transplanted) / Rs 5,000–8,000 (direct-seeded)", "key_risk": "Blast disease (cloudy/rainy), BPH (brown plant hopper), Sheath blight, Flood damage (deep water variety needed)", "suitable_states": "West Bengal, UP, Bihar, Odisha, Andhra Pradesh, Tamil Nadu, Chhattisgarh, Punjab (DSR)", }, "tomato": { "keywords": ["tomato","tamatar","tamater"], "season": "Rabi (Aug–Sep nursery → Oct–Nov transplant) OR Summer (Dec–Jan → Feb–March)", "duration_days": "70–90d (from transplant)", "rainfall_needed_mm": "400–600mm well-distributed; drip preferred", "drought_tolerance": "LOW — highly sensitive to drought at flowering and fruit set", "waterlog_tolerance": "VERY LOW — Phytophthora wilt within 24–48h of flooding", "yield_rainfed_q_acre": "40–60 q/acre", "yield_irrigated_q_acre": "80–120 q/acre (hybrid, drip + fertigation)", "soil_best": "Sandy loam / loam with good drainage; slightly acidic (pH 6–7)", "soil_avoid": "Heavy clay / waterlogged / very alkaline soils", "water_risk": "HIGH — needs consistent moisture; drought at fruit set = blossom drop; rain at harvest = fruit cracking", "profit_potential": "HIGH but VOLATILE — Rs 8–40/kg (huge price swings); can give Rs 50,000+/acre in good season OR near-zero in glut", "input_cost_per_acre": "Rs 20,000–35,000 (nursery + transplanting + staking + spray)", "key_risk": "Extreme price volatility, Tomato leaf curl virus (TLCuV), Early blight, Late blight in humid weather", "suitable_states": "Karnataka, Maharashtra, AP, MP, UP, Bihar, HP (hills)", }, "maize_corn": { "keywords": ["maize","corn","makka","makki","bhutta"], "season": "Kharif (June–July) OR Rabi (Oct–Nov, South India) OR Spring (Feb–March)", "duration_days": "90–110d (hybrid Kharif); 120d (Rabi)", "rainfall_needed_mm": "500–800mm well-distributed", "drought_tolerance": "Medium (sensitive at tasseling/silking — 7-day window)", "waterlog_tolerance": "LOW — tassel stage very sensitive; waterlogging causes barren ears", "yield_rainfed_q_acre": "10–16 q/acre (hybrid)", "yield_irrigated_q_acre": "20–28 q/acre", "soil_best": "Well-drained loam / sandy loam; pH 6–7.5", "soil_avoid": "Waterlogged / very heavy clay / highly acidic", "water_risk": "HIGH at silking — single week of drought causes 50-70% yield loss; otherwise medium risk", "profit_potential": "MEDIUM — Rs 1,800–2,500/q; poultry feed demand is stable; baby corn premium (Rs 4,000+/q)", "input_cost_per_acre": "Rs 8,000–14,000", "key_risk": "Fall armyworm (new invasive — major threat), stem borer, charcoal rot in drought; not MSP-supported everywhere", "suitable_states": "Karnataka, AP, Bihar, MP, UP, Maharashtra, Telangana", }, "sugarcane_ganna": { "keywords": ["sugarcane","ganna","ikshu","ikh"], "season": "Spring: Feb–March planting (main, 12 months); Autumn: Sep–Oct (11 months)", "duration_days": "10–14 months (spring); 8–10 months (autumn ratoon)", "rainfall_needed_mm": "1,500–2,500mm OR 50–60 irrigations (North India)", "drought_tolerance": "LOW — heavy water consumer; needs regular supply", "waterlog_tolerance": "Medium — tolerates 3–5 days flooding; NOT chronic waterlogging", "yield_rainfed_q_acre": "200–250 q/acre (Maharashtra, good rainfall)", "yield_irrigated_q_acre": "350–450 q/acre (North India, irrigated)", "soil_best": "Deep loam / clay loam with good water retention; pH 6.5–7.5", "soil_avoid": "Shallow soils, waterlogged fields, very sandy", "water_risk": "MEDIUM for assured irrigated areas; HIGH for drought-prone Maharashtra", "profit_potential": "HIGH but payment DELAYED — Rs 290–340/q (state-announced SAP); mill payment delayed 6–18 months is common", "input_cost_per_acre": "Rs 25,000–45,000 (highest input crop — heaviest feeder)", "key_risk": "Mill payment delays, ratoon stunting disease, red rot, pink bollworm; capital locked for 12+ months", "suitable_states": "UP, Maharashtra, Karnataka, AP, Tamil Nadu, Gujarat, Haryana", }, "arhar_pigeonpea": { "keywords": ["arhar","tur","toor","pigeonpea","red gram","cajanus"], "season": "Kharif (May–June for long-duration; June–July for short-duration)", "duration_days": "120–150d (short-duration); 200–240d (long-duration Maruti type)", "rainfall_needed_mm": "600–1,000mm; deep tap root uses subsoil moisture", "drought_tolerance": "HIGH — deep roots access subsoil moisture after rains stop", "waterlog_tolerance": "Medium — can tolerate brief 2–3 day flooding at vegetative stage", "yield_rainfed_q_acre": "4–7 q/acre (rainfed; highly variable)", "yield_irrigated_q_acre": "8–12 q/acre", "soil_best": "Medium deep red/black soils; pH 6–8; well-drained", "soil_avoid": "Waterlogged / very sandy / acidic soils", "water_risk": "LOW-MEDIUM — adapts to dry spells; sensitive only at flowering", "profit_potential": "HIGH — Rs 6,000–8,000/q; strong household dal demand; MSP supported; dual use (pods + green manure)", "input_cost_per_acre": "Rs 5,000–9,000", "key_risk": "Helicoverpa pod borer (most damaging), Phytophthora stem blight post-flood, price crash if national surplus", "suitable_states": "Maharashtra, UP, MP, Karnataka, AP, Gujarat, Bihar", }, "groundnut": { "keywords": ["groundnut","peanut","moongfali"], "season": "Kharif (June 15–July 15) OR Summer (Feb–March, irrigated)", "duration_days": "100–110d (bunch); 120–130d (spreading)", "rainfall_needed_mm": "450–600mm well-distributed", "drought_tolerance": "High (deep roots) but sensitive at pegging", "waterlog_tolerance": "VERY LOW — pod zone flooding = aflatoxin risk", "yield_rainfed_q_acre": "7–10 q pods/acre", "yield_irrigated_q_acre": "12–16 q pods/acre", "soil_best": "Sandy loam / well-drained light soils; Gujarat, Rajasthan", "soil_avoid": "Heavy clay / waterlogged soils", "water_risk": "MEDIUM — critical water at pegging; excess rain = aflatoxin contamination", "profit_potential": "HIGH — Rs 5,000–7,000/q; confectionery/oil demand; export potential", "input_cost_per_acre": "Rs 10,000–16,000 (high seed cost)", "key_risk": "Aflatoxin (storage), collar rot (post-flood), late leaf spot, Tikka disease", "suitable_states": "Gujarat, Rajasthan, AP, Karnataka, Tamil Nadu", }, "sunflower": { "keywords": ["sunflower","surajmukhi","surjmukhi"], "season": "Rabi (Oct–Nov sowing) OR Zaid/Summer (Feb–March, irrigated)", "duration_days": "90–100d", "rainfall_needed_mm": "400–600mm OR 4–6 irrigations (Rabi/Zaid = irrigated crop)", "drought_tolerance": "Medium (deep taproot) — but sensitive at head-filling stage", "waterlog_tolerance": "LOW — avoid standing water after sowing", "yield_rainfed_q_acre": "5–7 q/acre (Kharif rainfed)", "yield_irrigated_q_acre": "8–12 q/acre (Rabi/Zaid irrigated)", "soil_best": "Sandy loam / medium loam; pH 6.5–8; well-drained", "soil_avoid": "Very heavy clay / saline / waterlogged soils", "water_risk": "MEDIUM — critical water at flowering and seed fill; bird damage can cut yield 20–30%", "profit_potential": "MEDIUM-HIGH — MSP Rs 7,280/q (2024-25); oil demand growing; reliable procurement in AP/Karnataka", "input_cost_per_acre": "Rs 5,500–8,500", "key_risk": "Bird damage (major — net/scare needed), head rot (Alternaria), Sclerotinia wilt, price volatile", "suitable_states": "Karnataka, Andhra Pradesh, Maharashtra, Odisha, Bihar, Rajasthan", }, "sesame": { "keywords": ["sesame","til","gingelly","tilli"], "season": "Kharif (June–July) OR Zaid/Summer (Feb–March, short season)", "duration_days": "75–90d", "rainfall_needed_mm": "250–400mm (very drought tolerant — MINIMUM water crop)", "drought_tolerance": "HIGH — one of India's most drought-tolerant crops", "waterlog_tolerance": "VERY LOW — even 24h waterlogging causes total failure", "yield_rainfed_q_acre": "3–5 q/acre", "yield_irrigated_q_acre": "5–7 q/acre", "soil_best": "Sandy loam / loamy sand / light well-drained soils; pH 5.5–8", "soil_avoid": "Heavy clay / waterlogged / poorly drained soils — FATAL", "water_risk": "LOW (drought side) but VERY HIGH for waterlogging — NEVER sow in low-lying fields", "profit_potential": "HIGH — MSP Rs 9,267/q (2024-25; highest oilseed MSP); strong export demand; low input cost", "input_cost_per_acre": "Rs 4,000–6,000 (lowest-cost oilseed crop)", "key_risk": "Phyllody disease (mycoplasma via leafhopper — use Imidacloprid), very low national average yield, market price volatile", "suitable_states": "Rajasthan, Gujarat, Maharashtra, Andhra Pradesh, Odisha, West Bengal, Madhya Pradesh", }, } # Build keyword lookup for crop decision data _CROP_DECISION_KW_MAP: dict[str, str] = {} for _cdk, _cdv in _CROP_DECISION_DATA.items(): for _kw in _cdv["keywords"]: _CROP_DECISION_KW_MAP[_kw] = _cdk def build_crop_decision_context(query: str, detected_state: str | None = None) -> str: """ For crop_selection queries: inject ICAR economic + agronomic profiles of candidate crops so LLM gives a comparison table with REAL numbers. Injects profiles for crops that appear in query OR are suitable for state. """ q_lower = query.lower() # Detect which specific crops are mentioned mentioned: list[str] = [] for kw in sorted(_CROP_DECISION_KW_MAP, key=len, reverse=True): if kw in q_lower: ck = _CROP_DECISION_KW_MAP[kw] if ck not in mentioned: mentioned.append(ck) # If no specific crops mentioned, pick top 4 for the state/season if not mentioned: # Default comparison set based on common queries mentioned = ["soybean", "pearl_millet_bajra", "moong", "gram_chickpea"] lines = ["━━━ PRIORITY CONTEXT — CROP DECISION DATA (use these EXACT numbers) ━━━"] for ck in mentioned[:4]: # max 4 crops in table p = _CROP_DECISION_DATA.get(ck) if not p: continue lines.append( f"\n📊 {ck.upper().replace('_',' ')}:\n" f" Season: {p['season']} | Duration: {p['duration_days']}\n" f" Water need: {p['rainfall_needed_mm']} | Drought: {p['drought_tolerance']} | Waterlog: {p['waterlog_tolerance']}\n" f" Yield rainfed: {p['yield_rainfed_q_acre']} | Irrigated: {p['yield_irrigated_q_acre']}\n" f" Best soil: {p['soil_best']}\n" f" Profit: {p['profit_potential']}\n" f" Input cost: {p['input_cost_per_acre']}\n" f" Key risk: {p['key_risk']}\n" f" Best states: {p['suitable_states']}" ) lines.append( "\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" "Use these EXACT numbers in your comparison table. " "Always include: Duration | Water Need | Drought Tolerance | Yield | Profit Risk | Verdict.\n" "Give ONE clear winner at the end with a single-line reason." ) return "\n".join(lines) # ── Variety context injector for Before-Sowing AI Advisor ──────────────────── _VARIETY_CROP_KW: dict[str, str] = { # maps query keyword → _VARIETY_DATA key "gehu": "Wheat", "wheat": "Wheat", "गेहूं": "Wheat", "dhan": "Rice", "paddy": "Rice", "rice": "Rice", "dhaan": "Rice", "soybean": "Soybean", "soya": "Soybean", "bhat": "Soybean", "cotton": "Cotton", "kapas": "Cotton", "narma": "Cotton", "maize": "Maize", "makka": "Maize", "corn": "Maize", "mustard": "Mustard", "sarson": "Mustard", "sarson": "Mustard", "groundnut": "Groundnut", "moongfali": "Groundnut", "peanut": "Groundnut", "arhar": "Arhar", "tur": "Arhar", "pigeon": "Arhar", "gram": "Gram", "chana": "Gram", "chickpea": "Gram", "bajra": "Bajra", "pearl millet": "Bajra", "bajri": "Bajra", "moong": "Moong", "green gram": "Moong", "mung": "Moong", "urad": "Urad", "black gram": "Urad", "urd": "Urad", "sunflower": "Sunflower", "surajmukhi": "Sunflower", "surjmukhi": "Sunflower", "sesame": "Sesame", "til": "Sesame", "gingelly": "Sesame", "tilli": "Sesame", "onion": "Onion", "pyaz": "Onion", "kanda": "Onion", "tomato": "Tomato", "tamatar": "Tomato", } _VARIETY_QUERY_SIGNALS = re.compile( r"variety|varieties|variet|beej|seed|kism|किस्म|bona|lagaun|lagana|" r"konsa lagaun|kya lagaun|best crop|which crop|kaunsi fasal|sow|buwai|" r"which variety|konsi variety|sabse achhi|best variety|recommended", re.IGNORECASE, ) def build_variety_context(query: str, detected_state: str | None = None) -> str: """ Injects CURRENT ICAR variety data from _VARIETY_DATA into the LLM prompt for Before-Sowing queries. Prevents the LLM from falling back to old KCC records with obsolete varieties (Pankaj, Hunga, etc.). Called only for crop_selection / variety queries in the Before-Sowing tab. """ q_lower = query.lower() # Detect which crops the farmer is asking about detected_crops: list[str] = [] for kw in sorted(_VARIETY_CROP_KW, key=len, reverse=True): if kw in q_lower: crop = _VARIETY_CROP_KW[kw] if crop not in detected_crops: detected_crops.append(crop) if not detected_crops: return "" # no specific crop mentioned — skip injection lines = ["━━━ ICAR VARIETY REFERENCE (2024-25) — use ONLY these varieties ━━━", "⚠️ CRITICAL: The KCC database may contain OLD varieties (pre-2015). " "IGNORE them. Use ONLY the varieties listed below.\n"] for crop in detected_crops[:3]: # max 3 crops varieties = _VARIETY_DATA.get(crop, []) if not varieties: continue # Filter to state if possible if detected_state: state_vars = [v for v in varieties if detected_state in v.get("states", []) or "All states" in v.get("states", [])] show = state_vars[:3] if state_vars else varieties[:3] else: show = varieties[:3] lines.append(f"🌾 {crop.upper()} — Recommended varieties:") for v in show: lines.append( f" • {v['name']}: {v['days']} days, yield {v['yield']} q/acre, " f"seed rate {v['seed_rate']} | {v['traits']} | Buy: {v['source']}" ) lines.append("") if len(lines) <= 3: # nothing useful added return "" lines.append("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━") return "\n".join(lines) # Keyword lookup for irrigation crop detection _IRR_CROP_KW_MAP: dict[str, str] = {} for _ick, _icv in _ICAR_IRRIGATION_CARDS.items(): for _kw in _icv["keywords"]: _IRR_CROP_KW_MAP[_kw] = _ick def detect_irrigation_query(query: str) -> bool: """Return True if this is an irrigation/water management query.""" _IRR_TRIGGERS = {"drip","sinchai","paani dena","pani dena","kitna paani","kitna pani", "irrigation","water requirement","litres per","litre per","sprinkler", "furrow","flood irrigation","per day water","per din paani","pump", "drip line","emitter","micro irrigation","trickle","seepage"} q = query.lower() return any(t in q for t in _IRR_TRIGGERS) def build_irrigation_context(query: str, crop: str | None = None) -> str: """ Inject ICAR irrigation knowledge card when farmer asks about water/irrigation. Detects crop from query or uses detected_crop. Returns empty string if no card found. """ q_lower = query.lower() crop_key = None # Try to find crop from irrigation card keywords for kw in sorted(_IRR_CROP_KW_MAP, key=len, reverse=True): if kw in q_lower: crop_key = _IRR_CROP_KW_MAP[kw] break # Fallback to detected_crop if not crop_key and crop: cl = crop.lower() for kw, ck in _IRR_CROP_KW_MAP.items(): if kw in cl: crop_key = ck break if not crop_key: return ( "━━━ PRIORITY CONTEXT — ICAR IRRIGATION GUIDELINES ━━━\n" "No crop-specific card available. General guidance:\n" "Drip irrigation: 2–6 L/plant/day depending on crop and stage.\n" "Flood irrigation: 5–8 cm depth; frequency depends on soil and season.\n" "IMPORTANT: Ask farmer which crop and irrigation method before giving exact numbers.\n" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" ) card = _ICAR_IRRIGATION_CARDS[crop_key] return ( f"━━━ PRIORITY CONTEXT — ICAR IRRIGATION DATA FOR {crop_key.upper()} ━━━\n" f"Drip requirement: {card['drip_lpd']}\n" f"Stage-wise schedule: {card['stages']}\n" f"Flood/furrow: {card['flood']}\n" f"Most critical stage: {card['critical']}\n" f"Pro tip: {card['tip']}\n" f"Source: {card['source']}\n" f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" f"Use these EXACT numbers. Do NOT estimate or say 'it depends' without numbers.\n" ) def detect_named_disease(query: str) -> str | None: """ Detect if the farmer explicitly named a specific disease or pest. Returns the disease key (e.g. 'rust', 'aphid') or None. Checks longest keyword first to prefer specific matches over generic ones. """ q_lower = query.lower() for kw in sorted(_DISEASE_KW_MAP, key=len, reverse=True): if kw in q_lower: return _DISEASE_KW_MAP[kw] return None def get_icar_priority_context(query: str, crop=None, problem_type=None, state=None) -> str: """ Semantic ICAR retrieval for Tab 1 (Before Sowing) and other tabs. Uses the new ICAR KB FAISS index — returns expert context or empty string. """ if not _ICAR_AVAILABLE or _ICAR_RETRIEVER is None: return "" try: results = _ICAR_RETRIEVER.search(query, top_k=2) if results: return _ICAR_RETRIEVER.format_for_llm(results) + "\n\n" except Exception: pass return "" def build_icar_context(disease_key: str, crop: str | None = None) -> str: """ Format an ICAR disease card as a priority context block for the LLM prompt. This is injected ABOVE retrieved KCC records so the LLM gets ground-truth ICAR treatment data even when retrieval returns generic records. """ card = _ICAR_DISEASE_CARDS.get(disease_key) if not card: return "" crop_note = f" in {crop}" if crop else "" return ( f"━━━ PRIORITY CONTEXT — ICAR VALIDATED TREATMENT (use this first) ━━━\n" f"Named condition{crop_note}: {card['diagnosis']}\n" f"TREATMENT: {card['treatment']}\n" f"DOSE: {card['dose']}\n" f"TIMING: {card['timing']}\n" f"IPM / Prevention: {card['ipm']}\n" f"Source: {card['source']}\n" f"PPE MANDATORY: Wear gloves, mask, full-sleeve clothing when spraying.\n" f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n" f"NOTE: The KCC records below are supplementary context. The ICAR card above\n" f"takes precedence for the farmer's explicitly named condition.\n" ) def detect_crop(query: str) -> str | None: """ Detect the crop name from a query using keyword matching. Returns the DB crop name (e.g. 'Wheat') or None if not detected. """ q_lower = query.lower() # Check each keyword; longer matches take priority (sorted by length desc) for kw in sorted(CROP_KEYWORDS, key=len, reverse=True): if kw in q_lower: return CROP_KEYWORDS[kw] return None # ── Language Normalizer (#7) ────────────────────────────────────────────────── # Appends English translations to Hindi/transliterated terms so the # multilingual embedding model retrieves more relevant results. HINDI_NORMALIZER: dict[str, str] = { # Symptoms "pilli": "yellow leaves", "peeli": "yellow leaves", "pili": "yellow leaves", "patte": "leaves", "pattiya": "leaves", "pattiyan": "leaves", "daag": "spots lesions", "dhabbe": "spots marks", "dhabb": "spots", "murjha": "wilting", "murjhana": "wilting", "sukh": "drying", "jhad": "falling dropping", "jhadna": "falling", "girna": "falling", "gadna": "rotting", "sadna": "rotting", "galna": "rotting", "safed": "white", "kala": "black", "laal": "red", "bhura": "brown", "peela": "yellow", # Pests / insects "keeda": "insect pest", "kide": "insects pests", "kirmi": "worm", "borer": "stem borer insect","sundi": "caterpillar", "mahu": "aphid", "tikka": "spot disease", "tela": "aphid", "titli": "moth butterfly", "makhi": "fly", "safed makhi": "whitefly", # Diseases "rog": "disease", "bimari": "disease", "jhulsa": "blight", "rust": "rust fungal", "karela": "bitter gourd", # Deficiencies "kami": "deficiency", "poshan": "nutrition", # Actions "badhana": "increase production yield", "bachana": "protect prevention control", "control": "control management treatment", "upchar": "treatment cure", "dawai": "medicine pesticide", "spray": "spray application", # Weather "pala": "frost cold damage", "thand": "cold frost", "baarish": "rain flood", "sukha": "drought dry", # Misc "fasal": "crop", "khet": "field farm", "mausam": "season weather", "beej": "seed", "khaad": "fertilizer", "sinchai": "irrigation", } def normalize_query(query: str) -> str: """ Append English translations of Hindi agricultural terms to the query. The original query is kept intact; English terms are appended so the embedding model retrieves more semantically relevant KCC pairs. """ q_lower = query.lower() additions: list[str] = [] for hindi, english in HINDI_NORMALIZER.items(): if hindi in q_lower and english not in q_lower: additions.append(english) if additions: return query + " " + " ".join(additions) return query # ── Problem Classifier (#1) ─────────────────────────────────────────────────── # Rule-based (zero LLM cost). Classifies query into problem type so # safety guardrails and retrieval can be tuned accordingly. PROBLEM_KEYWORDS: dict[str, list[str]] = { "pest": [ "keeda", "kide", "kirmi", "borer", "sundi", "mahu", "tela", "makhi", "safed makhi", "titli", "moth", "aphid", "thrip", "mite", "whitefly", "insect", "pest", "caterpillar", "larva", "bug", "टिड्डी", "माहू", "grasshopper", "locust", "weevil", "nematode", ], "disease": [ "rog", "bimari", "daag", "dhabbe", "jhulsa", "blight", "rust", "rot", "mildew", "fungal", "fungus", "bacterial", "virus", "tikka", "blast", "mosaic", "wilt", "canker", "scab", "smut", "spots", "lesion", ], "nutrient": [ "peeli", "pilli", "yellow", "kami", "deficiency", "poshan", "khaad", "fertilizer", "urea", "npk", "zinc", "nitrogen", "phosphorus", "potassium", "micronutrient", "pallor", "chlorosis", ], "yield": [ "paidavar", "utpadan", "increase", "badhana", "improve", "variety", "production", "output", "harvest", "paidal", "zyada", ], "weather": [ "pala", "thand", "frost", "cold", "baarish", "flood", "drought", "sukha", "heat", "garmi", "hail", "ola", "andhi", "storm", "waterlogged", "waterlog", "paani bhar", "jal bhar", "drainage", "khet mein paani", "water stagnant", "nali", "drain", ], "organic": [ "organic","jaivik","jaivik kheti","prakritic kheti","natural farming","zero budget", "zbnf","neem khad","vermicompost","gobar gas","cow dung","compost","jeevamrit", "beejamrit","panch gavya","panchgavya","bio pesticide","biopesticide","bio fertilizer", "trichoderma","pseudomonas","rhizobium","psb","ksb","biological control","biocontrol", "neem oil","neem cake","organic certification","pgpr","am fungi","mycorrhiza", ], "post_harvest": [ "store","storage","store karna","bhandan","bhndaran","anaj rakhna","godam", "warehouse","silage","drying","sukhaana","moisture","aardrata","nami", "pest in storage","weevil storage","ghun","storage fungus","aflatoxin", "cold storage","refrigerate","grading","sorting","packaging","pack", "shelf life","kitne din chalega","kitne mahine","how long to store", "gehun store","dhan store","dal store","onion store","potato store", ], "seed_treatment": [ "seed treatment","beej upchar","beej uchar","seed treat","seed coating", "beej shod","fungicide seed","thiram","captan","carbendazim seed", "imidacloprid seed","rhizobium seed","psb seed","trichoderma seed", "beej shodhan","before sowing","preplanting treatment","seed dressing", ], "agronomy": [ "seed rate", "beej dar", "bij dar", "row spacing", "plant spacing", "doori", "kab lagaye", "kab lgaye", "when to sow", "when to plant", "kab boya", "sowing depth", "transplant time", "delayed monsoon", "monsoon delay", "monsoon late", "monsoon fail", "baarish nahi", "paani kam", "intercrop", "mixed crop", "companion crop", "relay crop", "thinning", "weeding", "mulching", "earthing up", "earthing", "spacing reduce", "spacing increase", "plant population", "kab kaate", "when to harvest", "maturity days", "crop duration", ], "crop_selection": [ # Hindi/Hinglish "kya lagaye", "kya ugaye", "kaunsi fasal", "kya boya", "crop selection", "variety selection", "kaun si", "konsi", "laga sakte", "laga sakta", "lagana chahiye", "kya lagau", "kya ugaun", "kya bohun", "is mausam", "is season", "abhi kya", "suitable crop", "konsi kheti", "kaunsa beej", "kya fasal", "fasal chunav", "kaun sa bij", "behtar fasal", "kaun si kheti", "kaun sa crop", "soybean ya", "cotton ya", "gehu ya", "dhan ya", "ya soybean", "ya cotton", "ya wheat", "ya paddy", # English comparison patterns "which crop", "which variety", "which is better", "should i go for", "should i grow", "should i plant", "better option", "better crop", "crop choice", "cotton or soybean", "soybean or cotton", "wheat or mustard", "cotton or", "soybean or", "wheat or", "rice or", "maize or", "or cotton", "or soybean", "or wheat", "or rice", "or maize", "vs cotton", "vs soybean", "vs wheat", "vs rice", "cotton vs", "soybean vs", "wheat vs", "paddy vs", "best crop for", "suitable for my", "recommend crop", "what to grow", "what should i grow", "what to plant", "what crop", "go for cotton", "go for soybean", "go for wheat", "better profit", "more profit crop", "profitable crop", "which gives more", "which is more profitable", "switch to", "change to crop", "alternative crop", ], "irrigation": [ "sinchai", "drip", "paani dena", "pani dena", "kitna paani", "kitna pani", "irrigation", "water requirement", "watering", "flow rate", "litres per day", "litre per plant", "sprinkler", "furrow", "flood irrigation", "micro irrigation", "per din paani", "per day water", "pump", "drip line", "emitter", "drip system", "trickle", "seepage", "suraksha sinchai", ], "scheme": [ "pm kisan", "kisan samman", "fasal bima", "pmfby", "scheme", "yojana", "subsidy", "subsidi", "anudan", "kisan credit", "kcc loan", "loan", "pradhan mantri", "sarkar yojana", "government scheme", "apply kaise", "registration kaise", "form kaise", "beneficiary", "kisan vikas patra", "soil health card", "mgnrega", "rashtriya krishi", "rkvy", "agriculture loan", ], } def classify_problem(query: str) -> str: """ Classify the farmer's query into a problem type using keyword matching. Returns one of: pest, disease, nutrient, yield, weather, crop_selection, general. """ q_lower = query.lower() scores: dict[str, int] = {k: 0 for k in PROBLEM_KEYWORDS} for ptype, keywords in PROBLEM_KEYWORDS.items(): for kw in keywords: if kw in q_lower: scores[ptype] += 1 best = max(scores, key=lambda k: scores[k]) return best if scores[best] > 0 else "general" # ── ★ Agriculture topic guard ───────────────────────────────────────────────── # Fast pre-filter before FAISS retrieval + LLM generation. # Saves compute and prevents the model from answering non-agriculture questions. _AGRI_SIGNALS = { "crop","plant","seed","soil","fertilizer","pesticide","pest","disease", "farm","farmer","kisan","kheti","fasal","gehu","gehun","dhan","kapas", "tamatar","aloo","pyaz","mandi","bhav","rate","harvest","sow","sowing", "irrigation","spray","insecticide","fungicide","organic","yield", "wheat","rice","paddy","cotton","maize","sugarcane","soybean","mustard", "chilli","brinjal","onion","tomato","potato","groundnut","gram","arhar", "aphid","borer","blight","mildew","rust","wilt","thrips","mite","whitefly", "caterpillar","jassid","leaf","rot","fungus","virus","bacteria", "khaad","dawai","beej","sinchai","pattiya","keeda","bimari","rog", "upchar","khet","paidavar","safed","makhi","tela","mahu","tikda", "pala","thand","frost","baarish","drought","sukha","ola","flood", "waterlog","waterlogged","jaldhar","drainage","drain","paani bhar gaya", "jal bhar","flooding","khet mein paani","field water","nali","water stagnant", "kvk","icar","kcc","variety","beej","nursery","transplant","cutting", "seed rate","spacing","doori","row spacing","kab lagaye","when to sow", "intercrop","mixed crop","mulch","thinning","weeding","earthing", "agronomy","agronomic","delayed monsoon","monsoon delay","monsoon late", # Organic farming "jaivik","prakritic","jeevamrit","beejamrit","panchgavya","vermicompost", "neem cake","gobar","cow dung","organic","zero budget","zbnf","trichoderma", "rhizobium","psb","ksb","bio pesticide","biopesticide","biocontrol", # Post-harvest "store","storage","godam","bhandan","anaj rakhna","moisture","nami","aardrata", "drying","sukhaana","grading","packaging","shelf life","cold storage", "hermetic","pusa bin","fumigation","ghun","weevil","aflatoxin", # Seed treatment "seed treatment","beej upchar","beej shodhan","thiram","beejamrit", } _NON_AGRI_RE = re.compile( r"\b(stock market|share market|bitcoin|crypto|politics|election|" r"movie|cricket|football|recipe|cooking|exam|bank account|loan|" r"insurance claim|marriage|divorce|love|dating|salary|job|" r"celebrity|news|tv show|web series|ipl|bollywood|actor|actress|" r"girlfriend|boyfriend|password|hack|war|army|defence)\b", re.IGNORECASE, ) # ── Harmful non-farm query detector ────────────────────────────────────────── # Catches queries that explicitly say "not for farm use" / "for home use" but # ask about harmful topics (poisons, chemicals for harming people/animals). # These pass is_agriculture_query() because "farm" appears in the query, # but they're NOT legitimate agricultural queries and must be refused. _HARMFUL_NON_FARM_RE = re.compile( r"not\s+for\s+farm|not\s+for\s+agri|not\s+farming|" r"ghar\s+ke\s+liye\s+(?:zeher|poison|dawa|chemical)|" r"non.agri(?:cultural)?\s+use|outside\s+(?:farm|agriculture|agri)\b", re.IGNORECASE, ) _HARM_SIGNAL_RE = re.compile( r"\b(poison|kill|mar|zeher|toxic|deadly|harm|hurt|weapon|murder)\b", re.IGNORECASE, ) # Second arm: mass-harm / collective-kill language — dangerous even WITHOUT # "not for farm" qualifier (e.g. "maar de", "sab khatam kar", "sabko zeher do") _MASS_HARM_RE = re.compile( r"maar\s+de|sabko\s+(?:maar|zeher|khatam|nasha)|" r"sab\s+(?:khatam\s+kar|maar\s+daal|ko\s+zeher)|" r"puri\s+(?:abadi|colony|gaon)\s+(?:ko\s+)?(?:maar|zeher|khatam)|" r"logo[n]?\s+ko\s+(?:maar|zeher|nasha|jaan\s+se)|" r"paan?i\s+mein\s+(?:zeher|poison|dawa)\s+(?:mila(?:na|o)?|dal(?:na|o)?)|" r"(?:kuein|kuan|well|water\s+supply|naali)\s+(?:mein\s+)?(?:zeher|poison)|" r"zeher\s+(?:dalna|milana|daalo|milao)\s+(?:mein\s+)?(?:pani|paani|kuein|kuan|naali)|" r"how\s+to\s+(?:poison\s+(?:a\s+)?(?:well|water|people|village)|kill\s+(?:people|humans|someone|villagers))|" r"insaan\s+(?:maar|khatam|zeher)|" r"gaon\s+ke\s+(?:log|lok|insaan)\s+(?:maar|zeher|khatam)|" r"ghareloo\s+(?:use|istemal|upyog)\s+(?:ke\s+liye\s+)?(?:zeher|aluminium|phosphide|deadly|hanikarak)", re.IGNORECASE, ) def is_harmful_non_farm_query(query: str) -> bool: """ Returns True if the query requests harmful info that is not legitimate farming advice. Arm 1 (original): "not for farm use" + harm signal → e.g. rat poison for home use Arm 2 (new): Mass-harm / collective-kill language → e.g. "maar de", "sabko zeher" These are dangerous regardless of whether "farm" appears. """ q = query.strip() # Arm 1: explicit non-farm qualifier + harm signal if _HARMFUL_NON_FARM_RE.search(q) and _HARM_SIGNAL_RE.search(q): return True # Arm 2: mass-harm / collective-kill language (always dangerous) if _MASS_HARM_RE.search(q): return True return False OFF_TOPIC_RESPONSE = ( "🌾 Main sirf kheti-baadi, fasal, keed-bimari, khaad, mandi bhav, " "aur kisan-sambandhit sawalon ka jawab de sakta hoon.\n\n" "Kripya apna kheti-sambandhit sawaal poochhein — main zaroor madad karoonga!" ) def is_agriculture_query(query: str) -> bool: """ Returns True if the query is agriculture-related. Short follow-ups (≤3 words) always pass — they're conversational replies. """ stripped = query.strip() # Very short → allow (continuation of conversation) if len(stripped.split()) <= 3: return True # Devanagari script → almost certainly an Indian farmer query if any('ऀ' <= c <= 'ॿ' for c in stripped): if _NON_AGRI_RE.search(stripped.lower()): return False return True q_lower = stripped.lower() # Hard non-agriculture + no agriculture override → reject if _NON_AGRI_RE.search(q_lower): words = set(re.findall(r"\b\w+\b", q_lower)) if words.intersection(_AGRI_SIGNALS): return True # e.g. "cotton market rate" has both signals return False # Any agriculture signal → allow words = set(re.findall(r"\b\w+\b", q_lower)) return bool(words.intersection(_AGRI_SIGNALS)) # ── Chemical Safety Guardrails (#6) ─────────────────────────────────────────── # Injected into the prompt so the LLM knows exactly which chemical class # is appropriate — preventing the carbendazim-for-stem-borer mistake. SAFETY_GUARDRAILS: dict[str, str] = { "pest": ( "SAFETY RULE — PEST/INSECT PROBLEM DETECTED: " "Recommend INSECTICIDES only (e.g. Chlorpyrifos, Imidacloprid, " "Quinalphos, Malathion, Cypermethrin). " "DO NOT recommend fungicides (Mancozeb, Carbendazim, Propiconazole, " "Copper oxychloride) for controlling the pest." ), "disease": ( "SAFETY RULE — FUNGAL/BACTERIAL DISEASE DETECTED: " "Recommend FUNGICIDES or bactericides only (e.g. Mancozeb, Carbendazim, " "Copper oxychloride, Propiconazole). " "DO NOT recommend insecticides for treating the disease." ), "nutrient": ( "SAFETY RULE — NUTRIENT DEFICIENCY DETECTED: " "Recommend FERTILIZERS and micronutrients only (Urea, DAP, MOP, " "Zinc Sulphate, Ferrous Sulphate, Boron). " "Do NOT recommend pesticides for nutrient problems." ), "weather": ( "SAFETY RULE — WEATHER/ABIOTIC STRESS DETECTED: " "Recommend protective measures (light irrigation for frost, drainage for " "flooding/waterlogging). Do NOT recommend pesticides for weather damage. " "For waterlogged fields: drainage first → withhold fertilizer → check roots." ), "agronomy": ( "SAFETY RULE — AGRONOMY QUERY: " "Give principle-based advice tied to the farmer's soil/water/season context. " "Never give generic percentages without explaining why. " "For waterlogged fields: drainage is always Step 1. " "For delayed monsoon: recommend shorter-duration crop varieties." ), "crop_selection": ( "CRITICAL GUARD — CROP SELECTION QUERY: " "Farmer is asking WHICH CROP TO GROW — do NOT diagnose any pest or disease. " "Do NOT assume farmer already chose a crop. " "Do NOT quote ₹ prices from retrieved KCC context (those are old historical data). " "Compare crops using: water need + duration + soil fit + current season window. " "Always redirect price questions to the Mandi Prices tab." ), "organic": ( "ORGANIC FARMING GUARD: Farmer is asking about organic/natural farming. " "DO NOT recommend ANY synthetic chemical pesticide or fertilizer. " "Stick to bio-inputs: Neem oil, Jeevamrit, Trichoderma, FYM, vermicompost, PSB, Rhizobium. " "For disease: Copper Oxychloride is permitted (it is NPOP-approved). " "Mention that chemicals will disqualify organic certification." ), "post_harvest": ( "POST-HARVEST GUARD: Give storage-specific advice. " "Always mention MOISTURE % target for safe storage. " "Mention appropriate container type. " "Never recommend raw Aluminium Phosphide to farmer — say 'contact licensed fumigator'. " "Duration advice must be specific (months, not vague 'long time')." ), "seed_treatment": ( "SEED TREATMENT GUARD: Give the 3-step sequence: chemical → dry → biological. " "NEVER mix Rhizobium/Trichoderma with Thiram/Captan in same application — chemicals kill bacteria. " "Always mention seeds should be sown within 24 hours of treatment. " "Treated seeds should NOT be stored — treat just before sowing." ), } # ───────────────────────────────────────────────────────────────────────────── # INFRASTRUCTURE FUNCTIONS — reconstructed # ───────────────────────────────────────────────────────────────────────────── _RESPONSE_CACHE: dict[str, str] = {} nn = "\n\n" _CHATBOT_PRESOW_KEYWORDS = [ "lagana", "ugana", "beej", "sow", "plant", "grow", "konsi fasal", "which crop", "crop selection", "kharif", "rabi", "price forecast", "bhav kitna", ] _CHATBOT_PEST_KEYWORDS = [ "kida", "rog", "bimari", "pest", "disease", "insect", "fungus", "patta", "patti", "daag", "jhulsa", "sukhna", "mahu", "whitefly", "thrips", "rust", "blight", "wilt", "borer", "caterpillar", "spray karun", "dawai", "dawa", "treatment", "bachao", ] STATE_COORDS: dict[str, tuple] = { "Andhra Pradesh": (15.9129, 79.7400), "Arunachal Pradesh": (28.2180, 94.7278), "Assam": (26.2006, 92.9376), "Bihar": (25.0961, 85.3131), "Chhattisgarh": (21.2787, 81.8661), "Goa": (15.2993, 74.1240), "Gujarat": (22.2587, 71.1924), "Haryana": (29.0588, 76.0856), "Himachal Pradesh": (31.1048, 77.1734), "Jharkhand": (23.6102, 85.2799), "Karnataka": (15.3173, 75.7139), "Kerala": (10.8505, 76.2711), "Madhya Pradesh": (22.9734, 78.6569), "Maharashtra": (19.7515, 75.7139), "Manipur": (24.6637, 93.9063), "Meghalaya": (25.4670, 91.3662), "Mizoram": (23.1645, 92.9376), "Nagaland": (26.1584, 94.5624), "Odisha": (20.9517, 85.0985), "Punjab": (31.1471, 75.3412), "Rajasthan": (27.0238, 74.2179), "Sikkim": (27.5330, 88.5122), "Tamil Nadu": (11.1271, 78.6569), "Telangana": (18.1124, 79.0193), "Tripura": (23.9408, 91.9882), "Uttar Pradesh": (26.8467, 80.9462), "Uttarakhand": (30.0668, 79.0193), "West Bengal": (22.9868, 87.8550), "Jammu & Kashmir": (33.7782, 76.5762), "Jammu and Kashmir": (33.7782, 76.5762), "Ladakh": (34.1526, 77.5770), "Delhi": (28.7041, 77.1025), } _WMO_CODES: dict[int, str] = { 0: "☀️ Clear", 1: "🌤️ Mostly Clear", 2: "⛅ Partly Cloudy", 3: "☁️ Overcast", 45: "🌫️ Foggy", 48: "🌫️ Icy Fog", 51: "🌦️ Light Drizzle", 53: "🌦️ Drizzle", 55: "🌧️ Heavy Drizzle", 61: "🌧️ Light Rain", 63: "🌧️ Rain", 65: "🌧️ Heavy Rain", 71: "❄️ Light Snow", 73: "❄️ Snow", 75: "❄️ Heavy Snow", 80: "🌦️ Showers", 81: "🌦️ Moderate Showers", 82: "⛈️ Heavy Showers", 95: "⛈️ Thunderstorm", 96: "⛈️ Thunderstorm+Hail", 99: "⛈️ Heavy Thunderstorm", } _STATE_SOIL: dict[str, str] = { "Punjab": "loamy alluvial (high fertility)", "Haryana": "sandy loam alluvial", "Uttar Pradesh": "alluvial (Gangetic plain)", "Bihar": "alluvial + sandy loam", "West Bengal": "alluvial + laterite (red)", "Madhya Pradesh": "black cotton (Vertisol) + red laterite", "Maharashtra": "black cotton (Vertisol) 60% + red laterite 40%", "Rajasthan": "sandy / arid + alluvial (eastern)", "Gujarat": "black cotton + coastal alluvial + sandy", "Karnataka": "red laterite + black cotton (northern)", "Andhra Pradesh": "black cotton + red sandy loam", "Telangana": "black cotton (Vertisol) dominant", "Tamil Nadu": "red laterite + alluvial + black cotton", "Kerala": "laterite + sandy coastal + forest loam", "Odisha": "red laterite + alluvial river valleys", "Chhattisgarh": "red laterite (dongar) + alluvial (plains)", "Jharkhand": "red laterite + forest loam", "Assam": "alluvial + clay loam + hill soils", "Himachal Pradesh": "mountain loam + brown forest soil", "Uttarakhand": "mountain loam + bhabhar terai alluvial", "Jammu & Kashmir": "mountain alluvial + brown forest soil", "Jammu and Kashmir": "mountain alluvial + brown forest soil", } _STATE_ALIASES: dict[str, str] = { "up": "Uttar Pradesh", "mp": "Madhya Pradesh", "ap": "Andhra Pradesh", "tn": "Tamil Nadu", "wb": "West Bengal", "hp": "Himachal Pradesh", "uk": "Uttarakhand", "jk": "Jammu & Kashmir", "j&k": "Jammu & Kashmir", "uttar pradesh": "Uttar Pradesh", "madhya pradesh": "Madhya Pradesh", "andhra pradesh": "Andhra Pradesh", "tamil nadu": "Tamil Nadu", "west bengal": "West Bengal", "himachal pradesh": "Himachal Pradesh", "himachal": "Himachal Pradesh", "uttarakhand": "Uttarakhand", "jammu": "Jammu & Kashmir", "kashmir": "Jammu & Kashmir", "maharashtra": "Maharashtra", "rajasthan": "Rajasthan", "punjab": "Punjab", "haryana": "Haryana", "gujarat": "Gujarat", "karnataka": "Karnataka", "telangana": "Telangana", "kerala": "Kerala", "odisha": "Odisha", "orissa": "Odisha", "jharkhand": "Jharkhand", "chhattisgarh": "Chhattisgarh", "assam": "Assam", "bihar": "Bihar", "goa": "Goa", "manipur": "Manipur", "meghalaya": "Meghalaya", "mizoram": "Mizoram", "nagaland": "Nagaland", "sikkim": "Sikkim", "tripura": "Tripura", "arunachal pradesh": "Arunachal Pradesh", "arunachal": "Arunachal Pradesh", } @st.cache_data(ttl=3600, show_spinner=False) def _fetch_weather(state: str) -> dict: coords = STATE_COORDS.get(state) if not coords: return {} lat, lon = coords url = ( f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" "&daily=temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode" "¤t_weather=true&timezone=Asia%2FKolkata&forecast_days=3" ) try: r = requests.get(url, timeout=8) if r.ok: data = r.json() url28 = ( f"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}" "&daily=temperature_2m_max,temperature_2m_mean,precipitation_sum" "&timezone=Asia%2FKolkata&forecast_days=14&past_days=14" ) try: r28 = requests.get(url28, timeout=8) if r28.ok: d28 = r28.json().get("daily", {}) cl = lambda v: [x for x in v if x is not None] tmean_v = d28.get("temperature_2m_mean", []) tmax_v = d28.get("temperature_2m_max", []) prec_v = d28.get("precipitation_sum", []) data["t2m_mean_28d"] = float(np.mean(cl(tmean_v))) if tmean_v else np.nan data["t2m_max_28d"] = float(np.max(cl(tmax_v))) if tmax_v else np.nan data["tp_total_28d"] = float(np.sum(cl(prec_v))) if prec_v else np.nan data["tp_mean_7d"] = float(np.mean(cl(prec_v[-7:]))) if len(prec_v) >= 7 else np.nan ys = cl(tmean_v) data["temp_trend"] = float(np.polyfit(range(len(ys)), ys, 1)[0]) if len(ys) >= 3 else 0.0 data["skt_mean_28d"] = data["t2m_mean_28d"] data["stl1_mean_28d"]= data["t2m_mean_28d"] except Exception: for k in ["t2m_mean_28d","t2m_max_28d","tp_total_28d","tp_mean_7d", "temp_trend","skt_mean_28d","stl1_mean_28d"]: data.setdefault(k, np.nan) return data except Exception: pass return {} def _format_weather_context(wx: dict, state: str) -> str: if not wx or "daily" not in wx: return "" daily = wx["daily"] tmax = (daily.get("temperature_2m_max") or [None])[0] tmin = (daily.get("temperature_2m_min") or [None])[0] rain = (daily.get("precipitation_sum") or [0])[0] or 0 code = (daily.get("weathercode") or [0])[0] or 0 desc = _WMO_CODES.get(code, "") lines = [f"WEATHER in {state} (today): {desc} | Max {tmax}°C | Min {tmin}°C | Rain {rain}mm"] if rain > 20: lines.append("🌧️ Heavy rain — avoid spraying. Fungal disease risk elevated.") if tmax and tmax > 40: lines.append("🌡️ Extreme heat — avoid urea application. Volatilisation risk.") if tmin and tmin < 5: lines.append("🥶 Cold — frost risk. Protect sensitive crops.") return "\n".join(lines) def detect_state(query: str) -> str | None: ql = query.lower() for alias, state in _STATE_ALIASES.items(): if alias in ql: return state return None def get_current_season() -> dict: m = datetime.now().month if m in (6, 7, 8, 9): return {"name": "Kharif", "months": "June–October", "crops": "Rice, Cotton, Soybean, Maize, Groundnut, Arhar, Moong, Urad", "planning_note": "Kharif sowing: June 15 – July 31. Scout pests post-monsoon."} elif m in (10, 11, 12, 1, 2, 3): return {"name": "Rabi", "months": "October–March", "crops": "Wheat, Mustard, Gram, Lentil, Peas, Potato, Onion", "planning_note": "Rabi sowing: Oct 15 – Nov 30. Watch aphid in mustard, rust in wheat."} else: return {"name": "Zaid (Summer)", "months": "April–June", "crops": "Moong, Urad, Cucumber, Watermelon, Bitter Gourd, Sunflower", "planning_note": "Zaid: 60-65 day window. Irrigation is critical."} def get_soil_context(state: str) -> str: return _STATE_SOIL.get(state, "") def _extract_location_from_query(query: str) -> str | None: words = query.split() for i, w in enumerate(words): if w.lower() in ("district","tehsil","block","mein","me","ka","ki") and i > 0: return words[i-1].title() return None def _apply_state_preference(docs: list, state: str | None) -> list: if not state or not docs: return docs su = state.upper() sd = [d for d in docs if su in (getattr(d, "source", "") or "").upper()] od = [d for d in docs if d not in sd] return sd + od def _make_cache_key(query: str, crop, state, problem_type: str, season_name: str) -> str: raw = f"{query.lower().strip()}|{crop}|{state}|{problem_type}|{season_name}" return hashlib.md5(raw.encode()).hexdigest() def _cache_store(key: str, value: str) -> None: if len(_RESPONSE_CACHE) > 500: for k in list(_RESPONSE_CACHE.keys())[:100]: del _RESPONSE_CACHE[k] _RESPONSE_CACHE[key] = value @st.cache_resource(show_spinner=False) def _get_gemini_client(): try: import google.generativeai as genai genai.configure(api_key=config.GEMINI_API_KEY) return genai except Exception: return None def _build_prompt(query: str, context: str, history: list, problem_type: str, lang: str, state: str = "", district: str = "", detected_crop: str = "", problem_detail: str = "") -> str: lang_rule = ( f"\n\nMANDATORY LANGUAGE RULE: Reply ONLY in {lang.upper()}. " "Do NOT switch languages mid-answer." ) hist_str = "" if history: hist_str = "\n\nCONVERSATION HISTORY (last 3 turns):\n" for msg in history[-6:]: role = "Farmer" if msg["role"] == "user" else "Advisor" hist_str += f"{role}: {msg['content'][:200]}\n" # UPGRADE 2: Farmer location context location_str = "" if state or district: loc_parts = [p for p in [district, state] if p] location_str = ( f"\n\nFARMER LOCATION: {', '.join(loc_parts)} — " "prioritize region-specific advice for this area" ) # UPGRADE 3: Verified dose injection verified_dose_str = "" if detected_crop and problem_detail: dose_info = get_verified_dose(detected_crop, problem_detail) if dose_info: verified_dose_str = ( f"\n\nVERIFIED DOSE (ICAR/KCC source — use this exactly, do not modify):\n" + json.dumps(dose_info, ensure_ascii=False) ) # UPGRADE 2: Forced grounding context header grounded_context = ( "RETRIEVED KCC RECORDS (from 16.5M farmer queries database):\n" + context + "\n\nMANDATORY GROUNDING RULES:\n" "- Your answer MUST be based on the retrieved KCC records above\n" "- For any chemical dose, seed rate, or specific agronomy number: use ONLY values from the retrieved records\n" "- If the retrieved records don't cover the question: say 'KCC records don't have specific data for this — general guidance: [answer]'\n" "- Do NOT use doses or chemicals from your general training if they contradict the retrieved records\n" "- The records above represent real answers given to Indian farmers by agricultural experts" ) return ( SYSTEM_PROMPT + lang_rule + location_str + hist_str + "\n\n" + grounded_context + verified_dose_str + "\n\nFARMER'S QUESTION: " + query + "\n\nADVISOR'S ANSWER (be concise, specific, actionable):" ) def _stream_llm_response(prompt: str): try: import groq as _groq except ImportError: yield "⚠️ groq not installed." return _GM = [ getattr(config, "GROQ_MODEL_PRIMARY", "meta-llama/llama-4-scout-17b-16e-instruct"), "llama-3.3-70b-versatile", "gemma2-9b-it", ] _GEMS = ["gemini-2.0-flash", "gemini-1.5-flash", "gemma-3-27b-it"] gc = _groq.Groq(api_key=config.GROQ_API_KEY) for model in _GM: try: stream = gc.chat.completions.create( model=model, messages=[{"role": "user", "content": prompt}], max_tokens=600, temperature=0.1, stream=True, ) for chunk in stream: d = chunk.choices[0].delta.content if d: yield d return except Exception as e: if "429" in str(e) or "rate" in str(e).lower(): time.sleep(2) continue try: import google.generativeai as genai genai.configure(api_key=config.GEMINI_API_KEY) for gm in _GEMS: try: resp = genai.GenerativeModel(gm).generate_content( prompt, generation_config={"max_output_tokens": 600, "temperature": 0.1}) yield resp.text return except Exception: continue except Exception: pass yield "⚠️ AI service temporarily unavailable. Please try again." def check_banned_pesticides(response: str) -> list[str]: """Check for India-banned pesticides — negation-aware. A mention inside a negated sentence (e.g. "do NOT use Endosulfan" or "Banned chemicals (Endosulfan, Monocrotophos) se bachein") is NOT flagged. Whole-sentence scan + 30-char inline window. Backport from v3 kcc_core/citation_guard.py (2026-05-10). Sources: MoA gazette notifications + India CIBRC + ICAR advisory. """ import re as _re # Union of v2's original list + v3's CIBRC-aligned set (21 entries total) _BANNED = [ # Organophosphates (banned/severely restricted) "monocrotophos","endosulfan","methyl parathion","phorate","triazophos", "phosphamidon","dichlorvos","profenofos","chlorpyrifos ethyl", "ethyl parathion","fenthion","phosalone","trichlorfon", # Carbamates "carbofuran","aldicarb","methomyl","carbaryl","carbosulfan", # Organochlorines "alachlor","dicofol","ddt","bhc","aldrin","dieldrin","heptachlor", "lindane","chlordane","tetradifon","pentachlorophenol", # Mercury / others (restricted/banned) "atrazine","mancozeb blends with zineb","benomyl","captafol", "paraquat","2 4 5 t","sodium cyanide","ethyl mercury chloride", ] _NEGATIONS = ( "do not use", "don't use", "never use", "avoid", "banned", "no use of", "not recommended", "instead of", "rather than", "not ", "non-", "no ", "without ", ) _SENT_SPLIT = _re.compile(r"[.!?\n]+") rl = response.lower() def _is_negated(chem: str) -> bool: """True if every occurrence of `chem` is inside a negated sentence.""" idx = rl.find(chem) while idx != -1: sent_start = max((m.end() for m in _SENT_SPLIT.finditer(rl, 0, idx)), default=0) next_term = _SENT_SPLIT.search(rl, idx) sent_end = next_term.start() if next_term else len(rl) sentence = rl[sent_start:sent_end] window = rl[max(0, idx - 30):idx] if any(neg in sentence for neg in _NEGATIONS) or any(neg in window for neg in _NEGATIONS): idx = rl.find(chem, idx + len(chem)) continue return False return True found = [] for b in _BANNED: pattern = r'\b' + _re.escape(b) + r'\b' if _re.search(pattern, rl) and not _is_negated(b): found.append(b) return found # Chemical-target correctness lookup (ICAR verified) # Maps disease/pest -> correct chemical mode of action _CHEMICAL_TARGET_RULES = { # Fungal wilts -- soil-borne, sprays DON'T work "fusarium wilt": {"banned_classes": ["metalaxyl","ridomil","mancozeb spray"], "correct": "Carbendazim 50% WP soil drench @ 2g/L OR Trichoderma viride @ 5g/kg seed"}, "verticillium wilt":{"banned_classes": ["metalaxyl","ridomil"], "correct": "Trichoderma-based soil application; no curative spray exists"}, # Whitefly -- pyrethroids cause resurgence "whitefly": {"banned_classes": ["bifenthrin","cypermethrin","lambda cyhalothrin","permethrin","deltamethrin"], "correct": "Pyriproxyfen 10% EC @ 0.75ml/L OR Diafenthiuron 50% WP @ 1.5g/L OR Spiromesifen 22.9% SC @ 0.5ml/L"}, # BPH -- synthetic pyrethroids make it worse "brown planthopper":{"banned_classes": ["cypermethrin","deltamethrin","lambda cyhalothrin","bifenthrin"], "correct": "Buprofezin 25% SC @ 1ml/L OR Dinotefuran 20% SG @ 0.4g/L"}, # Spider mite -- organophosphates ineffective "spider mite": {"banned_classes": ["monocrotophos","chlorpyrifos","malathion"], "correct": "Abamectin 1.9% EC @ 0.5ml/L OR Spiromesifen 22.9% SC @ 0.5ml/L"}, } def check_chemical_target_match(response: str, query: str) -> str | None: """Post-generation check: flag if response recommends wrong chemical class for detected disease.""" rl = response.lower() ql = query.lower() warnings = [] for disease, rules in _CHEMICAL_TARGET_RULES.items(): if disease in ql or disease in rl: for banned_chem in rules["banned_classes"]: if banned_chem in rl: warnings.append( f"⚠️ Correction: {banned_chem.title()} is not effective for {disease}. " f"Recommended: {rules['correct']}" ) break return "\n".join(warnings) if warnings else None def check_chemical_safety(response: str, problem_type: str) -> str | None: if problem_type in ("pest","disease") and "spray" in response.lower(): if "gloves" not in response.lower(): return "⚠️ Wear gloves and mask while spraying." return None # ── UPGRADE 3: Verified dose lookup (ICAR/KCC source) ──────────────────────── _DOSE_TABLE: dict | None = None _DOSE_TABLE_LOADED = False def _load_dose_table() -> dict: """Load kcc_dose_table.json once and cache in module.""" global _DOSE_TABLE, _DOSE_TABLE_LOADED if _DOSE_TABLE_LOADED: return _DOSE_TABLE or {} _DOSE_TABLE_LOADED = True import json as _j from pathlib import Path as _P dose_path = _P(__file__).parent / "kcc_dose_table.json" if dose_path.exists(): try: with open(dose_path, "r", encoding="utf-8") as f: _DOSE_TABLE = _j.load(f) except Exception as e: print(f"[DoseTable] Load error: {e}") _DOSE_TABLE = {} else: _DOSE_TABLE = {} return _DOSE_TABLE def get_verified_dose(crop: str, pest_disease: str) -> dict | None: """ Look up ICAR-verified dose for a crop + pest/disease combination. Returns dose dict or None if not found. crop : e.g. "wheat", "cotton", "rice" pest_disease : e.g. "aphid", "yellow_rust", "bollworm" """ if not crop or not pest_disease: return None table = _load_dose_table() if not table: return None # Normalize crop_key = crop.strip().lower().replace(" ", "_") pest_key = pest_disease.strip().lower().replace(" ", "_") crop_data = table.get(crop_key) if not crop_data: # Try partial match for k in table: if k in crop_key or crop_key in k: crop_data = table[k] break if not crop_data: return None # Exact match if pest_key in crop_data: return crop_data[pest_key] # Partial match for k in crop_data: if k in pest_key or pest_key in k: return crop_data[k] return None def _icar_mandatory_supplement(response: str, problem_type: str, detected_crop) -> str: banned = check_banned_pesticides(response) if banned: response += f"\n\n⚠️ Note: {', '.join(b.title() for b in banned)} is banned in India." if problem_type in ("pest","disease") and "⚠️ Wear gloves" not in response: response += "\n\n⚠️ Wear gloves and mask while spraying." # FIX 6: Check chemical-target correctness (query unavailable here; uses response context) chem_warning = check_chemical_target_match(response, "") if chem_warning: response += f"\n\n{chem_warning}" return response def understand_query_llm(query: str) -> dict: try: import groq as _groq client = _groq.Groq(api_key=config.GROQ_API_KEY) mini = (f'Farmer query: "{query}"\nReply ONLY with JSON: {{"crop": , ' '"problem": }}\nNo explanation.') resp = client.chat.completions.create( model="gemma2-9b-it", messages=[{"role": "user", "content": mini}], max_tokens=60, temperature=0, ) text = resp.choices[0].message.content.strip() m = re.search(r'\{.*?\}', text, re.DOTALL) if m: return json.loads(m.group()) except Exception: pass return {"crop": None, "problem": "general"} def rewrite_query_for_retrieval(query: str, crop, problem_type: str, state, season) -> str: parts = [query] if crop: parts.append(f"{crop} crop") if problem_type and problem_type != "general": parts.append(problem_type) if state: parts.append(state) if season: parts.append(season.get("name","")) return " ".join(p for p in parts if p) def _fetch_mandi_prices(state: str, crop: str) -> dict | None: try: api_key = getattr(config, "DATA_GOV_API_KEY", None) if not api_key: return None url = ( "https://api.data.gov.in/resource/9ef84268-d588-465a-a308-a864a43d0070" f"?api-key={api_key}&format=json&limit=5" f"&filters[State]={state}&filters[Commodity]={crop}" ) r = requests.get(url, timeout=6) if r.ok: records = r.json().get("records", []) prices = [float(rec["Modal_Price"]) for rec in records if rec.get("Modal_Price")] return {"avg": sum(prices)/len(prices), "records": records[:3]} if prices else None except Exception: pass return None def _build_price_context(season: dict, state: str) -> str: cmap = {"Kharif": ["Soybean","Cotton","Rice","Maize","Groundnut"], "Rabi": ["Wheat","Gram","Mustard"], "Zaid (Summer)": ["Moong","Urad"]} lines = [] for crop in cmap.get(season.get("name",""), [])[:3]: data = _fetch_mandi_prices(state, crop) if data: lines.append(f"{crop}: \u20b9{data['avg']:,.0f}/quintal ({state} mandi avg)") return "CURRENT MANDI PRICES (live):\n" + "\n".join(lines) if lines else "" def _build_presow_chatbot_context(crop: str, state: str) -> str: try: from mandi_advisor.enterprise_engine_v2 import get_presow_signal sig = get_presow_signal(crop, state) if not sig or not sig.get("p50"): return "" p25, p50, p75 = sig.get("p25",0), sig["p50"], sig.get("p75",0) conf = sig.get("confidence","MEDIUM") return ( f"HARVEST PRICE FORECAST for {crop} in {state} (presow_v4, {conf} confidence):\n" f" Pessimistic (P25): \u20b9{p25:,}/quintal\n" f" Likely (P50): \u20b9{p50:,}/quintal\n" f" Optimistic (P75): \u20b9{p75:,}/quintal\n" "Use for sowing decision. Do NOT quote old KCC price data." ) except Exception: return "" def _build_pest_risk_chatbot_context(state: str, district: str, crop: str, month: int) -> str: try: from mandi_advisor.pest_predictor import predict_pest_risk results = predict_pest_risk(state, crop, district=district, month=month) if not results: return "" high = [r for r in results if r.get("risk_score",0) >= 0.5] if not high: return "" lines = [f"PEST RISK for {crop} in {state} (14-day):"] for r in high[:3]: lines.append(f" {r.get('pest_category','?')}: {int(r.get('risk_score',0)*100)}% risk") return "\n".join(lines) except Exception: return "" def _render_feedback(idx: int, user_q: str, response: str, crop, state, problem: str) -> None: col1, col2, _ = st.columns([1, 1, 8]) fb_key = f"fb_{idx}_{hashlib.md5(response[:50].encode()).hexdigest()[:8]}" if fb_key not in st.session_state: st.session_state[fb_key] = None with col1: if st.button("\U0001f44d", key=f"up_{fb_key}", help="Helpful"): st.session_state[fb_key] = "up" with col2: if st.button("\U0001f44e", key=f"dn_{fb_key}", help="Not helpful"): st.session_state[fb_key] = "down" _PROJECT_ROOT = Path(__file__).parent.resolve() _EW_DATA = str(_PROJECT_ROOT / "early_warning" / "data") _EW_MODEL = str(_PROJECT_ROOT / "early_warning" / "model") @st.cache_data(ttl=86400, show_spinner=False) def _load_location_lookup() -> "pd.DataFrame": try: return pd.read_parquet(f"{_EW_DATA}/location_lookup.parquet") except Exception: return pd.DataFrame(columns=["StateName","DistrictName","BlockName","lat","lon"]) @st.cache_data(ttl=86400, show_spinner=False) def _load_neighbor_lookup() -> dict: try: df = pd.read_parquet(f"{_EW_DATA}/neighbor_lookup.parquet") out: dict = {} for r in df.itertuples(index=False): out.setdefault(r.BlockName, []).append(r.neighbor_block) return out except Exception: return {} @st.cache_data(ttl=86400, show_spinner=False) def _load_hist_outbreak_rate() -> "pd.DataFrame": try: return pd.read_parquet(f"{_EW_DATA}/hist_rate_lookup.parquet") except Exception: return pd.DataFrame(columns=["BlockName","Crop","call_month","hist_outbreak_rate"]) @st.cache_data(ttl=86400, show_spinner=False) def _load_block_top_crops() -> "pd.DataFrame": try: return pd.read_parquet(f"{_EW_DATA}/block_top_crops.parquet") except Exception: return pd.DataFrame(columns=["BlockName","Crop","count"]) @st.cache_data(ttl=86400, show_spinner=False) def _load_alerts_lookup() -> "pd.DataFrame": try: return pd.read_parquet(f"{_EW_DATA}/block_crop_alerts.parquet") except Exception: return pd.DataFrame(columns=["BlockName","Crop","call_month","category","exact_problem","count"]) @st.cache_data(ttl=86400, show_spinner=False) def _load_national_alerts() -> "pd.DataFrame": try: return pd.read_parquet(f"{_EW_DATA}/national_crop_alerts.parquet") except Exception: return pd.DataFrame(columns=["Crop","call_month","category","exact_problem","count"]) @st.cache_data(ttl=86400, show_spinner=False) def _load_wx_baselines() -> dict: try: df = pd.read_parquet(f"{_EW_DATA}/wx_baselines.parquet") return {(float(r.lat_g), float(r.lon_g), int(r.call_month)): {"t2m": float(r.t2m_baseline), "tp": float(r.tp_baseline), "trend": float(r.trend_baseline)} for r in df.itertuples(index=False)} except Exception: return {} @st.cache_data(ttl=86400, show_spinner=False) def _load_neighbor_hist_rate() -> dict: try: df = pd.read_parquet(f"{_EW_DATA}/neighbor_hist_rate.parquet") return {(r.BlockName, int(r.call_month)): float(r.neighbor_hist_rate) for r in df.itertuples(index=False)} except Exception: return {} @st.cache_data(ttl=86400, show_spinner=False) def _load_ndvi_baselines() -> dict: try: df = pd.read_parquet(f"{_EW_DATA}/ndvi_baseline.parquet") return {(float(r.lat_g), float(r.lon_g), int(r.call_month)): float(r.ndvi_baseline) for r in df.itertuples(index=False)} except Exception: return {} @st.cache_data(ttl=3600, show_spinner=False) def _load_ndvi_lookup() -> dict: try: df = pd.read_parquet(f"{_EW_DATA}/ndvi_lookup.parquet") return df.groupby(["lat_g","lon_g"])["ndvi"].mean().to_dict() except Exception: return {} @st.cache_resource(show_spinner=False) def _load_pest_model_v2(): try: import joblib as _jl le_s = _jl.load(f"{_EW_MODEL}/v2_le_state.pkl") le_c = _jl.load(f"{_EW_MODEL}/v2_le_crop.pkl") return le_s, le_c except Exception: class _DLE: classes_ = [] def transform(self, v): return [0]*len(v) return _DLE(), _DLE() def _get_crop_risk_v2(state: str, district: str, crop: str, month: int) -> tuple: try: from mandi_advisor.pest_predictor import predict_pest_risk results = predict_pest_risk(state, crop, district=district, month=month) if results: return float(max(r.get("risk_score", 0.0) for r in results)), {} except Exception: pass return 0.3, {} _CROP_STATE_MAP: dict[str, list] = { "Coconut": ["Kerala","Karnataka","Tamil Nadu","Andhra Pradesh","Goa","Odisha"], "Tea": ["Assam","West Bengal","Kerala","Himachal Pradesh","Uttarakhand","Arunachal Pradesh"], "Coffee": ["Karnataka","Kerala","Tamil Nadu"], "Rubber": ["Kerala","Karnataka","Tamil Nadu","Goa","Andhra Pradesh"], "Apple": ["Himachal Pradesh","Uttarakhand","Jammu & Kashmir","Arunachal Pradesh"], "Pineapple": ["Assam","Meghalaya","West Bengal","Kerala","Tripura","Nagaland"], "Sugarcane": ["Uttar Pradesh","Maharashtra","Karnataka","Tamil Nadu","Bihar", "Haryana","Punjab","Andhra Pradesh","Gujarat"], "Jute": ["West Bengal","Bihar","Assam","Odisha","Meghalaya"], "Cotton": ["Gujarat","Maharashtra","Telangana","Andhra Pradesh","Punjab", "Haryana","Rajasthan","Madhya Pradesh","Karnataka"], } def _is_crop_suitable_for_state(crop: str, state: str) -> bool: if crop not in _CROP_STATE_MAP: return True return state in _CROP_STATE_MAP[crop] _TRIGGER_RULES: dict[str, dict] = { "late_blight": {"rain_min": 15, "temp_max": 25}, "downy_mildew": {"rain_min": 10, "temp_max": 28}, "powdery_mildew": {"rain_max": 5, "temp_max": 28}, "blast": {"rain_min": 10, "temp_min": 20}, "stem_borer": {"temp_min": 25}, "bollworm": {"temp_min": 25}, "aphid": {"temp_max": 25, "rain_max": 5}, "rust": {"temp_max": 22, "rain_min": 5}, "brown_planthopper": {"rain_min": 8, "temp_min": 26}, "bacterial_blight": {"rain_min": 15, "temp_min": 24}, } def _check_triggers(pest_name: str, wx: dict) -> bool: if not wx: return False pl = pest_name.lower().replace(" ", "_") rules = next((r for k, r in _TRIGGER_RULES.items() if k in pl or pl in k), None) if not rules: return False try: t = float(wx.get("t2m_mean_28d") or 20) rain = float(wx.get("tp_total_28d") or 0) except (TypeError, ValueError): return False if "temp_min" in rules and t < rules["temp_min"]: return False if "temp_max" in rules and t > rules["temp_max"]: return False if "rain_min" in rules and rain < rules["rain_min"]: return False if "rain_max" in rules and rain > rules["rain_max"]: return False return True # ── Multi-Step RAG (#8) ─────────────────────────────────────────────────────── def multi_step_retrieve( retriever, query: str, normalized_query: str, detected_crop: "str | None", problem_type: str, settings: dict, state: str = "", district: str = "", ) -> "list[RetrievedDoc]": """ Two-step retrieval strategy: Step 1 — symptom match: normalized query + crop filter Step 2 — treatment match: query augmented with problem-type keywords Results are merged, deduplicated by answer text, re-ranked by score. """ k = settings["top_k"] # Step 1: broad symptom / context retrieval docs1 = retriever.search( normalized_query, top_k = max(k, 4), deduplicate = settings["deduplicate"], min_score = settings["min_score"], crop_filter = detected_crop, state = state, district = district, ) # Step 2: problem-type focused retrieval (skip if general) docs2: list[RetrievedDoc] = [] if problem_type != "general": problem_suffix = { "pest": "insect pest control treatment insecticide", "disease": "disease fungal control treatment fungicide spray", "nutrient": "deficiency fertilizer nutrient dose application", "yield": "production increase variety best practice", "weather": "frost cold heat drought protection remedy", "crop_selection": "crop variety season sowing recommendation", }.get(problem_type, "") if problem_suffix: step2_query = f"{normalized_query} {problem_suffix}" docs2 = retriever.search( step2_query, top_k = max(k, 4), deduplicate = settings["deduplicate"], min_score = settings["min_score"], crop_filter = detected_crop, state = state, district = district, ) # Merge: prefer step-1 results (more semantically close to query) seen_answers: set[str] = set() merged: list[RetrievedDoc] = [] for doc in docs1 + docs2: if doc.answer not in seen_answers: seen_answers.add(doc.answer) merged.append(doc) # Re-rank by similarity score, keep top-k merged.sort(key=lambda d: d.score, reverse=True) merged = merged[:k] for i, doc in enumerate(merged): doc.rank = i + 1 return merged # ── cached retriever (one load per Streamlit server process) ───────────────── class _ProxyRetriever: """ Drop-in replacement for KCCRetriever that calls the retrieval_api.py service on port 8502 instead of loading the 6GB FAISS index locally. Streamlit stays at ~500MB RAM while retrieval_api.py holds the index. Falls back to loading locally if port 8502 is unavailable. """ index_size: int = 16_565_975 def search(self, query: str, top_k: int = 5, **kwargs): import urllib.request as _ur, json as _j try: data = _j.dumps({"query": query, "top_k": top_k}).encode() req = _ur.Request("http://localhost:8502", data=data, headers={"Content-Type": "application/json"}, method="POST") with _ur.urlopen(req, timeout=15) as r: ctx = _j.loads(r.read())["context"] # Return a single fake doc carrying the full context from step3_retrieval import RetrievedDoc return [RetrievedDoc(score=1.0, answer=ctx, question="[proxy]", source="retrieval_api")] except Exception: # Port 8502 not available — fall back to local retriever return get_retriever().search(query, top_k=top_k, **kwargs) def format_context(self, docs) -> str: if not docs: return "" proxy_docs = [d for d in docs if getattr(d, "question", "") == "[proxy]"] real_docs = [d for d in docs if getattr(d, "question", "") != "[proxy]"] parts = [] seen = set() for d in proxy_docs: if d.answer not in seen: parts.append(d.answer) seen.add(d.answer) if real_docs: parts.append(get_retriever().format_context(real_docs)) sep = chr(10) + chr(10) return sep.join(parts) if parts else get_retriever().format_context(docs) @st.cache_resource(show_spinner="Connecting to knowledge base…") def _load_retriever(): """Return a _ProxyRetriever if port 8502 is up, else load FAISS locally.""" import urllib.request as _ur try: with _ur.urlopen("http://localhost:8502", timeout=3) as r: r.read() return _ProxyRetriever() except Exception: return get_retriever() def _render_early_warning_tab( user_state: str | None, user_district: str | None, user_block: str | None, block_lat, block_lon, ) -> None: """Render early-warning pest risk tab (Tab 5).""" if block_lat is None or block_lon is None: st.info("📍 Set your location in the sidebar to see the 14-day pest outlook.") return # ── Local resource init ─────────────────────────────────────────────── ndvi_lookup = _load_ndvi_lookup() le_state, le_crop = _load_pest_model_v2() wx = _fetch_weather(user_state or "") if not isinstance(wx, dict): wx = {} def _rg(v): return round(round(v / 0.25) * 0.25, 2) lat_g = _rg(block_lat); lon_g = _rg(block_lon) ndvi_val = ndvi_lookup.get((lat_g, lon_g), np.nan) # ── 5. NEIGHBOR CONTAGION signal ────────────────────────────────────────── neighbor_lookup = _load_neighbor_lookup() hist_df = _load_hist_outbreak_rate() crops_df = _load_block_top_crops() alerts_df = _load_alerts_lookup() nat_alerts = _load_national_alerts() curr_month = datetime.now().month curr_year = datetime.now().year neighbor_blocks = neighbor_lookup.get(user_block, []) block_hist = hist_df[hist_df["BlockName"] == user_block] block_alerts = alerts_df[alerts_df["BlockName"] == user_block] # Neighbor historical outbreak rates for this month (contagion signal) neighbor_hist_month = hist_df[ hist_df["BlockName"].isin(neighbor_blocks) & (hist_df["call_month"] == curr_month) ] # For each crop, get fraction of neighbors that had high historical outbreak rate neighbor_pressure: dict[str, float] = {} if len(neighbor_hist_month) > 0: n_blocks = len(neighbor_blocks) if neighbor_blocks else 1 for crop, grp in neighbor_hist_month.groupby("Crop"): high_rate = (grp["hist_outbreak_rate"] > 0.4).sum() neighbor_pressure[crop] = min(high_rate / n_blocks, 1.0) # ── SEASON FILTER ───────────────────────────────────────────────────────── month_hist = block_hist[block_hist["call_month"] == curr_month] if len(month_hist) > 0: top_crops = ( month_hist[month_hist["hist_outbreak_rate"] > 0] .sort_values("hist_outbreak_rate", ascending=False) .head(10)["Crop"].tolist() ) if not top_crops: top_crops = month_hist.sort_values("hist_outbreak_rate", ascending=False).head(10)["Crop"].tolist() else: block_crops = crops_df[crops_df["BlockName"] == user_block] top_crops = (block_crops.sort_values("count", ascending=False).head(8)["Crop"].tolist() if len(block_crops) > 0 else ["Wheat","Paddy (Dhan)","Chillies","Tomato","Onion","Cotton (Kapas)","Maize (Makka)","Mustard"]) # ── Crop relevance filtering (two-layer) ───────────────────────────────── # Layer 1: Remove generic / uninformative crop categories _EXCLUDE_CROPS = {"others", "other", "misc", "miscellaneous", "general"} top_crops = [c for c in top_crops if c.lower().strip() not in _EXCLUDE_CROPS] # Layer 2: Minimum call count — require at least 5 KCC calls from this block. # Crops with 1-2 calls are statistical noise and often geographically wrong. block_call_counts = {} if len(crops_df) > 0: block_crops_all = crops_df[crops_df["BlockName"] == user_block] block_call_counts = dict(zip(block_crops_all["Crop"], block_crops_all["count"])) MIN_CALL_COUNT = 5 top_crops = [c for c in top_crops if block_call_counts.get(c, MIN_CALL_COUNT) >= MIN_CALL_COUNT] # Layer 3: Geographic suitability — filter crops not grown in this state. # Prevents Jack Fruit appearing in MP, coconut in Punjab, etc. if user_state: geo_filtered = [c for c in top_crops if _is_crop_suitable_for_state(c, user_state)] # Only apply if filter didn't remove everything (safety fallback) if len(geo_filtered) >= 3: top_crops = geo_filtered # Encode state state_enc_val = int(le_state.transform([user_state])[0]) if user_state in le_state.classes_ else 0 FEATURES = [ "lat","lon","call_month","call_season","year", "crop_enc","state_enc", "t2m_mean_28d","t2m_max_28d","tp_total_28d","tp_mean_7d", "skt_mean_28d","stl1_mean_28d","temp_trend", "temp_anomaly","rain_anomaly","trend_anomaly", "ndvi_t7","ndvi_t14","ndvi_t21","ndvi_t28","ndvi_mean", "ndvi_anomaly", "hist_outbreak_rate","neighbor_outbreak_rate", ] # Load v5 lookup tables for anomaly computation wx_bl = _load_wx_baselines() nb_hist_map = _load_neighbor_hist_rate() ndvi_bl_map = _load_ndvi_baselines() # Round GPS to match lookup grid (0.25° for ERA5, 0.5° for NDVI) lat_g = round(block_lat * 4) / 4 # nearest 0.25° lon_g = round(block_lon * 4) / 4 # Weather baseline for this location+month bl_key = (lat_g, lon_g, curr_month) bl = wx_bl.get(bl_key, {}) temp_baseline = bl.get("t2m", wx.get("t2m_mean_28d", np.nan)) rain_baseline = bl.get("tp", wx.get("tp_total_28d", np.nan)) trend_baseline = bl.get("trend", wx.get("temp_trend", np.nan)) ndvi_baseline = ndvi_bl_map.get(bl_key, ndvi_val) # Anomaly = current forecast - climatological baseline temp_anomaly = wx.get("t2m_mean_28d", np.nan) - temp_baseline rain_anomaly = wx.get("tp_total_28d", np.nan) - rain_baseline trend_anomaly = wx.get("temp_trend", np.nan) - trend_baseline ndvi_anomaly = ndvi_val - ndvi_baseline if (ndvi_val and not np.isnan(ndvi_val)) else np.nan # Historical neighbor outbreak rate for this block+month nb_rate = nb_hist_map.get((user_block, curr_month), 0.5) # Build feature rows; also track hist_rate per crop for delta computation rows = [] hist_rates_map: dict[str, float] = {} for crop in top_crops: crop_enc_val = int(le_crop.transform([crop])[0]) if crop in le_crop.classes_ else 0 sub = block_hist[(block_hist["Crop"] == crop) & (block_hist["call_month"] == curr_month)] hist_rate = float(sub["hist_outbreak_rate"].mean()) if len(sub) > 0 else 0.0 hist_rates_map[crop] = hist_rate rows.append({ "lat": block_lat, "lon": block_lon, "call_month": curr_month, "call_season": (curr_month % 12) // 3, "year": curr_year, "crop_enc": crop_enc_val, "state_enc": state_enc_val, **{k: wx.get(k, np.nan) for k in ["t2m_mean_28d","t2m_max_28d","tp_total_28d", "tp_mean_7d","skt_mean_28d","stl1_mean_28d","temp_trend"]}, "temp_anomaly": temp_anomaly, "rain_anomaly": rain_anomaly, "trend_anomaly": trend_anomaly, "ndvi_t7": ndvi_val, "ndvi_t14": ndvi_val, "ndvi_t21": ndvi_val, "ndvi_t28": ndvi_val, "ndvi_mean": ndvi_val, "ndvi_anomaly": ndvi_anomaly, "hist_outbreak_rate": hist_rate, "neighbor_outbreak_rate": nb_rate, }) # ── 2. Score crops using district v2 stacking model (AUC 0.937) ─────────── # Replaces v5 LightGBM (which returned near-identical scores for all crops). # v2 uses 81 crop+weather+history features → meaningful differentiation per crop. results = [] with st.spinner("Computing outbreak risk with v2 stacking model…"): for crop in top_crops: cal_p, _ = _get_crop_risk_v2( user_state or "", user_district or "", crop, curr_month ) hist_rate = hist_rates_map.get(crop, 0.0) delta = cal_p - hist_rate nb_press = neighbor_pressure.get(crop, 0.0) results.append((crop, cal_p, delta, hist_rate, nb_press)) results.sort(key=lambda x: x[1], reverse=True) def _risk_label(p: float): if p >= 0.70: return "🔴 HIGH", "red" if p >= 0.45: return "🟡 MEDIUM", "orange" if p >= 0.20: return "🟢 LOW", "green" return "⚪ MINIMAL", "gray" _CATEGORY_ICON = {"pest": "🐛", "disease": "🦠", "nutrient": "🌿"} def _fmt_problem(name: str) -> str: return name.replace("_", " ").title() def _get_pest_alerts(crop: str, month: int): sub = block_alerts[(block_alerts["Crop"] == crop) & (block_alerts["call_month"] == month)] if len(sub) > 0: return sub.sort_values("count", ascending=False)[["category","exact_problem"]].values.tolist()[:3] nat = nat_alerts[(nat_alerts["Crop"] == crop) & (nat_alerts["call_month"] == month)] return nat.sort_values("count", ascending=False)[["category","exact_problem"]].values.tolist()[:3] st.markdown("### 🌾 Crop Outbreak Risk — Next 14 Days") st.caption(f"Based on 14-day weather forecast + satellite data for {user_block} | {len(neighbor_blocks)} neighboring blocks monitored") for crop, cal_p, delta, hist_rate, nb_press in results: label, _ = _risk_label(cal_p) pct = int(cal_p * 100) col_a, col_b, col_c = st.columns([3, 1, 2]) col_a.markdown(f"**{crop}**") col_b.markdown(f"`{pct}%`") # ── 3. Delta label ──────────────────────────────────────────────────── if abs(delta) >= 0.05: arrow = "↑" if delta > 0 else "↓" delta_str = f"{arrow}{abs(delta)*100:.0f}% vs normal" col_c.markdown(f"{label} 0 else 'steelblue'}'>{delta_str}", unsafe_allow_html=True) else: col_c.markdown(f"{label} ≈ normal", unsafe_allow_html=True) st.progress(min(pct, 100)) # Specific pest/disease + trigger check pest_alerts = _get_pest_alerts(crop, curr_month) info_parts = [] # ── 4. TRIGGER RULES ───────────────────────────────────────────────── triggered = [] for cat, pname in pest_alerts: icon = _CATEGORY_ICON.get(cat, "⚠️") display = f"{icon} {_fmt_problem(pname)}" if _check_triggers(pname, wx): display += " ⚡" # lightning = weather conditions are ACTIVE right now triggered.append(_fmt_problem(pname)) info_parts.append(display) if info_parts and pct >= 20: st.caption(f"Likely threats: {' · '.join(info_parts)}" + (" *(⚡ = forecast conditions actively favour this)*" if triggered else "")) # ── 5. NEIGHBOR CONTAGION ───────────────────────────────────────────── if nb_press >= 0.15 and pct >= 20: st.caption(f"🔗 Contagion signal: {nb_press*100:.0f}% of nearby blocks have high {crop} outbreak history this month.") # ── 6. EXPLAINABILITY — WHY this risk score ─────────────────────────── # B2B requirement: show the specific factors driving the prediction. # Surface top 2-3 drivers so agronomists can validate the model's reasoning. if pct >= 20: why_parts = [] t_mean = wx.get("t2m_mean_28d", np.nan) rh_mean = wx.get("_rh_mean", np.nan) rain14 = wx.get("tp_total_28d", np.nan) t_anom = temp_anomaly if not (isinstance(temp_anomaly, float) and np.isnan(temp_anomaly)) else None r_anom = rain_anomaly if not (isinstance(rain_anomaly, float) and np.isnan(rain_anomaly)) else None # Temperature driver if t_mean and not np.isnan(t_mean): if t_anom is not None and abs(t_anom) >= 1.5: direction = "above" if t_anom > 0 else "below" why_parts.append( f"🌡️ Temp **{t_mean:.1f}°C** ({'+' if t_anom>0 else ''}{t_anom:.1f}°C {direction} normal) " f"{'→ warm/humid = disease-favourable' if t_anom > 0 else '→ cooler than normal'}" ) else: why_parts.append(f"🌡️ Forecast temp: **{t_mean:.1f}°C** (near normal)") # Humidity driver if rh_mean and not np.isnan(rh_mean) and rh_mean > 0: rh_label = "🔴 high" if rh_mean >= 75 else ("🟡 moderate" if rh_mean >= 55 else "🟢 low") why_parts.append(f"💧 Humidity: **{rh_mean:.0f}%** ({rh_label}) → {'favours fungal diseases' if rh_mean>=75 else 'within normal range'}") # Rainfall driver if rain14 and not np.isnan(rain14): if r_anom is not None and abs(r_anom) >= 10: direction = "more" if r_anom > 0 else "less" why_parts.append( f"🌧️ Rain forecast: **{rain14:.0f}mm/14d** ({'+' if r_anom>0 else ''}{r_anom:.0f}mm {direction} than usual)" + (" → excess moisture = blight/rot risk" if r_anom > 20 else "") ) elif rain14 > 0: why_parts.append(f"🌧️ Rain forecast: **{rain14:.0f}mm/14d** (normal range)") # Historical context driver if hist_rate > 0.3: why_parts.append( f"📋 History: **{hist_rate*100:.0f}% of past years** this block had {crop} problems in this month" ) elif hist_rate > 0.1: why_parts.append( f"📋 History: {hist_rate*100:.0f}% historical outbreak rate for {crop} this month in this area" ) # Neighbor pressure driver if nb_press >= 0.3: why_parts.append(f"🗺️ Spread risk: **{nb_press*100:.0f}% of neighboring blocks** historically have outbreaks this month") if why_parts: with st.expander(f"🔍 Why {pct}% for {crop}? (click to expand)", expanded=False): for part in why_parts: st.markdown(f"- {part}") st.caption( "Model: Stacking ensemble (LGB + XGB + CatBoost) | " "81 features | AUC 0.937 | 19 years training data" ) st.markdown("---") # Alert summary for high-risk crops high_risk = [(c, p, d, hr, nb) for c, p, d, hr, nb in results if p >= 0.45] if high_risk: st.markdown("### ⚠️ Alert Summary") for crop, cal_p, delta, hist_rate, nb_press in high_risk[:4]: label, _ = _risk_label(cal_p) pest_alerts = _get_pest_alerts(crop, curr_month) triggered_pests = [_fmt_problem(pn) for _, pn in pest_alerts if _check_triggers(pn, wx)] all_pests = [f"{_CATEGORY_ICON.get(cat,'⚠️')} **{_fmt_problem(ep)}**" for cat, ep in pest_alerts] delta_note = "" if abs(delta) >= 0.05: delta_note = f" (↑{delta*100:.0f}% above normal)" if delta > 0 else f" (↓{abs(delta)*100:.0f}% below normal)" body = (f"**{crop}** — {label} ({int(cal_p*100)}%){delta_note} in **{user_block}** over next 14 days.\n\n") if all_pests: body += f"Likely threats: {', '.join(all_pests)}\n\n" if triggered_pests: body += f"⚡ **Weather conditions right now actively favour: {', '.join(triggered_pests)}** — act soon.\n\n" if nb_press >= 0.15: body += f"🔗 {nb_press*100:.0f}% of nearby blocks show high historical outbreak pressure.\n\n" # Inline WHY for alert summary (B2B explainability requirement) t_mean = wx.get("t2m_mean_28d", np.nan) rh_mean = wx.get("_rh_mean", 0) rain14 = wx.get("tp_total_28d", np.nan) why_inline = [] if t_mean and not np.isnan(t_mean): t_anom_v = temp_anomaly if not (isinstance(temp_anomaly, float) and np.isnan(temp_anomaly)) else 0 if abs(t_anom_v) >= 1.5: why_inline.append(f"temp {t_mean:.1f}°C ({'+' if t_anom_v>0 else ''}{t_anom_v:.1f}°C vs normal)") else: why_inline.append(f"temp {t_mean:.1f}°C") if rh_mean and rh_mean > 0: why_inline.append(f"humidity {rh_mean:.0f}%") if rain14 and not np.isnan(rain14): why_inline.append(f"rain {rain14:.0f}mm/14d") if hist_rate > 0.2: why_inline.append(f"{hist_rate*100:.0f}% historical rate") if why_inline: body += f"📊 **Key factors:** {' · '.join(why_inline)}\n\n" st.warning(body) # ── One-click "Ask chatbot" button ──────────────────────────────── threat_str_btn = (", ".join(_fmt_problem(ep) for _, ep in pest_alerts) if pest_alerts else "pest outbreak") btn_label = f"💬 How to protect my {crop} from {threat_str_btn.split(',')[0]}?" if st.button(btn_label, key=f"ew_ask_{crop}", use_container_width=True): # Pre-fill the chatbot with a specific actionable question st.session_state["prefill_query"] = ( f"Early warning system ne alert kiya hai ki {user_block} mein " f"{crop} mein {threat_str_btn} ka khatra hai agle 14 dinon mein. " f"Abhi se kya kadam uthaun? Konsi dawai lagaun aur kab?" ) st.rerun() # Store in session for chatbot context alert_parts = [] for c, p, d, hr, nb in high_risk[:3]: pests = _get_pest_alerts(c, curr_month) threat_str = ", ".join(_fmt_problem(ep) for _, ep in pests) if pests else "outbreak" alert_parts.append(f"{c}: {threat_str} ({int(p*100)}%)") st.session_state["ew_alert"] = ( f"Early warning detected for {user_block}, {user_district}, {user_state} — next 14 days: " + "; ".join(alert_parts) ) else: st.success("✅ No significant outbreaks predicted for this location in the next 14 days.") st.session_state.pop("ew_alert", None) st.markdown("---") st.caption( f"⚙️ Stacking Ensemble (LGB + XGB + CatBoost) v2 | 81 features: weather + crop × pest baselines + spatial contagion | " f"AUC 0.937 | 475 districts · 26 crop groups · 19-year training data (2007–2025) | " f"1-month early warning | {len(neighbor_blocks)} neighbor blocks · {len(top_crops)} in-season crops shown" ) # ── Streamlit UI ────────────────────────────────────────────────────────────── def _sidebar(retriever: KCCRetriever) -> dict: """Render sidebar controls; return settings dict.""" # Branded sidebar header st.sidebar.markdown("""
🌾 Farm Advisor
AI Farm Advisor
""", unsafe_allow_html=True) # System status (no technical details visible to clients) st.sidebar.markdown('

⚡ System Status

', unsafe_allow_html=True) st.sidebar.markdown('
✅ All Systems Online
Knowledge base ready · AI active
', unsafe_allow_html=True) st.sidebar.markdown('
', unsafe_allow_html=True) st.sidebar.markdown('

📍 Your Location

', unsafe_allow_html=True) st.sidebar.caption("Set once — weather, crops, prices all update for your area") # Load location data for cascading dropdowns loc_df = _load_location_lookup() all_states = sorted(loc_df["StateName"].unique().tolist()) if len(loc_df) else [] # State dropdown state_options = ["— Select State —"] + all_states prev_state = st.session_state.get("user_state_sel", "— Select State —") state_sel = st.sidebar.selectbox("State", state_options, index=state_options.index(prev_state) if prev_state in state_options else 0, key="user_state_sel") user_state_loc = None if state_sel == "— Select State —" else state_sel # District dropdown (filtered by state) user_district_loc = None user_block_loc = None block_lat = block_lon = None if user_state_loc: dist_df = loc_df[loc_df["StateName"] == user_state_loc] districts = ["— Select District —"] + sorted(dist_df["DistrictName"].unique().tolist()) prev_dist = st.session_state.get("user_district_sel", "— Select District —") dist_sel = st.sidebar.selectbox("District", districts, index=districts.index(prev_dist) if prev_dist in districts else 0, key="user_district_sel") user_district_loc = None if dist_sel == "— Select District —" else dist_sel # Block dropdown (filtered by state + district) if user_district_loc: block_df = dist_df[dist_df["DistrictName"] == user_district_loc] blocks = ["— Select Block —"] + sorted(block_df["BlockName"].unique().tolist()) prev_block = st.session_state.get("user_block_sel", "— Select Block —") block_sel = st.sidebar.selectbox("Block", blocks, index=blocks.index(prev_block) if prev_block in blocks else 0, key="user_block_sel") user_block_loc = None if block_sel == "— Select Block —" else block_sel if user_block_loc: row = block_df[block_df["BlockName"] == user_block_loc].iloc[0] block_lat = float(row["lat"]) block_lon = float(row["lon"]) st.sidebar.success(f"📍 {user_block_loc}") st.sidebar.caption(f"GPS: {block_lat:.3f}°N, {block_lon:.3f}°E") st.sidebar.markdown('
', unsafe_allow_html=True) st.sidebar.markdown('

⚙️ Retrieval Settings

', unsafe_allow_html=True) top_k = st.sidebar.slider( "Results to retrieve (top-K)", min_value=1, max_value=15, value=config.TOP_K, step=1, ) min_score = st.sidebar.slider( "Min. similarity score", min_value=0.0, max_value=1.0, value=0.0, step=0.05, help="Discard results below this cosine similarity threshold.", ) deduplicate = st.sidebar.checkbox("Deduplicate identical answers", value=True) show_sources = st.sidebar.checkbox("Show sources after each answer", value=True) # Store location in session state for use across tabs st.session_state["user_state_loc"] = user_state_loc st.session_state["user_district_loc"] = user_district_loc st.session_state["user_block_loc"] = user_block_loc st.session_state["block_lat"] = block_lat st.session_state["block_lon"] = block_lon # Also set active_state for existing weather/mandi features manual_state = user_state_loc # Live weather widget in sidebar wx_state = manual_state or st.session_state.get("active_state") if wx_state and wx_state in STATE_COORDS: with st.sidebar: wx = _fetch_weather(wx_state) daily = wx.get("daily", {}) if daily.get("time"): tmax = daily["temperature_2m_max"] rain = daily.get("precipitation_sum", [0]) code = daily.get("weathercode", [0]) desc = _WMO_CODES.get(code[0] if code else 0, "") st.sidebar.markdown(f"**🌤️ Today — {wx_state}**") st.sidebar.markdown( f"{desc} | 🌡️ {tmax[0]}°C | 🌧️ {rain[0]:.1f}mm rain" ) st.sidebar.markdown('
', unsafe_allow_html=True) if st.sidebar.button("🗑️ Clear Chat History"): st.session_state.messages = [] st.session_state.retrieval = [] st.session_state.active_crop = None st.session_state.active_problem = "general" st.session_state.active_state = None st.session_state.topic_origin = None st.rerun() return { "top_k": top_k, "min_score": min_score, "deduplicate": deduplicate, "show_sources": show_sources, "manual_state": manual_state, } def _render_sources(docs: List[RetrievedDoc]) -> None: """Render retrieved source documents in an expander.""" if not docs: return # Warn if majority of sources are pre-2015 (outdated pesticide registrations) years = [] for doc in docs: try: years.append(int(doc.year)) except (ValueError, TypeError): pass old_count = sum(1 for y in years if y < 2015) if years and old_count > len(years) // 2: st.caption( f"⚠️ **Note**: {old_count}/{len(docs)} sources are from before 2015. " "Pesticide registrations and doses may have been updated — verify with " "your local agriculture extension officer or KVK." ) with st.expander(f"📚 Sources ({len(docs)} retrieved Q&A pairs)", expanded=False): for doc in docs: try: yr = int(doc.year) year_tag = f"📅 {doc.year}" + (" ⚠️ *old*" if yr < 2015 else "") except (ValueError, TypeError): year_tag = f"📅 {doc.year}" st.markdown( f"**#{doc.rank}** — " f"🎯 {doc.score_pct} similarity | " f"📍 {doc.state} | 🌿 {doc.crop} | {year_tag}" ) st.markdown(f"> **Q:** {doc.query}") st.markdown(f"> **A:** {doc.answer[:500]}{'…' if len(doc.answer) > 500 else ''}") st.markdown("---") _IMAGE_VISION_PROMPT = """You are an expert plant pathologist. Carefully examine this crop photo. Return ONLY a valid JSON object with these exact fields: { "crop": "", "condition": "", "problem_type": "", "confidence": "", "visible_symptoms": "<1-2 sentence description of what you see in the image>" } Rules: - crop: identify from leaf/plant shape, colour, structure - condition: be specific (e.g. "Yellow Rust", "Stem Borer", "Nitrogen Deficiency") - If the image is unclear, set confidence to "Low" and do your best - Return ONLY the JSON, absolutely no other text""" @st.cache_data(ttl=3600, show_spinner=False) def _diagnose_image_gemini(image_bytes: bytes) -> dict: """ Use Gemini Vision (multimodal) to diagnose crop disease from an image. Replaces the HuggingFace classifier which: - doesn't support Indian crops (wheat, rice, cotton missing from PlantVillage) - has cold-start / 503 issues on the free inference API Gemini Vision supports ANY crop and is always available on our existing API key. Cached by image content hash (ttl=1h) — won't re-call Gemini on tab switch. Returns dict: {crop, condition, problem_type, confidence, visible_symptoms} """ client = _get_gemini_client() if client is None: return {} try: from google.genai import types as _gtypes # Detect image mime type from magic bytes if image_bytes[:3] == b'\xff\xd8\xff': mime = "image/jpeg" elif image_bytes[:8] == b'\x89PNG\r\n\x1a\n': mime = "image/png" elif image_bytes[:4] == b'RIFF' and image_bytes[8:12] == b'WEBP': mime = "image/webp" else: mime = "image/jpeg" # fallback response = client.models.generate_content( model = config.GEMINI_MODEL, contents = [ _gtypes.Part.from_bytes(data=image_bytes, mime_type=mime), _IMAGE_VISION_PROMPT, ], ) text = response.text.strip() match = re.search(r'\{.*\}', text, re.DOTALL) if match: return json.loads(match.group()) return {} except Exception as e: # Return error in dict so @st.cache_data (which forbids session_state) can propagate it return {"_error": str(e)} def _render_image_diagnosis(retriever: KCCRetriever, settings: dict) -> None: """Render the image diagnosis tab (Gemini Vision powered).""" st.markdown("### 📷 Upload a leaf/plant photo for instant disease diagnosis") st.caption( "Powered by Gemini Vision — supports **any Indian crop**: " "Wheat, Rice, Cotton, Tomato, Potato, Sugarcane, and more." ) uploaded = st.file_uploader( "Upload crop photo", type=["jpg", "jpeg", "png", "webp"], help="Take a close-up photo of the affected leaf or plant part." ) if uploaded is None: st.info("Upload a photo to get started. Gemini AI will identify the crop, disease, and suggest treatment.") return # Read bytes FIRST — st.image() advances the file pointer to EOF, # so uploaded.read() after st.image() returns b"" (empty). image_bytes = uploaded.read() col1, col2 = st.columns([1, 1]) with col1: st.image(image_bytes, caption="Uploaded image", use_container_width=True) with col2: with st.spinner("🔬 Gemini Vision analyzing your crop photo…"): diagnosis = _diagnose_image_gemini(image_bytes) if diagnosis.get("_error"): st.error(f"Could not analyze image. Error: {diagnosis['_error']}") return if not diagnosis or not diagnosis.get("crop"): st.error("Could not analyze image. Gemini returned an unexpected response. Please try another photo.") return crop_name = diagnosis.get("crop", "Unknown") condition = diagnosis.get("condition", "Unknown") problem_type = diagnosis.get("problem_type", "disease") confidence = diagnosis.get("confidence", "Medium") symptoms = diagnosis.get("visible_symptoms", "") # Confidence colour conf_color = {"High": "🟢", "Medium": "🟡", "Low": "🔴"}.get(confidence, "🟡") st.markdown(f"### 🌿 Crop: **{crop_name}**") st.markdown(f"### 🦠 Condition: **{condition}**") st.markdown(f"{conf_color} Confidence: **{confidence}**") if symptoms: st.caption(f"*{symptoms}*") if problem_type == "healthy" or condition.lower() == "healthy": st.success("✅ Your plant looks healthy! No disease or pest detected.") return # Auto-query RAG for treatment st.markdown("---") st.markdown("**💊 Getting treatment advice from KCC knowledge base…**") auto_query = f"{crop_name} {condition} control treatment" with st.spinner("Retrieving KCC advisory…"): docs = multi_step_retrieve( retriever, auto_query, normalize_query(auto_query), crop_name, problem_type, settings, ) if docs: context = retriever.format_context(docs) safety = SAFETY_GUARDRAILS.get(problem_type, "") meta = ( f"DETECTED CROP: {crop_name}\n" f"DETECTED CONDITION: {condition} (Gemini Vision, confidence: {confidence})\n" f"PROBLEM TYPE: {problem_type.upper()}\n" f"VISIBLE SYMPTOMS: {symptoms}\n" f"{safety}\n\n" ) prompt = _build_prompt( f"My {crop_name} has {condition}. What should I do?", meta + context, [], problem_type, "Hindi" ) with st.chat_message("assistant", avatar="🤖"): img_response = st.write_stream(_stream_llm_response(prompt)) for w in check_banned_pesticides(img_response): st.error(w) _render_sources(docs) else: st.warning("No specific KCC advice found. Consult your local KVK or call KCC helpline 1551.") # ── MSP 2025-26 (Ministry of Agriculture, GoI) ─────────────────────────────── # Source: PIB — Kharif MSP 2025-26 (Jun 2025) + Rabi MSP 2025-26 (Oct 2024) _MSP_2025: dict[str, int] = { # Rabi 2025-26 "Wheat": 2425, # PIB Oct 2024 (unchanged) "Gram": 5650, # was ₹5,440 "Mustard": 6200, # was ₹5,950 # Kharif 2025-26 "Rice": 2425, # was ₹2,300 "Maize": 2400, # was ₹2,225 "Sorghum": 3590, # was ₹3,371 "Bajra": 2735, # was ₹2,625 "Arhar": 7550, # unchanged "Moong": 8682, # was ₹8,558 "Urad": 7800, # was ₹7,400 "Groundnut": 7011, # was ₹6,783 "Soybean": 4892, # unchanged "Sunflower": 7280, # unchanged "Sesame": 9574, # was ₹9,267 "Cotton": 7121, # medium staple (long: ₹7,521) } # ── ICAR Variety Recommender Data ───────────────────────────────────────────── # Source: ICAR-CIMMYT, State Agriculture Universities, Seed Authority of India # Format: crop → list of {name, maturity_days, yield_q_acre, states, traits, seed_rate, source} _VARIETY_DATA: dict[str, list[dict]] = { "Soybean": [ {"name": "JS 9305", "days": "95-100", "yield": "8-12", "seed_rate": "30-35 kg/acre", "states": ["Madhya Pradesh", "Maharashtra", "Rajasthan"], "traits": "High yielding, YMV tolerant, suitable for late sowing", "source": "JNKVV Jabalpur / state seed corp"}, {"name": "NRC 37", "days": "100-105", "yield": "10-14", "seed_rate": "30-35 kg/acre", "states": ["Madhya Pradesh", "Maharashtra"], "traits": "ICAR-IISR variety, charcoal rot tolerant, high protein", "source": "ICAR-IISR Indore / NSC"}, {"name": "JS 335", "days": "95-100", "yield": "8-10", "seed_rate": "30-35 kg/acre", "states": ["Madhya Pradesh", "Maharashtra", "Rajasthan", "Gujarat"], "traits": "Most popular, wide adaptability, good for rain-fed", "source": "State seed corp / private dealers"}, ], "Wheat": [ {"name": "HD 3086 (Pusa Samridhi)", "days": "120-125", "yield": "18-22", "seed_rate": "40-45 kg/acre", "states": ["Uttar Pradesh", "Bihar", "West Bengal", "Madhya Pradesh"], "traits": "High yield, rust resistant, ICAR-IARI variety", "source": "ICAR-IARI Delhi / NSC / state corp"}, {"name": "PBW 725", "days": "155-160", "yield": "20-25", "seed_rate": "40-45 kg/acre", "states": ["Punjab", "Haryana", "Himachal Pradesh"], "traits": "Best for Punjab-Haryana, lodging resistant, chapati quality", "source": "PAU Ludhiana / Punjab Seed Corp"}, {"name": "DBW 187", "days": "145-150", "yield": "20-24", "seed_rate": "40 kg/acre", "states": ["Haryana", "Uttar Pradesh", "Rajasthan", "Madhya Pradesh"], "traits": "Yellow rust resistant, high protein, good chapati quality", "source": "ICAR-IIWBR Karnal / state seed corp"}, ], "Rice": [ {"name": "Pusa Basmati 1121", "days": "140-145", "yield": "16-18", "seed_rate": "5-6 kg/acre (nursery)", "states": ["Punjab", "Haryana", "Uttar Pradesh"], "traits": "Premium export basmati, extra-long grain, 20-25% price premium", "source": "ICAR-IARI / state seed corp"}, {"name": "Swarna (MTU 7029)", "days": "145-155", "yield": "22-26", "seed_rate": "5-6 kg/acre", "states": ["West Bengal", "Odisha", "Andhra Pradesh", "Bihar", "Assam"], "traits": "Most popular non-basmati, flood tolerant, good for Eastern India", "source": "ANGRAU / state seed corp"}, {"name": "IR 64", "days": "125-130", "yield": "20-24", "seed_rate": "5-6 kg/acre", "states": ["Karnataka", "Tamil Nadu", "Maharashtra", "Andhra Pradesh"], "traits": "Blast resistant, short duration, widely available", "source": "State seed corporations"}, ], "Cotton": [ {"name": "MRC 7031 (Bt)", "days": "160-175", "yield": "12-16 q lint", "seed_rate": "1 packet/acre (450g)", "states": ["Maharashtra", "Madhya Pradesh", "Telangana"], "traits": "Bt hybrid, CLCuV tolerant, good staple length, medium bollworm resistance", "source": "Mahyco / agri dealers"}, {"name": "RCH 650 (Bt)", "days": "155-165", "yield": "10-14 q lint", "seed_rate": "1 packet/acre", "states": ["Gujarat", "Rajasthan", "Punjab"], "traits": "Compact plant, suitable for high density planting, drought tolerant", "source": "Rasi Seeds / agri dealers"}, {"name": "Ajeet 155 (Non-Bt)", "days": "150-160", "yield": "10-12 q lint", "seed_rate": "1 packet/acre", "states": ["Telangana", "Andhra Pradesh", "Karnataka"], "traits": "Non-Bt option for organic farming, good fibre quality", "source": "Ajeet Seeds / agri dealers"}, ], "Maize": [ {"name": "DKC 9144", "days": "95-105", "yield": "25-30", "seed_rate": "8-10 kg/acre", "states": ["Karnataka", "Andhra Pradesh", "Bihar", "Rajasthan"], "traits": "High yield hybrid, good in kharif + rabi both, widely available", "source": "Dekalb-Bayer / agri dealers"}, {"name": "P3401 (Pioneer)", "days": "90-100", "yield": "28-35", "seed_rate": "8-10 kg/acre", "states": ["Punjab", "Haryana", "Madhya Pradesh", "Rajasthan"], "traits": "Best yield potential, lodging resistant, early maturity", "source": "Corteva Agriscience / agri dealers"}, {"name": "Shaktiman 1 (OPV)", "days": "90-100", "yield": "18-22", "seed_rate": "8-10 kg/acre", "states": ["Uttar Pradesh", "Bihar", "West Bengal", "Madhya Pradesh"], "traits": "Open-pollinated, cheaper seed, farmer can save seed", "source": "VPKAS / state seed corp"}, ], "Mustard": [ {"name": "Pusa Bold", "days": "120-125", "yield": "8-10", "seed_rate": "1.5-2 kg/acre", "states": ["Rajasthan", "Haryana", "Uttar Pradesh", "Madhya Pradesh"], "traits": "Most popular, high oil content (42%), bold seed", "source": "ICAR-IARI / state seed corp / NSC"}, {"name": "RH 749", "days": "120-125", "yield": "9-11", "seed_rate": "1.5 kg/acre", "states": ["Haryana", "Punjab", "Rajasthan"], "traits": "Aphid tolerant, Alternaria tolerant, high oil content", "source": "HAU Hisar / Haryana seed corp"}, {"name": "Giriraj", "days": "125-130", "yield": "8-10", "seed_rate": "1.5-2 kg/acre", "states": ["Rajasthan", "Gujarat", "Madhya Pradesh"], "traits": "Drought tolerant, good for arid zones, consistent yield", "source": "IARI / state seed corp"}, ], "Groundnut": [ {"name": "GG 20", "days": "110-115", "yield": "15-18", "seed_rate": "60-65 kg/acre", "states": ["Gujarat", "Rajasthan"], "traits": "ICRISAT variety, high oil, tolerant to dry spells", "source": "GAU Junagadh / Gujarat seed corp"}, {"name": "TAG 24", "days": "105-110", "yield": "14-16", "seed_rate": "60-65 kg/acre", "states": ["Andhra Pradesh", "Karnataka", "Tamil Nadu"], "traits": "Tolerant to late leaf spot, drought tolerant, widely grown South India", "source": "ICRISAT / state seed corp"}, {"name": "TG 37A", "days": "100-105", "yield": "14-18", "seed_rate": "55-60 kg/acre", "states": ["Madhya Pradesh", "Maharashtra", "Rajasthan"], "traits": "Early maturing, suitable for short season, good oil content", "source": "ICRISAT / agri dealers"}, ], "Arhar": [ {"name": "Asha (ICPL 87119)", "days": "155-165", "yield": "8-12", "seed_rate": "6-8 kg/acre", "states": ["Madhya Pradesh", "Maharashtra", "Gujarat", "Uttar Pradesh"], "traits": "Wilt resistant, medium duration, widely adaptable", "source": "ICRISAT / state seed corp"}, {"name": "Maruti", "days": "150-160", "yield": "8-10", "seed_rate": "6-8 kg/acre", "states": ["Maharashtra", "Karnataka", "Andhra Pradesh"], "traits": "Early maturing for medium duration, wilt tolerant", "source": "UAS Dharwad / state seed corp"}, ], "Gram": [ {"name": "JG 11", "days": "100-110", "yield": "8-10", "seed_rate": "25-30 kg/acre", "states": ["Madhya Pradesh", "Maharashtra", "Rajasthan"], "traits": "JNKVV variety, Fusarium wilt resistant, widely grown in MP", "source": "JNKVV Jabalpur / state seed corp"}, {"name": "Pusa 362", "days": "115-120", "yield": "8-10", "seed_rate": "25-30 kg/acre", "states": ["Uttar Pradesh", "Bihar", "Haryana", "Rajasthan"], "traits": "ICAR-IARI variety, ascochyta blight resistant", "source": "ICAR-IARI / state seed corp / NSC"}, ], "Bajra": [ {"name": "HHB 67 Improved", "days": "65-70", "yield": "10-14", "seed_rate": "1.5-2 kg/acre", "states": ["Haryana", "Rajasthan", "Gujarat", "Punjab"], "traits": "Downy mildew resistant, drought tolerant, best for arid/semi-arid zones", "source": "HAU Hisar / AICSIP / state seed corp"}, {"name": "ICMH 356 (hybrid)", "days": "70-75", "yield": "14-18", "seed_rate": "1.5 kg/acre", "states": ["Rajasthan", "Gujarat", "Maharashtra", "Andhra Pradesh"], "traits": "High-yield hybrid, ICRISAT variety, dual purpose (grain + fodder)", "source": "ICRISAT / Kaveri Seeds / agri dealers"}, {"name": "Pusa Composite 383", "days": "75-80", "yield": "10-12", "seed_rate": "2 kg/acre", "states": ["All states"], "traits": "OPV variety, farmer can save seed, wide adaptability, consistent yield", "source": "ICAR-IARI / NSC / state seed corp"}, ], "Moong": [ {"name": "Pusa Vishal (MH 421)", "days": "65-70", "yield": "5-7", "seed_rate": "10-12 kg/acre", "states": ["Uttar Pradesh", "Bihar", "Haryana", "Rajasthan", "Punjab"], "traits": "ICAR-IARI variety, MYMV tolerant (Yellow Mosaic), Kharif + Zaid both seasons", "source": "ICAR-IARI / state seed corp / NSC"}, {"name": "SML 668", "days": "60-65", "yield": "5-6", "seed_rate": "10-12 kg/acre", "states": ["Punjab", "Haryana", "Rajasthan", "Madhya Pradesh"], "traits": "Short duration, ideal for Zaid/summer sowing, disease tolerant", "source": "PAU Ludhiana / state seed corp"}, {"name": "HUM 16 (Pant Moong 4)", "days": "65-70", "yield": "4-6", "seed_rate": "10-12 kg/acre", "states": ["Uttar Pradesh", "Bihar", "Madhya Pradesh", "West Bengal"], "traits": "Yellow mosaic resistant, suitable for late Kharif, good germination", "source": "GBPUAT Pantnagar / state seed corp"}, ], "Urad": [ {"name": "Pant U 35", "days": "70-75", "yield": "5-7", "seed_rate": "10-12 kg/acre", "states": ["Uttar Pradesh", "Bihar", "Madhya Pradesh", "West Bengal"], "traits": "ICAR variety, powdery mildew tolerant, bold grain, good cooking quality", "source": "GBPUAT Pantnagar / state seed corp"}, {"name": "LBG 752", "days": "65-70", "yield": "5-6", "seed_rate": "8-10 kg/acre", "states": ["Andhra Pradesh", "Karnataka", "Tamil Nadu", "Telangana"], "traits": "Short duration, suitable for South India, YMD tolerant", "source": "ANGRAU / state seed corp"}, {"name": "KU 300", "days": "70-75", "yield": "5-6", "seed_rate": "10-12 kg/acre", "states": ["Rajasthan", "Gujarat", "Madhya Pradesh"], "traits": "Drought tolerant, suitable for rainfed Kharif, medium season", "source": "SKN Agriculture University / state seed corp"}, ], "Sunflower": [ {"name": "KBSH 44 (hybrid)", "days": "90-95", "yield": "8-10", "seed_rate": "2-2.5 kg/acre", "states": ["Karnataka", "Andhra Pradesh", "Maharashtra", "Tamil Nadu"], "traits": "Best yield hybrid, large head size, high oil content (42%), bird-resistant variety", "source": "UAS Dharwad / state seed corp / agri dealers"}, {"name": "MSFH 17 (hybrid)", "days": "90-100", "yield": "8-12", "seed_rate": "2-2.5 kg/acre", "states": ["Rajasthan", "Maharashtra", "Odisha", "Bihar"], "traits": "Tolerant to Sclerotinia wilt, uniform maturity, good for Rabi/Zaid season", "source": "State seed corp / agri dealers"}, {"name": "EC 68415 (OPV)", "days": "95-100", "yield": "6-8", "seed_rate": "2.5-3 kg/acre", "states": ["All states"], "traits": "Open-pollinated, farmer can save seed, wide adaptability, lower cost", "source": "NSC / state seed corp"}, ], "Sesame": [ {"name": "TMV 7", "days": "75-80", "yield": "4-5", "seed_rate": "1.5-2 kg/acre", "states": ["Tamil Nadu", "Andhra Pradesh", "Karnataka", "Telangana"], "traits": "TNAU variety, phyllody tolerant, high oil content (52%), best for South India", "source": "TNAU / state seed corp"}, {"name": "RT 351", "days": "80-85", "yield": "3-5", "seed_rate": "1.5-2 kg/acre", "states": ["Rajasthan", "Gujarat", "Madhya Pradesh", "Maharashtra"], "traits": "Drought tolerant, rust resistant, suitable for arid/semi-arid zones", "source": "CAZRI / RAU / state seed corp"}, {"name": "Pragati (OPV)", "days": "75-80", "yield": "3-4", "seed_rate": "1.5-2 kg/acre", "states": ["All states"], "traits": "Wide adaptability, branching type, farmer can save seed, reliable", "source": "ICAR-DRMR / NSC / state seed corp"}, ], "Onion": [ {"name": "Bhima Shakti", "days": "110-120", "yield": "100-120", "seed_rate": "3-4 kg seed (for nursery) → 50,000-60,000 seedlings/acre", "states": ["Maharashtra", "Karnataka", "Andhra Pradesh", "Madhya Pradesh"], "traits": "DOGR variety, pink colour, good storage, high yield", "source": "ICAR-DOGR Pune / NHRDF Nasik"}, {"name": "Agrifound Dark Red", "days": "110-120", "yield": "80-100", "seed_rate": "3-4 kg seed (for nursery) → 50,000-60,000 seedlings/acre", "states": ["All states"], "traits": "Dark red skin, good export quality, 3-4 months storage", "source": "NHRDF Nasik / state seed corp"}, ], "Tomato": [ {"name": "Arka Rakshak (hybrid)", "days": "70-80", "yield": "100-120", "seed_rate": "15-20g seed (nursery) → 5,000-6,000 seedlings/acre", "states": ["Karnataka", "Andhra Pradesh", "Maharashtra", "Tamil Nadu"], "traits": "IIHR variety, TYLCV resistant, determinate, high lycopene", "source": "ICAR-IIHR Bangalore / Syngenta"}, {"name": "NS 501", "days": "65-75", "yield": "80-100", "seed_rate": "15-20g seed (nursery) → 5,000-6,000 seedlings/acre", "states": ["All states"], "traits": "Popular hybrid, long shelf life, good for market", "source": "Namdhari Seeds / agri dealers"}, ], } # ── Input Cost + Profit Calculator Data ─────────────────────────────────────── # Per-acre estimates (2024-25 market rates, district-level variation ±20%) # Source: CACP (Commission for Agricultural Costs and Prices) reports + state govt data _INPUT_COST: dict[str, dict] = { "Soybean": {"seed": 1800, "fertilizer": 1500, "pesticide": 800, "labor": 4000, "irrigation": 500, "misc": 500, "yield_q": 10, "season": "Kharif"}, "Wheat": {"seed": 1600, "fertilizer": 3000, "pesticide": 600, "labor": 5000, "irrigation": 2500, "misc": 500, "yield_q": 20, "season": "Rabi"}, "Rice": {"seed": 800, "fertilizer": 2500, "pesticide": 1000,"labor": 7000, "irrigation": 2000, "misc": 700, "yield_q": 22, "season": "Kharif"}, "Cotton": {"seed": 800, "fertilizer": 3500, "pesticide": 2500,"labor": 8000, "irrigation": 1500, "misc": 1000, "yield_q": 14, "season": "Kharif"}, "Maize": {"seed": 1200, "fertilizer": 2500, "pesticide": 600, "labor": 4000, "irrigation": 1000, "misc": 400, "yield_q": 28, "season": "Kharif/Rabi"}, "Mustard": {"seed": 400, "fertilizer": 2000, "pesticide": 400, "labor": 3000, "irrigation": 1500, "misc": 300, "yield_q": 9, "season": "Rabi"}, "Groundnut": {"seed": 4000, "fertilizer": 1500, "pesticide": 800, "labor": 5000, "irrigation": 1000, "misc": 500, "yield_q": 15, "season": "Kharif"}, "Gram": {"seed": 1500, "fertilizer": 1200, "pesticide": 400, "labor": 3000, "irrigation": 800, "misc": 300, "yield_q": 8, "season": "Rabi"}, "Arhar": {"seed": 800, "fertilizer": 1500, "pesticide": 600, "labor": 4500, "irrigation": 600, "misc": 400, "yield_q": 8, "season": "Kharif"}, "Moong": {"seed": 1200, "fertilizer": 800, "pesticide": 400, "labor": 2500, "irrigation": 600, "misc": 200, "yield_q": 5, "season": "Kharif/Zaid"}, "Urad": {"seed": 1000, "fertilizer": 800, "pesticide": 400, "labor": 2500, "irrigation": 600, "misc": 200, "yield_q": 5, "season": "Kharif"}, "Onion": {"seed": 2000, "fertilizer": 3000, "pesticide": 1500,"labor": 8000, "irrigation": 2000, "misc": 1000, "yield_q": 100,"season": "Rabi/Kharif"}, "Tomato": {"seed": 2500, "fertilizer": 3500, "pesticide": 2000,"labor": 8000, "irrigation": 2000, "misc": 1500, "yield_q": 100,"season": "All"}, "Bajra": {"seed": 400, "fertilizer": 1500, "pesticide": 300, "labor": 2500, "irrigation": 400, "misc": 200, "yield_q": 12, "season": "Kharif"}, "Sunflower": {"seed": 600, "fertilizer": 2000, "pesticide": 500, "labor": 3000, "irrigation": 1200, "misc": 300, "yield_q": 8, "season": "Rabi/Zaid"}, "Sesame": {"seed": 300, "fertilizer": 1200, "pesticide": 400, "labor": 2500, "irrigation": 600, "misc": 200, "yield_q": 4, "season": "Kharif/Zaid"}, } # ── Soil Prep Checklist Data ─────────────────────────────────────────────────── _SOIL_PREP: dict[str, list[dict]] = { "cereal": [ # wheat, rice, maize, bajra, sorghum {"days_before": 30, "task": "Deep ploughing", "detail": "1 deep plough (25-30cm) to break hardpan + expose soil pests to sun. Use MB plough or chisel plough."}, {"days_before": 21, "task": "FYM / Compost application", "detail": "Apply 5-8 tonnes FYM or 2-3 tonnes vermicompost per acre. Mix well into soil."}, {"days_before": 14, "task": "Soil Health Card check", "detail": "Get SHC from KVK — apply fertilizer as per SHC recommendation only (saves 20-30% cost)."}, {"days_before": 10, "task": "Field levelling", "detail": "Level field for uniform irrigation. Laser levelling saves 30% water."}, {"days_before": 7, "task": "Basal fertilizer (DAP/SSP)", "detail": "Apply DAP 50kg/acre (or as per SHC). Mix into soil before sowing."}, {"days_before": 1, "task": "Seed treatment (MANDATORY)", "detail": "Treat seed with Carbendazim 50%WP @2g/kg + Imidacloprid 70%WS @5g/kg to prevent early disease and sucking pests."}, {"days_before": 0, "task": "Sowing at correct depth + irrigation plan", "detail": "Wheat: 5-6cm depth (Crown Root Irrigation at 20-25 DAS is CRITICAL — do not miss). Rice: transplant 3-4 week nursery seedlings, maintain 5cm water. Maize: 5cm depth; irrigate at knee-high (25 DAS), tasselling (55 DAS), grain fill (75 DAS). Bajra: 2-3cm depth; first irrigation at 3-4 leaf stage if rains fail."}, ], "oilseed": [ # soybean, mustard, groundnut, sunflower, sesame {"days_before": 30, "task": "Deep ploughing + lime if needed", "detail": "If soil pH < 6.0 (soybean): apply lime 200-400kg/acre. Plough to 20-25cm."}, {"days_before": 21, "task": "FYM + phosphate fertilizer", "detail": "FYM 4-5 tonnes/acre + SSP 100-150kg/acre as basal — critical for oilseed pod setting."}, {"days_before": 14, "task": "Soil Health Card + micronutrient test", "detail": "Oilseeds are sensitive to Sulphur and Zinc deficiency. Apply Gypsum 200kg/acre if Sulphur deficient."}, {"days_before": 7, "task": "Field preparation + ridges (groundnut)", "detail": "For groundnut: make ridges 30cm apart. For soybean: flat or slight ridge. Ensure good drainage."}, {"days_before": 1, "task": "Rhizobium + PSB seed treatment", "detail": "MANDATORY for soybean/groundnut: Soybean → Bradyrhizobium japonicum @200g/10kg. Groundnut → Bradyrhizobium sp. (Arachis) @200g/10kg. PSB @200g/10kg for all. Apply Carbendazim fungicide FIRST (dry 30 min), THEN biofertilizers — never mix simultaneously. Mustard/Sunflower: only fungicide (Thiram @3g/kg), no Rhizobium needed."}, {"days_before": 0, "task": "Sowing depth + irrigation schedule", "detail": "Soybean: 3-4cm depth, 30-35kg/acre — irrigate at branching (30 DAS) + flowering (45 DAS) + pod fill (60 DAS) if rains fail. Groundnut: 5-7cm, 55-65kg/acre — critical irrigation at pegging (35 DAS) + pod development (60 DAS). Mustard: 1-2cm, 1.5-2kg/acre — irrigate at branching (30 DAS) + flowering (60 DAS). Sunflower: 3-4cm, 2-3kg/acre."}, ], "pulse": [ # gram, arhar, moong, urad, lentil {"days_before": 21, "task": "Light ploughing + FYM", "detail": "Pulses prefer fine tilth. 2 cross ploughings + FYM 4 tonnes/acre. Do NOT over-fertilize N — pulses fix their own N."}, {"days_before": 14, "task": "Soil moisture check", "detail": "Pulses need dry seedbed. Sow only when top 5cm soil is moist but not wet — prevents damping off."}, {"days_before": 7, "task": "Phosphate fertilizer", "detail": "DAP 25-30kg/acre as starter dose — do not apply urea (pulses fix N via Rhizobium)."}, {"days_before": 1, "task": "Rhizobium + Carbendazim treatment", "detail": "Use CROP-SPECIFIC strain: Gram/Lentil → Mesorhizobium ciceri | Arhar → Bradyrhizobium sp. (Cajanus) | Moong/Urad → Bradyrhizobium sp. (Vigna). Dose: @200g/10kg seed. Apply Carbendazim @2g/kg first, dry 30 min, then Rhizobium. Sow same day."}, {"days_before": 0, "task": "Sowing depth + inter-row spacing", "detail": "Gram: 5-7cm depth, 30×10cm spacing | Arhar: 5cm, 60-70×20cm | Moong/Urad: 3-4cm, 30×10cm. Germination test ≥80% mandatory. Irrigate at: branching (25 DAS), flowering (45 DAS), pod fill (65 DAS) — SKIP if soil has moisture."}, ], "vegetable": [ # tomato, onion, chilli, brinjal, etc. {"days_before": 30, "task": "Nursery preparation (transplanted crops)", "detail": "Prepare raised nursery bed (15cm height, 1m width). Apply FYM 5-10kg/m² + Carbendazim 50%WP @2g/kg soil drench for damping off."}, {"days_before": 21, "task": "Main field deep ploughing + FYM", "detail": "3-4 ploughings to fine tilth. FYM 8-10 tonnes/acre or vermicompost 3-4 tonnes/acre. Mix well."}, {"days_before": 14, "task": "Apply lime + micronutrients", "detail": "pH 6.0-7.0 is ideal. Apply Borax 2kg/acre + ZnSO4 10kg/acre if deficient (check SHC)."}, {"days_before": 7, "task": "Bed preparation + drip/mulch if planned", "detail": "Install drip system before transplanting (saves 40-50% water). Black polythene mulch (25 micron) controls weeds + retains moisture."}, {"days_before": 1, "task": "Seedling hardening + fungicide drench", "detail": "Stop watering nursery 2 days before transplanting (hardening). Drench seedlings with Carbendazim @1g/L or Metalaxyl+Mancozeb @2.5g/L before lifting."}, {"days_before": 0, "task": "Transplanting in evening + immediate irrigation", "detail": "Transplant in evening (cooler). Water immediately after transplanting. Plant to plant distance: Tomato 45×60cm | Onion 10×15cm | Chilli 45×45cm."}, ], } def _get_crop_category(crop: str) -> str: """Map crop to soil prep category.""" _CEREAL = {"wheat","rice","maize","bajra","sorghum","barley","oat","jowar"} _OILSEED = {"soybean","mustard","groundnut","sunflower","sesame","linseed","castor","safflower"} _PULSE = {"gram","arhar","moong","urad","lentil","pea","cowpea","moth bean","pigeonpea"} _VEG = {"tomato","onion","potato","chilli","brinjal","okra","cauliflower","cabbage","carrot"} c = crop.lower().replace("/","").strip() for word in c.split(): if word in _CEREAL: return "cereal" if word in _OILSEED: return "oilseed" if word in _PULSE: return "pulse" if word in _VEG: return "vegetable" return "cereal" # default fallback def _render_variety_recommender(active_state: str | None) -> None: """Section: ICAR Variety Recommender — select crop → get top varieties for state.""" st.markdown("### 🌱 Variety Recommender — Which Seed to Buy?") st.caption("ICAR-recommended varieties for your state · Seed rate · Where to buy") col_crop, col_info = st.columns([1, 2]) with col_crop: crop_list = sorted(_VARIETY_DATA.keys()) sel_crop = st.selectbox("Select your crop", crop_list, key="vr_crop_sel") varieties = _VARIETY_DATA.get(sel_crop, []) if not varieties: st.info(f"Variety data for {sel_crop} coming soon. Ask the AI Crop Advisor below.") return # Filter to state if possible, else show all state_varieties = [v for v in varieties if not active_state or active_state in v.get("states", []) or "All states" in v.get("states", [])] if not state_varieties: state_varieties = varieties # show all if no state match # Display as cards num = len(state_varieties) cols = st.columns(num) if num <= 3 else st.columns(3) for i, v in enumerate(state_varieties[:3]): with cols[i % 3]: st.markdown(f"**🌿 {v['name']}**") st.markdown(f"⏱️ **{v['days']} days** maturity") st.markdown(f"📦 Yield: **{v['yield']} q/acre**") st.markdown(f"🌱 Seed rate: {v['seed_rate']}") st.caption(f"✅ {v['traits']}") st.caption(f"🏪 Source: {v['source']}") if active_state and active_state not in v.get("states", []) and "All states" not in v.get("states", []): st.warning(f"⚠️ Best in: {', '.join(v['states'][:2])}") with st.expander("💡 Seed Treatment (do this before every sowing)"): category = _get_crop_category(sel_crop) if category == "oilseed" and sel_crop.lower() in ("soybean","groundnut","arhar","moong","urad"): st.markdown(""" **Step 1 — Fungicide treatment:** `Carbendazim 50%WP @ 2g per kg seed` — mix well, shade dry 30 min **Step 2 — Rhizobium inoculant (legumes ONLY):** `Rhizobium @200g + PSB @200g per 10kg seed` — apply last (after fungicide), shade dry, sow immediately ⚠️ Do NOT mix Rhizobium with fungicide in same step — kills bacteria """) elif category == "cereal": st.markdown(""" **Fungicide treatment:** `Carbendazim 50%WP @2g/kg seed` (prevents seed-borne diseases) `Imidacloprid 70%WS @5g/kg seed` (prevents aphid/BPH early attack) Mix dry, shade dry 30 min, sow same day. """) else: st.markdown(""" **Rhizobium + Fungicide treatment:** `Carbendazim 50%WP @2g/kg seed` (fungicide first, dry 30 min) `Rhizobium specific strain @200g/10kg seed` (apply last) Never expose treated seed to direct sunlight. """) # ── Presow price model — AGMARKNET commodity name mapping ───────────────────── _PRESOW_CROP_MAP: dict[str, str] = { "Wheat": "Wheat", "Rice": "Rice", "Cotton": "Cotton", "Bajra": "Bajra", "Moong": "Green Gram (Moong)(Whole)", "Urad": "Black Gram (Urad)", "Sunflower": "Sunflower", "Sesame": "Sesamum(Sesame,Gingelly,Til)", "Soybean": "Soyabean", "Maize": "Maize", "Mustard": "Mustard", "Groundnut": "Groundnut", "Arhar": "Arhar(Tur)", # Whole grain as traded at mandi (not processed dal) "Gram": "Chana/Bengal Gram", "Onion": "Onion", "Tomato": "Tomato", } @st.cache_data(ttl=3600, show_spinner=False) def _get_presow_price(crop: str, state: str) -> dict: """Cached wrapper — fetch harvest-price forecast (presow_v4, 87% accuracy / 96.2% stable crops). Tries multiple AGMARKNET commodity name variants for resilience.""" _ARHAR_ALIASES = ["Arhar(Tur)", "Arhar", "Tur/Arhar", "Arhar/Tur", "Arhar Dal(Tur Dal)"] _ALIASES: dict[str, list[str]] = { "Arhar": _ARHAR_ALIASES, "Gram": ["Gram", "Chana/Bengal Gram", "Bengal Gram(Gram)(Whole)", "Chickpea"], "Moong": ["Green Gram (Moong)(Whole)", "Green Gram", "Moong"], "Urad": ["Black Gram (Urad)", "Urad", "Black Gram"], } try: from mandi_advisor.enterprise_engine_v2 import get_presow_signal primary = _PRESOW_CROP_MAP.get(crop, crop) aliases = _ALIASES.get(crop, [primary]) if primary not in aliases: aliases = [primary] + aliases for name in aliases: try: result = get_presow_signal(name, state or "India") if result and result.get("p50") and float(result["p50"]) > 500: return result # valid price (>₹500/q) except Exception: continue return {"error": "No valid forecast found", "confidence": "LOW"} except Exception as e: return {"error": str(e), "confidence": "LOW"} def _render_input_cost_calculator(active_state: str | None) -> None: """Input Cost + Profit Estimator with presow_v4 price forecast (87% accuracy within +/-15%). Shows pessimistic/likely/optimistic harvest-price scenarios from AGMARKNET 16yr data. """ st.markdown("### 💰 Input Cost & Profit Estimator") st.caption("Approximate 2024-25 rates · Actual costs vary ±20% by district · Consult local KVK for exact prices") col1, col2 = st.columns(2) with col1: # Filter to crops actually grown/traded in selected state _cidx = _load_state_crop_index() _cstate = _normalise_state_name(active_state or "") _cstate_crops = {e["crop"] for e in _cidx.get(_cstate, [])} _state_matched = sorted([c for c in _INPUT_COST if c in _cstate_crops]) # Only restrict if we have a meaningful match (>= 4 crops) # For hill/NE states with mostly horticulture, show full list if len(_state_matched) >= 4: crop_list = _state_matched st.caption(f"📊 Showing crops traded in {active_state or 'your state'} mandis · Select any crop to see all") else: crop_list = sorted(_INPUT_COST.keys()) if active_state and _state_matched: st.caption(f"📊 {', '.join(_state_matched)} are commonly traded in {active_state} · Showing all crops") sel_crop = st.selectbox("Select crop", crop_list, key="ic_crop") with col2: acres = st.number_input("Acreage (acres)", min_value=0.5, max_value=100.0, value=2.0, step=0.5, key="ic_acres") data = _INPUT_COST.get(sel_crop, {}) if not data: return msp_price = _MSP_2025.get(sel_crop) _NO_MSP_CROPS = {"Onion", "Tomato", "Potato", "Sesame", "Sunflower"} # ---- Presow price forecast ----------------------------------------------- presow = _get_presow_price(sel_crop, active_state or "India") if sel_crop in _PRESOW_CROP_MAP else {} has_forecast = bool(presow and "p50" in presow and not presow.get("error")) if has_forecast: p25 = int(presow["p25"]) p50 = int(presow["p50"]) p75 = int(presow["p75"]) conf = presow.get("confidence", "MEDIUM") hw = presow.get("harvest_window", "---") pp = presow.get("profit_probability", "MEDIUM") conf_em = {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴"}.get(conf, "🟡") pp_em = {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴"}.get(pp, "🟡") with st.expander("📊 Harvest Price Forecast (presow_v4 · AGMARKNET 16yr data)", expanded=True): st.caption( f"Crop: **{sel_crop}** | State: **{active_state or 'India'}** | " f"Expected harvest: **{hw}** | " f"Model confidence: {conf_em} **{conf}** | " f"Profit probability vs MSP: {pp_em} **{pp}** | " f"Accuracy: 87% within ±15%" ) fa, fb, fc = st.columns(3) def _vs_msp(px): if not msp_price: return None, "off" diff = px - msp_price return (f"+{diff:,}" if diff >= 0 else f"{diff:,}"), ("normal" if diff >= 0 else "inverse") d25, dc25 = _vs_msp(p25) d50, dc50 = _vs_msp(p50) d75, dc75 = _vs_msp(p75) fa.metric("📉 Pessimistic (p25)", f"₹{p25:,}/q", delta=d25, delta_color=dc25) fb.metric("📊 Likely (p50 median)", f"₹{p50:,}/q", delta=d50, delta_color=dc50) fc.metric("📈 Optimistic (p75)", f"₹{p75:,}/q", delta=d75, delta_color=dc75) if msp_price: st.caption(f"🏛️ MSP 2024-25: ₹{msp_price:,}/q") # Historical mandi benchmark from actual data _hidx = _load_state_crop_index() _hstate = _normalise_state_name(active_state or "") _hent = next((e for e in _hidx.get(_hstate, []) if e["crop"] == sel_crop), None) if _hent and _hent.get("avg_modal_price_per_quintal", 0) > 0: _hp = _hent["avg_modal_price_per_quintal"] st.caption(f"📈 Historical mandi avg 2018–2025 ({active_state}): ₹{_hp:,}/q") # Never show a price below MSP as default — MSP is the floor default_price = max(p50, msp_price) if msp_price else p50 if p50 < (msp_price or 0): st.caption( f"ℹ️ ML forecast for this region (₹{p50:,}/q) is below MSP (₹{msp_price:,}/q). " "Using MSP as default — adjust if your local market pays differently." ) else: default_price = msp_price or 3000 if sel_crop in _PRESOW_CROP_MAP: st.caption("ℹ️ Price forecast unavailable for this combination — using MSP as reference.") sell_price = st.number_input( "Expected sell price (₹/quintal) — adjust if needed", min_value=500, max_value=50000, value=int(default_price), step=50, key="ic_price" ) # ---- Cost breakdown ------------------------------------------------------ total_seed = data["seed"] * acres total_fert = data["fertilizer"] * acres total_pest = data["pesticide"] * acres total_labor = data["labor"] * acres total_irr = data["irrigation"] * acres total_misc = data["misc"] * acres total_input = total_seed + total_fert + total_pest + total_labor + total_irr + total_misc est_yield = data["yield_q"] * acres breakeven = total_input / est_yield if est_yield > 0 else 0 st.markdown(f"**Cost breakdown for {acres:.1f} acre(s) of {sel_crop}** *(Season: {data.get('season','---')})*") cost_rows = [ ("🌱 Seed / planting material", f"₹{total_seed:,.0f}"), ("🧪 Fertilizer (NPK + micronutrients)", f"₹{total_fert:,.0f}"), ("💊 Pesticide / fungicide", f"₹{total_pest:,.0f}"), ("👷 Labour (sowing + weeding + harvest)", f"₹{total_labor:,.0f}"), ("💧 Irrigation", f"₹{total_irr:,.0f}"), ("📦 Misc (transport, bags, etc.)", f"₹{total_misc:,.0f}"), ] for label, val in cost_rows: c1, c2 = st.columns([3, 1]) c1.markdown(label) c2.markdown(f"**{val}**") st.markdown(f"**🔴 Total Input Cost: ₹{total_input:,.0f}**") st.markdown("---") # ---- Profit scenarios ---------------------------------------------------- if has_forecast: st.markdown("**📊 Profit Scenarios at Harvest** *(based on price forecast)*") sc_cols = st.columns(4) sc_cols[0].metric("📦 Expected Yield", f"{est_yield:.0f} q") for col_idx, (label, px) in enumerate([ ("📉 Pessimistic", p25), ("📊 Likely", p50), ("📈 Optimistic", p75), ]): rev = est_yield * px prof = rev - total_input sc_cols[col_idx + 1].metric( f"{label} @ ₹{px:,}/q", f"₹{prof:,.0f}", delta="Profit" if prof >= 0 else "Loss", delta_color="normal" if prof >= 0 else "inverse", ) else: revenue = est_yield * sell_price profit = revenue - total_input m1, m2, m3, m4 = st.columns(4) m1.metric("📦 Expected Yield", f"{est_yield:.0f} q") m2.metric("💵 Revenue", f"₹{revenue:,.0f}") m3.metric("📈 Net Profit", f"₹{profit:,.0f}", delta="Profit" if profit >= 0 else "Loss", delta_color="normal" if profit >= 0 else "inverse") m4.metric("⚖️ Break-even", f"₹{breakeven:.0f}/q") st.caption(f"⚖️ Break-even price: **₹{breakeven:.0f}/q** — you must sell above this to recover all costs.") # ---- MSP / no-MSP messaging ---------------------------------------------- if msp_price: if sell_price < msp_price: st.warning(f"⚠️ Your expected price (₹{sell_price}) is **below MSP (₹{msp_price})**. " f"Contact nearest APMC or call 1800-180-1551 for MSP procurement.") elif sell_price >= msp_price * 1.2: st.success(f"✅ Selling at ₹{sell_price} = **{((sell_price/msp_price)-1)*100:.0f}% above MSP**. Good market timing!") elif sel_crop in _NO_MSP_CROPS: st.info(f"ℹ️ **{sel_crop} has no government MSP** — market price is set by demand/supply. " f"Monitor AGMARKNET (agmarknet.gov.in) or call **1800-270-0224** for daily mandi rates. " f"Consider price hedging via FPO/contract farming.") # ---- PMFBY insurance premium --------------------------------------------- if msp_price: _season_str = data.get("season", "") premium_pct = 0.02 if "Kharif" in _season_str else (0.015 if "Rabi" in _season_str else 0.05) sum_insured = msp_price * data["yield_q"] premium_per_acre = sum_insured * premium_pct * acres st.info(f"🛡️ **PMFBY Insurance:** For {acres:.1f} acre of {sel_crop} — " f"your premium = **₹{premium_per_acre:,.0f}** ({premium_pct*100:.1f}% of sum insured). " f"Enroll before cut-off date at nearest bank.") elif sel_crop in _NO_MSP_CROPS: st.caption("🛡️ PMFBY available for vegetable crops in select states — check with your nearest bank branch.") def _render_soil_prep_checklist(sel_crop: str | None) -> None: """Section: Soil Preparation Checklist — 30→0 days before sowing.""" st.markdown("### 📋 Soil Prep Checklist — Before You Sow") st.caption("Tick off each step as you complete it · Done right = 15-25% higher yield") if not sel_crop: sel_crop = st.selectbox("Select crop for checklist", sorted(_INPUT_COST.keys()), key="sp_crop") category = _get_crop_category(sel_crop) steps = _SOIL_PREP.get(category, _SOIL_PREP["cereal"]) for step in steps: days = step["days_before"] label = f"**{step['task']}**" + (f" *(~{days} days before sowing)*" if days > 0 else " *(Sowing day)*") done = st.checkbox(label, key=f"sp_{sel_crop}_{days}_{step['task'][:10]}") if done: st.caption(f" ✅ Done") else: st.caption(f" ℹ️ {step['detail']}") def _render_weather_compact(active_state: str | None) -> None: """Compact 3-day weather cards for the Before Sowing tab.""" # Use sidebar location — no duplicate selector here sel_state = active_state if not sel_state: st.info("📍 Select your **State → District → Block** in the sidebar to see local weather and crop recommendations.") return with st.spinner(f"Fetching forecast for {sel_state}…"): wx = _fetch_weather(sel_state) if not wx or "daily" not in wx: st.warning("Could not fetch weather. Check internet connection.") return daily = wx["daily"] dates = daily.get("time", [])[:3] tmax = daily.get("temperature_2m_max", []) tmin = daily.get("temperature_2m_min", []) rain = daily.get("precipitation_sum", []) codes = daily.get("weathercode", []) cols = st.columns(3) labels = ["Today", "Tomorrow", "Day 3"] for i, col in enumerate(cols): if i >= len(dates): break with col: desc = _WMO_CODES.get(codes[i] if i < len(codes) else 0, "") r = rain[i] if i < len(rain) else 0 col.metric(f"{labels[i]} ({dates[i]})", f"{tmax[i] if i < len(tmax) else '?'}°C", f"Min {tmin[i] if i < len(tmin) else '?'}°C") col.caption(f"{desc} | 🌧️ {r:.1f}mm") # Agricultural alerts ctx = _format_weather_context(wx, sel_state) alerts = [l for l in ctx.split("\n") if l.strip().startswith(("🌧️", "🥶", "🌡️"))] if alerts: for a in alerts: st.warning(a) else: st.success("✅ Weather looks favourable for sowing preparations.") return sel_state # caller can use this import json as _json_mod @st.cache_data(ttl=86400) def _load_state_crop_index() -> dict: """Load mandi-derived per-state crop index. Returns {} on failure.""" _idx_path = str(Path(__file__).parent / "mandi_advisor" / "state_crop_index.json") try: with open(_idx_path, "r", encoding="utf-8") as _f: return _json_mod.load(_f) except Exception: return {} def _normalise_state_name(state: str) -> str: """Map app state names to AGMARKNET state names used in mandi index.""" _ALIAS = { "Jammu & Kashmir": "Jammu and Kashmir", "J&K": "Jammu and Kashmir", "Uttarakhand": "Uttarakhand", "Orissa": "Odisha", } return _ALIAS.get(state, state) @st.cache_data(ttl=86400, show_spinner=False) def _load_district_crop_index() -> dict: """Load district-level crop index built from mandi_data_clean.parquet.""" _path = str(Path(__file__).parent / "mandi_advisor" / "district_crop_index.json") try: import json as _j with open(_path, "r", encoding="utf-8") as _f: return _j.load(_f) except Exception: return {} def _get_district_crops_for_season( state: str, district: str, season_name: str, top_n: int = 8) -> list: """Return top crops for a specific district+season from district mandi index.""" idx = _load_district_crop_index() if not idx: return [] # Try exact match first, then normalised key = f"{state}|{district}" entries = idx.get(key, []) if not entries: # Try case-insensitive match key_lower = key.lower() for k, v in idx.items(): if k.lower() == key_lower: entries = v break if not entries: return [] # Filter by season using commodity-to-season mapping _KHARIF_CROPS = { "Rice", "Paddy", "Cotton", "Soybean", "Soyabean", "Maize", "Groundnut", "Arhar", "Tur", "Moong", "Green Gram", "Urad", "Black Gram", "Bajra", "Sorghum", "Jowar", "Sesame", "Sunflower", "Sugarcane", "Okra", "Bitter Gourd", "Cucumber", "Watermelon", "Cowpea", "Castor" } _RABI_CROPS = { "Wheat", "Gram", "Chickpea", "Bengal Gram", "Mustard", "Rapeseed", "Lentil", "Masur", "Pea", "Potato", "Onion", "Tomato", "Barley", "Coriander", "Cumin", "Fenugreek", "Garlic" } _ZAID_CROPS = { "Moong", "Green Gram", "Urad", "Black Gram", "Maize", "Watermelon", "Cucumber", "Muskmelon", "Bitter Gourd", "Bottle Gourd", "Pumpkin", "Sesame", "Sunflower", "Cowpea" } season_set = {"Kharif": _KHARIF_CROPS, "Rabi": _RABI_CROPS, "Zaid (Summer)": _ZAID_CROPS}.get(season_name, set()) filtered = [] unmatched = [] for e in entries: crop = e["crop"] if any(kw in crop for kw in season_set): filtered.append(e) else: unmatched.append(e) # If nothing season-matched, return all (better than empty) result = filtered if filtered else unmatched return result[:top_n] def _get_state_crops_for_season( state, season_name: str, index: dict, top_n: int = 8) -> list: """ Return top crops for a state+season from the mandi index. Each entry: {crop, count, avg_modal_price_per_quintal, season} """ if not state or not index: return [] lookup = _normalise_state_name(state) crops = index.get(lookup) or index.get("_national") or [] SEASON_TOKENS = { "Kharif": {"Kharif"}, "Rabi": {"Rabi"}, "Zaid (Summer)": {"Zaid", "Summer", "Perennial"}, } tokens = SEASON_TOKENS.get(season_name, set()) matched, perennials = [], [] for entry in crops: s = entry.get("season", "") if any(t in s for t in tokens - {"Perennial"}): matched.append(entry) elif "Perennial" in s and "Perennial" in tokens: perennials.append(entry) seen, result = set(), [] for e in matched + perennials: if e["crop"] not in seen: seen.add(e["crop"]) result.append(e) if len(result) >= top_n: break return result def _render_sowing_calendar(active_state: str | None, season: dict) -> None: """Show what to sow now based on state + season.""" state_crops: dict[str, list] = { "Kharif": { "Madhya Pradesh": ["Soybean ★", "Cotton", "Maize", "Arhar", "Moong"], "Maharashtra": ["Soybean ★", "Cotton ★", "Sugarcane", "Tur", "Sorghum"], "Punjab": ["Rice/Paddy ★", "Maize", "Cotton", "Sugarcane"], "Haryana": ["Rice/Paddy ★", "Maize", "Cotton", "Bajra"], "Uttar Pradesh": ["Rice/Paddy ★", "Maize", "Arhar", "Sugarcane", "Cotton"], "Rajasthan": ["Bajra ★", "Moong", "Moth Bean", "Sesame", "Groundnut"], "Gujarat": ["Cotton ★", "Groundnut ★", "Maize", "Castor", "Arhar"], "Bihar": ["Rice/Paddy ★", "Maize", "Arhar", "Moong"], "West Bengal": ["Rice/Paddy ★", "Jute", "Maize"], "Andhra Pradesh": ["Rice/Paddy ★", "Cotton", "Maize", "Groundnut"], "Telangana": ["Rice/Paddy ★", "Cotton ★", "Maize", "Soybean"], "Karnataka": ["Rice/Paddy ★", "Sugarcane", "Cotton", "Ragi", "Groundnut"], "Tamil Nadu": ["Rice/Paddy ★", "Sugarcane", "Groundnut", "Cotton"], }, "Rabi": { "Madhya Pradesh": ["Wheat ★", "Gram ★", "Mustard", "Lentil", "Potato"], "Punjab": ["Wheat ★", "Mustard", "Potato"], "Haryana": ["Wheat ★", "Mustard ★", "Barley", "Potato"], "Uttar Pradesh": ["Wheat ★", "Mustard", "Gram", "Potato", "Pea"], "Rajasthan": ["Wheat ★", "Mustard ★", "Gram", "Barley", "Cumin"], "Maharashtra": ["Gram ★", "Wheat", "Sorghum", "Onion"], "Gujarat": ["Wheat ★", "Mustard", "Gram", "Cumin"], "Bihar": ["Wheat ★", "Mustard", "Lentil", "Potato"], }, "Zaid (Summer)": { "Jammu & Kashmir": ["Peas ★", "Potato", "Rajma", "Maize", "French Beans"], "Himachal Pradesh": ["Peas ★", "Potato ★", "Maize", "Rajma", "Tomato"], "Uttarakhand": ["Potato ★", "Peas", "Soybean", "Maize", "Tomato"], "Assam": ["Jute ★", "Maize", "Sesame", "Moong", "Cucumber"], "Meghalaya": ["Potato ★", "Maize", "Ginger", "Pineapple", "Tomato"], "Manipur": ["Rice (Boro) ★", "Maize", "Potato", "Sesame"], "Nagaland": ["Maize ★", "Potato", "Ginger", "Soybean"], "Mizoram": ["Maize ★", "Potato", "Ginger", "Sesame"], "Tamil Nadu": ["Rice (Kuruvai) ★", "Groundnut", "Sesame", "Bitter Gourd", "Cucumber"], "Karnataka": ["Groundnut ★", "Sesame", "Sunflower", "Cucumber", "Watermelon"], "Andhra Pradesh": ["Groundnut ★", "Sesame", "Sunflower", "Watermelon", "Cucumber"], "Telangana": ["Sunflower ★", "Groundnut", "Sesame", "Watermelon"], "Kerala": ["Sesame ★", "Cowpea", "Bitter Gourd", "Snake Gourd"], "Gujarat": ["Groundnut ★", "Sesame", "Watermelon", "Cucumber", "Moong"], "Rajasthan": ["Moong ★ (60d)", "Moth Bean", "Watermelon", "Cucumber"], "All States": ["Moong ★ (60d)", "Urad (65d)", "Cucumber", "Watermelon", "Bitter Gourd"], }, }.get(season["name"], {}) # ── Guard: require state before showing crops ────────────────────── if not active_state: st.info( "📍 **Select your State → District → Block** in the sidebar to see " "hyperlocal crop recommendations based on actual mandi trading data " "for your district." ) return # ── 1. Try district-level data first (most accurate) ────────────── user_district = st.session_state.get("user_district_loc") _district_crops = [] if user_district: _district_crops = _get_district_crops_for_season( active_state, user_district, season["name"], top_n=8 ) # ── 2. Fall back to state-level mandi index ─────────────────────── _midx = _load_state_crop_index() _mcrops = _get_state_crops_for_season(active_state, season["name"], _midx, top_n=8) # ── 3. Last resort: hardcoded seasonal list (no "All States" fallback) ── crops_for_state = state_crops.get(active_state, []) if _district_crops: st.markdown( f'

' f'📍 District-specific — based on mandi arrivals in {user_district}, {active_state}' ' (2018–2025) · sorted by trading volume

', unsafe_allow_html=True) _show = _district_crops[:4] cols = st.columns(len(_show)) for _i, _entry in enumerate(_show): _cname = _entry["crop"] _price = _entry.get("avg_price", 0) _pstr = f"avg \u20b9{_price:,}/q" if _price > 0 else "" cols[_i].success(f"\U0001f33f {_cname}\n{_pstr}") if len(_district_crops) > 4: st.caption("Also common in your district: " + " · ".join( e["crop"] for e in _district_crops[4:])) st.caption( f"⬆️ These crops are actually traded at mandis in {user_district} district — " "not generic regional suggestions." ) elif _mcrops: st.markdown( f'

' f'📊 Based on mandi arrival data 2018–2025 for {active_state}' '  ·  Select District for more specific data

', unsafe_allow_html=True) _show = _mcrops[:4] cols = st.columns(len(_show)) for _i, _entry in enumerate(_show): _cname = _entry["crop"] _price = _entry.get("avg_modal_price_per_quintal", 0) _pstr = f"avg \u20b9{_price:,}/q" if _price > 0 else "" cols[_i].success(f"\U0001f33f {_cname}\n{_pstr}") if len(_mcrops) > 4: st.caption("Also common: " + " · ".join(e["crop"] for e in _mcrops[4:])) elif crops_for_state: st.markdown(f"**📅 Recommended for {season['name']} in {active_state}:**") cols = st.columns(min(5, len(crops_for_state))) for i, crop in enumerate(crops_for_state[:5]): cols[i].success(f"\U0001f33f {crop}") else: st.info(f"No crop data available for {active_state} in {season['name']} season.") # ── After Harvest tab: crop → AGMARKNET commodity name mapping ────────────── _MANDI_CROP_MAP: dict[str, str] = { "Wheat": "Wheat", "Rice": "Paddy(Common)", "Maize": "Maize", "Soybean": "Soyabean", "Cotton": "Cotton", "Mustard": "Rapeseed/Mustard(Toria)", "Chickpea": "Bengal Gram(Gram)(Whole)", "Arhar": "Arhar (Tur/Red Gram)(Whole)", "Moong": "Green Gram(Whole)", "Urad": "Black Gram (Urad Whole)", "Groundnut":"Groundnut", "Onion": "Onion", "Tomato": "Tomato", "Potato": "Potato", "Bajra": "Bajra(Pearl Millet/Cumbu)", "Gram": "Bengal Gram(Gram)(Whole)", "Sunflower":"Sunflower", "Sesame": "Sesamum(Sesame,Gingelly,Til)", } def _hold_sell_recommendation(crop: str, modal_price: float, state: str = "India") -> str: """ Enhanced hold/sell recommendation using: 1. presow_v4 ML forecast (p25/p50/p75) as price benchmark 2. MSP as safety floor 3. Seasonal timing logic """ msp = _MSP_2025.get(crop) # Try presow_v4 forecast for data-backed benchmark presow_p50 = None presow_p75 = None presow_p25 = None forecast_note = "" try: from mandi_advisor.enterprise_engine_v2 import get_presow_signal _SELL_CROP_MAP = { "Wheat": "Wheat", "Rice": "Paddy(Desi)(Common)", "Cotton": "Cotton(Lint)(Long Staple)", "Soybean": "Soyabean", "Mustard": "Rapeseed/Mustard(Toria)", "Maize": "Maize", "Chickpea": "Bengal Gram(Gram)(Whole)", "Arhar": "Arhar (Tur/Red Gram)(Whole)", "Groundnut": "Groundnut", "Onion": "Onion", "Tomato": "Tomato", "Bajra": "Bajra(Pearl Millet/Cumbu)", } agmkt = _SELL_CROP_MAP.get(crop) if agmkt: sig = get_presow_signal(agmkt, state) if sig and not sig.get("error") and "p50" in sig: presow_p25 = int(sig.get("p25", 0)) presow_p50 = int(sig.get("p50", 0)) presow_p75 = int(sig.get("p75", 0)) conf = sig.get("confidence", "MEDIUM") conf_emoji = {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴"}.get(conf, "🟡") forecast_note = ( chr(10) + "**📊 ML Price Forecast (presow_v4):** " + "Pessimistic ₹" + str(presow_p25) + "/q | " + "Likely ₹" + str(presow_p50) + "/q | " + "Optimistic ₹" + str(presow_p75) + "/q " + conf_emoji + " " + conf ) except Exception: pass # Use presow_p50 as benchmark if available, else MSP benchmark = presow_p50 if presow_p50 else msp benchmark_label = "ML forecast" if presow_p50 else "MSP" if not benchmark: return ( "💡 No price benchmark available for " + crop + ". " "Check agmarknet.gov.in for historical prices before deciding." + forecast_note ) gap_pct = (modal_price - benchmark) / benchmark * 100 if msp and modal_price < msp: result = ( "⚠️ **Price ₹" + str(int(modal_price)) + "/q is BELOW MSP ₹" + str(msp) + "/q** for " + crop + "." + chr(10) + "→ Do NOT sell below MSP. Check nearest APMC / FCI procurement centre." + chr(10) + "→ Call Kisan Helpline **1800-180-1551** for MSP procurement info." ) elif gap_pct < -10: result = ( "🔴 **Price ₹" + str(int(modal_price)) + "/q is " + str(abs(int(gap_pct))) + "% BELOW " + benchmark_label + " ₹" + str(benchmark) + "/q**." + chr(10) + "→ Prices are lower than expected. **Hold if storage is good** (12-14% moisture)." + chr(10) + "→ Wait 4-6 weeks for seasonal price recovery." ) elif gap_pct < 5: result = ( "🟡 **Price ₹" + str(int(modal_price)) + "/q is near " + benchmark_label + " ₹" + str(benchmark) + "/q** (" + ("+" if gap_pct >= 0 else "") + str(int(gap_pct)) + "%)." + chr(10) + "→ If storage is good, **hold 3-4 weeks** " + chr(0x2014) + " prices typically rise post-harvest." + chr(10) + "→ If storage is poor or you need cash, sell now." ) elif gap_pct < 20: result = ( "✅ **Price ₹" + str(int(modal_price)) + "/q is +" + str(int(gap_pct)) + "% above " + benchmark_label + "** " + chr(0x2014) + " good price." + chr(10) + "→ **Sell 50-70% now** to lock in profit. Hold rest for possible further rise." + chr(10) + (("→ Optimistic forecast is ₹" + str(presow_p75) + "/q " + chr(0x2014) + " potential upside if you hold." + chr(10)) if presow_p75 and modal_price < presow_p75 else "") ) else: result = ( "🟢 **Excellent! ₹" + str(int(modal_price)) + "/q is +" + str(int(gap_pct)) + "% above " + benchmark_label + "** " + chr(0x2014) + " very good." + chr(10) + "→ **Sell now** " + chr(0x2014) + " this is significantly above expected price." + chr(10) + "→ Do not wait unless you have a firm buyer at higher price." ) return result + forecast_note def _render_before_sowing_tab(retriever: "KCCRetriever", settings: dict) -> None: """Tab 1: Before Sowing — Weather + Sowing Calendar + AI Crop Advisor + Price Preview.""" active_state = (st.session_state.get("user_state_loc") or st.session_state.get("active_state")) season = get_current_season() # ── Section 1: Weather ──────────────────────────────────────────────────── st.markdown("### 🌤️ Weather Forecast — Will Conditions Support Sowing?") sel_state = _render_weather_compact(active_state) # sel_state may be None if function returns early on error if sel_state: active_state = sel_state st.markdown("---") # ── Section 2: Sowing Calendar ──────────────────────────────────────────── st.markdown("### 📅 What to Sow This Season?") _render_sowing_calendar(active_state, season) st.markdown("---") # ── Section 3: Variety Recommender ─────────────────────────────────────── _render_variety_recommender(active_state) st.markdown("---") # ── Section 4: Soil Prep Checklist ─────────────────────────────────────── # Pass the crop selected in variety recommender if available, else None _soil_sel_crop = st.session_state.get("vr_crop_sel") _render_soil_prep_checklist(_soil_sel_crop) st.markdown("---") # ── Section 5: Input Cost & Profit Calculator ───────────────────────────── _render_input_cost_calculator(active_state) st.markdown("---") # ── Section 6: AI Crop Advisor ──────────────────────────────────────────── st.markdown("### 🤖 AI Crop Advisor") st.caption( "Ask: *'Cotton ya soybean kya lagaun Barwani mein?'* or " "*'Which crop for black soil with 650mm rainfall?'* — " "AI gives comparison with ICAR data + profit/risk analysis." ) # Init Before-Sowing chat session state (separate from main chatbot) if "bs_messages" not in st.session_state: st.session_state.bs_messages = [] if "bs_state" not in st.session_state: st.session_state.bs_state = active_state # Render conversation history for msg in st.session_state.bs_messages: with st.chat_message(msg["role"], avatar="👨‍🌾" if msg["role"] == "user" else "🌾"): st.markdown(msg["content"]) if bs_query := st.chat_input( "Ask about crop selection, variety, sowing time, soil prep…", key="bs_chat_input" ): with st.chat_message("user", avatar="👨‍🌾"): st.markdown(bs_query) st.session_state.bs_messages.append({"role": "user", "content": bs_query}) # Force crop_selection problem type for this tab detected_state = detect_state(bs_query) or active_state # Allow the AI to handle any pre-sowing query (crop selection, agronomy, weather, scheme) problem_type = classify_problem(bs_query) # If general, assume crop_selection context for this tab if problem_type == "general": problem_type = "crop_selection" normalized_q = normalize_query(bs_query) rewritten_q = rewrite_query_for_retrieval( normalized_q, None, problem_type, detected_state, season ) docs = retriever.search( rewritten_q, top_k=5, crop_filter=None, ) docs = _apply_state_preference(docs, detected_state) icar_ctx = get_icar_priority_context(bs_query, None, problem_type, detected_state) crop_ctx = build_crop_decision_context(bs_query, detected_state) variety_ctx = build_variety_context(bs_query, detected_state) # NEW: pins current ICAR varieties if crop_ctx: icar_ctx = icar_ctx + "\n" + crop_ctx if variety_ctx: icar_ctx = variety_ctx + "\n" + icar_ctx # variety context goes FIRST — highest priority kcc_context = retriever.format_context(docs) meta_lines = [] season_line = ( f"CURRENT AGRICULTURAL SEASON: {season['name']} " f"({season['months']}) — major crops: {season['crops']}" ) if season.get("planning_note"): season_line += f"\n{season['planning_note']}" meta_lines.append(season_line) if detected_state: soil_ctx = get_soil_context(detected_state) detected_district = _extract_location_from_query(bs_query) if detected_district: state_line = ( f"FARMER LOCATION (from query): {detected_district}, {detected_state} " f"(MANDATORY — tailor advice to this specific district)" ) else: state_line = f"FARMER'S STATE: {detected_state} (MANDATORY — do NOT assume a different state)" if soil_ctx: state_line += f" | {soil_ctx}" meta_lines.append(state_line) meta = "\n".join(meta_lines) full_ctx = ( f"{icar_ctx}\n\nRECORDS FROM KCC DATABASE:\n{kcc_context}\n\n" f"CONTEXT:\n{meta}" ) lang = detect_language(bs_query) prompt = _build_prompt(bs_query, full_ctx, [], problem_type, lang) with st.chat_message("assistant", avatar="🌾"): response = st.write_stream(_stream_llm_response(prompt)) st.session_state.bs_messages.append({"role": "assistant", "content": response}) if detected_state: st.session_state.active_state = detected_state st.markdown("---") # ── Section 7: Price Preview for top 3 Kharif/Rabi crops ───────────────── st.markdown("### 💰 MSP Reference Prices (2025–26)") st.caption("Minimum Support Price set by Govt of India — your floor price guarantee.") season_crops = { "Kharif": ["Soybean", "Cotton", "Rice", "Maize", "Groundnut", "Arhar", "Moong"], "Rabi": ["Wheat", "Gram", "Mustard", "Lentil"], "Zaid (Summer)": ["Moong", "Urad"], }.get(season["name"], []) msp_data = {c: _MSP_2025[c] for c in season_crops if c in _MSP_2025} if msp_data: cols = st.columns(min(4, len(msp_data))) for i, (crop, msp) in enumerate(list(msp_data.items())[:4]): cols[i].metric(f"🌾 {crop} MSP", f"₹{msp:,}/q") st.caption( "📌 If market price falls below MSP, contact your nearest APMC or call " "**1800-180-1551** for MSP procurement." ) def _render_after_harvest_tab() -> None: """Tab 3: After Harvest — Sell Smart (presow_v4 price engine + mandi API + hold/sell).""" try: import plotly.graph_objects as go _PLOTLY_OK = True except ImportError: _PLOTLY_OK = False st.markdown( """
📦 Sell Smart — After Harvest Price Forecast · Best Mandi · Hold or Sell · Storage Guide
""", unsafe_allow_html=True, ) _active_state = ( st.session_state.get("user_state_loc") or st.session_state.get("active_state") ) col_crop, col_state = st.columns(2) with col_crop: sell_crop = st.selectbox("🌾 Your Crop", list(_MANDI_CROP_MAP.keys()), key="tab3_crop") with col_state: state_list = sorted(STATE_COORDS.keys()) default_si = state_list.index(_active_state) if _active_state in state_list else 0 sell_state = st.selectbox("📍 Your State", state_list, index=default_si, key="tab3_state") st.divider() # ══ A. PRICE INTELLIGENCE ══════════════════════════════════════════════ st.markdown("### 📊 A. Price Intelligence (ML Forecast)") agmkt_name = _MANDI_CROP_MAP.get(sell_crop, sell_crop) presow_sig = {} try: from mandi_advisor.enterprise_engine_v2 import get_presow_signal presow_sig = get_presow_signal(agmkt_name, sell_state) or {} except Exception: pass msp_val = _MSP_2025.get(sell_crop) if presow_sig and presow_sig.get("p50"): p25 = int(presow_sig.get("p25") or 0) p50 = int(presow_sig.get("p50") or 0) p75 = int(presow_sig.get("p75") or 0) conf = presow_sig.get("confidence", "MEDIUM") harvest = presow_sig.get("harvest_window", "upcoming season") conf_col = {"HIGH": "#28a745", "MEDIUM": "#ffc107", "LOW": "#dc3545"}.get(conf, "#ffc107") conf_emoji = {"HIGH": "🟢", "MEDIUM": "🟡", "LOW": "🔴"}.get(conf, "🟡") if _PLOTLY_OK: import plotly.graph_objects as go gauge_ref = msp_val if msp_val else p50 fig = go.Figure(go.Indicator( mode="gauge+number+delta", value=p50, title={"text": f"{sell_crop} — Expected Harvest Price ({harvest})", "font": {"size": 13}}, delta={"reference": gauge_ref, "prefix": "vs MSP " if msp_val else ""}, gauge={ "axis": {"range": [int(p25 * 0.85), int(p75 * 1.15)]}, "bar": {"color": conf_col}, "steps": [ {"range": [int(p25 * 0.85), p25], "color": "#f8d7da"}, {"range": [p25, p50], "color": "#fff3cd"}, {"range": [p50, p75], "color": "#d4edda"}, {"range": [p75, int(p75 * 1.15)], "color": "#cce5ff"}, ], "threshold": { "line": {"color": "black", "width": 3}, "thickness": 0.85, "value": msp_val if msp_val else p50, }, }, number={"prefix": "₹", "suffix": "/q"}, )) fig.update_layout(height=260, margin=dict(t=60, b=10, l=20, r=20)) st.plotly_chart(fig, use_container_width=True) c1, c2, c3 = st.columns(3) def _delta(px): if not msp_val: return None d = (px - msp_val) / msp_val * 100 return f"{d:+.1f}% vs MSP" c1.metric("😟 Pessimistic (P25)", f"₹{p25:,}/q", _delta(p25)) c2.metric("📈 Likely Price (P50)", f"₹{p50:,}/q", _delta(p50)) c3.metric("🚀 Optimistic (P75)", f"₹{p75:,}/q", _delta(p75)) if msp_val: if p50 < msp_val: st.warning( f"⚠️ Forecast ₹{p50:,}/q is BELOW MSP ₹{msp_val:,}/q — " "sell via MSP procurement or wait for seasonal price rise. " "Call **1800-180-1551** for nearest procurement centre." ) else: st.success( f"✅ Forecast ₹{p50:,}/q is ₹{p50 - msp_val:,} above MSP — " "open-market sale looks profitable." ) st.caption( f"{conf_emoji} Forecast confidence: **{conf}** | " f"16-yr AGMARKNET data, presow_v4 model (87% accuracy) | " f"Harvest window: **{harvest}**" ) else: if msp_val: st.metric(f"📋 MSP 2025–26 for {sell_crop}", f"₹{msp_val:,}/quintal", "Government floor price") st.info( f"Price forecast model not available for {sell_crop} in {sell_state}. " "Check agmarknet.gov.in for live prices." ) st.divider() # ══ B. LIVE MANDI PRICES ══════════════════════════════════════════════ st.markdown("### 🏪 B. Live Mandi Prices") if not getattr(config, 'DATA_GOV_API_KEY', None): st.info( "🔑 Live mandi prices require a free API key from " "[data.gov.in](https://data.gov.in/user/register). " "Add it to config.py as DATA_GOV_API_KEY. " "ML price forecast above works without any API key." ) else: if st.button("🔍 Fetch Live Prices", key="tab3_fetch", use_container_width=False): commodity = _MANDI_CROP_MAP.get(sell_crop, sell_crop) with st.spinner(f"Fetching {sell_crop} prices in {sell_state}…"): _mandi_res = _fetch_mandi_prices(sell_state, commodity) live_records = _mandi_res["records"] if _mandi_res else [] if not live_records: st.warning( f"No live data today for **{sell_crop}** in **{sell_state}**. " "API coverage is partial — use ML forecast above as benchmark." ) else: rows = [] for r in live_records: try: modal = float(r.get("Modal_Price") or r.get("modal_price") or 0) except (ValueError, TypeError): modal = 0.0 rows.append({ "Market": r.get("Market") or r.get("market", ""), "District": r.get("District") or r.get("district", ""), "Min ₹/q": r.get("Min_Price") or r.get("min_price", ""), "Modal ₹/q": r.get("Modal_Price") or r.get("modal_price", ""), "Max ₹/q": r.get("Max_Price") or r.get("max_price", ""), "Date": r.get("Arrival_Date") or r.get("arrival_date", ""), "_modal": modal, }) df = pd.DataFrame(rows).sort_values("_modal", ascending=False).drop( columns=["_modal"]).reset_index(drop=True) if not df.empty: best = df.iloc[0] st.success( f"🥇 Best price today: **{best['Market']}** ({best['District']}) " f"— Modal **₹{best['Modal ₹/q']}/q**" ) st.dataframe(df, use_container_width=True) st.caption("Source: Ministry of Agriculture via data.gov.in") st.divider() # ══ C. TRANSPORT COST CALCULATOR ══════════════════════════════════════ with st.expander("🚛 C. Transport Calculator — Is the farther mandi worth it?"): col1, col2, col3 = st.columns(3) with col1: local_price = st.number_input("Local mandi price (₹/q)", 0, 50000, 0, 50, key="tc_local") with col2: far_price = st.number_input("Farther mandi price (₹/q)", 0, 50000, 0, 50, key="tc_far") with col3: qty_qtl = st.number_input("Quantity (quintals)", 1, 1000, 20, 1, key="tc_qty") distance_km = st.slider("Distance to farther mandi (km)", 10, 300, 60, key="tc_dist") transport_rate = st.slider("Transport rate (₹/q/km)", 0.5, 3.0, 1.2, 0.1, key="tc_rate") if local_price > 0 and far_price > 0: transport_cost = distance_km * transport_rate net_per_q = far_price - local_price - transport_cost total = net_per_q * qty_qtl cg, ct, cn = st.columns(3) cg.metric("Price gain", f"₹{far_price - local_price:,}/q") ct.metric("Transport cost", f"₹{transport_cost:,.1f}/q") cn.metric("Net gain", f"₹{net_per_q:,.1f}/q", f"Total: ₹{total:,.0f}") if net_per_q > 0: st.success(f"✅ Go to the farther mandi — you gain ₹{total:,.0f} after transport.") elif net_per_q > -30: st.warning("⚠️ Marginal — decide based on road condition and urgency.") else: st.error(f"❌ Stay local — farther mandi costs ₹{abs(net_per_q):,.1f}/q more than you gain.") # ══ D. HOLD OR SELL ════════════════════════════════════════════════════ with st.expander("🤔 D. Hold or Sell? (AI recommendation for your price)"): col_p, col_s2 = st.columns(2) with col_p: manual_price = st.number_input("Your local modal price (₹/q)", 0, 50000, 0, 50, key="tab3_mp") with col_s2: sl2 = sorted(STATE_COORDS.keys()) si2 = sl2.index(sell_state) if sell_state in sl2 else 0 manual_state2 = st.selectbox("State", sl2, index=si2, key="tab3_ms2") if manual_price > 0: st.markdown(_hold_sell_recommendation(sell_crop, float(manual_price), manual_state2)) # ══ E. MSP PROCUREMENT GUIDE ════════════════════════════════════════════ if sell_crop in _MSP_2025: with st.expander(f"🏛️ E. MSP Procurement — How to sell {sell_crop} at ₹{_MSP_2025.get(sell_crop, 0):,}/q govt. price"): msp_v = _MSP_2025.get(sell_crop, 0) st.markdown(f""" **MSP 2025–26 for {sell_crop}: ₹{msp_v:,}/quintal** — guaranteed by Government of India **Step-by-step MSP procurement:** 1. **Register** on PM-KISAN / e-NAM portal → [enam.gov.in](https://www.enam.gov.in) 2. **Contact your APMC** (Agricultural Produce Market Committee) — usually 10–30 km from your block 3. **FCI / NAFED centres** — for Wheat, Rice, Pulses, Oilseeds 4. **Documents needed:** Khasra number, bank passbook copy, Aadhaar card, crop registration slip 5. **Payment:** Direct bank transfer within 72 hours of procurement 📞 **Helplines (toll-free):** - Kisan Helpline: **1800-180-1551** - PM-KISAN: **155261** or **011-23381092** - e-NAM: **1800-270-0224** """) # ══ F. STORAGE ADVISORY ════════════════════════════════════════════════ with st.expander("📦 F. Post-Harvest Storage Guide (ICAR 2024–25)"): _STORAGE_GUIDE = { "Wheat": ("12–14%", "Hermetic bags (PUSA ZEC) or metal bins. Fumigate with Aluminium Phosphide 3 tablets/tonne. Check every 15 days for weevils. Lasts 12+ months."), "Rice": ("14%", "Gunny bags in dry ventilated godown. Fumigate if >3 months. Wooden pallets — avoid floor contact. Stack max 8–10 bags high."), "Maize": ("12–13%", "CRITICAL: Dry to <13% before storing — aflatoxin risk at high moisture. Hermetic bags strongly recommended. Moisture meter test mandatory."), "Soybean": ("11–12%", "Dry to 11%. Jute bags, cool dry store. Check weevils monthly. Oil degrades fast above 12% moisture."), "Chickpea": ("9–10%", "Airtight containers with neem leaves. Moisture <10% critical. Fumigate bulk with Aluminium Phosphide tablet."), "Gram": ("9–10%", "Airtight with neem leaves. Check monthly for pulse beetle (ghun/weevil)."), "Arhar": ("10%", "Gunny bags + neem leaves. Monthly inspection for ghun. Max 6-month storage."), "Groundnut": ("8–9%", "CRITICAL: Dry pods to <8%. High moisture = aflatoxin (cancer-causing). Use Aflasafe biological treatment."), "Mustard": ("7–8%", "Airtight metal bins. Avoid mixing oils. High moisture = rancidity within weeks."), "Cotton": ("8%", "Press bales, dry conditions. Keep away from moisture and fire. Inspect for residual bollworm contamination."), "Onion": ("65–70% RH", "Jali godown (ventilated). 12–15°C ideal. Grade before storage — remove damaged bulbs. Weekly rot check."), "Tomato": ("90–95% RH", "Cold storage 10–13°C. Cannot be stored >1 week without cold chain. Sell within 2–3 days for best price."), "Potato": ("85–90% RH", "Cold storage 2–4°C, dark. Avoid light (solanine). Monthly soft-rot inspection. Cold storage cost: ₹150–200/q/month."), "Bajra": ("10–12%", "Gunny bags or metal bins + neem leaves. Watch for storage pests in humid conditions."), "Moong": ("10%", "Airtight bins with neem / Aluminium Phosphide. Pulse beetle very common. Fumigate if >2 months."), "Urad": ("10%", "Same as Moong. Do not mix old and new stock. Airtight critical."), "Sunflower": ("8–9%", "Metal bins or hermetic bags. High oil content = rapid rancidity at high moisture."), "Sesame": ("6–8%", "Extremely sensitive — must dry to <6%. Metal tins. Any moisture = rapid oil oxidation."), } stor_crop = st.selectbox( "Select crop for storage guidance", list(_STORAGE_GUIDE.keys()), index=list(_STORAGE_GUIDE.keys()).index(sell_crop) if sell_crop in _STORAGE_GUIDE else 0, key="stor_crop_tab3", ) moist, guide = _STORAGE_GUIDE.get(stor_crop, ("", "No data available.")) if moist: st.metric("Safe moisture level for storage", moist) st.info(guide) st.caption("Source: ICAR Post-Harvest Technology Division 2024–25") def _render_b2b_intelligence_tab() -> None: """Tab 4: B2B Intelligence — enterprise dashboard with password gate.""" import config as _b2b_cfg _B2B_PWD = _b2b_cfg.B2B_DEMO_PASSWORD # ── Password gate (protects enterprise data from public access) ──────────── if "b2b_authenticated" not in st.session_state: st.session_state["b2b_authenticated"] = False if not st.session_state["b2b_authenticated"]: st.markdown("""

🏢 B2B Intelligence Dashboard

Enterprise-grade pest risk maps, price opportunity scanner,
and district-level input demand signals for agri-businesses.

""", unsafe_allow_html=True) col1, col2, col3 = st.columns([1, 2, 1]) with col2: pwd = st.text_input("🔐 Enter access password", type="password", placeholder="Contact us for demo access", key="b2b_pwd_input") if st.button("Access Dashboard →", use_container_width=True, type="primary"): if pwd == _B2B_PWD: st.session_state["b2b_authenticated"] = True st.rerun() else: st.error("Incorrect password. Contact the team for access.") st.markdown("""

For enterprise demo access, contact:
AgriAdvisor Enterprise Sales

""", unsafe_allow_html=True) return # ── Authenticated — show dashboard ──────────────────────────────────────── """Tab 4: B2B Intelligence — District-level pest/price signals for enterprise clients.""" from datetime import datetime try: import plotly.graph_objects as go import plotly.express as px _PLOTLY_OK = True except ImportError: _PLOTLY_OK = False st.markdown( """
🏢 B2B Intelligence Dashboard District targeting · Pest outbreak map · Price opportunity signals · Input demand forecast
Powered by AUC 0.937 Pest Model · presow_v4 Price Engine · 16.5M KCC Records
""", unsafe_allow_html=True, ) # ── Client type selector ───────────────────────────────────────────────── client_type = st.selectbox( "👔 I am a...", [ "🌾 Agri-Input Company (Pesticides / Fertilizers / Seeds)", "🏦 Bank / NBFC / Insurance Company", "🤝 FPO / Cooperative / Aggregator", "🏪 Commodity Trader / Exporter", "🔬 Research / Policy / Government", ], key="b2b_client_type", ) month = datetime.now().month month_name = datetime(2026, month, 1).strftime("%B") # ══════════════════════════════════════════════════════════════════════ # SECTION 1 — PEST OUTBREAK RISK MAP # ══════════════════════════════════════════════════════════════════════ st.markdown("---") st.markdown(f"### 🦟 1. Pest Outbreak Risk Map — {month_name} 2026") st.caption("Live predictions from AUC 0.937 stacking ensemble (LightGBM + XGBoost + CatBoost). 1-month early warning.") col_crop_ew, col_refresh = st.columns([3, 1]) with col_crop_ew: ew_crop = st.selectbox( "Select crop for risk scan", ["Wheat", "Rice", "Cotton", "Soybean", "Maize", "Mustard", "Groundnut", "Arhar", "Gram", "Onion", "Bajra", "Sugarcane"], key="b2b_ew_crop", ) with col_refresh: st.markdown("
", unsafe_allow_html=True) run_scan = st.button("🔄 Run Risk Scan", key="b2b_run_scan", use_container_width=True) # Key states for the scan _SCAN_STATES = [ "Uttar Pradesh", "Maharashtra", "Punjab", "Madhya Pradesh", "Rajasthan", "Gujarat", "Haryana", "Karnataka", "Andhra Pradesh", "Telangana", "Bihar", "West Bengal", ] if run_scan or st.session_state.get("b2b_scan_done"): st.session_state["b2b_scan_done"] = True st.session_state["b2b_scan_crop"] = ew_crop with st.spinner(f"Running pest risk scan for {ew_crop} across 12 major states…"): from mandi_advisor.pest_predictor import predict_pest_risk scan_results = [] for state in _SCAN_STATES: try: risks = predict_pest_risk(state, ew_crop, month=month) if risks: top = max(risks, key=lambda x: x.get("risk_score", 0)) high_risks = [r for r in risks if r.get("risk_score", 0) >= 60] scan_results.append({ "State": state, "Top Pest": top.get("pest", "Unknown")[:35], "Risk Score": top.get("risk_score", 0), "Risk Level": top.get("risk_level", "UNKNOWN"), "High-Risk Pests": len(high_risks), "Action": top.get("recommended_action", "")[:60], "Spray": top.get("spray", "")[:60], }) except Exception: pass if scan_results: scan_results.sort(key=lambda x: x["Risk Score"], reverse=True) # Risk level color coding def _risk_color(level): return { "CRITICAL": "#dc3545", "HIGH": "#fd7e14", "MEDIUM": "#ffc107", "LOW": "#28a745", "NEGLIGIBLE": "#6c757d", }.get(level, "#6c757d") # Summary cards critical = [r for r in scan_results if r["Risk Level"] in ("CRITICAL", "HIGH")] medium = [r for r in scan_results if r["Risk Level"] == "MEDIUM"] low_neg = [r for r in scan_results if r["Risk Level"] in ("LOW", "NEGLIGIBLE")] cc, cm, cl = st.columns(3) cc.metric("🔴 Critical / High Risk", f"{len(critical)} states", "Immediate action needed" if critical else "") cm.metric("🟡 Medium Risk", f"{len(medium)} states", "Monitor closely" if medium else "") cl.metric("🟢 Low / Negligible", f"{len(low_neg)} states", "") # Horizontal bar chart if _PLOTLY_OK: import plotly.express as px import pandas as pd df_scan = pd.DataFrame(scan_results) color_map = { "CRITICAL": "#dc3545", "HIGH": "#fd7e14", "MEDIUM": "#ffc107", "LOW": "#28a745", "NEGLIGIBLE": "#adb5bd", } fig = px.bar( df_scan.head(12), x="Risk Score", y="State", color="Risk Level", color_discrete_map=color_map, orientation="h", text="Risk Score", title=f"{ew_crop} Pest Risk Score by State — {month_name} 2026", labels={"Risk Score": "Risk Score (0–100)", "State": ""}, ) fig.update_traces(texttemplate="%{text}", textposition="outside") fig.update_layout( height=420, yaxis={"categoryorder": "total ascending"}, margin=dict(l=10, r=40, t=50, b=10), legend_title="Risk Level", ) st.plotly_chart(fig, use_container_width=True) # Full table with formatting import pandas as pd df_display = pd.DataFrame([{ "State": r["State"], "Risk Level": r["Risk Level"], "Score": r["Risk Score"], "Top Threat": r["Top Pest"], "High-Risk Pests": r["High-Risk Pests"], "Recommended Spray": r["Spray"], } for r in scan_results]) st.dataframe(df_display, use_container_width=True, hide_index=True) # B2B Insight box if critical: top_states = ", ".join([r["State"] for r in critical[:4]]) top_pest = critical[0]["Top Pest"] st.error( f"🎯 **B2B Targeting Signal:** {ew_crop} in **{top_states}** shows " f"CRITICAL/HIGH {top_pest} risk this month. " f"**{len(critical)} states** need preventive spray campaigns NOW — " f"3–4 weeks lead time before outbreak peak." ) with st.expander("📋 Generate District Targeting Brief"): st.markdown(f""" **Market Intelligence Brief — {month_name} 2026** **Crop:** {ew_crop} | **Primary Threat:** {top_pest} **High-Priority States for Immediate Input Push:** """) for r in critical[:5]: st.markdown( f"- **{r['State']}** — Risk Score {r['Risk Score']}/100 " f"| Recommended: {r['Spray']}" ) st.markdown(f""" **Recommended Actions for Agri-Input Partners:** 1. Pre-position stock of {critical[0]['Spray'].split(' or ')[0].split('@')[0].strip()} in high-risk states 2. Activate dealer network in {top_states} for the next 2 weeks 3. Run SMS/WhatsApp advisory campaign to registered farmers 4. Coordinate with KVK/ATMA offices in critical districts *Model confidence: AUC 0.937 | Lead time: 3–4 weeks | Data: 16.5M KCC + 26yr weather* """) st.markdown("---") # ══════════════════════════════════════════════════════════════════════ # SECTION 2 — PRICE OPPORTUNITY SCANNER # ══════════════════════════════════════════════════════════════════════ st.markdown(f"### 💰 2. Price Opportunity Scanner — Harvest Season Forecast") st.caption("presow_v4 model (87% accuracy, 96.2% for stable crops). Compare forecast vs MSP across crops.") if st.button("📊 Scan Price Opportunities", key="b2b_price_scan"): _PRICE_SCAN = [ ("Wheat", "Uttar Pradesh", "Wheat"), ("Wheat", "Punjab", "Wheat"), ("Wheat", "Haryana", "Wheat"), ("Rice", "West Bengal", "Paddy(Desi)(Common)"), ("Rice", "Andhra Pradesh", "Paddy(Desi)(Common)"), ("Cotton", "Maharashtra", "Cotton"), ("Cotton", "Gujarat", "Cotton"), ("Soybean", "Madhya Pradesh", "Soyabean"), ("Soybean", "Maharashtra", "Soyabean"), ("Mustard", "Rajasthan", "Mustard"), ("Mustard", "Haryana", "Mustard"), ("Maize", "Karnataka", "Maize"), ("Groundnut", "Gujarat", "Groundnut"), ("Arhar", "Maharashtra", "Arhar (Tur/Red Gram)(Whole)"), ("Bajra", "Rajasthan", "Bajra(Pearl Millet/Cumbu)"), ] from mandi_advisor.enterprise_engine_v2 import get_presow_signal price_rows = [] with st.spinner("Fetching price forecasts for 15 crop-state combinations…"): for crop, state, agmkt in _PRICE_SCAN: try: sig = get_presow_signal(agmkt, state) if sig and sig.get("p50"): p50 = int(sig["p50"]) msp = _MSP_2025.get(crop, 0) or 0 conf = sig.get("confidence", "MEDIUM") harvest = sig.get("harvest_window", "") vs_msp = p50 - msp if msp else None profit_prob = sig.get("profit_probability", "UNKNOWN") price_rows.append({ "Crop": crop, "State": state, "P50 Forecast": p50, "MSP": msp if msp else "No MSP", "vs MSP": vs_msp, "Confidence": conf, "Harvest": harvest, "Profit Outlook": profit_prob, }) except Exception: pass if price_rows: import pandas as pd df_price = pd.DataFrame(price_rows).sort_values("P50 Forecast", ascending=False) if _PLOTLY_OK: import plotly.express as px df_chart = df_price[df_price["vs MSP"].notna()].copy() df_chart["vs_msp_num"] = df_chart["vs MSP"].astype(float) df_chart["Label"] = df_chart["Crop"] + "\n" + df_chart["State"] df_chart["Outlook Color"] = df_chart["vs_msp_num"].apply( lambda x: "Above MSP" if x > 0 else "Below MSP" ) fig2 = px.bar( df_chart, x="Label", y="vs_msp_num", color="Outlook Color", color_discrete_map={"Above MSP": "#28a745", "Below MSP": "#dc3545"}, title="Harvest Price Forecast vs MSP (₹/quintal)", labels={"vs_msp_num": "₹ vs MSP", "Label": ""}, text="vs_msp_num", ) fig2.update_traces(texttemplate="%{text:+,.0f}", textposition="outside") fig2.update_layout(height=400, margin=dict(t=50, b=80, l=10, r=10)) st.plotly_chart(fig2, use_container_width=True) st.dataframe(df_price, use_container_width=True, hide_index=True) # Opportunity signals above_msp = [r for r in price_rows if isinstance(r["vs MSP"], (int, float)) and r["vs MSP"] > 200] below_msp = [r for r in price_rows if isinstance(r["vs MSP"], (int, float)) and r["vs MSP"] < -100] if above_msp: crops_above = list({r["Crop"] for r in above_msp}) st.success( f"📈 **Opportunity:** {', '.join(crops_above)} forecast ABOVE MSP — " "farmers will expand acreage next season. " "Push seeds, fertilizers, and insurance for these crops." ) if below_msp: crops_below = list({r["Crop"] for r in below_msp}) st.warning( f"📉 **Risk:** {', '.join(crops_below)} forecast BELOW MSP — " "farmers may shift to alternatives. " "Banks: watch for loan stress. Insurers: higher claim probability." ) st.markdown("---") # ══════════════════════════════════════════════════════════════════════ # SECTION 3 — INPUT DEMAND SIGNALS # ══════════════════════════════════════════════════════════════════════ st.markdown("### 🧪 3. Input Demand Signals — What Farmers Will Buy This Month") st.caption("Based on pest risk model + seasonal crop calendar. Use for stock pre-positioning.") _INPUT_DEMAND = { "Wheat": { "Oct–Nov": [("Seed treatment", "Thiram + Carbendazim 50WP @ 2.5g/kg"), ("Basal fertilizer", "DAP + MOP")], "Jan–Feb": [("Fungicide", "Propiconazole 25EC — rust risk"), ("Weedicide", "Sulfosulfuron 75WG")], "Mar–Apr": [("Insecticide", "Imidacloprid 17.8SL — aphid"), ("Urea", "Top-dress 50kg/acre")], }, "Cotton": { "Jun–Jul": [("Seed", "Bt Cotton — sow with onset of monsoon"), ("Basal", "DAP + Urea split")], "Aug–Sep": [("Insecticide", "Spinosad 45SC — bollworm"), ("Fungicide", "Copper Oxychloride — blight")], "Oct–Nov": [("Insecticide", "Imidacloprid — whitefly + CLCuV management"), ("Potash", "SOP 2 bags/acre")], }, "Rice": { "Jun–Jul": [("Nursery seed", "Certified paddy seed"), ("Basal", "DAP + Zinc sulfate")], "Aug–Sep": [("Insecticide", "Chlorpyrifos 20EC — stem borer, BPH"), ("Weedicide", "Bispyribac Na — post-emergence")], "Oct": [("Fungicide", "Tricyclazole 75WP — blast"), ("Urea", "Panicle initiation split")], }, "Soybean": { "Jun–Jul": [("Seed treatment", "Rhizobium + PSB + Thiram"), ("Basal", "SSP + DAP")], "Aug": [("Fungicide", "Chlorothalonil — anthracnose, target spot"), ("Insecticide", "Lambda-cyhalothrin — girdle beetle")], "Sep": [("Insecticide", "Thiacloprid — whitefly + YMV"), ("Micronutrient", "Boron spray — pod fill")], }, } _curr_season_crops = { "Kharif (Jun–Oct)": ["Cotton", "Rice", "Soybean", "Maize", "Groundnut", "Arhar"], "Rabi (Oct–Mar)": ["Wheat", "Gram", "Mustard", "Potato", "Onion"], "Zaid (Mar–Jun)": ["Moong", "Urad", "Maize", "Watermelon"], } curr_month = datetime.now().month if curr_month in (6, 7, 8, 9, 10): curr_season_key = "Kharif (Jun–Oct)" elif curr_month in (11, 12, 1, 2, 3): curr_season_key = "Rabi (Oct–Mar)" else: curr_season_key = "Zaid (Mar–Jun)" season_crops = _curr_season_crops[curr_season_key] st.markdown(f"**Current season: {curr_season_key}** — active crops: {', '.join(season_crops)}") # Month-wise demand calendar _MONTHLY_DEMAND = { 5: {"Cotton": "🌱 Seed + Soil prep", "Wheat": "🌾 Post-harvest storage", "Groundnut": "🌱 Seed + Rhizobium"}, 6: {"Cotton": "🌱 BT Seed + Basal fert", "Rice": "🌱 Nursery + seed treatment", "Soybean": "🌱 Seed + Rhizobium"}, 7: {"Cotton": "🚿 Weedicide + Zinc", "Rice": "🌾 Transplant + DAP", "Soybean": "🔬 Fungicide (anthracnose)"}, 8: {"Cotton": "🦟 Bollworm spray", "Rice": "🦟 Stem borer + BPH", "Soybean": "🦟 Girdle beetle"}, 9: {"Cotton": "🦟 Whitefly Imidacloprid", "Rice": "🔬 Blast fungicide", "Soybean": "🦟 Thiacloprid + Boron"}, 10: {"Cotton": "🌿 Potash top-dress", "Rice": "🌾 Harvest prep", "Wheat": "🌱 Seed procurement"}, 11: {"Wheat": "🌱 Seed treatment + DAP", "Mustard": "🌱 Sow + Basal", "Gram": "🌱 Rhizobium + sow"}, 12: {"Wheat": "🚿 Crown root irrigation", "Mustard": "🦟 Aphid watch", "Potato": "🔬 Late blight spray"}, 1: {"Wheat": "🔬 Rust fungicide (Propiconazole)", "Mustard": "🦟 Aphid Dimethoate", "Gram": "🔬 Botrytis watch"}, 2: {"Wheat": "🦟 Aphid (Imidacloprid)", "Mustard": "🌾 Pod fill — Boron", "Onion": "🔬 Purple blotch spray"}, 3: {"Wheat": "🌾 Harvest prep", "Moong": "🌱 Zaid sowing + Rhizobium", "Urad": "🌱 Zaid sowing"}, 4: {"Moong": "🦟 YMV — whitefly spray", "Urad": "🦟 Aphid watch", "Maize": "🌱 Zaid maize — DAP"}, } curr_demands = _MONTHLY_DEMAND.get(curr_month, {}) if curr_demands: st.markdown(f"**🗓️ Input Demand Signals for {month_name}:**") demand_cols = st.columns(min(4, len(curr_demands))) for i, (crop, action) in enumerate(curr_demands.items()): demand_cols[i % 4].info(f"**{crop}**\n\n{action}") # Client-specific insights st.markdown("---") st.markdown("### 💡 4. Client-Specific Intelligence") if "Input Company" in client_type: st.markdown(""" **🌾 For Agri-Input Companies (Pesticides / Seeds / Fertilizers):** | Signal | Implication | Action | |---|---|---| | HIGH pest risk states identified | Demand spike in 2–3 weeks | Pre-position stock at depot level | | Crop acreage expanding (above MSP forecast) | Higher seed + input demand | Increase dealer inventory | | Price below MSP forecast | Farmer may reduce acreage | Adjust forecasting for next season | | Whitefly CRITICAL in cotton states | Imidacloprid demand spike | Alert regional teams | """) st.info( "📡 **API Available:** Integrate our pest risk endpoint into your CRM for " "real-time district-level targeting. Contact us for enterprise API access." ) elif "Bank" in client_type or "Insurance" in client_type: st.markdown(""" **🏦 For Banks / NBFC / Insurance Companies:** | Signal | Risk Implication | Action | |---|---|---| | CRITICAL pest risk + forecast below MSP | High crop loss + income stress | Flag for loan restructuring | | HIGH pest risk in Kharif districts | Elevated insurance claim probability | Adjust reserve provisioning | | MSP forecast shortfall >10% | Farmer may default on crop loan | Proactive engagement with farmers | | Multiple consecutive LOW forecast years | Persistent distress zone | KCC/KCC loan review | """) st.warning( "⚠️ **Risk Alert:** Our model identifies districts where BOTH pest risk is HIGH " "AND price forecast is BELOW MSP — these are double-stress zones requiring proactive intervention." ) elif "FPO" in client_type or "Cooperative" in client_type: st.markdown(""" **🤝 For FPOs / Cooperatives / Aggregators:** | Signal | Opportunity | Action | |---|---|---| | Price forecast ABOVE MSP | Favorable selling season | Aggregate and sell in bulk | | Pest risk HIGH in neighboring district | Demand for collective spray services | Organize FPO custom hiring services | | Price forecast LOW | Negotiate forward contracts early | Lock in MSP procurement contracts | | Storage-sensitive crops | Hold vs sell decision | Leverage FPO cold storage | """) elif "Trader" in client_type or "Exporter" in client_type: st.markdown(""" **🏪 For Commodity Traders / Exporters:** | Signal | Opportunity | |---|---| | Price P75 significantly above historical | Potential forward buy opportunity | | Multiple states with HIGH risk same crop | Supply disruption risk — build inventory | | Price P25 below MSP in major states | Govt. procurement will absorb supply — limited open market | | LOW confidence crops | Avoid forward contracts — high price volatility | """) elif "Research" in client_type or "Government" in client_type: st.markdown(""" **🔬 For Research / Policy / Government:** **Model Specifications:** - Pest Model: Stacking ensemble (LightGBM + XGBoost + CatBoost + Logistic Regression) | AUC 0.937 - Coverage: 26 crop-pest combinations × 475 districts × 2007–2025 data - Price Model: LightGBM quantile regression (P25/P50/P75) | 87% overall, 96.2% stable crops - Coverage: 290 crops × 36 states × 2001–2026 AGMARKNET data (71.6M rows) - Chatbot: 16.5M KCC records, FAISS+BM25 hybrid retrieval, 99/99 eval score **Potential Policy Applications:** 1. District-level early warning for state agriculture departments 2. Pre-positioning of agricultural inputs at FCI/NAFED warehouses 3. MSP procurement planning based on price forecast shortfalls 4. Crop insurance actuarial modelling with pest risk integration """) st.markdown("---") st.caption( "🤖 Powered by: Pest Model AUC 0.937 · presow_v4 price engine (87% accuracy) · " "16.5M KCC records · ICAR 2024–25 agronomic database | " "For enterprise API access and white-label integration: contact Dhaat" ) def _inject_css() -> None: css = """ """ st.markdown(css, unsafe_allow_html=True) def _render_header(user_block_loc, user_district_loc): """Branded gradient header bar.""" loc_html = "" if user_block_loc and user_district_loc: loc_html = ( '' "📍 " + user_block_loc + ", " + user_district_loc + "" ) html = ( '
' '
' '
🌿
' '
' '
AI Farm Advisor
' '
AI Farm Advisor
' '
' '
' + loc_html + '
' '
' '🤖 Llama-4 Scout' '📚 16.5M Records' '🌐 Hindi · English' '
' '
' ) st.markdown(html, unsafe_allow_html=True) def main() -> None: # ── CSS injection (must be first) ───────────────────────────────────────── _inject_css() # ── load retriever ──────────────────────────────────────────────────────── try: retriever = _load_retriever() except FileNotFoundError as e: st.error( "**FAISS index not found.**\n\n" f"{e}\n\n" "Run `step2_embeddings.py` to build the index first." ) st.stop() # ── sidebar ─────────────────────────────────────────────────────────────── settings = _sidebar(retriever) # active_state = state set in sidebar location picker active_state = st.session_state.get("user_state_loc") or st.session_state.get("active_state") # ── main area ───────────────────────────────────────────────────────────── user_block_loc = st.session_state.get("user_block_loc") user_district_loc = st.session_state.get("user_district_loc") _render_header(user_block_loc, user_district_loc) tab_before, tab_during, tab_after, tab_b2b = st.tabs([ "🌱 Sow Smart", "🌿 Grow Strong", "📦 Sell Better", "🏢 B2B Intelligence", ]) with tab_before: _render_before_sowing_tab(retriever, settings) with tab_after: _render_after_harvest_tab() with tab_b2b: _render_b2b_intelligence_tab() with tab_during: # ── Session state init ───────────────────────────────────────────────── if "messages" not in st.session_state: st.session_state.messages = [] if "retrieval" not in st.session_state: st.session_state.retrieval = [] if "active_crop" not in st.session_state: st.session_state.active_crop = None if "active_problem" not in st.session_state: st.session_state.active_problem = "general" if "active_state" not in st.session_state: st.session_state.active_state = None if "topic_origin" not in st.session_state: st.session_state.topic_origin = None if "prefill_query" not in st.session_state: st.session_state.prefill_query = None # ── Two-column layout: EW on left, Chat on right ─────────────────────── col_ew, col_chat = st.columns([1, 1], gap="large") with col_ew: st.markdown("""
🚨 Pest Early Warning14-day forecast
""", unsafe_allow_html=True) ew_loc = st.session_state.get("user_block_loc") if not ew_loc: st.info( "📍 **Select your location** in the sidebar (State → District → Block) " "to see 14-day pest outbreak predictions for your farm." ) st.caption("The Early Warning uses weather forecast + 16.5M historical KCC records + satellite NDVI.") else: _render_early_warning_tab( st.session_state.get("user_state_loc"), st.session_state.get("user_district_loc"), st.session_state.get("user_block_loc"), st.session_state.get("block_lat"), st.session_state.get("block_lon"), ) with col_chat: st.markdown("""
💬 Crop DoctorAsk in Hindi · English · Regional
""", unsafe_allow_html=True) st.caption("Pest · Disease · Fertilizer · Spray dose · Timing | Hindi / English / Any language") # ── EW alert banner (cross-column) ──────────────────────────────── ew_alert = st.session_state.get("ew_alert") if ew_alert and st.session_state.get("user_block_loc"): # Extract first high-risk crop name for pre-fill suggestion first_crop = ew_alert.split(":")[1].split("(")[0].strip() if ":" in ew_alert else "" st.warning(f"🚨 **Alert from Early Warning:** {ew_alert[:120]}…") if first_crop: if st.button(f"💬 Ask: How to protect my {first_crop}?", key="ew_prefill_btn", use_container_width=True): st.session_state.prefill_query = ( f"Early warning ne bataya hai ki mere {first_crop} mein " f"pest outbreak ka khatra hai. Bachaav ke liye kya karun?" ) st.rerun() # ── Clear chat button ────────────────────────────────────────────── if st.session_state.messages: if st.button("🗑️ Clear conversation", key="clear_chat"): st.session_state.messages = [] st.session_state.retrieval = [] st.session_state.active_crop = None st.session_state.active_problem = "general" st.session_state.topic_origin = None st.session_state.prefill_query = None st.rerun() # Render chat history (inside col_chat context — Streamlit renders to last open column) with col_chat: for i, msg in enumerate(st.session_state.messages): with st.chat_message(msg["role"], avatar="👨‍🌾" if msg["role"] == "user" else "🤖"): st.markdown(msg["content"]) if msg["role"] == "assistant": src_idx = i // 2 if (settings["show_sources"] and src_idx < len(st.session_state.retrieval) and st.session_state.retrieval[src_idx]): _render_sources(st.session_state.retrieval[src_idx]) user_q = st.session_state.messages[i - 1]["content"] if i > 0 else "" _render_feedback( i, user_q, msg["content"], st.session_state.active_crop, st.session_state.active_state, st.session_state.active_problem, ) # ── chat input (must be top-level, not inside column) ───────────────── # Handle EW pre-fill: inject as if user typed it _prefill = st.session_state.pop("prefill_query", None) if _prefill: user_query = _prefill elif user_query_raw := st.chat_input("Ask about pest, disease, spray, fertilizer…"): user_query = user_query_raw else: user_query = None if user_query: with st.chat_message("user", avatar="👨‍🌾"): st.markdown(user_query) st.session_state.messages.append({"role": "user", "content": user_query}) with st.chat_message("assistant", avatar="🤖"): # ── ★ STEP 0: Topic guard — block non-agriculture queries ────── # Fast regex check before any FAISS or LLM call. if not is_agriculture_query(user_query): st.markdown(OFF_TOPIC_RESPONSE) st.session_state.messages.append( {"role": "assistant", "content": OFF_TOPIC_RESPONSE} ) st.session_state.retrieval.append([]) st.stop() # ── ★ STEP 0b: Harmful non-farm guard ─────────────────────── # Catches "not for farm use" harmful queries that slip through # is_agriculture_query() because "farm" appears in the text. if is_harmful_non_farm_query(user_query): st.markdown(OFF_TOPIC_RESPONSE) st.session_state.messages.append( {"role": "assistant", "content": OFF_TOPIC_RESPONSE} ) st.session_state.retrieval.append([]) st.stop() # ── Step 1: Pre-processing + multi-turn context ─────────────── # 1a. Language detection — short follow-ups (yes/ha/nahi/दोमट) # inherit the conversation language instead of mis-detecting. reply_language = detect_language(user_query) if len(user_query.split()) <= 2 and st.session_state.messages: # Find last user message that was longer (more reliable signal) for _prev in reversed(st.session_state.messages): if _prev["role"] == "user" and len(_prev["content"].split()) > 2: reply_language = detect_language(_prev["content"]) break # 1b. Season + state detection season = get_current_season() detected_state = ( settings.get("manual_state") or detect_state(user_query) or st.session_state.active_state ) # 1c. Keyword-based crop + problem detection detected_crop = detect_crop(user_query) problem_type = classify_problem(user_query) normalized_q = normalize_query(user_query) # 1d. Query rewriting for short/ambiguous queries # Expands "wheat pilli patti" → detailed English retrieval query rewritten_q = rewrite_query_for_retrieval( normalized_q, detected_crop, problem_type, detected_state, season, ) retrieval_q = rewritten_q # used for FAISS search # ── Cache check (skip RAG + LLM entirely on hit) ────────────── _ck = _make_cache_key( user_query, detected_crop, detected_state, problem_type, season["name"] ) if _ck in _RESPONSE_CACHE: full_response = _RESPONSE_CACHE[_ck] st.caption("⚡ Instant answer (cached)") st.write(full_response) st.session_state.messages.append( {"role": "assistant", "content": full_response} ) st.session_state.retrieval.append([]) st.rerun() # 1c. LLM understanding — only when keywords fail # Avoids extra API call for clear-cut queries _kw_crop_found = detected_crop is not None _kw_problem_found = problem_type != "general" if not _kw_crop_found or not _kw_problem_found: with st.spinner("🧠 Understanding query…"): llm_info = understand_query_llm(user_query) if not _kw_crop_found and llm_info.get("crop"): detected_crop = llm_info["crop"] if not _kw_problem_found and llm_info.get("problem", "general") != "general": problem_type = llm_info["problem"] # 1d. Smart crop inheritance: # Only inherit previous crop if the new query doesn't # clearly introduce a DIFFERENT topic (crop_selection resets context) new_crop_in_query = detect_crop(user_query) is not None or ( not _kw_crop_found and detected_crop is not None ) if detected_crop is None: # No crop in this query — inherit previous detected_crop = st.session_state.active_crop # else: use the newly detected crop (don't inherit) # 1e. Problem type inheritance — but crop_selection always wins if problem_type == "general": problem_type = st.session_state.active_problem # crop_selection question resets context (farmer is starting fresh) if problem_type == "crop_selection": st.session_state.active_crop = None st.session_state.active_problem = "crop_selection" st.session_state.topic_origin = None detected_crop = None # don't filter for crop_selection # 1f. Persist newly detected values for next turn if new_crop_in_query: st.session_state.active_crop = detected_crop if classify_problem(user_query) != "general" or ( not _kw_problem_found and problem_type != "general" ): st.session_state.active_problem = problem_type # Record original topic on first substantive turn if st.session_state.topic_origin is None and problem_type != "general": st.session_state.topic_origin = { "query": user_query, "crop": detected_crop, "problem": problem_type, } # ── Step 2: Multi-step retrieval ────────────────────────────── with st.spinner("🔍 Searching KCC knowledge base…"): t_ret = time.perf_counter() # UPGRADE 1: pass location for boosted retrieval _r_state = st.session_state.get("user_state_loc", "") or detected_state or "" _r_district = st.session_state.get("user_district_loc", "") or "" docs = multi_step_retrieve( retriever, retrieval_q, retrieval_q, detected_crop, problem_type, settings, state=_r_state, district=_r_district, ) # Soft re-rank: prefer results from detected state docs = _apply_state_preference(docs, detected_state) ret_ms = (time.perf_counter() - t_ret) * 1000 # ── Step 3: Confidence check ────────────────────────────────── top_score = docs[0].score if docs else 0.0 low_confidence = top_score < LOW_CONF_THRESHOLD # ── Step 3b: Named disease / pest + irrigation detection ────── named_disease_key = detect_named_disease(user_query) icar_priority_ctx = "" if named_disease_key: icar_priority_ctx = build_icar_context(named_disease_key, detected_crop) low_confidence = False # Irrigation: inject ICAR validated water-requirement numbers if detect_irrigation_query(user_query): irr_ctx = build_irrigation_context(user_query, detected_crop) if irr_ctx: icar_priority_ctx = icar_priority_ctx + irr_ctx low_confidence = False # Agronomy: inject ICAR seed rate / spacing / waterlogging / variety data if detect_agronomy_query(user_query) or problem_type == "agronomy": agro_ctx = build_agronomy_context(user_query, detected_crop) if agro_ctx: icar_priority_ctx = icar_priority_ctx + agro_ctx low_confidence = False # Crop selection: inject crop decision profiles (yield, water, risk, profit) if problem_type == "crop_selection": crop_sel_ctx = build_crop_decision_context(user_query, detected_state) if crop_sel_ctx: icar_priority_ctx = icar_priority_ctx + "\n" + crop_sel_ctx low_confidence = False # Nutrient deficiency: inject ICAR nutrient cards nutrient_key = detect_nutrient_deficiency(user_query) if nutrient_key: nutr_ctx = build_nutrient_context(nutrient_key) if nutr_ctx: icar_priority_ctx = icar_priority_ctx + nutr_ctx low_confidence = False # Post-harvest storage: inject post-harvest cards ph_key = detect_postharvest_query(user_query) if ph_key: ph_ctx = build_postharvest_context(ph_key) if ph_ctx: icar_priority_ctx = icar_priority_ctx + ph_ctx low_confidence = False # Government schemes: inject scheme cards with exact figures scheme_key = detect_govt_scheme(user_query) if scheme_key: scheme_ctx = build_scheme_context(scheme_key) if scheme_ctx: icar_priority_ctx = icar_priority_ctx + scheme_ctx low_confidence = False # ── Step 3f: Semantic ICAR catch-all (undetected conditions) ─────────── if not icar_priority_ctx and _ICAR_AVAILABLE and _ICAR_RETRIEVER is not None: try: _sem = _ICAR_RETRIEVER.search(user_query, top_k=2) if _sem: icar_priority_ctx = _ICAR_RETRIEVER.format_for_llm(_sem) + nn low_confidence = False except Exception: pass # ── Step 4: Build enriched context ──────────────────────────── kcc_context = retriever.format_context(docs) # ICAR cards go first so LLM sees validated data before KCC records context = (icar_priority_ctx + kcc_context) if icar_priority_ctx else kcc_context # UPGRADE 5: Golden set — prepend verified answer if high confidence match try: from step3_retrieval import get_golden_retriever as _get_gr _golden_ret = _get_gr() if _golden_ret.size > 0: _golden_hits = _golden_ret.lookup(user_query, top_k=2) if _golden_hits and _golden_hits[0]["score"] > 0.85: _gh = _golden_hits[0] context = ( "[VERIFIED KCC ANSWER — high confidence match]\n" f"Q: {_gh['query']}\n" f"A: {_gh['answer']}\n" f"(Crop: {_gh.get('crop','')}, State: {_gh.get('state','')}, " f"Match score: {_gh['score']:.2f})\n\n" + context ) except Exception: pass # Golden retriever errors never block main flow meta_lines: list[str] = [] if st.session_state.topic_origin: t = st.session_state.topic_origin meta_lines.append( f"⚠️ CONVERSATION CONTEXT (CRITICAL): Farmer is continuing a " f"conversation about {t['problem'].upper()} in " f"{t['crop'] or 'their crop'}. Original question: \"{t['query']}\". " f"This is a follow-up reply — do NOT change the topic. " f"Answer specifically about {t['crop'] or 'the crop'} {t['problem']}." ) if detected_crop: meta_lines.append( f"⚠️ DETECTED CROP (MANDATORY): {detected_crop} — " f"ALL recommendations must be for {detected_crop} only." ) if problem_type != "general": meta_lines.append(f"PROBLEM TYPE: {problem_type.upper()}") safety_note = SAFETY_GUARDRAILS.get(problem_type, "") if safety_note: meta_lines.append(safety_note) if low_confidence: meta_lines.append( f"LOW CONFIDENCE (top score: {top_score*100:.0f}%) — " "ask the farmer 1-2 clarifying questions instead of guessing." ) # ── Enrich context: state + season + soil + weather + mandi ── season_line = ( f"CURRENT AGRICULTURAL SEASON: {season['name']} " f"({season['months']}) — major crops: {season['crops']}" ) if season.get("planning_note"): season_line += f"\n{season['planning_note']}" meta_lines.append(season_line) if detected_state: soil_ctx = get_soil_context(detected_state) detected_district = _extract_location_from_query(user_query) if detected_district: state_line = ( f"FARMER LOCATION (from query): {detected_district}, {detected_state} " f"(MANDATORY — tailor advice to this specific district)" ) else: state_line = f"FARMER'S STATE: {detected_state} (MANDATORY — do NOT assume a different state)" if soil_ctx: state_line += f" | {soil_ctx}" meta_lines.append(state_line) # Weather context for weather-type queries if problem_type == "weather": wx = _fetch_weather(detected_state) if wx: wx_ctx = _format_weather_context(wx, detected_state) if wx_ctx: meta_lines.append(wx_ctx) # Mandi price context for crop selection queries if problem_type == "crop_selection" and config.DATA_GOV_API_KEY: with st.spinner("💰 Fetching live mandi prices…"): price_ctx = _build_price_context(season, detected_state) if price_ctx: meta_lines.append(price_ctx) # -- 3-MODEL: Presow price forecast (presow_v4) _is_sowing_q = any(kw in user_query.lower() for kw in _CHATBOT_PRESOW_KEYWORDS) if (problem_type == "crop_selection" or _is_sowing_q) and detected_crop: try: _presow_ctx = _build_presow_chatbot_context( detected_crop, detected_state or "India" ) if _presow_ctx: meta_lines.append(_presow_ctx) except Exception: pass # -- 3-MODEL: Pest risk early warning (AUC 0.937) _is_pest_sow_q = ( problem_type in ("crop_selection", "pest_disease") or any(kw in user_query.lower() for kw in _CHATBOT_PEST_KEYWORDS) ) if _is_pest_sow_q and detected_crop and detected_state: try: _pest_ctx = _build_pest_risk_chatbot_context( detected_state, st.session_state.get("user_district_loc") or "", detected_crop, datetime.now().month, ) if _pest_ctx: meta_lines.append(_pest_ctx) except Exception: pass # ── Confidence signal: summarize retrieved evidence quality ── if docs: doc_states = list({d.state for d in docs if d.state and d.state not in ("", "UNKNOWN")}) doc_years = [] for d in docs: try: doc_years.append(int(d.year)) except (ValueError, TypeError): pass yr_str = (f"{min(doc_years)}–{max(doc_years)}" if doc_years else "various years") state_str = ", ".join(doc_states[:3]) + ("…" if len(doc_states) > 3 else "") conf_label = ("HIGH" if top_score > 0.75 else "MEDIUM" if top_score > LOW_CONF_THRESHOLD else "LOW") old_src_warn = "" if doc_years and max(doc_years) < 2016: old_src_warn = (" ⚠️ ALL retrieved sources are pre-2016 — " "chemical doses / varieties may be outdated. " "Prefer ICAR reference doses over retrieved context.") meta_lines.append( f"EVIDENCE SUMMARY: {len(docs)} similar KCC cases found " f"(confidence: {conf_label}, top similarity: {top_score*100:.0f}%). " f"States covered: {state_str or 'various'}. " f"Data period: {yr_str}.{old_src_warn} " "Use this evidence to give a grounded, specific answer." ) if meta_lines: context = "\n".join(meta_lines) + "\n\n" + context # ── Step 5: Build prompt + generate ────────────────────────── history = [ m for m in st.session_state.messages[-6:] if "user" in m["role"] or "assistant" in m["role"] ] history_pairs = [] for j in range(0, len(history), 2): if j + 1 < len(history): history_pairs.append({ "user": history[j]["content"], "assistant": history[j + 1]["content"], }) # UPGRADE 2: pass location + crop+problem for dose lookup _farmer_state = st.session_state.get("user_state_loc", "") or detected_state or "" _farmer_district = st.session_state.get("user_district_loc", "") or "" prompt = _build_prompt( user_query, context, history_pairs, problem_type, reply_language, state=_farmer_state, district=_farmer_district, detected_crop=detected_crop or "", problem_detail=problem_type, ) badges = [] if detected_crop: badges.append(f"🌿 {detected_crop}") if detected_state: badges.append(f"📍 {detected_state}") if problem_type != "general": badges.append(f"🔬 {problem_type}") if low_confidence: badges.append("⚠️ Low confidence") badge_str = " | " + " | ".join(badges) if badges else "" st.caption(f"🔍 Retrieved {len(docs)} sources in {ret_ms:.0f} ms{badge_str}") full_response = st.write_stream(_stream_llm_response(prompt)) # ── Confidence card (Improvement #5) ────────────────────── # Show a visible, colour-coded confidence signal AFTER the # response — tells B2B clients and agronomists how much to # trust this answer without reading retrieval internals. avg_score = ( float(np.mean([d.score for d in docs])) if docs else 0.0 ) if top_score >= 0.85: conf_emoji, conf_txt, conf_colour = ( "🟢", "High confidence", "success" ) conf_detail = ( f"Top match score: **{top_score:.2f}** · " f"Avg score: {avg_score:.2f} — " "answer grounded in closely-matching KCC records." ) elif top_score >= LOW_CONF_THRESHOLD: conf_emoji, conf_txt, conf_colour = ( "🟡", "Moderate confidence", "warning" ) conf_detail = ( f"Top match score: **{top_score:.2f}** · " f"Avg score: {avg_score:.2f} — " "good match found; minor details may vary by location." ) else: conf_emoji, conf_txt, conf_colour = ( "🔴", "Low confidence", "error" ) conf_detail = ( f"Top match score: **{top_score:.2f}** · " f"Avg score: {avg_score:.2f} — " "no close KCC match found. " "**Please verify this advice with your local KVK or agriculture officer (1800-180-1551).**" ) # Only show the card for pest/disease/nutrient (high-stakes). # For scheme / mandi queries the score is less meaningful. if problem_type in ("pest", "disease", "nutrient", "agronomy", "crop_selection") or low_confidence: _msg = f"{conf_emoji} **{conf_txt}** — {conf_detail}" if conf_colour == "success": st.success(_msg) elif conf_colour == "warning": st.warning(_msg) else: st.error(_msg) # ── Post-generation safety checks ───────────────────────────── safety_violations = check_chemical_safety(full_response, problem_type) for v in safety_violations: st.warning(v) for v in check_banned_pesticides(full_response): st.error(v) # red — banned chemicals are critical safety issue # ── ICAR mandatory chemical supplement ──────────────────────── # If LLM skipped a mandatory ICAR chemical, show yellow info box _user_query = st.session_state.messages[-1]["content"] if st.session_state.messages else "" for _note in _icar_mandatory_supplement(_user_query, full_response): st.info(_note) # ── Stale price scanner: catch old KCC ₹ data shown as current ── # KCC records contain historical prices (2006-2024). If LLM quotes # them despite RULE #1, add a clear disclaimer so farmer isn't misled. _PRICE_PATTERN = re.compile( r'(₹\s*[\d,]+\s*/\s*(quintal|kg|mt|ton|q)' r'|Rs\.?\s*[\d,]+\s*/?\s*(quintal|kg)' r'|modal\s+price' r'|[\d,]{3,}\s*(rupees?|रुपए|रुपये)\s+per\s+(quintal|kg)' r'|\bprice\b.{0,30}\bquintal\b)', re.IGNORECASE, ) if _PRICE_PATTERN.search(full_response): st.warning( "⚠️ **Price Alert**: The figures above are from historical KCC records " "(2006–2024) and are NOT today's market prices. " "For live rates, check the **Mandi Prices** tab or agmarknet.gov.in." ) # ── Completeness validator (pest/disease only) ───────────────── if problem_type in ("pest", "disease", "nutrient") and len(full_response) > 80: resp_lower = full_response.lower() missing = [] # Check for dose/quantity mention has_dose = bool(re.search( r'\b(\d+\s*(ml|g|kg|gm|gram|litre|liter|oz|ml/|g/|%)' r'|\d+\s*-\s*\d+\s*(ml|g|kg)|per\s*(acre|litre|liter|hectare)' r'|dose|matra|khuraak|concentration)', resp_lower)) # Check for timing mention has_timing = bool(re.search( r'\b(spray|chhidkav|application|apply|din|days?|week|saptah' r'|morning|evening|subah|sham|before|after|interval|repeat' r'|baar|times?|season|mausam)', resp_lower)) # Check for chemical/treatment mention has_treatment = bool(re.search( r'\b(fungicide|insecticide|pesticide|dawai|dawa|spray|neem' r'|chlorpyrifos|mancozeb|copper|sulphur|sulfur|emamectin' r'|imidacloprid|thiamethoxam|profenofos|trichoderma)', resp_lower)) if not has_dose: missing.append("dose/quantity (matra)") if not has_timing: missing.append("timing/frequency (kab aur kitni baar)") if not has_treatment and problem_type in ("pest", "disease"): missing.append("treatment/chemical name") if missing: st.caption( f"ℹ️ **Tip**: Ask the chatbot to also provide — " + ", ".join(missing) + " — for a complete recommendation." ) # ── Cache the response ──────────────────────────────────────── _cache_store(_ck, full_response) # ── Persist state + update session ──────────────────────────── if detected_state: st.session_state.active_state = detected_state st.session_state.messages.append( {"role": "assistant", "content": full_response} ) st.session_state.retrieval.append( docs if settings["show_sources"] else [] ) if settings["show_sources"] and docs: _render_sources(docs) st.rerun() if __name__ == "__main__": main()