import os import sys import logging import uuid from fastapi import FastAPI, Body, UploadFile, File, Form, HTTPException from fastapi.middleware.cors import CORSMiddleware from typing import Optional import uvicorn import requests BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if BASE_DIR not in sys.path: sys.path.insert(0, BASE_DIR) from app.tasks.rag_updater import schedule_updates from app.utils import config from app.agents.crew_pipeline import run_pipeline from app.agents.climate_agent import advise_climate_resilient from app.utils.weather_api import fetch_forecast, alerts_for_q logging.basicConfig( format="%(asctime)s [%(levelname)s] %(message)s", level=logging.INFO ) app = FastAPI( title="Aglimate Farmer-First Climate-Resilient Advisory Backend", description=( "Backend for Aglimate, a Farmer-First Climate-Resilient Advisory Agent for smallholder farmers. " "Provides multilingual Qwen-based Q&A, RAG-powered updates, and a multimodal Qwen-VL endpoint for " "text + photo + GPS-aware climate-smart advice." ), version="2.0.0", ) app.add_middleware( CORSMiddleware, allow_origins=getattr(config, "ALLOWED_ORIGINS", ["*"]), allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) def _resolve_weather_q( q: Optional[str], state: Optional[str], lat: Optional[float], lon: Optional[float], ) -> str: if lat is not None and lon is not None: return f"{lat},{lon}" if q: return q if state: return f"{state}, Nigeria" raise HTTPException( status_code=400, detail="Provide q, state, or both lat and lon", ) @app.on_event("startup") def startup_event(): logging.info("Starting Aglimate AI backend...") schedule_updates() @app.get("/") def home(): return { "status": "Aglimate climate-resilient backend running", "version": "2.0.0", "vectorstore_path": config.VECTORSTORE_PATH } @app.get("/weather-test") def weather_test( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, aqi: str = "no", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) try: return fetch_forecast( q=q_val, days=getattr(config, "WEATHER_FORECAST_DAYS", 3), alerts=getattr(config, "WEATHER_ALERTS", "yes"), aqi=aqi, ) except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather-alerts") def weather_alerts( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) try: return alerts_for_q(q_val) except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.post("/ask") def ask_farmbot( query: str = Body(..., embed=True), session_id: str = Body(None, embed=True) ): """ Ask Aglimate AI a farming-related question. - Supports Hausa, Igbo, Yoruba, Swahili, Amharic, and English. - Automatically detects user language, translates if needed, and returns response in the same language. - Maintains separate conversation memory per session_id. """ if not session_id: session_id = str(uuid.uuid4()) # assign new session if missing logging.info(f"Received query: {query} [session_id={session_id}]") answer_data = run_pipeline(query, session_id=session_id) detected_lang = answer_data.get("detected_language", "Unknown") logging.info(f"Detected language: {detected_lang}") return { "query": query, "answer": answer_data.get("answer"), "session_id": answer_data.get("session_id"), "detected_language": detected_lang } @app.post("/advise") async def advise_climate_resilient_endpoint( query: str = Form(..., description="Farmer question or situation description"), session_id: Optional[str] = Form(None, description="Conversation session id"), latitude: Optional[float] = Form(None, description="GPS latitude (optional)"), longitude: Optional[float] = Form(None, description="GPS longitude (optional)"), photo: Optional[UploadFile] = File( None, description="Optional field photo (plants, soil, farm conditions)" ), video: Optional[UploadFile] = File( None, description="Optional short field video of the farm (optional)", ), ): """ Multimodal Farmer-First Climate-Resilient advisory endpoint. Accepts: - Text description from the farmer - Optional GPS coordinates (latitude, longitude) - Optional field photo All reasoning is handled by a multimodal Qwen-VL model (no Gemini). """ if not session_id: session_id = str(uuid.uuid4()) image_bytes = await photo.read() if photo is not None else None video_bytes = await video.read() if video is not None else None result = advise_climate_resilient( query=query, session_id=session_id, latitude=latitude, longitude=longitude, image_bytes=image_bytes, video_bytes=video_bytes, ) return result @app.get("/weather/current") def weather_current( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, aqi: str = "yes", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) url = "http://api.weatherapi.com/v1/current.json" params = {"key": config.WEATHER_API_KEY, "q": q_val, "aqi": aqi} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/forecast") def weather_forecast( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, days: Optional[int] = None, aqi: str = "yes", alerts: str = "yes", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) try: return fetch_forecast( q=q_val, days=days or getattr(config, "WEATHER_FORECAST_DAYS", 3), alerts=alerts, aqi=aqi, ) except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/history") def weather_history( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, dt: Optional[str] = None, end_dt: Optional[str] = None, hour: Optional[int] = None, aqi: str = "yes", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) url = "http://api.weatherapi.com/v1/history.json" params = {"key": config.WEATHER_API_KEY, "q": q_val, "aqi": aqi} if dt: params["dt"] = dt if end_dt: params["end_dt"] = end_dt if hour is not None: params["hour"] = hour try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/marine") def weather_marine( q: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, days: int = 3, tides: str = "no", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, None, lat, lon) url = "http://api.weatherapi.com/v1/marine.json" params = {"key": config.WEATHER_API_KEY, "q": q_val, "days": max(1, min(days, 7)), "tides": tides} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/future") def weather_future( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, dt: str = "", ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") if not dt: raise HTTPException(status_code=400, detail="dt is required for future weather") q_val = _resolve_weather_q(q, state, lat, lon) url = "http://api.weatherapi.com/v1/future.json" params = {"key": config.WEATHER_API_KEY, "q": q_val, "dt": dt} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/timezone") def weather_timezone( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) url = "http://api.weatherapi.com/v1/timezone.json" params = {"key": config.WEATHER_API_KEY, "q": q_val} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/search") def weather_search(q: str): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") url = "http://api.weatherapi.com/v1/search.json" params = {"key": config.WEATHER_API_KEY, "q": q} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/astronomy") def weather_astronomy( q: Optional[str] = None, state: Optional[str] = None, lat: Optional[float] = None, lon: Optional[float] = None, dt: Optional[str] = None, ): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") q_val = _resolve_weather_q(q, state, lat, lon) url = "http://api.weatherapi.com/v1/astronomy.json" params = {"key": config.WEATHER_API_KEY, "q": q_val} if dt: params["dt"] = dt try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") @app.get("/weather/ip") def weather_ip(ip: Optional[str] = None): if not config.WEATHER_API_KEY: raise HTTPException(status_code=500, detail="WEATHER_API_KEY is not configured") url = "http://api.weatherapi.com/v1/ip.json" q_val = ip or "auto:ip" params = {"key": config.WEATHER_API_KEY, "q": q_val} try: resp = requests.get(url, params=params, timeout=10) resp.raise_for_status() return resp.json() except Exception as e: raise HTTPException(status_code=502, detail=f"WeatherAPI request failed: {e}") if __name__ == "__main__": uvicorn.run( "app.main:app", host="0.0.0.0", port=getattr(config, "PORT", 7860), reload=bool(getattr(config, "DEBUG", False)) )