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 asyncio
|
||||||
import hashlib
|
import hashlib
|
||||||
import logging
|
import logging
|
||||||
from datetime import date, datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -22,14 +22,6 @@ logger = logging.getLogger(__name__)
|
|||||||
SLOT_NAMES = ("compilation", "morning", "midday", "afternoon")
|
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:
|
def format_task(task: dict) -> str:
|
||||||
parts = [task.get("title", "Untitled")]
|
parts = [task.get("title", "Untitled")]
|
||||||
@@ -263,57 +255,69 @@ async def _llm_synthesise(system_prompt: str, user_prompt: str, model: str) -> s
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _internal_system_prompt(profile_body: str) -> str:
|
def _unified_system_prompt(profile_body: str) -> str:
|
||||||
return (
|
return (
|
||||||
"You are a personal briefing assistant. Your job is to give the user a clear, "
|
"You are a personal assistant delivering a daily briefing. "
|
||||||
"concise summary of their internal workload: tasks, calendar, and projects. "
|
"Speak naturally and conversationally — as if talking to the user, not writing a report. "
|
||||||
"Be direct and prioritised — lead with what's urgent. Use plain text with light "
|
"Use no markdown: no headers, no bullet points, no bold, no lists. Write in flowing prose. "
|
||||||
"markdown. Do not include weather or news.\n\n"
|
"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 "")
|
+ (f"User profile:\n{profile_body}\n" if profile_body else "")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _external_system_prompt() -> str:
|
def _unified_user_prompt(internal_data: dict, external_data: dict, slot: str, temp_unit: str = "C") -> str:
|
||||||
return (
|
lines = [f"Date: {internal_data['date']}", f"Slot: {slot}", ""]
|
||||||
"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."
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# 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:
|
# Today's calendar events
|
||||||
lines = [f"Briefing slot: {slot}", f"Date: {data['date']}", ""]
|
if internal_data.get("calendar_events"):
|
||||||
if data.get("unchanged_task_count", 0) > 0:
|
lines.append("TODAY'S EVENTS:")
|
||||||
lines.append(
|
lines.extend(f" - {e}" for e in internal_data["calendar_events"])
|
||||||
f"({data['unchanged_task_count']} tasks are unchanged since the last briefing "
|
|
||||||
"— acknowledge briefly, do not list them.)"
|
|
||||||
)
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
changed = data.get("changed_tasks") or data.get("overdue_tasks", [])
|
|
||||||
if changed:
|
# Tasks due today
|
||||||
lines.append(f"CHANGED/NEW TASKS ({len(changed)}):")
|
if internal_data.get("due_today"):
|
||||||
lines.extend(f" - {t}" for t in changed)
|
lines.append("DUE TODAY:")
|
||||||
|
lines.extend(f" - {t}" for t in internal_data["due_today"])
|
||||||
lines.append("")
|
lines.append("")
|
||||||
if data.get("due_today"):
|
|
||||||
lines.append(f"DUE TODAY ({len(data['due_today'])}):")
|
# Overdue tasks (brief mention only)
|
||||||
lines.extend(f" - {t}" for t in data["due_today"])
|
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("")
|
lines.append("")
|
||||||
if data.get("high_priority"):
|
|
||||||
lines.append("HIGH PRIORITY (in progress):")
|
# News highlights (top 3 — right panel shows full list)
|
||||||
lines.extend(f" - {t}" for t in data["high_priority"])
|
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("")
|
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)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
@@ -324,31 +328,6 @@ def _format_temp(value: float, unit: str) -> str:
|
|||||||
return f"{value:.0f}"
|
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 ───────────────────────────────────────────────────────────
|
# ── Main entry point ───────────────────────────────────────────────────────────
|
||||||
@@ -434,7 +413,6 @@ async def run_compilation(
|
|||||||
|
|
||||||
# ── LLM Synthesis ──────────────────────────────────────────────────────────
|
# ── LLM Synthesis ──────────────────────────────────────────────────────────
|
||||||
# Build filtered internal data with only changed tasks
|
# Build filtered internal data with only changed tasks
|
||||||
today = internal_data["date"]
|
|
||||||
internal_data_filtered = dict(internal_data)
|
internal_data_filtered = dict(internal_data)
|
||||||
internal_data_filtered["unchanged_task_count"] = unchanged_count
|
internal_data_filtered["unchanged_task_count"] = unchanged_count
|
||||||
internal_data_filtered["changed_tasks"] = [format_task(t) for t in changed_tasks]
|
internal_data_filtered["changed_tasks"] = [format_task(t) for t in changed_tasks]
|
||||||
@@ -445,17 +423,10 @@ async def run_compilation(
|
|||||||
"weather": [],
|
"weather": [],
|
||||||
}
|
}
|
||||||
|
|
||||||
internal_text, external_text = await asyncio.gather(
|
briefing_text = await _llm_synthesise(
|
||||||
_llm_synthesise(
|
_unified_system_prompt(profile_body),
|
||||||
_internal_system_prompt(profile_body),
|
_unified_user_prompt(internal_data_filtered, external_data_filtered, slot, temp_unit),
|
||||||
_internal_user_prompt(internal_data_filtered, slot),
|
model,
|
||||||
model,
|
|
||||||
),
|
|
||||||
_llm_synthesise(
|
|
||||||
_external_system_prompt(),
|
|
||||||
_external_user_prompt(external_data_filtered, slot, temp_unit),
|
|
||||||
model,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Post-processing ─────────────────────────────────────────────────────────
|
# ── 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}
|
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)
|
logger.warning("Briefing compilation produced no content for user %d slot %s", user_id, slot)
|
||||||
return "", metadata
|
return "", metadata
|
||||||
|
|
||||||
greeting = slot_greeting(slot)
|
return briefing_text, metadata
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
async def run_slot_injection(user_id: int, slot: str, model: str | None = None) -> str:
|
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 = (
|
system = (
|
||||||
f"You are a briefing assistant providing a {slot} update. Be brief — "
|
f"You are a personal assistant giving a brief {slot} check-in. "
|
||||||
"the user has already seen the morning briefing. Focus on what's changed or new."
|
"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 = (
|
return await _llm_synthesise(
|
||||||
f"Slot: {slot}\n\n"
|
system,
|
||||||
+ _internal_user_prompt(internal_data, slot)
|
_unified_user_prompt(internal_data, external_data, slot, temp_unit),
|
||||||
+ "\n\n"
|
model,
|
||||||
+ _external_user_prompt(external_data, slot, temp_unit)
|
|
||||||
)
|
)
|
||||||
return await _llm_synthesise(system, user_prompt, model)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user