.env.example DELETED
@@ -1,2 +0,0 @@
1
- GOOGLE_API_KEY=<google_api_key>
2
- DATABASE_URL=postgresql+asyncpg://<username>:<password>@<host>:<port>/<database_name>
 
 
 
.github/workflows/lint.yml DELETED
@@ -1,23 +0,0 @@
1
- name: Ruff lint
2
-
3
- on: [push, pull_request]
4
-
5
- jobs:
6
- ruff:
7
- runs-on: ubuntu-latest
8
- steps:
9
- - name: Checkout code
10
- uses: actions/checkout@v2
11
-
12
- - name: Set up Python
13
- uses: actions/setup-python@v2
14
- with:
15
- python-version: '3.10'
16
-
17
- - name: Install dependencies
18
- run: |
19
- python -m pip install --upgrade pip
20
- pip install ruff
21
-
22
- - name: Run Ruff
23
- run: ruff check .
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore DELETED
@@ -1,9 +0,0 @@
1
- __pycache__/
2
- .env
3
-
4
- venv/
5
-
6
- *.log
7
-
8
- app.log
9
-
 
 
 
 
 
 
 
 
 
 
Dockerfile DELETED
@@ -1,20 +0,0 @@
1
- # Use official Python image
2
- FROM python:3.10-slim
3
-
4
- # Set work directory
5
- WORKDIR /app
6
-
7
- # Copy requirements
8
- COPY requirements.txt .
9
-
10
- # Install dependencies
11
- RUN pip install --no-cache-dir -r requirements.txt
12
-
13
- # Copy app code
14
- COPY . .
15
-
16
- # Expose port (Hugging Face Spaces expects 7860 or 8000, but FastAPI default is 8000)
17
- EXPOSE 7860
18
-
19
- # Start FastAPI with uvicorn on port 7860 for Hugging Face Spaces
20
- CMD ["uvicorn", "app.api.routes:router", "--host", "0.0.0.0", "--port", "7860"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/api/__init__.py DELETED
@@ -1 +0,0 @@
1
- from .routes import router #noqa
 
 
app/api/routes.py DELETED
@@ -1,56 +0,0 @@
1
- from fastapi import APIRouter,status, Query, Request
2
- from fastapi.responses import HTMLResponse
3
- from app.services.gemini import GenerateAnswer
4
- from app.schemas import SOLOLevel, MCQResponse
5
- from app.core.exceptions import CustomException
6
- from fastapi.templating import Jinja2Templates
7
- from app.core.logging import logger
8
-
9
- templates = Jinja2Templates(directory="app/templates")
10
-
11
- answer_service = GenerateAnswer()
12
-
13
- router = APIRouter()
14
-
15
-
16
- @router.get("/generate_mcq", response_class=HTMLResponse)
17
-
18
- async def generate_mcq(
19
- request: Request,
20
- topic: str = Query(default=None),
21
- solo_level: SOLOLevel = Query(default=None),
22
- ):
23
- """
24
- Generate a multiple-choice question (MCQ) based on the provided topic and SOLO level in jinja template.
25
- """
26
- if topic and solo_level:
27
- mcq = await answer_service.generate_mcq(
28
- topic=topic, solo_level=solo_level
29
- ) # generates mcq
30
- if not mcq:
31
- raise CustomException(detail="No MCQ generated.")
32
- return templates.TemplateResponse(
33
- "index.html", {"request": request, "mcq": mcq}
34
- )
35
- return templates.TemplateResponse("index.html", {"request": request})
36
-
37
-
38
- @router.get(
39
- "/api/generate_mcq", status_code=status.HTTP_200_OK, tags=["MCQ Generation "]
40
- )
41
- async def generate_mcq_api(
42
- topic: str,
43
- solo_level: SOLOLevel = Query(...),
44
- ) -> MCQResponse:
45
- """
46
- Generate a multiple-choice question (MCQ) based on the provided topic and SOLO level.
47
- """
48
- try:
49
- mcq = await answer_service.generate_mcq(topic=topic, solo_level=solo_level)
50
- if not mcq:
51
- raise CustomException(detail="No MCQ generated.")
52
- return mcq
53
-
54
- except Exception:
55
- logger.exception("Unexpected error while generating MCQ.")
56
- raise CustomException(detail="Internal server error during MCQ generation.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/__init__.py DELETED
File without changes
app/core/config.py DELETED
@@ -1,10 +0,0 @@
1
- from pydantic_settings import BaseSettings, SettingsConfigDict
2
-
3
- class Settings(BaseSettings):
4
- GOOGLE_API_KEY: str
5
-
6
- model_config=SettingsConfigDict(
7
- env_file=".env"
8
- )
9
-
10
- settings = Settings()
 
 
 
 
 
 
 
 
 
 
 
app/core/exceptions.py DELETED
@@ -1,15 +0,0 @@
1
- from http import HTTPStatus
2
- from fastapi import HTTPException, status
3
-
4
-
5
- class CustomException(HTTPException):
6
- def __init__(
7
- self,
8
- status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
9
- detail: str | None = None,
10
- ):
11
- if not detail:
12
- detail = HTTPStatus(status_code).description
13
-
14
- super().__init__(status_code=status_code, detail=detail)
15
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/core/logging.py DELETED
@@ -1,11 +0,0 @@
1
- import logging
2
-
3
- def logger_setup():
4
- logging.basicConfig(
5
- level=logging.INFO,
6
- format="%(asctime)s - %(levelname)s - %(message)s"
7
- )
8
- logger = logging.getLogger(__name__)
9
- return logger
10
-
11
- logger = logger_setup()
 
 
 
 
 
 
 
 
 
 
 
 
app/schemas/__init__.py DELETED
@@ -1 +0,0 @@
1
- from .schemas import * #noqa
 
 
app/schemas/schemas.py DELETED
@@ -1,18 +0,0 @@
1
- from enum import Enum
2
- from pydantic import BaseModel, Field
3
-
4
- class SOLOLevel(str,Enum):
5
- """
6
- Enum for SOLO levels.
7
- """
8
- UNISTRUCTURAL = "unistructural"
9
- MULTISTRUCTURAL = "multistructural"
10
-
11
-
12
- class MCQResponse(BaseModel):
13
- """
14
- Response model for MCQ generation.
15
- """
16
- question_text: str = Field(..., description="The generated question text.")
17
- options: list[str] = Field(..., description="List of answer options.")
18
- correct_answer: str = Field(..., description="The correct answer from the options.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/services/__init__.py DELETED
File without changes
app/services/gemini.py DELETED
@@ -1,50 +0,0 @@
1
- from langchain_google_genai import ChatGoogleGenerativeAI
2
- from langchain_core.prompts import PromptTemplate
3
- from app.core.config import settings
4
- from app.schemas import MCQResponse
5
-
6
-
7
- class GenerateAnswer:
8
- """
9
- Class to generate answers using Google Gemini API.
10
- """
11
-
12
- def __init__(self):
13
- self.llm = ChatGoogleGenerativeAI(
14
- model="gemini-2.0-flash",
15
- temperature=0.6,
16
- api_key=settings.GOOGLE_API_KEY,
17
- )
18
-
19
- async def generate_mcq(self, topic: str, solo_level: str):
20
- """
21
- Generate an answer to the given question using Google Gemini API.
22
- """
23
- prompt = PromptTemplate(
24
- template="""You are an AI tutor. Based on the SOLO taxonomy level and the content snippet provided, generate a single multiple-choice question (MCQ) that matches the SOLO level.
25
-
26
- Content Snippet:
27
- \"\"\"
28
- Photosynthesis is the process by which plants use sunlight, water, and carbon dioxide to create glucose and oxygen. Chlorophyll absorbs sunlight.
29
- \"\"\"
30
-
31
-
32
- SOLO Level: {solo_level}
33
- You should be based on this Topic: {topic}
34
-
35
- SOLO Level Consideration:
36
- - Unistructural: Focus on recalling a single piece of information from the content_snippet.
37
- - Multistructural: Focus on recalling several pieces of information from the content_snippet.
38
-
39
- Generate one MCQ with:
40
- - "question_text": A single question aligned to the SOLO level
41
- - "options": 3–4 plausible answer choices
42
- - "correct_answer": The correct answer (must match one of the options)""",
43
- input_variables=["topic", "solo_level"],
44
- )
45
- model=self.llm.with_structured_output(MCQResponse)
46
- chain=prompt | model
47
- response= await chain.ainvoke({"topic": topic, "solo_level": solo_level})
48
- return response
49
-
50
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/templates/index.html DELETED
@@ -1,361 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
-
4
- <head>
5
- <title>MCQ Generator</title>
6
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
- <style>
8
- :root {
9
- --primary: #4a6fa5;
10
- --secondary: #6a98d8;
11
- --accent: #ff6b6b;
12
- --light: #f8f9fa;
13
- --dark: #343a40;
14
- --success: #28a745;
15
- --danger: #dc3545;
16
- }
17
-
18
- * {
19
- margin: 0;
20
- padding: 0;
21
- box-sizing: border-box;
22
- transition: all 0.3s ease;
23
- }
24
-
25
- body {
26
- font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
27
- line-height: 1.6;
28
- color: var(--dark);
29
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
30
- min-height: 100vh;
31
- padding: 0;
32
- }
33
-
34
- .container {
35
- max-width: 800px;
36
- margin: 0 auto;
37
- padding: 20px;
38
- }
39
-
40
- header {
41
- background-color: var(--primary);
42
- color: white;
43
- padding: 20px 0;
44
- border-radius: 0 0 10px 10px;
45
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
46
- text-align: center;
47
- margin-bottom: 30px;
48
- }
49
-
50
- header h1 {
51
- margin: 0;
52
- font-size: 2.5rem;
53
- font-weight: 700;
54
- }
55
-
56
- header p {
57
- margin-top: 10px;
58
- font-size: 1.2rem;
59
- opacity: 0.9;
60
- }
61
-
62
- .card {
63
- background: white;
64
- border-radius: 12px;
65
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
66
- padding: 30px;
67
- margin-bottom: 30px;
68
- position: relative;
69
- overflow: hidden;
70
- }
71
-
72
- .card::before {
73
- content: '';
74
- position: absolute;
75
- top: 0;
76
- left: 0;
77
- width: 100%;
78
- height: 5px;
79
- background: linear-gradient(90deg, var(--primary), var(--secondary), var(--accent));
80
- }
81
-
82
- .form-group {
83
- margin-bottom: 20px;
84
- }
85
-
86
- label {
87
- display: block;
88
- margin-bottom: 8px;
89
- font-weight: 600;
90
- color: var(--dark);
91
- }
92
-
93
- input,
94
- select {
95
- width: 100%;
96
- padding: 12px 15px;
97
- border: 2px solid #dee2e6;
98
- border-radius: 8px;
99
- font-size: 16px;
100
- color: var(--dark);
101
- }
102
-
103
- input:focus,
104
- select:focus {
105
- border-color: var(--secondary);
106
- box-shadow: 0 0 0 3px rgba(106, 152, 216, 0.25);
107
- outline: none;
108
- }
109
-
110
- .btn {
111
- display: inline-block;
112
- font-weight: 600;
113
- text-align: center;
114
- white-space: nowrap;
115
- vertical-align: middle;
116
- user-select: none;
117
- border: none;
118
- padding: 12px 24px;
119
- font-size: 16px;
120
- line-height: 1.5;
121
- border-radius: 8px;
122
- cursor: pointer;
123
- transition: all 0.3s ease;
124
- }
125
-
126
- .btn-primary {
127
- color: white;
128
- background: linear-gradient(45deg, var(--primary), var(--secondary));
129
- box-shadow: 0 4px 6px rgba(74, 111, 165, 0.2);
130
- }
131
-
132
- .btn-primary:hover {
133
- transform: translateY(-2px);
134
- box-shadow: 0 6px 8px rgba(74, 111, 165, 0.3);
135
- }
136
-
137
- .btn-block {
138
- display: block;
139
- width: 100%;
140
- }
141
-
142
- .question {
143
- background: white;
144
- border-radius: 12px;
145
- box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
146
- padding: 25px;
147
- margin-top: 30px;
148
- position: relative;
149
- border-left: 5px solid var(--primary);
150
- }
151
-
152
- .question strong {
153
- color: var(--primary);
154
- font-size: 1.2rem;
155
- margin-bottom: 15px;
156
- display: block;
157
- }
158
-
159
- .question ul {
160
- list-style-type: none;
161
- padding: 0;
162
- margin: 20px 0;
163
- }
164
-
165
- .question li {
166
- padding: 10px 15px;
167
- border: 1px solid #dee2e6;
168
- border-radius: 8px;
169
- margin-bottom: 10px;
170
- transition: all 0.2s ease;
171
- cursor: pointer;
172
- }
173
-
174
- .question li:hover {
175
- background-color: #f8f9fa;
176
- transform: translateX(5px);
177
- }
178
-
179
- .correct-answer {
180
- margin-top: 20px;
181
- padding: 15px;
182
- background-color: rgba(40, 167, 69, 0.1);
183
- border-left: 3px solid var(--success);
184
- border-radius: 8px;
185
- display: none;
186
- }
187
-
188
- .correct-answer strong {
189
- color: var(--success);
190
- font-size: 1.1rem;
191
- margin-bottom: 5px;
192
- display: block;
193
- }
194
-
195
- .action-buttons {
196
- margin-top: 15px;
197
- display: none;
198
- }
199
-
200
- .btn-retry {
201
- background-color: var(--primary);
202
- color: white;
203
- margin-right: 10px;
204
- }
205
-
206
- .btn-show-answer {
207
- background-color: var(--secondary);
208
- color: white;
209
- }
210
-
211
- footer {
212
- text-align: center;
213
- padding: 20px;
214
- margin-top: 30px;
215
- color: var(--dark);
216
- font-size: 0.9rem;
217
- }
218
-
219
- /* Responsive design */
220
- @media (max-width: 768px) {
221
- .container {
222
- padding: 10px;
223
- }
224
-
225
- header h1 {
226
- font-size: 2rem;
227
- }
228
-
229
- .card {
230
- padding: 20px;
231
- }
232
-
233
- .btn {
234
- padding: 10px 20px;
235
- }
236
- }
237
- </style>
238
- </head>
239
-
240
- <body>
241
- <div class="container">
242
- <header>
243
- <h1>MCQ Generator</h1>
244
- <p>Create custom multiple-choice questions </p>
245
- </header>
246
-
247
- <div class="card">
248
- <form method="get" action="/generate_mcq">
249
- <div class="form-group">
250
- <label for="topic">Topic:</label>
251
- <input type="text" id="topic" name="topic" placeholder="Enter a subject or topic" required>
252
- </div>
253
-
254
- <div class="form-group">
255
- <label for="solo_level">SOLO Level:</label>
256
- <select id="solo_level" name="solo_level" required>
257
- <option value="" disabled selected>Select a level</option>
258
- <option value="unistructural">Unistructural (Basic facts)</option>
259
- <option value="multistructural">Multistructural (Multiple concepts)</option>
260
-
261
- </select>
262
- </div>
263
-
264
- <button type="submit" class="btn btn-primary btn-block">Generate Question</button>
265
- </form>
266
- </div>
267
-
268
- {% if mcq %}
269
- <div class="question">
270
- <strong>Question:</strong> {{ mcq.question_text }}
271
-
272
- <ul id="options-list">
273
- {% for option in mcq.options %}
274
- <li data-correct="{{ 'true' if option == mcq.correct_answer else 'false' }}"
275
- onclick="checkAnswer(this)">{{ option }}</li>
276
- {% endfor %}
277
- </ul>
278
-
279
- <div class="action-buttons" id="action-buttons">
280
- <button class="btn btn-retry" onclick="resetQuiz()">Try Again</button>
281
- <button class="btn btn-show-answer" onclick="showAnswer()">Show Answer</button>
282
- </div>
283
-
284
- <div class="correct-answer" id="feedback">
285
- <strong>✅ Correct Answer:</strong> {{ mcq.correct_answer }}
286
- </div>
287
- </div>
288
- {% endif %}
289
-
290
- <footer>
291
- <p>MCQ Generator</p>
292
- </footer>
293
- </div>
294
-
295
- <script>
296
- // Add some simple interactivity
297
- document.addEventListener('DOMContentLoaded', function () {
298
- // Highlight form fields on focus
299
- const formElements = document.querySelectorAll('input, select');
300
- formElements.forEach(element => {
301
- element.addEventListener('focus', function () {
302
- this.style.transform = 'translateY(-2px)';
303
- });
304
-
305
- element.addEventListener('blur', function () {
306
- this.style.transform = 'translateY(0)';
307
- });
308
- });
309
- });
310
-
311
- // MCQ interaction functions
312
- function checkAnswer(selectedLi) {
313
- const isCorrect = selectedLi.dataset.correct === 'true';
314
- const options = document.querySelectorAll('#options-list li');
315
-
316
- // Disable all options
317
- options.forEach(opt => {
318
- opt.onclick = null;
319
- opt.style.cursor = 'default';
320
- });
321
-
322
- if (isCorrect) {
323
- selectedLi.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
324
- selectedLi.style.borderColor = 'var(--success)';
325
- document.getElementById('feedback').style.display = 'block';
326
- } else {
327
- selectedLi.style.backgroundColor = 'rgba(220, 53, 69, 0.1)';
328
- selectedLi.style.borderColor = 'var(--danger)';
329
- document.getElementById('action-buttons').style.display = 'block';
330
- }
331
- }
332
-
333
- function resetQuiz() {
334
- const options = document.querySelectorAll('#options-list li');
335
- options.forEach(opt => {
336
- opt.style.backgroundColor = '';
337
- opt.style.borderColor = '#dee2e6';
338
- opt.onclick = function () { checkAnswer(this); };
339
- opt.style.cursor = 'pointer';
340
- });
341
-
342
- document.getElementById('action-buttons').style.display = 'none';
343
- document.getElementById('feedback').style.display = 'none';
344
- }
345
-
346
- function showAnswer() {
347
- const options = document.querySelectorAll('#options-list li');
348
- options.forEach(opt => {
349
- if (opt.dataset.correct === 'true') {
350
- opt.style.backgroundColor = 'rgba(40, 167, 69, 0.1)';
351
- opt.style.borderColor = 'var(--success)';
352
- }
353
- });
354
-
355
- document.getElementById('feedback').style.display = 'block';
356
- document.getElementById('action-buttons').style.display = 'none';
357
- }
358
- </script>
359
- </body>
360
-
361
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/utils/__init__.py DELETED
File without changes
main.py DELETED
@@ -1,10 +0,0 @@
1
- from fastapi import FastAPI
2
- from app.api import router
3
- import uvicorn
4
-
5
- app=FastAPI()
6
- app.include_router(router)
7
-
8
-
9
- if __name__=="__main__":
10
- uvicorn.run(app, host="0.0.0.0", port=3000)
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,51 +0,0 @@
1
- annotated-types==0.7.0
2
- anyio==4.9.0
3
- cachetools==5.5.2
4
- certifi==2025.4.26
5
- charset-normalizer==3.4.2
6
- click==8.1.8
7
- fastapi==0.115.12
8
- filetype==1.2.0
9
- google-ai-generativelanguage==0.6.18
10
- google-api-core==2.24.2
11
- google-auth==2.40.1
12
- googleapis-common-protos==1.70.0
13
- greenlet==3.2.1
14
- grpcio==1.71.0
15
- grpcio-status==1.71.0
16
- h11==0.16.0
17
- httpcore==1.0.9
18
- httpx==0.28.1
19
- idna==3.10
20
- Jinja2==3.1.6
21
- jsonpatch==1.33
22
- jsonpointer==3.0.0
23
- langchain-core==0.3.59
24
- langchain-google-genai==2.1.4
25
- langsmith==0.3.42
26
- Mako==1.3.10
27
- MarkupSafe==3.0.2
28
- orjson==3.10.18
29
- packaging==24.2
30
- proto-plus==1.26.1
31
- protobuf==5.29.4
32
- pyasn1==0.6.1
33
- pyasn1_modules==0.4.2
34
- pydantic==2.11.4
35
- pydantic-settings==2.9.1
36
- pydantic_core==2.33.2
37
- python-dotenv==1.1.0
38
- PyYAML==6.0.2
39
- requests==2.32.3
40
- requests-toolbelt==1.0.0
41
- rsa==4.9.1
42
- ruff==0.11.8
43
- sniffio==1.3.1
44
- SQLAlchemy==2.0.40
45
- starlette==0.46.2
46
- tenacity==9.1.2
47
- typing-inspection==0.4.0
48
- typing_extensions==4.13.2
49
- urllib3==2.4.0
50
- uvicorn==0.34.2
51
- zstandard==0.23.0