From 87625522341e80e957c328cd64eaa6a3bdd5270c Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 8 Apr 2026 22:19:38 -0400 Subject: [PATCH] =?UTF-8?q?feat(briefing):=20project=20continuity=20?= =?UTF-8?q?=E2=80=94=20yesterday's=20activity=20and=20stale=20project=20wa?= =?UTF-8?q?rnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/briefing_pipeline.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index e1f6a08..02e05f4 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -259,6 +259,45 @@ async def _gather_internal(user_id: int) -> dict: except Exception: logger.debug("Weather cross-reference failed", exc_info=True) + # Recent activity — what was the user working on yesterday? + recent_activity: list[str] = [] + stale_projects: list[str] = [] + try: + from sqlalchemy import select as _sel, func as _func + from fabledassistant.models.note import Note + from fabledassistant.models.project import Project + + yesterday = datetime.now(user_tz) - __import__('datetime').timedelta(days=1) + async with async_session() as session: + # Notes/tasks updated in last 24h, grouped by project + result = await session.execute( + _sel(Project.title, _func.count(Note.id)) + .join(Note, Note.project_id == Project.id) + .where(Note.user_id == user_id) + .where(Note.updated_at >= yesterday) + .group_by(Project.title) + .order_by(_func.count(Note.id).desc()) + .limit(3) + ) + for proj_title, count in result.all(): + recent_activity.append(f"{proj_title}: {count} item{'s' if count != 1 else ''} updated") + + # Stale projects — active projects with no note/task activity in 7+ days + week_ago = datetime.now(user_tz) - __import__('datetime').timedelta(days=7) + active_projects_q = await session.execute( + _sel(Project).where(Project.user_id == user_id, Project.status == "active") + ) + for p in active_projects_q.scalars().all(): + latest = await session.execute( + _sel(_func.max(Note.updated_at)) + .where(Note.project_id == p.id, Note.user_id == user_id) + ) + last_activity = latest.scalar() + if last_activity is None or last_activity < week_ago: + stale_projects.append(p.title or "Untitled") + except Exception: + logger.debug("Failed to gather recent activity for briefing", exc_info=True) + # Projects: active projects projects_summary = [] try: @@ -324,6 +363,8 @@ async def _gather_internal(user_id: int) -> dict: "focus_tasks": focus_tasks, "event_time_blocks": event_time_blocks, "weather_warnings": weather_warnings, + "recent_activity": recent_activity, + "stale_projects": stale_projects, } @@ -600,6 +641,17 @@ def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, te lines.append(" (Identify free blocks between events for focused work.)") lines.append("") + # Project continuity — what was the user working on? + if internal_data.get("recent_activity") and slot == "compilation": + lines.append("YESTERDAY'S ACTIVITY (mention what the user was working on to help them pick up):") + lines.extend(f" - {a}" for a in internal_data["recent_activity"]) + lines.append("") + + if internal_data.get("stale_projects") and slot == "compilation": + lines.append("STALE PROJECTS (no activity in 7+ days — gently flag):") + lines.extend(f" - {p}" for p in internal_data["stale_projects"]) + lines.append("") + # News — clustered by topic for thematic synthesis rss = external_data.get("rss_items") or [] if rss: