Lemma 4
Local AI Study Assistant
A study tool that turns lecture PDFs into summaries, flashcards, quizzes and sourced answers, using a language model that runs entirely on the student’s own machine.

Overview
The assistant takes lecture PDFs or pasted notes and produces four things: a structured summary, flashcards, a multiple-choice quiz, and answers to questions about the material with the passages they were drawn from. The language model is served by Ollama on the same machine, so no document leaves it. There are no API keys, no cloud service, no database and no tracking.
I wrote the retrieval pipeline myself instead of using a framework — extraction, cleaning, chunking, ranking — so that I can explain and test every stage.
Problem
The tools students use to revise from lecture notes generally require uploading those notes to someone else’s server. For course material that is often not permitted, and it is not free at scale.
A model small enough to run on a laptop brings its own problems: a whole chapter does not fit in its context window, and its structured output is unreliable.
Goals
- Keep every document on the user’s machine: process in memory, write nothing to disk, contact only the local model server.
- Answer questions only from the uploaded material, and show the passages an answer came from.
- Never show a broken result: validate what the model returns and fall back to something readable.
- Make the whole pipeline testable without the interface and without a model running.
Technologies
| Application | Python 3.11+, Streamlit, Ollama (local HTTP API), requests, pypdf, scikit-learn |
|---|---|
| Testing and CI | pytest, ruff, GitHub Actions, Make |
Architecture
The Streamlit file does presentation only. The logic lives in a package of small modules — document loading, text processing, retrieval, prompts, the Ollama client and response parsing — coordinated by a service layer.
For question answering, documents are split into overlapping chunks of about 1,500 characters that end on paragraph or sentence boundaries where possible. A TF-IDF matrix is built over the chunks, the question is scored against each one by cosine similarity, and only the top few are sent to the model with instructions to answer from them alone. If nothing matches, the “not found” answer is returned without calling the model at all.
Presentation
- app.pyStreamlit interface
Ingestion
- document_loaderPDF and pasted text, in memory
- text_processingCleaning and chunking
Orchestration
- study_service
- retrievalTF-IDF, cosine similarity
- prompts
Model
- ollama_clientHTTP to localhost
- response_parserJSON extraction and validation
My responsibilities
This is a solo project. I designed it, wrote it and tested it.
- The ingestion and retrieval pipeline: in-memory PDF extraction, text cleaning, overlapping chunking and TF-IDF ranking.
- The Ollama HTTP client, with an exception hierarchy that turns each failure into a readable message.
- Prompt construction and defensive parsing of the model’s JSON output.
- The Streamlit interface, the test suite and the CI workflow.
Every commit in the repository is mine.
Engineering decisions
TF-IDF instead of embeddings
TF-IDF needs no model download, no vector database and no cost, and its scores can be inspected and debugged. What it gives up is semantic matching: it compares words, not meaning, so a question phrased in different vocabulary than the source retrieves worse. The interface and the documentation say so.
No framework for a four-step pipeline
Chunk, vectorise, rank, ground: four steps. Writing them directly keeps the dependency list short and means I can account for each one. The cost is a few convenience helpers I would otherwise have had for free.
An injected HTTP session
The Ollama client takes its requests.Session through the constructor. Tests pass in a fake that returns canned responses or raises the exact exception under test, so nothing is patched and no fake server runs. The service layer takes a client in the same way, which lets a test assert on the prompt that would have been sent.
Validate the model’s JSON, then fall back
Local models often wrap JSON in Markdown fences or put a sentence before it. The parser strips fences, slices from the first bracket to the last, then checks the shape: a quiz question needs exactly four non-empty choices and a correct answer that matches one of them. If that fails, the user sees the error and the raw response, not a broken quiz.
Uploaded documents are untrusted input
A PDF can contain text that reads like an instruction. Document text is wrapped in explicit delimiters and the system message declares everything inside to be reference material, never instructions. This reduces the risk; it does not remove it, and the documentation does not claim otherwise.
Testing and quality
Six test modules with 71 test functions. Every HTTP call is replaced by an injected fake, so the suite needs no Ollama, no network and no external PDF, and it runs in GitHub Actions together with the ruff linter on every push and pull request.
The tests cover the code around the model, not the model. There is no end-to-end test against a live model, because that would make the pipeline depend on Ollama being installed on the runner.
Challenges
A timeout reported as “not running”
In the requests library, ConnectTimeout inherits from both ConnectionError and Timeout. With the except blocks in the wrong order, every connect timeout was reported as “Ollama is not running”, which sends a user looking in the wrong place. The order is fixed and a test now pins it.
A chunking loop that must terminate
With overlap, the next chunk starts before the previous one ends, and a careless configuration could leave the cursor standing still. The next start is always at least one character further on, and an overlap as large as the chunk size is rejected up front. Both rules are covered by tests.
Results
| Measure | Result | Source |
|---|---|---|
| Study modes | 4: summary, flashcards, quiz, question answering | Repository documentation |
| Automated tests | 71 test functions in 6 modules | Counted in the repository |
| Continuous integration | Lint and tests; latest run succeeded | GitHub Actions, checked 12 October 2026 |
| Network endpoints contacted | 1, on localhost | Repository documentation |
Table 1. There are no benchmark or accuracy figures here because none were measured.
What this does not show
- No OCR: scanned PDFs with no extractable text are rejected with a clear message.
- Summary, flashcard and quiz modes use only the first 12,000 characters or so of a long document, and the interface says when that happens.
- Output quality is bounded by whichever local model the user has installed.
Repository and links
abdeljalilsennaoui/local-ai-study-assistant(opens in a new tab)
There is no hosted demo for this project. The repository explains how to run it.
End of case study