Skip to content

Commit 3d88ccd

Browse files
committed
Keep daily arXiv recommendations current
1 parent 9513e87 commit 3d88ccd

4 files changed

Lines changed: 148 additions & 17 deletions

File tree

‎deployments/desktop/static/desktop.js‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1572,6 +1572,22 @@
15721572
return Math.max(0, Math.min(100, Math.round(percentage)));
15731573
}
15741574

1575+
function paperDisplayTags(paper) {
1576+
const rawCategories = Array.isArray(paper?.categories)
1577+
? paper.categories
1578+
: String(paper?.categories || "").split(/[,;]\s*/);
1579+
const categories = rawCategories
1580+
.map((item) => String(item || "").trim())
1581+
.filter((item) => item && /^[a-z-]+(\.[A-Z]{2})?$/i.test(item))
1582+
.slice(0, 4);
1583+
if (categories.length) return categories;
1584+
const fallback = [paper?.primary_category, paper?.arxiv_category, paper?.category]
1585+
.map((item) => String(item || "").trim())
1586+
.filter((item) => item && item !== "unknown" && item !== "cached");
1587+
if (fallback.length) return fallback.slice(0, 3);
1588+
return [paper?.source || ui().papers.sourceFallback].filter(Boolean);
1589+
}
1590+
15751591
function setStatus(message) {
15761592
$("statusText").textContent = statusText(message);
15771593
}
@@ -2244,7 +2260,7 @@
22442260
const reportId = state.paperReports.get(reportKey);
22452261
const isReading = state.paperReading.has(reportKey);
22462262
const score = normalizeScore(paper.score ?? paper.relevance ?? paper.relevance_score ?? 80);
2247-
const tags = paper.tags || [paper.source || ui().papers.sourceFallback, ui().papers.profileMatch];
2263+
const tags = paperDisplayTags(paper);
22482264
return `
22492265
<article class="paper-card" data-paper-number="${number}">
22502266
<div class="score-ring" style="--score: ${Math.min(score, 100)}%">${score}%</div>

‎deployments/feishu/daily-push-agent/main.py‎

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import xml.etree.ElementTree as ET
1616
from email.utils import parsedate_to_datetime
1717
from concurrent.futures import ThreadPoolExecutor, as_completed
18-
from datetime import datetime, timedelta
18+
from datetime import datetime, timedelta, date
1919
from pathlib import Path
2020
from typing import Any, Callable, List, Dict, Optional, Set
2121
from dataclasses import dataclass
@@ -37,7 +37,6 @@
3737
# 导入多个数据源
3838
arxiv_fetcher = importlib.import_module("skills.arxiv-fetcher.scripts.fetch_arxiv")
3939
arxiv_fetch_by_date = arxiv_fetcher.fetch_by_date
40-
arxiv_fetch_latest = getattr(arxiv_fetcher, "fetch_latest", None)
4140
arxiv_fetch_recent_list_page = getattr(arxiv_fetcher, "fetch_recent_list_page", None)
4241

4342
openreview_fetcher = importlib.import_module("skills.openreview-fetcher.scripts.fetch_openreview")
@@ -132,6 +131,25 @@ def _parse_feed_date(value: Any) -> str:
132131
return text[:10]
133132

134133

134+
def _parse_iso_date(value: Any) -> Optional[date]:
135+
text = str(value or "").strip()
136+
if not text:
137+
return None
138+
for fmt, width in (("%Y-%m-%d", 10), ("%Y%m%d", 8)):
139+
try:
140+
return datetime.strptime(text[:width], fmt).date()
141+
except ValueError:
142+
pass
143+
return None
144+
145+
146+
def _paper_date_in_window(paper: Dict[str, Any], start_date: date, end_date: date) -> bool:
147+
paper_date = _parse_iso_date((paper or {}).get("publish_date") or (paper or {}).get("published"))
148+
if paper_date is None:
149+
return True
150+
return start_date <= paper_date <= end_date
151+
152+
135153
def _xml_text(node: ET.Element, *paths: str) -> str:
136154
namespaces = {
137155
"atom": "http://www.w3.org/2005/Atom",
@@ -1113,12 +1131,14 @@ def emit_fetched(source: str, papers: List[Dict]) -> None:
11131131
except ValueError:
11141132
today = datetime.now()
11151133
end_date = today.strftime("%Y%m%d")
1134+
end_day = today.date()
11161135

11171136
def fetch_arxiv_papers(fetch_days: int) -> List[Dict]:
11181137
if not arxiv_categories:
11191138
print("Skipping arXiv fetch: no categories selected")
11201139
return []
1121-
start_date = (today - timedelta(days=fetch_days)).strftime("%Y%m%d")
1140+
start_day = (today - timedelta(days=fetch_days)).date()
1141+
start_date = start_day.strftime("%Y%m%d")
11221142
papers: List[Dict] = []
11231143
per_category_limit = max(1, int(math.ceil(limit_per_source / max(1, len(arxiv_categories)))))
11241144
for category in arxiv_categories:
@@ -1129,12 +1149,10 @@ def fetch_arxiv_papers(fetch_days: int) -> List[Dict]:
11291149
categories=[category],
11301150
limit=per_category_limit,
11311151
) or []
1132-
if not category_papers and callable(arxiv_fetch_latest):
1133-
print(f" Date-bounded arXiv fetch returned 0 papers for {category}; fetching latest via API...")
1134-
category_papers = arxiv_fetch_latest(
1135-
categories=[category],
1136-
limit=per_category_limit,
1137-
) or []
1152+
category_papers = [
1153+
paper for paper in category_papers
1154+
if _paper_date_in_window(paper, start_day, end_day)
1155+
]
11381156
if not category_papers and callable(arxiv_fetch_recent_list_page):
11391157
print(f" arXiv API returned 0 papers for {category}; fetching recent list page...")
11401158
category_papers = arxiv_fetch_recent_list_page(
@@ -1190,16 +1208,19 @@ def fetch_cached_arxiv_papers() -> List[Dict]:
11901208
if not arxiv_categories:
11911209
return []
11921210
try:
1211+
start_day = (today - timedelta(days=max(1, int(days or 1)))).date()
11931212
conn = db_ops.get_connection()
11941213
cursor = conn.cursor()
11951214
cursor.execute(
11961215
"""
11971216
SELECT * FROM papers
11981217
WHERE arxiv_id IS NOT NULL AND TRIM(arxiv_id) != ''
1218+
AND publish_date >= ?
1219+
AND publish_date <= ?
11991220
ORDER BY fetched_at DESC, id DESC
12001221
LIMIT ?
12011222
""",
1202-
(max(1, int(limit_per_source)),),
1223+
(start_day.isoformat(), end_day.isoformat(), max(1, int(limit_per_source))),
12031224
)
12041225
rows = cursor.fetchall()
12051226
conn.close()

‎skills/arxiv-fetcher/scripts/fetch_arxiv.py‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,17 +93,22 @@ def _parse_arxiv_list_page(html: str, category: str, limit: int) -> List[Dict]:
9393
pairs = re.findall(r"<dt>(.*?)</dt>\s*<dd>(.*?)</dd>", html or "", flags=re.DOTALL | re.IGNORECASE)
9494
papers: List[Dict] = []
9595
for dt_html, dd_html in pairs:
96-
id_match = re.search(r'href="/abs/([^"]+)"', dt_html)
96+
id_match = re.search(r"""href\s*=\s*["']/abs/([^"']+)["']""", dt_html, flags=re.IGNORECASE)
9797
if not id_match:
9898
continue
9999
arxiv_id = unescape(id_match.group(1)).strip()
100100
title_match = re.search(
101-
r'<div class="list-title[^"]*">\s*<span[^>]*>\s*Title:\s*</span>(.*?)</div>',
101+
r"""<div\s+class=["']list-title[^"']*["']>\s*<span[^>]*>\s*Title:\s*</span>(.*?)</div>""",
102102
dd_html,
103103
flags=re.DOTALL | re.IGNORECASE,
104104
)
105105
author_block = re.search(
106-
r'<div class="list-authors[^"]*">(.*?)</div>',
106+
r"""<div\s+class=["']list-authors[^"']*["']>(.*?)</div>""",
107+
dd_html,
108+
flags=re.DOTALL | re.IGNORECASE,
109+
)
110+
subjects_block = re.search(
111+
r"""<div\s+class=["']list-subjects[^"']*["']>(.*?)</div>""",
107112
dd_html,
108113
flags=re.DOTALL | re.IGNORECASE,
109114
)
@@ -115,13 +120,17 @@ def _parse_arxiv_list_page(html: str, category: str, limit: int) -> List[Dict]:
115120
title = _clean_html_text(title_match.group(1) if title_match else "")
116121
if not title:
117122
continue
123+
subjects_text = _clean_html_text(subjects_block.group(1) if subjects_block else "")
124+
categories = re.findall(r"\(([a-z-]+(?:\.[A-Z]{2})?)\)", subjects_text)
125+
if category and category not in categories:
126+
categories.insert(0, category)
118127
papers.append(
119128
{
120129
"arxiv_id": arxiv_id,
121130
"title": title,
122131
"authors": [author for author in authors if author],
123132
"abstract": "",
124-
"categories": [category],
133+
"categories": categories or [category],
125134
"publish_date": "",
126135
"url": f"https://arxiv.org/abs/{arxiv_id}",
127136
"paper_url": f"https://arxiv.org/abs/{arxiv_id}",
@@ -134,12 +143,12 @@ def _parse_arxiv_list_page(html: str, category: str, limit: int) -> List[Dict]:
134143

135144

136145
def fetch_recent_list_page(category: str, limit: int = 100) -> List[Dict]:
137-
"""Fetch recent papers from arxiv.org/list/<category>/recent as an API fallback."""
146+
"""Fetch recent papers from arxiv.org/list/<archive>/recent as an API fallback."""
138147
category = str(category or "").strip()
139148
if not category:
140149
return []
141150
archive = category.split(".", 1)[0]
142-
url = f"{ARXIV_LIST_URL}/{urllib.parse.quote(archive, safe='-')}/new"
151+
url = f"{ARXIV_LIST_URL}/{urllib.parse.quote(archive, safe='-')}/recent?skip=0&show=2000"
143152
try:
144153
print(f"Fetching arXiv recent list page for {category}...")
145154
response = requests.get(url, timeout=DEFAULT_REQUEST_TIMEOUT, headers=DEFAULT_REQUEST_HEADERS)

‎tests/test_desktop_gui.py‎

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from deployments.desktop.shared import agents
1414

1515
reading_agent = importlib.import_module("agents.reading-agent.main")
16+
arxiv_fetcher = importlib.import_module("skills.arxiv-fetcher.scripts.fetch_arxiv")
1617

1718
PROJECT_ROOT = Path(__file__).resolve().parents[1]
1819

@@ -1555,6 +1556,81 @@ def fake_daily_push(**kwargs):
15551556
assert captured["custom_rss_urls"] == ["https://example.com/feed.xml"]
15561557

15571558

1559+
def test_daily_arxiv_fetch_filters_out_old_papers(monkeypatch: pytest.MonkeyPatch) -> None:
1560+
monkeypatch.setattr(
1561+
agents.daily_agent,
1562+
"arxiv_fetch_by_date",
1563+
lambda **_kwargs: [
1564+
{
1565+
"title": "Old May Biomolecular Paper",
1566+
"arxiv_id": "2605.99999v1",
1567+
"publish_date": "2026-05-20",
1568+
"categories": ["q-bio.BM"],
1569+
}
1570+
],
1571+
)
1572+
monkeypatch.setattr(agents.daily_agent, "arxiv_fetch_recent_list_page", lambda **_kwargs: [])
1573+
monkeypatch.setattr(agents.daily_agent, "openreview_fetch_by_date", lambda **_kwargs: [])
1574+
monkeypatch.setattr(agents.daily_agent, "journal_fetch_recent", lambda **_kwargs: [])
1575+
monkeypatch.setattr(agents.daily_agent, "prepare_paper_features", lambda papers: papers)
1576+
1577+
class EmptyRows:
1578+
def fetchall(self):
1579+
return []
1580+
1581+
class EmptyConnection:
1582+
def cursor(self):
1583+
return self
1584+
1585+
def execute(self, *_args, **_kwargs):
1586+
return EmptyRows()
1587+
1588+
def fetchall(self):
1589+
return []
1590+
1591+
def close(self):
1592+
return None
1593+
1594+
monkeypatch.setattr(agents.daily_agent.db_ops, "get_connection", lambda: EmptyConnection())
1595+
1596+
papers = agents.daily_agent.fetch_and_process_papers(
1597+
days=1,
1598+
arxiv_categories=["q-bio.BM"],
1599+
conferences=[],
1600+
journals=[],
1601+
enable_semantic_scholar=False,
1602+
enable_custom_rss=False,
1603+
target_date="2026-06-19",
1604+
)
1605+
1606+
assert papers == []
1607+
1608+
1609+
def test_arxiv_recent_list_parser_handles_current_html_shape() -> None:
1610+
html = """
1611+
<dt>
1612+
<a href ="/abs/2606.20489" title="Abstract" id="2606.20489">arXiv:2606.20489</a>
1613+
</dt>
1614+
<dd>
1615+
<div class='meta'>
1616+
<div class='list-title mathjax'><span class='descriptor'>Title:</span>
1617+
West Nile virus outbreak in Italy modelled with the quantum Game of Life
1618+
</div>
1619+
<div class='list-authors'><a href="/search/q-bio?query=Fontana">Andrea Fontana</a></div>
1620+
<div class='list-subjects'><span class='descriptor'>Subjects:</span>
1621+
<span class="primary-subject">Populations and Evolution (q-bio.PE)</span>; Applications (stat.AP)
1622+
</div>
1623+
</div>
1624+
</dd>
1625+
"""
1626+
papers = arxiv_fetcher._parse_arxiv_list_page(html, "q-bio.BM", limit=10) # noqa: SLF001
1627+
1628+
assert papers[0]["arxiv_id"] == "2606.20489"
1629+
assert papers[0]["title"].startswith("West Nile virus")
1630+
assert papers[0]["authors"] == ["Andrea Fontana"]
1631+
assert papers[0]["categories"] == ["q-bio.BM", "q-bio.PE", "stat.AP"]
1632+
1633+
15581634
def test_desktop_relevance_threshold_changes_daily_push_weights(monkeypatch: pytest.MonkeyPatch) -> None:
15591635
base = {
15601636
"threshold_high_relevant": 0.40,
@@ -1741,6 +1817,15 @@ def test_desktop_paper_source_defaults_are_arxiv_only() -> None:
17411817
assert "selectedSettingConferences()" not in collect_daily_options
17421818

17431819

1820+
def test_desktop_paper_cards_show_arxiv_category_tags() -> None:
1821+
script = (PROJECT_ROOT / "deployments/desktop/static/desktop.js").read_text(encoding="utf-8")
1822+
1823+
assert "function paperDisplayTags(paper)" in script
1824+
assert "paperDisplayTags(paper)" in script
1825+
assert "paper?.categories" in script
1826+
assert "const tags = paper.tags || [paper.source || ui().papers.sourceFallback, ui().papers.profileMatch]" not in script
1827+
1828+
17441829
def test_desktop_settings_controls_use_compact_typography() -> None:
17451830
css = (PROJECT_ROOT / "deployments/desktop/static/desktop.css").read_text(encoding="utf-8")
17461831

0 commit comments

Comments
 (0)