From 2e3f90384d7497bafcc14f51a5a89b7eb6d5f8c2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 20:33:16 -0400 Subject: [PATCH] =?UTF-8?q?feat(briefing):=20actionable=20intelligence=20?= =?UTF-8?q?=E2=80=94=20days=20overdue,=20project=20context,=20suggest=20ne?= =?UTF-8?q?xt=20steps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/briefing_pipeline.py | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 95f33b7..0d2988d 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -23,14 +23,25 @@ SLOT_NAMES = ("compilation", "morning", "midday", "afternoon") -def format_task(task: dict) -> str: +def format_task(task: dict, today: str | None = None) -> str: parts = [task.get("title", "Untitled")] if task.get("status"): parts.append(f"[{task['status'].replace('_', ' ').title()}]") if task.get("due_date"): parts.append(f"due {task['due_date']}") + if today and task["due_date"] < today and task.get("status") not in ("done", "cancelled"): + from datetime import date + try: + due = date.fromisoformat(task["due_date"]) + td = date.fromisoformat(today) + days = (td - due).days + parts.append(f"({days} day{'s' if days != 1 else ''} overdue)") + except ValueError: + pass if task.get("priority") and task["priority"] not in ("none", ""): parts.append(f"priority: {task['priority']}") + if task.get("project_title"): + parts.append(f"project: {task['project_title']}") return " — ".join(parts) @@ -134,6 +145,13 @@ async def _gather_internal(user_id: int) -> dict: all_tasks: list[dict] = [] try: all_task_objs, _total = await list_notes(user_id, is_task=True, limit=100) + # Build project title lookup for enrichment + project_titles: dict[int, str] = {} + try: + for p in await list_projects(user_id): + project_titles[p.id] = p.title or "Untitled" + except Exception: + pass all_tasks = [ { "task_id": t.id, @@ -141,21 +159,22 @@ async def _gather_internal(user_id: int) -> dict: "status": t.status, "due_date": t.due_date.isoformat() if t.due_date else None, "priority": t.priority, + "project_title": project_titles.get(t.project_id) if t.project_id else None, } for t in all_task_objs ] overdue = [ - format_task(t) for t in all_tasks - if t.get("due_date") and t["due_date"] < today and t.get("status") != "done" + format_task(t, today) for t in all_tasks + if t.get("due_date") and t["due_date"] < today and t.get("status") not in ("done", "cancelled") ] due_today = [ - format_task(t) for t in all_tasks - if t.get("due_date") == today and t.get("status") != "done" + format_task(t, today) for t in all_tasks + if t.get("due_date") == today and t.get("status") not in ("done", "cancelled") ] high_priority = [ - format_task(t) for t in all_tasks - if t.get("priority") == "high" and t.get("status") not in ("done",) and - format_task(t) not in overdue and format_task(t) not in due_today + format_task(t, today) for t in all_tasks + if t.get("priority") == "high" and t.get("status") not in ("done", "cancelled") and + format_task(t, today) not in overdue and format_task(t, today) not in due_today ][:5] except Exception: logger.warning("Failed to gather tasks for briefing", exc_info=True) @@ -267,8 +286,11 @@ def _unified_system_prompt(profile_body: str) -> str: "Weave together what matters today: mention the weather in a sentence, note any calendar " "events or tasks due today, and briefly reference one or two noteworthy news stories. " "Only mention projects if a task from one is specifically due today. " - "Be warm, concise, and human — aim for 3 to 5 sentences. " - "Future context like emails and messages will be added over time — keep the tone open and helpful.\n\n" + "Be warm, concise, and human — aim for 4 to 8 sentences.\n\n" + "IMPORTANT: Be actionable, not just informational. For overdue or due-today tasks, " + "suggest a concrete next step — offer to reschedule, mark in progress, or break it down. " + "For upcoming events, mention if preparation might help. " + "The user can reply to act on your suggestions via tool calls.\n\n" + (f"User profile:\n{profile_body}\n" if profile_body else "") ) @@ -300,17 +322,23 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te # Tasks due today if internal_data.get("due_today"): - lines.append("DUE TODAY:") + lines.append("DUE TODAY (suggest next steps — offer to mark in progress or help prioritize):") lines.extend(f" - {t}" for t in internal_data["due_today"]) lines.append("") - # Overdue tasks (brief mention only) + # Overdue tasks if internal_data.get("overdue_tasks"): overdue = internal_data["overdue_tasks"] - lines.append(f"OVERDUE ({len(overdue)} task{'s' if len(overdue) != 1 else ''}):") - lines.extend(f" - {t}" for t in overdue[:3]) - if len(overdue) > 3: - lines.append(f" (and {len(overdue) - 3} more)") + lines.append(f"OVERDUE ({len(overdue)} task{'s' if len(overdue) != 1 else ''}) — suggest rescheduling or breaking down:") + lines.extend(f" - {t}" for t in overdue[:5]) + if len(overdue) > 5: + lines.append(f" (and {len(overdue) - 5} more)") + lines.append("") + + # High priority in-progress + if internal_data.get("high_priority"): + lines.append("HIGH PRIORITY:") + lines.extend(f" - {t}" for t in internal_data["high_priority"]) lines.append("") # News highlights (top 3 with excerpts — right panel shows full list)