From 9f6608fc8fe02ad80812e4fc306a207bd41c8de2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 11 Apr 2026 13:32:52 -0400 Subject: [PATCH] fix(briefing): stale weather auto-refresh + task filter + overdue prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three briefing quality fixes surfaced by reading today's 2026-04-11 compilation output: - **Stale weather**: get_weather was returning 48h-old cache data after a missed scheduler run. Tool now auto-refreshes any cached location older than 6h (fetching fresh data from Open-Meteo), and stamps each location with cache_age_hours + is_stale so the model can hedge instead of faithfully relaying old numbers. - **Cancelled tasks leaking into prose**: briefing loop now defaults list_tasks calls to status=["todo","in_progress"] when the model doesn't specify, so cancelled/done items stop showing up in the summary. Localized to the briefing path — chat still sees full history. - **Overdue in-progress tasks missed by midday check-in**: tightened the check-in prompt to explicitly require two list_tasks calls — one for in_progress (catches items dragging past their due date) and one filtered by due date — so long-running tasks stop getting silently dropped. --- .../services/briefing_pipeline.py | 10 +++ src/fabledassistant/services/tools.py | 67 ++++++++++++++++--- 2 files changed, 69 insertions(+), 8 deletions(-) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 30b0965..02cd6ca 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -106,6 +106,10 @@ def _agentic_system_prompt( f"You are the user's personal assistant giving a brief {slot} check-in. " "Use tools to see what's changed since this morning. Focus on progress and " "what's still unaddressed.\n\n" + "When checking tasks, call list_tasks at least twice:\n" + "- once with status=\"in_progress\" to see anything already being worked on " + "(regardless of due date — these can quietly drag past their due dates)\n" + "- once filtered by due date for what's coming up or overdue today\n\n" "Rules:\n" "- Call tools to see current state. Never assert facts without tool results.\n" "- If nothing meaningful has changed, say so briefly — don't invent progress.\n" @@ -239,6 +243,12 @@ async def run_agentic_briefing( except Exception: arguments = {} + # Default list_tasks to active statuses only so cancelled/done + # items don't slip into briefing prose. The model can still + # pass an explicit status filter when it wants something else. + if tool_name == "list_tasks" and not arguments.get("status"): + arguments["status"] = ["todo", "in_progress"] + try: if tool_name == "get_rss_items" and rss_override is not None: # Use topic-scored/filtered items already computed by diff --git a/src/fabledassistant/services/tools.py b/src/fabledassistant/services/tools.py index f0e32b8..aef2357 100644 --- a/src/fabledassistant/services/tools.py +++ b/src/fabledassistant/services/tools.py @@ -2132,10 +2132,12 @@ async def execute_tool( elif tool_name == "get_weather": from fabledassistant.services.weather import ( - get_cached_weather, geocode, _fetch_open_meteo, parse_forecast + get_cached_weather, geocode, _fetch_open_meteo, parse_forecast, + refresh_location_cache, ) from fabledassistant.services.briefing_pipeline import _get_temp_unit - from datetime import timezone as _tz + from datetime import timezone as _tz, timedelta as _td + import json as _wx_json 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) @@ -2167,15 +2169,64 @@ async def execute_tool( }]}} except Exception as e: return {"success": False, "error": f"Could not get weather for '{location}': {e}"} - # Fall back to cached configured locations + # Fall back to cached configured locations. Auto-refresh any + # row older than 6 hours so stale forecasts don't slip into + # briefings after an outage or missed scheduler run. 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()] - locations = [ - {**loc, "days": _apply_unit(loc.get("days", []))[:num_days], "temp_unit": temp_unit} - for loc in locations - ] - return {"success": True, "data": {"locations": locations}} + now_utc = datetime.now(_tz.utc) + stale_cutoff = now_utc - _td(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, "briefing_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) + # Mark each location with a stale flag so the model can hedge + # or skip instead of faithfully relaying old numbers. + 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}} elif tool_name == "get_rss_items": from fabledassistant.services.rss import get_recent_items