dapr-agents/cookbook/agents/weather/tools.py

191 lines
8.4 KiB
Python

from typing import Optional
from dapr_agents import AgentTool
from datetime import datetime
import requests
import time
class WeatherForecast(AgentTool):
name: str = 'WeatherForecast'
description: str = 'A tool for retrieving the weather/temperature for a given city.'
# Default user agent
user_agent: str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15"
def handle_error(self, response: requests.Response, url: str, stage: str) -> None:
"""Handles error responses and raises a ValueError with detailed information."""
if response.status_code != 200:
raise ValueError(
f"Failed to get data during {stage}. Status: {response.status_code}. "
f"URL: {url}. Response: {response.text}"
)
if not response.json():
raise ValueError(
f"No data found during {stage}. URL: {url}. Response: {response.text}"
)
def _run(self, city: str, state: Optional[str] = None, country: Optional[str] = "usa") -> dict:
"""
Retrieves weather data by first fetching geocode data for the city and then fetching weather data.
Args:
city (str): The name of the city to get weather for.
state (Optional[str]): The two-letter state abbreviation (optional).
country (Optional[str]): The two-letter country abbreviation. Defaults to 'usa'.
Returns:
dict: A dictionary containing the city, state, country, and current temperature.
"""
headers = {
"User-Agent": self.user_agent
}
# Construct the geocode URL, conditionally including the state if it's provided
geocode_url = f"https://nominatim.openstreetmap.org/search?city={city}&country={country}"
if state:
geocode_url += f"&state={state}"
geocode_url += "&limit=1&format=jsonv2"
# Geocode request
geocode_response = requests.get(geocode_url, headers=headers)
self.handle_error(geocode_response, geocode_url, "geocode lookup")
# Add delay between requests
time.sleep(2)
geocode_data = geocode_response.json()
lat, lon = geocode_data[0]["lat"], geocode_data[0]["lon"]
# Use different APIs based on the country
if country.lower() == "usa":
# Weather.gov request for USA
weather_gov_url = f"https://api.weather.gov/points/{lat},{lon}"
weather_response = requests.get(weather_gov_url, headers=headers)
self.handle_error(weather_response, weather_gov_url, "weather lookup")
# Add delay between requests
time.sleep(2)
weather_data = weather_response.json()
forecast_url = weather_data["properties"]["forecast"]
# Forecast request
forecast_response = requests.get(forecast_url, headers=headers)
self.handle_error(forecast_response, forecast_url, "forecast lookup")
forecast_data = forecast_response.json()
today_forecast = forecast_data["properties"]["periods"][0]
# Return the weather data along with the city, state, and country
return {
"city": city,
"state": state,
"country": country,
"temperature": today_forecast["temperature"],
"unit": "Fahrenheit"
}
else:
# Met.no API for non-USA countries
met_no_url = f"https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={lat}&lon={lon}"
weather_response = requests.get(met_no_url, headers=headers)
self.handle_error(weather_response, met_no_url, "Met.no weather lookup")
weather_data = weather_response.json()
temperature_unit = weather_data["properties"]["meta"]["units"]["air_temperature"]
today_forecast = weather_data["properties"]["timeseries"][0]["data"]["instant"]["details"]["air_temperature"]
# Return the weather data along with the city, state, and country
return {
"city": city,
"state": state,
"country": country,
"temperature": today_forecast,
"unit": temperature_unit
}
class HistoricalWeather(AgentTool):
name: str = 'HistoricalWeather'
description: str = 'A tool for retrieving historical weather data (temperature) for a given city.'
# Default user agent
user_agent: str = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15"
def handle_error(self, response: requests.Response, url: str, stage: str) -> None:
"""Handles error responses and raises a ValueError with detailed information."""
if response.status_code != 200:
raise ValueError(
f"Failed to get data during {stage}. Status: {response.status_code}. "
f"URL: {url}. Response: {response.text}"
)
if not response.json():
raise ValueError(
f"No data found during {stage}. URL: {url}. Response: {response.text}"
)
def _run(self, city: str, state: Optional[str] = None, country: Optional[str] = "usa", start_date: Optional[str] = None, end_date: Optional[str] = None) -> dict:
"""
Retrieves historical weather data for the city by first fetching geocode data and then historical weather data.
Args:
city (str): The name of the city to get weather for.
state (Optional[str]): The two-letter state abbreviation (optional).
country (Optional[str]): The two-letter country abbreviation. Defaults to 'usa'.
start_date (Optional[str]): Start date for historical data (YYYY-MM-DD format).
end_date (Optional[str]): End date for historical data (YYYY-MM-DD format).
Returns:
dict: A dictionary containing the city, state, country, and historical temperature data.
"""
headers = {
"User-Agent": self.user_agent
}
# Validate dates
current_date = datetime.now().strftime('%Y-%m-%d')
if start_date >= current_date or end_date >= current_date:
raise ValueError("Both start_date and end_date must be earlier than the current date.")
if (datetime.strptime(end_date, "%Y-%m-%d") - datetime.strptime(start_date, "%Y-%m-%d")).days > 30:
raise ValueError("The time span between start_date and end_date cannot exceed 30 days.")
# Construct the geocode URL, conditionally including the state if it's provided
geocode_url = f"https://nominatim.openstreetmap.org/search?city={city}&country={country}"
if state:
geocode_url += f"&state={state}"
geocode_url += "&limit=1&format=jsonv2"
# Geocode request
geocode_response = requests.get(geocode_url, headers=headers)
self.handle_error(geocode_response, geocode_url, "geocode lookup")
# Add delay between requests
time.sleep(2)
geocode_data = geocode_response.json()
lat, lon = geocode_data[0]["lat"], geocode_data[0]["lon"]
# Historical weather request
historical_weather_url = f"https://archive-api.open-meteo.com/v1/archive?latitude={lat}&longitude={lon}&start_date={start_date}&end_date={end_date}&hourly=temperature_2m"
weather_response = requests.get(historical_weather_url, headers=headers)
self.handle_error(weather_response, historical_weather_url, "historical weather lookup")
weather_data = weather_response.json()
# Extract time and temperature data
timestamps = weather_data["hourly"]["time"]
temperatures = weather_data["hourly"]["temperature_2m"]
temperature_unit = weather_data["hourly_units"]["temperature_2m"]
# Combine timestamps and temperatures into a dictionary
temperature_data = {timestamps[i]: temperatures[i] for i in range(len(timestamps))}
# Return the structured weather data along with the city, state, country
return {
"city": city,
"state": state,
"country": country,
"start_date": start_date,
"end_date": end_date,
"temperature_data": temperature_data,
"unit": temperature_unit
}