puzan789 commited on
Commit
775ba42
·
1 Parent(s): 9982eff
.env.example ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ GOOGLE_API_KEY=<google_api_key>
2
+ DATABASE_URL=postgresql+asyncpg://<username>:<password>@<host>:<port>/<database_name>
.github/workflows/lint.yml ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ .env
3
+
4
+ venv/
5
+
6
+ *.log
7
+
8
+ app.log
9
+
Dockerfile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1 @@
 
 
1
+ from .routes import router #noqa
app/api/routes.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
File without changes
app/core/config.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1 @@
 
 
1
+ from .schemas import * #noqa
app/schemas/schemas.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
File without changes
app/services/gemini.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
File without changes
main.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
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 ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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