"""
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"
]
)