From e63b4634ea21af3d25e1a2edce7a242b6476f332 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 11 Apr 2026 13:30:26 -0400 Subject: [PATCH] feat(briefing): surface news items in compilation via get_rss_items The compilation prompt mentioned "news themes" but didn't name the tool, and the model was never calling get_rss_items. Result: today's briefing had zero news coverage despite the tool being wired up and in the allowlist. - Explicitly list the tools to call in the compilation prompt so get_rss_items gets invoked alongside list_tasks/list_events/get_weather. - When the model calls get_rss_items during a compilation run, intercept and return the already-scored/filtered items (topic prefs + reaction-weighted) instead of the raw feed dump execute_tool would return. Aligns the model's view of news with the sidebar's rss_item_ids metadata. --- .../services/briefing_pipeline.py | 40 +++++++++++++++---- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/fabledassistant/services/briefing_pipeline.py b/src/fabledassistant/services/briefing_pipeline.py index 26e4206..30b0965 100644 --- a/src/fabledassistant/services/briefing_pipeline.py +++ b/src/fabledassistant/services/briefing_pipeline.py @@ -73,13 +73,18 @@ def _agentic_system_prompt( if slot == "compilation": base = ( "You are the user's personal assistant giving their full morning briefing. " - "Use the tools available to see what's actually relevant today — tasks due, " - "overdue items, events on the calendar, weather, news themes, project state — " - "and weave it into a warm, natural-sounding summary.\n\n" + "Weave real data from tool calls into a warm, natural-sounding summary.\n\n" + "Tools to call every compilation (skip only if you already know a category is empty):\n" + "- list_tasks — what's due today, overdue, or in progress\n" + "- list_events — what's on the calendar today\n" + "- get_weather — today's forecast\n" + "- get_rss_items — recent news/blog items from the user's feeds\n" + "- list_projects (optional) — active project context for narrative continuity\n\n" "Rules:\n" "- Call tools to see the data. Never assert facts you didn't learn from a tool.\n" - "- If a tool returns nothing (no events today, no overdue tasks), say so honestly. " - "Don't fabricate items to fill space.\n" + "- If a tool returns nothing (no events today, no overdue tasks, no news items), " + "say so honestly. Don't fabricate items to fill space.\n" + "- For news, pick one or two items worth mentioning — surface the theme, not a laundry list.\n" "- Write flowing prose. No markdown, no headers, no bullet points.\n" "- Aim for 6 to 10 sentences. Skip topics that have nothing interesting.\n" "- Close on one or two concrete, actionable suggestions.\n\n" @@ -131,6 +136,7 @@ async def run_agentic_briefing( slot: str, model: str, conv_id: int | None = None, + rss_override: list[dict] | None = None, ) -> tuple[str, list[dict]]: """ Run the agentic briefing loop for a user and slot. @@ -234,7 +240,27 @@ async def run_agentic_briefing( arguments = {} try: - result = await execute_tool(user_id, tool_name, arguments, conv_id=conv_id) + if tool_name == "get_rss_items" and rss_override is not None: + # Use topic-scored/filtered items already computed by + # the briefing pipeline rather than the raw feed dump + # that execute_tool would return. Keeps the model's + # view of news aligned with the user's topic prefs + # and the sidebar's rss_item_ids metadata. + slim = [ + { + "id": item.get("id"), + "title": item.get("title", ""), + "url": item.get("url", ""), + "source": item.get("feed_title", ""), + "summary": (item.get("content") or "")[:400], + "published_at": item.get("published_at"), + "topics": item.get("topics") or [], + } + for item in rss_override + ] + result = {"success": True, "data": {"items": slim, "count": len(slim)}} + else: + result = await execute_tool(user_id, tool_name, arguments, conv_id=conv_id) except Exception as exc: logger.warning( "Tool %s failed during agentic briefing: %s", tool_name, exc, @@ -338,7 +364,7 @@ async def run_compilation( weather_card = parse_weather_card_data(weather_rows[0], temp_unit) if weather_rows else None briefing_text, agentic_messages = await run_agentic_briefing( - user_id, slot, model, conv_id=None, + user_id, slot, model, conv_id=None, rss_override=filtered_rss, ) metadata: dict = {