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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user