Implement Server-Side AI Question Bank Generator, Adaptive Weakness Sampling 60/40, and Curriculum Studio One-Click Generation

This commit is contained in:
Hamza-Ayed
2026-09-03 15:38:35 +03:00
parent c9c03892d1
commit 8d7ffcf2eb
7 changed files with 603 additions and 266 deletions
@@ -298,4 +298,32 @@ class CurriculumController
]);
}
}
/**
* Generate & Refresh AI Question Bank for any Unit or Lesson (Triggered from Curriculum Studio)
* POST /api/curriculum/generate-questions
*/
public function generateQuestionBank(Request $request, Response $response): void
{
$body = $request->getBody();
$unitPath = $body['unit_path'] ?? 'grade_10/math_10/semester_1/unit_01';
$courseId = (int)($body['course_id'] ?? 1);
$count = (int)($body['count'] ?? 50);
try {
require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php';
$result = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, $courseId, $count);
$response->json([
'status' => 'success',
'message' => "تم توليد وتحديث بنك الأسئلة بالذكاء الاصطناعي بنجاح ({$result['total_in_bank']} سؤالاً معيارياً في قاعدة البيانات)!",
'data' => $result
]);
} catch (\Throwable $e) {
$response->status(500)->json([
'status' => 'error',
'message' => 'تعذر توليد بنك الأسئلة: ' . $e->getMessage()
]);
}
}
}
+61 -16
View File
@@ -70,7 +70,61 @@ class ExamController
return;
}
$questions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]);
$allQuestions = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$examId]);
$questions = [];
if (count($allQuestions) > 15) {
// Adaptive Sampling Engine based on Student Weakness History
$studentId = $request->user_id ?? 0;
$weakTopics = [];
if ($studentId > 0) {
// Fetch failed topics from student's answers
$fails = Database::select(
"SELECT q.topic_tag, COUNT(*) as cnt
FROM student_question_answers sqa
JOIN questions q ON sqa.question_id = q.id
WHERE sqa.student_id = ? AND sqa.is_correct = 0 AND q.topic_tag IS NOT NULL
GROUP BY q.topic_tag ORDER BY cnt DESC LIMIT 5",
[$studentId]
);
foreach ($fails as $f) {
$weakTopics[] = $f['topic_tag'];
}
}
// Split questions into weak vs other topics
$weakPool = [];
$otherPool = [];
foreach ($allQuestions as $q) {
if (!empty($weakTopics) && in_array($q['topic_tag'], $weakTopics)) {
$weakPool[] = $q;
} else {
$otherPool[] = $q;
}
}
shuffle($weakPool);
shuffle($otherPool);
if (!empty($weakPool)) {
// 60% focused on weak concepts for adaptive remediation
$targetWeakCount = min(count($weakPool), 9);
$questions = array_slice($weakPool, 0, $targetWeakCount);
// 40% from rest of unit concepts
$needed = 15 - count($questions);
$questions = array_merge($questions, array_slice($otherPool, 0, $needed));
} else {
// Stratified random sampling from the comprehensive bank
$questions = array_slice($otherPool, 0, 15);
}
shuffle($questions);
} else {
$questions = $allQuestions;
}
foreach ($questions as &$q) {
$options = Database::select("SELECT id, option_text, feedback_text" . ($isTeacher ? ", is_correct" : "") . " FROM question_options WHERE question_id = ? ORDER BY id ASC", [$q['id']]);
@@ -78,6 +132,7 @@ class ExamController
}
$exam['questions'] = $questions;
$exam['questions_count'] = count($questions);
$response->json([
'status' => 'success',
@@ -394,27 +449,17 @@ class ExamController
}
}
if (count($existingQuestions) < 15 || $hasCalculus) {
if (count($existingQuestions) < 45 || $hasCalculus) {
// Wipe legacy corrupt questions
foreach ($existingQuestions as $oq) {
Database::query("DELETE FROM question_options WHERE question_id = ?", [$oq['id']]);
}
Database::query("DELETE FROM questions WHERE exam_id = ?", [$examId]);
$bank = self::getUnit1QuestionBank();
foreach ($bank as $qData) {
$qId = (int)Database::insert(
"INSERT INTO questions (uuid, exam_id, question_text, question_type, bloom_taxonomy, topic_tag, explanation_text, points)
VALUES (UUID(), ?, ?, 'multiple_choice', ?, ?, ?, ?)",
[$examId, $qData['question_text'], $qData['bloom'], $qData['topic'], $qData['explanation'], $qData['points']]
);
foreach ($qData['options'] as $idx => $optText) {
Database::insert(
"INSERT INTO question_options (question_id, option_text, is_correct) VALUES (?, ?, ?)",
[$qId, $optText, ($idx === $qData['correct_index']) ? 1 : 0]
);
}
}
require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php';
$unitPath = 'grade_10/math_10/semester_1/unit_01';
$res = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, 1, 50);
return (int)$res['exam_id'];
}
return $examId;