Spaces:
Sleeping
Sleeping
| """AgroSense FastAPI backend. | |
| Run: uvicorn api.main:app --reload | |
| Docs: http://127.0.0.1:8000/docs | |
| """ | |
| from __future__ import annotations | |
| from functools import lru_cache | |
| from pathlib import Path | |
| from fastapi import Depends, FastAPI, File, Form, Header, HTTPException, UploadFile | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from fastapi.responses import FileResponse, RedirectResponse, Response | |
| from fastapi.staticfiles import StaticFiles | |
| from pydantic import BaseModel, Field | |
| from agrosense import RAGEngine, __version__ | |
| app = FastAPI( | |
| title="AgroSense API", | |
| version=__version__, | |
| description="RAG-based agriculture farming advisor (offline POC).", | |
| ) | |
| # Allow the Streamlit UI (and any local frontend) to call the API. | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| def get_engine() -> RAGEngine: | |
| """Build the engine once and reuse it across requests.""" | |
| return RAGEngine() | |
| def require_admin(x_admin_token: str | None = Header(default=None)) -> None: | |
| """Gate admin endpoints on the AGROSENSE_ADMIN_TOKEN header.""" | |
| from agrosense import config as _cfg | |
| if not _cfg.ADMIN_TOKEN or x_admin_token != _cfg.ADMIN_TOKEN: | |
| raise HTTPException(status_code=401, detail="Invalid or missing admin token.") | |
| class QueryRequest(BaseModel): | |
| query: str = Field(..., min_length=1, examples=[ | |
| "My soil is sandy, rainfall 900 mm, I plan to grow maize. " | |
| "What fertilizer and pest management should I follow?" | |
| ]) | |
| top_k: int | None = Field(default=None, ge=1, le=20) | |
| rerank_k: int | None = Field(default=None, ge=1, le=20) | |
| # Optional: attach a live local weather forecast + advisory to the answer. | |
| location: str | None = Field(default=None, examples=["Belagavi"]) | |
| latitude: float | None = Field(default=None, ge=-90, le=90) | |
| longitude: float | None = Field(default=None, ge=-180, le=180) | |
| # Optional: attach satellite monitoring (imagery + agroclimate) for the location. | |
| include_satellite: bool = Field(default=False) | |
| # Optional: answer language (ISO 639-1, e.g. "hi"). "en" = no translation. | |
| language: str = Field(default="en") | |
| # Optional: attach live mandi prices for a commodity (defaults to top retrieved crop). | |
| include_prices: bool = Field(default=False) | |
| commodity: str | None = Field(default=None) | |
| state: str | None = Field(default=None) | |
| # Optional: attach fused decision advisories (weather + satellite + NDVI). | |
| include_advisories: bool = Field(default=False) | |
| # Optional crop growth stage for crop-aware advisories. | |
| stage: str | None = Field(default=None, examples=["flowering"]) | |
| def _warm_up() -> None: | |
| get_engine() # load KB + build index at boot, not on first request | |
| def health() -> dict: | |
| engine = get_engine() | |
| return { | |
| "status": "ok", | |
| "version": __version__, | |
| "documents": engine.num_documents, | |
| } | |
| # --- Knowledge-base admin (token-gated) --- | |
| def admin_list_kb() -> dict: | |
| from agrosense.kb_admin import EDITABLE_FIELDS | |
| entries = get_engine().list_kb_entries() | |
| return {"count": len(entries), "fields": EDITABLE_FIELDS, "entries": entries} | |
| def admin_add_kb(entry: dict) -> dict: | |
| try: | |
| new = get_engine().add_kb_entry(entry) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {"ok": True, "entry": new, "count": get_engine().num_documents} | |
| def admin_update_kb(entry_id: str, entry: dict) -> dict: | |
| try: | |
| updated = get_engine().update_kb_entry(entry_id, entry) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail=f"Entry '{entry_id}' not found.") | |
| return {"ok": True, "entry": updated, "count": get_engine().num_documents} | |
| def admin_delete_kb(entry_id: str) -> dict: | |
| if not get_engine().delete_kb_entry(entry_id): | |
| raise HTTPException(status_code=404, detail=f"Entry '{entry_id}' not found.") | |
| return {"ok": True, "count": get_engine().num_documents} | |
| # --- Document downloads (user manual, technical guide, SOP) --- | |
| _DOCS_DIR = Path(__file__).resolve().parent.parent / "docs" | |
| _DOC_FILES = { | |
| "user-manual": "AgroSense_User_Manual", | |
| "technical-guide": "AgroSense_Technical_Guide", | |
| "sop": "AgroSense_SOP", | |
| "gtm-plan": "AgroSense_GTM_Monetization_Plan", | |
| "instagram-campaign": "AgroSense_Instagram_Campaign", | |
| "facebook-campaign": "AgroSense_Facebook_Campaign", | |
| "linkedin-campaign": "AgroSense_LinkedIn_Campaign", | |
| "content-calendar": "AgroSense_30Day_Calendar", | |
| } | |
| _DOC_MEDIA = { | |
| "pdf": "application/pdf", | |
| "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", | |
| } | |
| def download_doc(doc: str, fmt: str) -> FileResponse: | |
| """Download a document (user-manual | technical-guide | sop) as pdf or docx.""" | |
| if doc not in _DOC_FILES or fmt not in _DOC_MEDIA: | |
| raise HTTPException(status_code=404, | |
| detail="Unknown document or format. Use " | |
| "/downloads/{user-manual|technical-guide|sop}.{pdf|docx}.") | |
| path = _DOCS_DIR / f"{_DOC_FILES[doc]}.{fmt}" | |
| if not path.exists(): | |
| raise HTTPException(status_code=404, detail="Document not generated yet. " | |
| "Run scripts/build_manual.py and scripts/build_tech_docs.py.") | |
| return FileResponse(path, media_type=_DOC_MEDIA[fmt], filename=path.name) | |
| # Serve the single-page web app (web/) at /ui and redirect root to it. | |
| _WEB_DIR = Path(__file__).resolve().parent.parent / "web" | |
| def _root() -> RedirectResponse: | |
| return RedirectResponse(url="/ui/") | |
| def _landing() -> FileResponse: | |
| """The public marketing landing page (campaign destination).""" | |
| page = _WEB_DIR / "landing" / "index.html" | |
| if not page.exists(): | |
| raise HTTPException(status_code=404, detail="Landing page not found.") | |
| return FileResponse(page, media_type="text/html") | |
| if _WEB_DIR.is_dir(): | |
| app.mount("/ui", StaticFiles(directory=str(_WEB_DIR), html=True), name="ui") | |
| def query(req: QueryRequest) -> dict: | |
| engine = get_engine() | |
| answer = engine.answer( | |
| req.query, | |
| top_k=req.top_k, | |
| rerank_k=req.rerank_k, | |
| location=req.location, | |
| latitude=req.latitude, | |
| longitude=req.longitude, | |
| include_satellite=req.include_satellite, | |
| language=req.language, | |
| include_prices=req.include_prices, | |
| commodity=req.commodity, | |
| state=req.state, | |
| include_advisories=req.include_advisories, | |
| stage=req.stage, | |
| ) | |
| return answer.to_dict() | |
| def weather(location: str | None = None, | |
| latitude: float | None = None, | |
| longitude: float | None = None) -> dict: | |
| """Live local forecast + farming advisory (Open-Meteo, no API key). | |
| Returns {"available": false} when offline or the location can't be resolved, | |
| so callers can degrade gracefully. | |
| """ | |
| engine = get_engine() | |
| forecast = engine.get_weather(location=location, latitude=latitude, longitude=longitude) | |
| if forecast is None: | |
| return {"available": False, | |
| "detail": "Forecast unavailable (offline or unknown location)."} | |
| return {"available": True, **forecast.to_dict()} | |
| def satellite(location: str | None = None, | |
| latitude: float | None = None, | |
| longitude: float | None = None, | |
| days: int = 30) -> dict: | |
| """Satellite monitoring: MODIS true-color + NDVI imagery (NASA GIBS) and | |
| satellite-derived agroclimate (NASA POWER). No API key required. | |
| Returns {"available": false} when offline or the location can't be resolved. | |
| """ | |
| engine = get_engine() | |
| report = engine.get_satellite(location=location, latitude=latitude, | |
| longitude=longitude, days=days) | |
| if report is None: | |
| return {"available": False, | |
| "detail": "Satellite data unavailable (offline or unknown location)."} | |
| return {"available": True, **report.to_dict()} | |
| def news(query: str | None = None, region: str | None = None, | |
| topic: str | None = None, limit: int = 15) -> dict: | |
| """Latest headlines via Google News RSS (keyless). `region` selects the locale | |
| (IN/US/GB/AU/CA/SG, default IN); `topic` is a label from available_topics (mapped | |
| server-side); `query` is a raw override. Empty list on failure.""" | |
| from agrosense.news import LOCALES, TOPICS | |
| engine = get_engine() | |
| if query is None and topic is not None and topic in TOPICS: | |
| query = TOPICS[topic] | |
| items = engine.get_news(query=query, region=region, limit=limit) | |
| return {"count": len(items), "items": [i.to_dict() for i in items], | |
| "region": (region or "IN").upper(), | |
| "available_regions": {k: v[3] for k, v in LOCALES.items()}, | |
| "available_topics": list(TOPICS.keys())} | |
| def hazards(location: str | None = None, latitude: float | None = None, | |
| longitude: float | None = None, radius_km: float | None = None) -> dict: | |
| """Natural-hazard events (NASA EONET, keyless) + active fires (NASA FIRMS, needs | |
| a free map-key) near a location. Returns {"available": false} if unresolved.""" | |
| engine = get_engine() | |
| report = engine.get_hazards(location=location, latitude=latitude, | |
| longitude=longitude, radius_km=radius_km) | |
| if report is None: | |
| return {"available": False, "detail": "Provide a location or lat/lon."} | |
| return {"available": True, **report} | |
| async def vision_classify(file: UploadFile = File(...), task: str = "all") -> dict: | |
| """Classify an uploaded leaf/plant image: task = disease | plant | pest | all. | |
| Uses the trained model if configured, else an honest Pillow heuristic.""" | |
| engine = get_engine() | |
| data = await file.read() | |
| out: dict = {} | |
| if task in ("disease", "all", "both"): | |
| out["disease"] = engine.predict_plant_disease(data) | |
| if task in ("plant", "all", "both"): | |
| out["plant"] = engine.predict_plant_species(data) | |
| if task in ("pest", "all"): | |
| out["pest"] = engine.predict_pest(data) | |
| return out | |
| async def telemedicine(crop: str | None = Form(None), symptoms: str | None = Form(None), | |
| location: str | None = Form(None), | |
| file: UploadFile | None = File(None)) -> dict: | |
| """Plant telemedicine consult: diagnosis + health + grounded prescription. | |
| Accepts crop/symptoms/location form fields and an optional leaf/plant photo.""" | |
| engine = get_engine() | |
| image_bytes = await file.read() if file is not None else None | |
| return engine.plant_consultation(crop=crop, symptoms=symptoms, | |
| image_bytes=image_bytes, location=location) | |
| def notifications() -> dict: | |
| """Audit log of expert notifications sent (in-app log channel).""" | |
| return {"sent": get_engine().get_notifications()} | |
| def experts() -> dict: | |
| """Directory of VERIFIED plant doctors available for live consultation.""" | |
| return {"experts": get_engine().list_experts()} | |
| def clubs(type: str | None = None, key: str | None = None, search: str | None = None) -> dict: | |
| """List farmers' digital clubs, filtered by type (location|commodity), key or search.""" | |
| return {"clubs": get_engine().list_clubs(ctype=type, key=key, search=search)} | |
| async def club_create(name: str = Form(""), type: str = Form("location"), | |
| key: str = Form(""), description: str = Form(""), | |
| creator: str = Form("")) -> dict: | |
| """Create a club (type 'location' or 'commodity'). Public.""" | |
| try: | |
| club = get_engine().create_club(name, type, key, description, creator) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {"ok": True, "club": club} | |
| def club_get(club_id: str) -> dict: | |
| """A club's detail: members, discussion feed and the video meeting room URL.""" | |
| club = get_engine().get_club(club_id) | |
| if club is None: | |
| raise HTTPException(status_code=404, detail="Club not found.") | |
| return club | |
| async def club_join(club_id: str, member: str = Form("")) -> dict: | |
| try: | |
| return get_engine().join_club(club_id, member) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Club not found.") | |
| async def club_post(club_id: str, author: str = Form("Member"), text: str = Form(""), | |
| link: str = Form("")) -> dict: | |
| if not text.strip(): | |
| raise HTTPException(status_code=400, detail="Message text is required.") | |
| try: | |
| return get_engine().post_to_club(club_id, author, text, link) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Club not found.") | |
| # --- government subsidies & schemes --- | |
| def subsidies(level: str | None = None, state: str | None = None, | |
| category: str | None = None, search: str | None = None) -> dict: | |
| """List agricultural schemes/subsidies, filtered by level (central|state), | |
| state, category or search. Returns summaries; GET /subsidies/{id} for full detail.""" | |
| from agrosense.subsidies import DISCLAIMER | |
| items = get_engine().list_subsidies(level, state, category, search) | |
| return {"count": len(items), "schemes": items, "disclaimer": DISCLAIMER} | |
| def subsidy_updates(limit: int = 12) -> dict: | |
| """Recent scheme announcements/updates across all schemes, newest first.""" | |
| return {"updates": get_engine().subsidy_updates(limit)} | |
| def subsidy_detail(scheme_id: str) -> dict: | |
| """Full publication for one scheme: benefits, eligibility, application process, | |
| documents, official portal, helpline and announcements.""" | |
| from agrosense.subsidies import DISCLAIMER | |
| scheme = get_engine().get_subsidy(scheme_id) | |
| if scheme is None: | |
| raise HTTPException(status_code=404, detail="Scheme not found.") | |
| scheme["disclaimer"] = DISCLAIMER | |
| return scheme | |
| async def subsidy_add_update(scheme_id: str, text: str = Form(""), | |
| date: str = Form("")) -> dict: | |
| """Admin: append an announcement/update to a scheme.""" | |
| if not text.strip(): | |
| raise HTTPException(status_code=400, detail="Update text is required.") | |
| try: | |
| scheme = get_engine().add_subsidy_update(scheme_id, text, date or None) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Scheme not found.") | |
| return {"ok": True, "scheme": scheme} | |
| # --- agricultural finance (bank assistance / loans + apply) --- | |
| def finance(category: str | None = None, provider: str | None = None, | |
| search: str | None = None) -> dict: | |
| """List agri-credit / bank-assistance products, filtered by category, provider or | |
| search. Also returns the available category list for filters.""" | |
| from agrosense.finance import DISCLAIMER | |
| eng = get_engine() | |
| items = eng.list_finance(category, provider, search) | |
| return {"count": len(items), "products": items, | |
| "categories": eng.finance_categories(), "disclaimer": DISCLAIMER} | |
| def finance_detail(product_id: str) -> dict: | |
| """Full detail for one finance product: interest, amount, tenure, eligibility, | |
| application process, documents, portal and helpline.""" | |
| from agrosense.finance import DISCLAIMER | |
| product = get_engine().get_finance(product_id) | |
| if product is None: | |
| raise HTTPException(status_code=404, detail="Finance product not found.") | |
| product["disclaimer"] = DISCLAIMER | |
| return product | |
| async def finance_apply(product_id: str, name: str = Form(""), contact: str = Form(""), | |
| amount: str = Form(""), location: str = Form(""), | |
| message: str = Form("")) -> dict: | |
| """Lodge a loan enquiry/application against a product (name + contact required).""" | |
| try: | |
| app = get_engine().apply_finance(product_id, name, contact, amount or None, | |
| location, message) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Finance product not found.") | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {"ok": True, "application": app, | |
| "message": "Enquiry lodged. A bank/officer can follow up; also apply on the " | |
| "official portal to proceed."} | |
| def admin_finance_applications() -> dict: | |
| apps = get_engine().list_finance_applications() | |
| return {"count": len(apps), "applications": apps} | |
| # --- land records (state-wise RoR portals + downloadable guide) --- | |
| def land_records(location: str | None = None, state: str | None = None) -> dict: | |
| """Land-record system for a place/state: official portal, local record name | |
| (RTC/Pahani, 7/12, Khatauni, ...), what to search by, steps, and what it contains. | |
| `location` may be a city — it is resolved to its state.""" | |
| return get_engine().land_record_info(location=location, state=state) | |
| def land_record_guide(fmt: str, location: str | None = None, | |
| state: str | None = None) -> Response: | |
| """Download a printable land-record search guide (pdf|docx) for the resolved state.""" | |
| if fmt not in _DOC_MEDIA: | |
| raise HTTPException(status_code=404, detail="Use guide.pdf or guide.docx.") | |
| try: | |
| filename, data = get_engine().land_record_guide(location=location, state=state, fmt=fmt) | |
| except ImportError: # python-docx / fpdf2 not installed | |
| raise HTTPException(status_code=503, detail="Document generation needs " | |
| "'python-docx' (docx) or 'fpdf2' (pdf) installed.") | |
| return Response(content=data, media_type=_DOC_MEDIA[fmt], | |
| headers={"Content-Disposition": f'attachment; filename="{filename}"'}) | |
| # --- farmer trading platform (peer-to-peer produce marketplace, in Market tab) --- | |
| def market_listings(type: str | None = None, commodity: str | None = None, | |
| state: str | None = None, search: str | None = None, | |
| include_closed: bool = False) -> dict: | |
| """Browse produce listings — filter by type (sell|buy), commodity, state or search.""" | |
| items = get_engine().list_listings(type, commodity, state, search, include_closed) | |
| return {"count": len(items), "listings": items, | |
| "disclaimer": "POC marketplace — no payment/escrow/KYC. Verify the other " | |
| "party and agree terms independently before transacting."} | |
| async def market_create(type: str = Form("sell"), commodity: str = Form(""), | |
| quantity: str = Form(""), unit: str = Form("kg"), | |
| price: str = Form(""), currency: str = Form("INR"), | |
| grade: str = Form(""), location: str = Form(""), | |
| state: str = Form(""), seller: str = Form(""), | |
| contact: str = Form(""), harvest_date: str = Form(""), | |
| description: str = Form("")) -> dict: | |
| """Post a sell offer or buy requirement. Public.""" | |
| try: | |
| listing = get_engine().create_listing({ | |
| "type": type, "commodity": commodity, "quantity": quantity, "unit": unit, | |
| "price": price, "currency": currency, "grade": grade, "location": location, | |
| "state": state, "seller": seller, "contact": contact, | |
| "harvest_date": harvest_date, "description": description}) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {"ok": True, "listing": listing} | |
| def market_listing_get(listing_id: str) -> dict: | |
| """A listing's detail: produce, price, location, inquiries and a deal video room.""" | |
| listing = get_engine().get_listing(listing_id) | |
| if listing is None: | |
| raise HTTPException(status_code=404, detail="Listing not found.") | |
| return listing | |
| async def market_inquire(listing_id: str, name: str = Form("Interested party"), | |
| contact: str = Form(""), message: str = Form(""), | |
| quantity: str = Form("")) -> dict: | |
| """Express interest in a listing (buyer/seller leaves contact + message).""" | |
| try: | |
| return get_engine().inquire_listing(listing_id, name, contact, message, | |
| quantity or None) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Listing not found.") | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| async def market_close(listing_id: str, status: str = Form("closed")) -> dict: | |
| """Mark a listing closed/sold (or reopen). Public in this POC.""" | |
| try: | |
| return {"ok": True, "listing": get_engine().close_listing(listing_id, status)} | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Listing not found.") | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| async def doctor_apply(name: str = Form(""), specialization: str = Form(""), | |
| region: str = Form(""), contact: str = Form(""), | |
| languages: str = Form(""), credentials: str = Form(""), | |
| registration_no: str = Form("")) -> dict: | |
| """Public onboarding: apply to become a plant doctor (status 'pending' until an | |
| admin verifies). Requires an ICAR/registration number.""" | |
| try: | |
| doc = get_engine().onboard_doctor({ | |
| "name": name, "specialization": specialization, "region": region, | |
| "contact": contact, "languages": languages, "credentials": credentials, | |
| "registration_no": registration_no}) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| return {"ok": True, "doctor": doc, | |
| "message": "Application received — pending admin verification."} | |
| def doctor_profile(doctor_id: str) -> dict: | |
| """Public profile page of a verified plant doctor (ratings + registration).""" | |
| prof = get_engine().get_doctor_profile(doctor_id) | |
| if prof is None: | |
| raise HTTPException(status_code=404, detail="Doctor not found or not verified.") | |
| return prof | |
| async def doctor_rate(doctor_id: str, stars: int = Form(...), | |
| comment: str = Form("")) -> dict: | |
| """Farmer rates a verified doctor (1-5 stars + optional comment).""" | |
| try: | |
| prof = get_engine().rate_doctor(doctor_id, stars, comment) | |
| except ValueError as exc: | |
| raise HTTPException(status_code=400, detail=str(exc)) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail="Doctor not found or not verified.") | |
| return {"ok": True, "profile": prof} | |
| def admin_list_doctors(status: str | None = None) -> dict: | |
| docs = get_engine().list_doctors(status=status) | |
| return {"count": len(docs), "doctors": docs} | |
| def admin_verify_doctor(doctor_id: str, approve: bool = Form(True), | |
| note: str = Form("")) -> dict: | |
| try: | |
| doc = get_engine().verify_doctor(doctor_id, approve=approve, note=note) | |
| except KeyError: | |
| raise HTTPException(status_code=404, detail=f"Doctor '{doctor_id}' not found.") | |
| return {"ok": True, "doctor": doc} | |
| async def consult_request(farmer_name: str = Form("Farmer"), crop: str | None = Form(None), | |
| symptoms: str | None = Form(None), channel: str = Form("video"), | |
| specialization: str | None = Form(None), | |
| language: str | None = Form(None)) -> dict: | |
| """Create a live agri-doctor consultation: routes to an expert, attaches the AI | |
| consult as context, and returns a video room link + chat thread.""" | |
| return get_engine().request_live_consult( | |
| farmer_name=farmer_name, crop=crop, symptoms=symptoms, channel=channel, | |
| specialization=specialization, language=language) | |
| def consult_get(request_id: str) -> dict: | |
| rep = get_engine().get_consult(request_id) | |
| return rep or {"error": "not found", "id": request_id} | |
| async def consult_message(request_id: str, sender: str = Form("farmer"), | |
| text: str = Form(...)) -> dict: | |
| rep = get_engine().add_consult_message(request_id, sender, text) | |
| return rep or {"error": "not found", "id": request_id} | |
| def commodities() -> dict: | |
| """Commodity prices for the ticker: Gold/Silver/Crude Oil/Coffee (Yahoo Finance, | |
| keyless) and Arecanut/Coconut (Agmarknet, key-gated -> null without a key).""" | |
| engine = get_engine() | |
| return {"items": engine.get_commodities()} | |
| def traditional(activity: str = "general", location: str | None = None) -> dict: | |
| """Traditional Advisor: Panchang (astrological) suitability for a daily farm | |
| activity + traditional/desi practices for the area. Keyless.""" | |
| return get_engine().traditional_advice(activity=activity, location=location) | |
| def radio(country: str = "IN", state: str | None = None, tag: str | None = None, | |
| search: str | None = None, limit: int = 60) -> dict: | |
| """Internet radio stations (default: all India) via Radio Browser (keyless). | |
| Returns playable stream URLs. Empty list on failure.""" | |
| items = get_engine().get_radio_stations(country=country, state=state, tag=tag, | |
| search=search, limit=limit) | |
| return {"count": len(items), "stations": items} | |
| def datetime_header() -> dict: | |
| """Current date in Gregorian + Indian National (Saka) calendars, and IST time.""" | |
| from agrosense.calendars import datetime_header as _hdr | |
| return _hdr() | |
| def planetary(location: str | None = None, | |
| latitude: float | None = None, | |
| longitude: float | None = None) -> dict: | |
| """Planetary positions (Sun, Moon + phase, planets) for the current time at a | |
| place or lat/lon: altitude/azimuth, RA/Dec, what's above the horizon. Keyless, | |
| computed locally. Returns {"available": false} if the location can't resolve. | |
| """ | |
| engine = get_engine() | |
| report = engine.get_planetary(location=location, latitude=latitude, longitude=longitude) | |
| if report is None: | |
| return {"available": False, | |
| "detail": "Planetary data unavailable (unknown location)."} | |
| return {"available": True, **report.to_dict()} | |
| def environment(location: str | None = None, | |
| latitude: float | None = None, | |
| longitude: float | None = None) -> dict: | |
| """Location environment profile: lat/lon, altitude, population, humidity, | |
| sunlight, wind (speed + direction), air quality, pollen, groundwater proxy. | |
| Keyless (Open-Meteo). Returns {"available": false} when offline/unknown location. | |
| """ | |
| engine = get_engine() | |
| profile = engine.get_environment(location=location, latitude=latitude, | |
| longitude=longitude) | |
| if profile is None: | |
| return {"available": False, | |
| "detail": "Environment data unavailable (offline or unknown location)."} | |
| return {"available": True, **profile.to_dict()} | |
| def advisories(location: str | None = None, | |
| latitude: float | None = None, | |
| longitude: float | None = None, | |
| crop: str | None = None, | |
| stage: str | None = None) -> dict: | |
| """Decision-fusion advisories combining weather + satellite + NDVI for a place | |
| or lat/lon, tuned to the crop and growth stage (seedling/vegetative/flowering/ | |
| maturity). Returns {"available": false} when offline or location can't resolve. | |
| """ | |
| engine = get_engine() | |
| report = engine.get_fusion_advisories(location=location, latitude=latitude, | |
| longitude=longitude, crop=crop, stage=stage) | |
| if report is None: | |
| return {"available": False, | |
| "detail": "Advisories unavailable (offline or unknown location)."} | |
| return {"available": True, **report.to_dict()} | |
| def languages() -> dict: | |
| """Languages AgroSense can translate answers into.""" | |
| from agrosense.translation import SUPPORTED_LANGUAGES | |
| return {"languages": SUPPORTED_LANGUAGES} | |
| def prices(commodity: str | None = None, | |
| state: str | None = None, | |
| market: str | None = None) -> dict: | |
| """Live mandi prices (Agmarknet via data.gov.in). Requires a free api-key. | |
| Returns {"available": false} when no key is configured or the service is down. | |
| """ | |
| engine = get_engine() | |
| report = engine.get_market_prices(commodity=commodity, state=state, market=market) | |
| if report is None: | |
| return {"available": False, | |
| "detail": "Prices unavailable (no data.gov.in key configured or offline)."} | |
| return {"available": True, **report.to_dict()} | |