| import os |
| import requests |
| from loguru import logger |
| from dotenv import load_dotenv |
|
|
| load_dotenv() |
|
|
| |
| |
| TARGET_CITIES = { |
| "New York": {"lat": 40.7128, "lon": -74.0060, "apify": "New York, New York"}, |
| "London": {"lat": 51.5074, "lon": -0.1278, "apify": "London, England"}, |
| "Miami": {"lat": 25.7617, "lon": -80.1918, "apify": "Miami, Florida"}, |
| "Tokyo": {"lat": 35.6762, "lon": 139.6503, "apify": "Tokyo, Japan"}, |
| "Los Angeles": {"lat": 34.0522, "lon": -118.2437, "apify": "Los Angeles, California"}, |
| } |
|
|
|
|
| |
| def fetch_weather_apify(cities: dict = TARGET_CITIES) -> dict: |
| """ |
| Fetch 10-day weather forecast via Apify's weather-scraper actor. |
| Returns a dict keyed by city name. |
| """ |
| token = os.getenv("APIFY_API_TOKEN") |
| if not token: |
| raise ValueError("APIFY_API_TOKEN not set in .env") |
|
|
| from apify_client import ApifyClient |
| client = ApifyClient(token) |
| locations = [info["apify"] for info in cities.values()] |
|
|
| logger.info(f"Fetching Apify weather for: {locations}") |
|
|
| run_input = { |
| "locations": locations, |
| "timeFrame": "ten_day", |
| "units": "imperial", |
| "maxItems": len(locations) * 10, |
| "proxyConfiguration": {"useApifyProxy": True}, |
| } |
|
|
| run = client.actor("epctex/weather-scraper").call(run_input=run_input) |
| items = list(client.dataset(run["defaultDatasetId"]).iterate_items()) |
|
|
| |
| results = {} |
| for city_name in cities: |
| city_data = [ |
| item for item in items |
| if cities[city_name]["apify"].split(",")[0].lower() |
| in str(item.get("city", "")).lower() |
| ] |
| if city_data: |
| results[city_name] = city_data |
| logger.success(f" β {city_name}: {len(city_data)} forecast records") |
| else: |
| logger.warning(f" β {city_name}: no data from Apify, falling back") |
| results[city_name] = _fetch_openmeteo_single(city_name, cities[city_name]) |
|
|
| return results |
|
|
|
|
| |
| def _fetch_openmeteo_single(city_name: str, city_info: dict) -> list: |
| """ |
| Free fallback: Open-Meteo gives hourly/daily temp forecasts. |
| Returns list of daily dicts matching Apify output shape. |
| """ |
| url = "https://api.open-meteo.com/v1/forecast" |
| params = { |
| "latitude": city_info["lat"], |
| "longitude": city_info["lon"], |
| "daily": "temperature_2m_max,temperature_2m_min,precipitation_sum,weathercode", |
| "temperature_unit": "fahrenheit", |
| "forecast_days": 10, |
| "timezone": "auto", |
| } |
| resp = requests.get(url, params=params, timeout=10) |
| resp.raise_for_status() |
| data = resp.json()["daily"] |
|
|
| records = [] |
| for i, date in enumerate(data["time"]): |
| records.append({ |
| "city": city_name, |
| "date": date, |
| "temperature": f"{data['temperature_2m_max'][i]}/{data['temperature_2m_min'][i]}", |
| "high_f": data["temperature_2m_max"][i], |
| "low_f": data["temperature_2m_min"][i], |
| "precipitation": data["precipitation_sum"][i], |
| "source": "open-meteo", |
| }) |
|
|
| logger.info(f" Open-Meteo fallback: {city_name} β {len(records)} days") |
| return records |
|
|
|
|
| def fetch_weather_openmeteo(cities: dict = TARGET_CITIES) -> dict: |
| """Fetch all cities directly from Open-Meteo (no Apify needed).""" |
| results = {} |
| for city_name, info in cities.items(): |
| results[city_name] = _fetch_openmeteo_single(city_name, info) |
| return results |
|
|
|
|
| def get_today_high(city_name: str, weather_data: dict) -> float | None: |
| """Extract today's high temperature for a city from fetched weather data.""" |
| records = weather_data.get(city_name, []) |
| if not records: |
| return None |
|
|
| today = records[0] |
|
|
| |
| if isinstance(today.get("temperature"), str) and "/" in today["temperature"]: |
| try: |
| return float(today["temperature"].split("/")[0]) |
| except ValueError: |
| pass |
|
|
| |
| return today.get("high_f") |
|
|
|
|
| def summarize_weather(weather_data: dict) -> str: |
| """ |
| Returns a human-readable weather summary string for the LLM agent prompt. |
| """ |
| lines = ["=== Current Weather Forecasts ==="] |
| for city, records in weather_data.items(): |
| if not records: |
| lines.append(f" {city}: No data available") |
| continue |
| today = records[0] |
|
|
| temp = today.get("temperature", "N/A") |
| if today.get("high_f"): |
| temp = f"{today['high_f']}Β°F high / {today.get('low_f', '?')}Β°F low" |
|
|
| forecast = today.get("forecast", today.get("weathercode", "N/A")) |
| lines.append(f" {city}: {temp} | Conditions: {forecast}") |
|
|
| return "\n".join(lines) |
|
|