Spaces:
Sleeping
Sleeping
| """AgroSense Streamlit chat UI. | |
| Run: streamlit run ui/app.py | |
| Talks to the FastAPI backend if it's reachable; otherwise falls back to running | |
| the RAG engine in-process so the UI works even without the API server. | |
| """ | |
| from __future__ import annotations | |
| import os | |
| import sys | |
| from pathlib import Path | |
| import requests | |
| import streamlit as st | |
| # Make the local package importable when run via `streamlit run ui/app.py`. | |
| sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) | |
| API_URL = os.getenv("AGROSENSE_API_URL", "http://127.0.0.1:8000") | |
| SAMPLE_QUERIES = [ | |
| "My soil is sandy, rainfall 900 mm, I plan to grow maize. What fertilizer and pest steps?", | |
| "Give me the fertilizer schedule for aromatic crops like mint.", | |
| "How do I prevent disease in paddy on loamy soil?", | |
| "What pest management is recommended for cotton?", | |
| ] | |
| st.set_page_config(page_title="AgroSense Advisor", page_icon="π±", layout="centered") | |
| # Increase the base font size by 3px (Streamlit's default root is 16px). Most text | |
| # (incl. the rem-based tickers) scales off the root, so this enlarges the whole app. | |
| st.markdown( | |
| "<style>html, body, [data-testid='stAppViewContainer']" | |
| " { font-size: 19px !important; }</style>", | |
| unsafe_allow_html=True, | |
| ) | |
| def query_via_api(text: str, location: str | None, language: str, | |
| include_prices: bool) -> dict | None: | |
| try: | |
| payload: dict = {"query": text, "language": language, | |
| "include_prices": include_prices} | |
| if location: | |
| payload["location"] = location | |
| resp = requests.post(f"{API_URL}/query", json=payload, timeout=60) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except Exception: | |
| return None | |
| def get_local_engine(): | |
| from agrosense import RAGEngine | |
| return RAGEngine() | |
| def query_local(text: str, location: str | None, language: str, | |
| include_prices: bool) -> dict: | |
| return get_local_engine().answer( | |
| text, location=location, language=language, include_prices=include_prices | |
| ).to_dict() | |
| def get_satellite(location: str) -> dict | None: | |
| """Fetch satellite monitoring via the API, falling back to the local engine.""" | |
| try: | |
| resp = requests.get(f"{API_URL}/satellite", params={"location": location}, timeout=40) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| report = get_local_engine().get_satellite(location=location) | |
| return report.to_dict() if report else None | |
| except Exception: | |
| return None | |
| def get_environment(location: str) -> dict | None: | |
| """Fetch the location environment profile via the API, falling back to engine.""" | |
| try: | |
| resp = requests.get(f"{API_URL}/environment", params={"location": location}, timeout=40) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| p = get_local_engine().get_environment(location=location) | |
| return p.to_dict() if p else None | |
| except Exception: | |
| return None | |
| def _m(value, suffix=""): | |
| return f"{value}{suffix}" if value is not None else "β" | |
| def render_environment(p: dict) -> None: | |
| sun, wind, aq = p.get("sunlight", {}), p.get("wind", {}), p.get("air_quality", {}) | |
| gw, pollen = p.get("groundwater", {}), p.get("pollen", {}) | |
| a, b, c = st.columns(3) | |
| a.metric("Latitude", _m(p.get("latitude"))) | |
| b.metric("Longitude", _m(p.get("longitude"))) | |
| c.metric("Altitude", _m(p.get("elevation_m"), " m")) | |
| # Wind: keep speed + bearing in the VALUE (not delta, which renders a β² arrow). | |
| wind_val = "β" | |
| if wind.get("speed_kmh") is not None: | |
| wind_val = f"{wind['speed_kmh']} km/h" | |
| if wind.get("direction_compass"): | |
| wind_val += f" {wind['direction_compass']}" | |
| d, e, f = st.columns(3) | |
| d.metric("Population", _m(p.get("population"))) | |
| e.metric("Humidity", _m(p.get("humidity_pct"), " %")) | |
| f.metric("Wind", wind_val) | |
| if wind.get("direction_deg") is not None: | |
| f.caption(f"from {wind['direction_deg']}Β°") | |
| g, h, i = st.columns(3) | |
| g.metric("Sunshine today", _m(sun.get("sunshine_hours"), " h")) | |
| h.metric("UV index (max)", _m(sun.get("uv_index_max"))) | |
| i.metric("Solar now", _m(sun.get("shortwave_wm2"), " W/mΒ²")) | |
| # AQI: category in the VALUE, not delta. | |
| aqi_val = "β" | |
| if aq.get("us_aqi") is not None: | |
| aqi_val = str(aq["us_aqi"]) | |
| if aq.get("category"): | |
| aqi_val += f" Β· {aq['category']}" | |
| j, k, l = st.columns(3) | |
| j.metric("Air quality (US AQI)", aqi_val) | |
| k.metric("PM2.5", _m(aq.get("pm2_5"), " Β΅g/mΒ³")) | |
| l.metric("PM10", _m(aq.get("pm10"), " Β΅g/mΒ³")) | |
| if pollen.get("available"): | |
| st.write("**Pollen** (grains/mΒ³): " | |
| + ", ".join(f"{k2}: {v}" for k2, v in pollen.get("values", {}).items())) | |
| else: | |
| st.caption("πΌ Pollen: " + pollen.get("note", "unavailable")) | |
| if gw.get("level_m") is not None: | |
| st.metric("Ground water table", f"{gw['level_m']} m below ground") | |
| else: | |
| st.metric("Ground water β soil moisture (3-9 cm)", | |
| _m(gw.get("soil_moisture_m3m3"), " mΒ³/mΒ³")) | |
| st.caption("π§ " + gw.get("note", "")) | |
| st.caption(f"Sources: {', '.join(p.get('sources', []))}") | |
| def get_hazards(location: str) -> dict | None: | |
| """Natural-hazard events + active fires near the location.""" | |
| try: | |
| resp = requests.get(f"{API_URL}/hazards", params={"location": location}, timeout=40) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| return get_local_engine().get_hazards(location=location) | |
| except Exception: | |
| return None | |
| def render_hazards(rep: dict) -> None: | |
| events = rep.get("events", []) | |
| if events: | |
| st.markdown(f"**{len(events)} natural-hazard event(s) within range** " | |
| f"(NASA EONET):") | |
| for e in events[:8]: | |
| dist = f" Β· {e['distance_km']} km away" if e.get("distance_km") is not None else "" | |
| st.warning(f"**{e['category']}** β {e['title']}{dist}") | |
| else: | |
| st.success("No active EONET hazard events within range.") | |
| fires = rep.get("fires") | |
| if fires is None: | |
| st.caption("π₯ Active fires: set AGROSENSE_FIRMS_MAP_KEY (free NASA FIRMS key) to enable.") | |
| elif not fires: | |
| st.caption("π₯ No active fire detections nearby (NASA FIRMS).") | |
| else: | |
| nearest = fires[0] | |
| st.error(f"π₯ {len(fires)} active fire detection(s) nearby (NASA FIRMS); " | |
| f"nearest {nearest.get('distance_km')} km away " | |
| f"(confidence {nearest.get('confidence')}, {nearest.get('acq_date')}).") | |
| def render_consult_session(c: dict) -> None: | |
| exp = c.get("expert") | |
| st.markdown(f"**Consultation {c['id']}** Β· status: **{c['status']}** Β· " | |
| f"channel: {c['channel']}") | |
| if exp: | |
| st.success(f"π¨ββοΈ Assigned: **{exp['name']}** β {exp['specialization']} \n" | |
| f"{exp['region']} Β· speaks {', '.join(exp['languages'])}") | |
| else: | |
| st.info("Queued β awaiting an available expert.") | |
| st.caption(f"Shared with expert: {c.get('summary', '')}") | |
| notes = c.get("notifications") or [] | |
| ok_channels = ", ".join(n["channel"] for n in notes if n.get("ok")) | |
| if ok_channels: | |
| st.caption(f"π Expert notified via: {ok_channels} " | |
| "(set AGROSENSE_NOTIFY_WEBHOOK / SMTP env for real delivery).") | |
| if c.get("room_url"): | |
| st.link_button("π₯ Join live video room", c["room_url"], width="stretch") | |
| st.caption("Public Jitsi room (may ask the first joiner to sign in as moderator).") | |
| st.markdown("**Conversation**") | |
| for m in c.get("messages", []): | |
| who = {"farmer": "π§βπΎ You", "expert": "π¨ββοΈ Expert", "system": "βΉοΈ System"}.get( | |
| m["sender"], m["sender"]) | |
| ts = f" Β· _{m['at']}_" if m.get("at") else "" | |
| st.markdown(f"- **{who}:** {m['text']}{ts}") | |
| def render_consultation(c: dict) -> None: | |
| hs = c.get("health_status", "Inconclusive") | |
| box = {"Likely healthy": st.success, "Needs attention": st.warning}.get(hs, st.info) | |
| box(f"**Diagnosis:** {c.get('diagnosis', 'β')} \n" | |
| f"**Health:** {hs} Β· **Severity:** {c.get('severity', 'n/a')} Β· " | |
| f"confidence {round(c.get('confidence', 0) * 100)}% (basis: {c.get('diagnosis_basis')})") | |
| if c.get("weather_note"): | |
| st.warning("π¦οΈ Timing: " + c["weather_note"]) | |
| st.markdown("**π Prescription**") | |
| for p in c.get("prescription", []): | |
| st.markdown(f"- **{p['category']}:** {p['instruction']}") | |
| if c.get("citations"): | |
| st.caption("Sources: " + ", ".join(c["citations"])) | |
| if c.get("follow_up"): | |
| st.markdown(f"**π Follow-up:** {c['follow_up']}") | |
| st.caption("βοΈ " + c.get("disclaimer", "")) | |
| def classify_image(image_bytes: bytes) -> dict: | |
| """Classify a plant/leaf image (disease + species), API then engine fallback.""" | |
| try: | |
| files = {"file": ("upload.jpg", image_bytes, "image/jpeg")} | |
| resp = requests.post(f"{API_URL}/vision/classify", files=files, | |
| params={"task": "all"}, timeout=60) | |
| resp.raise_for_status() | |
| return resp.json() | |
| except Exception: | |
| eng = get_local_engine() | |
| return {"disease": eng.predict_plant_disease(image_bytes), | |
| "plant": eng.predict_plant_species(image_bytes), | |
| "pest": eng.predict_pest(image_bytes)} | |
| def get_advisories(location: str, crop: str | None, stage: str | None) -> dict | None: | |
| """Fetch fused decision advisories via the API, falling back to the engine.""" | |
| params = {"location": location} | |
| if crop: | |
| params["crop"] = crop | |
| if stage: | |
| params["stage"] = stage | |
| try: | |
| resp = requests.get(f"{API_URL}/advisories", params=params, timeout=60) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| rep = get_local_engine().get_fusion_advisories(location=location, crop=crop, stage=stage) | |
| return rep.to_dict() if rep else None | |
| except Exception: | |
| return None | |
| def get_planetary(location: str) -> dict | None: | |
| """Fetch planetary positions via the API, falling back to the engine.""" | |
| try: | |
| resp = requests.get(f"{API_URL}/planetary", params={"location": location}, timeout=30) | |
| resp.raise_for_status() | |
| data = resp.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| rep = get_local_engine().get_planetary(location=location) | |
| return rep.to_dict() if rep else None | |
| except Exception: | |
| return None | |
| def render_planetary(rep: dict) -> None: | |
| mp = rep.get("moon_phase", {}) | |
| if mp: | |
| st.markdown(f"π **Moon phase:** {mp.get('name', 'β')} β " | |
| f"{round(mp.get('illumination', 0) * 100)}% illuminated") | |
| rows = [ | |
| {"Body": b["name"], "AltitudeΒ°": b["altitude_deg"], "AzimuthΒ°": b["azimuth_deg"], | |
| "Dir": b["azimuth_compass"], "Visible": "β " if b["above_horizon"] else "β"} | |
| for b in rep.get("bodies", []) | |
| ] | |
| if rows: | |
| st.dataframe(rows, hide_index=True) | |
| st.caption(f"{rep.get('utc_time')} Β· {rep.get('source')}") | |
| def render_advisories(report: dict) -> None: | |
| advs = report.get("advisories", []) | |
| if not advs: | |
| st.success("No urgent signals β conditions look unremarkable right now.") | |
| for a in advs: | |
| msg = f"**{a['title']}** β {a['action']} \n_Why: {a['rationale']}_" | |
| urgency = a.get("urgency") | |
| (st.error if urgency == "high" else st.warning if urgency == "medium" | |
| else st.info)(msg) | |
| img = report.get("imagery", {}) | |
| if img.get("ndvi"): | |
| st.image(img["ndvi"], caption="NDVI context (MODIS)", width="stretch") | |
| st.caption(f"Source: {report.get('source')}") | |
| def render_satellite(report: dict) -> None: | |
| st.markdown(f"**{report['location_name']}** Β· source: {report['source']}") | |
| c1, c2 = st.columns(2) | |
| with c1: | |
| st.image(report["imagery"]["true_color"], | |
| caption=f"True color Β· {report['truecolor_date']}", width="stretch") | |
| with c2: | |
| st.image(report["imagery"]["ndvi"], | |
| caption=f"NDVI (greener = denser vegetation) Β· {report['ndvi_date']}", | |
| width="stretch") | |
| ac = report.get("agroclimate") | |
| if ac: | |
| m1, m2, m3 = st.columns(3) | |
| m1.metric("Avg solar", f"{ac['avg_solar_mj']} MJ/mΒ²/d") | |
| m2.metric("Temp range", f"{ac['avg_tmin_c']}β{ac['avg_tmax_c']} Β°C") | |
| m3.metric(f"Rain ({ac['days']}d)", f"{ac['total_precip_mm']} mm") | |
| for note in ac.get("notes", []): | |
| st.info(note) | |
| nd = report.get("numeric_ndvi") | |
| if nd and nd.get("latest") is not None: | |
| st.markdown(f"**Field NDVI** Β· {nd['source']}") | |
| n1, n2, n3 = st.columns(3) | |
| n1.metric("Latest NDVI", nd["latest"], help=f"on {nd.get('latest_date')}") | |
| n2.metric("Mean NDVI", nd["mean"]) | |
| n3.metric("Trend", str(nd.get("trend"))) | |
| obs = nd.get("observations", []) | |
| if len(obs) >= 2: | |
| st.line_chart( | |
| {"date": [o["date"] for o in obs], "NDVI": [o["ndvi"] for o in obs]}, | |
| x="date", y="NDVI", | |
| ) | |
| for note in nd.get("notes", []): | |
| st.success(note) | |
| elif report.get("numeric_ndvi") is None: | |
| st.caption("Field-level NDVI not configured β set Earth Engine credentials to " | |
| "enable Sentinel-2 (~10 m) NDVI. Showing MODIS imagery + agroclimate.") | |
| if report["imagery"].get("worldview"): | |
| st.markdown(f"[π Open interactive view in NASA Worldview]({report['imagery']['worldview']})") | |
| def answer_query(text: str, location: str | None, language: str, | |
| include_prices: bool) -> tuple[dict, str]: | |
| result = query_via_api(text, location, language, include_prices) | |
| if result is not None: | |
| return result, "API" | |
| return query_local(text, location, language, include_prices), "in-process" | |
| def render_prices(prices: dict) -> None: | |
| s = prices.get("summary") or {} | |
| if s: | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Modal min", f"βΉ{s['modal_min']}") | |
| c2.metric("Modal avg", f"βΉ{s['modal_avg']}") | |
| c3.metric("Modal max", f"βΉ{s['modal_max']}") | |
| rows = [ | |
| {"Market": r["market"], "State": r["state"], "Variety": r["variety"], | |
| "Min": r["min_price"], "Max": r["max_price"], "Modal": r["modal_price"], | |
| "Date": r["arrival_date"]} | |
| for r in prices.get("records", []) | |
| ] | |
| if rows: | |
| st.dataframe(rows, hide_index=True) | |
| for note in prices.get("notes", []): | |
| st.info(note) | |
| st.caption(f"Source: {prices.get('source')} (βΉ per quintal)") | |
| def get_news_items(region: str, query: str | None) -> list[dict]: | |
| """Latest headlines for a region/topic via the API, falling back to the engine.""" | |
| params: dict = {"limit": 15, "region": region} | |
| if query: | |
| params["query"] = query | |
| try: | |
| resp = requests.get(f"{API_URL}/news", params=params, timeout=20) | |
| resp.raise_for_status() | |
| return resp.json().get("items", []) | |
| except Exception: | |
| pass | |
| try: | |
| return [i.to_dict() | |
| for i in get_local_engine().get_news(query=query, region=region, limit=15)] | |
| except Exception: | |
| return [] | |
| def get_current_weather(location: str) -> dict | None: | |
| """Local weather for the top-bar strip, via API then engine fallback.""" | |
| try: | |
| r = requests.get(f"{API_URL}/weather", params={"location": location}, timeout=20) | |
| r.raise_for_status() | |
| data = r.json() | |
| return data if data.get("available") else None | |
| except Exception: | |
| pass | |
| try: | |
| wf = get_local_engine().get_weather(location=location) | |
| return wf.to_dict() if wf else None | |
| except Exception: | |
| return None | |
| def render_weather_strip(w: dict) -> None: | |
| daily = (w.get("daily") or [{}])[0] | |
| bits = [f"π€οΈ <b>{w.get('location_name', '')}</b>"] | |
| if w.get("current_temp_c") is not None: | |
| bits.append(f"{w['current_temp_c']}Β°C now") | |
| if w.get("current_humidity") is not None: | |
| bits.append(f"humidity {w['current_humidity']}%") | |
| if daily.get("tmin_c") is not None and daily.get("tmax_c") is not None: | |
| bits.append(f"today {daily['tmin_c']}β{daily['tmax_c']}Β°C") | |
| if daily.get("precip_mm") is not None: | |
| prob = daily.get("precip_prob") | |
| bits.append(f"rain {daily['precip_mm']}mm" | |
| + (f" ({prob}%)" if prob is not None else "")) | |
| st.markdown( | |
| "<div style='background:#10324a;color:#e8f4ff;padding:7px 14px;border-radius:8px;" | |
| "font-size:0.9rem;margin-top:2px;'>" + " Β· ".join(bits) + "</div>", | |
| unsafe_allow_html=True, | |
| ) | |
| advs = w.get("advisories") or [] | |
| if advs: | |
| st.caption("β οΈ " + advs[0]) | |
| def get_commodities() -> list[dict]: | |
| """Commodity prices for the ticker, via API then engine fallback.""" | |
| try: | |
| resp = requests.get(f"{API_URL}/commodities", timeout=20) | |
| resp.raise_for_status() | |
| return resp.json().get("items", []) | |
| except Exception: | |
| pass | |
| try: | |
| return get_local_engine().get_commodities() | |
| except Exception: | |
| return [] | |
| def render_commodities_ticker() -> None: | |
| items = get_commodities() | |
| if not items: | |
| return | |
| chips = [] | |
| for c in items: | |
| if c.get("price") is None: | |
| chips.append(f"<span class='cmd'>{c['name']}: <i>n/a</i></span>") | |
| continue | |
| chg = c.get("change_pct") | |
| if chg is None: | |
| delta = "" | |
| else: | |
| color = "#7CFC9A" if chg >= 0 else "#FF8A8A" | |
| arrow = "β²" if chg >= 0 else "βΌ" | |
| delta = f" <span style='color:{color}'>{arrow}{abs(chg)}%</span>" | |
| chips.append( | |
| f"<span class='cmd'>{c['name']}: {c.get('currency','')}{c['price']}" | |
| f"/{c['unit']}{delta}</span>") | |
| strip = "γγ".join(chips) | |
| st.markdown( | |
| f"""<div class="cmd-wrap"><div class="cmd-ticker">πΉ Commodities:γ{strip}</div></div> | |
| <style> | |
| .cmd-wrap{{width:100%;overflow:hidden;box-sizing:border-box;background:#2a210e; | |
| border-radius:8px;margin:4px 0;padding:6px 0;}} | |
| .cmd-ticker{{display:inline-block;white-space:nowrap;padding-left:100%; | |
| animation:cmdticker 110s linear infinite;}} | |
| .cmd-ticker:hover{{animation-play-state:paused;}} | |
| .cmd-ticker .cmd{{color:#ffe9b8;font-size:0.9rem;margin:0 0.4rem;}} | |
| @keyframes cmdticker{{0%{{transform:translateX(0)}}100%{{transform:translateX(-100%)}}}} | |
| </style>""", | |
| unsafe_allow_html=True, | |
| ) | |
| st.caption("Gold/Silver/Oil/Coffee: Yahoo Finance. Arecanut/Coconut: Agmarknet " | |
| "(needs a data.gov.in key).") | |
| def render_top_bar() -> None: | |
| from agrosense.calendars import datetime_header | |
| from agrosense.news import LOCALES, TOPICS | |
| h = datetime_header() | |
| st.markdown( | |
| f"""<div style="background:#0e3b2e;color:#eaffea;padding:8px 14px;border-radius:8px; | |
| display:flex;justify-content:space-between;flex-wrap:wrap;font-size:0.9rem;"> | |
| <span>π <b>{h['gregorian']}</b></span> | |
| <span>πͺ {h['indian_national']}{(' Β· π ' + h['lunar_day']) if h.get('lunar_day') else ''}{(' Β· β ' + h['panchang']['nakshatra']) if h.get('panchang') else ''}</span> | |
| <span>π <b>{h['ist_time']} IST</b></span></div>""", | |
| unsafe_allow_html=True, | |
| ) | |
| c1, c2 = st.columns(2) | |
| region = c1.selectbox("News region", options=list(LOCALES.keys()), | |
| format_func=lambda k: LOCALES[k][3], index=0, key="news_region") | |
| topic = c2.selectbox("News topic", options=list(TOPICS.keys()), index=0, key="news_topic") | |
| items = get_news_items(region, TOPICS[topic]) | |
| if items: | |
| ticker = "γβ’γ".join( | |
| f'<a href="{i["link"]}" target="_blank">{i["title"]}</a>' for i in items) | |
| st.markdown( | |
| f"""<div class="agro-ticker-wrap"><div class="agro-ticker"> | |
| π° Google News:γ{ticker}</div></div> | |
| <style> | |
| .agro-ticker-wrap{{width:100%;overflow:hidden;box-sizing:border-box; | |
| background:#142b22;border-radius:8px;margin:6px 0 4px 0;padding:6px 0;}} | |
| .agro-ticker{{display:inline-block;white-space:nowrap;padding-left:100%; | |
| animation:agroticker 160s linear infinite;}} | |
| .agro-ticker:hover{{animation-play-state:paused;}} | |
| .agro-ticker a{{color:#ffd;text-decoration:none;font-size:0.88rem;}} | |
| .agro-ticker a:hover{{text-decoration:underline;}} | |
| @keyframes agroticker{{0%{{transform:translateX(0)}}100%{{transform:translateX(-100%)}}}} | |
| </style>""", | |
| unsafe_allow_html=True, | |
| ) | |
| # Commodity prices ticker (slow scroll, directly below the news scroll). | |
| render_commodities_ticker() | |
| st.caption("Time updates on refresh. Headlines: Google News top stories.") | |
| render_top_bar() | |
| st.title("π± AgroSense") | |
| st.caption("RAG-based agriculture farming advisor β grounded, cited answers (offline POC)") | |
| with st.sidebar: | |
| st.subheader("About") | |
| st.write( | |
| "Answers are composed **only** from the AgroSense knowledge base and shown " | |
| "with source citations. No external LLM is required in this POC." | |
| ) | |
| st.divider() | |
| st.subheader("π Location (optional)") | |
| location = st.text_input( | |
| "Place name", placeholder="e.g. Belagavi", key="location", | |
| help="Drives local weather (top bar), satellite, advisories, environment, planets.", | |
| ).strip() or None | |
| st.caption("Leave blank to skip weather. Requires internet.") | |
| show_satellite = st.checkbox( | |
| "π°οΈ Show satellite monitoring", value=False, | |
| help="MODIS true-color + NDVI imagery (NASA GIBS) and agroclimate (NASA POWER).", | |
| disabled=location is None, | |
| ) | |
| show_advisories = st.checkbox( | |
| "π§ Show decision advisories", value=False, | |
| help="Fuse weather + satellite + NDVI into prioritized, actionable advisories.", | |
| disabled=location is None, | |
| ) | |
| adv_crop = st.text_input("Crop (for advisories)", value="", | |
| placeholder="e.g. Tomato", disabled=location is None).strip() or None | |
| adv_stage = st.selectbox( | |
| "Growth stage", options=["(any)", "seedling", "vegetative", "flowering", "maturity"], | |
| index=0, disabled=location is None, | |
| help="Crop + stage tune the advisory thresholds and urgencies.") | |
| adv_stage = None if adv_stage == "(any)" else adv_stage | |
| show_environment = st.checkbox( | |
| "π Show location environment", value=False, | |
| help="Altitude, population, humidity, sunlight, wind, air quality, pollen, groundwater.", | |
| disabled=location is None, | |
| ) | |
| show_planetary = st.checkbox( | |
| "πͺ Show planetary positions", value=False, | |
| help="Sun, Moon (phase) and planets β altitude/azimuth for the current time.", | |
| disabled=location is None, | |
| ) | |
| show_hazards = st.checkbox( | |
| "β οΈ Show hazards & fires", value=False, | |
| help="NASA EONET natural-hazard events + NASA FIRMS active fires near the location.", | |
| disabled=location is None, | |
| ) | |
| st.divider() | |
| st.subheader("πΏ Plant image diagnosis") | |
| plant_image = st.file_uploader("Upload a leaf/plant photo", | |
| type=["jpg", "jpeg", "png"], key="plant_img") | |
| st.divider() | |
| st.subheader("π©Ί Plant telemedicine") | |
| tele_crop = st.text_input("Crop", key="tele_crop", placeholder="e.g. Tomato") | |
| tele_symptoms = st.text_area("Describe symptoms", key="tele_symptoms", | |
| placeholder="e.g. yellow spots spreading on lower leaves") | |
| run_consult = st.button("π©Ί Get consultation", width="stretch") | |
| st.divider() | |
| st.subheader("π¨ββοΈ Live agri-doctor") | |
| farmer_name = st.text_input("Your name", value="Farmer", key="farmer_name") | |
| consult_channel = st.selectbox("Channel", ["video", "chat", "phone"], key="consult_channel") | |
| consult_lang = st.selectbox( | |
| "Preferred language", | |
| ["(any)", "English", "Hindi", "Kannada", "Telugu", "Tamil", "Gujarati", "Urdu"], | |
| key="consult_lang") | |
| request_consult_btn = st.button("π¨ββοΈ Request live consultation", width="stretch") | |
| st.divider() | |
| st.subheader("π Language") | |
| from agrosense.translation import SUPPORTED_LANGUAGES | |
| lang_code = st.selectbox( | |
| "Answer language", options=list(SUPPORTED_LANGUAGES.keys()), | |
| format_func=lambda c: SUPPORTED_LANGUAGES[c], index=0, | |
| help="Non-English requires a translation backend (argostranslate / deep-translator).", | |
| ) | |
| st.divider() | |
| st.subheader("π° Market prices") | |
| include_prices = st.checkbox( | |
| "Attach mandi prices to answers", value=False, | |
| help="Live Agmarknet prices for the crop in your question. Needs a free data.gov.in key.", | |
| ) | |
| st.divider() | |
| st.subheader("Try a sample") | |
| for q in SAMPLE_QUERIES: | |
| if st.button(q, width="stretch"): | |
| st.session_state["pending"] = q | |
| # Local weather β now shown with the location panels (not the top bar). | |
| if location: | |
| with st.expander(f"π€οΈ Local weather β {location}", expanded=True): | |
| w = get_current_weather(location) | |
| if w: | |
| render_weather_strip(w) | |
| else: | |
| st.warning("Local weather unavailable (offline or location not found).") | |
| if show_environment and location: | |
| with st.expander(f"π Location & environment β {location}", expanded=True): | |
| with st.spinner("Fetching environment profile..."): | |
| envp = get_environment(location) | |
| if envp: | |
| render_environment(envp) | |
| else: | |
| st.warning("Environment data unavailable (offline or location not found).") | |
| if show_advisories and location: | |
| label = location + (f" Β· {adv_crop}" if adv_crop else "") + (f" Β· {adv_stage}" if adv_stage else "") | |
| with st.expander(f"π§ Decision advisories β {label}", expanded=True): | |
| with st.spinner("Fusing weather + satellite signals..."): | |
| adv = get_advisories(location, adv_crop, adv_stage) | |
| if adv: | |
| render_advisories(adv) | |
| else: | |
| st.warning("Advisories unavailable (offline or location not found).") | |
| if run_consult: | |
| img_bytes = plant_image.getvalue() if plant_image is not None else None | |
| with st.spinner("Preparing your plant consultation..."): | |
| st.session_state["consultation"] = get_local_engine().plant_consultation( | |
| crop=tele_crop or None, symptoms=tele_symptoms or None, | |
| image_bytes=img_bytes, location=location) | |
| if st.session_state.get("consultation"): | |
| with st.expander("π©Ί Plant telemedicine consultation", expanded=True): | |
| render_consultation(st.session_state["consultation"]) | |
| if request_consult_btn: | |
| with st.spinner("Connecting you to an agri-doctor..."): | |
| st.session_state["consult_session"] = get_local_engine().request_live_consult( | |
| farmer_name=farmer_name or "Farmer", crop=tele_crop or None, | |
| symptoms=tele_symptoms or None, channel=consult_channel, | |
| language=None if consult_lang == "(any)" else consult_lang) | |
| if st.session_state.get("consult_session"): | |
| cs = st.session_state["consult_session"] | |
| with st.expander(f"π¨ββοΈ Live agri-doctor consultation β {cs['id']}", expanded=True): | |
| render_consult_session(cs) | |
| msg = st.text_input("Message your agri-doctor", key="consult_msg_input") | |
| if st.button("Send message", key="consult_send") and msg: | |
| st.session_state["consult_session"] = get_local_engine().add_consult_message( | |
| cs["id"], "farmer", msg) | |
| st.rerun() | |
| if plant_image is not None: | |
| with st.expander("πΏ Plant image diagnosis", expanded=True): | |
| img_bytes = plant_image.getvalue() | |
| col_img, col_res = st.columns([1, 2]) | |
| with col_img: | |
| st.image(img_bytes, caption="Uploaded image", width="stretch") | |
| with col_res: | |
| with st.spinner("Analyzing image..."): | |
| result = classify_image(img_bytes) | |
| dis = result.get("disease", {}) | |
| pl = result.get("plant", {}) | |
| pest = result.get("pest", {}) | |
| st.markdown(f"**Disease/health:** {dis.get('label', 'β')} " | |
| f"({round(dis.get('confidence', 0) * 100)}%) \n" | |
| f"<small>{dis.get('note', '')}</small>", unsafe_allow_html=True) | |
| st.markdown(f"**Plant ID:** {pl.get('label', 'β')} " | |
| f"({round(pl.get('confidence', 0) * 100)}%) \n" | |
| f"<small>{pl.get('note', '')}</small>", unsafe_allow_html=True) | |
| st.markdown(f"**Pest ID:** {pest.get('label', 'β')} " | |
| f"({round(pest.get('confidence', 0) * 100)}%) \n" | |
| f"<small>{pest.get('note', '')}</small>", unsafe_allow_html=True) | |
| st.caption(f"Backends β disease: {dis.get('backend')}, plant: {pl.get('backend')}, " | |
| f"pest: {pest.get('backend')}") | |
| if show_hazards and location: | |
| with st.expander(f"β οΈ Hazards & fires β {location}", expanded=True): | |
| with st.spinner("Checking NASA EONET / FIRMS..."): | |
| hz = get_hazards(location) | |
| if hz: | |
| render_hazards(hz) | |
| else: | |
| st.warning("Hazard data unavailable (offline or location not found).") | |
| if show_planetary and location: | |
| with st.expander(f"πͺ Planetary positions β {location}", expanded=True): | |
| with st.spinner("Computing sky positions..."): | |
| pl = get_planetary(location) | |
| if pl: | |
| render_planetary(pl) | |
| else: | |
| st.warning("Planetary data unavailable (location not found).") | |
| if show_satellite and location: | |
| with st.expander(f"π°οΈ Satellite monitoring β {location}", expanded=True): | |
| with st.spinner("Fetching satellite imagery & agroclimate..."): | |
| report = get_satellite(location) | |
| if report: | |
| render_satellite(report) | |
| else: | |
| st.warning("Satellite data unavailable (offline or location not found).") | |
| if "history" not in st.session_state: | |
| st.session_state["history"] = [] | |
| for turn in st.session_state["history"]: | |
| with st.chat_message(turn["role"]): | |
| st.markdown(turn["content"]) | |
| prompt = st.chat_input("Ask about crops, fertilizer, disease, or pests...") | |
| if "pending" in st.session_state and not prompt: | |
| prompt = st.session_state.pop("pending") | |
| if prompt: | |
| st.session_state["history"].append({"role": "user", "content": prompt}) | |
| with st.chat_message("user"): | |
| st.markdown(prompt) | |
| with st.chat_message("assistant"): | |
| with st.spinner("Retrieving and composing a grounded answer..."): | |
| result, mode = answer_query(prompt, location, lang_code, include_prices) | |
| st.markdown(result["answer"]) | |
| if lang_code != "en" and not result.get("translation_backend"): | |
| st.warning("Translation backend unavailable β showing the English answer. " | |
| "Install argostranslate or deep-translator to enable.") | |
| prices = result.get("prices") | |
| if prices: | |
| with st.expander(f"π° Market prices β {prices.get('commodity')}", expanded=True): | |
| render_prices(prices) | |
| elif include_prices: | |
| st.caption("Market prices unavailable β set a data.gov.in key " | |
| "(AGROSENSE_DATAGOV_API_KEY) to enable.") | |
| meta = result.get("backends", {}) | |
| st.caption( | |
| f"via {mode} Β· {result.get('latency_ms', 0)} ms Β· " | |
| f"lang: {result.get('language')} Β· " | |
| f"embeddings: {meta.get('embedding')} Β· vector: {meta.get('vector_store')}" | |
| ) | |
| st.session_state["history"].append({"role": "assistant", "content": result["answer"]}) | |