feat: rewrite briefing pipeline to conversational prose

Replace two-pass structured LLM synthesis (## Your Day / ## The World
sections with bullets and formatted news cards) with a single
conversational pass. The new prompt instructs the model to write 3-5
flowing sentences covering weather, today's tasks/events, and 1-2 news
highlights — no markdown, no headers, no lists. Full news detail stays
in the right panel; weather detail stays in the weather card.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 13:37:47 -04:00
parent 2a8c0cfa56
commit 9f3b9e45c6
+66 -103
View File
@@ -7,7 +7,7 @@ Slot names: 'compilation' (4am), 'morning' (8am), 'midday' (12pm), 'afternoon' (
import asyncio
import hashlib
import logging
from datetime import date, datetime, timezone
from datetime import datetime, timezone
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
import httpx
@@ -22,14 +22,6 @@ logger = logging.getLogger(__name__)
SLOT_NAMES = ("compilation", "morning", "midday", "afternoon")
def slot_greeting(slot: str) -> str:
return {
"compilation": "Good morning",
"morning": "Good morning — you're at the office",
"midday": "Midday check-in",
"afternoon": "End of day wrap-up",
}.get(slot, "Update")
def format_task(task: dict) -> str:
parts = [task.get("title", "Untitled")]
@@ -263,57 +255,69 @@ async def _llm_synthesise(system_prompt: str, user_prompt: str, model: str) -> s
return ""
def _internal_system_prompt(profile_body: str) -> str:
def _unified_system_prompt(profile_body: str) -> str:
return (
"You are a personal briefing assistant. Your job is to give the user a clear, "
"concise summary of their internal workload: tasks, calendar, and projects. "
"Be direct and prioritised — lead with what's urgent. Use plain text with light "
"markdown. Do not include weather or news.\n\n"
"You are a personal assistant delivering a daily briefing. "
"Speak naturally and conversationally — as if talking to the user, not writing a report. "
"Use no markdown: no headers, no bullet points, no bold, no lists. Write in flowing prose. "
"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"
+ (f"User profile:\n{profile_body}\n" if profile_body else "")
)
def _external_system_prompt() -> str:
return (
"You are a briefing assistant for external information. Your job is to present "
"selected news items and summarise any remaining RSS content. "
"IMPORTANT: Weather is handled separately — do NOT include any weather section.\n\n"
"Format each news item EXACTLY as:\n"
"**[Headline text](source_url)**\n"
"*Outlet Name · Day Month*\n"
"One or two sentence summary.\n\n"
"Present news items in the EXACT ORDER they are provided. Do not reorder them. "
"After the news cards, add a brief paragraph for any remaining context."
)
def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, temp_unit: str = "C") -> str:
lines = [f"Date: {internal_data['date']}", f"Slot: {slot}", ""]
# Weather (brief — card handles detail)
weather = external_data.get("weather") or []
if weather:
loc = weather[0]
days = loc.get("days") or []
if days:
d = days[0]
t_min = _format_temp(d["temp_min"], temp_unit)
t_max = _format_temp(d["temp_max"], temp_unit)
unit_sym = f"°{temp_unit}"
lines.append(
f"WEATHER: {loc['location_label']}{d['description']}, "
f"{t_min}{t_max}{unit_sym}"
)
lines.append("")
def _internal_user_prompt(data: dict, slot: str) -> str:
lines = [f"Briefing slot: {slot}", f"Date: {data['date']}", ""]
if data.get("unchanged_task_count", 0) > 0:
lines.append(
f"({data['unchanged_task_count']} tasks are unchanged since the last briefing "
"— acknowledge briefly, do not list them.)"
)
# Today's calendar events
if internal_data.get("calendar_events"):
lines.append("TODAY'S EVENTS:")
lines.extend(f" - {e}" for e in internal_data["calendar_events"])
lines.append("")
changed = data.get("changed_tasks") or data.get("overdue_tasks", [])
if changed:
lines.append(f"CHANGED/NEW TASKS ({len(changed)}):")
lines.extend(f" - {t}" for t in changed)
# Tasks due today
if internal_data.get("due_today"):
lines.append("DUE TODAY:")
lines.extend(f" - {t}" for t in internal_data["due_today"])
lines.append("")
if data.get("due_today"):
lines.append(f"DUE TODAY ({len(data['due_today'])}):")
lines.extend(f" - {t}" for t in data["due_today"])
# Overdue tasks (brief mention only)
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("")
if data.get("high_priority"):
lines.append("HIGH PRIORITY (in progress):")
lines.extend(f" - {t}" for t in data["high_priority"])
# News highlights (top 3 — right panel shows full list)
rss = external_data.get("rss_items") or []
if rss:
lines.append("NEWS HIGHLIGHTS (mention 1-2 briefly, the full list is shown separately):")
for item in rss[:3]:
source = item.get("feed_title") or item.get("source") or "News"
lines.append(f" [{source}] {item.get('title', '')}")
lines.append("")
if data["calendar_events"]:
lines.append("CALENDAR TODAY:")
lines.extend(f" - {e}" for e in data["calendar_events"])
lines.append("")
if data["active_projects"]:
lines.append(f"ACTIVE PROJECTS: {', '.join(data['active_projects'])}")
return "\n".join(lines)
@@ -324,31 +328,6 @@ def _format_temp(value: float, unit: str) -> str:
return f"{value:.0f}"
def _external_user_prompt(data: dict, slot: str, temp_unit: str = "C") -> str:
unit_sym = f"°{temp_unit}"
lines = [f"Briefing slot: {slot}", ""]
if data["weather"]:
lines.append("WEATHER:")
for loc in data["weather"]:
lines.append(f" {loc['location_label']}:")
for day in loc["days"][:3]:
t_min = _format_temp(day["temp_min"], temp_unit)
t_max = _format_temp(day["temp_max"], temp_unit)
lines.append(
f" {day['date']}: {day['description']}, "
f"{t_min}{t_max}{unit_sym}, {day['precip_mm']}mm rain"
)
if loc["changes_since_last_fetch"]:
lines.append(" FORECAST CHANGES:")
lines.extend(f" - {c}" for c in loc["changes_since_last_fetch"])
lines.append("")
if data["rss_items"]:
lines.append(f"RSS DIGEST ({len(data['rss_items'])} items):")
for item in data["rss_items"][:15]:
lines.append(f" [{item.get('feed_title', 'Feed')}] {item['title']}")
if item.get("content"):
lines.append(f" {item['content'][:200]}")
return "\n".join(lines)
# ── Main entry point ───────────────────────────────────────────────────────────
@@ -434,7 +413,6 @@ async def run_compilation(
# ── LLM Synthesis ──────────────────────────────────────────────────────────
# Build filtered internal data with only changed tasks
today = internal_data["date"]
internal_data_filtered = dict(internal_data)
internal_data_filtered["unchanged_task_count"] = unchanged_count
internal_data_filtered["changed_tasks"] = [format_task(t) for t in changed_tasks]
@@ -445,17 +423,10 @@ async def run_compilation(
"weather": [],
}
internal_text, external_text = await asyncio.gather(
_llm_synthesise(
_internal_system_prompt(profile_body),
_internal_user_prompt(internal_data_filtered, slot),
model,
),
_llm_synthesise(
_external_system_prompt(),
_external_user_prompt(external_data_filtered, slot, temp_unit),
model,
),
briefing_text = await _llm_synthesise(
_unified_system_prompt(profile_body),
_unified_user_prompt(internal_data_filtered, external_data_filtered, slot, temp_unit),
model,
)
# ── Post-processing ─────────────────────────────────────────────────────────
@@ -463,18 +434,11 @@ async def run_compilation(
metadata: dict = {"rss_item_ids": rss_item_ids, "rss_items": rss_items_meta, "weather": weather_card}
if not internal_text and not external_text:
if not briefing_text:
logger.warning("Briefing compilation produced no content for user %d slot %s", user_id, slot)
return "", metadata
greeting = slot_greeting(slot)
parts = [f"**{greeting}{today}**", ""]
if internal_text:
parts += ["## Your Day", "", internal_text, ""]
if external_text:
parts += ["## The World", "", external_text]
return "\n".join(parts).strip(), metadata
return briefing_text, metadata
async def run_slot_injection(user_id: int, slot: str, model: str | None = None) -> str:
@@ -492,13 +456,12 @@ async def run_slot_injection(user_id: int, slot: str, model: str | None = None)
)
system = (
f"You are a briefing assistant providing a {slot} update. Be brief — "
"the user has already seen the morning briefing. Focus on what's changed or new."
f"You are a personal assistant giving a brief {slot} check-in. "
"The user already had their morning briefing — focus only on what's changed or newly relevant. "
"Speak naturally in 2-3 sentences, no markdown formatting, no headers or bullet points."
)
user_prompt = (
f"Slot: {slot}\n\n"
+ _internal_user_prompt(internal_data, slot)
+ "\n\n"
+ _external_user_prompt(external_data, slot, temp_unit)
return await _llm_synthesise(
system,
_unified_user_prompt(internal_data, external_data, slot, temp_unit),
model,
)
return await _llm_synthesise(system, user_prompt, model)