""" Weather Tool - Get weather data and forecasts Free sources used: - Open-Meteo API (completely free, no API key required) - OpenWeatherMap (free tier available) Primary use: Open-Meteo (no key required) """ from __future__ import annotations import logging from datetime import datetime from typing import Optional import requests log = logging.getLogger(__name__) # Free weather APIs OPEN_METEO_API = "https://api.open-meteo.com/v1" GEOCODING_API = "https://geocoding-api.open-meteo.com/v1" def weather_get_coordinates( location: str, ) -> dict: """ Get coordinates for a location name. Args: location: City name or location (e.g., "New York", "London, UK") Returns: Dictionary with location coordinates """ try: url = f"{GEOCODING_API}/search" params = { "name": location, "count": 1, "language": "en", "format": "json", } response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() results = data.get("results", []) if not results: return { "success": False, "error": f"Location not found: {location}", "source": "open-meteo", } loc = results[0] return { "success": True, "source": "open-meteo", "name": loc.get("name", ""), "country": loc.get("country", ""), "latitude": loc.get("latitude"), "longitude": loc.get("longitude"), "elevation": loc.get("elevation"), "timezone": loc.get("timezone"), "population": loc.get("population"), } except Exception as e: log.error(f"Geocoding failed: {e}") return { "success": False, "error": str(e), "source": "open-meteo", } def weather_get_current( location: str, units: str = "celsius", ) -> dict: """ Get current weather for a location. Args: location: City name or location units: Temperature units (celsius or fahrenheit) Returns: Dictionary with current weather data """ try: # First get coordinates geo = weather_get_coordinates(location) if not geo.get("success"): return geo lat = geo["latitude"] lon = geo["longitude"] url = f"{OPEN_METEO_API}/forecast" params = { "latitude": lat, "longitude": lon, "current": "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code,cloud_cover,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m", "temperature_unit": units, "timezone": "auto", } response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() current = data.get("current", {}) # Weather code descriptions weather_codes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", } weather_code = current.get("weather_code", 0) weather_description = weather_codes.get(weather_code, "Unknown") return { "success": True, "source": "open-meteo", "location": geo.get("name", location), "country": geo.get("country", ""), "latitude": lat, "longitude": lon, "timezone": data.get("timezone", ""), "temperature": current.get("temperature_2m"), "feels_like": current.get("apparent_temperature"), "humidity": current.get("relative_humidity_2m"), "weather_code": weather_code, "weather_description": weather_description, "cloud_cover": current.get("cloud_cover"), "pressure_msl": current.get("pressure_msl"), "wind_speed": current.get("wind_speed_10m"), "wind_direction": current.get("wind_direction_10m"), "wind_gusts": current.get("wind_gusts_10m"), "precipitation": current.get("precipitation"), "rain": current.get("rain"), "snowfall": current.get("snowfall"), "units": units, "timestamp": datetime.now().isoformat(), } except Exception as e: log.error(f"Weather fetch failed: {e}") return { "success": False, "error": str(e), "source": "open-meteo", } def weather_get_forecast( location: str, days: int = 7, units: str = "celsius", ) -> dict: """ Get weather forecast for a location. Args: location: City name or location days: Number of forecast days (1-16) units: Temperature units (celsius or fahrenheit) Returns: Dictionary with weather forecast """ try: # First get coordinates geo = weather_get_coordinates(location) if not geo.get("success"): return geo lat = geo["latitude"] lon = geo["longitude"] url = f"{OPEN_METEO_API}/forecast" params = { "latitude": lat, "longitude": lon, "daily": "weather_code,temperature_2m_max,temperature_2m_min,apparent_temperature_max,apparent_temperature_min,sunrise,sunset,uv_index_max,precipitation_sum,rain_sum,showers_sum,snowfall_sum,precipitation_probability_max,wind_speed_10m_max,wind_gusts_10m_max", "temperature_unit": units, "timezone": "auto", "forecast_days": min(days, 16), } response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() daily = data.get("daily", {}) # Weather code descriptions weather_codes = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Fog", 48: "Depositing rime fog", 51: "Light drizzle", 53: "Moderate drizzle", 55: "Dense drizzle", 56: "Light freezing drizzle", 57: "Dense freezing drizzle", 61: "Slight rain", 63: "Moderate rain", 65: "Heavy rain", 66: "Light freezing rain", 67: "Heavy freezing rain", 71: "Slight snow", 73: "Moderate snow", 75: "Heavy snow", 77: "Snow grains", 80: "Slight rain showers", 81: "Moderate rain showers", 82: "Violent rain showers", 85: "Slight snow showers", 86: "Heavy snow showers", 95: "Thunderstorm", 96: "Thunderstorm with slight hail", 99: "Thunderstorm with heavy hail", } forecasts = [] dates = daily.get("time", []) for i, date in enumerate(dates): weather_code = daily.get("weather_code", [])[i] if i < len(daily.get("weather_code", [])) else 0 forecasts.append({ "date": date, "temp_max": daily.get("temperature_2m_max", [])[i] if i < len(daily.get("temperature_2m_max", [])) else None, "temp_min": daily.get("temperature_2m_min", [])[i] if i < len(daily.get("temperature_2m_min", [])) else None, "feels_like_max": daily.get("apparent_temperature_max", [])[i] if i < len(daily.get("apparent_temperature_max", [])) else None, "feels_like_min": daily.get("apparent_temperature_min", [])[i] if i < len(daily.get("apparent_temperature_min", [])) else None, "weather_code": weather_code, "weather_description": weather_codes.get(weather_code, "Unknown"), "precipitation": daily.get("precipitation_sum", [])[i] if i < len(daily.get("precipitation_sum", [])) else None, "rain": daily.get("rain_sum", [])[i] if i < len(daily.get("rain_sum", [])) else None, "snowfall": daily.get("snowfall_sum", [])[i] if i < len(daily.get("snowfall_sum", [])) else None, "precipitation_probability": daily.get("precipitation_probability_max", [])[i] if i < len(daily.get("precipitation_probability_max", [])) else None, "uv_index": daily.get("uv_index_max", [])[i] if i < len(daily.get("uv_index_max", [])) else None, "wind_speed_max": daily.get("wind_speed_10m_max", [])[i] if i < len(daily.get("wind_speed_10m_max", [])) else None, "wind_gusts_max": daily.get("wind_gusts_10m_max", [])[i] if i < len(daily.get("wind_gusts_10m_max", [])) else None, "sunrise": daily.get("sunrise", [])[i] if i < len(daily.get("sunrise", [])) else None, "sunset": daily.get("sunset", [])[i] if i < len(daily.get("sunset", [])) else None, }) return { "success": True, "source": "open-meteo", "location": geo.get("name", location), "country": geo.get("country", ""), "latitude": lat, "longitude": lon, "timezone": data.get("timezone", ""), "units": units, "forecast": forecasts, "count": len(forecasts), } except Exception as e: log.error(f"Weather forecast fetch failed: {e}") return { "success": False, "error": str(e), "source": "open-meteo", } def weather_get_air_quality( location: str, ) -> dict: """ Get air quality index for a location. Args: location: City name or location Returns: Dictionary with air quality data """ try: # First get coordinates geo = weather_get_coordinates(location) if not geo.get("success"): return geo lat = geo["latitude"] lon = geo["longitude"] url = "https://air-quality-api.open-meteo.com/v1/air-quality" params = { "latitude": lat, "longitude": lon, "current": "us_aqi,pm10,pm2_5,carbon_monoxide,nitrogen_dioxide,sulphur_dioxide,ozone,ammonia", "timezone": "auto", } response = requests.get(url, params=params, timeout=10) response.raise_for_status() data = response.json() current = data.get("current", {}) # US AQI categories aqi = current.get("us_aqi", 0) if aqi <= 50: category = "Good" elif aqi <= 100: category = "Moderate" elif aqi <= 150: category = "Unhealthy for Sensitive Groups" elif aqi <= 200: category = "Unhealthy" elif aqi <= 300: category = "Very Unhealthy" else: category = "Hazardous" return { "success": True, "source": "open-meteo", "location": geo.get("name", location), "country": geo.get("country", ""), "us_aqi": aqi, "aqi_category": category, "pm2_5": current.get("pm2_5"), "pm10": current.get("pm10"), "carbon_monoxide": current.get("carbon_monoxide"), "nitrogen_dioxide": current.get("nitrogen_dioxide"), "sulphur_dioxide": current.get("sulphur_dioxide"), "ozone": current.get("ozone"), "ammonia": current.get("ammonia"), "timestamp": datetime.now().isoformat(), } except Exception as e: log.error(f"Air quality fetch failed: {e}") return { "success": False, "error": str(e), "source": "open-meteo", } # Tool schemas for OpenAI function calling WEATHER_GET_CURRENT_SCHEMA = { "type": "function", "function": { "name": "weather_get_current", "description": "Get current weather conditions for any location worldwide. No API key required.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or location (e.g., 'New York', 'London, UK', 'Tokyo')", }, "units": { "type": "string", "description": "Temperature units", "default": "celsius", "enum": ["celsius", "fahrenheit"], }, }, "required": ["location"], }, }, } WEATHER_GET_FORECAST_SCHEMA = { "type": "function", "function": { "name": "weather_get_forecast", "description": "Get weather forecast for up to 16 days. Includes temperature, precipitation, UV index, and more.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or location", }, "days": { "type": "integer", "description": "Number of forecast days (1-16)", "default": 7, }, "units": { "type": "string", "description": "Temperature units", "default": "celsius", "enum": ["celsius", "fahrenheit"], }, }, "required": ["location"], }, }, } WEATHER_GET_AIR_QUALITY_SCHEMA = { "type": "function", "function": { "name": "weather_get_air_quality", "description": "Get air quality index and pollutant levels for a location. Includes PM2.5, PM10, ozone, and more.", "parameters": { "type": "object", "properties": { "location": { "type": "string", "description": "City name or location", }, }, "required": ["location"], }, }, }