CoolFace
Apppublic

FaizanMirZa77/FormatX

sourceHugging Faceupdated 13d agoView on Hugging Face
0likes
test_service.py155 linesDownload Raw Back to root
1"""2test_service.py3───────────────4Test generation and evaluation service.5 6- MCQ evaluation:   exact match on A/B/C/D7- Short answer:     sentence-transformers (all-MiniLM-L6-v2) semantic similarity8                    + keyword overlap for a combined score9- Grading:          A/B/C/D/F based on percentage10- Model loading:    lazy singleton — loaded once on first use, cached in memory11"""12 13import re14import logging15from functools import lru_cache16 17log = logging.getLogger("formatx.test")18 19# ── Sentence-transformer model (lazy load) ────────────────────────────────────20 21_model = None22 23def _get_model():24    global _model25    if _model is None:26        log.info("[TEST] Loading sentence-transformers model (first use)...")27        from sentence_transformers import SentenceTransformer28        _model = SentenceTransformer("all-MiniLM-L6-v2")29        log.info("[TEST] Model loaded.")30    return _model31 32 33# ── MCQ evaluation ────────────────────────────────────────────────────────────34 35def evaluate_mcq(user_answer: str, correct_answer: str) -> float:36    """Returns 1.0 if correct, 0.0 if wrong. Case-insensitive."""37    return 1.0 if user_answer.strip().upper() == correct_answer.strip().upper() else 0.038 39 40# ── Short answer evaluation ───────────────────────────────────────────────────41 42def evaluate_short_answer(43    user_answer:    str,44    correct_answer: str,45    keywords:       str,46) -> float:47    """48    Returns a score between 0.0 and 1.0.49 50    Combined score = 0.6 × semantic_similarity + 0.4 × keyword_overlap51 52    Semantic similarity: cosine similarity between sentence embeddings.53    Keyword overlap: fraction of expected keywords found in user answer.54    """55    if not user_answer or not user_answer.strip():56        return 0.057 58    # ── Semantic similarity ───────────────────────────────────────59    try:60        model      = _get_model()61        embeddings = model.encode([user_answer.strip(), correct_answer.strip()])62        from sentence_transformers import util63        sim = float(util.cos_sim(embeddings[0], embeddings[1]))64        sim = max(0.0, min(1.0, sim))65    except Exception as e:66        log.warning(f"[TEST] Semantic similarity failed: {e} — using 0")67        sim = 0.068 69    # ── Keyword overlap ───────────────────────────────────────────70    kw_score = 0.071    if keywords:72        kw_list  = [k.strip().lower() for k in keywords.split(",") if k.strip()]73        if kw_list:74            answer_lower = user_answer.lower()75            matched      = sum(1 for kw in kw_list if kw in answer_lower)76            kw_score     = matched / len(kw_list)77 78    combined = 0.6 * sim + 0.4 * kw_score79    return round(combined, 3)80 81 82# ── Grade computation ─────────────────────────────────────────────────────────83 84def compute_grade(percentage: float) -> str:85    if percentage >= 90: return "A"86    if percentage >= 80: return "B"87    if percentage >= 70: return "C"88    if percentage >= 60: return "D"89    return "F"90 91 92# ── Test evaluation ───────────────────────────────────────────────────────────93 94def evaluate_test(questions: list[dict], answers: dict[str, str]) -> dict:95    """96    Evaluate a submitted test.97 98    questions: list of question dicts from DB99    answers:   {str(question_id): user_answer_string}100 101    Returns full result dict with per-question breakdown.102    """103    results      = []104    total_marks  = 0105    earned_marks = 0.0106 107    for q in questions:108        qid         = str(q["id"])109        user_ans    = answers.get(qid, "").strip()110        qtype       = q["question_type"]111        correct     = q["correct_answer"]112        marks       = q["marks"]113        total_marks += marks114 115        if qtype == "mcq":116            score_ratio = evaluate_mcq(user_ans, correct)117            is_correct  = score_ratio == 1.0118            earned      = marks * score_ratio119            feedback    = "Correct" if is_correct else f"Incorrect. Correct answer: {correct}"120        else:121            score_ratio = evaluate_short_answer(user_ans, correct, q.get("keywords", ""))122            is_correct  = score_ratio >= 0.6   # 60% threshold = passing123            earned      = marks * score_ratio124            feedback    = (125                f"Good answer (similarity: {round(score_ratio*100)}%)"126                if is_correct else127                f"Partial/incorrect (similarity: {round(score_ratio*100)}%). "128                f"Model answer: {correct[:200]}"129            )130 131        earned_marks += earned132 133        results.append({134            "questionId":    q["id"],135            "questionText":  q["question_text"],136            "questionType":  qtype,137            "userAnswer":    user_ans,138            "correctAnswer": correct if qtype == "mcq" else correct[:300],139            "isCorrect":     is_correct,140            "marksEarned":   round(earned, 2),141            "totalMarks":    marks,142            "feedback":      feedback,143        })144 145    percentage = round((earned_marks / total_marks * 100) if total_marks > 0 else 0.0, 2)146    grade      = compute_grade(percentage)147 148    return {149        "totalMarks":    total_marks,150        "obtainedMarks": round(earned_marks, 2),151        "percentage":    percentage,152        "grade":         grade,153        "results":       results,154    }155