The SABIFY AI Service is a Python-based AI microservice responsible for providing artificial intelligence capabilities to the SABIFY Learning Management System (LMS) Link.
The service is currently focused on AI-powered multiple-choice quiz generation. It is designed as a separate service so that AI functionality can evolve independently from the main Go LMS application.

SABIFY uses a separate Python AI service to handle AI-related operations.
The main SABIFY LMS is built with Go, while AI functionality is implemented in Python.
The current architecture is:
SABIFY LMS
Go Application
|
|
| HTTP POST
v
+----------------------+
| Python AI Service |
| FastAPI |
+----------------------+
|
v
Hugging Face Router
|
v
DeepSeek-V3-0324
|
v
Generated Quiz JSON
|
v
Python Validation
|
v
Shuffle Answer Options
|
v
Go LMS
The AI service does not directly save quizzes to the database.
Instead:
Teacher
↓
Generate with AI
↓
Go LMS
↓
Python AI Service
↓
Generated Questions
↓
Go LMS
↓
Teacher Reviews Questions
↓
Teacher Edits/Adds/Removes Questions
↓
Create Quiz
↓
PostgreSQL
This ensures that teachers remain in control of the final quiz.
SABIFY currently uses a service-oriented architecture for AI functionality.
The main LMS is responsible for:
- Authentication
- Authorization
- Courses
- Course materials
- Quizzes
- Questions
- Student submissions
- Automatic grading
- Teacher dashboards
- Student dashboards
- Database operations
Technology:
Go
Chi Router
PostgreSQL
Go Templates
The AI service is responsible for:
- AI quiz generation
- Prompt construction
- Communication with Hugging Face
- LLM response processing
- JSON extraction
- Response validation
- Answer-option randomization
- Returning structured quiz data to the LMS
Technology:
Python
FastAPI
Pydantic
HTTPX
Hugging Face Router
DeepSeek-V3-0324
| Component | Technology |
|---|---|
| Language | Python |
| API Framework | FastAPI |
| Data Validation | Pydantic |
| HTTP Client | HTTPX |
| AI Provider | Hugging Face Router |
| AI Model | deepseek-ai/DeepSeek-V3-0324 |
| API Format | JSON |
| Main LMS | Go |
| Database | PostgreSQL |
A simplified version of the AI service structure is:
ai-service/
│
├── app/
│ ├── api/
│ │ └── routes/
│ │ └── quiz.py
│ │
│ ├── core/
│ │ └── config.py
│ │
│ ├── models/
│ │ └── schemas.py
│ │
│ ├── services/
│ │ └── quiz_generator.py
│ │
│ └── main.py
│
├── .env
├── requirements.txt
└── README.md
The AI service requires a Hugging Face API token.
Example .env:
HF_TOKEN=your_huggingface_tokenThe Go LMS also needs to know where the AI service is running.
Example Go .env:
AI_SERVICE_URL=http://localhost:8082The AI service and Go LMS are therefore connected through HTTP.
Install dependencies:
pip install -r requirements.txtRun FastAPI:
uvicorn app.main:app --reload --port 8082The AI service will then be available at:
http://localhost:8082
The quiz generation endpoint is:
POST http://localhost:8082/api/v1/quiz/generate
POST /api/v1/quiz/generateThe endpoint generates multiple-choice questions based on either:
- A topic
- Uploaded course material
At least one of these must contain usable content.
The request follows this schema:
class QuizGenerationRequest(BaseModel):
course_id: Optional[str] = None
material_id: Optional[str] = None
material_text: Optional[str] = None
topic: Optional[str] = None
number_of_questions: int = Field(default=5, ge=1, le=20)
difficulty: str = Field(default="basic")
question_type: str = Field(default="multiple_choice")| Field | Type | Required | Description |
|---|---|---|---|
course_id |
string | No | ID of the SABIFY course |
material_id |
string | No | ID of course material |
material_text |
string | No | Text extracted from course material |
topic |
string | No* | Topic to generate questions about |
number_of_questions |
integer | No | Number of questions |
difficulty |
string | No | Question difficulty |
question_type |
string | No | Type of questions |
topic is required when material_text is not supplied.
A simple topic-based request:
{
"course_id": "e3cbe98b-3f17-4664-bc59-70129f037e09",
"topic": "Biology",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}The Go LMS currently sends the quiz title and description as the topic.
For example, the teacher entered:
Quiz title:
digestive system
Description:
About digestive system
The Go application combines these into:
digestive system: About digestive system
and sends the request to the AI service.
The browser payload was:
{
"course_id": "e3cbe98b-3f17-4664-bc59-70129f037e09",
"title": "digestive system",
"description": "About digestive system",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}The Go service converts this into the AI service request.
The AI service returns:
class GeneratedQuestion(BaseModel):
question: str
options: List[str]
correct_answer: int
explanation: str
class QuizGenerationResponse(BaseModel):
questions: List[GeneratedQuestion]Each question contains:
| Field | Type | Description |
|---|---|---|
question |
string | The question |
options |
array | Exactly four answer options |
correct_answer |
integer | Zero-based index of the correct option |
explanation |
string | Explanation of the correct answer |
The correct_answer field uses a zero-based index.
0 = Option A
1 = Option B
2 = Option C
3 = Option D
Example:
{
"options": [
"Epidermis",
"Dermis",
"Hypodermis",
"Stratum corneum"
],
"correct_answer": 0
}The correct answer is:
Option A - Epidermis
One successful response from the AI service generated Biology questions about the skin:
{
"questions": [
{
"question": "What is the outermost layer of the skin called?",
"options": [
"Epidermis",
"Dermis",
"Hypodermis",
"Stratum corneum"
],
"correct_answer": 0,
"explanation": "The epidermis is the outermost layer of the skin, providing a waterproof barrier and creating our skin tone."
},
{
"question": "Which layer of the skin contains blood vessels and nerve endings?",
"options": [
"Epidermis",
"Dermis",
"Hypodermis",
"Subcutaneous layer"
],
"correct_answer": 1,
"explanation": "The dermis is the middle layer of the skin, containing tough connective tissue, hair follicles, sweat glands, blood vessels, and nerve endings."
},
{
"question": "What is the primary function of the hypodermis?",
"options": [
"Protection from UV radiation",
"Temperature regulation",
"Fat storage and insulation",
"Sensation of touch"
],
"correct_answer": 2,
"explanation": "The hypodermis, also called the subcutaneous layer, is primarily responsible for fat storage and insulation, helping to regulate body temperature."
},
{
"question": "Which of the following is NOT a function of the skin?",
"options": [
"Protection from pathogens",
"Vitamin D synthesis",
"Production of red blood cells",
"Regulation of body temperature"
],
"correct_answer": 2,
"explanation": "While the skin performs many functions, red blood cells are produced in the bone marrow, not the skin."
},
{
"question": "What gives skin its color?",
"options": [
"Keratin",
"Melanin",
"Collagen",
"Elastin"
],
"correct_answer": 1,
"explanation": "Melanin is the pigment produced by melanocytes in the epidermis that gives skin its color and provides protection from UV radiation."
}
]
}The service was also tested with English/Literature content about figures of speech.
The model initially returned:
{
"questions": [
{
"question": "Which figure of speech involves the repetition of the same sound at the beginning of words in a sentence?",
"options": [
"Alliteration",
"Metaphor",
"Simile",
"Hyperbole"
],
"correct_answer": 0
},
{
"question": "What figure of speech directly compares two unlike things using 'like' or 'as'?",
"options": [
"Simile",
"Metaphor",
"Personification",
"Onomatopoeia"
],
"correct_answer": 0
},
{
"question": "Which figure of speech gives human characteristics to non-human objects or animals?",
"options": [
"Personification",
"Alliteration",
"Irony",
"Oxymoron"
],
"correct_answer": 0
},
{
"question": "What figure of speech involves an exaggerated statement not meant to be taken literally?",
"options": [
"Hyperbole",
"Metonymy",
"Synecdoche",
"Euphemism"
],
"correct_answer": 0
},
{
"question": "Which figure of speech involves a contradiction in terms, often for emphasis or humor?",
"options": [
"Oxymoron",
"Paradox",
"Allusion",
"Apostrophe"
],
"correct_answer": 0
}
]
}Although these questions were correct, every correct answer was placed at index 0.
This created an undesirable pattern:
Question 1 → A
Question 2 → A
Question 3 → A
Question 4 → A
Question 5 → A
The service therefore performs answer-option shuffling before returning the response.
The generation process is:
1. Receive request
↓
2. Determine whether material or topic is provided
↓
3. Build context
↓
4. Build system prompt
↓
5. Build user prompt
↓
6. Send request to Hugging Face
↓
7. Receive LLM response
↓
8. Clean response
↓
9. Parse JSON
↓
10. Validate questions
↓
11. Shuffle answer options
↓
12. Update correct_answer
↓
13. Return QuizGenerationResponse
When no course material is provided, the service uses the topic.
Example:
{
"topic": "Newton's Laws of Motion",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}The AI should generate questions specifically about Newton's Laws of Motion.
For example:
Which law states that an object remains at rest or in uniform motion unless acted upon by an external force?
The AI must not generate unrelated questions about programming, databases, or other subjects.
The service also supports generating questions from uploaded material.
When material_text is supplied:
{
"material_text": "Photosynthesis is the process by which green plants convert light energy into chemical energy...",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}The service uses:
Material Text:
"""
Photosynthesis is the process by which green plants...
"""
as the context for the AI.
Material-based generation is intended to allow questions to be generated from actual course content.
The service currently uses the following logic:
if request.material_text and request.material_text.strip():
context_prompt = f"Material Text:\n\"\"\"\n{request.material_text}\n\"\"\""
else:
context_prompt = f"Topic: {request.topic}"Therefore:
material_text exists
↓
use material_text
Otherwise:
topic
↓
use topic
The system prompt explicitly instructs the model to stay within the requested subject.
Important requirements include:
Every question MUST be directly related to the provided topic.
Every answer option MUST be relevant to that question and topic.
Every explanation MUST explain the answer using knowledge relevant to that topic.
Never substitute your own topic for the user's topic.
Never introduce an unrelated subject.
For example:
Topic: Biology
must produce Biology questions.
It should not produce:
Programming
Databases
Software Engineering
Computer Networks
unless those subjects are explicitly requested.
The LLM provides the initial correct answer index.
For example:
{
"options": [
"Alliteration",
"Metaphor",
"Simile",
"Hyperbole"
],
"correct_answer": 0
}The Python service then shuffles the options.
Before:
A → Alliteration ← Correct
B → Metaphor
C → Simile
D → Hyperbole
After shuffling:
A → Simile
B → Hyperbole
C → Alliteration ← Correct
D → Metaphor
The service updates:
"correct_answer": 2The correct answer therefore remains correct even though its position changes.
This is important because the AI may repeatedly place the correct answer at position 0.
The service uses:
@staticmethod
def _shuffle_question_options(question: dict) -> None:
"""
Shuffle the answer options while keeping correct_answer
pointing to the correct option.
"""
options = question["options"]
correct_index = question["correct_answer"]
correct_option = options[correct_index]
random.shuffle(options)
question["correct_answer"] = options.index(correct_option)This ensures that:
Correct option before shuffle
↓
Option is saved
↓
Options are shuffled
↓
Correct option is located again
↓
correct_answer is updated
The service validates the LLM output before returning it.
The number of returned questions must equal the requested number.
For example:
Requested: 5
Returned: 5
is valid.
But:
Requested: 5
Returned: 4
causes an error.
Every question must contain exactly four options.
Valid:
"options": [
"A",
"B",
"C",
"D"
]Invalid:
"options": [
"A",
"B"
]Invalid:
"options": [
"A",
"B",
"C",
"D",
"E"
]The service rejects placeholder answers such as:
Option 0
Option 1
Option 2
Option 3
string
This protects the LMS from receiving unusable generated questions.
Each question must contain a meaningful explanation.
The service rejects:
"explanation": "string"
because this is usually an LLM/schema placeholder rather than an educational explanation.
LLMs can sometimes return JSON surrounded by additional content.
The service contains:
_clean_llm_json()which handles:
- DeepSeek reasoning tokens
- Markdown code fences
- Extra text surrounding JSON
- Extracting the outer JSON object
For example, if the model returns:
```json
{
"questions": [...]
}
the service extracts:
```json
{
"questions": [...]
}
before parsing it.
The service uses FastAPI HTTPException for expected API errors.
General errors are converted into HTTP 500 responses.
Possible errors include:
400 Bad Request
500 Internal Server Error
503 Service Unavailable
If both are empty:
{
"topic": "",
"material_text": ""
}the endpoint returns:
400 Bad Request
with:
Either 'material_text' or 'topic' must be provided.
If the application requests:
{
"number_of_questions": 5
}but the AI returns four questions, the service rejects the response.
Error:
AI returned an incorrect number of questions.
If an AI response contains fewer or more than four options:
AI returned a question without exactly four options.
the response is rejected.
If the AI returns:
{
"options": [
"Option 0",
"Option 1",
"Option 2",
"Option 3"
]
}the service rejects it.
Error:
AI returned placeholder answer options.
If the Hugging Face API does not return HTTP 200, the service raises an HTTP error containing the Hugging Face response.
This helps identify provider-side problems such as:
- Invalid API token
- Rate limits
- Model availability
- Provider errors
- Network failures
If the LLM returns invalid JSON, the service raises:
Failed to parse LLM JSON output
The raw LLM response is included in the server-side error for debugging.
The browser showed:
TypeError: Failed to fetch
and Firefox showed:
TypeError: NetworkError
The Go server, however, showed that the request successfully generated a response.
Example:
POST /teacher/quizzes/generate HTTP/1.1 200
with a response time of approximately:
23.994483026s
The Go HTTP server originally had:
WriteTimeout: 10 * time.Second,while AI generation could take approximately 24 seconds.
Therefore:
AI generation
≈ 24 seconds
Go WriteTimeout
= 10 seconds
The server's write timeout was too short for the AI operation.
The timeout was increased to:
WriteTimeout: 60 * time.Second,The Go server must be restarted after changing this value.
The final configuration is:
srv := &http.Server{
Addr: cfg.addr,
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 5 * time.Second,
WriteTimeout: 60 * time.Second,
}A direct curl request returned:
HTTP/1.1 303 See Other
Location: /login
The Go LMS protects the quiz generation endpoint with authentication.
The curl request did not contain the authenticated SABIFY session cookie.
Therefore the LMS correctly redirected the request to:
/login
This does not indicate that the AI service is broken.
The browser request works because the teacher is already authenticated in SABIFY.
The AI generated questions such as:
Question 1 → A
Question 2 → A
Question 3 → A
Question 4 → A
Question 5 → A
The questions themselves were correct, but the quiz became predictable.
The LLM frequently placed the correct answer at index 0.
Python now randomly shuffles the four options and updates correct_answer.
This makes the final quiz less predictable.
A Biology request could occasionally result in unrelated questions such as programming questions.
LLMs are probabilistic and can sometimes drift from the requested subject if the prompt does not enforce topic relevance strongly enough.
The system prompt was strengthened with explicit topic-relevance rules.
The model is instructed:
The target topic is the ONLY subject you are allowed to generate questions about.
Do not change the subject.
Do not introduce another subject.
Do not generate generic questions.
Do not generate programming questions unless programming is the target topic.
Topic adherence is therefore explicitly enforced at the prompt level.
The service can be tested independently of the Go LMS.
Example:
curl -X POST http://localhost:8082/api/v1/quiz/generate \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_HF_TOKEN" \
-d '{
"topic": "Biology",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}'The service should return:
{
"questions": [...]
}with exactly five questions.
The normal production-like flow is:
1. Start PostgreSQL
2. Start Python AI service
3. Start Go LMS
4. Login as teacher
5. Open Create Quiz
6. Select course
7. Enter quiz title
8. Enter description
9. Click Generate Quiz with AI
10. Review generated questions
11. Edit questions if necessary
12. Add/remove questions if necessary
13. Click Create Quiz
When generation succeeds:
Generate with AI
↓
Generating...
↓
AI response received
↓
Question builder populated
↓
AI Generated badge displayed
The teacher should then be able to modify the generated questions before saving.
The Go LMS calls:
POST /api/v1/quiz/generate
on the Python service.
The Go client sends:
{
"course_id": "...",
"topic": "...",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}The Python service returns:
{
"questions": [
{
"question": "...",
"options": [
"...",
"...",
"...",
"..."
],
"correct_answer": 1,
"explanation": "..."
}
]
}The Go LMS converts the response into the existing quiz-question builder.
The Python service does not decide whether a teacher owns a course.
The Go LMS performs this validation before calling the AI service.
The Go application verifies:
Current user
↓
Authenticated?
↓
Teacher?
↓
Course exists?
↓
Course belongs to teacher?
↓
Call AI service
This prevents a teacher from using the endpoint to generate content for another teacher's course.
The AI service does not directly access the SABIFY PostgreSQL database.
It only generates content.
The database flow is:
AI Service
↓
Generated JSON
↓
Go LMS
↓
Teacher Review
↓
Existing Create Quiz Handler
↓
PostgreSQL
This separation keeps the AI service independent from the LMS database layer.
{
"course_id": "string",
"material_id": "string",
"material_text": "string",
"topic": "string",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}All fields except number_of_questions, difficulty, and question_type are optional at the API schema level.
However, either:
material_text
or:
topic
must contain usable content.
{
"questions": [
{
"question": "string",
"options": [
"string",
"string",
"string",
"string"
],
"correct_answer": 0,
"explanation": "string"
}
]
}The actual service enforces:
questions = requested number
options = exactly 4
correct_answer = 0-3
explanation = meaningful
Request:
{
"topic": "Biology - Human Digestive System",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}Expected content:
What is the primary function of the small intestine?
A. Absorb nutrients
B. Store bile
C. Produce red blood cells
D. Pump blood
The service should not generate:
What is a variable in Python?
because that is unrelated to the requested topic.
Request:
{
"topic": "Newton's Laws of Motion",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}Expected content should focus on:
- Newton's First Law
- Newton's Second Law
- Newton's Third Law
- Force
- Mass
- Acceleration
- Inertia
Request:
{
"topic": "Figures of Speech",
"number_of_questions": 5,
"difficulty": "basic",
"question_type": "multiple_choice"
}Questions may cover:
- Simile
- Metaphor
- Alliteration
- Hyperbole
- Personification
- Oxymoron
- Onomatopoeia
AI generation is currently performed synchronously.
A typical request can take several seconds because the Go application must:
Browser
↓
Go
↓
Python
↓
Hugging Face
↓
DeepSeek
↓
Python
↓
Go
↓
Browser
AI generation has previously taken approximately 24 seconds for a five-question request.
The Go server therefore uses:
WriteTimeout: 60 * time.SecondThe Python HTTP client also uses:
httpx.Client(timeout=60.0)The current MVP does not yet automatically:
- Read all course materials
- Retrieve relevant sections from PDFs
- Perform RAG
- Use previous examination questions
- Automatically determine the teacher's preferred question distribution
- Automatically detect every possible hallucination
- Automatically fact-check every generated question
- Save generated quizzes directly to the database
- Generate all possible question types
The teacher remains responsible for reviewing AI-generated questions before publishing.
Possible future improvements include:
Instead of only using the quiz title and description:
Course
↓
Course Materials
↓
Relevant Material
↓
AI Quiz Generation
A future version can use:
Course Materials
↓
Chunking
↓
Embeddings
↓
Vector Database
↓
Relevant Context
↓
LLM
↓
Quiz
Possible vector databases include:
pgvector
Qdrant
FAISS
Support:
Basic
Intermediate
Advanced
or:
Easy
Medium
Hard
Future versions can support:
Multiple Choice
True / False
Short Answer
Fill in the Blank
Essay
Matching
The same AI service can eventually support:
Teacher
↓
AI Teacher Assistant
↓
Lesson planning
Question generation
Explanations
Student performance analysis
Teaching recommendations
Students could interact with:
Student
↓
AI Learning Coach
↓
Study recommendations
Weak-topic detection
Personalized explanations
Revision plans
Future AI analytics could use:
Quiz Results
Course Activity
Submission History
Learning Progress
↓
AI/ML
↓
Learning Insights
↓
Teacher Dashboard
The SABIFY AI service follows an important principle:
AI generates; the teacher decides.
The AI is an assistant rather than the final authority.
The workflow is intentionally:
AI Generation
↓
Validation
↓
Teacher Review
↓
Teacher Editing
↓
Publication
This makes the system safer and more useful for real educational environments.
The SABIFY AI Service provides an independent AI layer for the SABIFY LMS.
Current functionality:
✓ FastAPI API
✓ Hugging Face integration
✓ DeepSeek-V3-0324
✓ Topic-based quiz generation
✓ Material-based quiz generation
✓ Structured JSON responses
✓ Four-option MCQs
✓ Correct-answer indexing
✓ Response validation
✓ Placeholder detection
✓ JSON cleaning
✓ Answer-option shuffling
✓ Go LMS integration
✓ Teacher review workflow
Current primary endpoint:
POST /api/v1/quiz/generate
The service is designed to expand beyond quiz generation into a broader AI education platform supporting intelligent teaching, learning, analytics, personalization, and educational assistance.