Tools added: - Wikipedia: search, get article, get full article - News: Hacker News, Reddit, aggregated news search - Finance: stocks (yfinance), crypto (CoinGecko), exchange rates - Medical: PubMed, FDA, disease data, health topics - Weather: current, forecast, air quality (Open-Meteo) - Science: arXiv, Semantic Scholar, DOAJ - Web: DuckDuckGo search, instant answers, page content All tools use completely free APIs with no authentication required.
421 lines
14 KiB
Python
421 lines
14 KiB
Python
"""
|
|
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"],
|
|
},
|
|
},
|
|
}
|