Art 0.2.0

Built from art-dev 2da005a.
This commit is contained in:
Eric Furst 2026-10-25 07:54:09 -04:00
commit d7e264bcf2
15 changed files with 1344 additions and 53 deletions

View file

@ -261,3 +261,167 @@ def test_draft_yaml_loads_as_profiles():
data = prof.Profiles.model_validate(__import__("yaml").safe_load(profiles_yaml(d)))
assert data.profiles[0].exemplars[1].arxiv == "2609.21066"
assert data.tracked_authors == [] # suggestions stay commented out
def test_daily_digest_window_does_not_repeat_previous_papers(conn, monkeypatch):
"""A paper fetched during one daily run must not reappear in the next day's digest."""
import json
from art import digest
monkeypatch.setattr(digest.embed, "model_name", lambda: "e")
monkeypatch.setattr(digest.rerank, "model_name", lambda: "r")
def run(command, started, finished, args=None):
conn.execute("INSERT INTO runs (command, args, started_at, finished_at, host) VALUES (?, ?, ?, ?, 'h')",
(command, json.dumps(args or {}), started, finished))
return conn.execute("SELECT MAX(id) FROM runs").fetchone()[0]
def fetched(item_id, at):
store.upsert_items(conn, [_item(id=item_id, doi=item_id, fetched_at=at)], "daily")
# Monday's daily run starts at 10:00, fetches paper A at 10:00:05, finishes at 10:02.
run("daily", "2026-09-28T10:00:00+00:00", "2026-09-28T10:02:00+00:00")
fetched("mon", "2026-09-28T10:00:05+00:00")
# Tuesday's daily run starts at 10:00 and fetches paper B.
tue = run("daily", "2026-09-29T10:00:00+00:00", None)
fetched("tue", "2026-09-29T10:00:05+00:00")
assert set(digest.select(conn, None, tue).items) == {"tue"}
def test_since_digest_does_not_move_the_daily_window(conn, monkeypatch):
"""A --since digest (a backfill view) must not hide papers from the next daily digest."""
import json
from art import digest
monkeypatch.setattr(digest.embed, "model_name", lambda: "e")
monkeypatch.setattr(digest.rerank, "model_name", lambda: "r")
conn.execute("INSERT INTO runs (command, args, started_at, finished_at, host) VALUES "
"('daily', '{}', '2026-09-28T10:00:00+00:00', '2026-09-28T10:02:00+00:00', 'h')")
store.upsert_items(conn, [_item(id="noon", doi="noon", fetched_at="2026-09-28T12:00:00+00:00")], "daily")
conn.execute("INSERT INTO runs (command, args, started_at, finished_at, host) VALUES "
"('digest', '{\"since\": \"2026-09-01\"}', '2026-09-28T15:00:00+00:00', '2026-09-28T15:00:30+00:00', 'h')")
conn.execute("INSERT INTO runs (command, args, started_at, host) VALUES "
"('daily', '{}', '2026-09-29T10:00:00+00:00', 'h')")
tue = conn.execute("SELECT MAX(id) FROM runs").fetchone()[0]
assert "noon" in digest.select(conn, None, tue).items
def test_crossref_feed_windows(conn, monkeypatch):
"""First fetch: the last week as daily (or backfill_from); later: since the last fetch less the overlap."""
from datetime import datetime, timedelta, timezone
from art.feeds import collect
calls = []
monkeypatch.setattr(collect.fetch, "fetch_crossref",
lambda issn, since, source: calls.append((issn, since, source)) or
[_item(id=f"10.1021/{len(calls)}", doi=f"10.1021/{len(calls)}", source=source)])
stats = {"fetched": 0, "new": 0}
feed = {"name": "Langmuir", "source": "Langmuir", "crossref_issn": "0743-7463"}
collect._fetch_crossref_feed(conn, feed, None, stats)
week_ago = (datetime.now(timezone.utc).date() - timedelta(days=7)).isoformat()
assert calls[-1] == ("0743-7463", week_ago, "Langmuir")
assert conn.execute("SELECT origin FROM items").fetchone()[0] == "daily"
collect._fetch_crossref_feed(conn, feed, None, stats) # next run: from the last fetch
assert calls[-1][1] == week_ago
collect._fetch_crossref_feed(conn, {**feed, "name": "Soft Matter", "crossref_issn": "1744-683X",
"backfill_from": "2026-01-01"}, None, stats)
assert calls[-1][:2] == ("1744-683X", "2026-01-01")
def test_rss_doi_from_link(monkeypatch):
rss = b"""<?xml version="1.0"?><rss version="2.0"><channel><title>t</title>
<item><title>Yield stress foods</title><link>https://link.springer.com/article/10.1007/s00397-026-01599-y</link>
<description>Rigorous rheological characterization.</description></item></channel></rss>"""
monkeypatch.setattr(fetch, "http_get", lambda *a, **k: rss)
it = fetch.fetch_rss("Rheologica Acta", "https://example.org/rss", "Rheol. Acta")[0]
assert it["id"] == it["doi"] == "10.1007/s00397-026-01599-y"
def test_rss_aps_abstract_authors_and_date(monkeypatch):
rss = b"""<?xml version="1.0"?><rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://purl.org/rss/1.0/" xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:prism="http://prismstandard.org/namespaces/basic/2.0/">
<item rdf:about="http://link.aps.org/doi/10.1103/5pj6-t7zg"><title>Random walk</title>
<link>http://link.aps.org/doi/10.1103/5pj6-t7zg</link>
<description>Author(s): Ann Lee, Bo Wu, and Cy Diaz&lt;br /&gt;&lt;p&gt;Particles become fixed\xe2\x80\xa6&lt;/p&gt;&lt;br /&gt;[Phys. Rev. E 114, 044119] Published Tue Oct 13, 2026</description>
<dc:creator>Ann Lee, Bo Wu, and Cy Diaz</dc:creator><prism:doi>10.1103/5pj6-t7zg</prism:doi>
<dc:date>2026-10-13T10:00:00+00:00</dc:date></item></rdf:RDF>"""
monkeypatch.setattr(fetch, "http_get", lambda *a, **k: rss)
it = fetch.fetch_rss("Physical Review E", "https://example.org/rss", "Phys. Rev. E")[0]
assert it["doi"] == "10.1103/5pj6-t7zg"
assert it["abstract"] == "Particles become fixed\u2026"
assert it["authors"] == ["Ann Lee", "Bo Wu", "Cy Diaz"]
assert it["date"] == "2026-10-13"
def test_crossref_date_falls_back_to_created():
w = {"DOI": "10.1039/D6SM00542J", "title": ["T"], "published-online": {"date-parts": [[2026]]},
"published": {"date-parts": [[2026]]}, "created": {"date-parts": [[2026, 9, 29]]}}
assert fetch._crossref_item(w, "Soft Matter")["date"] == "2026-09-29"
w["published-online"] = {"date-parts": [[2026, 9, 25]]}
assert fetch._crossref_item(w, "Soft Matter")["date"] == "2026-09-25"
def test_link_keyed_items_are_rekeyed_to_their_doi(tmp_path, monkeypatch):
monkeypatch.setattr(store.env, "path", lambda key: tmp_path / "art.sqlite")
conn = store.connect()
link = "https://link.springer.com/article/10.1007/s00397-026-01580-9"
store.upsert_items(conn, [_item(id=link, doi=None, link=link, source="Rheol. Acta")], "daily")
conn.execute("INSERT INTO screens VALUES (?, 'p', 'v', 'e', 'r', 0.5, 'description', 1.0, 'cosine', 0.6, 0.2, 1, 1)", (link,))
conn.commit()
conn = store.connect() # the re-key runs on open
assert [r[0] for r in conn.execute("SELECT id FROM items")] == ["10.1007/s00397-026-01580-9"]
assert conn.execute("SELECT item_id FROM screens").fetchone()[0] == "10.1007/s00397-026-01580-9"
# the same paper arriving from the feed is now recognised, not new
assert store.upsert_items(conn, [_item(id="10.1007/s00397-026-01580-9", doi="10.1007/s00397-026-01580-9",
source="Rheol. Acta")], "daily") == 0
def test_tracked_author_paper_without_rerank_is_listed(conn, monkeypatch):
"""A paper screened before its author was tracked has no re-rank score; the digest must still list it."""
from art import digest
monkeypatch.setattr(digest.embed, "model_name", lambda: "e")
monkeypatch.setattr(digest.rerank, "model_name", lambda: "r")
p = prof.Profile(id="p", name="P", description="d")
data = prof.Profiles(tracked_authors=[prof.TrackedAuthor(name="Ada Owner", match=["Ada Owner"])], profiles=[p])
monkeypatch.setattr(digest.prof, "load", lambda path=None: data)
store.upsert_items(conn, [_item(id="x1", doi="x1", authors=["Ada Owner"], date="2026-09-20")], "backfill")
conn.execute("INSERT INTO screens VALUES ('x1', 'p', ?, 'e', 'r', 0.7, 'description', 3.0, NULL, NULL, 0.2, 0, 1)",
(p.version,))
sel = digest.select(conn, "2026-09-01")
assert [e.item["id"] for e in sel.tracked_only] == ["x1"]
assert "not re-ranked" in "\n".join(digest._entry(conn, sel.tracked_only[0], {"p": p}))
def test_label_bands_and_sampling():
import random
from art.label import band, sample
assert [band(x) for x in (None, 0.05, 0.15, 0.3, 0.9)] == ["none", "<0.1", "0.1-0.2", "0.2-0.4", ">=0.4"]
pop = {f"n{i}": {"band": "none"} for i in range(50)} | {f"h{i}": {"band": ">=0.4"} for i in range(3)}
picked = sample(pop, labeled={"h0"}, n=6, rng=random.Random(0))
assert len(picked) == 6 and "h0" not in picked
assert sum(i.startswith("h") for i in picked) == 2 # both unlabeled high-band papers, the rest from "none"
def test_weighted_recall():
from art.evaluate import _rates
# one relevant paper caught (weight 1), one relevant paper never re-ranked standing for 9 papers
rows = [(1.0, True, 0.8), (9.0, True, None), (1.0, False, 0.5), (30.0, False, None)]
recall, precision = _rates(rows, 0.2)
assert abs(recall - 0.1) < 1e-9 and abs(precision - 0.5) < 1e-9
def test_llm_call_records_ollama_version(tmp_path, monkeypatch):
from types import SimpleNamespace
from art import llm
monkeypatch.setattr(store.env, "path", lambda key: tmp_path / "art.sqlite")
conn = store.connect()
response = SimpleNamespace(response="A summary.", prompt_eval_count=100, eval_count=10,
done_reason="stop", thinking=None)
http = mock.Mock(**{"get.return_value.json.return_value": {"version": "0.40.11"}})
fake = mock.Mock(generate=mock.Mock(return_value=response), _client=http)
monkeypatch.setattr(llm, "client", lambda: fake)
monkeypatch.setattr(llm, "ensure_model", lambda: ("art-researcher:abc", "sha256:def"))
llm.server_version.cache_clear()
result = llm.generate(conn, "label", {"title": "T", "abstract": "A"})
llm.server_version.cache_clear()
assert result.outcome == "ok"
row = conn.execute("SELECT ollama_version, num_ctx FROM llm_calls").fetchone()
assert row["ollama_version"] == "0.40.11" and row["num_ctx"] == llm.cfg()["num_ctx"]