""" Weather Tool - Get weather information from OpenWeatherMap API. """ import os import time import requests from typing import Any, Dict from tools.base import BaseTool # Rate limiting: max requests per hour MAX_REQUESTS_PER_HOUR = 60 _request_times = [] class WeatherTool(BaseTool): """Weather lookup tool using OpenWeatherMap API.""" def __init__(self): """Initialize weather tool with API key.""" super().__init__() self.api_key = os.getenv("OPENWEATHERMAP_API_KEY") self.base_url = "https://api.openweathermap.org/data/2.5/weather" if not self.api_key: # Allow running without API key (will return error message) pass @property def name(self) -> str: return "weather" @property def description(self) -> str: return "Get current weather information for a location. Requires city name or coordinates." def get_schema(self) -> Dict[str, Any]: """Get tool schema.""" return { "name": self.name, "description": self.description, "inputSchema": { "type": "object", "properties": { "location": { "type": "string", "description": "City name, state/country (e.g., 'San Francisco, CA' or 'London, UK') or coordinates 'lat,lon'" }, "units": { "type": "string", "description": "Temperature units: 'metric' (Celsius), 'imperial' (Fahrenheit), or 'kelvin' (default)", "enum": ["metric", "imperial", "kelvin"], "default": "metric" } }, "required": ["location"] } } def _check_rate_limit(self) -> bool: """Check if rate limit is exceeded.""" global _request_times now = time.time() # Remove requests older than 1 hour _request_times = [t for t in _request_times if now - t < 3600] if len(_request_times) >= MAX_REQUESTS_PER_HOUR: return False return True def _record_request(self): """Record a request for rate limiting.""" global _request_times _request_times.append(time.time()) def _get_weather(self, location: str, units: str = "metric") -> Dict[str, Any]: """ Get weather data from OpenWeatherMap API. Args: location: City name or coordinates units: Temperature units (metric, imperial, kelvin) Returns: Weather data dictionary """ if not self.api_key: raise ValueError( "OpenWeatherMap API key not configured. " "Set OPENWEATHERMAP_API_KEY environment variable. " "Get a free API key at https://openweathermap.org/api" ) # Check rate limit if not self._check_rate_limit(): raise ValueError( f"Rate limit exceeded. Maximum {MAX_REQUESTS_PER_HOUR} requests per hour." ) # Build query parameters params = { "appid": self.api_key, "units": units } # Check if location is coordinates (lat,lon format) if "," in location and location.replace(",", "").replace(".", "").replace("-", "").strip().isdigit(): # Looks like coordinates parts = location.split(",") if len(parts) == 2: try: params["lat"] = float(parts[0].strip()) params["lon"] = float(parts[1].strip()) except ValueError: # Not valid coordinates, treat as city name params["q"] = location else: # City name params["q"] = location # Make API request try: response = requests.get(self.base_url, params=params, timeout=10) self._record_request() if response.status_code == 200: return response.json() elif response.status_code == 401: raise ValueError("Invalid API key. Check OPENWEATHERMAP_API_KEY environment variable.") elif response.status_code == 404: raise ValueError(f"Location '{location}' not found. Please check the location name.") elif response.status_code == 429: raise ValueError("API rate limit exceeded. Please try again later.") else: raise ValueError(f"Weather API error: {response.status_code} - {response.text}") except requests.exceptions.Timeout: raise ValueError("Weather API request timed out. Please try again.") except requests.exceptions.RequestException as e: raise ValueError(f"Failed to fetch weather data: {str(e)}") def _format_weather(self, data: Dict[str, Any], units: str) -> str: """Format weather data into human-readable string.""" main = data.get("main", {}) weather = data.get("weather", [{}])[0] wind = data.get("wind", {}) name = data.get("name", "Unknown") country = data.get("sys", {}).get("country", "") # Temperature unit symbols temp_unit = "°C" if units == "metric" else "°F" if units == "imperial" else "K" speed_unit = "m/s" if units == "metric" else "mph" if units == "imperial" else "m/s" # Build description description = weather.get("description", "unknown conditions").title() temp = main.get("temp", 0) feels_like = main.get("feels_like", temp) humidity = main.get("humidity", 0) pressure = main.get("pressure", 0) wind_speed = wind.get("speed", 0) location_str = f"{name}" if country: location_str += f", {country}" result = f"Weather in {location_str}:\n" result += f" {description}, {temp:.1f}{temp_unit} (feels like {feels_like:.1f}{temp_unit})\n" result += f" Humidity: {humidity}%\n" result += f" Pressure: {pressure} hPa\n" result += f" Wind: {wind_speed:.1f} {speed_unit}" return result def execute(self, arguments: Dict[str, Any]) -> str: """ Execute weather tool. Args: arguments: Dictionary with 'location' and optional 'units' Returns: Formatted weather information string """ location = arguments.get("location", "").strip() if not location: raise ValueError("Missing required argument: location") units = arguments.get("units", "metric") if units not in ["metric", "imperial", "kelvin"]: units = "metric" try: data = self._get_weather(location, units) return self._format_weather(data, units) except ValueError as e: # Re-raise ValueError (user errors) raise except Exception as e: # Wrap other exceptions raise ValueError(f"Weather lookup failed: {str(e)}")