From 304affb8374124216357d6d910e61b9980ca2eb7 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 20:59:06 -0400 Subject: [PATCH] =?UTF-8?q?feat(briefing):=20daily=20planning=20=E2=80=94?= =?UTF-8?q?=20prioritized=20focus=20list=20and=20calendar=20gap=20analysis?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/briefing_pipeline.py | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 0d2988d..19f3be4 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -182,6 +182,7 @@ async def _gather_internal(user_id: int) -> dict: # Calendar events today — internal store calendar_events: list[str] = [] + internal_events: list[dict] = [] try: from fabledassistant.services.events import list_events as list_internal_events today_date = datetime.now(user_tz).date() @@ -225,6 +226,51 @@ async def _gather_internal(user_id: int) -> dict: except Exception: logger.warning("Failed to gather projects for briefing", exc_info=True) + # Build prioritized "Your Day" focus list + # Priority: overdue > due today > high priority in-progress > other in-progress + focus_tasks: list[str] = [] + seen_titles: set[str] = set() + for t in sorted(all_tasks, key=lambda x: ( + 0 if (x.get("due_date") and x["due_date"] < today and x.get("status") not in ("done", "cancelled")) else + 1 if (x.get("due_date") == today and x.get("status") not in ("done", "cancelled")) else + 2 if (x.get("priority") == "high" and x.get("status") not in ("done", "cancelled")) else + 3 if (x.get("status") == "in_progress") else 4 + )): + if t.get("status") in ("done", "cancelled"): + continue + formatted = format_task(t, today) + if formatted not in seen_titles: + seen_titles.add(formatted) + focus_tasks.append(formatted) + if len(focus_tasks) >= 5: + break + + # Calendar gap analysis — extract event times for LLM + event_time_blocks: list[str] = [] + for e in internal_events: + try: + if e.get("all_day"): + continue + start = e.get("start_dt") + end = e.get("end_dt") + if not start: + continue + from datetime import datetime as _dt + if isinstance(start, str): + start = _dt.fromisoformat(start) + if isinstance(end, str): + end = _dt.fromisoformat(end) + s_local = start.astimezone(user_tz) if start.tzinfo else start.replace(tzinfo=timezone.utc).astimezone(user_tz) + s_str = s_local.strftime("%-I:%M %p") + if end: + e_local = end.astimezone(user_tz) if end.tzinfo else end.replace(tzinfo=timezone.utc).astimezone(user_tz) + e_str = e_local.strftime("%-I:%M %p") + event_time_blocks.append(f"{e.get('title', 'Event')}: {s_str}–{e_str}") + else: + event_time_blocks.append(f"{e.get('title', 'Event')}: {s_str}–?") + except Exception: + pass + return { "date": today, "overdue_tasks": overdue, @@ -233,6 +279,8 @@ async def _gather_internal(user_id: int) -> dict: "calendar_events": calendar_events, "active_projects": projects_summary, "all_tasks_raw": all_tasks, + "focus_tasks": focus_tasks, + "event_time_blocks": event_time_blocks, } @@ -341,6 +389,18 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te lines.extend(f" - {t}" for t in internal_data["high_priority"]) lines.append("") + # Your Day — prioritized focus + calendar gaps (compilation slot only) + if internal_data.get("focus_tasks") and slot == "compilation": + lines.append("YOUR DAY — top tasks to focus on (mention these as a suggested plan):") + for i, t in enumerate(internal_data["focus_tasks"], 1): + lines.append(f" {i}. {t}") + if internal_data.get("event_time_blocks"): + lines.append("") + lines.append(" Scheduled time blocks:") + lines.extend(f" {b}" for b in internal_data["event_time_blocks"]) + lines.append(" (Identify free blocks between events for focused work.)") + lines.append("") + # News highlights (top 3 with excerpts — right panel shows full list) rss = external_data.get("rss_items") or [] if rss: