Skip to content

Commit 4431373

Browse files
authored
Merge pull request #359 from PyAutoLabs/claude/interest-papers-recommendations-tyc9cs
fix(arxiv): over-pick the interests batch, and widen the band it picks from
2 parents f43db19 + 6b07e42 commit 4431373

2 files changed

Lines changed: 144 additions & 23 deletions

File tree

.github/scripts/arxiv_interests.py

Lines changed: 108 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -50,23 +50,52 @@
5050
import arxiv_fetch # noqa: E402
5151

5252
#: The reading categories. astro-ph.HE carries the black-hole and transient
53-
#: work that .CO/.GA do not; .IM carries the instrumentation and methods
54-
#: papers. `cat:` matches cross-lists, so a stats-heavy paper whose primary
55-
#: category is stat.ME is still caught when it cross-lists to astro-ph.
56-
CATEGORIES = ("astro-ph.CO", "astro-ph.GA", "astro-ph.HE", "astro-ph.IM")
53+
#: work that .CO/.GA do not; .IM the instrumentation and methods papers;
54+
#: gr-qc the black-hole theory, gravitational-wave and primordial-black-hole
55+
#: work that never reaches astro-ph at all; .SR the stellar populations that
56+
#: galaxy-evolution papers are built on; stat.ME/stat.ML the inference methods
57+
#: behind the Stats bucket. `cat:` matches cross-lists, so a paper whose
58+
#: primary category is elsewhere is still caught when it cross-lists in.
59+
#:
60+
#: The last four were added 2026-08-27, on measurement rather than taste: the
61+
#: first live run announced 61 papers in a band, against a design that assumed
62+
#: several hundred. At that volume the ranker below was filtering almost
63+
#: nothing (52 of 61 scored) and CANDIDATE_CAP never bound, so the four
64+
#: astro-ph categories were leaving the shortlist stage with no work to do.
65+
#: Widening spends that headroom on coverage instead of retiring the stage.
66+
CATEGORIES = ("astro-ph.CO", "astro-ph.GA", "astro-ph.HE", "astro-ph.IM",
67+
"gr-qc", "astro-ph.SR", "stat.ME", "stat.ML")
5768
QUERY = " OR ".join(f"cat:{c}" for c in CATEGORIES)
5869

59-
#: How many papers the final list holds. The human asked for ten a day.
60-
PICK_COUNT = 10
70+
#: How many papers the final list holds — the human asked for ten a day. This
71+
#: is NOT the number the prompt is asked for; see PICK_COUNT.
72+
BATCH_SIZE = 10
73+
74+
#: How many the prompt returns, ranked most interesting first. Three more than
75+
#: land, because the append on the Memory side drops any paper already on the
76+
#: strong-lensing inbox or in the reading queue — and on the very first live
77+
#: run exactly that happened (2608.26039 was on both lists' radar), so a
78+
#: ten-pick day filed nine. The extra three are dedup slack: append takes them
79+
#: in order and stops at its own cap, so ordering is what makes this work, and
80+
#: a day with no overlap still files ten.
81+
#:
82+
#: The cap itself is deliberately NOT restated here. `interests_actions.py`
83+
#: owns it (its own BATCH_SIZE, the default of `append --limit`); duplicating
84+
#: the number across two repos is how they drift.
85+
OVERPICK = 3
86+
PICK_COUNT = BATCH_SIZE + OVERPICK
6187

6288
#: How many the prompt gets to choose from. Enough that ten good ones are
6389
#: reliably in there, small enough that one prompt can read every abstract
64-
#: closely — the whole reason for the scoring stage.
90+
#: closely — the whole reason for the scoring stage. With the widened
91+
#: categories above this cap now BINDS on an ordinary day, which is the point:
92+
#: it went from decoration back to doing the job it was written for.
6593
CANDIDATE_CAP = 60
6694

