Skip to main content
Glama
ImDPS
by ImDPS
weather.py23.3 kB
""" Enhanced Weather Tool for Gemini LLM Integration. Provides comprehensive weather information using the Open-Meteo API. """ import logging import re import aiohttp import asyncio from typing import Dict, Any, Optional, List, Tuple from datetime import datetime try: from src.tools import ToolDefinition except ImportError: from tools import ToolDefinition logger = logging.getLogger(__name__) class WeatherTool: """Enhanced tool for providing weather information using Open-Meteo API.""" def __init__(self): logger.info("Enhanced weather tool initialized with Open-Meteo API") self.base_url = "https://api.open-meteo.com/v1" self.geocoding_url = "https://geocoding-api.open-meteo.com/v1" self.session = None # Common city coordinates for faster lookups self.city_coordinates = { "New York": (40.7128, -74.0060), "London": (51.5074, -0.1278), "Tokyo": (35.6762, 139.6503), "Sydney": (-33.8688, 151.2093), "San Francisco": (37.7749, -122.4194), "Paris": (48.8566, 2.3522), "Berlin": (52.5200, 13.4050), "Mumbai": (19.0760, 72.8777), "Toronto": (43.6532, -79.3832), "Dubai": (25.2048, 55.2708), "Los Angeles": (34.0522, -118.2437), "Chicago": (41.8781, -87.6298), "Miami": (25.7617, -80.1918), "Seattle": (47.6062, -122.3321), "Boston": (42.3601, -71.0589), "Austin": (30.2672, -97.7431), "Denver": (39.7392, -104.9903), "Phoenix": (33.4484, -112.0740), "Las Vegas": (36.1699, -115.1398), "Nashville": (36.1627, -86.7816), # Indian cities "Raipur": (21.2333, 81.6333), "Bhilai": (21.2092, 81.4285), "Delhi": (28.6139, 77.2090), "Mumbai": (19.0760, 72.8777), "Bangalore": (12.9716, 77.5946), "Chennai": (13.0827, 80.2707), "Kolkata": (22.5726, 88.3639), "Hyderabad": (17.3850, 78.4867), "Pune": (18.5204, 73.8567), "Ahmedabad": (23.0225, 72.5714), "Jaipur": (26.9124, 75.7873), "Lucknow": (26.8467, 80.9462), "Kanpur": (26.4499, 80.3319), "Nagpur": (21.1458, 79.0882), "Indore": (22.7196, 75.8577), "Thane": (19.2183, 72.9781), "Bhopal": (23.2599, 77.4126), "Visakhapatnam": (17.6868, 83.2185), "Pimpri-Chinchwad": (18.6298, 73.7997), "Patna": (25.5941, 85.1376), "Vadodara": (22.3072, 73.1812), "Ghaziabad": (28.6654, 77.4391), "Ludhiana": (30.9010, 75.8573), "Agra": (27.1767, 78.0081), "Nashik": (19.9975, 73.7898), "Faridabad": (28.4089, 77.3178), "Meerut": (28.7041, 77.1025), "Rajkot": (22.3039, 70.8022), "Kalyan-Dombivali": (19.2183, 73.1104), "Vasai-Virar": (19.4259, 72.8225), "Varanasi": (25.3176, 82.9739), "Srinagar": (34.0837, 74.7973), "Aurangabad": (19.8762, 75.3433), "Dhanbad": (23.7947, 86.4304), "Amritsar": (31.6340, 74.8723), "Allahabad": (25.4358, 81.8463), "Ranchi": (23.3441, 85.3096), "Howrah": (22.5958, 88.2636), "Coimbatore": (11.0168, 76.9558), "Jabalpur": (23.1815, 79.9864), "Gwalior": (26.2183, 78.1828), "Vijayawada": (16.5062, 80.6480), "Jodhpur": (26.2389, 73.0243), "Madurai": (9.9252, 78.1198), "Raipur": (21.2333, 81.6333), "Kota": (25.2138, 75.8648), "Guwahati": (26.1833, 91.7500), "Chandigarh": (30.7333, 76.7794), "Solapur": (17.6599, 75.9064), "Hubli-Dharwad": (15.3647, 75.1240), "Bareilly": (28.3670, 79.4304), "Moradabad": (28.8389, 78.7568), "Mysore": (12.2958, 76.6394), "Gurgaon": (28.4595, 77.0266), "Aligarh": (27.8833, 78.0833), "Jalandhar": (31.3256, 75.5792), "Tiruchirappalli": (10.7905, 78.7047), "Bhubaneswar": (20.2961, 85.8245), "Salem": (11.6643, 78.1460), "Warangal": (17.9689, 79.5941), "Guntur": (16.2991, 80.4575), "Bhiwandi": (19.3000, 73.0667), "Saharanpur": (29.9675, 77.5536), "Gorakhpur": (26.7606, 83.3732), "Bikaner": (28.0229, 73.3119), "Amravati": (20.9374, 77.7796), "Noida": (28.5355, 77.3910), "Jamshedpur": (22.8046, 86.2029), "Bhilai": (21.2092, 81.4285), "Cuttack": (20.4625, 85.8830), "Kochi": (9.9312, 76.2673), "Udaipur": (24.5854, 73.7125), "Mangalore": (12.9141, 74.8560), "Kozhikode": (11.2588, 75.7804), "Bokaro": (23.7871, 85.9564), "Rajahmundry": (17.0005, 81.8040), "Bellary": (15.1394, 76.9214), "Patiala": (30.3398, 76.3869), "Bilaspur": (22.0736, 82.1564), "Kurnool": (15.8281, 78.0373), "Bikaner": (28.0229, 73.3119), "Paradip": (20.3164, 86.6085), "Bardhaman": (23.2324, 87.1905), "Kakinada": (16.9604, 82.2381), "Bhavnagar": (21.7645, 72.1519), "Bidar": (17.9104, 77.5199), "Rourkela": (22.2492, 84.8828), "Karnal": (29.6857, 76.9905), "Bathinda": (30.2070, 74.9455), "Rampur": (28.8104, 79.0260), "Shivamogga": (13.9299, 75.5681), "Ratlam": (23.0472, 75.0699), "Ujjain": (23.1765, 75.7885), "Ongole": (15.5036, 80.0495), "Bharatpur": (27.2156, 77.4909), "Sikar": (27.6121, 75.1399), "Cuddalore": (11.7461, 79.7644), "Hospet": (15.2667, 76.4000), "Sangli": (16.8544, 74.5642), "Bijapur": (16.8244, 75.7154), "Khandwa": (21.8247, 76.3529), "Yavatmal": (20.4000, 78.1333), "Chittoor": (13.2156, 79.1004), "Hindupur": (13.8281, 77.4914), "Nizamabad": (18.6725, 78.0941), "Sagar": (23.8383, 78.7378), "Tumkur": (13.3422, 77.1016), "Hisar": (29.1492, 75.7217), "Rohtak": (28.8955, 76.6066), "Panipat": (29.3909, 76.9635), "Darbhanga": (26.1522, 85.8972), "Kharagpur": (22.3460, 87.2320), "Aizawl": (23.7307, 92.7173), "Ichalkaranji": (16.6914, 74.4605), "Tirupati": (13.6288, 79.4192), "Karnal": (29.6857, 76.9905), "Bathinda": (30.2070, 74.9455), "Rampur": (28.8104, 79.0260), "Shivamogga": (13.9299, 75.5681), "Ratlam": (23.0472, 75.0699), "Ujjain": (23.1765, 75.7885), "Ongole": (15.5036, 80.0495), "Bharatpur": (27.2156, 77.4909), "Sikar": (27.6121, 75.1399), "Cuddalore": (11.7461, 79.7644), "Hospet": (15.2667, 76.4000), "Sangli": (16.8544, 74.5642), "Bijapur": (16.8244, 75.7154), "Khandwa": (21.8247, 76.3529), "Yavatmal": (20.4000, 78.1333), "Chittoor": (13.2156, 79.1004), "Hindupur": (13.8281, 77.4914), "Nizamabad": (18.6725, 78.0941), "Sagar": (23.8383, 78.7378), "Tumkur": (13.3422, 77.1016), "Hisar": (29.1492, 75.7217), "Rohtak": (28.8955, 76.6066), "Panipat": (29.3909, 76.9635), "Darbhanga": (26.1522, 85.8972), "Kharagpur": (22.3460, 87.2320), "Aizawl": (23.7307, 92.7173), "Ichalkaranji": (16.6914, 74.4605), "Tirupati": (13.6288, 79.4192) } # Location aliases for better matching self.location_aliases = { "nyc": "New York", "new york city": "New York", "the big apple": "New York", "london uk": "London", "tokyo japan": "Tokyo", "sydney australia": "Sydney", "san fran": "San Francisco", "sf": "San Francisco", "paris france": "Paris", "berlin germany": "Berlin", "mumbai india": "Mumbai", "bombay": "Mumbai", "toronto canada": "Toronto", "dubai uae": "Dubai", "la": "Los Angeles", "chicago il": "Chicago", "miami fl": "Miami", "seattle wa": "Seattle", "boston ma": "Boston", "austin tx": "Austin", "denver co": "Denver", "phoenix az": "Phoenix", "vegas": "Las Vegas", "nashville tn": "Nashville" } async def _get_session(self) -> aiohttp.ClientSession: """Get or create an aiohttp session.""" if self.session is None or self.session.closed: timeout = aiohttp.ClientTimeout(total=10) self.session = aiohttp.ClientSession(timeout=timeout) return self.session async def _geocode_location(self, location: str) -> Optional[Tuple[float, float]]: """Geocode a location name to coordinates using Open-Meteo geocoding API.""" try: session = await self._get_session() # First check our predefined coordinates if location in self.city_coordinates: logger.info(f"Using predefined coordinates for {location}") return self.city_coordinates[location] # Use Open-Meteo geocoding API params = { "name": location, "count": 5, # Get more results to find the best match "language": "en", "format": "json" } async with session.get(f"{self.geocoding_url}/search", params=params) as response: if response.status == 200: data = await response.json() if data.get("results") and len(data["results"]) > 0: # Get the first result (most relevant) result = data["results"][0] lat, lon = result["latitude"], result["longitude"] # Log the found location details country = result.get("country", "Unknown") admin1 = result.get("admin1", "") logger.info(f"Geocoded '{location}' to {lat}, {lon} ({result['name']}, {admin1}, {country})") return (lat, lon) else: logger.warning(f"No geocoding results found for location: {location}") return None else: logger.warning(f"Geocoding API request failed with status {response.status} for location: {location}") return None except Exception as e: logger.error(f"Error geocoding location {location}: {e}") return None async def _get_weather_data(self, latitude: float, longitude: float) -> Optional[Dict[str, Any]]: """Get weather data from Open-Meteo API.""" try: session = await self._get_session() params = { "latitude": latitude, "longitude": longitude, "current": "temperature_2m,relative_humidity_2m,apparent_temperature,precipitation,rain,showers,snowfall,weather_code,pressure_msl,surface_pressure,wind_speed_10m,wind_direction_10m,wind_gusts_10m,visibility,cloud_cover,cloud_cover_low,cloud_cover_mid,cloud_cover_high", "timezone": "auto", "forecast_days": 1 } async with session.get(f"{self.base_url}/forecast", params=params) as response: if response.status == 200: data = await response.json() return data else: logger.error(f"Weather API request failed with status {response.status}") return None except Exception as e: logger.error(f"Error fetching weather data: {e}") return None def _get_weather_condition(self, weather_code: int) -> str: """Convert WMO weather codes to human-readable conditions.""" weather_conditions = { 0: "Clear sky", 1: "Mainly clear", 2: "Partly cloudy", 3: "Overcast", 45: "Foggy", 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 fall", 73: "Moderate snow fall", 75: "Heavy snow fall", 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" } return weather_conditions.get(weather_code, "Unknown") def _format_temperature(self, temp_celsius: float) -> str: """Format temperature in both Celsius and Fahrenheit.""" temp_fahrenheit = (temp_celsius * 9/5) + 32 return f"{temp_celsius:.1f}°C ({temp_fahrenheit:.1f}°F)" def _format_wind(self, speed_kmh: float, direction: int) -> str: """Format wind information.""" speed_mph = speed_kmh * 0.621371 directions = ["N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW"] direction_name = directions[round(direction / 22.5) % 16] return f"{speed_mph:.1f} mph {direction_name}" def _format_pressure(self, pressure_hpa: float) -> str: """Format pressure in both hPa and inHg.""" pressure_inhg = pressure_hpa * 0.02953 return f"{pressure_hpa:.1f} hPa ({pressure_inhg:.2f} inHg)" def _format_visibility(self, visibility_km: float) -> str: """Format visibility.""" visibility_miles = visibility_km * 0.621371 return f"{visibility_miles:.1f} miles" async def get_weather(self, location: str) -> str: """Get comprehensive weather information for a specific location. This tool provides detailed current weather information using the Open-Meteo API. It can handle queries about temperature, conditions, humidity, wind, pressure, and visibility. Supports natural language queries and location aliases. Args: location: City name or location to get weather for (e.g., "New York", "London", "Tokyo") Returns: Comprehensive weather information for the specified location """ logger.info(f"get_weather called with location: {location}") try: if not location or not location.strip(): raise ValueError("Location cannot be empty") location = location.strip() # Normalize location name normalized_location = self._normalize_location(location) if not normalized_location: return f"Could not identify location: '{location}'. Please try a different city name." # Geocode the location coordinates = await self._geocode_location(normalized_location) if not coordinates: return f"Could not find coordinates for location: '{normalized_location}'. Please try a different city name." # Get weather data weather_data = await self._get_weather_data(coordinates[0], coordinates[1]) if not weather_data or "current" not in weather_data: return f"Could not retrieve weather data for '{normalized_location}'. Please try again later." # Format the response result = self._format_weather_response(normalized_location, weather_data["current"]) logger.info(f"Weather data retrieved successfully for {normalized_location}") return result except Exception as e: logger.error(f"Error getting weather for {location}: {e}") return f"Failed to get weather data for '{location}': {str(e)}" def _normalize_location(self, location: str) -> Optional[str]: """Normalize location name and find the best match.""" location_lower = location.lower().strip() # Check for exact matches first if location in self.city_coordinates: return location # Check aliases if location_lower in self.location_aliases: return self.location_aliases[location_lower] # Check for partial matches in predefined cities for city in self.city_coordinates.keys(): if location_lower in city.lower() or city.lower() in location_lower: return city # Check for word-based matches in predefined cities location_words = set(location_lower.split()) for city in self.city_coordinates.keys(): city_words = set(city.lower().split()) if location_words.intersection(city_words): return city # If no match found in predefined cities, return the original location # The geocoding API will handle it return location def _format_weather_response(self, location: str, current: Dict[str, Any]) -> str: """Format weather information into a readable response.""" result = f"🌤️ Weather for {location}:\n\n" # Temperature if "temperature_2m" in current: temp = current["temperature_2m"] result += f"🌡️ Temperature: {self._format_temperature(temp)}\n" # Apparent temperature (feels like) if "apparent_temperature" in current: apparent_temp = current["apparent_temperature"] result += f"🤔 Feels like: {self._format_temperature(apparent_temp)}\n" # Weather condition if "weather_code" in current: condition = self._get_weather_condition(current["weather_code"]) result += f"☁️ Condition: {condition}\n" # Humidity if "relative_humidity_2m" in current: humidity = current["relative_humidity_2m"] result += f"💧 Humidity: {humidity}%\n" # Wind if "wind_speed_10m" in current and "wind_direction_10m" in current: wind_speed = current["wind_speed_10m"] wind_direction = current["wind_direction_10m"] result += f"💨 Wind: {self._format_wind(wind_speed, wind_direction)}\n" # Pressure if "pressure_msl" in current: pressure = current["pressure_msl"] result += f"📊 Pressure: {self._format_pressure(pressure)}\n" # Visibility if "visibility" in current: visibility = current["visibility"] result += f"👁️ Visibility: {self._format_visibility(visibility)}\n" # Precipitation if "precipitation" in current and current["precipitation"] > 0: precip = current["precipitation"] result += f"🌧️ Precipitation: {precip:.1f} mm\n" # Cloud cover if "cloud_cover" in current: cloud_cover = current["cloud_cover"] result += f"☁️ Cloud Cover: {cloud_cover}%\n" # Add timestamp if "time" in current: try: timestamp = datetime.fromisoformat(current["time"].replace("Z", "+00:00")) result += f"\n🕐 Last updated: {timestamp.strftime('%Y-%m-%d %H:%M UTC')}" except: pass return result def get_available_locations(self) -> List[str]: """Get list of available locations.""" return list(self.city_coordinates.keys()) def search_locations(self, query: str) -> List[str]: """Search for locations matching a query.""" query_lower = query.lower() matches = [] for location in self.city_coordinates.keys(): if query_lower in location.lower(): matches.append(location) return matches async def close(self): """Close the aiohttp session.""" if self.session and not self.session.closed: await self.session.close() # Global instance weather_tool = WeatherTool() def register_tool() -> ToolDefinition: """Register the enhanced weather tool.""" return ToolDefinition( name="get_weather", description="Get comprehensive weather information for cities worldwide using real-time data from Open-Meteo API. Provides temperature, conditions, humidity, wind, pressure, visibility, and more. Supports natural language queries and location aliases.", handler=weather_tool.get_weather, input_schema={ "type": "object", "properties": { "location": { "type": "string", "description": "City name or location to get weather for (e.g., 'New York', 'London', 'Tokyo', 'NYC', 'San Francisco', 'Los Angeles', 'Chicago', 'Miami', 'Seattle', 'Boston', 'Austin', 'Denver', 'Phoenix', 'Las Vegas', 'Nashville')" } }, "required": ["location"] }, examples=[ "What's the weather in New York?", "How's the weather in London?", "What's the temperature in Tokyo?", "Weather in Sydney", "How's the weather in San Francisco?", "What's the weather like in Paris?", "Temperature in Berlin", "Weather in Mumbai", "How's the weather in Toronto?", "What's the weather in Dubai?", "Weather in Los Angeles", "How's the weather in Chicago?", "What's the weather in Miami?", "Weather in Seattle", "How's the weather in Boston?", "What's the weather in Austin?", "Weather in Denver", "How's the weather in Phoenix?", "What's the weather in Las Vegas?", "Weather in Nashville" ] )

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ImDPS/MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server