How I built my AI research assistant
I built a Python assistant to find robotics and AI papers, prepare reading notes and send me a daily Telegram digest.
Why I wanted to build this
I used to save papers from arXiv, newsletters and GitHub, then lose them among open tabs. I wanted a regular reading list and a searchable record of what I had already found. I also wanted to follow robotics news that would not appear in academic databases.
Running the assistant without a server
I split the Python code into collection, ranking, PDF reading, AI analysis, storage and notification modules. GitHub Actions runs them on a temporary runner, so my computer does not need to stay on. Credentials are stored in repository secrets. A manual trigger lets me test changes without waiting for the daily schedule.
This is the simplified scheduled workflow; the repository also supplies secrets and commits the generated notes.
name: Daily research and robotics radar
on:
schedule:
- cron: '0 5 * * *'
timezone: 'Europe/London'
workflow_dispatch:
jobs:
research-radar:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: python -m pip install -r requirements.txt
- run: python -m radar.cli --dry-run
- run: python -m radar.deep_read --max-papers 2
- run: python -m radar.deliver
# The repository is committed after these steps.Collecting new papers and robotics news
The assistant collects academic work from arXiv and OpenAlex, plus industry news from RSS feeds. Each result becomes a record with a title, date, source link and abstract or excerpt. A three-day lookback helps cover delayed indexing or missed runs.
I compare identifiers and normalised titles against the archive to remove duplicates. Weighted topic keywords favour my interests in human–robot interaction, manipulation and multi-agent robotics. Title matches count more than abstract matches. This ranks relevance to me, not scientific quality.
The archive can add up to 35 papers and 20 news items per day. The digest contains seven papers and six news items; two papers are selected for fuller analysis.
The request fetches recent arXiv entries. The configuration below shows how I weight topics when selecting a reading list.
params = {
"search_query": "cat:cs.RO",
"start": 0,
"max_results": 45,
"sortBy": "submittedDate",
"sortOrder": "descending",
}
response = session.get(
"https://export.arxiv.org/api/query",
params=params,
timeout=25,
)
response.raise_for_status()
# Parse the returned Atom feed into paper records.interests:
- name: Human–robot interaction
weight: 6
phrases: ["human-robot interaction", "contact-rich", "handover"]
- name: Dexterous manipulation and embodied AI
weight: 5
phrases: ["dexterous", "grasping", "tactile sensing"]
- name: Swarm and multi-agent robotics
weight: 5
phrases: ["swarm robotics", "multi-agent", "quadcopter"]Extracting text from full research papers
For selected papers, the script downloads an accessible PDF temporarily and extracts its text with PyMuPDF. Page labels let the analysis refer back to the source. Download and text limits control request size, and a failed download can be replaced by another candidate.
Gemini receives the extracted text and returns structured fields covering the question, method, datasets, baselines, results, limitations and reproducibility. Missing details should be marked “Not reported”. I also request short quotations with page references.
The script checks whether each quotation appears on its stated page. That catches some errors but cannot establish that the interpretation is correct. Text extraction can also miss information in figures, tables or scanned pages. I still inspect the paper and its evaluation before using a finding in my own work.
These examples show page-by-page text extraction and the fields I request from Gemini.
import fitz
def extract_pages(pdf_bytes):
with fitz.open(stream=pdf_bytes, filetype="pdf") as doc:
return [
(i + 1, page.get_text("text", sort=True))
for i, page in enumerate(doc)
]
# Extracted text is labelled [[PAGE 1]], [[PAGE 2]], etc.
# The original PDF is not saved in the Git repository.FIELDS = (
"question", "contribution", "method", "datasets",
"baselines", "results", "limitations",
"reproducibility", "research_opportunities",
)
# I request a JSON object with these fields and
# up to three short quotations linked to PDF pages.
# Unavailable information should be marked "Not reported".Saving a research library I can actually use
Markdown files hold the briefings and individual notes, while a JSON catalog tracks existing records. Updates preserve my personal annotations. I keep links rather than PDFs in Git to avoid filling the repository history with large files.
A Telegram bot sends a shorter version of the digest, with links back to the sources. A delivery record prevents identical messages when I rerun a job. The digest uses abstracts and news excerpts; full-paper reports are labelled separately.
The folder structure keeps paper notes separate from daily digests. The delivery example sends the shorter digest through the Telegram API.
storage/
catalog.json
digests/
YYYY-MM-DD.md
notes/
paper/
news/
deep/
YYYY-MM-DD.md
reviewed.json
telegram-delivered.jsonimport requests
response = requests.post(
f"https://api.telegram.org/bot{token}/sendMessage",
data={"chat_id": chat_id, "text": digest_text},
timeout=25,
)
response.raise_for_status()What I had to fix during the first runs
Early runs collected records successfully but failed during deeper reading. One PDF exceeded my download cap; another produced an incomplete model response. My first configured Gemini model was also unavailable to the API project.
I tested the API independently, changed the model configuration, increased the PDF and response limits, and requested structured JSON. Incomplete reviews are now marked as incomplete. I check actual review counts and Telegram delivery, rather than treating a successful workflow run as proof that every stage worked.