"""Weather tool.""" from __future__ import annotations import json as _wx_json import logging from datetime import datetime, timedelta, timezone from fabledassistant.services.tools._registry import tool logger = logging.getLogger(__name__) @tool( name="get_weather", description=( "Get the weather forecast. By default returns today only — use days=7 when the user " "explicitly asks for a multi-day forecast or 'this week'. " "Pass a city/region name to look up any location, or omit to get the user's configured home/work locations." ), parameters={ "location": {"type": "string", "description": "City/region to look up, or 'home'/'work' for saved locations. Omit for all saved."}, "days": {"type": "integer", "description": "Number of days to return (1–8). Default 1 (today only). Use 7 or 8 when the user asks for a weekly forecast or multiple days."}, }, read_only=True, briefing=True, ) async def get_weather_tool(*, user_id, arguments, **_ctx): from fabledassistant.services.weather import ( _fetch_open_meteo, geocode, get_cached_weather, get_temp_unit, parse_forecast, refresh_location_cache, ) location = (arguments.get("location") or "").strip() num_days = max(1, min(8, int(arguments.get("days") or 1))) temp_unit = await get_temp_unit(user_id) def _apply_unit(days: list[dict]) -> list[dict]: if temp_unit != "F": return days def _c_to_f(v: float | None) -> float | None: return round(v * 9 / 5 + 32, 1) if v is not None else None return [ {**d, "temp_max": _c_to_f(d.get("temp_max")), "temp_min": _c_to_f(d.get("temp_min"))} for d in days ] _known = {"home", "work", "all", ""} if location.lower() not in _known: try: lat, lon, label = await geocode(location) raw = await _fetch_open_meteo(lat, lon) days = _apply_unit(parse_forecast(raw))[:num_days] return {"success": True, "data": {"locations": [{ "location_key": "query", "location_label": label, "fetched_at": datetime.now(timezone.utc).isoformat(), "days": days, "temp_unit": temp_unit, "changes_since_last_fetch": [], }]}} except Exception as e: return {"success": False, "error": f"Could not get weather for '{location}': {e}"} locations = await get_cached_weather(user_id) if location.lower() not in ("all", ""): locations = [loc for loc in locations if loc["location_key"] == location.lower()] now_utc = datetime.now(timezone.utc) stale_cutoff = now_utc - timedelta(hours=6) stale_keys = [] for loc in locations: try: fetched = datetime.fromisoformat(loc.get("fetched_at", "")) except Exception: fetched = None if fetched is None or fetched < stale_cutoff: stale_keys.append(loc["location_key"]) if stale_keys: try: from fabledassistant.services.settings import get_setting as _wx_get_setting raw_cfg = await _wx_get_setting(user_id, "journal_config", "{}") cfg = _wx_json.loads(raw_cfg) if isinstance(raw_cfg, str) else (raw_cfg or {}) cfg_locs = cfg.get("locations", {}) if isinstance(cfg, dict) else {} for key in stale_keys: meta = cfg_locs.get(key) or {} if meta.get("lat") and meta.get("lon"): try: await refresh_location_cache( user_id=user_id, location_key=key, location_label=meta.get("label", key), lat=meta["lat"], lon=meta["lon"], ) except Exception: logger.debug("Weather refresh failed for %s", key, exc_info=True) locations = await get_cached_weather(user_id) if location.lower() not in ("all", ""): locations = [loc for loc in locations if loc["location_key"] == location.lower()] except Exception: logger.debug("Weather staleness refresh failed", exc_info=True) stamped_locations = [] for loc in locations: days = _apply_unit(loc.get("days", []))[:num_days] try: fetched = datetime.fromisoformat(loc.get("fetched_at", "")) age_h = (now_utc - fetched).total_seconds() / 3600.0 except Exception: age_h = None stamped_locations.append({ **loc, "days": days, "temp_unit": temp_unit, "cache_age_hours": round(age_h, 1) if age_h is not None else None, "is_stale": age_h is not None and age_h > 12.0, }) return {"success": True, "data": {"locations": stamped_locations}}