Rolo Tutor is an AI teaching assistant built for Vrije Universiteit Amsterdam. It teaches introductory Python instead of handing out answers — the tagline we settled on was "Designed to Teach, not Answer." Students reach it outside class hours, either on the web or through the course Discord server, and we presented it at a VU educational-technology conference.
Teaching, not answering
Asked how to reverse a list in Python, the tutor walks through three methods with runnable examples and closes with a short summary of which one to reach for, rather than dropping a single snippet. Every reply carries thumbs up / down controls, and the verdict goes straight to the database where the dashboard picks it up.
Architecture
Four services in one monorepo.
rolo-frontend — React 18 (CRA with CRACO path aliases) and React Router 6. Sign-in is Microsoft Entra ID / OAuth 2.0 via @azure/msal-browser and @azure/msal-react, restricted to @vu.nl and @student.vu.nl addresses. The chat renders with a typewriter effect and full Markdown + GFM through react-markdown and remark-gfm, so code blocks, tables and lists come out properly. Bootstrap 5, MUI and Emotion for the shell, Quicksand for type.
rolo-backend — a Node/Express API on Google Cloud Run.
POST /api/google-chat— Gemini 2.5 Flash via@google/generative-ai, replaying the user's full prior conversation as context so the tutor remembers the thread.POST /api/chat— the earlier OpenAI Assistants implementation, behind the same request shape.POST /api/cluster— topic clustering. A 3072-dimension embedding per student question, cached in aquestion_embeddingstable, then k-means (k=5) viaml-kmeanswith drill-down into sub-clusters. Around 200 questions cluster in about 8 seconds from cache.POST /api/executeQuery— a parameterised query layer.
Storage is Google BigQuery: web conversations, Discord conversations, and the embedding cache.
rolo-discord — a Discord.js bot with slash commands, automatic threading per question, button interactions, and a sweep for unanswered questions. It writes to the same database, so web and Discord traffic sit side by side in the dashboard.
rolo-tutor — the original Flask prototype the Node stack grew out of: OpenAI Assistants API, syntax highlighting, MongoDB persistence, and an optional local Ollama backend.
The dashboard
The dashboard is the part I care most about, because it is what turns a chat log into something a course team can act on.
The feedback table lists every message with its source (Web App or Discord), the thumbs verdict and a timestamp — sortable, paginated, and exportable to CSV. Message Count Over Time and Traffic Over Time are Chart.js with chartjs-plugin-zoom, splitting web-app against Discord-bot volume and showing unique users per day.
Topic clustering, two levels deep
The bubble chart groups questions by embedding similarity, with bubble area standing in for cluster size. Each cluster is labelled by a representative question, so hovering gives you the topic and its volume at a glance.
Clicking a bubble asks what you want from it: read the questions, or go a level deeper.
View Questions lists everything in that cluster verbatim — the raw student phrasing, not a summary of it.
Go to Subclusters re-runs k-means inside the cluster alone and redraws the chart against its members, so a broad topic breaks into the specific things students got stuck on. From there you can open questions per sub-cluster, or step back up.
The point is that "what is the cohort actually stuck on this week" is a couple of clicks, not a query.
Delivery
The frontend (behind nginx) and the backend are both Dockerised, with CI/CD to GCP: a push to main builds and deploys both services to Cloud Run automatically.
My role
I built the front end end-to-end — auth, chat UI, feedback capture, and the whole dashboard — along with the Node backend endpoints behind it, the CI/CD pipeline, and the GCP deployment.
The live deployment sits behind VU single sign-on, so the screenshots here come from a local run.