ShinyRO commited on
Commit
b0e08b2
Β·
verified Β·
1 Parent(s): a5103f7

init: upload required file for inference

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ final/causal_cross_transformer_dkt_model/causal_cross_transformer_dkt_model.keras filter=lfs diff=lfs merge=lfs -text
37
+ final/lstm_dkt_model_train/lstm_dkt_model.keras filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ==============================================================================
2
+ # Production Dockerfile for EduPredict DKT Inference Engine
3
+ # ==============================================================================
4
+ # Uses python-slim for a lightweight container footprint.
5
+ # Recommends tensorflow-cpu for standard servers to avoid GPU CUDA package overhead.
6
+
7
+ FROM python:3.11-slim
8
+
9
+ # Set environment variables
10
+ ENV PYTHONDONTWRITEBYTECODE=1
11
+ ENV PYTHONUNBUFFERED=1
12
+ ENV PORT=8000
13
+
14
+ WORKDIR /app
15
+
16
+ # Install system utilities needed for building packages
17
+ RUN apt-get update && apt-get install -y --no-install-recommends \
18
+ build-essential \
19
+ && rm -rf /var/lib/apt/lists/*
20
+
21
+ # Copy requirements and install dependencies
22
+ # We use tensorflow-cpu for highly optimized, lightweight server deployment
23
+ RUN pip install --no-cache-dir --upgrade pip && \
24
+ pip install --no-cache-dir \
25
+ fastapi \
26
+ uvicorn \
27
+ pydantic \
28
+ numpy \
29
+ tensorflow-cpu \
30
+ google-generativeai
31
+
32
+ # Copy application files
33
+ COPY inference_api.py .
34
+ COPY top_category.json .
35
+
36
+ # Copy model artifacts
37
+ COPY final/ final/
38
+
39
+ # Expose port (Hugging Face requires port 7860)
40
+ EXPOSE 7860
41
+
42
+ # Start application via Uvicorn on port 7860
43
+ CMD ["uvicorn", "inference_api:app", "--host", "0.0.0.0", "--port", "7860"]
final/causal_cross_transformer_dkt_model/causal_cross_transformer_dkt_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:deded1366991248df7f833161dbad9820734f175c07e8accf9d0b158ce4ac11a
3
+ size 556217
final/causal_cross_transformer_dkt_model/vocab.json ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab": {
3
+ "1": 1,
4
+ "2": 2,
5
+ "4": 3,
6
+ "5": 4,
7
+ "8": 5,
8
+ "9": 6,
9
+ "10": 7,
10
+ "11": 8,
11
+ "12": 9,
12
+ "13": 10,
13
+ "14": 11,
14
+ "15": 12,
15
+ "16": 13,
16
+ "17": 14,
17
+ "18": 15,
18
+ "21": 16,
19
+ "22": 17,
20
+ "24": 18,
21
+ "25": 19,
22
+ "26": 20,
23
+ "27": 21,
24
+ "32": 22,
25
+ "34": 23,
26
+ "35": 24,
27
+ "37": 25,
28
+ "39": 26,
29
+ "40": 27,
30
+ "42": 28,
31
+ "46": 29,
32
+ "47": 30,
33
+ "48": 31,
34
+ "49": 32,
35
+ "50": 33,
36
+ "51": 34,
37
+ "53": 35,
38
+ "54": 36,
39
+ "58": 37,
40
+ "61": 38,
41
+ "63": 39,
42
+ "64": 40,
43
+ "65": 41,
44
+ "67": 42,
45
+ "70": 43,
46
+ "74": 44,
47
+ "75": 45,
48
+ "77": 46,
49
+ "79": 47,
50
+ "80": 48,
51
+ "81": 49,
52
+ "82": 50,
53
+ "83": 51,
54
+ "84": 52,
55
+ "85": 53,
56
+ "86": 54,
57
+ "92": 55,
58
+ "110": 56,
59
+ "163": 57,
60
+ "166": 58,
61
+ "276": 59,
62
+ "277": 60,
63
+ "278": 61,
64
+ "279": 62,
65
+ "280": 63,
66
+ "290": 64,
67
+ "292": 65,
68
+ "293": 66,
69
+ "294": 67,
70
+ "296": 68,
71
+ "297": 69,
72
+ "298": 70,
73
+ "299": 71,
74
+ "301": 72,
75
+ "303": 73,
76
+ "307": 74,
77
+ "308": 75,
78
+ "309": 76,
79
+ "310": 77,
80
+ "311": 78,
81
+ "312": 79,
82
+ "314": 80,
83
+ "317": 81,
84
+ "322": 82,
85
+ "323": 83,
86
+ "325": 84,
87
+ "346": 85,
88
+ "350": 86,
89
+ "368": 87,
90
+ "375": 88,
91
+ "378": 89
92
+ },
93
+ "vocab_size": 90,
94
+ "idx_to_name": {
95
+ "0": "Misc/Other",
96
+ "1": "Box and Whisker",
97
+ "2": "Circle Graph",
98
+ "3": "Histogram as Table or Graph",
99
+ "4": "Number Line",
100
+ "5": "Scatter Plot",
101
+ "6": "Stem and Leaf Plot",
102
+ "7": "Table",
103
+ "8": "Venn Diagram",
104
+ "9": "Mean",
105
+ "10": "Median",
106
+ "11": "Mode",
107
+ "12": "Range",
108
+ "13": "Counting Methods",
109
+ "14": "Probability of Two Distinct Events",
110
+ "15": "Probability of a Single Event",
111
+ "16": "Interior Angles Figures with More than 3 Sides",
112
+ "17": "Interior Angles Triangle",
113
+ "18": "Congruence",
114
+ "19": "Complementary and Supplementary Angles",
115
+ "20": "Angles on Parallel Lines Cut by a Transversal",
116
+ "21": "Pythagorean Theorem",
117
+ "22": "Nets of 3D Figures",
118
+ "23": "Unit Conversion Within a System",
119
+ "24": "Effect of Changing Dimensions of a Shape Prportionally",
120
+ "25": "Skill_37",
121
+ "26": "Area Circle",
122
+ "27": "Circumference ",
123
+ "28": "Perimeter of a Polygon",
124
+ "29": "Calculations with Similar Figures",
125
+ "30": "Conversion of Fraction Decimals Percents",
126
+ "31": "Equivalent Fractions",
127
+ "32": "Ordering Positive Decimals",
128
+ "33": "Ordering Fractions",
129
+ "34": "Ordering Integers",
130
+ "35": "Ordering Real Numbers",
131
+ "36": "Rounding",
132
+ "37": "Addition Whole Numbers",
133
+ "38": "Division Fractions",
134
+ "39": "Estimation",
135
+ "40": "Fraction Of",
136
+ "41": "Least Common Multiple",
137
+ "42": "Multiplication Fractions",
138
+ "43": "Percent Of",
139
+ "44": "Subtraction Whole Numbers",
140
+ "45": "Square Root",
141
+ "46": "Finding Percents",
142
+ "47": "Proportion",
143
+ "48": "Scale Factor",
144
+ "49": "Unit Rate",
145
+ "50": "Scientific Notation",
146
+ "51": "Divisibility Rules",
147
+ "52": "Prime Number",
148
+ "53": "Absolute Value",
149
+ "54": "Exponents",
150
+ "55": "Pattern Finding ",
151
+ "56": "D.4.8-understanding-concept-of-probabilities",
152
+ "57": "Absolute Value",
153
+ "58": "Algebraic Solving",
154
+ "59": "Multiplication and Division Positive Decimals",
155
+ "60": "Addition and Subtraction Integers",
156
+ "61": "Addition and Subtraction Positive Decimals",
157
+ "62": "Multiplication and Division Integers",
158
+ "63": "Addition and Subtraction Fractions",
159
+ "64": "Reflection",
160
+ "65": "Rotations",
161
+ "66": "Translations",
162
+ "67": "Area Irregular Figure",
163
+ "68": "Area Rectangle",
164
+ "69": "Area Trapezoid",
165
+ "70": "Area Triangle",
166
+ "71": "Surface Area Cylinder",
167
+ "72": "Surface Area Rectangular Prism",
168
+ "73": "Volume Cylinder",
169
+ "74": "Volume Rectangular Prism",
170
+ "75": "Volume Sphere",
171
+ "76": "Order of Operations +,-,/,* () positive reals",
172
+ "77": "Order of Operations All",
173
+ "78": "Equation Solving Two or Fewer Steps",
174
+ "79": "Equation Solving More Than Two Steps",
175
+ "80": "Angles - Obtuse, Acute, and Right",
176
+ "81": "Greatest Common Factor",
177
+ "82": "Write Linear Equation from Ordered Pairs",
178
+ "83": "Write Linear Equation from Situation",
179
+ "84": "Write Linear Equation from Graph",
180
+ "85": "Polynomial Factors",
181
+ "86": "Solving Systems of Linear Equations",
182
+ "87": "Solving for a variable",
183
+ "88": "Solving Inequalities",
184
+ "89": "Solving Systems of Linear Equations by Graphing"
185
+ },
186
+ "skill_id_to_name": {
187
+ "10": "Table",
188
+ "12": "Mean",
189
+ "13": "Median",
190
+ "279": "Multiplication and Division Integers",
191
+ "311": "Equation Solving Two or Fewer Steps",
192
+ "312": "Equation Solving More Than Two Steps",
193
+ "278": "Addition and Subtraction Positive Decimals",
194
+ "86": "Exponents",
195
+ "5": "Number Line",
196
+ "375": "Solving Inequalities",
197
+ "340": "Distributive Property",
198
+ "277": "Addition and Subtraction Integers",
199
+ "24": "Congruence",
200
+ "54": "Rounding",
201
+ "67": "Multiplication Fractions",
202
+ "280": "Addition and Subtraction Fractions",
203
+ "58": "Addition Whole Numbers",
204
+ "51": "Ordering Integers",
205
+ "49": "Ordering Positive Decimals",
206
+ "309": "Order of Operations +,-,/,* () positive reals",
207
+ "368": "Solving for a variable",
208
+ "301": "Surface Area Rectangular Prism",
209
+ "32": "Nets of 3D Figures",
210
+ "46": "Calculations with Similar Figures",
211
+ "18": "Probability of a Single Event",
212
+ "64": "Fraction Of",
213
+ "15": "Range",
214
+ "14": "Mode",
215
+ "47": "Conversion of Fraction Decimals Percents",
216
+ "74": "Subtraction Whole Numbers",
217
+ "85": "Absolute Value",
218
+ "50": "Ordering Fractions",
219
+ "79": "Proportion",
220
+ "110": "D.4.8-understanding-concept-of-probabilities",
221
+ "25": "Complementary and Supplementary Angles",
222
+ "40": "Circumference ",
223
+ "39": "Area Circle",
224
+ "34": "Unit Conversion Within a System",
225
+ "81": "Unit Rate",
226
+ "69": "Multiplication Whole Numbers",
227
+ "70": "Percent Of",
228
+ "276": "Multiplication and Division Positive Decimals",
229
+ "61": "Division Fractions",
230
+ "83": "Divisibility Rules",
231
+ "65": "Least Common Multiple",
232
+ "53": "Ordering Real Numbers",
233
+ "75": "Square Root",
234
+ "310": "Order of Operations All",
235
+ "84": "Prime Number",
236
+ "77": "Finding Percents",
237
+ "63": "Estimation",
238
+ "82": "Scientific Notation",
239
+ "27": "Pythagorean Theorem",
240
+ "343": "Midpoint",
241
+ "8": "Scatter Plot",
242
+ "92": "Pattern Finding ",
243
+ "11": "Venn Diagram",
244
+ "2": "Circle Graph",
245
+ "48": "Equivalent Fractions",
246
+ "9": "Stem and Leaf Plot",
247
+ "104": "Skill_104",
248
+ "4": "Histogram as Table or Graph",
249
+ "1": "Box and Whisker",
250
+ "322": "Write Linear Equation from Ordered Pairs",
251
+ "325": "Write Linear Equation from Graph",
252
+ "323": "Write Linear Equation from Situation",
253
+ "292": "Rotations",
254
+ "290": "Reflection",
255
+ "37": "Skill_37",
256
+ "42": "Perimeter of a Polygon",
257
+ "80": "Scale Factor",
258
+ "293": "Translations",
259
+ "21": "Interior Angles Figures with More than 3 Sides",
260
+ "22": "Interior Angles Triangle",
261
+ "16": "Counting Methods",
262
+ "17": "Probability of Two Distinct Events",
263
+ "26": "Angles on Parallel Lines Cut by a Transversal",
264
+ "294": "Area Irregular Figure",
265
+ "307": "Volume Rectangular Prism",
266
+ "365": "Interpreting Coordinate Graphs ",
267
+ "297": "Area Trapezoid",
268
+ "296": "Area Rectangle",
269
+ "295": "Area Parallelogram",
270
+ "299": "Surface Area Cylinder",
271
+ "308": "Volume Sphere",
272
+ "298": "Area Triangle",
273
+ "321": "Computation with Real Numbers",
274
+ "173": "Choose an Equation from Given Information",
275
+ "190": "Intercept",
276
+ "193": "Linear Equations",
277
+ "221": "Slope",
278
+ "303": "Volume Cylinder",
279
+ "35": "Effect of Changing Dimensions of a Shape Prportionally",
280
+ "356": "Quadratic Formula to Solve Quadratic Equation",
281
+ "317": "Greatest Common Factor",
282
+ "324": "Recognize Linear Pattern",
283
+ "314": "Angles - Obtuse, Acute, and Right",
284
+ "102": "Skill_102",
285
+ "166": "Algebraic Solving",
286
+ "163": "Absolute Value",
287
+ "204": "Percents",
288
+ "217": "Rate",
289
+ "203": "Percent Discount",
290
+ "165": "Algebraic Simplification",
291
+ "43": "Reading a Ruler or Scale",
292
+ "378": "Solving Systems of Linear Equations by Graphing",
293
+ "350": "Solving Systems of Linear Equations",
294
+ "346": "Polynomial Factors",
295
+ "371": "Simplifying Expressions positive exponents",
296
+ "334": "Finding Slope from Ordered Pairs",
297
+ "331": "Finding Slope From Situation",
298
+ "362": "Parts of a Polyomial, Terms, Coefficient, Monomial, Exponent, Variable",
299
+ "333": "Finding Slope From Equation",
300
+ "348": "Recognize Quadratic Pattern"
301
+ }
302
+ }
final/lstm_dkt_model_train/lstm_dkt_model.keras ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bf83856125026d9d9250c9f78d9e8a6e52cf0e6da1225257642985370b4a5db9
3
+ size 447878
final/lstm_dkt_model_train/vocab.json ADDED
@@ -0,0 +1,302 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "vocab": {
3
+ "1": 1,
4
+ "2": 2,
5
+ "4": 3,
6
+ "5": 4,
7
+ "8": 5,
8
+ "9": 6,
9
+ "10": 7,
10
+ "11": 8,
11
+ "12": 9,
12
+ "13": 10,
13
+ "14": 11,
14
+ "15": 12,
15
+ "16": 13,
16
+ "17": 14,
17
+ "18": 15,
18
+ "21": 16,
19
+ "22": 17,
20
+ "24": 18,
21
+ "25": 19,
22
+ "26": 20,
23
+ "27": 21,
24
+ "32": 22,
25
+ "34": 23,
26
+ "35": 24,
27
+ "37": 25,
28
+ "39": 26,
29
+ "40": 27,
30
+ "42": 28,
31
+ "46": 29,
32
+ "47": 30,
33
+ "48": 31,
34
+ "49": 32,
35
+ "50": 33,
36
+ "51": 34,
37
+ "53": 35,
38
+ "54": 36,
39
+ "58": 37,
40
+ "61": 38,
41
+ "63": 39,
42
+ "64": 40,
43
+ "65": 41,
44
+ "67": 42,
45
+ "70": 43,
46
+ "74": 44,
47
+ "75": 45,
48
+ "77": 46,
49
+ "79": 47,
50
+ "80": 48,
51
+ "81": 49,
52
+ "82": 50,
53
+ "83": 51,
54
+ "84": 52,
55
+ "85": 53,
56
+ "86": 54,
57
+ "92": 55,
58
+ "110": 56,
59
+ "163": 57,
60
+ "166": 58,
61
+ "276": 59,
62
+ "277": 60,
63
+ "278": 61,
64
+ "279": 62,
65
+ "280": 63,
66
+ "290": 64,
67
+ "292": 65,
68
+ "293": 66,
69
+ "294": 67,
70
+ "296": 68,
71
+ "297": 69,
72
+ "298": 70,
73
+ "299": 71,
74
+ "301": 72,
75
+ "303": 73,
76
+ "307": 74,
77
+ "308": 75,
78
+ "309": 76,
79
+ "310": 77,
80
+ "311": 78,
81
+ "312": 79,
82
+ "314": 80,
83
+ "317": 81,
84
+ "322": 82,
85
+ "323": 83,
86
+ "325": 84,
87
+ "346": 85,
88
+ "350": 86,
89
+ "368": 87,
90
+ "375": 88,
91
+ "378": 89
92
+ },
93
+ "vocab_size": 90,
94
+ "idx_to_name": {
95
+ "0": "Misc/Other",
96
+ "1": "Box and Whisker",
97
+ "2": "Circle Graph",
98
+ "3": "Histogram as Table or Graph",
99
+ "4": "Number Line",
100
+ "5": "Scatter Plot",
101
+ "6": "Stem and Leaf Plot",
102
+ "7": "Table",
103
+ "8": "Venn Diagram",
104
+ "9": "Mean",
105
+ "10": "Median",
106
+ "11": "Mode",
107
+ "12": "Range",
108
+ "13": "Counting Methods",
109
+ "14": "Probability of Two Distinct Events",
110
+ "15": "Probability of a Single Event",
111
+ "16": "Interior Angles Figures with More than 3 Sides",
112
+ "17": "Interior Angles Triangle",
113
+ "18": "Congruence",
114
+ "19": "Complementary and Supplementary Angles",
115
+ "20": "Angles on Parallel Lines Cut by a Transversal",
116
+ "21": "Pythagorean Theorem",
117
+ "22": "Nets of 3D Figures",
118
+ "23": "Unit Conversion Within a System",
119
+ "24": "Effect of Changing Dimensions of a Shape Prportionally",
120
+ "25": "Skill_37",
121
+ "26": "Area Circle",
122
+ "27": "Circumference ",
123
+ "28": "Perimeter of a Polygon",
124
+ "29": "Calculations with Similar Figures",
125
+ "30": "Conversion of Fraction Decimals Percents",
126
+ "31": "Equivalent Fractions",
127
+ "32": "Ordering Positive Decimals",
128
+ "33": "Ordering Fractions",
129
+ "34": "Ordering Integers",
130
+ "35": "Ordering Real Numbers",
131
+ "36": "Rounding",
132
+ "37": "Addition Whole Numbers",
133
+ "38": "Division Fractions",
134
+ "39": "Estimation",
135
+ "40": "Fraction Of",
136
+ "41": "Least Common Multiple",
137
+ "42": "Multiplication Fractions",
138
+ "43": "Percent Of",
139
+ "44": "Subtraction Whole Numbers",
140
+ "45": "Square Root",
141
+ "46": "Finding Percents",
142
+ "47": "Proportion",
143
+ "48": "Scale Factor",
144
+ "49": "Unit Rate",
145
+ "50": "Scientific Notation",
146
+ "51": "Divisibility Rules",
147
+ "52": "Prime Number",
148
+ "53": "Absolute Value",
149
+ "54": "Exponents",
150
+ "55": "Pattern Finding ",
151
+ "56": "D.4.8-understanding-concept-of-probabilities",
152
+ "57": "Absolute Value",
153
+ "58": "Algebraic Solving",
154
+ "59": "Multiplication and Division Positive Decimals",
155
+ "60": "Addition and Subtraction Integers",
156
+ "61": "Addition and Subtraction Positive Decimals",
157
+ "62": "Multiplication and Division Integers",
158
+ "63": "Addition and Subtraction Fractions",
159
+ "64": "Reflection",
160
+ "65": "Rotations",
161
+ "66": "Translations",
162
+ "67": "Area Irregular Figure",
163
+ "68": "Area Rectangle",
164
+ "69": "Area Trapezoid",
165
+ "70": "Area Triangle",
166
+ "71": "Surface Area Cylinder",
167
+ "72": "Surface Area Rectangular Prism",
168
+ "73": "Volume Cylinder",
169
+ "74": "Volume Rectangular Prism",
170
+ "75": "Volume Sphere",
171
+ "76": "Order of Operations +,-,/,* () positive reals",
172
+ "77": "Order of Operations All",
173
+ "78": "Equation Solving Two or Fewer Steps",
174
+ "79": "Equation Solving More Than Two Steps",
175
+ "80": "Angles - Obtuse, Acute, and Right",
176
+ "81": "Greatest Common Factor",
177
+ "82": "Write Linear Equation from Ordered Pairs",
178
+ "83": "Write Linear Equation from Situation",
179
+ "84": "Write Linear Equation from Graph",
180
+ "85": "Polynomial Factors",
181
+ "86": "Solving Systems of Linear Equations",
182
+ "87": "Solving for a variable",
183
+ "88": "Solving Inequalities",
184
+ "89": "Solving Systems of Linear Equations by Graphing"
185
+ },
186
+ "skill_id_to_name": {
187
+ "10": "Table",
188
+ "12": "Mean",
189
+ "13": "Median",
190
+ "279": "Multiplication and Division Integers",
191
+ "311": "Equation Solving Two or Fewer Steps",
192
+ "312": "Equation Solving More Than Two Steps",
193
+ "278": "Addition and Subtraction Positive Decimals",
194
+ "86": "Exponents",
195
+ "5": "Number Line",
196
+ "375": "Solving Inequalities",
197
+ "340": "Distributive Property",
198
+ "277": "Addition and Subtraction Integers",
199
+ "24": "Congruence",
200
+ "54": "Rounding",
201
+ "67": "Multiplication Fractions",
202
+ "280": "Addition and Subtraction Fractions",
203
+ "58": "Addition Whole Numbers",
204
+ "51": "Ordering Integers",
205
+ "49": "Ordering Positive Decimals",
206
+ "309": "Order of Operations +,-,/,* () positive reals",
207
+ "368": "Solving for a variable",
208
+ "301": "Surface Area Rectangular Prism",
209
+ "32": "Nets of 3D Figures",
210
+ "46": "Calculations with Similar Figures",
211
+ "18": "Probability of a Single Event",
212
+ "64": "Fraction Of",
213
+ "15": "Range",
214
+ "14": "Mode",
215
+ "47": "Conversion of Fraction Decimals Percents",
216
+ "74": "Subtraction Whole Numbers",
217
+ "85": "Absolute Value",
218
+ "50": "Ordering Fractions",
219
+ "79": "Proportion",
220
+ "110": "D.4.8-understanding-concept-of-probabilities",
221
+ "25": "Complementary and Supplementary Angles",
222
+ "40": "Circumference ",
223
+ "39": "Area Circle",
224
+ "34": "Unit Conversion Within a System",
225
+ "81": "Unit Rate",
226
+ "69": "Multiplication Whole Numbers",
227
+ "70": "Percent Of",
228
+ "276": "Multiplication and Division Positive Decimals",
229
+ "61": "Division Fractions",
230
+ "83": "Divisibility Rules",
231
+ "65": "Least Common Multiple",
232
+ "53": "Ordering Real Numbers",
233
+ "75": "Square Root",
234
+ "310": "Order of Operations All",
235
+ "84": "Prime Number",
236
+ "77": "Finding Percents",
237
+ "63": "Estimation",
238
+ "82": "Scientific Notation",
239
+ "27": "Pythagorean Theorem",
240
+ "343": "Midpoint",
241
+ "8": "Scatter Plot",
242
+ "92": "Pattern Finding ",
243
+ "11": "Venn Diagram",
244
+ "2": "Circle Graph",
245
+ "48": "Equivalent Fractions",
246
+ "9": "Stem and Leaf Plot",
247
+ "104": "Skill_104",
248
+ "4": "Histogram as Table or Graph",
249
+ "1": "Box and Whisker",
250
+ "322": "Write Linear Equation from Ordered Pairs",
251
+ "325": "Write Linear Equation from Graph",
252
+ "323": "Write Linear Equation from Situation",
253
+ "292": "Rotations",
254
+ "290": "Reflection",
255
+ "37": "Skill_37",
256
+ "42": "Perimeter of a Polygon",
257
+ "80": "Scale Factor",
258
+ "293": "Translations",
259
+ "21": "Interior Angles Figures with More than 3 Sides",
260
+ "22": "Interior Angles Triangle",
261
+ "16": "Counting Methods",
262
+ "17": "Probability of Two Distinct Events",
263
+ "26": "Angles on Parallel Lines Cut by a Transversal",
264
+ "294": "Area Irregular Figure",
265
+ "307": "Volume Rectangular Prism",
266
+ "365": "Interpreting Coordinate Graphs ",
267
+ "297": "Area Trapezoid",
268
+ "296": "Area Rectangle",
269
+ "295": "Area Parallelogram",
270
+ "299": "Surface Area Cylinder",
271
+ "308": "Volume Sphere",
272
+ "298": "Area Triangle",
273
+ "321": "Computation with Real Numbers",
274
+ "173": "Choose an Equation from Given Information",
275
+ "190": "Intercept",
276
+ "193": "Linear Equations",
277
+ "221": "Slope",
278
+ "303": "Volume Cylinder",
279
+ "35": "Effect of Changing Dimensions of a Shape Prportionally",
280
+ "356": "Quadratic Formula to Solve Quadratic Equation",
281
+ "317": "Greatest Common Factor",
282
+ "324": "Recognize Linear Pattern",
283
+ "314": "Angles - Obtuse, Acute, and Right",
284
+ "102": "Skill_102",
285
+ "166": "Algebraic Solving",
286
+ "163": "Absolute Value",
287
+ "204": "Percents",
288
+ "217": "Rate",
289
+ "203": "Percent Discount",
290
+ "165": "Algebraic Simplification",
291
+ "43": "Reading a Ruler or Scale",
292
+ "378": "Solving Systems of Linear Equations by Graphing",
293
+ "350": "Solving Systems of Linear Equations",
294
+ "346": "Polynomial Factors",
295
+ "371": "Simplifying Expressions positive exponents",
296
+ "334": "Finding Slope from Ordered Pairs",
297
+ "331": "Finding Slope From Situation",
298
+ "362": "Parts of a Polyomial, Terms, Coefficient, Monomial, Exponent, Variable",
299
+ "333": "Finding Slope From Equation",
300
+ "348": "Recognize Quadratic Pattern"
301
+ }
302
+ }
inference_api.py ADDED
@@ -0,0 +1,726 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ EduPredict Math β€” Unified Inference Engine & REST API
3
+ ======================================================
4
+ Provides a unified API to switch between:
5
+ 1. LSTM (Multivariate Response Time DKT)
6
+ 2. Causal Cross-Transformer (SAINT/SAKT style DKT)
7
+
8
+ Handles input student interaction sequence, processes it according to the selected model,
9
+ aggregates mastery scores by top categories, and triggers personalized Generative AI
10
+ hints using Google Gemini based on student preference.
11
+ """
12
+
13
+ import os
14
+ import json
15
+ import math
16
+ import logging
17
+ from typing import Optional, List, Dict, Any
18
+
19
+ import numpy as np
20
+ import tensorflow as tf
21
+ from fastapi import FastAPI, HTTPException
22
+ from pydantic import BaseModel, Field
23
+
24
+ # ──────────────────────────────────────────────────────────────
25
+ # Logging Configuration
26
+ # ──────────────────────────────────────────────────────────────
27
+ logging.basicConfig(
28
+ level=logging.INFO,
29
+ format="%(asctime)s [%(levelname)s] %(message)s",
30
+ handlers=[logging.StreamHandler()]
31
+ )
32
+ logger = logging.getLogger("DKTInferenceEngine")
33
+
34
+ # ──────────────────────────────────────────────────────────────
35
+ # Global Configuration
36
+ # ──────────────────────────────────────────────────────────────
37
+ # Easy Switch Model Choice: 'LSTM' or 'Transformer'
38
+ MODEL_TYPE = "Transformer"
39
+
40
+ # Thresholds
41
+ STRUGGLE_THRESHOLD = 0.50 # Skills below this are flagged as struggling
42
+ LENGTH_THRESHOLD = 3 # strictly greater than 3 to trigger GenAI
43
+ AVG_LEN_THRESHOLD = 1 # Min interactions for skill to be included in top category avg
44
+
45
+ # Model directories and path mappings
46
+ if MODEL_TYPE == "LSTM":
47
+ MODEL_PATH = "final/lstm_dkt_model_train/lstm_dkt_model.keras"
48
+ VOCAB_PATH = "final/lstm_dkt_model_train/vocab.json"
49
+ MAX_SEQ_LEN = 100
50
+ elif MODEL_TYPE == "Transformer":
51
+ MODEL_PATH = "final/causal_cross_transformer_dkt_model/causal_cross_transformer_dkt_model.keras"
52
+ VOCAB_PATH = "final/causal_cross_transformer_dkt_model/vocab.json"
53
+ MAX_SEQ_LEN = 100
54
+ else:
55
+ raise ValueError(f"Unknown MODEL_TYPE: {MODEL_TYPE}")
56
+
57
+ TOP_CATEGORY_PATH = "top_category.json"
58
+ GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
59
+
60
+ # ──────────────────────────────────────────────────────────────
61
+ # Keras Custom Layers Definition
62
+ # ──────────────────────────────────────────────────────────────
63
+
64
+ class TemporalAttentionLayer(tf.keras.layers.Layer):
65
+ """LSTM-specific custom temporal attention layer."""
66
+ def __init__(self, units: int, **kwargs):
67
+ super().__init__(**kwargs)
68
+ self.units = units
69
+
70
+ def build(self, input_shape):
71
+ self.W_query = tf.keras.layers.Dense(self.units, name="attn_query")
72
+ self.W_key = tf.keras.layers.Dense(self.units, name="attn_key")
73
+ self.W_value = tf.keras.layers.Dense(self.units, name="attn_value")
74
+ super().build(input_shape)
75
+
76
+ def call(self, lstm_outputs, mask=None):
77
+ Q = self.W_query(lstm_outputs)
78
+ K = self.W_key(lstm_outputs)
79
+ V = self.W_value(lstm_outputs)
80
+
81
+ d_k = tf.cast(tf.shape(K)[-1], tf.float32)
82
+ scores = tf.matmul(Q, K, transpose_b=True) / tf.sqrt(d_k)
83
+
84
+ seq_len = tf.shape(scores)[1]
85
+ causal = tf.linalg.band_part(tf.ones((seq_len, seq_len), dtype=tf.float32), -1, 0)
86
+ scores = scores + (1.0 - causal) * (-1e9)
87
+
88
+ if mask is not None:
89
+ pad_mask = tf.cast(mask[:, tf.newaxis, :], tf.float32)
90
+ scores = scores + (1.0 - pad_mask) * (-1e9)
91
+
92
+ weights = tf.nn.softmax(scores, axis=-1)
93
+ return tf.matmul(weights, V)
94
+
95
+ def compute_mask(self, inputs, mask=None):
96
+ return None
97
+
98
+ def get_config(self):
99
+ config = super().get_config()
100
+ config.update({"units": self.units})
101
+ return config
102
+
103
+
104
+ class MultivariateResponseTimeDKT(tf.keras.Model):
105
+ """LSTM DKT Model wrapper."""
106
+ def __init__(self, vocab_size: int, embed_dim: int = 64, lstm_units: int = 64,
107
+ attention_units: int = 64, dropout_rate: float = 0.4, l2_reg: float = 1e-4, **kwargs):
108
+ super().__init__(**kwargs)
109
+ self.vocab_size = vocab_size
110
+ self.proj_inputs = tf.keras.layers.Dense(embed_dim, activation="relu", kernel_regularizer=tf.keras.regularizers.l2(l2_reg))
111
+ self.proj_hist = tf.keras.layers.Dense(embed_dim, activation="relu", kernel_regularizer=tf.keras.regularizers.l2(l2_reg))
112
+ self.spatial_dropout = tf.keras.layers.SpatialDropout1D(dropout_rate)
113
+ self.lstm = tf.keras.layers.LSTM(lstm_units, return_sequences=True, use_cudnn=False, kernel_regularizer=tf.keras.regularizers.l2(l2_reg), name="lstm")
114
+ self.attention = TemporalAttentionLayer(attention_units)
115
+ self.layer_norm = tf.keras.layers.LayerNormalization()
116
+ self.dropout_out = tf.keras.layers.Dropout(dropout_rate)
117
+ self.output_dense = tf.keras.layers.Dense(vocab_size, activation="sigmoid", name="skill_output")
118
+
119
+ def call(self, inputs, training=False):
120
+ mask = tf.reduce_any(tf.not_equal(inputs, 0.0), axis=-1)
121
+ Q = inputs[:, :, :self.vocab_size]
122
+ C = inputs[:, :, self.vocab_size : 2 * self.vocab_size]
123
+ R = inputs[:, :, 2 * self.vocab_size :]
124
+
125
+ cum_attempts = tf.cumsum(Q, axis=1)
126
+ cum_corrects = tf.cumsum(C, axis=1)
127
+ skill_specific_acc = cum_corrects / tf.maximum(cum_attempts, 1.0)
128
+ hist_features = tf.concat([cum_attempts, cum_corrects, skill_specific_acc, R], axis=-1)
129
+
130
+ base_interaction = tf.concat([Q, C], axis=-1)
131
+ x_emb = self.proj_inputs(base_interaction)
132
+ h_emb = self.proj_hist(hist_features)
133
+
134
+ x = tf.concat([x_emb, h_emb], axis=-1)
135
+ x = self.spatial_dropout(x, training=training)
136
+
137
+ lstm_out = self.lstm(x, mask=mask)
138
+ context = self.attention(lstm_out, mask=mask)
139
+
140
+ combined = tf.concat([lstm_out, context], axis=-1)
141
+ combined = self.layer_norm(combined)
142
+ combined = self.dropout_out(combined, training=training)
143
+ return self.output_dense(combined)
144
+
145
+ def get_config(self):
146
+ return {"vocab_size": self.vocab_size}
147
+
148
+
149
+ class MultiHeadTemporalAttention(tf.keras.layers.Layer):
150
+ """Transformer-specific Multi-Head Causal Attention."""
151
+ def __init__(self, units: int, num_heads: int = 4, **kwargs):
152
+ super().__init__(**kwargs)
153
+ self.supports_masking = True
154
+ assert units % num_heads == 0, "units must be divisible by num_heads"
155
+ self.units = units
156
+ self.num_heads = num_heads
157
+ self.head_dim = units // num_heads
158
+
159
+ def build(self, input_shape):
160
+ self.W_query = tf.keras.layers.Dense(self.units, name="mha_query")
161
+ self.W_key = tf.keras.layers.Dense(self.units, name="mha_key")
162
+ self.W_value = tf.keras.layers.Dense(self.units, name="mha_value")
163
+ self.W_out = tf.keras.layers.Dense(self.units, name="mha_output")
164
+ super().build(input_shape)
165
+
166
+ def _split_heads(self, x):
167
+ batch = tf.shape(x)[0]
168
+ seq_len = tf.shape(x)[1]
169
+ x = tf.reshape(x, (batch, seq_len, self.num_heads, self.head_dim))
170
+ return tf.transpose(x, perm=[0, 2, 1, 3])
171
+
172
+ def call(self, query, key_value, mask=None):
173
+ Q = self._split_heads(self.W_query(query))
174
+ K = self._split_heads(self.W_key(key_value))
175
+ V = self._split_heads(self.W_value(key_value))
176
+ d_k = tf.cast(self.head_dim, tf.float32)
177
+ scores = tf.matmul(Q, K, transpose_b=True) / tf.sqrt(d_k)
178
+ seq_len = tf.shape(scores)[2]
179
+ causal = tf.linalg.band_part(tf.ones((seq_len, seq_len), dtype=tf.float32), -1, 0)
180
+ scores = scores + (1.0 - causal[tf.newaxis, tf.newaxis, :, :]) * (-1e9)
181
+ if mask is not None:
182
+ pad_mask = tf.cast(mask[:, tf.newaxis, tf.newaxis, :], tf.float32)
183
+ scores = scores + (1.0 - pad_mask) * (-1e9)
184
+ weights = tf.nn.softmax(scores, axis=-1)
185
+ context = tf.matmul(weights, V)
186
+ context = tf.transpose(context, perm=[0, 2, 1, 3])
187
+ batch = tf.shape(context)[0]
188
+ seq_len_out = tf.shape(context)[1]
189
+ context = tf.reshape(context, (batch, seq_len_out, self.units))
190
+ return self.W_out(context)
191
+
192
+ def compute_mask(self, inputs, mask=None):
193
+ return mask
194
+
195
+ def get_config(self):
196
+ config = super().get_config()
197
+ config.update({"units": self.units, "num_heads": self.num_heads})
198
+ return config
199
+
200
+
201
+ class TransformerDecoderBlock(tf.keras.layers.Layer):
202
+ """Transformer decoder block."""
203
+ def __init__(self, embed_dim, num_heads, ffn_dim, dropout_rate=0.1, **kwargs):
204
+ super().__init__(**kwargs)
205
+ self.supports_masking = True
206
+ self.embed_dim = embed_dim
207
+ self.num_heads = num_heads
208
+ self.ffn_dim = ffn_dim
209
+ self.dropout_rate = dropout_rate
210
+
211
+ def build(self, input_shape):
212
+ self.att = MultiHeadTemporalAttention(self.embed_dim, self.num_heads)
213
+ self.ffn = tf.keras.Sequential([
214
+ tf.keras.layers.Dense(self.ffn_dim, activation="gelu", name="ffn_dense_1"),
215
+ tf.keras.layers.Dense(self.embed_dim, name="ffn_dense_2")
216
+ ])
217
+ self.layernorm1 = tf.keras.layers.LayerNormalization(epsilon=1e-6, name="layernorm_1")
218
+ self.layernorm2 = tf.keras.layers.LayerNormalization(epsilon=1e-6, name="layernorm_2")
219
+ self.dropout1 = tf.keras.layers.Dropout(self.dropout_rate)
220
+ self.dropout2 = tf.keras.layers.Dropout(self.dropout_rate)
221
+ super().build(input_shape)
222
+
223
+ def call(self, query, key_value, training=False, mask=None):
224
+ attn_output = self.att(query, key_value, mask=mask)
225
+ attn_output = self.dropout1(attn_output, training=training)
226
+ out1 = self.layernorm1(query + attn_output)
227
+
228
+ ffn_output = self.ffn(out1)
229
+ ffn_output = self.dropout2(ffn_output, training=training)
230
+ out2 = self.layernorm2(out1 + ffn_output)
231
+ return out2
232
+
233
+ def compute_mask(self, inputs, mask=None):
234
+ return mask
235
+
236
+ def get_config(self):
237
+ config = super().get_config()
238
+ config.update({
239
+ "embed_dim": self.embed_dim,
240
+ "num_heads": self.num_heads,
241
+ "ffn_dim": self.ffn_dim,
242
+ "dropout_rate": self.dropout_rate
243
+ })
244
+ return config
245
+
246
+
247
+ class TransformerMultivariateDKT(tf.keras.Model):
248
+ """Causal Cross-Transformer DKT model."""
249
+ def __init__(self, vocab_size, embed_dim=64, attention_units=64,
250
+ num_transformer_blocks=2, num_attn_heads=4,
251
+ ffn_dim=128, dropout_rate=0.4, l2_reg=1e-4,
252
+ max_seq_len=100, **kwargs):
253
+ super().__init__(**kwargs)
254
+ self.vocab_size = vocab_size
255
+ self._config = dict(embed_dim=embed_dim, attention_units=attention_units,
256
+ num_transformer_blocks=num_transformer_blocks,
257
+ num_attn_heads=num_attn_heads, ffn_dim=ffn_dim,
258
+ dropout_rate=dropout_rate, l2_reg=l2_reg)
259
+
260
+ reg = tf.keras.regularizers.l2(l2_reg)
261
+ self.proj_target = tf.keras.layers.Dense(attention_units, activation="gelu", kernel_regularizer=reg)
262
+ self.proj_inputs = tf.keras.layers.Dense(embed_dim, activation="gelu", kernel_regularizer=reg)
263
+ self.proj_hist = tf.keras.layers.Dense(embed_dim, activation="gelu", kernel_regularizer=reg)
264
+ self.combined_proj = tf.keras.layers.Dense(attention_units, activation="gelu", kernel_regularizer=reg)
265
+ self.pos_embedding = tf.keras.layers.Embedding(max_seq_len, attention_units, name="pos_emb")
266
+ self.input_dropout = tf.keras.layers.Dropout(dropout_rate)
267
+ self.transformer_blocks = [
268
+ TransformerDecoderBlock(attention_units, num_attn_heads, ffn_dim, dropout_rate, name=f"transformer_block_{i}")
269
+ for i in range(num_transformer_blocks)
270
+ ]
271
+ self.output_dense = tf.keras.layers.Dense(1, activation="sigmoid", name="skill_output")
272
+
273
+ def call(self, inputs, training=False):
274
+ Q_next = inputs[:, :, :self.vocab_size]
275
+ Q_past = inputs[:, :, self.vocab_size : 2 * self.vocab_size]
276
+ C_past = inputs[:, :, 2 * self.vocab_size : 3 * self.vocab_size]
277
+ R_past = inputs[:, :, 3 * self.vocab_size :]
278
+
279
+ mask = tf.reduce_any(tf.not_equal(Q_next, 0.0), axis=-1)
280
+
281
+ cum_attempts = tf.cumsum(Q_past, axis=1)
282
+ cum_corrects = tf.cumsum(C_past, axis=1)
283
+ skill_specific_acc = cum_corrects / tf.maximum(cum_attempts, 1.0)
284
+
285
+ overall_attempts = tf.cumsum(tf.reduce_sum(Q_past, axis=-1, keepdims=True), axis=1)
286
+ overall_corrects = tf.cumsum(tf.reduce_sum(C_past, axis=-1, keepdims=True), axis=1)
287
+ overall_acc = overall_corrects / tf.maximum(overall_attempts, 1.0)
288
+
289
+ hist_features = tf.concat([
290
+ cum_attempts, cum_corrects, skill_specific_acc,
291
+ overall_attempts, overall_corrects, overall_acc, R_past
292
+ ], axis=-1)
293
+
294
+ target_emb = self.proj_target(Q_next)
295
+ x_past_emb = self.proj_inputs(tf.concat([Q_past, C_past], axis=-1))
296
+ h_past_emb = self.proj_hist(hist_features)
297
+ past_emb = tf.concat([x_past_emb, h_past_emb], axis=-1)
298
+ past_emb = self.combined_proj(past_emb)
299
+
300
+ seq_len = tf.shape(inputs)[1]
301
+ positions = tf.range(seq_len)
302
+ pos_emb = self.pos_embedding(positions)[tf.newaxis, :, :]
303
+
304
+ target_emb = target_emb + pos_emb
305
+ past_emb = past_emb + pos_emb
306
+
307
+ target_emb = self.input_dropout(target_emb, training=training)
308
+ past_emb = self.input_dropout(past_emb, training=training)
309
+
310
+ x = target_emb
311
+ for block in self.transformer_blocks:
312
+ x = block(query=x, key_value=past_emb, training=training, mask=mask)
313
+ return self.output_dense(x)
314
+
315
+ def get_config(self):
316
+ return {"vocab_size": self.vocab_size, **self._config}
317
+
318
+ # ──────────────────────────────────────────────────────────────
319
+ # Global Variables (Populated at Startup)
320
+ # ──────────────────────────────────────────────────────────────
321
+ _model = None
322
+ _vocab = None
323
+ _vocab_size = None
324
+ _idx_to_name = None
325
+ _skill_id_to_name = None
326
+ _top_categories = None
327
+
328
+ # ──────────────────────────────────────────────────────────────
329
+ # Artifact Ingestion & Model Loading
330
+ # ──────────────────────────────────────────────────────────────
331
+
332
+ def load_artefacts():
333
+ """Load the trained model and vocabulary based on global config."""
334
+ logger.info("=" * 60)
335
+ logger.info(f"LOADING ARTIFACTS FOR MODEL_TYPE: {MODEL_TYPE}")
336
+ logger.info(f"Model File: {MODEL_PATH}")
337
+ logger.info(f"Vocab File: {VOCAB_PATH}")
338
+ logger.info("=" * 60)
339
+
340
+ custom_objects = {
341
+ "TemporalAttentionLayer": TemporalAttentionLayer,
342
+ "MultivariateResponseTimeDKT": MultivariateResponseTimeDKT,
343
+ "MultiHeadTemporalAttention": MultiHeadTemporalAttention,
344
+ "TransformerDecoderBlock": TransformerDecoderBlock,
345
+ "TransformerMultivariateDKT": TransformerMultivariateDKT,
346
+ }
347
+
348
+ if not os.path.exists(VOCAB_PATH):
349
+ logger.error(f"Vocabulary file not found at {VOCAB_PATH}!")
350
+ raise FileNotFoundError(f"Vocabulary file not found: {VOCAB_PATH}")
351
+
352
+ with open(VOCAB_PATH, "r", encoding="utf-8") as f:
353
+ meta = json.load(f)
354
+
355
+ vocab = meta["vocab"]
356
+ vocab_size = meta["vocab_size"]
357
+ idx_to_name = {int(k): v for k, v in meta["idx_to_name"].items()}
358
+ skill_id_to_name = meta.get("skill_id_to_name", {})
359
+
360
+ if not os.path.exists(MODEL_PATH):
361
+ logger.error(f"Model file not found at {MODEL_PATH}!")
362
+ raise FileNotFoundError(f"Model file not found: {MODEL_PATH}")
363
+
364
+ # Load model with compile=False to avoid needing custom loss
365
+ model = tf.keras.models.load_model(
366
+ MODEL_PATH,
367
+ custom_objects=custom_objects,
368
+ compile=False
369
+ )
370
+
371
+ logger.info(f"Loaded vocab of size {vocab_size} and idx_to_name of size {len(idx_to_name)}.")
372
+ logger.info("Model loaded successfully with compile=False.")
373
+ return model, vocab, vocab_size, idx_to_name, skill_id_to_name
374
+
375
+
376
+ def load_top_categories():
377
+ """Load the categories from top_category.json."""
378
+ logger.info(f"Loading top categories from: {TOP_CATEGORY_PATH}")
379
+ if not os.path.exists(TOP_CATEGORY_PATH):
380
+ logger.error(f"top_category.json not found at {TOP_CATEGORY_PATH}!")
381
+ raise FileNotFoundError(f"top_category.json not found: {TOP_CATEGORY_PATH}")
382
+
383
+ with open(TOP_CATEGORY_PATH, "r", encoding="utf-8") as f:
384
+ data = json.load(f)
385
+
386
+ # Filter out "Miscellaneous / Other"
387
+ filtered = [cat for cat in data if cat["title"] != "Miscellaneous / Other"]
388
+ logger.info(f"Loaded {len(filtered)} active top categories.")
389
+ return filtered
390
+
391
+ # ──────────────────────────────────────────────────────────────
392
+ # Helper Functions
393
+ # ──────────────────────────────────────────────────────────────
394
+
395
+ def encode_multi_hot(skill_id_str: str, vocab: dict, vocab_size: int) -> np.ndarray:
396
+ """Encode compound skill string (e.g. '10_12') into a multi-hot vector."""
397
+ vec = np.zeros(vocab_size, dtype=np.float32)
398
+ for ind in str(skill_id_str).split("_"):
399
+ vec[vocab.get(ind, 0)] = 1.0
400
+ return vec
401
+
402
+
403
+ def predict_mastery(
404
+ student_history: list[tuple[str, int, int]],
405
+ model: tf.keras.Model,
406
+ vocab: dict,
407
+ vocab_size: int,
408
+ idx_to_name: dict[int, str],
409
+ ) -> np.ndarray:
410
+ """
411
+ Run DKT model forward pass to output predicted mastery probabilities
412
+ for all vocab_size skills.
413
+
414
+ student_history list elements: (skill_id, correctness, ms_first_response)
415
+ """
416
+ T = len(student_history)
417
+ if T < 1:
418
+ logger.info("Empty history. Returning neutral default mastery of 0.5.")
419
+ return np.full(vocab_size, 0.5, dtype=np.float32)
420
+
421
+ logger.info(f"[Inference Engine] Input Sequence Length T = {T} (MODEL_TYPE: {MODEL_TYPE})")
422
+
423
+ # Pre-encode all historical interactions
424
+ Q = [encode_multi_hot(skill_id, vocab, vocab_size) for skill_id, _, _ in student_history]
425
+ C = [Q[t] * float(correct) for t, (_, correct, _) in enumerate(student_history)]
426
+
427
+ # Process log response times (capped at 8 or 10 min, convert to log(1 + seconds))
428
+ R = []
429
+ for _, _, ms in student_history:
430
+ ms_val = float(ms)
431
+ if ms_val < 0: ms_val = 0.0
432
+ # Capping at 8 min (480000ms) for LSTM and 10 min (600000ms) for Transformer
433
+ cap_val = 480000.0 if MODEL_TYPE == "LSTM" else 600000.0
434
+ ms_val = min(ms_val, cap_val)
435
+ r_val = math.log1p(ms_val / 1000.0)
436
+ R.append(np.array([r_val], dtype=np.float32))
437
+
438
+ if MODEL_TYPE == "LSTM":
439
+ # LSTM input shape: (B, T, 2 * vocab_size + 1)
440
+ # Create input array of shape (1, MAX_SEQ_LEN, vocab_size * 2 + 1)
441
+ X = np.zeros((1, MAX_SEQ_LEN, vocab_size * 2 + 1), dtype=np.float32)
442
+
443
+ # Pre-pad: active steps are placed at the end of the sequence
444
+ n_steps = min(T, MAX_SEQ_LEN)
445
+ pad_offset = MAX_SEQ_LEN - n_steps
446
+
447
+ for t in range(n_steps):
448
+ hist_t = T - n_steps + t
449
+ x_step = np.concatenate([Q[hist_t], C[hist_t], R[hist_t]])
450
+ X[0, pad_offset + t, :] = x_step
451
+
452
+ logger.info(f"[LSTM Prep] Formatted sequence of length {n_steps} (pre-padded by {pad_offset} steps). Shape: {X.shape}")
453
+
454
+ # Forward pass
455
+ predictions = model(X, training=False) # Shape: (1, MAX_SEQ_LEN, vocab_size)
456
+
457
+ # Extract predictions at the final step index (MAX_SEQ_LEN - 1)
458
+ probs = predictions[0, MAX_SEQ_LEN - 1, :].numpy()
459
+
460
+ elif MODEL_TYPE == "Transformer":
461
+ # Transformer input shape: (B, T, 3 * vocab_size + 1)
462
+ # We need target one-hot skill in inputs[:, :, :vocab_size].
463
+ # To get the predicted mastery of ALL vocab_size skills, we construct a batch of size vocab_size
464
+ X = np.zeros((vocab_size, T, vocab_size * 3 + 1), dtype=np.float32)
465
+
466
+ # Fill steps 0 ... T-2 (historical steps)
467
+ for t in range(T - 1):
468
+ # Target skill for step t is the actual skill at t+1
469
+ x_step = np.concatenate([Q[t + 1], Q[t], C[t], R[t]])
470
+ X[:, t, :] = x_step
471
+
472
+ # Fill the final step T-1 for each candidate target skill index
473
+ for i in range(vocab_size):
474
+ Q_target = np.zeros(vocab_size, dtype=np.float32)
475
+ Q_target[i] = 1.0
476
+ x_last = np.concatenate([Q_target, Q[T - 1], C[T - 1], R[T - 1]])
477
+ X[i, T - 1, :] = x_last
478
+
479
+ # Truncate if T exceeds MAX_SEQ_LEN
480
+ n_steps = T
481
+ if n_steps > MAX_SEQ_LEN:
482
+ X = X[:, -MAX_SEQ_LEN:, :]
483
+ n_steps = MAX_SEQ_LEN
484
+
485
+ # Post-pad: pad with zeros at the end if n_steps < MAX_SEQ_LEN
486
+ if n_steps < MAX_SEQ_LEN:
487
+ pad = MAX_SEQ_LEN - n_steps
488
+ X_padded = np.zeros((vocab_size, MAX_SEQ_LEN, vocab_size * 3 + 1), dtype=np.float32)
489
+ X_padded[:, :n_steps, :] = X
490
+ X = X_padded
491
+
492
+ logger.info(f"[Transformer Prep] Formatted batch of size {vocab_size} with seq length {n_steps}. Shape: {X.shape}")
493
+
494
+ # Forward pass
495
+ y_pred = model(X, training=False) # Shape: (vocab_size, MAX_SEQ_LEN, 1)
496
+
497
+ # Extract probabilities at the last valid time-step
498
+ valid_step_idx = n_steps - 1
499
+ probs = y_pred[:, valid_step_idx, 0].numpy() # Shape: (vocab_size,)
500
+
501
+ else:
502
+ raise ValueError(f"Unknown MODEL_TYPE: {MODEL_TYPE}")
503
+
504
+ # Log raw probabilities sample
505
+ logger.info(f"[Model Output Log] Raw prediction probs (first 10 skills): {probs[:10]}")
506
+ return probs
507
+
508
+ # ──────────────────────────────────────────────────────────────
509
+ # Pydantic Schemas
510
+ # ──────────────────────────────────────────────────────────────
511
+
512
+ class Interaction(BaseModel):
513
+ skill_id: str = Field(..., description="Skill ID or underscore-separated ID compound like '2_37'")
514
+ correctness: int = Field(..., description="Is the response correct (1) or incorrect (0)")
515
+ ms_first_response: int = Field(..., description="Time taken to answer in milliseconds")
516
+
517
+
518
+ class PredictRequest(BaseModel):
519
+ student_history: List[Interaction] = Field(..., description="Sequence of past student interactions")
520
+ personal_preference: str = Field(..., description="Generative AI prompt style personalization preference")
521
+
522
+
523
+ class PredictResponse(BaseModel):
524
+ category_mastery: Dict[str, Optional[float]] = Field(..., description="Mastery scores of the 6 top categories")
525
+ explanation: Optional[str] = Field(None, description="Supportive explanation generated by Gemini AI (if triggered)")
526
+
527
+ # ──────────────────────────────────────────────────────────────
528
+ # FastAPI Application
529
+ # ──────────────────────────────────────────────────────────────
530
+
531
+ app = FastAPI(
532
+ title="EduPredict Math β€” Unified Inference API",
533
+ description="Serving LSTM & Cross-Transformer DKT models with personalized Gemini support.",
534
+ version="1.0.0"
535
+ )
536
+
537
+
538
+ @app.on_event("startup")
539
+ def startup_event():
540
+ global _model, _vocab, _vocab_size, _idx_to_name, _skill_id_to_name, _top_categories
541
+ _model, _vocab, _vocab_size, _idx_to_name, _skill_id_to_name = load_artefacts()
542
+ _top_categories = load_top_categories()
543
+ logger.info("Application successfully initialized and ready for requests.")
544
+
545
+
546
+ @app.post("/predict", response_model=PredictResponse)
547
+ def predict(req: PredictRequest):
548
+ logger.info("=" * 80)
549
+ logger.info("INGESTED NEW PREDICT REQUEST")
550
+ logger.info("=" * 80)
551
+
552
+ # ── Step 1: Extract History ──
553
+ history = [(item.skill_id, item.correctness, item.ms_first_response) for item in req.student_history]
554
+ logger.info(f"Student history loaded with {len(history)} total interactions.")
555
+
556
+ if len(history) == 0:
557
+ raise HTTPException(status_code=400, detail="Student history cannot be empty.")
558
+
559
+ # ── Step 2: Run DKT Model Inference ──
560
+ probs = predict_mastery(history, _model, _vocab, _vocab_size, _idx_to_name)
561
+
562
+ # Track per-skill historical interaction count
563
+ # Let's count how many times each vocabulary index was active in the history
564
+ skill_history_lens = {}
565
+ for item in req.student_history:
566
+ active_indices = [_vocab.get(s, 0) for s in str(item.skill_id).split("_")]
567
+ for idx in active_indices:
568
+ skill_history_lens[idx] = skill_history_lens.get(idx, 0) + 1
569
+
570
+ logger.info(f"Historical interaction counts mapped for {len(skill_history_lens)} unique skill indices.")
571
+
572
+ # ── Step 3: Check GenAI Trigger for Most Recent Question ──
573
+ most_recent = req.student_history[-1]
574
+ recent_skill_ids = str(most_recent.skill_id).split("_")
575
+ logger.info(f"Most recent question skills to check: {recent_skill_ids}")
576
+
577
+ struggling_skills_triggered = []
578
+
579
+ for sid in recent_skill_ids:
580
+ idx = _vocab.get(sid, 0)
581
+ mastery = float(probs[idx])
582
+ history_len = skill_history_lens.get(idx, 0)
583
+ skill_name = _idx_to_name.get(idx, f"Skill_{idx}")
584
+
585
+ logger.info(f"Checking trigger -> Skill '{sid}' ({skill_name}): mastery = {mastery:.4f}, history sequence len = {history_len}")
586
+
587
+ if mastery < STRUGGLE_THRESHOLD:
588
+ if history_len > LENGTH_THRESHOLD:
589
+ logger.info(f"-> TRIGGER MET! mastery {mastery:.4f} < {STRUGGLE_THRESHOLD} AND history {history_len} > {LENGTH_THRESHOLD}")
590
+ struggling_skills_triggered.append(skill_name)
591
+ else:
592
+ logger.info(f"-> Mastery is below threshold but history len ({history_len}) is not > threshold ({LENGTH_THRESHOLD}).")
593
+
594
+ # Call Gemini GenAI if triggered
595
+ explanation = None
596
+ if struggling_skills_triggered:
597
+ explanation = trigger_gemini_explanation(struggling_skills_triggered, req.personal_preference)
598
+ else:
599
+ logger.info("Generative AI was not triggered (no skills met both mastery and history length triggers).")
600
+
601
+ # ── Step 4: Top Category Mastery Score Aggregation ──
602
+ category_mastery = {}
603
+
604
+ for cat in _top_categories:
605
+ title = cat["title"]
606
+ included_str_indices = cat["skill_id_included"]
607
+
608
+ cat_scores = []
609
+
610
+ for idx_str in included_str_indices:
611
+ idx = int(idx_str)
612
+ # Ensure it is within our vocabulary
613
+ if idx in _idx_to_name:
614
+ history_len = skill_history_lens.get(idx, 0)
615
+ if history_len >= AVG_LEN_THRESHOLD:
616
+ mastery_val = float(probs[idx])
617
+ cat_scores.append(mastery_val)
618
+
619
+ if len(cat_scores) > 0:
620
+ avg_val = sum(cat_scores) / len(cat_scores)
621
+ category_mastery[title] = round(avg_val, 4)
622
+ logger.info(f"Category '{title}': Avg of {len(cat_scores)} eligible skills = {category_mastery[title]:.4f}")
623
+ else:
624
+ category_mastery[title] = None
625
+ logger.info(f"Category '{title}': No skills met the avg_len_threshold of {AVG_LEN_THRESHOLD}. Returning null.")
626
+
627
+ # Return consolidated response
628
+ logger.info("Successfully completed predict request.")
629
+ logger.info(f"Final output categories: {category_mastery}")
630
+ return PredictResponse(category_mastery=category_mastery, explanation=explanation)
631
+
632
+
633
+ def trigger_gemini_explanation(skills: List[str], preference: str) -> str:
634
+ """Call Google Gemini Generative AI to generate personalized hint."""
635
+ logger.info(f"[GenAI Trigger] Contacting Gemini for struggling skills: {skills}")
636
+ logger.info(f"[GenAI Trigger] Preference: \"{preference}\"")
637
+
638
+ prompt = (
639
+ f"A student is struggling with the following math concepts: {', '.join(skills)}.\n"
640
+ f"Their personalized explanation style preference is: \"{preference}\"\n\n"
641
+ "Please generate a warm, encouraging 2-3 sentence hint explaining the core rule, logic, "
642
+ "or concept they might be forgetting, tailored exactly to their preference. "
643
+ "Keep the language simple, supportive, and active."
644
+ "The output should be in Bahasa Indonesia."
645
+ )
646
+
647
+ logger.info(f"[Gemini Prompt]\n{prompt}")
648
+
649
+ if not GEMINI_API_KEY:
650
+ placeholder = (
651
+ f"[Gemini API key not configured] "
652
+ f"Keep practicing! Revisit the core rules of {', '.join(skills)}. "
653
+ f"Try taking it step by step β€” you are fully capable of masteries like this!"
654
+ )
655
+ logger.warning("GEMINI_API_KEY is not set. Returning offline placeholder.")
656
+ return placeholder
657
+
658
+ try:
659
+ import google.generativeai as genai
660
+ genai.configure(api_key=GEMINI_API_KEY)
661
+ model = genai.GenerativeModel("gemini-2.0-flash")
662
+ response = model.generate_content(prompt)
663
+ text = response.text.strip()
664
+ logger.info(f"[Gemini Response]\n{text}")
665
+ return text
666
+ except Exception as e:
667
+ logger.error(f"Error calling Gemini: {e}")
668
+ fallback = (
669
+ f"[Gemini API connection error] "
670
+ f"Review the key rules for {', '.join(skills)}. "
671
+ f"Draw a picture or break it down into simple equations β€” you've got this!"
672
+ )
673
+ return fallback
674
+
675
+ # ──────────────────────────────────────────────────────────────
676
+ # Standalone Smoke Test
677
+ # ──────────────────────────────────────────────────────────────
678
+
679
+ if __name__ == "__main__":
680
+ # Let's perform a standalone smoke-test
681
+ logger.info("=" * 60)
682
+ logger.info("RUNNING STANDALONE SMOKE-TEST")
683
+ logger.info("=" * 60)
684
+
685
+ # Initialize
686
+ _model, _vocab, _vocab_size, _idx_to_name, _skill_id_to_name = load_artefacts()
687
+ _top_categories = load_top_categories()
688
+
689
+ # Create a mock student history with compound skills
690
+ mock_history = [
691
+ ("2", 1, 12000), # Circle Graph (idx 2)
692
+ ("2_37", 0, 25000), # Circle Graph & Addition Whole Numbers (idx 2, idx 25)
693
+ ("2", 0, 18000), # Circle Graph (idx 2)
694
+ ("2", 0, 15000), # Circle Graph (idx 2) -> Length for idx 2 is 4 (which is > 3 trigger threshold)
695
+ ]
696
+
697
+ logger.info("\n--- STEP 1: Running Predict Mastery ---")
698
+ probs = predict_mastery(mock_history, _model, _vocab, _vocab_size, _idx_to_name)
699
+
700
+ # Track per-skill historical length
701
+ mock_lens = {}
702
+ for item in mock_history:
703
+ indices = [_vocab.get(s, 0) for s in str(item[0]).split("_")]
704
+ for idx in indices:
705
+ mock_lens[idx] = mock_lens.get(idx, 0) + 1
706
+
707
+ logger.info("\n--- STEP 2: Running GenAI Trigger Check on Last Interaction ---")
708
+ most_recent_skill = mock_history[-1][0]
709
+ for s in most_recent_skill.split("_"):
710
+ idx = _vocab.get(s, 0)
711
+ mastery = float(probs[idx])
712
+ hlen = mock_lens.get(idx, 0)
713
+ logger.info(f"Skill '{s}' (name: {_idx_to_name[idx]}): mastery = {mastery:.4f}, history count = {hlen}")
714
+
715
+ logger.info("\n--- STEP 3: Running Category Aggregation ---")
716
+ for cat in _top_categories:
717
+ title = cat["title"]
718
+ scores = []
719
+ for sidx_str in cat["skill_id_included"]:
720
+ sidx = int(sidx_str)
721
+ if sidx in _idx_to_name and mock_lens.get(sidx, 0) >= AVG_LEN_THRESHOLD:
722
+ scores.append(float(probs[sidx]))
723
+ avg = sum(scores) / len(scores) if scores else None
724
+ logger.info(f" Category '{title}': avg = {avg}")
725
+
726
+ logger.info("\nStandalone Smoke-Test finished successfully!")
top_category.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [
2
+ {
3
+ "title": "Data Analysis, Statistics, and Probability",
4
+ "skill_id_included": [
5
+ "1", "2", "3", "4", "5", "6", "7", "8", "9", "10",
6
+ "11", "12", "13", "14", "15", "56"
7
+ ]
8
+ },
9
+ {
10
+ "title": "Geometry and Spatial Reasoning",
11
+ "skill_id_included": [
12
+ "16", "17", "18", "19", "20", "21", "22", "64", "65", "66",
13
+ "80"
14
+ ]
15
+ },
16
+ {
17
+ "title": "Measurement, Area, and Volume",
18
+ "skill_id_included": [
19
+ "23", "24", "26", "27", "28", "29", "67", "68", "69", "70",
20
+ "71", "72", "73", "74", "75"
21
+ ]
22
+ },
23
+ {
24
+ "title": "Number Sense, Properties, and Operations",
25
+ "skill_id_included": [
26
+ "25", "30", "31", "32", "33", "34", "35", "36", "37", "38",
27
+ "39", "40", "41", "42", "44", "45", "50", "51", "52", "53",
28
+ "54", "57", "59", "60", "61", "62", "63", "76", "77", "81"
29
+ ]
30
+ },
31
+ {
32
+ "title": "Ratios, Proportions, and Percentages",
33
+ "skill_id_included": [
34
+ "43", "46", "47", "48", "49"
35
+ ]
36
+ },
37
+ {
38
+ "title": "Algebraic Thinking, Equations, and Inequalities",
39
+ "skill_id_included": [
40
+ "55", "58", "78", "79", "82", "83", "84", "85", "86", "87",
41
+ "88", "89"
42
+ ]
43
+ },
44
+ {
45
+ "title": "Miscellaneous / Other",
46
+ "skill_id_included": [
47
+ "0"
48
+ ]
49
+ }
50
+ ]