mfirat007 commited on
Commit
054e65a
·
verified ·
1 Parent(s): 5095cdf

Upload 39 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .gitignore
3
+ **/__pycache__
4
+ **/.pytest_cache
5
+ **/.mypy_cache
6
+ **/.ruff_cache
7
+ **/*.pyc
8
+ **/*.pyo
9
+ backend/.env
10
+ backend/aididact.db
11
+ backend/alembic/versions/*.pyc
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+ ENV PORT=7860
6
+
7
+ WORKDIR /app
8
+
9
+ COPY backend/requirements.txt /app/requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
11
+
12
+ COPY backend /app
13
+
14
+ EXPOSE 7860
15
+
16
+ CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-7860}"]
README.md CHANGED
@@ -1,12 +1,100 @@
1
- ---
2
- title: AIDidact
3
- emoji: 🏆
4
- colorFrom: red
5
- colorTo: indigo
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- short_description: AI-powered microlearning ecosystem built with FastAPI
10
- ---
11
-
12
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AIDidact
2
+
3
+ AIDidact is a scalable AI-powered microlearning ecosystem for personalized, self-directed learning. The platform is organized around modular 5 ECTS learning units, hybrid recommendations, cheating-resistant assessments, and analytics-driven learner support.
4
+
5
+ ## Included Deliverables
6
+
7
+ - [Architecture blueprint](C:\Users\user\Desktop\AI Flow\AIDidact\docs\architecture.md)
8
+ - [Core algorithms](C:\Users\user\Desktop\AI Flow\AIDidact\docs\algorithms.md)
9
+ - [Relational data schema](C:\Users\user\Desktop\AI Flow\AIDidact\schemas\schema.sql)
10
+ - [REST API specification](C:\Users\user\Desktop\AI Flow\AIDidact\api\openapi.yaml)
11
+ - [FastAPI backend scaffold](C:\Users\user\Desktop\AI Flow\AIDidact\backend\README.md)
12
+
13
+ ## System Overview
14
+
15
+ AIDidact supports millions of learners across web and mobile channels. At registration, learners provide prior learning, goals, educational level, readiness signals, and optional learning preferences. That information is transformed into a learner profile vector used to personalize module recommendations, pacing, and feedback.
16
+
17
+ Each microlearning module is worth 5 ECTS credits and includes Bloom-aligned objectives, chunked 10 to 15 minute content units, interactive activities, formative assessments, and explicit completion criteria. Modules are self-paced, measurable, and adaptive where possible.
18
+
19
+ ## Textual Architecture Diagram
20
+
21
+ ```mermaid
22
+ flowchart LR
23
+ A["Web App"] --> G["API Gateway"]
24
+ B["Mobile App"] --> G
25
+ C["Admin Console"] --> G
26
+
27
+ G --> U["Auth and Consent Service"]
28
+ G --> P["Profile Service"]
29
+ G --> M["Module Service"]
30
+ G --> R["Recommendation Service"]
31
+ G --> S["Assessment Service"]
32
+ G --> T["Progress Service"]
33
+ G --> D["Dashboard Service"]
34
+
35
+ U --> PG["PostgreSQL"]
36
+ P --> PG
37
+ M --> PG
38
+ S --> PG
39
+ T --> PG
40
+
41
+ P --> K["Event Bus"]
42
+ M --> K
43
+ S --> K
44
+ T --> K
45
+
46
+ K --> L["Analytics Processor"]
47
+ L --> NS["NoSQL Log Store"]
48
+ L --> WH["Analytics Warehouse"]
49
+ L --> FS["Feature Store"]
50
+
51
+ R --> FS
52
+ R --> VX["Vector Index"]
53
+ R --> RM["Hybrid Recommender Models"]
54
+
55
+ S --> Q["Question Generation"]
56
+ S --> E["AI Evaluation"]
57
+ S --> I["Integrity Engine"]
58
+
59
+ D --> WH
60
+ ```
61
+
62
+ ## Key Design Decisions
63
+
64
+ - Transactional data lives in PostgreSQL; high-volume telemetry flows to NoSQL and warehouse systems.
65
+ - Recommendation uses hybrid content-based plus collaborative filtering and refreshes from learner progress.
66
+ - Assessment uses blueprint-based randomized questions, AI-assisted open-ended grading, and anomaly detection.
67
+ - GDPR compliance is built in through consent management, auditability, pseudonymization, retention rules, and human review paths.
68
+
69
+ ## Hugging Face Spaces
70
+
71
+ This repository is now prepared for Hugging Face Spaces using `Docker`.
72
+
73
+ Recommended Space setup:
74
+
75
+ - SDK: `Docker`
76
+ - Template: `Blank`
77
+ - Hardware: `CPU Basic`
78
+
79
+ For the easiest deployment path on Spaces:
80
+
81
+ - use the included `Dockerfile`
82
+ - keep `DATABASE_URL` as SQLite for demo use
83
+ - optionally switch to an external PostgreSQL instance later via Space Secrets
84
+
85
+ Main deployment files:
86
+
87
+ - [Dockerfile](C:\Users\user\Desktop\AI Flow\AIDidact\Dockerfile)
88
+ - [backend README](C:\Users\user\Desktop\AI Flow\AIDidact\backend\README.md)
89
+ - [environment example](C:\Users\user\Desktop\AI Flow\AIDidact\backend\.env.example)
90
+
91
+ ## Example User Flow
92
+
93
+ 1. Learner registers and grants required consents.
94
+ 2. Diagnostic assessment establishes readiness and baseline mastery.
95
+ 3. Profile vector is generated from prior learning, goals, level, readiness, and preferences.
96
+ 4. Recommendation service ranks starter modules.
97
+ 5. Learner completes chunked units, activities, and formative checks.
98
+ 6. Events stream to analytics and update mastery, dashboards, and next recommendations.
99
+ 7. Learner completes a unique summative assessment with integrity controls.
100
+ 8. Module is marked complete only if score, engagement, and integrity thresholds are satisfied.
api/openapi.yaml ADDED
@@ -0,0 +1,433 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ openapi: 3.1.0
2
+ info:
3
+ title: AIDidact API
4
+ version: 1.0.0
5
+ description: API contract for the AIDidact AI-powered microlearning ecosystem.
6
+ servers:
7
+ - url: https://api.aididact.example.com
8
+ paths:
9
+ /v1/auth/register:
10
+ post:
11
+ summary: Register a learner
12
+ requestBody:
13
+ required: true
14
+ content:
15
+ application/json:
16
+ schema:
17
+ $ref: '#/components/schemas/RegisterLearnerRequest'
18
+ responses:
19
+ '201':
20
+ description: Registered
21
+ content:
22
+ application/json:
23
+ schema:
24
+ $ref: '#/components/schemas/LearnerRegistrationResponse'
25
+ /v1/learners/{learnerId}/profile:
26
+ get:
27
+ summary: Get learner profile
28
+ parameters:
29
+ - $ref: '#/components/parameters/LearnerId'
30
+ responses:
31
+ '200':
32
+ description: Learner profile
33
+ content:
34
+ application/json:
35
+ schema:
36
+ $ref: '#/components/schemas/LearnerProfile'
37
+ patch:
38
+ summary: Update learner goals or preferences
39
+ parameters:
40
+ - $ref: '#/components/parameters/LearnerId'
41
+ requestBody:
42
+ required: true
43
+ content:
44
+ application/json:
45
+ schema:
46
+ $ref: '#/components/schemas/UpdateLearnerProfileRequest'
47
+ responses:
48
+ '200':
49
+ description: Updated
50
+ /v1/learners/{learnerId}/diagnostic-assessment:
51
+ post:
52
+ summary: Submit readiness assessment
53
+ parameters:
54
+ - $ref: '#/components/parameters/LearnerId'
55
+ requestBody:
56
+ required: true
57
+ content:
58
+ application/json:
59
+ schema:
60
+ $ref: '#/components/schemas/DiagnosticSubmission'
61
+ responses:
62
+ '202':
63
+ description: Accepted
64
+ /v1/modules:
65
+ get:
66
+ summary: List modules
67
+ responses:
68
+ '200':
69
+ description: Module list
70
+ /v1/modules/{moduleId}:
71
+ get:
72
+ summary: Get module detail
73
+ parameters:
74
+ - $ref: '#/components/parameters/ModuleId'
75
+ responses:
76
+ '200':
77
+ description: Module detail
78
+ content:
79
+ application/json:
80
+ schema:
81
+ $ref: '#/components/schemas/ModuleDetail'
82
+ /v1/learners/{learnerId}/recommendations:
83
+ get:
84
+ summary: Get ranked module recommendations
85
+ parameters:
86
+ - $ref: '#/components/parameters/LearnerId'
87
+ responses:
88
+ '200':
89
+ description: Recommendations
90
+ content:
91
+ application/json:
92
+ schema:
93
+ type: object
94
+ properties:
95
+ items:
96
+ type: array
97
+ items:
98
+ $ref: '#/components/schemas/RecommendationItem'
99
+ /v1/enrollments:
100
+ post:
101
+ summary: Enroll learner in module
102
+ requestBody:
103
+ required: true
104
+ content:
105
+ application/json:
106
+ schema:
107
+ type: object
108
+ required: [learnerId, moduleId]
109
+ properties:
110
+ learnerId:
111
+ type: string
112
+ format: uuid
113
+ moduleId:
114
+ type: string
115
+ format: uuid
116
+ responses:
117
+ '201':
118
+ description: Enrolled
119
+ /v1/interactions:
120
+ post:
121
+ summary: Ingest interaction event
122
+ requestBody:
123
+ required: true
124
+ content:
125
+ application/json:
126
+ schema:
127
+ $ref: '#/components/schemas/InteractionEvent'
128
+ responses:
129
+ '202':
130
+ description: Accepted
131
+ /v1/assessments/{assessmentId}/attempts:
132
+ post:
133
+ summary: Create unique assessment attempt
134
+ parameters:
135
+ - $ref: '#/components/parameters/AssessmentId'
136
+ requestBody:
137
+ required: true
138
+ content:
139
+ application/json:
140
+ schema:
141
+ type: object
142
+ required: [learnerId]
143
+ properties:
144
+ learnerId:
145
+ type: string
146
+ format: uuid
147
+ responses:
148
+ '201':
149
+ description: Attempt created
150
+ content:
151
+ application/json:
152
+ schema:
153
+ $ref: '#/components/schemas/AssessmentAttempt'
154
+ /v1/assessments/attempts/{attemptId}/responses:
155
+ post:
156
+ summary: Submit responses
157
+ parameters:
158
+ - $ref: '#/components/parameters/AttemptId'
159
+ requestBody:
160
+ required: true
161
+ content:
162
+ application/json:
163
+ schema:
164
+ $ref: '#/components/schemas/AssessmentResponseSubmission'
165
+ responses:
166
+ '202':
167
+ description: Accepted for grading
168
+ /v1/assessments/attempts/{attemptId}:
169
+ get:
170
+ summary: Get assessment result
171
+ parameters:
172
+ - $ref: '#/components/parameters/AttemptId'
173
+ responses:
174
+ '200':
175
+ description: Result
176
+ content:
177
+ application/json:
178
+ schema:
179
+ $ref: '#/components/schemas/AssessmentAttemptResult'
180
+ /v1/learners/{learnerId}/dashboard:
181
+ get:
182
+ summary: Get learner dashboard
183
+ parameters:
184
+ - $ref: '#/components/parameters/LearnerId'
185
+ responses:
186
+ '200':
187
+ description: Dashboard
188
+ content:
189
+ application/json:
190
+ schema:
191
+ $ref: '#/components/schemas/LearnerDashboard'
192
+ components:
193
+ parameters:
194
+ LearnerId:
195
+ in: path
196
+ name: learnerId
197
+ required: true
198
+ schema:
199
+ type: string
200
+ format: uuid
201
+ ModuleId:
202
+ in: path
203
+ name: moduleId
204
+ required: true
205
+ schema:
206
+ type: string
207
+ format: uuid
208
+ AssessmentId:
209
+ in: path
210
+ name: assessmentId
211
+ required: true
212
+ schema:
213
+ type: string
214
+ format: uuid
215
+ AttemptId:
216
+ in: path
217
+ name: attemptId
218
+ required: true
219
+ schema:
220
+ type: string
221
+ format: uuid
222
+ schemas:
223
+ RegisterLearnerRequest:
224
+ type: object
225
+ required: [email, firstName, lastName, timezone, educationalLevel, goals, priorLearning]
226
+ properties:
227
+ email:
228
+ type: string
229
+ format: email
230
+ firstName:
231
+ type: string
232
+ lastName:
233
+ type: string
234
+ timezone:
235
+ type: string
236
+ educationalLevel:
237
+ type: string
238
+ priorLearning:
239
+ type: array
240
+ items:
241
+ $ref: '#/components/schemas/PriorLearningItem'
242
+ goals:
243
+ type: array
244
+ items:
245
+ $ref: '#/components/schemas/LearnerGoal'
246
+ preferences:
247
+ $ref: '#/components/schemas/LearnerPreferences'
248
+ LearnerRegistrationResponse:
249
+ type: object
250
+ properties:
251
+ learnerId:
252
+ type: string
253
+ format: uuid
254
+ nextStep:
255
+ type: string
256
+ PriorLearningItem:
257
+ type: object
258
+ properties:
259
+ learningType:
260
+ type: string
261
+ enum: [formal, non_formal, informal]
262
+ title:
263
+ type: string
264
+ provider:
265
+ type: string
266
+ description:
267
+ type: string
268
+ LearnerGoal:
269
+ type: object
270
+ properties:
271
+ goalHorizon:
272
+ type: string
273
+ enum: [short_term, long_term]
274
+ goalText:
275
+ type: string
276
+ targetDate:
277
+ type: string
278
+ format: date
279
+ LearnerPreferences:
280
+ type: object
281
+ properties:
282
+ preferredModalities:
283
+ type: array
284
+ items:
285
+ type: string
286
+ preferredSessionLengthMinutes:
287
+ type: integer
288
+ DiagnosticSubmission:
289
+ type: object
290
+ properties:
291
+ responses:
292
+ type: array
293
+ items:
294
+ type: object
295
+ additionalProperties: true
296
+ LearnerProfile:
297
+ type: object
298
+ properties:
299
+ learnerId:
300
+ type: string
301
+ format: uuid
302
+ readinessLevel:
303
+ type: string
304
+ readinessScore:
305
+ type: number
306
+ masteryMap:
307
+ type: object
308
+ additionalProperties: true
309
+ UpdateLearnerProfileRequest:
310
+ type: object
311
+ properties:
312
+ goals:
313
+ type: array
314
+ items:
315
+ $ref: '#/components/schemas/LearnerGoal'
316
+ preferences:
317
+ $ref: '#/components/schemas/LearnerPreferences'
318
+ ModuleDetail:
319
+ type: object
320
+ properties:
321
+ id:
322
+ type: string
323
+ format: uuid
324
+ title:
325
+ type: string
326
+ description:
327
+ type: string
328
+ objectives:
329
+ type: array
330
+ items:
331
+ type: object
332
+ additionalProperties: true
333
+ units:
334
+ type: array
335
+ items:
336
+ type: object
337
+ additionalProperties: true
338
+ completionCriteria:
339
+ type: object
340
+ additionalProperties: true
341
+ RecommendationItem:
342
+ type: object
343
+ properties:
344
+ moduleId:
345
+ type: string
346
+ format: uuid
347
+ rank:
348
+ type: integer
349
+ score:
350
+ type: number
351
+ reasons:
352
+ type: array
353
+ items:
354
+ type: string
355
+ InteractionEvent:
356
+ type: object
357
+ properties:
358
+ learnerId:
359
+ type: string
360
+ format: uuid
361
+ moduleId:
362
+ type: string
363
+ format: uuid
364
+ unitId:
365
+ type: string
366
+ format: uuid
367
+ activityId:
368
+ type: string
369
+ format: uuid
370
+ eventType:
371
+ type: string
372
+ eventTimestamp:
373
+ type: string
374
+ format: date-time
375
+ durationMs:
376
+ type: integer
377
+ payload:
378
+ type: object
379
+ additionalProperties: true
380
+ AssessmentAttempt:
381
+ type: object
382
+ properties:
383
+ attemptId:
384
+ type: string
385
+ format: uuid
386
+ assessmentId:
387
+ type: string
388
+ format: uuid
389
+ timeLimitMinutes:
390
+ type: integer
391
+ items:
392
+ type: array
393
+ items:
394
+ type: object
395
+ additionalProperties: true
396
+ AssessmentResponseSubmission:
397
+ type: object
398
+ properties:
399
+ responses:
400
+ type: array
401
+ items:
402
+ type: object
403
+ additionalProperties: true
404
+ AssessmentAttemptResult:
405
+ type: object
406
+ properties:
407
+ attemptId:
408
+ type: string
409
+ format: uuid
410
+ score:
411
+ type: number
412
+ passStatus:
413
+ type: string
414
+ integrityStatus:
415
+ type: string
416
+ humanReviewRequired:
417
+ type: boolean
418
+ LearnerDashboard:
419
+ type: object
420
+ properties:
421
+ learnerId:
422
+ type: string
423
+ format: uuid
424
+ timeOnTaskMinutes7d:
425
+ type: number
426
+ dropoutRisk:
427
+ type: number
428
+ successProbability:
429
+ type: number
430
+ adaptiveFeedback:
431
+ type: array
432
+ items:
433
+ type: string
backend/.env.example ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ APP_NAME=AIDidact API
2
+ APP_VERSION=0.1.0
3
+ API_PREFIX=/v1
4
+ ENVIRONMENT=development
5
+ DEBUG=true
6
+ DATABASE_URL=sqlite+aiosqlite:///./aididact.db
7
+ SQL_ECHO=false
8
+ AUTO_CREATE_TABLES=true
9
+ APP_PORT=7860
backend/README.md ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AIDidact FastAPI Backend
2
+
3
+ This is a production-oriented FastAPI scaffold for the AIDidact microlearning platform. It now uses async SQLAlchemy with a PostgreSQL-first configuration, plus Alembic scaffolding for schema migrations.
4
+
5
+ ## Included
6
+
7
+ - FastAPI app entrypoint
8
+ - modular routers aligned with the platform domains
9
+ - Pydantic request and response models
10
+ - async SQLAlchemy models and repositories
11
+ - Alembic migration scaffold
12
+ - lightweight recommendation, profile, analytics, and assessment service logic
13
+ - seeded demo modules and one summative assessment
14
+
15
+ ## Project Structure
16
+
17
+ ```text
18
+ backend/
19
+ app/
20
+ api/
21
+ core/
22
+ db/
23
+ models/
24
+ services/
25
+ main.py
26
+ alembic/
27
+ alembic.ini
28
+ requirements.txt
29
+ pyproject.toml
30
+ ```
31
+
32
+ ## Run Locally
33
+
34
+ 1. Install Python 3.11 or newer.
35
+ 2. Create a virtual environment.
36
+ 3. Install dependencies:
37
+
38
+ ```bash
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ 4. Copy `.env.example` to `.env` and set `DATABASE_URL`.
43
+
44
+ 5. Run migrations:
45
+
46
+ ```bash
47
+ alembic revision --autogenerate -m "initial schema"
48
+ alembic upgrade head
49
+ ```
50
+
51
+ For quick local development, `AUTO_CREATE_TABLES=true` will also create tables on startup.
52
+
53
+ 6. Start the API:
54
+
55
+ ```bash
56
+ uvicorn app.main:app --reload
57
+ ```
58
+
59
+ 7. Open:
60
+
61
+ - `http://127.0.0.1:8000/docs`
62
+ - `http://127.0.0.1:8000/redoc`
63
+
64
+ ## Next Production Steps
65
+
66
+ - add JWT authentication and consent middleware
67
+ - connect recommendation features to a vector index and analytics feature store
68
+ - introduce background workers for grading, anomaly detection, and recommendation refreshes
69
+ - split module, assessment, and analytics domains into separate deployable services as traffic grows
70
+
71
+ ## Hugging Face Spaces
72
+
73
+ For Hugging Face Spaces, choose:
74
+
75
+ - `Docker`
76
+ - `Blank`
77
+ - `CPU Basic`
78
+
79
+ Recommended first deployment setup:
80
+
81
+ - keep `DATABASE_URL=sqlite+aiosqlite:///./aididact.db`
82
+ - keep `AUTO_CREATE_TABLES=true`
83
+ - do not run Alembic in the first demo deployment
84
+
85
+ If you later use an external PostgreSQL database, add `DATABASE_URL` as a Hugging Face Space Secret and optionally disable automatic table creation after migrations are in place.
backend/alembic.ini ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [alembic]
2
+ script_location = alembic
3
+ prepend_sys_path = .
4
+ sqlalchemy.url = postgresql+asyncpg://postgres:postgres@localhost:5432/aididact
5
+
6
+ [loggers]
7
+ keys = root,sqlalchemy,alembic
8
+
9
+ [handlers]
10
+ keys = console
11
+
12
+ [formatters]
13
+ keys = generic
14
+
15
+ [logger_root]
16
+ level = WARN
17
+ handlers = console
18
+
19
+ [logger_sqlalchemy]
20
+ level = WARN
21
+ handlers =
22
+ qualname = sqlalchemy.engine
23
+
24
+ [logger_alembic]
25
+ level = INFO
26
+ handlers = console
27
+ qualname = alembic
28
+
29
+ [handler_console]
30
+ class = StreamHandler
31
+ args = (sys.stderr,)
32
+ level = NOTSET
33
+ formatter = generic
34
+
35
+ [formatter_generic]
36
+ format = %(levelname)-5.5s [%(name)s] %(message)s
backend/alembic/env.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from logging.config import fileConfig
4
+
5
+ from alembic import context
6
+ from sqlalchemy import engine_from_config, pool
7
+
8
+ from app.core.config import get_settings
9
+ from app.db.base import Base
10
+ from app.models import orm # noqa: F401
11
+
12
+ config = context.config
13
+ settings = get_settings()
14
+ config.set_main_option(
15
+ "sqlalchemy.url",
16
+ settings.database_url.replace("+asyncpg", "").replace("+aiosqlite", ""),
17
+ )
18
+
19
+ if config.config_file_name is not None:
20
+ fileConfig(config.config_file_name)
21
+
22
+ target_metadata = Base.metadata
23
+
24
+
25
+ def run_migrations_offline() -> None:
26
+ url = config.get_main_option("sqlalchemy.url")
27
+ context.configure(url=url, target_metadata=target_metadata, literal_binds=True, compare_type=True)
28
+
29
+ with context.begin_transaction():
30
+ context.run_migrations()
31
+
32
+
33
+ def run_migrations_online() -> None:
34
+ connectable = engine_from_config(
35
+ config.get_section(config.config_ini_section, {}),
36
+ prefix="sqlalchemy.",
37
+ poolclass=pool.NullPool,
38
+ )
39
+
40
+ with connectable.connect() as connection:
41
+ context.configure(connection=connection, target_metadata=target_metadata, compare_type=True)
42
+
43
+ with context.begin_transaction():
44
+ context.run_migrations()
45
+
46
+
47
+ if context.is_offline_mode():
48
+ run_migrations_offline()
49
+ else:
50
+ run_migrations_online()
backend/alembic/script.py.mako ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """${message}
2
+
3
+ Revision ID: ${up_revision}
4
+ Revises: ${down_revision | comma,n}
5
+ Create Date: ${create_date}
6
+ """
7
+ from alembic import op
8
+ import sqlalchemy as sa
9
+
10
+ ${imports if imports else ""}
11
+
12
+ # revision identifiers, used by Alembic.
13
+ revision = ${repr(up_revision)}
14
+ down_revision = ${repr(down_revision)}
15
+ branch_labels = ${repr(branch_labels)}
16
+ depends_on = ${repr(depends_on)}
17
+
18
+
19
+ def upgrade() -> None:
20
+ ${upgrades if upgrades else "pass"}
21
+
22
+
23
+ def downgrade() -> None:
24
+ ${downgrades if downgrades else "pass"}
backend/alembic/versions/.gitkeep ADDED
@@ -0,0 +1 @@
 
 
1
+
backend/app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """AIDidact FastAPI backend package."""
backend/app/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """API router package."""
backend/app/api/deps.py ADDED
@@ -0,0 +1 @@
 
 
1
+ from app.db.session import get_db_session
backend/app/api/router.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter
2
+
3
+ from app.api.routes import assessments, auth, learners, modules, progress, recommendations
4
+
5
+ api_router = APIRouter()
6
+ api_router.include_router(auth.router, prefix="/auth", tags=["Auth"])
7
+ api_router.include_router(learners.router, prefix="/learners", tags=["Learners"])
8
+ api_router.include_router(modules.router, prefix="/modules", tags=["Modules"])
9
+ api_router.include_router(recommendations.router, prefix="/learners", tags=["Recommendations"])
10
+ api_router.include_router(progress.router, tags=["Progress"])
11
+ api_router.include_router(assessments.router, prefix="/assessments", tags=["Assessments"])
backend/app/api/routes/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Route modules for AIDidact."""
backend/app/api/routes/assessments.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends, status
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.api.deps import get_db_session
7
+ from app.models.schemas import (
8
+ AssessmentAttempt,
9
+ AssessmentAttemptCreate,
10
+ AssessmentAttemptResult,
11
+ AssessmentResponseSubmission,
12
+ )
13
+ from app.services import engines, repository
14
+
15
+ router = APIRouter()
16
+
17
+
18
+ @router.post("/{assessment_id}/attempts", response_model=AssessmentAttempt, status_code=status.HTTP_201_CREATED)
19
+ async def create_attempt(
20
+ assessment_id: UUID,
21
+ payload: AssessmentAttemptCreate,
22
+ session: AsyncSession = Depends(get_db_session),
23
+ ) -> AssessmentAttempt:
24
+ await repository.get_learner(session, payload.learner_id)
25
+ assessment = await repository.get_assessment(session, assessment_id)
26
+ attempt = engines.generate_assessment_attempt(assessment, payload.learner_id)
27
+ return await repository.create_attempt(session, attempt)
28
+
29
+
30
+ @router.post("/attempts/{attempt_id}/responses", status_code=status.HTTP_202_ACCEPTED)
31
+ async def submit_responses(
32
+ attempt_id: UUID,
33
+ payload: AssessmentResponseSubmission,
34
+ session: AsyncSession = Depends(get_db_session),
35
+ ) -> dict[str, str]:
36
+ attempt = await repository.get_attempt(session, attempt_id)
37
+ result = engines.evaluate_attempt(attempt, len(payload.responses))
38
+ await repository.save_result(session, result)
39
+ return {"status": "accepted"}
40
+
41
+
42
+ @router.get("/attempts/{attempt_id}", response_model=AssessmentAttemptResult)
43
+ async def get_attempt_result(
44
+ attempt_id: UUID, session: AsyncSession = Depends(get_db_session)
45
+ ) -> AssessmentAttemptResult:
46
+ return await repository.get_result(session, attempt_id)
backend/app/api/routes/auth.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, status
2
+ from sqlalchemy.ext.asyncio import AsyncSession
3
+
4
+ from app.api.deps import get_db_session
5
+ from app.models.schemas import (
6
+ LearnerPreferences,
7
+ LearnerRecord,
8
+ LearnerRegistrationResponse,
9
+ RegisterLearnerRequest,
10
+ )
11
+ from app.services import engines, repository
12
+
13
+ router = APIRouter()
14
+
15
+
16
+ @router.post("/register", response_model=LearnerRegistrationResponse, status_code=status.HTTP_201_CREATED)
17
+ async def register_learner(
18
+ payload: RegisterLearnerRequest, session: AsyncSession = Depends(get_db_session)
19
+ ) -> LearnerRegistrationResponse:
20
+ learner = LearnerRecord(
21
+ email=payload.email,
22
+ first_name=payload.first_name,
23
+ last_name=payload.last_name,
24
+ locale=payload.locale,
25
+ timezone=payload.timezone,
26
+ educational_level=payload.educational_level,
27
+ consent_personalization=payload.consent_personalization,
28
+ consent_analytics=payload.consent_analytics,
29
+ consent_version=payload.consent_version,
30
+ prior_learning=payload.prior_learning,
31
+ goals=payload.goals,
32
+ preferences=payload.preferences or LearnerPreferences(),
33
+ )
34
+ await repository.create_learner(session, engines.build_profile(learner))
35
+ return LearnerRegistrationResponse(learner_id=learner.id)
backend/app/api/routes/learners.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.api.deps import get_db_session
7
+ from app.models.schemas import DiagnosticSubmission, LearnerProfile, UpdateLearnerProfileRequest
8
+ from app.services import engines, repository
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("/{learner_id}/profile", response_model=LearnerProfile)
14
+ async def get_profile(learner_id: UUID, session: AsyncSession = Depends(get_db_session)) -> LearnerProfile:
15
+ learner = await repository.get_learner(session, learner_id)
16
+ return engines.learner_to_profile(learner)
17
+
18
+
19
+ @router.patch("/{learner_id}/profile", response_model=LearnerProfile)
20
+ async def update_profile(
21
+ learner_id: UUID,
22
+ payload: UpdateLearnerProfileRequest,
23
+ session: AsyncSession = Depends(get_db_session),
24
+ ) -> LearnerProfile:
25
+ learner = await repository.get_learner(session, learner_id)
26
+ if payload.goals is not None:
27
+ learner.goals = payload.goals
28
+ if payload.preferences is not None:
29
+ learner.preferences = payload.preferences
30
+ await repository.update_learner(session, engines.build_profile(learner))
31
+ return engines.learner_to_profile(learner)
32
+
33
+
34
+ @router.post("/{learner_id}/diagnostic-assessment", response_model=LearnerProfile)
35
+ async def submit_diagnostic(
36
+ learner_id: UUID,
37
+ payload: DiagnosticSubmission,
38
+ session: AsyncSession = Depends(get_db_session),
39
+ ) -> LearnerProfile:
40
+ learner = await repository.get_learner(session, learner_id)
41
+ learner = await repository.update_learner(session, engines.build_profile(learner, payload))
42
+ return engines.learner_to_profile(learner)
backend/app/api/routes/modules.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.api.deps import get_db_session
7
+ from app.models.schemas import ModuleDetail
8
+ from app.services import repository
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("", response_model=list[ModuleDetail])
14
+ async def list_modules(session: AsyncSession = Depends(get_db_session)) -> list[ModuleDetail]:
15
+ return await repository.list_modules(session)
16
+
17
+
18
+ @router.get("/{module_id}", response_model=ModuleDetail)
19
+ async def get_module(module_id: UUID, session: AsyncSession = Depends(get_db_session)) -> ModuleDetail:
20
+ return await repository.get_module(session, module_id)
backend/app/api/routes/progress.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends, status
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.api.deps import get_db_session
7
+ from app.models.schemas import Enrollment, EnrollmentCreate, InteractionEvent, LearnerDashboard, ProgressSummary
8
+ from app.services import engines, repository
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.post("/enrollments", response_model=Enrollment, status_code=status.HTTP_201_CREATED)
14
+ async def create_enrollment(
15
+ payload: EnrollmentCreate, session: AsyncSession = Depends(get_db_session)
16
+ ) -> Enrollment:
17
+ await repository.get_learner(session, payload.learner_id)
18
+ await repository.get_module(session, payload.module_id)
19
+ enrollment = Enrollment(learner_id=payload.learner_id, module_id=payload.module_id)
20
+ return await repository.create_enrollment(session, enrollment)
21
+
22
+
23
+ @router.get("/learners/{learner_id}/progress", response_model=ProgressSummary)
24
+ async def get_progress(
25
+ learner_id: UUID, session: AsyncSession = Depends(get_db_session)
26
+ ) -> ProgressSummary:
27
+ await repository.get_learner(session, learner_id)
28
+ return await engines.build_progress_summary(session, learner_id)
29
+
30
+
31
+ @router.post("/interactions", status_code=status.HTTP_202_ACCEPTED)
32
+ async def create_interaction(
33
+ payload: InteractionEvent, session: AsyncSession = Depends(get_db_session)
34
+ ) -> dict[str, str]:
35
+ await repository.get_learner(session, payload.learner_id)
36
+ await repository.create_interaction(session, payload)
37
+ return {"status": "accepted"}
38
+
39
+
40
+ @router.get("/learners/{learner_id}/dashboard", response_model=LearnerDashboard)
41
+ async def get_dashboard(
42
+ learner_id: UUID, session: AsyncSession = Depends(get_db_session)
43
+ ) -> LearnerDashboard:
44
+ learner = await repository.get_learner(session, learner_id)
45
+ return await engines.build_dashboard(session, learner)
backend/app/api/routes/recommendations.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from uuid import UUID
2
+
3
+ from fastapi import APIRouter, Depends
4
+ from sqlalchemy.ext.asyncio import AsyncSession
5
+
6
+ from app.api.deps import get_db_session
7
+ from app.models.schemas import RecommendationResponse
8
+ from app.services import engines, repository
9
+
10
+ router = APIRouter()
11
+
12
+
13
+ @router.get("/{learner_id}/recommendations", response_model=RecommendationResponse)
14
+ async def get_recommendations(
15
+ learner_id: UUID, session: AsyncSession = Depends(get_db_session)
16
+ ) -> RecommendationResponse:
17
+ learner = await repository.get_learner(session, learner_id)
18
+ modules = await repository.list_modules(session)
19
+ return engines.rank_modules(learner, modules)
backend/app/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Core configuration and shared utilities."""
backend/app/core/config.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import lru_cache
2
+
3
+ from pydantic import Field
4
+ from pydantic_settings import BaseSettings, SettingsConfigDict
5
+
6
+
7
+ class Settings(BaseSettings):
8
+ app_name: str = "AIDidact API"
9
+ app_version: str = "0.1.0"
10
+ api_prefix: str = "/v1"
11
+ environment: str = Field(default="development")
12
+ debug: bool = Field(default=True)
13
+ allowed_origins: list[str] = Field(default_factory=lambda: ["*"])
14
+ database_url: str = Field(default="sqlite+aiosqlite:///./aididact.db")
15
+ sql_echo: bool = Field(default=False)
16
+ auto_create_tables: bool = Field(default=True)
17
+ app_port: int = Field(default=7860)
18
+
19
+ model_config = SettingsConfigDict(
20
+ env_file=".env",
21
+ env_file_encoding="utf-8",
22
+ case_sensitive=False,
23
+ )
24
+
25
+
26
+ @lru_cache
27
+ def get_settings() -> Settings:
28
+ return Settings()
backend/app/db/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Storage backends for the AIDidact FastAPI scaffold."""
backend/app/db/base.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from sqlalchemy.orm import DeclarativeBase
2
+
3
+
4
+ class Base(DeclarativeBase):
5
+ pass
backend/app/db/session.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import AsyncGenerator
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
4
+
5
+ from app.core.config import get_settings
6
+
7
+ settings = get_settings()
8
+
9
+ engine_kwargs: dict = {
10
+ "echo": settings.sql_echo,
11
+ "future": True,
12
+ }
13
+
14
+ if settings.database_url.startswith("sqlite+aiosqlite"):
15
+ engine_kwargs["connect_args"] = {"check_same_thread": False}
16
+
17
+ engine = create_async_engine(
18
+ settings.database_url,
19
+ **engine_kwargs,
20
+ )
21
+
22
+ AsyncSessionLocal = async_sessionmaker(
23
+ bind=engine,
24
+ class_=AsyncSession,
25
+ expire_on_commit=False,
26
+ autoflush=False,
27
+ )
28
+
29
+
30
+ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
31
+ async with AsyncSessionLocal() as session:
32
+ yield session
backend/app/main.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+
6
+ from app.api.router import api_router
7
+ from app.core.config import get_settings
8
+ from app.db.base import Base
9
+ from app.db.session import AsyncSessionLocal, engine
10
+ from app.models import orm # noqa: F401
11
+ from app.services.bootstrap import seed_demo_data
12
+
13
+ settings = get_settings()
14
+
15
+
16
+ @asynccontextmanager
17
+ async def lifespan(_: FastAPI):
18
+ if settings.auto_create_tables:
19
+ async with engine.begin() as conn:
20
+ await conn.run_sync(Base.metadata.create_all)
21
+ async with AsyncSessionLocal() as session:
22
+ await seed_demo_data(session)
23
+ yield
24
+
25
+
26
+ app = FastAPI(
27
+ title=settings.app_name,
28
+ version=settings.app_version,
29
+ debug=settings.debug,
30
+ lifespan=lifespan,
31
+ )
32
+
33
+ app.add_middleware(
34
+ CORSMiddleware,
35
+ allow_origins=settings.allowed_origins,
36
+ allow_credentials=True,
37
+ allow_methods=["*"],
38
+ allow_headers=["*"],
39
+ )
40
+
41
+
42
+ @app.get("/health")
43
+ def healthcheck() -> dict[str, str]:
44
+ return {"status": "ok"}
45
+
46
+
47
+ app.include_router(api_router, prefix=settings.api_prefix)
backend/app/models/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Pydantic models used across the backend."""
backend/app/models/orm.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from datetime import datetime
5
+
6
+ from sqlalchemy import Boolean, Date, DateTime, Float, ForeignKey, Integer, JSON, String, Text, UniqueConstraint, Uuid, func
7
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
8
+
9
+ from app.db.base import Base
10
+
11
+
12
+ class LearnerORM(Base):
13
+ __tablename__ = "learners"
14
+
15
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
16
+ email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
17
+ first_name: Mapped[str] = mapped_column(String(100), nullable=False)
18
+ last_name: Mapped[str] = mapped_column(String(100), nullable=False)
19
+ locale: Mapped[str] = mapped_column(String(20), default="en-US")
20
+ timezone: Mapped[str] = mapped_column(String(64), nullable=False)
21
+ educational_level: Mapped[str] = mapped_column(String(50), nullable=False)
22
+ consent_personalization: Mapped[bool] = mapped_column(Boolean, default=False)
23
+ consent_analytics: Mapped[bool] = mapped_column(Boolean, default=False)
24
+ consent_version: Mapped[str | None] = mapped_column(String(50))
25
+ readiness_level: Mapped[str] = mapped_column(String(30), default="unknown")
26
+ readiness_score: Mapped[float] = mapped_column(Float, default=0.0)
27
+ mastery_map: Mapped[dict] = mapped_column(JSON, default=dict)
28
+ risk_profile: Mapped[dict] = mapped_column(JSON, default=dict)
29
+ profile_vector: Mapped[list] = mapped_column(JSON, default=list)
30
+ preferences: Mapped[dict] = mapped_column(JSON, default=dict)
31
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
32
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
33
+
34
+ prior_learning_items: Mapped[list["LearnerPriorLearningORM"]] = relationship(
35
+ back_populates="learner", cascade="all, delete-orphan"
36
+ )
37
+ goals: Mapped[list["LearnerGoalORM"]] = relationship(back_populates="learner", cascade="all, delete-orphan")
38
+ enrollments: Mapped[list["EnrollmentORM"]] = relationship(back_populates="learner", cascade="all, delete-orphan")
39
+ interactions: Mapped[list["InteractionORM"]] = relationship(back_populates="learner", cascade="all, delete-orphan")
40
+
41
+
42
+ class LearnerPriorLearningORM(Base):
43
+ __tablename__ = "learner_prior_learning"
44
+
45
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
46
+ learner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("learners.id", ondelete="CASCADE"), nullable=False)
47
+ learning_type: Mapped[str] = mapped_column(String(20), nullable=False)
48
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
49
+ provider: Mapped[str | None] = mapped_column(String(255))
50
+ description: Mapped[str | None] = mapped_column(Text)
51
+ evidence_url: Mapped[str | None] = mapped_column(Text)
52
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
53
+
54
+ learner: Mapped["LearnerORM"] = relationship(back_populates="prior_learning_items")
55
+
56
+
57
+ class LearnerGoalORM(Base):
58
+ __tablename__ = "learner_goals"
59
+
60
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
61
+ learner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("learners.id", ondelete="CASCADE"), nullable=False)
62
+ goal_horizon: Mapped[str] = mapped_column(String(20), nullable=False)
63
+ goal_text: Mapped[str] = mapped_column(Text, nullable=False)
64
+ target_date: Mapped[datetime | None] = mapped_column(Date)
65
+ priority: Mapped[int] = mapped_column(Integer, default=3)
66
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
67
+
68
+ learner: Mapped["LearnerORM"] = relationship(back_populates="goals")
69
+
70
+
71
+ class ModuleORM(Base):
72
+ __tablename__ = "modules"
73
+
74
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
75
+ code: Mapped[str] = mapped_column(String(50), unique=True, nullable=False)
76
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
77
+ description: Mapped[str] = mapped_column(Text, nullable=False)
78
+ ects_credits: Mapped[float] = mapped_column(Float, default=5.0)
79
+ difficulty_level: Mapped[str] = mapped_column(String(30), nullable=False)
80
+ estimated_total_minutes: Mapped[int] = mapped_column(Integer, nullable=False)
81
+ self_paced: Mapped[bool] = mapped_column(Boolean, default=True)
82
+ adaptive_enabled: Mapped[bool] = mapped_column(Boolean, default=True)
83
+ topics: Mapped[list] = mapped_column(JSON, default=list)
84
+ completion_criteria: Mapped[dict] = mapped_column(JSON, default=dict)
85
+ prerequisites: Mapped[list] = mapped_column(JSON, default=list)
86
+ module_vector: Mapped[list] = mapped_column(JSON, default=list)
87
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
88
+ updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
89
+
90
+ objectives: Mapped[list["ModuleObjectiveORM"]] = relationship(
91
+ back_populates="module", cascade="all, delete-orphan", order_by="ModuleObjectiveORM.sort_order"
92
+ )
93
+ units: Mapped[list["ModuleUnitORM"]] = relationship(
94
+ back_populates="module", cascade="all, delete-orphan", order_by="ModuleUnitORM.sort_order"
95
+ )
96
+ activities: Mapped[list["ModuleActivityORM"]] = relationship(
97
+ back_populates="module", cascade="all, delete-orphan", order_by="ModuleActivityORM.sort_order"
98
+ )
99
+ assessments: Mapped[list["AssessmentDefinitionORM"]] = relationship(
100
+ back_populates="module", cascade="all, delete-orphan"
101
+ )
102
+
103
+
104
+ class ModuleObjectiveORM(Base):
105
+ __tablename__ = "module_objectives"
106
+
107
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
108
+ module_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
109
+ text: Mapped[str] = mapped_column(Text, nullable=False)
110
+ bloom_level: Mapped[str] = mapped_column(String(30), nullable=False)
111
+ competency_tag: Mapped[str] = mapped_column(String(100), nullable=False)
112
+ measurable_outcome: Mapped[str] = mapped_column(Text, nullable=False)
113
+ sort_order: Mapped[int] = mapped_column(Integer, default=0)
114
+
115
+ module: Mapped["ModuleORM"] = relationship(back_populates="objectives")
116
+
117
+
118
+ class ModuleUnitORM(Base):
119
+ __tablename__ = "module_units"
120
+
121
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
122
+ module_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
123
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
124
+ unit_type: Mapped[str] = mapped_column(String(30), nullable=False)
125
+ chunk_minutes: Mapped[int] = mapped_column(Integer, nullable=False)
126
+ content_ref: Mapped[str] = mapped_column(Text, nullable=False)
127
+ adaptive_rules: Mapped[dict] = mapped_column(JSON, default=dict)
128
+ sort_order: Mapped[int] = mapped_column(Integer, default=0)
129
+
130
+ module: Mapped["ModuleORM"] = relationship(back_populates="units")
131
+
132
+
133
+ class ModuleActivityORM(Base):
134
+ __tablename__ = "module_activities"
135
+
136
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
137
+ module_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
138
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
139
+ activity_type: Mapped[str] = mapped_column(String(30), nullable=False)
140
+ instructions: Mapped[str] = mapped_column(Text, nullable=False)
141
+ max_score: Mapped[float | None] = mapped_column(Float)
142
+ required: Mapped[bool] = mapped_column(Boolean, default=True)
143
+ sort_order: Mapped[int] = mapped_column(Integer, default=0)
144
+
145
+ module: Mapped["ModuleORM"] = relationship(back_populates="activities")
146
+
147
+
148
+ class AssessmentDefinitionORM(Base):
149
+ __tablename__ = "assessments"
150
+
151
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
152
+ module_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
153
+ assessment_type: Mapped[str] = mapped_column(String(30), nullable=False)
154
+ title: Mapped[str] = mapped_column(String(255), nullable=False)
155
+ blueprint: Mapped[dict] = mapped_column(JSON, default=dict)
156
+ time_limit_minutes: Mapped[int] = mapped_column(Integer, nullable=False)
157
+
158
+ module: Mapped["ModuleORM"] = relationship(back_populates="assessments")
159
+ attempts: Mapped[list["AssessmentAttemptORM"]] = relationship(back_populates="assessment")
160
+
161
+
162
+ class EnrollmentORM(Base):
163
+ __tablename__ = "enrollments"
164
+ __table_args__ = (UniqueConstraint("learner_id", "module_id", name="uq_enrollment_learner_module"),)
165
+
166
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
167
+ learner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("learners.id", ondelete="CASCADE"), nullable=False)
168
+ module_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("modules.id", ondelete="CASCADE"), nullable=False)
169
+ enrollment_status: Mapped[str] = mapped_column(String(30), default="active")
170
+ recommended_by: Mapped[str] = mapped_column(String(30), default="system")
171
+ started_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
172
+ completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True))
173
+ mastery_score: Mapped[float] = mapped_column(Float, default=0.0)
174
+
175
+ learner: Mapped["LearnerORM"] = relationship(back_populates="enrollments")
176
+
177
+
178
+ class InteractionORM(Base):
179
+ __tablename__ = "interactions"
180
+
181
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
182
+ learner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("learners.id", ondelete="CASCADE"), nullable=False)
183
+ module_id: Mapped[uuid.UUID | None] = mapped_column(ForeignKey("modules.id", ondelete="SET NULL"))
184
+ unit_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True))
185
+ activity_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True))
186
+ session_id: Mapped[uuid.UUID | None] = mapped_column(Uuid(as_uuid=True))
187
+ event_type: Mapped[str] = mapped_column(String(50), nullable=False)
188
+ event_timestamp: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
189
+ duration_ms: Mapped[int | None] = mapped_column(Integer)
190
+ payload: Mapped[dict] = mapped_column(JSON, default=dict)
191
+ client_context: Mapped[dict] = mapped_column(JSON, default=dict)
192
+
193
+ learner: Mapped["LearnerORM"] = relationship(back_populates="interactions")
194
+
195
+
196
+ class AssessmentAttemptORM(Base):
197
+ __tablename__ = "assessment_attempts"
198
+
199
+ id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4)
200
+ assessment_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("assessments.id", ondelete="CASCADE"), nullable=False)
201
+ learner_id: Mapped[uuid.UUID] = mapped_column(ForeignKey("learners.id", ondelete="CASCADE"), nullable=False)
202
+ time_limit_minutes: Mapped[int] = mapped_column(Integer, nullable=False)
203
+ items: Mapped[list] = mapped_column(JSON, default=list)
204
+ status: Mapped[str] = mapped_column(String(30), default="in_progress")
205
+ integrity_status: Mapped[str] = mapped_column(String(20), default="pending")
206
+ created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
207
+ score: Mapped[float | None] = mapped_column(Float)
208
+ pass_status: Mapped[str | None] = mapped_column(String(20))
209
+ feedback: Mapped[list] = mapped_column(JSON, default=list)
210
+ human_review_required: Mapped[bool] = mapped_column(Boolean, default=False)
211
+
212
+ assessment: Mapped["AssessmentDefinitionORM"] = relationship(back_populates="attempts")
backend/app/models/schemas.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import date, datetime, timezone
4
+ from typing import Any, Literal
5
+ from uuid import UUID, uuid4
6
+
7
+ from pydantic import BaseModel, ConfigDict, EmailStr, Field
8
+
9
+
10
+ def utcnow() -> datetime:
11
+ return datetime.now(timezone.utc)
12
+
13
+
14
+ class PriorLearningItem(BaseModel):
15
+ model_config = ConfigDict(from_attributes=True)
16
+
17
+ learning_type: Literal["formal", "non_formal", "informal"]
18
+ title: str
19
+ provider: str | None = None
20
+ description: str | None = None
21
+
22
+
23
+ class LearnerGoal(BaseModel):
24
+ model_config = ConfigDict(from_attributes=True)
25
+
26
+ goal_horizon: Literal["short_term", "long_term"]
27
+ goal_text: str
28
+ target_date: date | None = None
29
+ priority: int = 3
30
+
31
+
32
+ class LearnerPreferences(BaseModel):
33
+ model_config = ConfigDict(from_attributes=True)
34
+
35
+ preferred_modalities: list[str] = Field(default_factory=list)
36
+ preferred_session_length_minutes: int | None = None
37
+ language_preferences: list[str] = Field(default_factory=list)
38
+ accessibility_needs: dict[str, Any] = Field(default_factory=dict)
39
+
40
+
41
+ class RegisterLearnerRequest(BaseModel):
42
+ email: EmailStr
43
+ first_name: str
44
+ last_name: str
45
+ locale: str = "en-US"
46
+ timezone: str
47
+ educational_level: str
48
+ consent_personalization: bool = False
49
+ consent_analytics: bool = False
50
+ consent_version: str | None = None
51
+ prior_learning: list[PriorLearningItem]
52
+ goals: list[LearnerGoal]
53
+ preferences: LearnerPreferences | None = None
54
+
55
+
56
+ class LearnerRegistrationResponse(BaseModel):
57
+ learner_id: UUID
58
+ next_step: str = "complete_diagnostic_assessment"
59
+
60
+
61
+ class DiagnosticSubmission(BaseModel):
62
+ responses: list[dict[str, Any]]
63
+
64
+
65
+ class UpdateLearnerProfileRequest(BaseModel):
66
+ goals: list[LearnerGoal] | None = None
67
+ preferences: LearnerPreferences | None = None
68
+
69
+
70
+ class LearnerProfile(BaseModel):
71
+ learner_id: UUID
72
+ readiness_level: str
73
+ readiness_score: float
74
+ goals: list[LearnerGoal]
75
+ preferences: LearnerPreferences
76
+ mastery_map: dict[str, float]
77
+ risk_profile: dict[str, Any]
78
+ profile_vector: list[float]
79
+
80
+
81
+ class ModuleObjective(BaseModel):
82
+ model_config = ConfigDict(from_attributes=True)
83
+
84
+ id: UUID = Field(default_factory=uuid4)
85
+ text: str
86
+ bloom_level: str
87
+ competency_tag: str
88
+ measurable_outcome: str
89
+
90
+
91
+ class ModuleUnit(BaseModel):
92
+ model_config = ConfigDict(from_attributes=True)
93
+
94
+ id: UUID = Field(default_factory=uuid4)
95
+ title: str
96
+ unit_type: str
97
+ chunk_minutes: int
98
+ content_ref: str
99
+ adaptive_rules: dict[str, Any] = Field(default_factory=dict)
100
+
101
+
102
+ class ModuleActivity(BaseModel):
103
+ model_config = ConfigDict(from_attributes=True)
104
+
105
+ id: UUID = Field(default_factory=uuid4)
106
+ title: str
107
+ activity_type: str
108
+ instructions: str
109
+ max_score: float | None = None
110
+ required: bool = True
111
+
112
+
113
+ class ModuleCompletionCriteria(BaseModel):
114
+ model_config = ConfigDict(from_attributes=True)
115
+
116
+ min_unit_completion_ratio: float
117
+ min_formative_score: float
118
+ require_summative_pass: bool = True
119
+ min_time_on_task_minutes: int | None = None
120
+ integrity_clearance_required: bool = True
121
+
122
+
123
+ class ModuleSummary(BaseModel):
124
+ model_config = ConfigDict(from_attributes=True)
125
+
126
+ id: UUID
127
+ code: str
128
+ title: str
129
+ description: str
130
+ ects_credits: float = 5.0
131
+ difficulty_level: str
132
+ estimated_total_minutes: int
133
+ self_paced: bool = True
134
+ adaptive_enabled: bool = True
135
+ topics: list[str] = Field(default_factory=list)
136
+
137
+
138
+ class ModuleDetail(ModuleSummary):
139
+ objectives: list[ModuleObjective]
140
+ units: list[ModuleUnit]
141
+ activities: list[ModuleActivity]
142
+ completion_criteria: ModuleCompletionCriteria
143
+ prerequisites: list[UUID] = Field(default_factory=list)
144
+ module_vector: list[float] = Field(default_factory=list)
145
+
146
+
147
+ class RecommendationItem(BaseModel):
148
+ model_config = ConfigDict(from_attributes=True)
149
+
150
+ module_id: UUID
151
+ rank: int
152
+ score: float
153
+ reasons: list[str]
154
+
155
+
156
+ class RecommendationResponse(BaseModel):
157
+ learner_id: UUID
158
+ generated_at: datetime = Field(default_factory=utcnow)
159
+ items: list[RecommendationItem]
160
+
161
+
162
+ class EnrollmentCreate(BaseModel):
163
+ learner_id: UUID
164
+ module_id: UUID
165
+
166
+
167
+ class Enrollment(BaseModel):
168
+ model_config = ConfigDict(from_attributes=True)
169
+
170
+ id: UUID = Field(default_factory=uuid4)
171
+ learner_id: UUID
172
+ module_id: UUID
173
+ enrollment_status: str = "active"
174
+ recommended_by: str = "system"
175
+ started_at: datetime | None = Field(default_factory=utcnow)
176
+ completed_at: datetime | None = None
177
+ mastery_score: float = 0.0
178
+
179
+
180
+ class ProgressItem(BaseModel):
181
+ model_config = ConfigDict(from_attributes=True)
182
+
183
+ module_id: UUID
184
+ completion_percent: float
185
+ mastery_score: float
186
+ next_recommended_action: str
187
+
188
+
189
+ class ProgressSummary(BaseModel):
190
+ learner_id: UUID
191
+ active_enrollments: list[ProgressItem]
192
+
193
+
194
+ class InteractionEvent(BaseModel):
195
+ model_config = ConfigDict(from_attributes=True)
196
+
197
+ learner_id: UUID
198
+ module_id: UUID | None = None
199
+ unit_id: UUID | None = None
200
+ activity_id: UUID | None = None
201
+ session_id: UUID | None = None
202
+ event_type: str
203
+ event_timestamp: datetime = Field(default_factory=utcnow)
204
+ duration_ms: int | None = None
205
+ payload: dict[str, Any] = Field(default_factory=dict)
206
+ client_context: dict[str, Any] = Field(default_factory=dict)
207
+
208
+
209
+ class AssessmentAttemptCreate(BaseModel):
210
+ learner_id: UUID
211
+
212
+
213
+ class AssessmentItem(BaseModel):
214
+ model_config = ConfigDict(from_attributes=True)
215
+
216
+ question_id: UUID = Field(default_factory=uuid4)
217
+ question_type: str
218
+ prompt: str
219
+ objective_tag: str
220
+ time_limit_seconds: int | None = None
221
+ metadata: dict[str, Any] = Field(default_factory=dict)
222
+
223
+
224
+ class AssessmentAttempt(BaseModel):
225
+ model_config = ConfigDict(from_attributes=True)
226
+
227
+ attempt_id: UUID = Field(default_factory=uuid4)
228
+ assessment_id: UUID
229
+ learner_id: UUID
230
+ time_limit_minutes: int
231
+ items: list[AssessmentItem]
232
+ status: str = "in_progress"
233
+ integrity_status: str = "pending"
234
+ created_at: datetime = Field(default_factory=utcnow)
235
+
236
+
237
+ class AssessmentResponseItem(BaseModel):
238
+ question_id: UUID
239
+ answer: Any
240
+
241
+
242
+ class AssessmentResponseSubmission(BaseModel):
243
+ responses: list[AssessmentResponseItem]
244
+
245
+
246
+ class AssessmentAttemptResult(BaseModel):
247
+ model_config = ConfigDict(from_attributes=True)
248
+
249
+ attempt_id: UUID
250
+ score: float
251
+ pass_status: str
252
+ integrity_status: str
253
+ feedback: list[str]
254
+ human_review_required: bool = False
255
+
256
+
257
+ class LearnerDashboard(BaseModel):
258
+ model_config = ConfigDict(from_attributes=True)
259
+
260
+ learner_id: UUID
261
+ mastery: dict[str, float]
262
+ time_on_task_minutes_7d: float
263
+ dropout_risk: float
264
+ success_probability: float
265
+ adaptive_feedback: list[str]
266
+
267
+
268
+ class LearnerRecord(BaseModel):
269
+ model_config = ConfigDict(arbitrary_types_allowed=True)
270
+
271
+ id: UUID = Field(default_factory=uuid4)
272
+ email: EmailStr
273
+ first_name: str
274
+ last_name: str
275
+ locale: str
276
+ timezone: str
277
+ educational_level: str
278
+ consent_personalization: bool
279
+ consent_analytics: bool
280
+ consent_version: str | None = None
281
+ prior_learning: list[PriorLearningItem] = Field(default_factory=list)
282
+ goals: list[LearnerGoal] = Field(default_factory=list)
283
+ preferences: LearnerPreferences = Field(default_factory=LearnerPreferences)
284
+ readiness_level: str = "unknown"
285
+ readiness_score: float = 0.0
286
+ mastery_map: dict[str, float] = Field(default_factory=dict)
287
+ risk_profile: dict[str, Any] = Field(default_factory=dict)
288
+ profile_vector: list[float] = Field(default_factory=list)
289
+ created_at: datetime = Field(default_factory=utcnow)
290
+
291
+
292
+ class AssessmentDefinition(BaseModel):
293
+ model_config = ConfigDict(from_attributes=True)
294
+
295
+ id: UUID = Field(default_factory=uuid4)
296
+ module_id: UUID
297
+ assessment_type: str
298
+ title: str
299
+ blueprint: dict[str, Any]
300
+ time_limit_minutes: int
301
+
302
+
303
+ class StoredInteraction(BaseModel):
304
+ model_config = ConfigDict(from_attributes=True)
305
+
306
+ id: UUID = Field(default_factory=uuid4)
307
+ event: InteractionEvent
backend/app/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Domain services for recommendations, assessments, analytics, and storage."""
backend/app/services/bootstrap.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from sqlalchemy.ext.asyncio import AsyncSession
4
+
5
+ from app.models.schemas import (
6
+ AssessmentDefinition,
7
+ ModuleActivity,
8
+ ModuleCompletionCriteria,
9
+ ModuleDetail,
10
+ ModuleObjective,
11
+ ModuleUnit,
12
+ )
13
+ from app.services import repository
14
+
15
+
16
+ async def seed_demo_data(session: AsyncSession) -> None:
17
+ if await repository.has_modules(session):
18
+ return
19
+
20
+ module_one = ModuleDetail(
21
+ id="11111111-1111-1111-1111-111111111111",
22
+ code="AID-101",
23
+ title="AI-Supported Self-Regulated Learning",
24
+ description="Introduces learners to using AI for planning, monitoring, and reflecting on self-directed study.",
25
+ difficulty_level="foundation",
26
+ estimated_total_minutes=750,
27
+ topics=["self-regulation", "ai literacy"],
28
+ objectives=[
29
+ ModuleObjective(
30
+ text="Explain how AI tools can support planning and self-monitoring.",
31
+ bloom_level="understand",
32
+ competency_tag="self_regulated_learning",
33
+ measurable_outcome="Learner explains planning and monitoring strategies.",
34
+ ),
35
+ ModuleObjective(
36
+ text="Apply AI-supported reflection to improve study habits.",
37
+ bloom_level="apply",
38
+ competency_tag="reflective_practice",
39
+ measurable_outcome="Learner produces an evidence-based reflection.",
40
+ ),
41
+ ],
42
+ units=[
43
+ ModuleUnit(
44
+ title="Planning with AI",
45
+ unit_type="video",
46
+ chunk_minutes=12,
47
+ content_ref="/content/modules/aid-101/planning-with-ai",
48
+ ),
49
+ ModuleUnit(
50
+ title="Monitoring Progress",
51
+ unit_type="reading",
52
+ chunk_minutes=10,
53
+ content_ref="/content/modules/aid-101/monitoring-progress",
54
+ ),
55
+ ],
56
+ activities=[
57
+ ModuleActivity(
58
+ title="Study Strategy Reflection",
59
+ activity_type="reflection",
60
+ instructions="Reflect on how AI could improve your weekly study routine.",
61
+ max_score=20,
62
+ )
63
+ ],
64
+ completion_criteria=ModuleCompletionCriteria(
65
+ min_unit_completion_ratio=0.85,
66
+ min_formative_score=70,
67
+ min_time_on_task_minutes=300,
68
+ ),
69
+ module_vector=[0.35, 0.7, 0.5],
70
+ )
71
+
72
+ module_two = ModuleDetail(
73
+ id="22222222-2222-2222-2222-222222222222",
74
+ code="AID-205",
75
+ title="Scenario-Based Problem Solving with AI",
76
+ description="Builds authentic problem-solving skills through case analysis and AI-supported reasoning.",
77
+ difficulty_level="intermediate",
78
+ estimated_total_minutes=780,
79
+ topics=["problem solving", "scenario analysis"],
80
+ objectives=[
81
+ ModuleObjective(
82
+ text="Analyze scenario constraints and choose suitable AI-supported strategies.",
83
+ bloom_level="analyze",
84
+ competency_tag="applied_ai_literacy",
85
+ measurable_outcome="Learner compares alternative strategies in a case.",
86
+ ),
87
+ ModuleObjective(
88
+ text="Evaluate outputs for reliability, bias, and usefulness.",
89
+ bloom_level="evaluate",
90
+ competency_tag="critical_evaluation",
91
+ measurable_outcome="Learner critiques AI output quality.",
92
+ ),
93
+ ],
94
+ units=[
95
+ ModuleUnit(
96
+ title="Interpreting Realistic Cases",
97
+ unit_type="simulation",
98
+ chunk_minutes=15,
99
+ content_ref="/content/modules/aid-205/interpreting-cases",
100
+ ),
101
+ ModuleUnit(
102
+ title="Evaluating AI Output",
103
+ unit_type="interactive",
104
+ chunk_minutes=14,
105
+ content_ref="/content/modules/aid-205/evaluating-output",
106
+ ),
107
+ ],
108
+ activities=[
109
+ ModuleActivity(
110
+ title="Case Analysis Quiz",
111
+ activity_type="quiz",
112
+ instructions="Select and justify the best response in each scenario.",
113
+ max_score=30,
114
+ )
115
+ ],
116
+ completion_criteria=ModuleCompletionCriteria(
117
+ min_unit_completion_ratio=0.9,
118
+ min_formative_score=75,
119
+ min_time_on_task_minutes=320,
120
+ ),
121
+ prerequisites=["11111111-1111-1111-1111-111111111111"],
122
+ module_vector=[0.65, 0.8, 0.6],
123
+ )
124
+
125
+ await repository.create_module(session, module_one)
126
+ await repository.create_module(session, module_two)
127
+
128
+ await repository.create_assessment(
129
+ session,
130
+ AssessmentDefinition(
131
+ id="33333333-3333-3333-3333-333333333333",
132
+ module_id=module_one.id,
133
+ assessment_type="summative",
134
+ title="AID-101 Summative Assessment",
135
+ blueprint={
136
+ "objectives": ["self_regulated_learning", "reflective_practice"],
137
+ "time_per_item_seconds": 420,
138
+ },
139
+ time_limit_minutes=45,
140
+ )
141
+ )
backend/app/services/engines.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from statistics import mean
4
+ from uuid import UUID
5
+
6
+ from sqlalchemy.ext.asyncio import AsyncSession
7
+
8
+ from app.models.schemas import (
9
+ AssessmentAttempt,
10
+ AssessmentAttemptResult,
11
+ AssessmentDefinition,
12
+ AssessmentItem,
13
+ DiagnosticSubmission,
14
+ InteractionEvent,
15
+ LearnerDashboard,
16
+ LearnerProfile,
17
+ LearnerRecord,
18
+ ModuleDetail,
19
+ ProgressItem,
20
+ ProgressSummary,
21
+ RecommendationItem,
22
+ RecommendationResponse,
23
+ )
24
+ from app.services import repository
25
+
26
+
27
+ def build_profile(learner: LearnerRecord, diagnostic: DiagnosticSubmission | None = None) -> LearnerRecord:
28
+ prior_score = min(len(learner.prior_learning) / 10, 1.0)
29
+ goal_score = min(len(learner.goals) / 5, 1.0)
30
+ pref_score = 0.2 if learner.preferences.preferred_modalities else 0.0
31
+ readiness = 0.5
32
+
33
+ if diagnostic and diagnostic.responses:
34
+ raw = mean(float(item.get("score", 0.5)) for item in diagnostic.responses)
35
+ readiness = max(0.0, min(raw, 1.0))
36
+
37
+ learner.readiness_score = round(readiness * 100, 2)
38
+ learner.readiness_level = (
39
+ "advanced" if readiness >= 0.75 else "intermediate" if readiness >= 0.45 else "foundation"
40
+ )
41
+ learner.profile_vector = [
42
+ round(prior_score, 4),
43
+ round(goal_score, 4),
44
+ round(pref_score, 4),
45
+ round(readiness, 4),
46
+ ]
47
+ learner.mastery_map = {
48
+ "self_regulated_learning": round(readiness, 2),
49
+ "applied_ai_literacy": round((prior_score + readiness) / 2, 2),
50
+ "reflective_practice": round((goal_score + pref_score + readiness) / 2, 2),
51
+ }
52
+ learner.risk_profile = {
53
+ "dropout_risk": round(max(0.1, 1 - readiness), 2),
54
+ "recommended_support_level": "high" if readiness < 0.4 else "standard",
55
+ }
56
+ return learner
57
+
58
+
59
+ def learner_to_profile(learner: LearnerRecord) -> LearnerProfile:
60
+ return LearnerProfile(
61
+ learner_id=learner.id,
62
+ readiness_level=learner.readiness_level,
63
+ readiness_score=learner.readiness_score,
64
+ goals=learner.goals,
65
+ preferences=learner.preferences,
66
+ mastery_map=learner.mastery_map,
67
+ risk_profile=learner.risk_profile,
68
+ profile_vector=learner.profile_vector,
69
+ )
70
+
71
+
72
+ def rank_modules(learner: LearnerRecord, modules: list[ModuleDetail]) -> RecommendationResponse:
73
+ scored: list[RecommendationItem] = []
74
+ learner_topics = {goal.goal_text.lower() for goal in learner.goals}
75
+
76
+ for module in modules:
77
+ topic_match = sum(1 for topic in module.topics if any(topic.lower() in g for g in learner_topics))
78
+ readiness_alignment = 1 - abs((learner.readiness_score / 100) - module.module_vector[0])
79
+ workload_score = 1 - min(module.estimated_total_minutes / 1500, 1.0)
80
+ score = round(0.45 * topic_match + 0.35 * readiness_alignment + 0.20 * workload_score, 4)
81
+ reasons = [
82
+ f"Aligned with {module.topics[0] if module.topics else 'your stated goals'}",
83
+ f"Suitable for {learner.readiness_level} readiness",
84
+ "Supports a self-paced 5 ECTS pathway",
85
+ ]
86
+ scored.append(RecommendationItem(module_id=module.id, rank=0, score=score, reasons=reasons))
87
+
88
+ ranked = sorted(scored, key=lambda item: item.score, reverse=True)
89
+ for index, item in enumerate(ranked, start=1):
90
+ item.rank = index
91
+
92
+ return RecommendationResponse(learner_id=learner.id, items=ranked[:10])
93
+
94
+
95
+ def generate_assessment_attempt(assessment: AssessmentDefinition, learner_id: UUID) -> AssessmentAttempt:
96
+ objectives = assessment.blueprint.get("objectives", [])
97
+ items = [
98
+ AssessmentItem(
99
+ question_type="scenario_open",
100
+ prompt=f"Scenario {index + 1}: Apply {objective} in a realistic learning context and justify your response.",
101
+ objective_tag=objective,
102
+ time_limit_seconds=assessment.blueprint.get("time_per_item_seconds", 300),
103
+ metadata={"unique_variant_seed": f"{learner_id}-{index}"},
104
+ )
105
+ for index, objective in enumerate(objectives)
106
+ ]
107
+ return AssessmentAttempt(
108
+ assessment_id=assessment.id,
109
+ learner_id=learner_id,
110
+ time_limit_minutes=assessment.time_limit_minutes,
111
+ items=items,
112
+ )
113
+
114
+
115
+ def evaluate_attempt(attempt: AssessmentAttempt, responses_count: int) -> AssessmentAttemptResult:
116
+ completion_ratio = responses_count / max(len(attempt.items), 1)
117
+ score = round(min(100.0, 55 + completion_ratio * 45), 2)
118
+ integrity_status = "review" if responses_count < len(attempt.items) else "pass"
119
+ pass_status = "pass" if score >= 70 and integrity_status == "pass" else "review"
120
+ feedback = [
121
+ "Responses were evaluated against module objectives and participation thresholds.",
122
+ "Final certification should also check plagiarism and anomaly signals in production.",
123
+ ]
124
+ return AssessmentAttemptResult(
125
+ attempt_id=attempt.attempt_id,
126
+ score=score,
127
+ pass_status=pass_status,
128
+ integrity_status=integrity_status,
129
+ feedback=feedback,
130
+ human_review_required=integrity_status != "pass",
131
+ )
132
+
133
+
134
+ async def build_progress_summary(session: AsyncSession, learner_id: UUID) -> ProgressSummary:
135
+ enrollments = await repository.list_enrollments_for_learner(session, learner_id)
136
+ interactions = await repository.list_interactions_for_learner(session, learner_id)
137
+ summary_items: list[ProgressItem] = []
138
+
139
+ for enrollment in enrollments:
140
+ module_events = [i for i in interactions if i.module_id == enrollment.module_id]
141
+ completion = min(len(module_events) * 10.0, 100.0)
142
+ mastery = round(min(100.0, 40 + len(module_events) * 5.5), 2)
143
+ summary_items.append(
144
+ ProgressItem(
145
+ module_id=enrollment.module_id,
146
+ completion_percent=completion,
147
+ mastery_score=mastery,
148
+ next_recommended_action="Complete the next content chunk and formative reflection.",
149
+ )
150
+ )
151
+
152
+ return ProgressSummary(learner_id=learner_id, active_enrollments=summary_items)
153
+
154
+
155
+ async def build_dashboard(session: AsyncSession, learner: LearnerRecord) -> LearnerDashboard:
156
+ interactions = await repository.list_interactions_for_learner(session, learner.id)
157
+ total_minutes = round(sum((item.duration_ms or 0) for item in interactions) / 60000, 2)
158
+ dropout_risk = learner.risk_profile.get("dropout_risk", 0.3)
159
+ success_probability = round(max(0.2, 1 - dropout_risk + 0.15), 2)
160
+
161
+ return LearnerDashboard(
162
+ learner_id=learner.id,
163
+ mastery=learner.mastery_map,
164
+ time_on_task_minutes_7d=total_minutes,
165
+ dropout_risk=dropout_risk,
166
+ success_probability=success_probability,
167
+ adaptive_feedback=[
168
+ "Continue with short focused sessions of 10 to 15 minutes.",
169
+ "Prioritize scenario-based practice to improve transfer and assessment performance.",
170
+ ],
171
+ )
backend/app/services/repository.py ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from uuid import UUID
4
+
5
+ from fastapi import HTTPException, status
6
+ from sqlalchemy import select
7
+ from sqlalchemy.ext.asyncio import AsyncSession
8
+ from sqlalchemy.orm import selectinload
9
+
10
+ from app.models.orm import (
11
+ AssessmentAttemptORM,
12
+ AssessmentDefinitionORM,
13
+ EnrollmentORM,
14
+ InteractionORM,
15
+ LearnerGoalORM,
16
+ LearnerORM,
17
+ LearnerPriorLearningORM,
18
+ ModuleActivityORM,
19
+ ModuleORM,
20
+ ModuleObjectiveORM,
21
+ ModuleUnitORM,
22
+ )
23
+ from app.models.schemas import (
24
+ AssessmentAttempt,
25
+ AssessmentAttemptResult,
26
+ AssessmentDefinition,
27
+ Enrollment,
28
+ InteractionEvent,
29
+ LearnerGoal,
30
+ LearnerPreferences,
31
+ LearnerProfile,
32
+ LearnerRecord,
33
+ ModuleActivity,
34
+ ModuleCompletionCriteria,
35
+ ModuleDetail,
36
+ ModuleObjective,
37
+ ModuleUnit,
38
+ PriorLearningItem,
39
+ )
40
+
41
+
42
+ def _learner_to_schema(model: LearnerORM) -> LearnerRecord:
43
+ return LearnerRecord(
44
+ id=model.id,
45
+ email=model.email,
46
+ first_name=model.first_name,
47
+ last_name=model.last_name,
48
+ locale=model.locale,
49
+ timezone=model.timezone,
50
+ educational_level=model.educational_level,
51
+ consent_personalization=model.consent_personalization,
52
+ consent_analytics=model.consent_analytics,
53
+ consent_version=model.consent_version,
54
+ prior_learning=[
55
+ PriorLearningItem(
56
+ learning_type=item.learning_type,
57
+ title=item.title,
58
+ provider=item.provider,
59
+ description=item.description,
60
+ )
61
+ for item in model.prior_learning_items
62
+ ],
63
+ goals=[
64
+ LearnerGoal(
65
+ goal_horizon=item.goal_horizon,
66
+ goal_text=item.goal_text,
67
+ target_date=item.target_date,
68
+ priority=item.priority,
69
+ )
70
+ for item in model.goals
71
+ ],
72
+ preferences=LearnerPreferences.model_validate(model.preferences or {}),
73
+ readiness_level=model.readiness_level,
74
+ readiness_score=model.readiness_score,
75
+ mastery_map=model.mastery_map or {},
76
+ risk_profile=model.risk_profile or {},
77
+ profile_vector=model.profile_vector or [],
78
+ created_at=model.created_at,
79
+ )
80
+
81
+
82
+ def _module_to_schema(model: ModuleORM) -> ModuleDetail:
83
+ return ModuleDetail(
84
+ id=model.id,
85
+ code=model.code,
86
+ title=model.title,
87
+ description=model.description,
88
+ ects_credits=model.ects_credits,
89
+ difficulty_level=model.difficulty_level,
90
+ estimated_total_minutes=model.estimated_total_minutes,
91
+ self_paced=model.self_paced,
92
+ adaptive_enabled=model.adaptive_enabled,
93
+ topics=model.topics or [],
94
+ objectives=[
95
+ ModuleObjective(
96
+ id=item.id,
97
+ text=item.text,
98
+ bloom_level=item.bloom_level,
99
+ competency_tag=item.competency_tag,
100
+ measurable_outcome=item.measurable_outcome,
101
+ )
102
+ for item in model.objectives
103
+ ],
104
+ units=[
105
+ ModuleUnit(
106
+ id=item.id,
107
+ title=item.title,
108
+ unit_type=item.unit_type,
109
+ chunk_minutes=item.chunk_minutes,
110
+ content_ref=item.content_ref,
111
+ adaptive_rules=item.adaptive_rules or {},
112
+ )
113
+ for item in model.units
114
+ ],
115
+ activities=[
116
+ ModuleActivity(
117
+ id=item.id,
118
+ title=item.title,
119
+ activity_type=item.activity_type,
120
+ instructions=item.instructions,
121
+ max_score=item.max_score,
122
+ required=item.required,
123
+ )
124
+ for item in model.activities
125
+ ],
126
+ completion_criteria=ModuleCompletionCriteria.model_validate(model.completion_criteria or {}),
127
+ prerequisites=model.prerequisites or [],
128
+ module_vector=model.module_vector or [],
129
+ )
130
+
131
+
132
+ def _assessment_to_schema(model: AssessmentDefinitionORM) -> AssessmentDefinition:
133
+ return AssessmentDefinition(
134
+ id=model.id,
135
+ module_id=model.module_id,
136
+ assessment_type=model.assessment_type,
137
+ title=model.title,
138
+ blueprint=model.blueprint or {},
139
+ time_limit_minutes=model.time_limit_minutes,
140
+ )
141
+
142
+
143
+ def _attempt_to_schema(model: AssessmentAttemptORM) -> AssessmentAttempt:
144
+ return AssessmentAttempt(
145
+ attempt_id=model.id,
146
+ assessment_id=model.assessment_id,
147
+ learner_id=model.learner_id,
148
+ time_limit_minutes=model.time_limit_minutes,
149
+ items=model.items or [],
150
+ status=model.status,
151
+ integrity_status=model.integrity_status,
152
+ created_at=model.created_at,
153
+ )
154
+
155
+
156
+ async def create_learner(session: AsyncSession, record: LearnerRecord) -> LearnerRecord:
157
+ learner = LearnerORM(
158
+ id=record.id,
159
+ email=str(record.email),
160
+ first_name=record.first_name,
161
+ last_name=record.last_name,
162
+ locale=record.locale,
163
+ timezone=record.timezone,
164
+ educational_level=record.educational_level,
165
+ consent_personalization=record.consent_personalization,
166
+ consent_analytics=record.consent_analytics,
167
+ consent_version=record.consent_version,
168
+ readiness_level=record.readiness_level,
169
+ readiness_score=record.readiness_score,
170
+ mastery_map=record.mastery_map,
171
+ risk_profile=record.risk_profile,
172
+ profile_vector=record.profile_vector,
173
+ preferences=record.preferences.model_dump(),
174
+ prior_learning_items=[
175
+ LearnerPriorLearningORM(
176
+ learning_type=item.learning_type,
177
+ title=item.title,
178
+ provider=item.provider,
179
+ description=item.description,
180
+ )
181
+ for item in record.prior_learning
182
+ ],
183
+ goals=[
184
+ LearnerGoalORM(
185
+ goal_horizon=item.goal_horizon,
186
+ goal_text=item.goal_text,
187
+ target_date=item.target_date,
188
+ priority=item.priority,
189
+ )
190
+ for item in record.goals
191
+ ],
192
+ )
193
+ session.add(learner)
194
+ await session.commit()
195
+ await session.refresh(learner)
196
+ return await get_learner(session, learner.id)
197
+
198
+
199
+ async def update_learner(session: AsyncSession, record: LearnerRecord) -> LearnerRecord:
200
+ learner = await session.get(LearnerORM, record.id, options=(selectinload(LearnerORM.prior_learning_items), selectinload(LearnerORM.goals)))
201
+ if learner is None:
202
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Learner not found")
203
+
204
+ learner.first_name = record.first_name
205
+ learner.last_name = record.last_name
206
+ learner.locale = record.locale
207
+ learner.timezone = record.timezone
208
+ learner.educational_level = record.educational_level
209
+ learner.consent_personalization = record.consent_personalization
210
+ learner.consent_analytics = record.consent_analytics
211
+ learner.consent_version = record.consent_version
212
+ learner.readiness_level = record.readiness_level
213
+ learner.readiness_score = record.readiness_score
214
+ learner.mastery_map = record.mastery_map
215
+ learner.risk_profile = record.risk_profile
216
+ learner.profile_vector = record.profile_vector
217
+ learner.preferences = record.preferences.model_dump()
218
+ learner.prior_learning_items = [
219
+ LearnerPriorLearningORM(
220
+ learning_type=item.learning_type,
221
+ title=item.title,
222
+ provider=item.provider,
223
+ description=item.description,
224
+ )
225
+ for item in record.prior_learning
226
+ ]
227
+ learner.goals = [
228
+ LearnerGoalORM(
229
+ goal_horizon=item.goal_horizon,
230
+ goal_text=item.goal_text,
231
+ target_date=item.target_date,
232
+ priority=item.priority,
233
+ )
234
+ for item in record.goals
235
+ ]
236
+ await session.commit()
237
+ return await get_learner(session, learner.id)
238
+
239
+
240
+ async def get_learner(session: AsyncSession, learner_id: UUID) -> LearnerRecord:
241
+ stmt = (
242
+ select(LearnerORM)
243
+ .where(LearnerORM.id == learner_id)
244
+ .options(selectinload(LearnerORM.prior_learning_items), selectinload(LearnerORM.goals))
245
+ )
246
+ learner = (await session.execute(stmt)).scalar_one_or_none()
247
+ if learner is None:
248
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Learner not found")
249
+ return _learner_to_schema(learner)
250
+
251
+
252
+ async def list_modules(session: AsyncSession) -> list[ModuleDetail]:
253
+ stmt = select(ModuleORM).options(
254
+ selectinload(ModuleORM.objectives),
255
+ selectinload(ModuleORM.units),
256
+ selectinload(ModuleORM.activities),
257
+ )
258
+ modules = (await session.execute(stmt)).scalars().all()
259
+ return [_module_to_schema(item) for item in modules]
260
+
261
+
262
+ async def get_module(session: AsyncSession, module_id: UUID) -> ModuleDetail:
263
+ stmt = (
264
+ select(ModuleORM)
265
+ .where(ModuleORM.id == module_id)
266
+ .options(selectinload(ModuleORM.objectives), selectinload(ModuleORM.units), selectinload(ModuleORM.activities))
267
+ )
268
+ module = (await session.execute(stmt)).scalar_one_or_none()
269
+ if module is None:
270
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Module not found")
271
+ return _module_to_schema(module)
272
+
273
+
274
+ async def create_module(session: AsyncSession, module: ModuleDetail) -> ModuleDetail:
275
+ module_orm = ModuleORM(
276
+ id=module.id,
277
+ code=module.code,
278
+ title=module.title,
279
+ description=module.description,
280
+ ects_credits=module.ects_credits,
281
+ difficulty_level=module.difficulty_level,
282
+ estimated_total_minutes=module.estimated_total_minutes,
283
+ self_paced=module.self_paced,
284
+ adaptive_enabled=module.adaptive_enabled,
285
+ topics=module.topics,
286
+ completion_criteria=module.completion_criteria.model_dump(),
287
+ prerequisites=[str(item) for item in module.prerequisites],
288
+ module_vector=module.module_vector,
289
+ objectives=[
290
+ ModuleObjectiveORM(
291
+ id=item.id,
292
+ text=item.text,
293
+ bloom_level=item.bloom_level,
294
+ competency_tag=item.competency_tag,
295
+ measurable_outcome=item.measurable_outcome,
296
+ sort_order=index,
297
+ )
298
+ for index, item in enumerate(module.objectives)
299
+ ],
300
+ units=[
301
+ ModuleUnitORM(
302
+ id=item.id,
303
+ title=item.title,
304
+ unit_type=item.unit_type,
305
+ chunk_minutes=item.chunk_minutes,
306
+ content_ref=item.content_ref,
307
+ adaptive_rules=item.adaptive_rules,
308
+ sort_order=index,
309
+ )
310
+ for index, item in enumerate(module.units)
311
+ ],
312
+ activities=[
313
+ ModuleActivityORM(
314
+ id=item.id,
315
+ title=item.title,
316
+ activity_type=item.activity_type,
317
+ instructions=item.instructions,
318
+ max_score=item.max_score,
319
+ required=item.required,
320
+ sort_order=index,
321
+ )
322
+ for index, item in enumerate(module.activities)
323
+ ],
324
+ )
325
+ session.add(module_orm)
326
+ await session.commit()
327
+ return await get_module(session, module.id)
328
+
329
+
330
+ async def create_assessment(session: AsyncSession, definition: AssessmentDefinition) -> AssessmentDefinition:
331
+ assessment = AssessmentDefinitionORM(
332
+ id=definition.id,
333
+ module_id=definition.module_id,
334
+ assessment_type=definition.assessment_type,
335
+ title=definition.title,
336
+ blueprint=definition.blueprint,
337
+ time_limit_minutes=definition.time_limit_minutes,
338
+ )
339
+ session.add(assessment)
340
+ await session.commit()
341
+ return await get_assessment(session, definition.id)
342
+
343
+
344
+ async def get_assessment(session: AsyncSession, assessment_id: UUID) -> AssessmentDefinition:
345
+ assessment = await session.get(AssessmentDefinitionORM, assessment_id)
346
+ if assessment is None:
347
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assessment not found")
348
+ return _assessment_to_schema(assessment)
349
+
350
+
351
+ async def create_enrollment(session: AsyncSession, enrollment: Enrollment) -> Enrollment:
352
+ model = EnrollmentORM(
353
+ id=enrollment.id,
354
+ learner_id=enrollment.learner_id,
355
+ module_id=enrollment.module_id,
356
+ enrollment_status=enrollment.enrollment_status,
357
+ recommended_by=enrollment.recommended_by,
358
+ mastery_score=enrollment.mastery_score,
359
+ completed_at=enrollment.completed_at,
360
+ )
361
+ session.add(model)
362
+ await session.commit()
363
+ await session.refresh(model)
364
+ return Enrollment.model_validate(model)
365
+
366
+
367
+ async def list_enrollments_for_learner(session: AsyncSession, learner_id: UUID) -> list[Enrollment]:
368
+ stmt = select(EnrollmentORM).where(EnrollmentORM.learner_id == learner_id)
369
+ enrollments = (await session.execute(stmt)).scalars().all()
370
+ return [Enrollment.model_validate(item) for item in enrollments]
371
+
372
+
373
+ async def create_interaction(session: AsyncSession, event: InteractionEvent) -> InteractionEvent:
374
+ model = InteractionORM(
375
+ learner_id=event.learner_id,
376
+ module_id=event.module_id,
377
+ unit_id=event.unit_id,
378
+ activity_id=event.activity_id,
379
+ session_id=event.session_id,
380
+ event_type=event.event_type,
381
+ event_timestamp=event.event_timestamp,
382
+ duration_ms=event.duration_ms,
383
+ payload=event.payload,
384
+ client_context=event.client_context,
385
+ )
386
+ session.add(model)
387
+ await session.commit()
388
+ return event
389
+
390
+
391
+ async def list_interactions_for_learner(session: AsyncSession, learner_id: UUID) -> list[InteractionEvent]:
392
+ stmt = select(InteractionORM).where(InteractionORM.learner_id == learner_id)
393
+ interactions = (await session.execute(stmt)).scalars().all()
394
+ return [
395
+ InteractionEvent(
396
+ learner_id=item.learner_id,
397
+ module_id=item.module_id,
398
+ unit_id=item.unit_id,
399
+ activity_id=item.activity_id,
400
+ session_id=item.session_id,
401
+ event_type=item.event_type,
402
+ event_timestamp=item.event_timestamp,
403
+ duration_ms=item.duration_ms,
404
+ payload=item.payload or {},
405
+ client_context=item.client_context or {},
406
+ )
407
+ for item in interactions
408
+ ]
409
+
410
+
411
+ async def create_attempt(session: AsyncSession, attempt: AssessmentAttempt) -> AssessmentAttempt:
412
+ model = AssessmentAttemptORM(
413
+ id=attempt.attempt_id,
414
+ assessment_id=attempt.assessment_id,
415
+ learner_id=attempt.learner_id,
416
+ time_limit_minutes=attempt.time_limit_minutes,
417
+ items=[item.model_dump(mode="json") for item in attempt.items],
418
+ status=attempt.status,
419
+ integrity_status=attempt.integrity_status,
420
+ )
421
+ session.add(model)
422
+ await session.commit()
423
+ return await get_attempt(session, attempt.attempt_id)
424
+
425
+
426
+ async def get_attempt(session: AsyncSession, attempt_id: UUID) -> AssessmentAttempt:
427
+ attempt = await session.get(AssessmentAttemptORM, attempt_id)
428
+ if attempt is None:
429
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assessment attempt not found")
430
+ return _attempt_to_schema(attempt)
431
+
432
+
433
+ async def save_result(session: AsyncSession, result: AssessmentAttemptResult) -> AssessmentAttemptResult:
434
+ attempt = await session.get(AssessmentAttemptORM, result.attempt_id)
435
+ if attempt is None:
436
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assessment attempt not found")
437
+ attempt.score = result.score
438
+ attempt.pass_status = result.pass_status
439
+ attempt.integrity_status = result.integrity_status
440
+ attempt.feedback = result.feedback
441
+ attempt.human_review_required = result.human_review_required
442
+ attempt.status = "completed"
443
+ await session.commit()
444
+ return result
445
+
446
+
447
+ async def get_result(session: AsyncSession, attempt_id: UUID) -> AssessmentAttemptResult:
448
+ attempt = await session.get(AssessmentAttemptORM, attempt_id)
449
+ if attempt is None or attempt.score is None:
450
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Assessment result not ready")
451
+ return AssessmentAttemptResult(
452
+ attempt_id=attempt.id,
453
+ score=attempt.score,
454
+ pass_status=attempt.pass_status or "review",
455
+ integrity_status=attempt.integrity_status,
456
+ feedback=attempt.feedback or [],
457
+ human_review_required=attempt.human_review_required,
458
+ )
459
+
460
+
461
+ async def module_exists(session: AsyncSession, module_id: UUID) -> bool:
462
+ return await session.get(ModuleORM, module_id) is not None
463
+
464
+
465
+ async def learner_exists(session: AsyncSession, learner_id: UUID) -> bool:
466
+ return await session.get(LearnerORM, learner_id) is not None
467
+
468
+
469
+ async def has_modules(session: AsyncSession) -> bool:
470
+ stmt = select(ModuleORM.id).limit(1)
471
+ return (await session.execute(stmt)).first() is not None
backend/pyproject.toml ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "aididact-backend"
3
+ version = "0.1.0"
4
+ description = "FastAPI scaffold for the AIDidact AI-powered microlearning ecosystem."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ dependencies = [
8
+ "fastapi==0.116.1",
9
+ "uvicorn[standard]==0.35.0",
10
+ "pydantic==2.11.7",
11
+ "pydantic-settings==2.10.1",
12
+ "email-validator==2.2.0",
13
+ "sqlalchemy==2.0.43",
14
+ "asyncpg==0.30.0",
15
+ "alembic==1.16.4",
16
+ "greenlet==3.2.4",
17
+ "aiosqlite==0.21.0",
18
+ ]
19
+
20
+ [build-system]
21
+ requires = ["setuptools>=68"]
22
+ build-backend = "setuptools.build_meta"
backend/requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.116.1
2
+ uvicorn[standard]==0.35.0
3
+ pydantic==2.11.7
4
+ pydantic-settings==2.10.1
5
+ email-validator==2.2.0
6
+ sqlalchemy==2.0.43
7
+ asyncpg==0.30.0
8
+ alembic==1.16.4
9
+ greenlet==3.2.4
10
+ aiosqlite==0.21.0
docs/algorithms.md ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Core Algorithms
2
+
3
+ ## 1. Learner Profile Vector
4
+
5
+ ```text
6
+ FUNCTION build_learner_profile_vector(registration, diagnostic, history):
7
+ prior_learning_embedding = embed(registration.prior_learning)
8
+ goal_embedding = embed(registration.goals)
9
+ education_features = encode_categorical(registration.educational_level)
10
+ readiness_features = normalize(diagnostic.scores)
11
+ preference_features = encode_optional(registration.preferences)
12
+ behavior_features = aggregate(history)
13
+
14
+ profile_vector = concatenate(
15
+ prior_learning_embedding,
16
+ goal_embedding,
17
+ education_features,
18
+ readiness_features,
19
+ preference_features,
20
+ behavior_features
21
+ )
22
+
23
+ mastery_map = initialize_mastery(diagnostic)
24
+ RETURN { profile_vector, mastery_map }
25
+ ```
26
+
27
+ ## 2. Module Vectorization
28
+
29
+ ```text
30
+ FUNCTION build_module_vector(module):
31
+ objective_embedding = embed(module.objectives)
32
+ topic_embedding = embed(module.topics)
33
+ bloom_features = encode_bloom_levels(module.objectives)
34
+ workload_features = normalize(module.estimated_minutes, module.ects)
35
+ activity_features = encode_multilabel(module.activity_types)
36
+ prerequisite_features = encode_graph_position(module.prerequisites)
37
+
38
+ RETURN concatenate(
39
+ objective_embedding,
40
+ topic_embedding,
41
+ bloom_features,
42
+ workload_features,
43
+ activity_features,
44
+ prerequisite_features
45
+ )
46
+ ```
47
+
48
+ ## 3. Hybrid Recommendation
49
+
50
+ ```text
51
+ FUNCTION recommend_modules(learner, candidates):
52
+ FOR module IN candidates:
53
+ content_score = cosine_similarity(learner.profile_vector, module.vector)
54
+ collaborative_score = collaborative_model.predict(learner.id, module.id)
55
+ readiness_score = readiness_alignment(learner.mastery_map, module.required_competencies)
56
+ pacing_score = pace_alignment(learner, module)
57
+ diversity_penalty = novelty_penalty(learner.recent_topics, module.topic_cluster)
58
+
59
+ final_score =
60
+ 0.35 * content_score +
61
+ 0.30 * collaborative_score +
62
+ 0.20 * readiness_score +
63
+ 0.15 * pacing_score -
64
+ diversity_penalty
65
+
66
+ IF not prerequisites_satisfied(learner, module):
67
+ final_score = -INF
68
+
69
+ RETURN top_k_by_score(candidates)
70
+ ```
71
+
72
+ ## 4. Dynamic Assessment Generation
73
+
74
+ ```text
75
+ FUNCTION generate_unique_assessment(blueprint, learner):
76
+ selected_items = []
77
+
78
+ FOR objective IN blueprint.objectives:
79
+ pool = filter_question_pool(
80
+ objective=objective,
81
+ difficulty=target_difficulty(learner, objective)
82
+ )
83
+
84
+ item = choose_unique_variant(pool, learner.previous_items)
85
+
86
+ IF item.parameterized:
87
+ item = inject_unique_parameters(item, learner.id, now())
88
+
89
+ IF item.ai_generatable:
90
+ item = ai_generate_variant(item.template, blueprint.rubric)
91
+
92
+ selected_items.append(item)
93
+
94
+ RETURN shuffle(selected_items)
95
+ ```
96
+
97
+ ## 5. Open-Ended AI Evaluation
98
+
99
+ ```text
100
+ FUNCTION evaluate_open_response(response, rubric):
101
+ deterministic_checks = run_basic_checks(response, rubric)
102
+ ai_result = ai_score_with_rubric(response.text, rubric)
103
+ final_score = weighted_merge(deterministic_checks, ai_result.score)
104
+
105
+ save_audit_trace(response.id, rubric, ai_result.rationale, ai_result.confidence)
106
+
107
+ IF ai_result.confidence < 0.60:
108
+ queue_human_review(response.id)
109
+
110
+ RETURN final_score
111
+ ```
112
+
113
+ ## 6. Integrity Detection
114
+
115
+ ```text
116
+ FUNCTION integrity_check(attempt):
117
+ plagiarism = compare_to_corpus(attempt.responses)
118
+ peer_similarity = compare_to_peer_attempts(attempt)
119
+ timing_anomaly = detect_impossible_timing(attempt.event_stream)
120
+ session_anomaly = detect_session_irregularity(attempt.session_metadata)
121
+
122
+ risk = weighted_sum(plagiarism, peer_similarity, timing_anomaly, session_anomaly)
123
+
124
+ IF risk >= 0.85:
125
+ RETURN "block"
126
+ IF risk >= 0.60:
127
+ RETURN "review"
128
+ RETURN "pass"
129
+ ```
130
+
131
+ ## 7. Dropout Risk Prediction
132
+
133
+ ```text
134
+ FUNCTION predict_dropout_risk(learner_features):
135
+ risk_probability = dropout_model.predict([
136
+ learner_features.time_on_task_7d,
137
+ learner_features.inactivity_days,
138
+ learner_features.quiz_failure_rate,
139
+ learner_features.pace_slippage,
140
+ learner_features.unfinished_activity_ratio
141
+ ])
142
+
143
+ IF risk_probability > 0.75:
144
+ trigger_high_priority_intervention()
145
+ ELSE IF risk_probability > 0.45:
146
+ trigger_nudge()
147
+
148
+ RETURN risk_probability
149
+ ```
docs/architecture.md ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AIDidact Architecture Blueprint
2
+
3
+ ## 1. Core Components
4
+
5
+ ### Learner profiling system
6
+
7
+ - Collects formal, non-formal, and informal prior learning.
8
+ - Captures short-term and long-term learning goals.
9
+ - Stores educational level and diagnostic readiness.
10
+ - Accepts optional learning preferences such as modality and pacing.
11
+ - Produces a learner profile vector and baseline mastery map.
12
+
13
+ ### Microlearning module system
14
+
15
+ Each 5 ECTS module contains:
16
+
17
+ - Bloom-aligned learning objectives
18
+ - content chunks capped at 10 to 15 minutes
19
+ - interactive activities such as quizzes, reflections, and simulations
20
+ - formative assessments
21
+ - explicit completion criteria
22
+
23
+ ### Recommendation engine
24
+
25
+ - Matches learner profile vectors against module metadata vectors.
26
+ - Combines content similarity with collaborative filtering signals.
27
+ - Re-ranks recommendations based on progress, mastery, pacing, and risk.
28
+
29
+ ### Assessment and integrity
30
+
31
+ - Builds unique question sets per learner from pools and AI-generated variants.
32
+ - Supports scenario-based questions and open-ended tasks.
33
+ - Uses time limits, randomization, plagiarism checks, and anomaly detection.
34
+
35
+ ### Learning analytics
36
+
37
+ - Tracks time-on-task, interaction patterns, assessment performance, and progression paths.
38
+ - Produces learner dashboards, dropout risk, success probability, and adaptive feedback.
39
+
40
+ ## 2. Scalable Architecture
41
+
42
+ ### Frontend
43
+
44
+ - Web SPA for learners and admins.
45
+ - Mobile app with sync support and notifications.
46
+
47
+ ### Backend services
48
+
49
+ - `auth-service`
50
+ - `profile-service`
51
+ - `module-service`
52
+ - `progress-service`
53
+ - `assessment-service`
54
+ - `recommendation-service`
55
+ - `dashboard-service`
56
+ - `notification-service`
57
+
58
+ ### AI and ML services
59
+
60
+ - embedding generation service
61
+ - recommendation ranker
62
+ - AI question generation service
63
+ - AI rubric evaluation service
64
+ - plagiarism and anomaly detection engine
65
+ - predictive analytics models
66
+
67
+ ### Data storage
68
+
69
+ - PostgreSQL for learners, modules, assessments, and progress
70
+ - NoSQL store for high-volume event logs
71
+ - analytics warehouse for dashboards and modeling
72
+ - vector index for learner/module semantic matching
73
+ - object storage for assets, submissions, and artifacts
74
+
75
+ ## 3. Runtime Sequence
76
+
77
+ ### Registration to first recommendation
78
+
79
+ 1. Register learner and capture consent.
80
+ 2. Store learner intake data.
81
+ 3. Run diagnostic assessment.
82
+ 4. Build profile vector and readiness features.
83
+ 5. Rank starter modules.
84
+
85
+ ### Learning progression
86
+
87
+ 1. Learner consumes a content chunk.
88
+ 2. Progress and interaction events are captured.
89
+ 3. Analytics processor updates features.
90
+ 4. Recommendation engine refreshes next-best content or modules.
91
+
92
+ ### Assessment completion
93
+
94
+ 1. Assessment blueprint defines objectives and difficulty targets.
95
+ 2. Service assembles unique items per learner.
96
+ 3. Responses are graded using deterministic logic and AI rubric scoring.
97
+ 4. Integrity engine evaluates plagiarism and anomalies.
98
+ 5. Pass decision requires sufficient score, engagement, and integrity clearance.
99
+
100
+ ## 4. Scalability Notes
101
+
102
+ - Stateless services behind autoscaling groups.
103
+ - Event streaming for telemetry and downstream analytics.
104
+ - Read/write separation to keep analytics off the transactional path.
105
+ - Recommendation candidate generation can be precomputed; final ranking can run on demand.
106
+ - AI-heavy tasks should be asynchronous where possible and fully audited.
107
+
108
+ ## 5. GDPR and Trust
109
+
110
+ - Consent versioning and purpose-bound processing.
111
+ - Pseudonymous analytics identifiers.
112
+ - Right-to-access, deletion, correction, and portability support.
113
+ - Human review for contested grading or high-stakes automated outcomes.
114
+ - Clear model audit logs for recommendations and AI evaluation.
schemas/schema.sql ADDED
@@ -0,0 +1,271 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
2
+
3
+ CREATE TABLE learners (
4
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
5
+ email VARCHAR(255) UNIQUE NOT NULL,
6
+ first_name VARCHAR(100) NOT NULL,
7
+ last_name VARCHAR(100) NOT NULL,
8
+ locale VARCHAR(20) NOT NULL DEFAULT 'en-US',
9
+ timezone VARCHAR(64) NOT NULL,
10
+ educational_level VARCHAR(50) NOT NULL,
11
+ registration_status VARCHAR(30) NOT NULL DEFAULT 'active',
12
+ consent_personalization BOOLEAN NOT NULL DEFAULT FALSE,
13
+ consent_analytics BOOLEAN NOT NULL DEFAULT FALSE,
14
+ consent_version VARCHAR(50),
15
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
16
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
17
+ deleted_at TIMESTAMPTZ
18
+ );
19
+
20
+ CREATE TABLE learner_prior_learning (
21
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
22
+ learner_id UUID NOT NULL REFERENCES learners(id),
23
+ learning_type VARCHAR(20) NOT NULL CHECK (learning_type IN ('formal', 'non_formal', 'informal')),
24
+ title VARCHAR(255) NOT NULL,
25
+ provider VARCHAR(255),
26
+ description TEXT,
27
+ evidence_url TEXT,
28
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
29
+ );
30
+
31
+ CREATE TABLE learner_goals (
32
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
33
+ learner_id UUID NOT NULL REFERENCES learners(id),
34
+ goal_horizon VARCHAR(20) NOT NULL CHECK (goal_horizon IN ('short_term', 'long_term')),
35
+ goal_text TEXT NOT NULL,
36
+ target_date DATE,
37
+ priority SMALLINT NOT NULL DEFAULT 3,
38
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
39
+ );
40
+
41
+ CREATE TABLE learner_preferences (
42
+ learner_id UUID PRIMARY KEY REFERENCES learners(id),
43
+ preferred_modalities JSONB NOT NULL DEFAULT '[]'::jsonb,
44
+ preferred_session_length_minutes SMALLINT,
45
+ accessibility_needs JSONB NOT NULL DEFAULT '{}'::jsonb,
46
+ language_preferences JSONB NOT NULL DEFAULT '[]'::jsonb,
47
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
48
+ );
49
+
50
+ CREATE TABLE learner_profiles (
51
+ learner_id UUID PRIMARY KEY REFERENCES learners(id),
52
+ readiness_level VARCHAR(30) NOT NULL,
53
+ readiness_score NUMERIC(5,2) NOT NULL,
54
+ competency_vector JSONB NOT NULL,
55
+ goal_vector JSONB NOT NULL,
56
+ profile_vector JSONB NOT NULL,
57
+ mastery_map JSONB NOT NULL,
58
+ risk_profile JSONB NOT NULL,
59
+ profile_version INTEGER NOT NULL DEFAULT 1,
60
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
61
+ );
62
+
63
+ CREATE TABLE modules (
64
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
65
+ code VARCHAR(50) UNIQUE NOT NULL,
66
+ title VARCHAR(255) NOT NULL,
67
+ description TEXT NOT NULL,
68
+ ects_credits NUMERIC(4,1) NOT NULL DEFAULT 5.0,
69
+ difficulty_level VARCHAR(30) NOT NULL,
70
+ estimated_total_minutes INTEGER NOT NULL,
71
+ self_paced BOOLEAN NOT NULL DEFAULT TRUE,
72
+ adaptive_enabled BOOLEAN NOT NULL DEFAULT TRUE,
73
+ status VARCHAR(20) NOT NULL DEFAULT 'draft',
74
+ metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
75
+ module_vector JSONB,
76
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
78
+ );
79
+
80
+ CREATE TABLE module_objectives (
81
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
82
+ module_id UUID NOT NULL REFERENCES modules(id),
83
+ objective_text TEXT NOT NULL,
84
+ bloom_level VARCHAR(30) NOT NULL,
85
+ competency_tag VARCHAR(100) NOT NULL,
86
+ measurable_outcome TEXT NOT NULL,
87
+ sort_order SMALLINT NOT NULL
88
+ );
89
+
90
+ CREATE TABLE module_units (
91
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
92
+ module_id UUID NOT NULL REFERENCES modules(id),
93
+ title VARCHAR(255) NOT NULL,
94
+ unit_type VARCHAR(30) NOT NULL,
95
+ chunk_minutes SMALLINT NOT NULL CHECK (chunk_minutes BETWEEN 1 AND 15),
96
+ content_ref TEXT NOT NULL,
97
+ adaptive_rules JSONB NOT NULL DEFAULT '{}'::jsonb,
98
+ sort_order SMALLINT NOT NULL
99
+ );
100
+
101
+ CREATE TABLE module_activities (
102
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
103
+ module_id UUID NOT NULL REFERENCES modules(id),
104
+ unit_id UUID REFERENCES module_units(id),
105
+ activity_type VARCHAR(30) NOT NULL,
106
+ title VARCHAR(255) NOT NULL,
107
+ instructions TEXT NOT NULL,
108
+ scoring_rubric JSONB,
109
+ max_score NUMERIC(6,2),
110
+ required BOOLEAN NOT NULL DEFAULT TRUE,
111
+ sort_order SMALLINT NOT NULL
112
+ );
113
+
114
+ CREATE TABLE module_completion_rules (
115
+ module_id UUID PRIMARY KEY REFERENCES modules(id),
116
+ min_unit_completion_ratio NUMERIC(5,2) NOT NULL,
117
+ min_formative_score NUMERIC(5,2) NOT NULL,
118
+ require_summative_pass BOOLEAN NOT NULL DEFAULT TRUE,
119
+ min_time_on_task_minutes INTEGER,
120
+ integrity_clearance_required BOOLEAN NOT NULL DEFAULT TRUE,
121
+ rules_json JSONB NOT NULL DEFAULT '{}'::jsonb
122
+ );
123
+
124
+ CREATE TABLE module_prerequisites (
125
+ module_id UUID NOT NULL REFERENCES modules(id),
126
+ prerequisite_module_id UUID NOT NULL REFERENCES modules(id),
127
+ PRIMARY KEY (module_id, prerequisite_module_id)
128
+ );
129
+
130
+ CREATE TABLE assessments (
131
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
132
+ module_id UUID NOT NULL REFERENCES modules(id),
133
+ assessment_type VARCHAR(30) NOT NULL CHECK (assessment_type IN ('diagnostic', 'formative', 'summative')),
134
+ title VARCHAR(255) NOT NULL,
135
+ blueprint JSONB NOT NULL,
136
+ time_limit_minutes INTEGER,
137
+ randomization_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
138
+ integrity_policy JSONB NOT NULL DEFAULT '{}'::jsonb,
139
+ active BOOLEAN NOT NULL DEFAULT TRUE,
140
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
141
+ );
142
+
143
+ CREATE TABLE assessment_question_pool (
144
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
145
+ assessment_id UUID NOT NULL REFERENCES assessments(id),
146
+ objective_id UUID REFERENCES module_objectives(id),
147
+ question_type VARCHAR(30) NOT NULL,
148
+ difficulty_band VARCHAR(20) NOT NULL,
149
+ prompt_template TEXT NOT NULL,
150
+ answer_key JSONB,
151
+ rubric JSONB,
152
+ tags JSONB NOT NULL DEFAULT '[]'::jsonb,
153
+ ai_generatable BOOLEAN NOT NULL DEFAULT FALSE,
154
+ active BOOLEAN NOT NULL DEFAULT TRUE,
155
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
156
+ );
157
+
158
+ CREATE TABLE assessment_attempts (
159
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
160
+ assessment_id UUID NOT NULL REFERENCES assessments(id),
161
+ learner_id UUID NOT NULL REFERENCES learners(id),
162
+ started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
163
+ submitted_at TIMESTAMPTZ,
164
+ duration_seconds INTEGER,
165
+ status VARCHAR(30) NOT NULL DEFAULT 'in_progress',
166
+ unique_question_set JSONB NOT NULL,
167
+ score NUMERIC(6,2),
168
+ pass_status VARCHAR(20),
169
+ integrity_status VARCHAR(20) NOT NULL DEFAULT 'pending',
170
+ ai_evaluation_trace JSONB,
171
+ anomaly_flags JSONB NOT NULL DEFAULT '[]'::jsonb
172
+ );
173
+
174
+ CREATE TABLE assessment_responses (
175
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
176
+ attempt_id UUID NOT NULL REFERENCES assessment_attempts(id),
177
+ question_pool_id UUID REFERENCES assessment_question_pool(id),
178
+ prompt_snapshot TEXT NOT NULL,
179
+ response_payload JSONB NOT NULL,
180
+ auto_score NUMERIC(6,2),
181
+ ai_score NUMERIC(6,2),
182
+ final_score NUMERIC(6,2),
183
+ plagiarism_score NUMERIC(6,4),
184
+ feedback TEXT,
185
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
186
+ );
187
+
188
+ CREATE TABLE enrollments (
189
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
190
+ learner_id UUID NOT NULL REFERENCES learners(id),
191
+ module_id UUID NOT NULL REFERENCES modules(id),
192
+ enrollment_status VARCHAR(30) NOT NULL DEFAULT 'active',
193
+ recommended_by VARCHAR(30) NOT NULL DEFAULT 'system',
194
+ started_at TIMESTAMPTZ,
195
+ completed_at TIMESTAMPTZ,
196
+ mastery_score NUMERIC(6,2),
197
+ UNIQUE (learner_id, module_id)
198
+ );
199
+
200
+ CREATE TABLE learner_progress (
201
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
202
+ learner_id UUID NOT NULL REFERENCES learners(id),
203
+ module_id UUID NOT NULL REFERENCES modules(id),
204
+ unit_id UUID REFERENCES module_units(id),
205
+ activity_id UUID REFERENCES module_activities(id),
206
+ progress_state VARCHAR(30) NOT NULL,
207
+ attempts_count INTEGER NOT NULL DEFAULT 0,
208
+ best_score NUMERIC(6,2),
209
+ time_on_task_seconds INTEGER NOT NULL DEFAULT 0,
210
+ last_interaction_at TIMESTAMPTZ,
211
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
212
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
213
+ );
214
+
215
+ CREATE TABLE interactions (
216
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
217
+ learner_id UUID NOT NULL REFERENCES learners(id),
218
+ module_id UUID REFERENCES modules(id),
219
+ unit_id UUID REFERENCES module_units(id),
220
+ activity_id UUID REFERENCES module_activities(id),
221
+ session_id UUID,
222
+ event_type VARCHAR(50) NOT NULL,
223
+ event_timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(),
224
+ duration_ms INTEGER,
225
+ payload JSONB NOT NULL DEFAULT '{}'::jsonb,
226
+ client_context JSONB NOT NULL DEFAULT '{}'::jsonb
227
+ );
228
+
229
+ CREATE TABLE recommendation_impressions (
230
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
231
+ learner_id UUID NOT NULL REFERENCES learners(id),
232
+ module_id UUID NOT NULL REFERENCES modules(id),
233
+ rank_position INTEGER NOT NULL,
234
+ recommendation_reason JSONB NOT NULL DEFAULT '{}'::jsonb,
235
+ model_version VARCHAR(50) NOT NULL,
236
+ accepted BOOLEAN,
237
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
238
+ );
239
+
240
+ CREATE TABLE analytics_daily_learner_metrics (
241
+ learner_id UUID NOT NULL REFERENCES learners(id),
242
+ metric_date DATE NOT NULL,
243
+ time_on_task_minutes NUMERIC(8,2) NOT NULL DEFAULT 0,
244
+ completed_units INTEGER NOT NULL DEFAULT 0,
245
+ formative_avg_score NUMERIC(6,2),
246
+ summative_pass_count INTEGER NOT NULL DEFAULT 0,
247
+ inactivity_days INTEGER NOT NULL DEFAULT 0,
248
+ dropout_risk NUMERIC(6,4),
249
+ success_probability NUMERIC(6,4),
250
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
251
+ PRIMARY KEY (learner_id, metric_date)
252
+ );
253
+
254
+ CREATE TABLE analytics_model_audit_logs (
255
+ id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
256
+ learner_id UUID REFERENCES learners(id),
257
+ related_entity_type VARCHAR(50) NOT NULL,
258
+ related_entity_id UUID,
259
+ model_name VARCHAR(100) NOT NULL,
260
+ model_version VARCHAR(50) NOT NULL,
261
+ input_summary JSONB NOT NULL,
262
+ output_summary JSONB NOT NULL,
263
+ explanation TEXT,
264
+ human_review_required BOOLEAN NOT NULL DEFAULT FALSE,
265
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
266
+ );
267
+
268
+ CREATE INDEX idx_interactions_learner_time ON interactions (learner_id, event_timestamp DESC);
269
+ CREATE INDEX idx_progress_learner_module ON learner_progress (learner_id, module_id);
270
+ CREATE INDEX idx_attempts_learner_assessment ON assessment_attempts (learner_id, assessment_id);
271
+ CREATE INDEX idx_recommendation_impressions_learner ON recommendation_impressions (learner_id, created_at DESC);