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.
This commit is contained in:
2026-04-11 13:30:26 -04:00
parent 593d737e26
commit e63b4634ea
@@ -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 = {