import math import requests import matplotlib.pyplot as plt import seaborn as sns import tempfile import os from config import NASA_FIRMS_MAP_KEY from datetime import datetime, timedelta from smolagents import tool from fpdf import FPDF @tool def get_coordinates(city: str) -> dict: """Get latitude and longitude of a city using OpenStreetMap Nominatim API. Args: city: Name of the city to get coordinates for Returns: Dict with city name, latitude, longitude, or error message """ url = "https://nominatim.openstreetmap.org/search" params = {"q": city, "format": "json", "limit": 1} headers = {"User-Agent": "ClimateRiskTool/1.0"} try: response = requests.get(url, params=params, headers=headers, timeout=10) data = response.json() if not data: return {"error": f"City '{city}' not found"} return { "city": city, "latitude": float(data[0]["lat"]), "longitude": float(data[0]["lon"]), } except Exception as e: return {"error": str(e)} @tool def get_weather_forecast(lat: float, lon: float) -> dict: """Get weather forecast data for risk analysis. Args: lat: Latitude coordinate lon: Longitude coordinate Returns: Dict with weather forecast data or error message """ url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "daily": [ "temperature_2m_max", "temperature_2m_min", "precipitation_sum", "wind_speed_10m_max", "wind_gusts_10m_max", "relative_humidity_2m_min", ], "forecast_days": 7, "timezone": "auto", } try: response = requests.get(url, params=params, timeout=10) return response.json() except Exception as e: return {"error": str(e)} @tool def get_flood_data(lat: float, lon: float) -> dict: """Get flood forecast data. Args: lat: Latitude coordinate lon: Longitude coordinate Returns: Dict with flood forecast data or error message """ url = "https://flood-api.open-meteo.com/v1/flood" params = { "latitude": lat, "longitude": lon, "daily": ["river_discharge", "river_discharge_mean", "river_discharge_max"], "forecast_days": 7, } try: response = requests.get(url, params=params, timeout=10) return response.json() except Exception as e: return {"error": str(e)} @tool def get_earthquake_data( lat: float, lon: float, radius_km: float = 100, days: int = 30 ) -> dict: """Get raw earthquake data from USGS. Args: lat: Latitude coordinate lon: Longitude coordinate radius_km: Search radius in kilometers (default 100km) days: Number of days to look back (default 30 days) Returns: Dict with raw earthquake data from USGS """ url = "https://earthquake.usgs.gov/fdsnws/event/1/query" end_date = datetime.now() start_date = end_date - timedelta(days=days) params = { "format": "geojson", "starttime": start_date.strftime("%Y-%m-%d"), "endtime": end_date.strftime("%Y-%m-%d"), "latitude": lat, "longitude": lon, "maxradiuskm": radius_km, "minmagnitude": 1.0, "orderby": "time-desc", } try: response = requests.get(url, params=params, timeout=15) response.raise_for_status() data = response.json() earthquakes = [] for feature in data.get("features", []): props = feature["properties"] coords = feature["geometry"]["coordinates"] earthquakes.append( { "magnitude": props.get("mag"), "place": props.get("place"), "time": props.get("time"), "depth": coords[2] if len(coords) > 2 else None, "latitude": coords[1], "longitude": coords[0], "alert": props.get("alert"), "significance": props.get("sig"), "event_type": props.get("type"), "title": props.get("title"), } ) return { "earthquakes": earthquakes, "query_location": { "lat": lat, "lon": lon, "radius_km": radius_km, "days": days, }, "data_source": "USGS", } except Exception as e: return {"error": str(e)} @tool def get_nasa_fire_data( lat: float, lon: float, radius_km: float = 50, days: int = 2 ) -> dict: """Get raw wildfire detection data from NASA FIRMS satellites. Args: lat: Latitude coordinate lon: Longitude coordinate radius_km: Search radius in kilometers (default 50km) days: Number of days to look back (default 2 days) Returns: Dict with raw fire detection data from NASA satellites """ if not NASA_FIRMS_MAP_KEY or NASA_FIRMS_MAP_KEY == "your-nasa-firms-api-key-here": return {"error": "NASA FIRMS API key not configured in .env file"} try: lat_offset = radius_km / 111.0 lon_offset = radius_km / (111.0 * abs(math.cos(math.radians(lat)))) bbox = f"{lat - lat_offset},{lon - lon_offset},{lat + lat_offset},{lon + lon_offset}" modis_url = f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{NASA_FIRMS_MAP_KEY}/MODIS_NRT/{bbox}/{days}" viirs_url = f"https://firms.modaps.eosdis.nasa.gov/api/area/csv/{NASA_FIRMS_MAP_KEY}/VIIRS_NOAA20_NRT/{bbox}/{days}" all_fires = [] try: modis_response = requests.get(modis_url, timeout=15) if modis_response.status_code == 200 and modis_response.text.strip(): all_fires.extend(_parse_nasa_csv(modis_response.text, "MODIS")) except: pass try: viirs_response = requests.get(viirs_url, timeout=15) if viirs_response.status_code == 200 and viirs_response.text.strip(): all_fires.extend(_parse_nasa_csv(viirs_response.text, "VIIRS")) except: pass return { "fires": all_fires, "query_location": { "lat": lat, "lon": lon, "radius_km": radius_km, "days": days, }, "data_source": "NASA_FIRMS", } except Exception as e: return {"error": str(e)} def _parse_nasa_csv(csv_text: str, source: str) -> list: """Parse NASA FIRMS CSV data. Args: csv_text: CSV text data from NASA FIRMS API source: Source identifier (MODIS or VIIRS) Returns: List of fire detection dictionaries """ fires = [] lines = csv_text.strip().split("\n") if len(lines) < 2: return fires for line in lines[1:]: try: values = line.split(",") if len(values) >= 9: fires.append( { "latitude": float(values[0]), "longitude": float(values[1]), "brightness": float(values[2]) if values[2] else 0, "scan": float(values[3]) if values[3] else 0, "track": float(values[4]) if values[4] else 0, "acq_date": values[5], "acq_time": values[6], "satellite": values[7], "confidence": int(values[8]) if values[8].isdigit() else 50, "version": values[9] if len(values) > 9 else "", "bright_t31": ( float(values[10]) if len(values) > 10 and values[10] else 0 ), "frp": ( float(values[11]) if len(values) > 11 and values[11] else 0 ), "daynight": values[12] if len(values) > 12 else "", "source": source, } ) except (ValueError, IndexError): continue return fires @tool def find_local_emergency_resources(lat: float, lon: float) -> dict: """Find local emergency resources and contacts. Args: lat: Latitude coordinate lon: Longitude coordinate Returns: Dict with local emergency resources or error message """ try: query = f""" [out:json][timeout:15]; ( node[amenity=hospital](around:10000,{lat},{lon}); node[amenity=fire_station](around:10000,{lat},{lon}); node[amenity=police](around:10000,{lat},{lon}); ); out center meta; """ response = requests.post( "https://overpass-api.de/api/interpreter", data=query, timeout=20 ) if response.status_code == 200: data = response.json() resources = [] for element in data.get("elements", [])[:5]: tags = element.get("tags", {}) resources.append( { "name": tags.get("name", "Unnamed facility"), "type": tags.get("amenity", "unknown"), "latitude": element.get("lat", lat), "longitude": element.get("lon", lon), } ) return {"local_resources": resources} return {"local_resources": []} except Exception as e: return {"error": str(e)} @tool def generate_analysis_report( data: dict, filename: str = "climate_risk_report.pdf" ) -> dict: """Generate a consolidated analysis report with visualizations. Args: data: Consolidated data from various tools, expected to include: - weather forecast - flood data - earthquake data - fire data filename: Desired filename for the exported PDF report Returns: Dict with success message and file path or error """ try: # Temporary directory for plots with tempfile.TemporaryDirectory() as temp_dir: # Initialize the PDF pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() pdf.set_font("Arial", size=12) pdf.set_text_color(50, 50, 50) # Add Title pdf.set_font("Arial", style="B", size=16) pdf.cell(0, 10, "Climate Risk Analysis Report", ln=True, align="C") pdf.ln(10) # Line break # Helper function to save and plot visualizations def save_plot(fig, plot_name): path = f"{temp_dir}/{plot_name}.png" fig.savefig(path) plt.close(fig) return path # Plot weather data weather_data = data.get("weather_forecast", {}).get("daily", {}) if weather_data: dates = [ d for d in range(1, len(weather_data["temperature_2m_max"]) + 1) ] weather_df = { "Day": dates, "Max Temperature (°C)": weather_data["temperature_2m_max"], "Min Temperature (°C)": weather_data["temperature_2m_min"], "Precipitation (mm)": weather_data["precipitation_sum"], } fig, ax = plt.subplots(figsize=(8, 5)) sns.lineplot( x="Day", y="Max Temperature (°C)", data=weather_df, ax=ax, label="Max Temp", color="red", ) sns.lineplot( x="Day", y="Min Temperature (°C)", data=weather_df, ax=ax, label="Min Temp", color="blue", ) sns.barplot( x="Day", y="Precipitation (mm)", data=weather_df, ax=ax, color="gray", alpha=0.5, ) ax.set_title("Weather Forecast") ax.set_xlabel("Day") ax.set_ylabel("Values") ax.legend() weather_plot_path = save_plot(fig, "weather_plot") pdf.image(weather_plot_path, x=10, y=None, w=180) pdf.ln(10) # Plot earthquake data earthquake_data = data.get("earthquake_data", {}).get("earthquakes", []) if earthquake_data: magnitudes = [ eq["magnitude"] for eq in earthquake_data if eq.get("magnitude") ] depths = [eq["depth"] for eq in earthquake_data if eq.get("depth")] places = [eq["place"] for eq in earthquake_data] fig, ax = plt.subplots(figsize=(8, 5)) sns.scatterplot( x=depths, y=magnitudes, hue=places, ax=ax, palette="tab10", s=100 ) ax.set_title("Earthquake Analysis") ax.set_xlabel("Depth (km)") ax.set_ylabel("Magnitude") ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left") earthquake_plot_path = save_plot(fig, "earthquake_plot") pdf.image(earthquake_plot_path, x=10, y=None, w=180) pdf.ln(10) # Plot fire data fire_data = data.get("fire_data", {}).get("fires", []) if fire_data: brightness = [fire["brightness"] for fire in fire_data] confidence = [fire["confidence"] for fire in fire_data] fig, ax = plt.subplots(figsize=(8, 5)) sns.histplot( brightness, bins=20, ax=ax, kde=True, color="orange", label="Brightness", ) sns.histplot( confidence, bins=20, ax=ax, kde=True, color="green", alpha=0.5, label="Confidence", ) ax.set_title("Wildfire Brightness vs Confidence") ax.set_xlabel("Value") ax.legend() fire_plot_path = save_plot(fig, "fire_plot") pdf.image(fire_plot_path, x=10, y=None, w=180) pdf.ln(10) # Save PDF report pdf_output_path = os.path.join(temp_dir, filename) pdf.output(pdf_output_path) return {"success": True, "file_path": pdf_output_path} except Exception as e: return {"error": str(e)} @tool def get_full_daily_forecast(lat: float, lon: float) -> dict: """ Get all available daily weather forecast parameters from Open-Meteo API. Args: lat: Latitude. lon: Longitude. Returns: Dict with all daily forecast data or error. """ daily_params = [ "temperature_2m_max", "temperature_2m_mean", "temperature_2m_min", "apparent_temperature_max", "apparent_temperature_mean", "apparent_temperature_min", "precipitation_sum", "rain_sum", "showers_sum", "snowfall_sum", "precipitation_hours", "precipitation_probability_max", "precipitation_probability_mean", "precipitation_probability_min", "weather_code", "sunrise", "sunset", "sunshine_duration", "daylight_duration", "wind_speed_10m_max", "wind_gusts_10m_max", "wind_direction_10m_dominant", "shortwave_radiation_sum", "et0_fao_evapotranspiration", "uv_index_max", "uv_index_clear_sky_max" ] url = "https://api.open-meteo.com/v1/forecast" params = { "latitude": lat, "longitude": lon, "timezone": "auto", "daily": ",".join(daily_params) } try: response = requests.get(url, params=params, timeout=10) return response.json() except Exception as e: return {"error": str(e)} @tool def climate_change_data( lat: float, lon: float, start_date: str = "1950-01-01", end_date: str = "2050-12-31", models: list[str] = None ) -> dict: """ Get all available daily climate parameters from Open-Meteo Climate API. Args: lat: Latitude. lon: Longitude. start_date: Start date in yyyy-mm-dd (default 1950-01-01). end_date: End date in yyyy-mm-dd (default 2050-12-31). models: Optional list of climate models (default: all models). Returns: Dict with all daily climate data or error. """ daily_params = [ "temperature_2m_max", "temperature_2m_min", "temperature_2m_mean", "cloud_cover_mean", "relative_humidity_2m_max", "relative_humidity_2m_min", "relative_humidity_2m_mean", "soil_moisture_0_to_10cm_mean", "precipitation_sum", "rain_sum", "snowfall_sum", "wind_speed_10m_mean", "wind_speed_10m_max", "pressure_msl_mean", "shortwave_radiation_sum" ] if models is None: models = [ "CMCC_CM2_VHR4", "FGOALS_f3_H", "HiRAM_SIT_HR", "MRI_AGCM3_2_S", "EC_Earth3P_HR", "MPI_ESM1_2_XR", "NICAM16_8S" ] url = "https://climate-api.open-meteo.com/v1/climate" params = { "latitude": lat, "longitude": lon, "start_date": start_date, "end_date": end_date, "models": ",".join(models), "daily": ",".join(daily_params), "timezone": "auto" } try: response = requests.get(url, params=params, timeout=60) return response.json() except Exception as e: return {"error": str(e)} @tool def get_full_air_quality_forecast( lat: float, lon: float, forecast_days: int = 5, past_days: int = 0, domain: str = "auto" ) -> dict: """ Get all available hourly air quality forecast parameters from Open-Meteo Air Quality API. Args: lat: Latitude. lon: Longitude. forecast_days: Number of forecast days (default 5, max 7). past_days: Number of past days (default 0, max 92). domain: 'auto', 'cams_europe', or 'cams_global'. Returns: Dict with all hourly air quality data or error. """ hourly_params = [ "pm10", "pm2_5", "carbon_monoxide", "carbon_dioxide", "nitrogen_dioxide", "sulphur_dioxide", "ozone", "aerosol_optical_depth", "dust", "uv_index", "uv_index_clear_sky", "ammonia", "methane", "alder_pollen", "birch_pollen", "grass_pollen", "mugwort_pollen", "olive_pollen", "ragweed_pollen", "european_aqi", "us_aqi" ] url = "https://air-quality-api.open-meteo.com/v1/air-quality" params = { "latitude": lat, "longitude": lon, "forecast_days": min(max(forecast_days, 0), 7), "past_days": min(max(past_days, 0), 92), "hourly": ",".join(hourly_params), "domains": domain, "timezone": "auto", } try: response = requests.get(url, params=params, timeout=30) return response.json() except Exception as e: return {"error": str(e)} @tool def get_full_marine_daily_forecast(lat: float, lon: float) -> dict: """ Get all available daily marine forecast parameters from Open-Meteo Marine API. Args: lat: Latitude. lon: Longitude. Returns: Dict with all daily marine forecast data or error. """ daily_params = [ "wave_height_max", "wind_wave_height_max", "swell_wave_height_max", "wave_direction_dominant", "wind_wave_direction_dominant", "swell_wave_direction_dominant", "wave_period_max", "wind_wave_period_max", "swell_wave_period_max", "wind_wave_peak_period_max", "swell_wave_peak_period_max" ] url = "https://marine-api.open-meteo.com/v1/marine" params = { "latitude": lat, "longitude": lon, "timezone": "auto", "daily": ",".join(daily_params) } try: response = requests.get(url, params=params, timeout=10) return response.json() except Exception as e: return {"error": str(e)} @tool def get_full_flood_daily_forecast(lat: float, lon: float) -> dict: """ Get all available daily flood parameters from Open-Meteo Flood API. Args: lat: Latitude. lon: Longitude. Returns: Dict with all daily flood forecast data or error. """ daily_params = [ "river_discharge", "river_discharge_mean", "river_discharge_median", "river_discharge_max", "river_discharge_min", "river_discharge_p25", "river_discharge_p75" ] url = "https://flood-api.open-meteo.com/v1/flood" params = { "latitude": lat, "longitude": lon, "daily": ",".join(daily_params) } try: response = requests.get(url, params=params, timeout=10) return response.json() except Exception as e: return {"error": str(e)} @tool def get_full_satellite_radiation( lat: float, lon: float, start_date: str = None, end_date: str = None, hourly_native: bool = False, tilt: int = 0, azimuth: int = 0 ) -> dict: """ Get all available hourly satellite solar radiation parameters from Open-Meteo Satellite API. Args: lat: Latitude. lon: Longitude. start_date: (optional) Start date (yyyy-mm-dd). If None, today. end_date: (optional) End date (yyyy-mm-dd). If None, today. hourly_native: Use native satellite temporal resolution (10/15/30min) if True, else hourly. tilt: Tilt for GTI (default 0 = horizontal). azimuth: Azimuth for GTI (default 0 = south). Returns: Dict with all hourly satellite solar radiation data or error. """ hourly_params = [ "shortwave_radiation", "diffuse_radiation", "direct_radiation", "direct_normal_irradiance", "global_tilted_irradiance", "terrestrial_radiation", "shortwave_radiation_instant", "diffuse_radiation_instant", "direct_radiation_instant", "direct_normal_irradiance_instant", "global_tilted_irradiance_instant", "terrestrial_radiation_instant" ] url = "https://satellite-api.open-meteo.com/v1/archive" today = datetime.utcnow().date() if start_date is None: start_date = str(today) if end_date is None: end_date = str(today) params = { "latitude": lat, "longitude": lon, "start_date": start_date, "end_date": end_date, "hourly": ",".join(hourly_params), "models": "satellite_radiation_seamless", "timezone": "auto", "tilt": tilt, "azimuth": azimuth, } if hourly_native: params["hourly_native"] = "true" try: response = requests.get(url, params=params, timeout=30) return response.json() except Exception as e: return {"error": str(e)}