agrosense / ui /app.py
johnpitteera's picture
Upload folder using huggingface_hub
d27b187 verified
Raw
History Blame Contribute Delete
33 kB
"""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
@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"🌀️ <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;'>" + " &nbsp;Β·&nbsp; ".join(bits) + "</div>",
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"<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"]})