Spaces:
Sleeping
Sleeping
init: upload required file for inference
Browse files- .gitattributes +2 -0
- Dockerfile +43 -0
- final/causal_cross_transformer_dkt_model/causal_cross_transformer_dkt_model.keras +3 -0
- final/causal_cross_transformer_dkt_model/vocab.json +302 -0
- final/lstm_dkt_model_train/lstm_dkt_model.keras +3 -0
- final/lstm_dkt_model_train/vocab.json +302 -0
- inference_api.py +726 -0
- top_category.json +50 -0
.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 |
+
]
|