Graduation project - Computer Science - Faculty of Information Technology - Ho Chi Minh City Open University
Thesis title (Vietnamese): Phát triển Trợ lý hỗ trợ học tập trên LMS sử dụng RAG
Thesis title (English): Developing an LMS Learning Assistant Using RAG
| System name | EdOU — Education Open University |
| Demo site | edou.id.vn. It runs on the author's own computer, so it is not online all the time. |
| Student | Hoàng Phi Hùng — 2351010082 |
| Major / Faculty | Computer Science / Information Technology |
| Project type | Graduation project |
| Languages and frameworks | .NET 10, Angular 22, Python FastAPI |
| Infrastructure | Docker Compose, Cloudflare Tunnel, Cloudflare DNS (the domain is registered at Mắt Bão) |
Today, most learning management systems (LMS) only share documents. Teachers upload lessons, and students download them and study alone. Many schools now want to use large language models (LLM) to help students. But there is a common problem: the model does not know the school's own materials. So it can give wrong answers, or it can invent information that is not in the lesson.
EdOU is built to solve this problem. It has the normal school features: semesters, subjects, courses, sections and lessons. It also has an assistant that answers questions from the course documents only. The system uses a technique called RAG (Retrieval-Augmented Generation). It reads PDF files, cuts them into small parts and turns them into vectors. When a student asks a question, the system finds the most relevant parts and sends them to the model. The answer also shows the page number, so the student can check it.
Some technical points:
- Vector data is separated by course (
course_id). So the assistant of one course cannot use documents from another course. - The answer is sent to the browser little by little through WebSocket. Users do not have to wait a long time.
- The .NET backend stays behind an API Gateway built with YARP. Behind the gateway there are the main API and an AI microservice built with FastAPI.
- The whole system runs in Docker Compose and goes to the Internet through a Cloudflare Tunnel. We do not need to open any port on the server.
The system has three roles:
- Administrator: manages user accounts, semesters, subjects, courses and student enrollments.
- Instructor: creates content for the courses he or she teaches, such as sections, lessons, forums and assessments. Instructors also approve enrollments and grade student work.
- Student: studies lessons, joins forum discussions, submits work and asks the AI assistant of the course.
A course is divided into sections, and each section has modules. There are four types of modules:
- Document lesson: a PDF file that students can read in the browser.
- Media lesson: video or audio.
- Discussion forum: topics and posts in two levels, with pin, lock, show/hide and file attachments.
- Assessment: essay or quiz, with a pass score, a maximum number of attempts and a due date.
The AI assistant works like this. First, the PDF of a lesson is read and cut into chunks, page by page. Then Ollama (qwen3-embedding:0.6b) turns the chunks into vectors, and the vectors are saved in ChromaDB. When a student asks a question, the system looks for the closest chunks inside that course and lets DeepSeek write the answer. The answer comes back token by token.
For security, the system uses JWT (HMAC-SHA256) for login and for role-based access. The gateway removes the headers X-User-Id and X-User-Role that the client sends, and then creates them again from the verified claims. This stops a user from pretending to have another role. The API also has a rate limit of 20 requests per 60 seconds.
The diagram below shows the main parts of the system and the path of a request:
graph TD
UserClient["Client browser<br/>(Angular 22 SPA :4200)"]
Gateway["YARP reverse proxy<br/>(:5000 in Docker / :7266 local HTTPS)"]
subgraph "Backend services"
DotNetAPI["EdOU Core API (.NET 10)<br/>(:5001 / :80)"]
SQLServer[("Microsoft SQL Server 2022<br/>(:1433)")]
Cloudinary[("Cloudinary cloud storage<br/>(media / PDFs / avatars)")]
end
subgraph "AI and RAG microservice"
FastAPI["EdOU RAG service (FastAPI)<br/>(:8000, internal)"]
ChromaDB[("ChromaDB vector store<br/>(:8001)")]
Ollama[("Ollama embeddings<br/>(:11434 - qwen3-embedding:0.6b)")]
DeepSeek["DeepSeek LLM API<br/>(deepseek-chat)"]
end
UserClient -->|"/api/auth/*, /api/courses/*..."| Gateway
UserClient -->|"/api/rag/chat, /api/rag/ws/*"| Gateway
Gateway -->|"Route /api/* (adds X-User-Id / Role)"| DotNetAPI
Gateway -->|"Route /api/rag/* (removes the /api/rag prefix)"| FastAPI
DotNetAPI -->|"EF Core 10 (CRUD / migrations)"| SQLServer
DotNetAPI -->|"Uploads / deletes files"| Cloudinary
DotNetAPI -->|"Starts document loading (IRagServiceClient)"| FastAPI
FastAPI -->|"Stores / searches vectors"| ChromaDB
FastAPI -->|"Creates embeddings"| Ollama
FastAPI -->|"Writes the answer and streams it"| DeepSeek
FastAPI -->|"Downloads the PDF and cuts it into chunks"| Cloudinary
/api/auth/loginand/api/auth/register/**are public. They go straight toEdOU.APIand they do not need a token.- For all other
/api/**routes, the gateway checks the JWT signature and expiry. If the token is valid, the gateway deletes the headersX-User-IdandX-User-Rolethat come from the client. Then it creates them again from the claims in the token. So a client cannot choose its own role. - Routes that start with
/api/rag/**also need a JWT. After that, the gateway removes the/api/ragprefix and forwards the request to the FastAPI microservice. - For WebSocket connections at
/api/rag/ws/**, the browser cannot send anAuthorizationheader. So the gateway takes the token from the query string?access_token=<JWT>.
The Nginx container of the frontend also serves the SPA and forwards /api/ to the gateway.
| Layer | Technology | Notes |
|---|---|---|
| Backend | .NET 10, ASP.NET Core 10 | Web API, dependency injection, runs migrations at startup |
| EF Core 10.0.10 | Code-first, TPT inheritance for User, Module and Submission |
|
| SQL Server 2022 | Relational database | |
| YARP 2.3.0 | Reverse proxy; checks the JWT at the edge | |
| IdentityModel JWT 8.14.0 | Creates and checks tokens (HMAC-SHA256) | |
| CloudinaryDotNet 1.29.2 | Stores PDF files, media and avatars | |
| Frontend | Angular 22.1 | Standalone components, signals, no zone.js |
| Angular Material 22.1 | Data tables, dialogs, forms, snackbars | |
| RxJS 7.8 | HTTP calls and interceptors | |
| AI / RAG | FastAPI 0.115 on Python 3.13 | Loads documents and streams the answer |
| ChromaDB 0.5+ | Stores vectors and filters by course_id |
|
Ollama, qwen3-embedding:0.6b |
Creates embeddings on the local machine | |
DeepSeek deepseek-chat |
Writes the answer | |
| pdfplumber, LangChain text splitters | Reads PDF files and cuts the text into chunks | |
| Testing | pytest 9.1.1, Selenium 4.48 | RBAC, rate limit, TTFT |
| Deployment | Docker Compose | 8 containers on the bridge network edou-network |
| Cloudflare Tunnel | Publishes the system without opening ports on the server |
| Student registration | Instructor registration |
|---|---|
![]() |
![]() |
EdOU/
├── Backend/ # .NET 10 solution
│ ├── EdOU.API/ # ASP.NET Core Web API (controllers, middleware, DI)
│ ├── EdOU.ApiGateway/ # YARP reverse proxy and gateway authentication
│ ├── EdOU.BLL/ # Business layer (services, strategy handlers, RAG client)
│ ├── EdOU.Common/ # Constants, DTOs, enums, exceptions
│ └── EdOU.DAL/ # EF Core (DbContext, entities, repositories, migrations)
├── Frontend/edou-client/ # Angular 22 SPA (features, layouts, shared)
├── Microservices/edou-fastapi/ # RAG microservice (FastAPI, ChromaDB, Ollama, DeepSeek)
├── Test/ # pytest suites (rag_ws, rate_limit_api, rbac)
├── Document/ # Documents, logo and screenshots
└── docker-compose.yml # Runs the 8 containers
- Docker and Docker Compose v2.
- A
.envfile created from.env.example. You need to set these values:SQLSERVER_SA_PASSWORD: the SA password of SQL Server.DEEPSEEK_API_KEY: the API key used to write the answers.CHAT_PROMPT_TEMPLATE: a prompt template that contains{context}and{question}. If it is empty, the AI chat shows an error.CLOUDFLARE_TUNNEL_TOKEN: only needed when you run thecloudflaredservice. This value has no default.
- The backend reads the
JwtandCloudinarysettings fromappsettings.json. This file is in.gitignore, so you have to create it fromappsettings.example.json.
docker compose up --buildAfter the system starts:
| Service | Address |
|---|---|
| Online demo | https://edou.id.vn |
| API Gateway | http://localhost:5000 |
| Main API | http://localhost:5001 |
| SQL Server 2022 | localhost:1433 |
| ChromaDB | localhost:8001 |
| Ollama | localhost:11434 |
| RAG microservice | localhost:8000 (only on loopback) |
| Cloudflare Tunnel | Connects out only; no inbound port |
The system is still online at edou.id.vn, but it runs on the author's personal computer, so it is not on 24/7. If you want to see the demo, please contact the author first so he can turn the computer on.
When you work on the frontend, you can run it with
ng serve(port 4200) or use the Nginx container. The proxy configuration is inFrontend/edou-client/proxy.conf.json.
There are three test suites in the Test/ folder:
| Suite | What it checks |
|---|---|
rbac/ |
Permissions: it calls the API with a student token, checks the Angular route guard and tests the login flow with Selenium |
rate_limit_api/ |
Rate limit: it sends 30 requests at the same time and checks that the quota comes back after 60 seconds |
rag_ws/ |
AI assistant speed: it measures the time to the first token (TTFT) for 20 questions |
cd Test
pip install -r requirements.txt
# each suite has its own config.py, so run them one by one
pytest rbac
pytest rate_limit_api
pytest rag_wsThe tests need the system to be running: the backend at https://localhost:7028 and the frontend at http://localhost:4200. The sample accounts are admin@ou.edu.vn / P@ssw0rd, student1@ou.edu.vn and instructor1@ou.edu.vn / 12345678. The rag_ws suite also needs CHAT_PROMPT_TEMPLATE, and it writes the result to Test/rag_ws/reports/ttft_benchmark_results.csv.
This section records the real state of each part when the project was finished, including the parts that are not complete:
| Item | Status |
|---|---|
| Semesters, subjects, courses, sections, polymorphic modules | Done (API and UI) |
| Discussion forums (topics, posts, pin, lock, show/hide, attachments) | Done; replies can be nested only one level |
| AI assistant that answers from course documents, streams over WebSocket and shows page numbers | Done |
| PDF loading → vectors → ChromaDB when a document lesson is created or updated | Done, only for .pdf files |
| Rate limit, permissions, TTFT measurement | Test suites are available |
| Submissions and grading | The API is done; the submission and grading UI is not finished |
| Assessments (quiz/essay) | The UI only shows the settings; students cannot take a test yet |
AI practice generation (/api/lessons/{id}/practices/ai-generate) |
It only creates a practice record; it does not generate questions yet |
| Video/audio player | The lesson type exists in the backend; the UI shows the file as a link to open or download |
| Server-side rendering | There is skeleton code (server.ts), but the production build is still a static SPA served by Nginx |
This project is released under the MIT license. See LICENSE.
While I worked on this project, I used Gemini, ChatGPT and DeepSeek to help me write code: to suggest solutions, to find bugs and to review the code. About 50% of the source code was written with help from these tools.
I did the planning, the requirements analysis, the architecture design, the technology choices, the testing and the final work on the system myself, and I am responsible for them. AI was only a tool and did not decide the direction of the project. This README was also written with help from AI, and I checked the content again.
© 2026 - EdOU - Developed by Hoàng Phi Hùng - Ho Chi Minh City Open University
















