"""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( "", 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 @st.cache_resource(show_spinner="Loading AgroSense knowledge base...") 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() @st.cache_data(show_spinner=False, ttl=1800) 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 @st.cache_data(show_spinner=False, ttl=1800) 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', []))}") @st.cache_data(show_spinner=False, ttl=1800) 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)} @st.cache_data(show_spinner=False, ttl=1800) 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 @st.cache_data(show_spinner=False, ttl=900) 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)") @st.cache_data(show_spinner=False, ttl=900) 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 [] @st.cache_data(show_spinner=False, ttl=900) 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"🌀️ {w.get('location_name', '')}"] 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( "
" + "  Β·  ".join(bits) + "
", unsafe_allow_html=True, ) advs = w.get("advisories") or [] if advs: st.caption("⚠️ " + advs[0]) @st.cache_data(show_spinner=False, ttl=900) 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"{c['name']}: n/a") 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" {arrow}{abs(chg)}%" chips.append( f"{c['name']}: {c.get('currency','')}{c['price']}" f"/{c['unit']}{delta}") strip = "γ€€γ€€".join(chips) st.markdown( f"""
πŸ’Ή Commodities:γ€€{strip}
""", 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"""
πŸ“… {h['gregorian']} πŸͺ” {h['indian_national']}{(' Β· πŸŒ™ ' + h['lunar_day']) if h.get('lunar_day') else ''}{(' Β· ⭐ ' + h['panchang']['nakshatra']) if h.get('panchang') else ''} πŸ• {h['ist_time']} IST
""", 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'{i["title"]}' for i in items) st.markdown( f"""
πŸ“° Google News:γ€€{ticker}
""", 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"{dis.get('note', '')}", unsafe_allow_html=True) st.markdown(f"**Plant ID:** {pl.get('label', 'β€”')} " f"({round(pl.get('confidence', 0) * 100)}%) \n" f"{pl.get('note', '')}", unsafe_allow_html=True) st.markdown(f"**Pest ID:** {pest.get('label', 'β€”')} " f"({round(pest.get('confidence', 0) * 100)}%) \n" f"{pest.get('note', '')}", 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"]})