# Art Art is a small, locally run LLM toolchain for rheology research. It accompanies the talk "Art: An LLM toolchain for rheology research" (Society of Rheology Annual Meeting, Boston, October 2026, session "AI and ML in Rheology"), and is meant as a starting point that other rheologists can read in one sitting, adopt, and adapt. Everything runs on your own machine: embedding and re-ranking models are cached locally from HuggingFace, and text generation (coming in the next stage) goes to [Ollama](https://ollama.com) on `localhost`. No cloud APIs. Art has two parts: 1. **bingham**, a literature screener. It pulls new abstracts from the *Journal of Rheology* and arXiv, screens them against your interest profiles, and writes a daily Markdown digest of the papers worth reading. A Bingham plastic does not flow until the stress exceeds a yield threshold; most abstracts stay put, and only those that clear a calibrated threshold come through. 2. **Corpus search** (planned), hybrid retrieval over a locally indexed collection of rheology papers, with raw-evidence inspection and grounded synthesis with citations. ## Status bingham fetches, screens, and writes digests. The LLM passes (a short summary, a topic label, and a one-sentence match reason for each highlighted paper) are next; until then, each digest entry shows the opening sentences of the abstract and the screening scores. Thresholds are provisional until calibrated on a hand-labeled set. ## How bingham works ``` fetch -> store -> embed -> screen -> re-rank -> digest (code) (SQLite) (bge) (cosine (cross- (code) + BM25) encoder) ``` - **Fetch.** arXiv announcements from the per-category RSS feeds at `rss.arxiv.org`; *J. Rheol.* from Crossref (by ISSN) merged by DOI with the AIP current-issue feeds, which carry abstracts but no authors. Backfills and skipped days come from arXiv's OAI-PMH interface. - **Store.** One SQLite file holds papers, embeddings, scores, and a record of every run, so no stage is recomputed unless its model or profile has changed. - **Screen.** Each paper (title and abstract) is embedded once with `BAAI/bge-large-en-v1.5`. For each profile it gets a cosine similarity (the best match over the profile description and its exemplar papers) and a BM25 score against the profile's keywords, which catches abbreviations such as SAOS, LAOS, and DST and names that embeddings blur. The top candidates by either score, plus any paper by a tracked author, go on. - **Re-rank.** `BAAI/bge-reranker-v2-m3` reads each (profile description, paper) pair together and scores it from 0 to 1. A paper is highlighted when its score clears the profile's threshold. - **Digest.** One Markdown file per day in `digests/`, grouped by profile and ordered by score, with a footer recording counts, models, host, device, and time per stage. Retrieval models decide relevance; the LLM, when added, only writes about papers that have already passed the screen, and never produces identifiers (DOIs, arXiv IDs, titles, authors, and dates come straight from the feed metadata). ## Getting started ### Requirements - Python 3.12 and [uv](https://docs.astral.sh/uv/) - About 3.5 GB of disk for the two HuggingFace models - A GPU is optional. Tested on an NVIDIA RTX 4090 (CUDA) under Linux and an Apple M3 Pro (MPS) under macOS; the device is chosen automatically (`cuda`, then `mps`, then `cpu`). - [Ollama](https://ollama.com), for the LLM passes once they are added (not needed yet) ### Install ``` git clone https://lem.che.udel.edu/git/furst/art.git cd art uv sync uv run bingham download-models # once, with network access ``` `download-models` fetches the embedder and re-ranker into `./models/`. Every other command runs with HuggingFace in offline mode. On Linux with an NVIDIA GPU, use a driver from the 580 series or later (CUDA 13). With older drivers, the standard PyTorch wheels cannot use the GPU, and Ollama 0.34 falls back to the CPU without an error (it still reports "100% GPU" in `ollama ps`, while generation slows to minutes per call). If you cannot update the driver, route `torch` to the CUDA 12.6 index in `pyproject.toml` (`[tool.uv.sources]`, index `https://download.pytorch.org/whl/cu126`) and use an Ollama release that supports your driver. ### First run ``` uv run bingham fetch # J. Rheol. back to jor.backfill_from, plus today's arXiv uv run bingham fetch --source arxiv --since 2026-09-21 # optional: a week of arXiv to screen uv run bingham screen uv run bingham digest --since 2026-09-21 ``` The first `fetch` backfills *J. Rheol.* from Crossref (about 145 papers since January 2025). arXiv RSS is empty on Saturdays and Sundays, so a weekend first run fetches no arXiv papers unless you add `--since`. Backfilled papers stay out of the default daily digest; `digest --since DATE` includes them. ## Running ``` uv run bingham daily # fetch + screen + digest; the usual command uv run bingham fetch [--source arxiv|jor|all] [--since DATE] [--refresh] uv run bingham screen uv run bingham digest [--since DATE] uv run bingham topic "shear thickening and frictional contacts" [-n 15] ``` - **`daily`** writes `digests/YYYY-MM-DD.md` covering everything fetched since the previous digest. It is safe to run by hand at irregular intervals: if a weekday's arXiv announcement was missed, the next run fills the gap from OAI-PMH and says so in the digest footer. Gaps longer than `arxiv.max_catchup_days` are stored as backfill instead. - **`fetch --since DATE`** backfills arXiv (OAI-PMH) and *J. Rheol.* (Crossref) from a date. Requests to arXiv are spaced at least three seconds apart. - **`topic`** ranks every stored paper against a free-text query, with no profile and no LLM. - Raw feed responses are cached for `feed_cache_ttl_hours` (6); `--refresh` ignores the cache. ### Measured run times | Machine | Screening a week (about 850 papers, 8 profiles) | A typical day | |---|---|---| | RTX 4090 workstation | 37 s, including model loading | a few seconds | | M3 Pro laptop | about 20 s per profile | under a minute (estimated) | ## Adapting it to your own interests Your research-specific files live in a `user/` directory inside the checkout. It is ignored by this repository, so your profiles, the people you track, and your contact address never end up in the code. Art reads each file from `user/` first and falls back to the generic versions in `data/` (or the examples in `data/examples/`), and merges `user/config.yaml` over `config.yaml`. ``` mkdir user cp data/examples/profiles.yaml data/examples/config.yaml data/examples/interest_profile.md user/ ``` - **`user/profiles.yaml`**: one entry per topic, each with a `description` (two or three sentences in the field's vocabulary, used for embedding and re-ranking), `keywords` for BM25, and optional `exemplars` (DOIs or arXiv IDs of papers that typify the topic; abstracts are fetched once, or can be pasted in). `tracked_authors` lists people whose papers are always shown. Editing a profile re-screens against that profile only. - **`user/config.yaml`**: your contact email (sent to arXiv and Crossref), arXiv categories, and per-profile thresholds. Anything in `config.yaml` can be overridden here; nested keys override and lists replace. - **`user/interest_profile.md`**: a prose version of your interests, used only by the LLM-as-classifier baseline. - **`data/vocabulary.md`** and **`data/feeds.yaml`**: the rheology term list that keywords are drawn from, and journal RSS feeds (*Rheologica Acta*, *Soft Matter*, *JNNFM*, and *Macromolecules* are listed but disabled). Copy either into `user/` to customize it. Re-ranker scores depend on how broad a profile's description is, so thresholds are set per profile. The digest applies the current thresholds, so changing one needs no re-screen. **Drafting a profile from your own papers.** Instead of writing `user/profiles.yaml` from scratch, let `bingham init` draft it from papers you already have: ``` uv run bingham fetch # first, so there is a background corpus to compare against uv run bingham init --orcid 0000-0002-1825-0097 # your ORCID iD uv run bingham init --ids my_papers.txt # or one DOI or arXiv ID per line uv run bingham init --csl zotero.json --bib refs.bib # or Zotero exports (CSL JSON, BibTeX); sources can be combined ``` It fetches abstracts from Crossref and arXiv (cached in `user/seed/`), groups the papers into topics (`--topics`, default 5) by embedding similarity, picks each topic's most central papers as exemplars, suggests keywords from phrases that recur in the topic and from `data/vocabulary.md`, and suggests tracked authors from coauthors who appear on several of the papers. The LLM writes only each topic's name and description, as a draft. The results go to `user/profiles.draft.yaml`, `user/interest_profile.draft.md`, and `user/vocabulary.draft.md` (abbreviations defined in your papers but missing from the vocabulary); existing files are never overwritten. Papers too unlike the rest to form a topic are listed at the end of the draft rather than forced into one. With fewer than about 30 seed papers, expect to merge and edit topics by hand. Then check what each profile would pick up from the stored papers, and choose thresholds: ``` uv run bingham profile-test --file user/profiles.draft.yaml ``` When you are satisfied, save the draft as `user/profiles.yaml`. **Using Art on more than one machine.** Make `user/` a small private git repository of its own and clone it into each checkout (`git clone user`). Sync with `git pull` and `git push` inside `user/`. The SQLite store, digests, and models stay local to each machine. To keep `user/` elsewhere, set `ART_USER_DIR`. ## Repository layout ``` config.yaml base settings (paths, arXiv, J. Rheol., screening, LLM) data/examples/ example profiles, user config, and interest profile data/vocabulary.md rheology vocabulary data/feeds.yaml journal RSS feeds user/ your own files (not tracked; see above) modelfiles/ Ollama Modelfile for the researcher-tone model docs/failure-modes.md curated log of small-model failure modes art/ cli.py the bingham command env.py offline HuggingFace settings, config, device, host store.py SQLite schema and access feeds/fetch.py arXiv RSS, OAI-PMH, and API; journal RSS; Crossref feeds/collect.py daily and backfill fetch logic, arXiv catch-up feeds/cache.py raw-fetch cache with a TTL profile/profiles.py load and validate profiles.yaml screen/ embed.py, bm25.py, rerank.py, screen.py digest.py Markdown digest cluster.py average-linkage clustering (field pulse, profile drafts) toolbox/ seeds.py (ORCID, ID lists, BibTeX, CSL JSON), draft.py (bingham init) summarize/summarize.py LLM summary and topic label (being wired in) tests/ uv run pytest ``` Generated files (`data/art.sqlite`, `data/cache/`, `digests/`, `models/`) are not tracked. ## Tests ``` uv run pytest ``` The tests cover the deterministic parts: feed parsing (against a saved arXiv RSS snapshot), merging papers from several sources, author matching, and the arXiv catch-up logic. They need no network and no models. ## Related work Art copies patterns, with attribution in the code, from: - [llm-workshop](https://lem.che.udel.edu/git/furst/llm-workshop), sections `02-ollama`, `03-rag`, and `04-semantic-search` - [ssearch](https://lem.che.udel.edu/git/furst/ssearch), the hybrid retrieval stack (bge embeddings, bge re-ranker, BM25, offline model cache) - [Stan](https://github.com/EntropicLearners/stan) ([arXiv:2603.04657](https://arxiv.org/abs/2603.04657)), for its taxonomy of small-model failure modes and its practice of splitting composite LLM tasks ## License MIT; see [LICENSE](LICENSE). Copyright (c) 2026 Eric M. Furst.