| 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()) |
|
|
| 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)) |
| ) |
|
|