This project is the result of work assigned as part of a research grant with the Politecnico di Torino (PoliTO). The team's task was to demonstrate a prototype of an agent integrable with the PoliTO course channels, capable of helping students with their studies.
The part I was responsible for was defining the entire RAG (Retrieval-Augmented Generation) architecture of the system, together with designing and implementing the new-material-generation component (exercises and personalized lessons) based on the course content.
The current project is an MVP (Minimum Viable Product): the goal is to demonstrate the technical feasibility of the proposed architecture, not to deliver a production-ready system.Some choices described in this document (authentication, permission handling, scalability) are simplified compared to what a real deployment with active students would require.
PoliBot is a system that, starting from a course's material (slide decks in PDF), allows:
- Answering student questions about the course content, citing the specific sources (slide, page, topic) the answer comes from, reducing the risk of made-up answers (hallucination). Retrieval is multi-tenant aware: results can be scoped to a course and to a mix of public material plus a specific student's own private notes, with no cross-student leakage.
- Generating new exercises on a requested topic, based on the style and structure of exercises already present in the course material, but with different data or scenarios.
- Generating personalized lessons on a specific topic, with a configurable number of slides within a user-defined minimum/maximum range, optionally tailored to an accessibility profile (
standard,visual,adhd,dyslexia) that adjusts content structure and can attach a self-contained interactive HTML widget per slide. - Editing a previously generated slide through a natural-language instruction (e.g. "shorten this and add a warning about X"), re-grounded in course context retrieved for that instruction.
- Running a guided, step-by-step exercise session: the student submits an answer for each step of a fixed multi-step problem, an LLM grades it as correct/incorrect and returns a hint on failure, and incorrect attempts are logged to PostgreSQL tagged by skill type, for later analysis of common mistakes.
- Storing generated lessons and exercises as PDF files in centralized object storage (RustFS) with the material indexed in PostgreSQL, made accessible via a time-limited presigned URL.
The system is organized into two main flows, which share the same underlying infrastructure (vector database, embedding models).
The pipeline is split into an offline phase and an online phase.
Offline phase (ingestion, run once or whenever the course material is updated):
- Course slides (PDF) are read and split into chunks at the slide/page level, using a semantic chunking strategy with a 50-token overlap between contiguous chunks, to preserve continuity of concepts across related slides.
- Images and diagrams present in the slides are described through a vision-language model (captioning), and the resulting textual caption is treated as a regular text chunk.
- Each chunk (text and caption) is transformed into a dense vector, and a sparse vector is also generated for keyword-based search.
- The vectors (dense and sparse) are stored in Qdrant, together with associated metadata (course, slide, page, topic).
Online phase (run for each student query):
- Query reformulation: the raw student question is rewritten by a small local model before retrieval, to improve match quality against the indexed chunks.
- Embedding: the reformulated question is transformed into a vector using the same model used during ingestion.
- Retrieval: a hybrid search is performed on Qdrant, combining similarity search (dense vectors) and keyword search (sparse vectors), fusing the results with Reciprocal Rank Fusion (RRF). An optional Qdrant payload filter restricts candidates to a given
course_id, and to chunks whoseaccess_scopeispublicor whoseowner_idmatches the requesting student — enforcing multi-tenant isolation of private material at the retrieval layer. - Reranking: the top candidates are reordered by a dedicated reranking model (a cross-encoder), which evaluates the relevance of each chunk to the query more precisely than similarity search alone.
- Augmentation: the most relevant chunks, together with their metadata, are inserted into a structured prompt sent to the generative model, with explicit instructions to answer only based on the provided material.
- Output: the model generates the final answer, citing the sources the information comes from.
This flow reuses the same retrieval infrastructure as the RAG flow, adapted to a generative task rather than question answering.
- Template Retrieval: given a topic requested by the student, the system retrieves similar exercises or content already present in the course material, which serve as reference templates for style, notation, and difficulty.
- Generation: a dedicated prompt guides the model to generate new content (an exercise, or a lesson's slides) while preserving the structure of the retrieved examples, but with different data or topics. The output is constrained to a structured schema (Pydantic), ensuring the result is always in a predictable, usable format (statement, data, solution for exercises; title, content, LaTeX equation, optional HTML widget, and accessibility notes for each lesson slide). For lessons, generation is driven by a requested
style_profile(standard,visual,adhd,dyslexia) that shapes content structure and whether a self-contained interactive HTML widget is attached to a slide. - PDF Rendering: the structured content is turned into an actual PDF document, through a LaTeX template filled by a templating engine (Jinja2) and compiled with pdflatex. This approach ensures correct rendering of mathematical notation, which is common in a technical course's material.
- Storage: the generated PDF (lesson or exercise set) is uploaded to S3-compatible storage (RustFS) and indexed as a
Materialrow in PostgreSQL; a presigned URL (valid for 1 hour) and the material's id are returned to the caller. - Slide editing: an already-generated slide can be revised via a free-text instruction; the instruction plus the slide's own title/content are used to retrieve fresh course context, which is fed back to the model together with the original slide JSON to produce an updated slide matching the same schema.
A separate flow lets a student work through a fixed multi-step problem interactively rather than receiving a finished exercise PDF:
- Start: a session is created (in-memory, keyed by a UUID) for a student and topic, returning the description of the first step.
- Step evaluation: for each submitted answer, a grading model (the small reformulation model, for low latency) judges it against the expected result for that step and returns either
CORRECT(advancing to the next step) orINCORRECTwith a hint and askill_typetag, without revealing the answer. - Error logging: every incorrect attempt is persisted to PostgreSQL (
StudentError: student, exercise, step index, skill type, error detail), building a dataset of where students actually struggle, per skill.
Note: the current step content/solutions are a single hardcoded four-step optimization problem (STEPS_METADATA in exercise_fsm.py), independent of the topic passed at session start — this is a prototype stand-in for a future topic-driven step generator.
The system uses several models, each for a specific task, through Ollama:
| Model | Role | Execution |
|---|---|---|
nomic-embed-text |
Text embedding (chunks and queries) | Local |
gemma2:2b |
Query reformulation; final answer generation for Q&A; exercise and lesson generation (structured output); guided-exercise step grading | Local |
moondream |
Image/diagram captioning (vision-language model) | Local |
bge-reranker-v2-m3 |
Reranking of retrieval results | Local |
Note: exercise generation includes a JSON-repair step, since the lesson/exercise model doesn't always honor the structured output schema constraint on the first attempt: malformed responses are patched (e.g. fixing unescaped LaTeX backslashes) and, if still invalid, re-generated via the same gemma2:2b model used for query reformulation, which honors the schema more reliably. All LLM inference (Q&A, captioning, exercise/lesson generation, slide editing, step grading) runs locally via Ollama, with no cloud model dependency.
- RAG Framework: LlamaIndex
- Vector database: Qdrant (with native support for dense and sparse vectors)
- Relational database: PostgreSQL (generated-material index; guided-exercise student error log)
- Object storage: RustFS (S3-compatible, written in Rust)
- Backend: FastAPI
- Test UI: Gradio
- LLM models: Ollama (local only)
- PDF rendering: Jinja2 + LaTeX (pdflatex)
- Dependency management: Poetry
- Docker and Docker Compose
- Python 3.11 or 3.12 (3.13+ not supported yet) and Poetry
- A LaTeX distribution (for PDF rendering):
sudo apt install texlive-latex-base texlive-latex-recommended texlive-latex-extra
- Ollama installed and running locally
Copy the example file and fill it in with your own values:
cp .env.example .envIn particular, set:
POSTGRES_PASSWORDandRUSTFS_SECRET_KEY: generate secure values withopenssl rand -hex 32instead of leaving the placeholders
docker-compose up -d
docker-compose ps # verify Qdrant, PostgreSQL, and RustFS are "Up"ollama pull nomic-embed-text
ollama pull gemma2:2b
ollama pull moondreampoetry installPlace the slide PDFs in a dedicated folder, then run:
poetry run python -m polibot.ingestion.run --source <pdf_folder_path> --course <course_name>Verify that ingestion succeeded by checking that the Qdrant collection is not empty:
curl http://localhost:6333/collections/polibotpoetry run uvicorn polibot.api.main:app --reloadThe backend will be reachable at http://localhost:8000 (interactive docs at http://localhost:8000/docs).
In a second terminal:
poetry run python gradio_app.pyThe interface will be reachable at http://127.0.0.1:7860, with three sections: Chat (questions and answers), Generate Exercise (exercise generation), and Generate Lesson (personalized lesson generation). Multi-tenant retrieval, slide editing, and the guided-exercise FSM are only exposed via the API (/query, /edit-slide, /exercises/start, /exercises/step) for now, not yet wired into this test UI.
- Captioning images in the slides is a slow operation (roughly one minute per image, on the local model used); full ingestion with captioning enabled may take a long time on courses with many images.
- There is no end-user (student) authentication mechanism;
owner_id/student_idare passed as plain request fields and trusted as-is, so the multi-tenant retrieval filter and the exercise FSM's error log are not protected against a client claiming another student's id. The system is intended to be integrated in the future into the official PoliTO course channel, which will handle this aspect. - All inference now runs on small local models (
gemma2:2bfor text,moondreamfor captioning) instead of a larger cloud model; answer quality and reasoning depth may be more limited than what a larger cloud model would provide. This is a deliberate speed/simplicity trade-off for the MVP. - Guided-exercise (FSM) sessions are held in an in-memory dict (
ACTIVE_SESSIONS), so they don't survive an API restart and don't work across multiple backend replicas; the step content itself is a single hardcoded problem, not yet generated per topic.