67-
#: Paging: the API caps a page at ~200 and a 3-day Monday band is several
68-
#: hundred papers. The cap is a runaway guard, not an expected limit; hitting
69-
#: it is reported rather than swallowed.
95+
#: Paging: the API caps a page at ~200. Measured 61 papers/day on the original
96+
#: four categories; the widened set should run a few hundred, and a 3-day
97+
#: Monday band several hundred more. The cap is a runaway guard, not an
98+
#: expected limit; hitting it is reported rather than swallowed.
7099
PAGE_SIZE = 200
71100
MAX_PAGES = 12
72101

@@ -129,6 +158,30 @@
129158
TITLE_WEIGHT = 3
130159
ABSTRACT_WEIGHT = 1
131160

161+
#: The categories whose papers are astronomy by default, and what that is worth
162+
#: on top of the keyword score.
163+
#:
164+
#: This exists because of what widening CATEGORIES brings in. In stat.ME and
165+
#: stat.ML, "Bayesian", "posterior", "inference" and "hierarchical model" are
166+
#: the house vocabulary — every paper there scores on the Stats terms, so a
167+
#: Bayesian method for clinical trials would out-score a lens-modelling paper
168+
#: on keywords alone and crowd it off a 60-paper shortlist. The bonus is the
169+
#: tie-breaker that keeps an astronomy shortlist astronomical: a paper from a
170+
#: home category starts ahead, and a stats paper has to genuinely out-score it
171+
#: to take a slot. gr-qc counts as home — black-hole theory and GW work is the
172+
#: reason it was added.
173+
#:
174+
#: It is a thumb on the scale, not a filter: a strong stats paper still makes
175+
#: the list, and Claude still judges everything that survives.
176+
HOME_CATEGORIES = ("astro-ph", "gr-qc")
177+
HOME_BONUS = 4
178+
179+
180+
def is_home(primary_category: str | None) -> bool:
181+
"""Whether a paper's primary category is astronomy rather than borrowed."""
182+
cat = (primary_category or "").strip()
183+
return any(cat == c or cat.startswith(c + ".") for c in HOME_CATEGORIES)
184+
132185
#: The strong-lensing net, borrowed whole from the other digest so the two
133186
#: cannot drift apart on what "strong lensing" means. Used only to FLAG.
134187
LENSING_TERMS = tuple(t.lower() for t in (arxiv_fetch._ABS + arxiv_fetch._TI))
@@ -168,12 +221,18 @@ def rank(papers: list[dict], cap: int = CANDIDATE_CAP) -> tuple[list[dict], int]
168221
"""
169222
scored = []
170223
for p in papers:
171-
total, topic, per = score(p["title"], p["abstract"])
172-
if not total:
224+
keywords, topic, per = score(p["title"], p["abstract"])
225+
if not keywords:
173226
continue
227+
# The bonus rides on top of a NON-ZERO keyword score, never instead of
228+
# one: being an astro-ph paper is a tie-breaker among papers that are
229+
# already on topic, not a way in for one that matched nothing.
230+
home = is_home(p.get("primary_category"))
174231
scored.append({**p,
175232
"topic": topic,
176-
"score": total,
233+
"score": keywords + (HOME_BONUS if home else 0),
234+
"keyword_score": keywords,
235+
"home": home,
177236
"topic_scores": per,
178237
"strong_lensing": is_lensing(p["title"], p["abstract"])})
179238
scored.sort(key=lambda p: (-p["score"], p["url"]))
@@ -264,17 +323,49 @@ def check(label, ok):
264323
check("a non-lensing paper is not flagged",
265324
not is_lensing("A quiescent galaxy at z=5", "JWST spectroscopy."))
266325

326+
check("astro-ph and gr-qc are home, stat.ML is borrowed",
327+
is_home("astro-ph.GA") and is_home("gr-qc")
328+
and not is_home("stat.ML") and not is_home(None))
329+
267330
papers = [
268331
{"title": f"Dark matter paper {i}", "abstract": "dark matter halo",
332+
"primary_category": "astro-ph.CO",
269333
"url": f"https://arxiv.org/abs/2608.{i:05d}"} for i in range(20)
270-
] + [{"title": "Unrelated", "abstract": "nothing", "url": "x"}]
334+
] + [{"title": "Unrelated", "abstract": "nothing",
335+
"primary_category": "astro-ph.GA", "url": "x"}]
271336
top, scored = rank(papers, cap=5)
272337
check(f"rank drops the unscored and caps ({len(top)} of {scored})",
273338
len(top) == 5 and scored == 20)
274339
check("rank is deterministic", rank(papers, cap=5)[0] == top)
275340
check("every candidate carries a topic and a lensing flag",
276341
all(p["topic"] and "strong_lensing" in p for p in top))
277342

343+
# The bonus is a tie-breaker among on-topic papers, never a way in.
344+
pair = [{"title": "Bayesian hierarchical inference for trial design",
345+
"abstract": "A posterior over treatment effects.",
346+
"primary_category": "stat.ME", "url": "https://arxiv.org/abs/1"},
347+
{"title": "Bayesian inference for galaxy scaling relations",
348+
"abstract": "A posterior over stellar mass.",
349+
"primary_category": "astro-ph.GA", "url": "https://arxiv.org/abs/2"}]
350+
ranked, _ = rank(pair, cap=2)
351+
check(f"an astro paper outranks an equal-scoring stats one "
352+
f"({ranked[0]['primary_category']} first)",
353+
ranked[0]["primary_category"] == "astro-ph.GA")
354+
check("the borrowed paper is kept, not filtered out", len(ranked) == 2)
355+
check("the keyword score is reported alongside the boosted one",
356+
ranked[0]["score"] == ranked[0]["keyword_score"] + HOME_BONUS
357+
and ranked[1]["score"] == ranked[1]["keyword_score"])
358+
359+
off_topic = [{"title": "A new species of Antarctic lichen",
360+
"abstract": "Nothing here is astronomy at all.",
361+
"primary_category": "astro-ph.GA", "url": "https://x/3"}]
362+
check("the bonus cannot admit a paper that matched nothing",
363+
rank(off_topic, cap=5)[0] == [])
364+
365+
check(f"the prompt over-picks for dedup slack "
366+
f"(asks {PICK_COUNT}, {BATCH_SIZE} land)",
367+
PICK_COUNT == BATCH_SIZE + OVERPICK and OVERPICK > 0)
368+
278369
print(f"selftest: {'PASS' if not failures else f'{failures} FAILURE(S)'}",
279370
file=sys.stderr)
280371
return 1 if failures else 0
@@ -308,6 +399,7 @@ def main() -> int:
308399
"until": band_end.isoformat(),
309400
"categories": list(CATEGORIES),
310401
"pick": PICK_COUNT,
402+
"batch": BATCH_SIZE,
311403
"band_count": len(band),
312404
"scored_count": scored,
313405
"truncated": truncated,
@@ -321,6 +413,8 @@ def main() -> int:
321413
"published": p["published"],
322414
"topic": p["topic"],
323415
"score": p["score"],
416+
"keyword_score": p["keyword_score"],
417+
"home": p["home"],
324418
"strong_lensing": p["strong_lensing"]}
325419
for p in candidates],
326420
}

.github/workflows/arxiv_interests.yml

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ name: pyauto-arxiv-interests
2626
#
2727
# Two stages, the house pattern: a deterministic ranker
2828
# (.github/scripts/arxiv_interests.py) narrows a whole day of astro-ph to ~60
29-
# candidates, and Claude reads those abstracts and picks the ten. Neither could
30-
# do the other's job — no keyword query answers "most interesting", and no
31-
# prompt reads five hundred abstracts a day.
29+
# candidates, and Claude reads those abstracts and picks the day's papers.
30+
# Neither could do the other's job — no keyword query answers "most
31+
# interesting", and no prompt reads a whole day of abstracts.
32+
#
33+
# Claude is asked for MORE than land (PICK_COUNT vs the Memory-side batch cap),
34+
# ranked, because the append below drops papers already on the strong-lensing
35+
# inbox or in the reading queue and then trims to the cap. Without the slack an
36+
# overlap costs a slot, which is exactly what the first live run did: ten
37+
# picked, one already on the lensing list, nine filed.
3238
#
3339
# Timing: 02:30 UTC, half an hour behind arxiv_papers.yml, on the same Mon-Fri
3440
# cadence and the SAME announcement band (arxiv_fetch.announcement_band, shared
@@ -113,7 +119,8 @@ jobs:
113119
Read `arxiv_interest_candidates.json` in this directory. It has shape:
114120
115121
{ "uk_date": "YYYY-MM-DD (Mon)",
116-
"pick": 10,
122+
"pick": <how many to return>,
123+
"batch": <how many actually land on the board>,
117124
"band_count": <papers announced in the band>,
118125
"count": <candidates below>,
119126
"papers": [ { "title": "<title>",
@@ -123,14 +130,19 @@ jobs:
123130
"id": "<bare arXiv id>",
124131
"primary_category": "astro-ph.GA",
125132
"topic": "<suggested reading-queue section>",
126-
"score": <keyword score>,
133+
"score": <ranker score, incl. category bonus>,
134+
"keyword_score": <before the bonus>,
135+
"home": true|false,
127136
"strong_lensing": true|false }, ... ] }
128137
129138
These are the papers announced on arXiv overnight that a keyword
130139
ranker judged closest to the reader's interests. The ranker is a
131140
shortlist, not a verdict: `score` and `topic` are hints from
132141
substring matching, and you should overrule either when the
133-
abstract says otherwise.
142+
abstract says otherwise. `home` is false for papers borrowed from
143+
the statistics categories, which is why their `score` sits below
144+
their `keyword_score` — a thumb on the scale to stop stats
145+
vocabulary crowding out astronomy, not a judgement about the paper.
134146
135147
The reader is a working astronomer whose interests are, roughly:
136148
supermassive black holes and AGN; dark matter (particle nature,
@@ -156,7 +168,17 @@ jobs:
156168
want to see it: a significant result, a new method they could use,
157169
a dataset or survey they work with, a genuine surprise. Prefer
158170
substance over volume — do not fill the list with near-duplicate
159-
survey papers when a more varied ten is available.
171+
survey papers when a more varied list is available.
172+
173+
ORDERING IS LOAD-BEARING, and it is why `pick` is larger than
174+
`batch`. Only the first `batch` papers that survive filing reach
175+
the board: the step below drops any paper already on the
176+
strong-lensing inbox or in the reading queue, then stops at
177+
`batch`. So the extra picks are dedup slack — put your strongest
178+
first and your marginal ones last, because the marginal ones are
179+
what a day with no overlap discards, and what a day with two
180+
overlaps promotes. Do not pad: if only eight papers are genuinely
181+
worth the reader's morning, return eight.
160182
161183
For each pick, assign `topic` — the PyAutoMemory reading-queue
162184
section it belongs in. Use EXACTLY one of these four strings:
@@ -178,7 +200,8 @@ jobs:
178200
"topic": "<one of the four strings above>" },
179201
... ] }
180202
181-
Most interesting first. Titles VERBATIM from the candidates file —
203+
Most interesting first — see ORDERING above; the tail is what gets
204+
dropped. Titles VERBATIM from the candidates file —
182205
do not re-word or re-case them; they are matched against the
183206
reading queue to avoid filing a paper twice. Strip any `arXiv:`
184207
prefix and any trailing `vN` from the id.
@@ -224,7 +247,11 @@ jobs:
224247
225248
# append is idempotent and dedupes against the interests list, the
226249
# reading queue AND the strong-lensing inbox, so a re-run adds
227-
# nothing and a paper never lands on both lists.
250+
# nothing and a paper never lands on both lists. It is also what
251+
# enforces the batch size: --limit defaults to interests_actions.py's
252+
# own BATCH_SIZE, so the over-pick above (PICK_COUNT) is trimmed HERE,
253+
# in order, after the dedup. Do not pass --limit from this side — one
254+
# owner for that number, or the two repos drift.
228255
added=$(python3 scripts/interests_actions.py append \
229256
--papers-file arxiv_interest_picks.json)
230257
# Unconditional, and the whole reason this step does not skip empty

0 commit comments

Comments
 (0)