final quiz generation

parent 7212b9bd
# Network settings
bind = "0.0.0.0:8000" bind = "0.0.0.0:8000"
# Worker settings
worker_class = "uvicorn.workers.UvicornWorker" worker_class = "uvicorn.workers.UvicornWorker"
workers = 4 workers = 4
timeout = 300
# Logging settings
accesslog = "-" accesslog = "-"
errorlog = "-" errorlog = "-"
loglevel = "info" loglevel = "info"
\ No newline at end of file
...@@ -162,9 +162,7 @@ def create_app() -> FastAPI: ...@@ -162,9 +162,7 @@ def create_app() -> FastAPI:
shutil.move(temp_csv_path, csv_dest_path) shutil.move(temp_csv_path, csv_dest_path)
print(f"--- Background task: Saved new embeddings to '{csv_dest_path}' ---", flush=True) print(f"--- Background task: Saved new embeddings to '{csv_dest_path}' ---", flush=True)
# ==========================================================
# === CORRECTED LOGIC: APPEND NEW CURRICULUM TO EXISTING ===
# ==========================================================
# --- 3. Read both JSON files --- # --- 3. Read both JSON files ---
print("--- Background task: Reading generated JSON structure... ---", flush=True) print("--- Background task: Reading generated JSON structure... ---", flush=True)
with open(temp_json_path, 'r', encoding='utf-8') as f: with open(temp_json_path, 'r', encoding='utf-8') as f:
...@@ -324,8 +322,7 @@ def create_app() -> FastAPI: ...@@ -324,8 +322,7 @@ def create_app() -> FastAPI:
""" """
pdf_bytes = await file.read() pdf_bytes = await file.read()
# --- THIS IS THE FIX ---
# We now pass BOTH required arguments to the add_task method.
background_tasks.add_task( background_tasks.add_task(
process_pdf_curriculum_in_background, process_pdf_curriculum_in_background,
pdf_bytes, pdf_bytes,
...@@ -443,6 +440,25 @@ def create_app() -> FastAPI: ...@@ -443,6 +440,25 @@ def create_app() -> FastAPI:
except Exception as e: except Exception as e:
logger.error(f"Error in get_dynamic_quiz_handler: {e}") logger.error(f"Error in get_dynamic_quiz_handler: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@app.get("/quiz-interface")
async def serve_quiz_interface():
"""Serve the dynamic quiz generator HTML file"""
try:
# Check for the file in a 'static' folder first
static_file = Path("static/dynamic_quiz_interface.html")
if static_file.exists():
return FileResponse(static_file)
# Fallback to the root directory
current_file = Path("dynamic_quiz_interface.html")
if current_file.exists():
return FileResponse(current_file)
raise HTTPException(status_code=404, detail="Dynamic quiz interface not found")
except Exception as e:
print(f"Error serving quiz interface: {e}")
raise HTTPException(status_code=500, detail=f"Error serving interface: {str(e)}")
@app.options("/get-audio-response") @app.options("/get-audio-response")
async def audio_response_options(): async def audio_response_options():
......
...@@ -361,18 +361,21 @@ class AgentService: ...@@ -361,18 +361,21 @@ class AgentService:
def get_dynamic_quiz( def get_dynamic_quiz(
self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, count: int self, grade: int, subject: str, unit: str, concept: str, is_arabic: bool, count: int
) -> List[Dict]: ) -> List[Dict]:
""" """
Generates a dynamic quiz of 'count' questions using a hybrid approach: Generates a dynamic quiz of 'count' questions using a hybrid approach with BATCHED generation:
1. Always generates a "freshness batch" of new questions. 1. Always generates a "freshness batch" of new questions.
2. Retrieves all questions and checks if the total meets the 'count'. 2. Retrieves all questions and checks if the total meets the 'count'.
3. If not, generates the remaining number of questions needed. 3. If not, generates the remaining number of questions needed IN BATCHES.
""" """
if not self.pgvector: if not self.pgvector:
raise HTTPException(status_code=503, detail="Vector service is not available for this feature.") raise HTTPException(status_code=503, detail="Vector service is not available for this feature.")
# --- PART 1: Follow the original logic to ensure freshness --- # Define maximum questions per batch to avoid token limits
MAX_QUESTIONS_PER_BATCH = 10
# --- PART 1: Generate freshness questions ---
# 1. Calculate how many new questions to generate for freshness. # 1. Calculate how many new questions to generate for freshness.
num_fresh_questions = min(max(1, math.floor(count / 3)), 5) num_fresh_questions = min(max(1, math.floor(count / 3)), 5)
logger.info(f"Request for {count} questions. Step 1: Generating {num_fresh_questions} new 'freshness' questions.") logger.info(f"Request for {count} questions. Step 1: Generating {num_fresh_questions} new 'freshness' questions.")
...@@ -386,7 +389,7 @@ class AgentService: ...@@ -386,7 +389,7 @@ class AgentService:
except Exception as e: except Exception as e:
logger.warning(f"Could not generate 'freshness' questions for the quiz due to an error: {e}") logger.warning(f"Could not generate 'freshness' questions for the quiz due to an error: {e}")
# --- PART 2: Check for a shortfall and generate more if needed --- # --- PART 2: Check for shortfall and generate more if needed (WITH BATCHING) ---
# 3. Retrieve ALL available questions for the topic from the database. # 3. Retrieve ALL available questions for the topic from the database.
all_mcqs_after_freshness = self.pgvector.get_mcqs( all_mcqs_after_freshness = self.pgvector.get_mcqs(
...@@ -397,20 +400,37 @@ class AgentService: ...@@ -397,20 +400,37 @@ class AgentService:
# 4. Calculate if there is still a shortfall. # 4. Calculate if there is still a shortfall.
questions_still_needed = count - len(all_mcqs_after_freshness) questions_still_needed = count - len(all_mcqs_after_freshness)
# 5. If we still need more questions, generate the exact number missing. # 5. If we still need more questions, generate them IN BATCHES.
if questions_still_needed > 0: if questions_still_needed > 0:
logger.info(f"After freshness batch, have {len(all_mcqs_after_freshness)} questions. Generating {questions_still_needed} more to meet count of {count}.") logger.info(f"After freshness batch, have {len(all_mcqs_after_freshness)} questions. Need to generate {questions_still_needed} more to meet count of {count}.")
# Cap this second generation step as a safeguard. total_generated = 0
num_to_generate_gap = min(questions_still_needed, 10) remaining = questions_still_needed
try: while remaining > 0:
self.generate_and_store_mcqs( # Determine batch size (cap at MAX_QUESTIONS_PER_BATCH and also cap total at 20 per session)
grade=grade, subject=subject, unit=unit, concept=concept, batch_size = min(remaining, MAX_QUESTIONS_PER_BATCH)
is_arabic=is_arabic, num_questions=num_to_generate_gap
)
except Exception as e: try:
logger.warning(f"Could not generate the remaining {num_to_generate_gap} questions due to an error: {e}") logger.info(f"Generating batch {total_generated // MAX_QUESTIONS_PER_BATCH + 1} of {batch_size} questions...")
self.generate_and_store_mcqs(
grade=grade, subject=subject, unit=unit, concept=concept,
is_arabic=is_arabic, num_questions=batch_size
)
total_generated += batch_size
remaining -= batch_size
logger.info(f"Successfully generated batch. Total gap-filling questions generated: {total_generated}")
except Exception as e:
logger.error(f"Failed to generate batch of {batch_size} questions: {e}")
# If we've generated at least some questions, continue
if total_generated > 0:
logger.warning(f"Continuing with {total_generated} gap-filling questions generated so far.")
break
else:
logger.warning(f"Could not generate gap-filling questions: {e}")
break
# --- PART 3: Final Assembly and Return --- # --- PART 3: Final Assembly and Return ---
...@@ -422,10 +442,14 @@ class AgentService: ...@@ -422,10 +442,14 @@ class AgentService:
if not final_pool: if not final_pool:
raise HTTPException(status_code=404, detail="No questions could be found or generated for this topic.") raise HTTPException(status_code=404, detail="No questions could be found or generated for this topic.")
# Check if we have enough questions
if len(final_pool) < count:
logger.warning(f"Could only gather {len(final_pool)} questions out of {count} requested. Returning all available questions.")
# 7. Randomly select the desired number of questions from the final pool. # 7. Randomly select the desired number of questions from the final pool.
random.shuffle(final_pool) random.shuffle(final_pool)
final_quiz = final_pool[:count] final_quiz = final_pool[:min(count, len(final_pool))]
logger.info(f"Returning a dynamic quiz of {len(final_quiz)} questions for '{concept}'.") logger.info(f"Returning a dynamic quiz of {len(final_quiz)} questions for '{concept}'.")
return final_quiz return final_quiz
\ No newline at end of file
...@@ -15,4 +15,4 @@ echo "MCQ table setup complete." ...@@ -15,4 +15,4 @@ echo "MCQ table setup complete."
sleep 5 sleep 5
# Start the web server and keep it as the main process # Start the web server and keep it as the main process
exec gunicorn -c gunicorn.conf.py main:app exec gunicorn -c gunicorn.conf.py main:app
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dynamic Quiz Generator</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
max-width: 800px;
margin: 40px auto;
padding: 20px;
background-color: #f9f9f9;
color: #333;
line-height: 1.6;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
h1 {
text-align: center;
color: #2c3e50;
}
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 20px;
}
.form-group {
display: flex;
flex-direction: column;
}
.form-group.full-width {
grid-column: 1 / -1;
}
label {
margin-bottom: 5px;
font-weight: bold;
color: #555;
}
input[type="text"], input[type="number"] {
width: 95%;
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
font-size: 16px;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
margin-top: 10px;
}
button {
display: block;
width: 100%;
padding: 12px;
font-size: 16px;
font-weight: bold;
background: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #0056b3;
}
button:disabled {
background: #95a5a6;
cursor: not-allowed;
}
button.secondary {
background: #28a745;
margin-top: 10px;
}
button.secondary:hover {
background: #218838;
}
button.secondary:disabled {
background: #95a5a6;
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: 5px;
font-weight: bold;
display: none;
}
.status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.status.processing { background-color: #e7f3ff; color: #004085; border: 1px solid #b3d9ff; }
#resultsContainer {
margin-top: 30px;
border-top: 2px solid #eee;
padding-top: 20px;
}
.quiz-question {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 20px;
margin-bottom: 20px;
}
.quiz-question h3 {
margin-top: 0;
color: #343a40;
}
.quiz-question ul {
list-style-type: none;
padding: 0;
}
.quiz-question li {
padding: 8px 0;
}
.correct-answer {
color: #28a745;
font-weight: bold;
}
</style>
</head>
<body>
<div class="container">
<h1>Dynamic Quiz Generator</h1>
<div class="form-grid">
<div class="form-group">
<label for="gradeInput">Grade:</label>
<input type="number" id="gradeInput" value="4">
</div>
<div class="form-group">
<label for="subjectInput">Subject:</label>
<input type="text" id="subjectInput" value="Science">
</div>
<div class="form-group full-width">
<label for="unitInput">Unit:</label>
<input type="text" id="unitInput" placeholder="e.g., الوحدة الأولى: الأنظمة الحية">
</div>
<div class="form-group full-width">
<label for="conceptInput">Concept:</label>
<input type="text" id="conceptInput" placeholder="e.g., المفهوم الأول: التكيف والبقاء">
</div>
<div class="form-group">
<label for="countInput">Number of Questions:</label>
<input type="number" id="countInput" value="5">
</div>
<div class="form-group">
<label>Language:</label>
<div class="checkbox-group">
<input type="checkbox" id="isArabicInput">
<label for="isArabicInput">Arabic Quiz</label>
</div>
</div>
</div>
<button id="generateButton">Generate Dynamic Quiz</button>
<button id="downloadCsvButton" class="secondary" style="display:none;">Download Quiz as CSV</button>
<div id="status"></div>
<div id="resultsContainer" style="display:none;"></div>
</div>
<script>
const gradeInput = document.getElementById('gradeInput');
const subjectInput = document.getElementById('subjectInput');
const unitInput = document.getElementById('unitInput');
const conceptInput = document.getElementById('conceptInput');
const countInput = document.getElementById('countInput');
const isArabicInput = document.getElementById('isArabicInput');
const generateButton = document.getElementById('generateButton');
const downloadCsvButton = document.getElementById('downloadCsvButton');
const statusDiv = document.getElementById('status');
const resultsContainer = document.getElementById('resultsContainer');
let currentQuizData = null;
generateButton.addEventListener('click', async () => {
const grade = gradeInput.value;
const subject = subjectInput.value;
const unit = unitInput.value.trim();
const concept = conceptInput.value.trim();
const count = countInput.value;
const isArabic = isArabicInput.checked;
if (!unit || !concept) {
showStatus('Please fill in both the Unit and Concept fields.', 'error');
return;
}
showStatus('Generating dynamic quiz... This may take a moment.', 'processing');
generateButton.disabled = true;
downloadCsvButton.style.display = 'none';
resultsContainer.innerHTML = '';
resultsContainer.style.display = 'none';
currentQuizData = null;
const formData = new FormData();
formData.append('grade', grade);
formData.append('subject', subject);
formData.append('unit', unit);
formData.append('concept', concept);
formData.append('count', count);
formData.append('is_arabic', isArabic);
try {
const response = await fetch('/quiz/dynamic', {
method: 'POST',
body: formData,
});
const responseData = await response.json();
if (!response.ok) {
throw new Error(responseData.detail || `Server error: ${response.statusText}`);
}
showStatus(responseData.message, 'success');
currentQuizData = responseData.quiz;
displayQuizResults(responseData.quiz);
downloadCsvButton.style.display = 'block';
} catch (error) {
showStatus(`An error occurred: ${error.message}`, 'error');
} finally {
generateButton.disabled = false;
}
});
downloadCsvButton.addEventListener('click', () => {
if (!currentQuizData || currentQuizData.length === 0) {
showStatus('No quiz data available to download.', 'error');
return;
}
const csvContent = convertQuizToCSV(currentQuizData);
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
const filename = `quiz_${subjectInput.value}_${Date.now()}.csv`;
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatus('Quiz downloaded successfully!', 'success');
});
function convertQuizToCSV(quiz) {
const headers = ['Question Number', 'Question Text', 'Correct Answer', 'Wrong Answer 1', 'Wrong Answer 2', 'Wrong Answer 3'];
const escapeCSV = (str) => {
if (str === null || str === undefined) return '';
const stringValue = String(str);
if (stringValue.includes(',') || stringValue.includes('"') || stringValue.includes('\n')) {
return `"${stringValue.replace(/"/g, '""')}"`;
}
return stringValue;
};
const rows = quiz.map((q, index) => {
return [
index + 1,
escapeCSV(q.question_text),
escapeCSV(q.correct_answer),
escapeCSV(q.wrong_answer_1),
escapeCSV(q.wrong_answer_2),
escapeCSV(q.wrong_answer_3)
].join(',');
});
return '\uFEFF' + [headers.join(','), ...rows].join('\n');
}
function displayQuizResults(quiz) {
if (!quiz || quiz.length === 0) {
resultsContainer.innerHTML = '<p>No questions were returned.</p>';
resultsContainer.style.display = 'block';
return;
}
quiz.forEach((question, index) => {
const questionDiv = document.createElement('div');
questionDiv.className = 'quiz-question';
const questionTitle = document.createElement('h3');
questionTitle.textContent = `Question ${index + 1}: ${question.question_text}`;
const optionsList = document.createElement('ul');
const options = [
{ text: question.correct_answer, isCorrect: true },
{ text: question.wrong_answer_1, isCorrect: false },
{ text: question.wrong_answer_2, isCorrect: false },
{ text: question.wrong_answer_3, isCorrect: false }
].sort(() => Math.random() - 0.5);
options.forEach(option => {
const listItem = document.createElement('li');
listItem.textContent = option.text;
if (option.isCorrect) {
listItem.classList.add('correct-answer');
listItem.textContent += ' (Correct)';
}
optionsList.appendChild(listItem);
});
questionDiv.appendChild(questionTitle);
questionDiv.appendChild(optionsList);
resultsContainer.appendChild(questionDiv);
});
resultsContainer.style.display = 'block';
}
function showStatus(message, type) {
statusDiv.className = `status ${type}`;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
}
</script>
</body>
</html>
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment