feath: save questions generated with AI

This commit is contained in:
2026-03-17 17:59:55 -03:00
parent 55f9a3196e
commit e8cdf61468
4 changed files with 80 additions and 25 deletions
@@ -615,9 +615,10 @@ pub struct ApplyTemplatePayload {
// ==================== RAG Question Generation ==================== // ==================== RAG Question Generation ====================
/// POST /api/test-templates/generate-with-rag - Generate questions using RAG from MySQL question bank /// POST /test-templates/generate-with-rag - Generate questions using RAG from MySQL question bank
pub async fn generate_questions_with_rag( pub async fn generate_questions_with_rag(
Org(org_ctx): Org, Org(org_ctx): Org,
claims: Claims,
State(pool): State<PgPool>, State(pool): State<PgPool>,
Json(payload): Json<RagGenerationPayload>, Json(payload): Json<RagGenerationPayload>,
) -> Result<Json<Vec<TestTemplateQuestion>>, (StatusCode, String)> { ) -> Result<Json<Vec<TestTemplateQuestion>>, (StatusCode, String)> {
@@ -701,6 +702,10 @@ pub async fn generate_questions_with_rag(
tracing::info!("Calling Ollama at {} with model {}", url, model); tracing::info!("Calling Ollama at {} with model {}", url, model);
// Save topic for later use
let topic = payload.topic.clone().unwrap_or_else(|| "English grammar".to_string());
let num_questions = payload.num_questions.unwrap_or(5);
// Simplified system prompt for better performance on slower machines // Simplified system prompt for better performance on slower machines
let system_prompt = format!( let system_prompt = format!(
r#"You are an English Teacher creating quiz questions. r#"You are an English Teacher creating quiz questions.
@@ -725,8 +730,8 @@ pub async fn generate_questions_with_rag(
Skills: reading, listening, speaking, writing. Distribute across all 4."#, Skills: reading, listening, speaking, writing. Distribute across all 4."#,
rag_context, rag_context,
payload.num_questions.unwrap_or(5), num_questions,
payload.topic.unwrap_or_else(|| "English grammar".to_string()) topic
); );
tracing::debug!("System prompt length: {} chars", system_prompt.len()); tracing::debug!("System prompt length: {} chars", system_prompt.len());
@@ -811,9 +816,59 @@ pub async fn generate_questions_with_rag(
if generated_questions.is_empty() { if generated_questions.is_empty() {
return Err((StatusCode::INTERNAL_SERVER_ERROR, "AI failed to generate questions".to_string())); return Err((StatusCode::INTERNAL_SERVER_ERROR, "AI failed to generate questions".to_string()));
} }
tracing::info!("Generated {} questions using RAG from MySQL bank", generated_questions.len()); // Save generated questions to question bank
let mut saved_count = 0;
for question in &generated_questions {
let question_type = match question.question_type.as_str() {
"true-false" => common::models::QuestionBankType::TrueFalse,
"short-answer" => common::models::QuestionBankType::ShortAnswer,
"essay" => common::models::QuestionBankType::Essay,
"matching" => common::models::QuestionBankType::Matching,
"ordering" => common::models::QuestionBankType::Ordering,
_ => common::models::QuestionBankType::MultipleChoice,
};
let result = sqlx::query(
r#"
INSERT INTO question_bank (
organization_id, created_by, question_text, question_type,
options, correct_answer, explanation, points, difficulty,
source, source_metadata, is_active
)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, true)
"#
)
.bind(org_ctx.id)
.bind(claims.sub)
.bind(&question.question_text)
.bind(&question_type)
.bind(&question.options)
.bind(&question.correct_answer)
.bind(&question.explanation)
.bind(question.points)
.bind("medium")
.bind("rag-ai")
.bind(&json!({
"generated_by": "rag-ai",
"source": "mysql-bank",
"topic": topic,
"generated_at": chrono::Utc::now().to_rfc3339(),
}))
.execute(&pool)
.await;
if result.is_ok() {
saved_count += 1;
}
}
tracing::info!(
"Generated {} questions using RAG from MySQL bank, saved {} to question bank",
generated_questions.len(),
saved_count
);
Ok(Json(generated_questions)) Ok(Json(generated_questions))
} }
@@ -328,14 +328,14 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500" className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
> >
<option value="beginner">Beginner</option> <option value="beginner">Beginner</option>
<option value="beginner_1">Beginner 1</option> <option value="beginner1">Beginner 1</option>
<option value="beginner_2">Beginner 2</option> <option value="beginner2">Beginner 2</option>
<option value="intermediate">Intermediate</option> <option value="intermediate">Intermediate</option>
<option value="intermediate_1">Intermediate 1</option> <option value="intermediate1">Intermediate 1</option>
<option value="intermediate_2">Intermediate 2</option> <option value="intermediate2">Intermediate 2</option>
<option value="advanced">Advanced</option> <option value="advanced">Advanced</option>
<option value="advanced_1">Advanced 1</option> <option value="advanced1">Advanced 1</option>
<option value="advanced_2">Advanced 2</option> <option value="advanced2">Advanced 2</option>
</select> </select>
</div> </div>
@@ -90,14 +90,14 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
const getLevelLabel = (level: CourseLevel) => { const getLevelLabel = (level: CourseLevel) => {
const labels: Record<CourseLevel, string> = { const labels: Record<CourseLevel, string> = {
beginner: 'Beginner', beginner: 'Beginner',
beginner_1: 'Beginner 1', beginner1: 'Beginner 1',
beginner_2: 'Beginner 2', beginner2: 'Beginner 2',
intermediate: 'Intermediate', intermediate: 'Intermediate',
intermediate_1: 'Intermediate 1', intermediate1: 'Intermediate 1',
intermediate_2: 'Intermediate 2', intermediate2: 'Intermediate 2',
advanced: 'Advanced', advanced: 'Advanced',
advanced_1: 'Advanced 1', advanced1: 'Advanced 1',
advanced_2: 'Advanced 2', advanced2: 'Advanced 2',
}; };
return labels[level] || level; return labels[level] || level;
}; };
@@ -185,14 +185,14 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
> >
<option value="">Todos los niveles</option> <option value="">Todos los niveles</option>
<option value="beginner">Beginner</option> <option value="beginner">Beginner</option>
<option value="beginner_1">Beginner 1</option> <option value="beginner1">Beginner 1</option>
<option value="beginner_2">Beginner 2</option> <option value="beginner2">Beginner 2</option>
<option value="intermediate">Intermediate</option> <option value="intermediate">Intermediate</option>
<option value="intermediate_1">Intermediate 1</option> <option value="intermediate1">Intermediate 1</option>
<option value="intermediate_2">Intermediate 2</option> <option value="intermediate2">Intermediate 2</option>
<option value="advanced">Advanced</option> <option value="advanced">Advanced</option>
<option value="advanced_1">Advanced 1</option> <option value="advanced1">Advanced 1</option>
<option value="advanced_2">Advanced 2</option> <option value="advanced2">Advanced 2</option>
</select> </select>
</div> </div>
<div> <div>
+1 -1
View File
@@ -1135,7 +1135,7 @@ export interface BackgroundTask {
// ==================== Test Templates ==================== // ==================== Test Templates ====================
export type CourseLevel = 'beginner' | 'beginner_1' | 'beginner_2' | 'intermediate' | 'intermediate_1' | 'intermediate_2' | 'advanced' | 'advanced_1' | 'advanced_2'; export type CourseLevel = 'beginner' | 'beginner1' | 'beginner2' | 'intermediate' | 'intermediate1' | 'intermediate2' | 'advanced' | 'advanced1' | 'advanced2';
export type CourseType = 'intensive' | 'regular'; export type CourseType = 'intensive' | 'regular';
export type TestType = 'CA' | 'MWT' | 'MOT' | 'FOT' | 'FWT'; export type TestType = 'CA' | 'MWT' | 'MOT' | 'FOT' | 'FWT';
export type QuestionType = 'multiple-choice' | 'true-false' | 'short-answer' | 'essay' | 'matching' | 'ordering'; export type QuestionType = 'multiple-choice' | 'true-false' | 'short-answer' | 'essay' | 'matching' | 'ordering';