Article pages: structured "Why it belongs" editorial read

Per Codex — make /a/<id> feel like Upbeat Bytes has editorial judgment, not just
a summary wrapper. Trust-building, short, not an essay.

* article_summaries gains what_happened / why_matters / why_belongs (+ migration).
* summarize.explain_article: a separate, fallback-able LLM pass producing three
  short notes (parsed from a labelled WHAT/MATTERS/BELONGS format). generate_summary
  now stores them alongside the summary, and tops up older summaries on next view.
  get_explanation returns them only when all three are present.
* API: share_page + /api/summary expose the explanation.
* share.py: renders the three-part section (accent rule) when complete; otherwise
  the single "Why it's here" reason line is the calm fallback. The page polls and
  swaps in both the summary and the section as they cache.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-09 20:05:26 -04:00
parent 9befbffd94
commit 337dc3f901
6 changed files with 192 additions and 21 deletions
+9 -5
View File
@@ -886,9 +886,12 @@ def create_app() -> FastAPI:
if not row or row["duplicate_of"] is not None or not row["accepted"]:
return not_found
summary = summarize.get_summary(conn, aid)
if not summary:
_kick_summary(aid, background_tasks) # generate for next time; page polls
return HTMLResponse(share.render_share_page(dict(row), PUBLIC_BASE_URL, summary=summary))
explanation = summarize.get_explanation(conn, aid)
if not summary or not explanation:
_kick_summary(aid, background_tasks) # generate/top-up for next time; page polls
return HTMLResponse(
share.render_share_page(dict(row), PUBLIC_BASE_URL, summary=summary, explanation=explanation)
)
# --- Privacy-respecting first-party analytics -------------------------
@@ -1243,10 +1246,11 @@ def create_app() -> FastAPI:
def article_summary(article_id: int, background_tasks: BackgroundTasks) -> dict:
with get_conn() as conn:
summary = summarize.get_summary(conn, article_id)
explanation = summarize.get_explanation(conn, article_id)
if summary:
return {"status": "ready", "summary": summary}
return {"status": "ready", "summary": summary, "explanation": explanation}
_kick_summary(article_id, background_tasks)
return {"status": "pending", "summary": None}
return {"status": "pending", "summary": None, "explanation": None}
@app.get("/today", response_class=HTMLResponse)
def today_digest() -> HTMLResponse: