Skip to content

About

Self-hosted LLM agent that turns arXiv & Semantic Scholar into a curated, searchable paper library: screen, tag, note, chat, notify, and more!

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

Related Work Agent

Related Work Agent screenshot

A free, open-source, self-hosted literature review assistant. It tracks multiple research projects at once — each with its own research interest, keywords, and nightly crawl schedule — crawling arXiv and Semantic Scholar, screening candidates with an LLM, and surfacing everything you need to judge a paper in seconds: a brief AI summary, figure 1, a direct PDF link, citation count, author affiliations, and a chat that has read the whole paper for whatever the summary doesn't answer. New papers land in an always-up-to-date database and get emailed to you, so keeping up with your field stays fun instead of overwhelming — even with how much gets published these days.

All it needs to run is any LLM that speaks the standard chat-completions API — that can be a small locally-hosted model just as well as a large one; we've only tested at the scale described below, but nothing about the tool requires it.

Why this exists

I built this initially only for myself to solve a pressing real problem: staying on top of a fast-moving subfield without spending an hour a day on arXiv, without getting too biased by LinkedIn hype trains, and — just as often — to onboard fast onto a new topic when I only had one or two papers as leads.

Personally, we'd gotten curious about LLM anatomy after reading dnhkng's Rys post. A few weeks of automated crawling later, our instance of this agent surfaced arXiv:2606.06574 — a paper we probably wouldn't have found by manually searching, given how little visibility it had (few stars on its GitHub repo, for example). That paper turned into a real research thread, which led to our own work, RE-PoLar.

Our setup

We've been running this in production at Berliner Hochschule für Technik (BHT) (thanks, RIS!) against a real self-hosted LLM since ~mid-2026, handling multiple concurrent research projects with nightly automated crawls. Concretely, here's what we actually run:

  • Model: a quantized MiniMax M2.7 deployment, self-hosted on one DGX node (8× A100, 40GB each) via vLLM.
  • App deployment: Kubernetes (single-replica Deployment + CronJob sharing one SQLite database on a PVC), Docker image built from the included Dockerfile.

None of that is required to run this tool — see Quick start below for pointing it at any endpoint that speaks the same standard API (a self-hosted vLLM/Ollama/llama.cpp server, or a completely different setup entirely).

How it works

flowchart TD
    subgraph Phase1["1. Find candidates"]
        direction LR
        RI["Research interest<br/>(free text — or LLM-drafted from<br/>papers already in My Collection)"] -->|"LLM: extract keywords<br/>(or enter manually)"| KW["Keywords<br/>(saved & reused across crawls)"]
        SCHED["Crawl trigger<br/>(manual date range, or<br/>nightly auto)"] --> ARX["arXiv search"]
        KW --> ARX
        ARX --> CAND["Candidate papers"]
        SS["Semantic Scholar<br/>recommendations"] --> CAND
        CITE["New papers citing<br/>My Collection<br/>(Semantic Scholar)"] --> CAND
    end

    subgraph Phase2["2. Screen & enrich"]
        direction LR
        Q{Relevant?} -- yes --> DEEP["LLM: deep relevance check<br/>(full text) + why it's relevant"]
        Q -- no --> DROP["Discarded"]
        DEEP -- no --> DROP
        DEEP -- yes --> FIG["Figure, citation count<br/>+ institutions<br/>(arXiv / Semantic Scholar)"]
        FIG --> SUM["LLM: neutral summary +<br/>main contributions"]
    end

    subgraph Phase3["3. Store & notify"]
        direction LR
        DB[("SQLite database")] --> NEW["New Papers (inbox)"]
        NEW -->|"user tags"| COLL["My Collection<br/>(Important / To Read / Discuss)"]
        COLL --> TRASH["Trash"]
        NEW --> TRASH
        NEW -->|"Re-screen<br/>(after interest change)"| TRASH
        NEW -->|"Nightly full-text re-read<br/>(screened from abstract only)"| TRASH
        DB -->|"LLM: pick featured paper"| MAIL["Notification email"]
    end

    CAND -->|"LLM: quick relevance check<br/>(vs. research interest +<br/>Collection excerpts)"| Q
    SUM --> DB
    COLL -.->|"seeds"| SS
    COLL -.->|"seeds"| CITE

    classDef external fill:#2563eb,color:#fff,stroke:#1e40af,stroke-width:1px
    classDef llm fill:#7c3aed,color:#fff,stroke:#5b21b6,stroke-width:1px
    classDef store fill:#334155,color:#fff,stroke:#1e293b,stroke-width:1px
    classDef endpoint fill:#16a34a,color:#fff,stroke:#15803d,stroke-width:1px
    classDef dropped fill:#64748b,color:#e2e8f0,stroke:#334155,stroke-width:1px,stroke-dasharray:3 3
    class ARX,SS,CITE external
    class Q,DEEP,SUM llm
    class DB store
    class RI,NEW,COLL,MAIL endpoint
    class DROP,TRASH dropped
Loading

The dashed edges are a feedback loop: papers you've already curated into My Collection seed Semantic Scholar's recommendations, and every one of them is also checked for new papers that cite it. The recommendations are asked for in groups of 10 seed papers (up to 100, best-tagged first): Semantic Scholar returns at most 500 per request, ranked by similarity over the last few months rather than by date, so a single request has only a few papers from the last days among them; each smaller group gets its own 500, and together they reach many more recent papers. A citing paper isn't necessarily relevant (widely cited papers are cited by work from every corner of the field), so these candidates go through the same screening as the rest. Collection papers also quietly inform two LLM steps not drawn above — keyword extraction and relevance screening both get calibration excerpts from your Collection too — so the tool gets better targeted the more you curate.

Each paper card shows a neutral AI summary (shared by every project the paper is in) and, per project, a short "Why it's relevant" line that the deep relevance check writes against that project's research interest. A paper that fits a different project can be copied or moved into that project's My Collection from its card. After you change the research interest, Re-screen in New Papers re-checks the papers still waiting there and moves the ones that no longer fit to Trash, where you can restore them.

Next to "AI Summary" each card says what the summary was made from: the paper's full text, or "⚠ from abstract only" when no full text could be fetched (hover for why). Regenerate tries the full text again.

Ask about a paper. Every card has an "Ask about this paper" chat. The model reads the paper's full text (arXiv's HTML version, else the PDF; the chat says "⚠ Abstract only" when neither is available) together with the project's research interest, and answers in markdown with rendered math. The chat is saved with the paper, per project like the notes, and visible to everyone on the project; Copy / Move takes it along. Answers stream in as they are written (the model's thinking is never shown, stored, or sent back), keep going if you close the page, and can be stopped. Questions sent while an answer is coming are queued. The header shows how much of the model's context window the conversation fills; when it is full you clear the chat rather than have old turns silently dropped. Each question re-sends the paper, but the LLM server's prefix cache usually makes follow-up questions start answering within a second.

Each card also says how the paper got into the project: found by a crawl, added by hand, or copied / moved from another project. For a crawl find it says which way, and lists all of them when there were several: the keyword it matched, a Semantic Scholar recommendation, or the My Collection paper it cites (e.g. Found via keyword "sparse attention" · cites "Attention Is All You Need" (+2)). arXiv is searched with all keywords at once, so the keyword is looked up in the title and abstract afterwards; when arXiv matched a word variant that isn't there literally, the card just says "arXiv keyword search". Hovering the line spells it out, one sentence per way (e.g. It cites "Attention Is All You Need" from My Collection.).

New Papers shows one card at a time: step through them with the ‹ / › buttons, the arrow keys or a swipe on a phone. Collecting or trashing a paper brings up the next one in its place.

The crawl email lists each source with how many papers it found and how many of those were added (e.g. Papers citing My Collection: 60 (5 added)), so over a few nights you can see which source brings the relevant papers. A paper found several ways counts in each of its rows, so the rows add up to more than the total, but it is screened only once.

A paper you add by URL fills in its card while you watch: it shows what is being fetched ("Fetching the figure", "Writing the AI summary…") and redraws itself as the details arrive, no reload needed.

A paper that is already in the project (New Papers, My Collection or Trash) is never added again. Papers the agent rejected are remembered too and skipped without another LLM call, as long as the research interest and My Collection stay the same; change either and earlier rejects get checked again.

You can also add any paper directly by pasting its URL. It first tries to resolve the paper to its arXiv version — an explicit arXiv link on the page, a DOI match, or (as a last resort) a title search on arXiv itself — and if found, treats it as a regular arXiv paper with full enrichment. If no arXiv version exists, the landing page's title/abstract/authors are still read regardless of domain, but the actual PDF (used for page count and author-institution extraction) is only downloaded from a small allowlist of open-access hosts — arXiv, ACL Anthology, PMLR, OpenReview, NeurIPS Proceedings, CVF (CVPR/ICCV/ECCV), AAAI, IJCAI, JMLR — out of respect for publisher licensing terms. For anything else, you still get the metadata, just not a downloaded PDF.

Every LLM step is a plain prompt template in app/prompts.py. For example, the relevance check that filters candidate papers:

Evaluate if this paper is relevant for the research project.

Research interest:
{research_interest}

{ref_text}

Candidate paper:
Title: {title}
Abstract: {abstract}

Return JSON only: {"relevant": true} or {"relevant": false}

{ref_text} is up to REFERENCE_PAPERS_LIMIT papers (default 15, configurable) from your "My Collection" for that project — each as its title plus the first 400 characters of its abstract — included so the LLM has concrete calibration examples of what "relevant" means to you beyond the free-text description. Selected by tag priority (important → to_discuss → to_read → untagged), then filled with an even mix of your newest and oldest additions once a tier runs out of room. The same selection feeds keyword extraction and research-interest generation/refinement too. Direct links to all prompt templates, mapped to the diagram above:

Diagram step Prompt
Research interest from Collection / refine existing text RESEARCH_INTEREST_FROM_COLLECTION / RESEARCH_INTEREST_IMPROVE
Extract keywords KEYWORD_EXTRACTION
Quick relevance check RELEVANCE_QUICK
Deep relevance check + why it's relevant RELEVANCE_DEEP
Summary + main contributions SUMMARY_WITH_FULL_CONTENT / MAIN_CONTRIBUTIONS
Why it's relevant, for copied/moved papers + the one-off refill RELEVANCE_REASON
Institutions (LLM fallback, batched after all papers screened) INSTITUTION_EXTRACTION
Pick featured paper FEATURED_PAPER_SELECTION
Ask about a paper (chat on a card) PAPER_CHAT_SYSTEM

Quick start

git clone <this-repo>
cd paper-agent

python3.11 -m venv .venv   # must be 3.11
source .venv/bin/activate
pip install -r requirements.txt

cp .env.example .env
# edit .env: set LLM_API_BASE / LLM_API_KEY / LLM_MODELS to point at any
# endpoint that speaks the standard chat-completions API (a self-hosted
# vLLM/Ollama/llama.cpp server, or a gateway in front of one), plus
# SECRET_KEY and SITE_PASSWORD

LOG_LLM=1 .venv/bin/python -m flask run --port 5001

Open http://localhost:5001, log in with SITE_PASSWORD, create a project, describe your research interest, and hit "Start crawl."

Configuration

All configuration is via environment variables — see .env.example for the full list with explanations. The ones that matter most:

Variable Required What it does
LLM_API_BASE yes Base URL of a standard chat-completions endpoint (vLLM, Ollama, etc.)
LLM_API_KEY yes API key for that endpoint
LLM_MODELS yes Comma-separated model names, tried in order as a fallback cascade
SECRET_KEY yes Flask session secret
SITE_PASSWORD yes Single shared password for the whole site
SCHOLAR_API_KEY no Semantic Scholar API key — without it, Scholar calls are unauthenticated (rate-limit risk)
RESEND_API_KEY / NOTIFIER_TYPE no Email notifications — see .env.example for the SMTP fallback option
CHAT_CONTEXT_TOKENS no Context window of the first model in LLM_MODELS, for the paper chat's usage meter (default 196000)

Deployment

Docker image + Kubernetes manifests are included. The web server (gunicorn, single worker process — see AGENTS.md for why) and the nightly crawl (CronJob) share one SQLite database on a PVC.

docker build -t <your-registry>/<your-image>:latest .
docker push <your-registry>/<your-image>:latest

Copy the example manifests and fill in your own registry, ingress host, and secrets:

cp k8s/deployment.yaml.example k8s/deployment.yaml
cp k8s/cronjob.yaml.example k8s/cronjob.yaml
cp k8s/ingress.yaml.example k8s/ingress.yaml
cp k8s/secret.yaml.example k8s/secret.yaml
cp k8s/backup-cronjob.yaml.example k8s/backup-cronjob.yaml

Then:

kubectl apply -f k8s/pvc.yaml
kubectl apply -f k8s/backup-pvc.yaml
kubectl apply -f k8s/secret.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/ingress.yaml
kubectl apply -f k8s/cronjob.yaml
kubectl apply -f k8s/backup-cronjob.yaml

Backups

related-work-db-backup is a separate daily CronJob (k8s/backup-cronjob.yaml.example, scripts/backup_db.py) that snapshots the live database onto its own PVC (k8s/backup-pvc.yaml). It uses SQLite's own online backup API rather than a plain file copy, which matters because the app runs in WAL mode (PRAGMA journal_mode=WAL): a raw cp of the .db file can miss recently-committed data that's still sitting in the -wal file, or catch the file mid-checkpoint. Each snapshot is verified (PRAGMA integrity_check + a row count) immediately after it's written, and old snapshots are pruned once they exceed BACKUP_RETENTION_DAYS (default 30).

Schema migrations run automatically on startup (plain ALTER TABLE statements, no Alembic — see AGENTS.md). To redeploy after a code change:

docker build -t <your-registry>/<your-image>:latest .
docker push <your-registry>/<your-image>:latest
kubectl rollout restart deployment/related-work

The CronJob always pulls :latest at job start, so it needs no separate restart. To test the nightly crawl without waiting for its schedule, run scripts/nightly_crawl.py directly with CRAWL_HOUR_OVERRIDE=all set (bypasses the per-project hour filter).

Planned

  • Spend fewer tokens on screening. The pipeline was built for recall, not cost: every candidate gets an LLM relevance check. A cheap first pass (embedding similarity to My Collection, or a reranker) could drop the clear misses before the LLM sees them. To build it, first replay past crawls to measure how many papers that were accepted the pre-filter would have cut, and only use it if that number is close to zero.
  • Negative examples for the recommendations. Semantic Scholar's recommendation endpoint also accepts papers you don't want more of. The papers in Trash could go there, to steer recommendations away from what was rejected. Not done yet because Trash also holds papers that fit the topic but weren't interesting enough, so the effect is unclear; it would need a before/after comparison of what the recommendations bring in.

Contributing

See AGENTS.md for repo conventions, dev commands, and an architecture deep-dive — written for both human and AI-agent contributors.

License

MIT

About

Self-hosted LLM agent that turns arXiv & Semantic Scholar into a curated, searchable paper library: screen, tag, note, chat, notify, and more!

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Contributors

Languages