Implement Server-Side AI Question Bank Generator, Adaptive Weakness Sampling 60/40, and Curriculum Studio One-Click Generation
This commit is contained in:
@@ -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()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user