Personalized brief: refill to full count when a boundary hides a highlight

When a reader's boundary (avoid-term, muted topic/flavor, pause) removes a brief
item, top the highlights back up with other readable, boundary-respecting good
news instead of showing fewer cards — so 'Highlights from Today' stays full and
still honors what they don't want to see. (Reverses the earlier filter-down-only
MVP, now that the count is fixed at seven.)

- /api/brief: after filtering by prefs, refill from the accepted pool (same
  categorical SQL filters + avoid-terms) excluding already-shown items.
- Shared _prefs_sql_kw helper for feed/replacement/brief filters.
- Tests: refill stays full and respects mute + avoid-terms (89 total).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jay
2026-05-31 13:13:54 +00:00
parent e26831473c
commit 803da64e16
2 changed files with 81 additions and 6 deletions
+31 -6
View File
@@ -57,6 +57,18 @@ def get_conn():
conn.close()
def _prefs_sql_kw(fp, now) -> dict:
"""Categorical prefs → queries.feed keyword filters (avoid-terms stay Python)."""
return dict(
include_topics=fp.include_topics or None,
include_flavors=fp.include_flavors or None,
mute_topics=list(fp.muted_topics(now)) or None,
mute_flavors=list(fp.muted_flavors(now)) or None,
max_cortisol=fp.max_cortisol,
max_ragebait=fp.max_ragebait,
)
def _pick_lead(items: list[dict]) -> list[dict]:
"""Lead with a gentle, readable, ideally illustrated story.
@@ -303,15 +315,28 @@ def create_app() -> FastAPI:
prefs: str | None = Query(None),
) -> BriefResponse:
fp = prefs_from_json(prefs)
now = datetime.now(timezone.utc)
with get_conn() as conn:
data = queries.brief(conn, brief_date=date, limit=limit)
items = data["items"]
if not fp.is_empty():
# MVP: filter the stored brief DOWN; no refill from outside the brief.
# Runs before hero selection, so personal avoid-terms take precedence.
items = filter_articles(items, fp, datetime.now(timezone.utc))
items = data["items"]
if not fp.is_empty():
# Apply personal boundaries (avoid-terms take precedence over curation).
items = filter_articles(items, fp, now)
# Keep the highlights full: if a boundary hid a story, top up with
# other readable, boundary-respecting good news rather than show fewer.
if len(items) < limit:
have = {a["id"] for a in items}
pool = queries.feed(
conn, accepted_only=True, limit=limit * 5 + 20, offset=0, **_prefs_sql_kw(fp, now)
)
for a in filter_articles(pool, fp, now):
if len(items) >= limit:
break
if a["id"] not in have:
items.append(a)
have.add(a["id"])
# Lead with a gentle, readable story (charged or paywalled stories stay
# in the five, just not as the first thing seen).
# in the set, just not as the first thing seen).
items = _pick_lead(items)
return BriefResponse(
brief_date=data["brief_date"],