docrag/tools/weather_tool.py
Z User b811162f78 Implement tool calling loop for LLM
- Pass all registered tools to LLM during chat completion
- Handle tool_calls from LLM response
- Execute tools and feed results back to LLM
- Loop until LLM returns final response
- Updated system prompt to encourage tool use
- Updated streaming to handle tool calls
- Increased MAX_TOOL_ITERATIONS to 5
2026-03-29 16:07:56 +00:00

421 lines
14 KiB
Python
Executable File

"""
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"],
},
},
}