تو مگو همه به جنگند و زِ صلح من چه آید تو یکی نِه‌ای هزاری تو چراغِ خود برافروز...

باقر (Bagher)، که پس از تغییراتی ابلفضلی در ساختار اسکواد کوئرا کالج، این بار اما با شروع سری جدید #المپیک‌فناوری پردیس تصمیم به تاسیس آکادمی آموزشی جدیدی با نام باقر‌آکادمی گرفته که قرار است مرز‌های آموزش برنامه‌نویسی و هوش‌مصنوعی را این بار با اما همکاری پارک فناوری پردیس، از قلب‌های تپنده‌ی فناوری ایران، جا‌به‌جا کند. باقر‌آکادمی اما برخلاف مجموعه‌های آموزشی دیگر از یک روش جدید و پیشرفته برای ارزیابی دانشجویانش استفاده می‌کند!

انواع مختلفی از سوالات آموزشی که در باقرآکادمی پشتیبانی می‌شوند، مانند سوالات چند‌گزینه‌ای و یا کوتاه‌پاسخ، می‌توانند برخلاف سوالات چند‌گزینه‌ای و کوتاه‌پاسخی که پیش‌تر دیده‌اید، شامل متغیر‌ها، روابط ریاضی و بازه‌های عددی در قالب‌ مانیفست سوالات (Question Manifest) باشند. کارکنان این مجموعه، سوالات را در هنگام طراحی به صورت مانیفست‌هایی زنده‌ و متغیر برای تولید نه تنها یک نمونه سوال، بلکه هزاران نمونه سوال جدید مشابه اما با بدنه‌های متغیر، بیان‌ می‌کنند! تو یکی بلکه هزاری تو سوال خود برافروز...

تصویر سوال باقرآکادمی

پروژه اولیه

برای دانلود پروژه‌ی اولیه روی این لینک کلیک کنید.

ساختار فایل‌ها

bagher-academy
├── core
│   ├── __init__.py
│   ├── evaluator.py
│   ├── placeholder.py
│   ├── renderer.py
│   └── variable_resolver.py
├── data
│   ├── configs
│   └── templates
├── engine
├── main.py
├── models
│   ├── __init__.py
│   ├── base_question.py
│   ├── matching.py
│   ├── mcq.py
│   ├── numeric_range.py
│   ├── question_bank.py
│   ├── short_answer.py
│   └── true_false.py
├── questions
├── utils
└── valid_files

راه‌اندازی پروژه

تعریف مانیفست سؤال‌ها

هر سؤال در باقرآکادمی به‌صورت یک مانیفست JSON در مسیر data/templates/ تعریف می‌شود. مثلاً برای سؤال کوتاه‌پاسخ:

{
  "id": "short",
  "type": "short_answer",
  "template": {
    "stem": "یک جسم از ارتفاع {{height}} متر رها می‌شود. شتاب گرانش g = {{g}} m/s² است. سرعت برخورد جسم با زمین چقدر است؟",
    "answer": "{{sqrt(2*g*height)}}"
  },
  "variables": {
    "height": [5, 20],
    "g": [9.7, 9.9]
  }
}

اجرای برنامه CLI

تولید سه نسخه از سؤال کوتاه‌پاسخ (بدون ذخیره)

python main.py --template short

تولید پنج نسخه از سؤال پرتابه (numeric_range)

python main.py --template projectile_range --count 5

تولید و ذخیره خروجی به‌صورت فایل JSON

python main.py --template short --count 3 --save

در این حالت، فایل‌های زیر به‌صورت خودکار ساخته می‌شوند:

questions/
└── short/
    ├── question1.json
    ├── question2.json
    └── question3.json

ساختار خروجی هر سؤال

نمونه خروجی برای short_answer:

{
  "id": "short",
  "type": "short_answer",
  "rendered": {
    "stem": "یک جسم از ارتفاع 12 متر رها می‌شود. شتاب گرانش g = 9.8 m/s² است. سرعت برخورد جسم با زمین چقدر است؟",
    "answer": "15.34"
  },
  "vars": {
    "height": 12,
    "g": 9.8
  }
}

جزئیات پروژه

در این پروژه، هدف پیاده‌سازی یک ساختار برای تولید و مدیریت انواع سوالات آموزشی است که شامل قالب‌های مختلف مانند چندگزینه‌ای، پاسخ کوتاه، درست/نادرست، تطبیقی و بازه عددی می‌شود. این سیستم باید بتواند مانیفست سوال را از فایل‌های JSON بارگذاری کند، متغیرهای (Variables) مرتبط با هر سوال را با استفاده از محدوده‌ها و قواعد مشخص تولید کند و سپس متن نهایی سوال و گزینه‌ها یا پاسخ‌ها را با جایگذاری مقادیر متغیرها و محاسبات ریاضی محاسبه‌شده ایجاد کند. ساختار پروژه بر اساس کلاس‌های پایه و قابل ارث‌بری طراحی شده است؛ BaseQuestion رابط اصلی را فراهم می‌کند و کلاس‌های خاص مانند MultipleChoiceQuestion یا NumericRangeQuestion این رابط را پیاده‌سازی می‌کنند و مسئول مدیریت قالب، اعتبارسنجی و تولید نسخه‌های متغیرهای مختلف هستند. بخش‌های کمکی مانند SafeEvaluator برای محاسبه‌ی عبارات ریاضی، PlaceholderProcessor برای پردازش متن‌های قالب‌بندی‌شده با placeholder و VariableResolver برای تولید مقادیر متغیرها استفاده می‌شوند تا کل جریان تولید سوال، از بارگذاری قالب تا رندر نهایی قابل اجرا باشد.

معرفی مانیفست‌های باقرآکادمی - یکی بلکه هزاری سوال!

در باقرآکادمی، هر سوال با یک مانیفست به صورت قالب JSON تعریف می‌شود که شامل شناسه یکتا (idنوع سوال (typeقالب متنی (template) و محدوده مقادیر متغیرها (variables) است. این مانیفست‌ها، ساختار اصلی سوال را تعیین می‌کنند و به سیستم اجازه می‌دهند تا نسخه‌های مختلفی از یک سوال با مقادیر متغیر متفاوت تولید کند. قالب‌ها می‌توانند شامل placeholderهایی در قالب {{...}} باشند که با مقادیر تولید شده جایگزین می‌شوند و حتی محاسبات ریاضی درون آن‌ها توسط SafeEvaluator انجام می‌شود.

برای مثال، مانیفست سوالات با نوع کوتاه‌پاسخ short_answer با شناسه short به صورت زیر تعریف شده است:

{
  "id": "short",
  "type": "short_answer",
  "template": {
    "stem": "یک جسم از ارتفاع {{height}} متر رها می‌شود. شتاب گرانش g = {{g}} m/s² است. سرعت برخورد جسم با زمین چقدر است؟",
    "answer": "{{sqrt(2*g*height)}}"
  },
  "variables": {
    "height": [5, 20],
    "g": [9.7, 9.9]
  }
}
  • در این مثال، متن سوال شامل دو متغیر ارتفاع (height) و شتاب گرانش (g) است که در کد بالا با رنگ آبی مشخص شده‌اند و پاسخ به صورت عبارت ریاضی sqrt(2*g*height) مشخص شده است. باقرآکادمی در هر اجرا، مقادیر مجاز در محدوده مشخص شده برای متغیر‌ها را تولید کرده و پاسخ را محاسبه و جایگذاری می‌کند. خروجی نهایی، سوالات مختلفی است که ممکن است چیزی شبیه به این باشد:
{
  "stem": "یک جسم از ارتفاع 12 متر رها می‌شود. شتاب گرانش g = 9.8 m/s² است. سرعت برخورد جسم با زمین چقدر است؟",
  "answer": "15.34",
  "vars": {"height": 12, "g": 9.8}
}

مانیفست numeric_range با شناسه projectile_range نیز مشابه عمل می‌کند، اما پاسخ عددی با تلرانس مشخص ارائه می‌شود تا امکان مقایسه با جواب کاربر فراهم باشد:

{
  "id": "projectile_range",
  "type": "numeric_range",
  "template": {
    "stem": "اگر یک جسم با سرعت اولیه {{v0}} m/s به صورت افقی پرتاب شود، فاصله طی شده قبل از برخورد زمین چقدر است؟",
    "answer": "{{v0*sqrt(2*height/g)}}",
    "tolerance": 0.05
  },
  "variables": {
    "v0": [10, 30],
    "height": [5, 20],
    "g": [9.7, 9.9]
  }
}

خروجی پردازش شده این سوال ممکن است به شکل زیر باشد:

{
  "stem": "اگر یک جسم با سرعت اولیه 15 m/s به صورت افقی پرتاب شود، فاصله طی شده قبل از برخورد زمین چقدر است؟",
  "answer": "21.0",
  "tolerance": 0.05,
  "vars": {"v0": 15, "height": 10, "g": 9.8}
}

و برای انواع دیگر سوالات نیز به همین ترتیب خواهد بود. به طور خلاصه، مانیفست‌ها، قالب‌های JSON انعطاف‌پذیری هستند که مشخص می‌کنند سوال چه متغیرهایی دارد، متن چگونه تولید شود و پاسخ چگونه محاسبه گردد. باقرآکادمی با استفاده از کلاس‌های مدیریت سوالات، پردازش placeholderها و تولید مقادیر متغیر و پاسخ‌ نهایی، سوالات آماده برای آزمون‌ها تولید می‌کند که قابلیت ارزیابی درست کاربران را دارند.

پیاده‌سازی پوشه core

پیاده سازی کلاس SafeEvaluator از فایل evaluator.py

کلاس SafeEvaluator در فایل core/evaluator.py باید ابزاری باشد که بتواند عبارات ریاضی متنی را بر اساس مجموعه‌ای از متغیرها ارزیابی کرده و مقدار عددی دقیق آن را بازگرداند. ورودی اصلی متد eval یک رشته‌ی بیان ریاضی است، که ممکن است شامل عملگرهایی مثل جمع، تفریق، ضرب، تقسیم، توان، باقی‌مانده، و حتی فراخوانی توابعی مانند sqrt، log، sin یا cos باشد. خروجی آن باید یک عدد (صحیح یا اعشاری) باشد که با دقت مشخصی (مثلاً تا سه رقم اعشار) گرد شده است تا نتایج در تمام اجراها، قطعی و مشخص باقی بمانند. در صورتی که عبارت شامل نام‌های ناشناخته، توابع غیرمجاز یا سینتکس نامعتبر باشد، باید خطای ValueError بازگردانده شود تا از اجرای کد ناامن جلوگیری شود.

همچنین کلاس باید از ثابت‌های ریاضی مثل pi و e پشتیبانی کند تا تست‌هایی که از توابع مثلثاتی یا لگاریتمی استفاده می‌کنند به‌درستی کار کنند. اگر متغیرهایی مانند x، y یا a در عبارت وجود داشته باشند، مقادیر آن‌ها از دیکشنری ورودی گرفته می‌شود تا بتوان عبارات پارامتری را نیز ارزیابی کرد. به‌علاوه، در تست‌هایی که چند عمل ریاضی پشت‌سر‌هم ترکیب شده‌اند، ترتیب تقدم عملگرها و تو در تویی توابع باید حفظ شود تا نتایج عددی دقیقاً با خروجی کتابخانه‌ی math برابر باشند.

متد eval در کلاس SafeEvaluator نقش اصلی را در تبدیل یک رشته‌ی متنی حاوی عبارت ریاضی به مقدار عددی ایفا می‌کند. ورودی این متد یک رشته است که ممکن است شامل اعداد، متغیرها، عملگرهای ریاضی و توابع مجاز مانند sqrt, log, sin باشد و خروجی آن یک مقدار عددی (صحیح یا اعشاری) است که با **دقت مشخص گرد شده است.**همانطور که پیش‌تر گفته شد، این متد باید تمام ترکیب‌های تو در تو و زنجیره‌ای از عملیات ریاضی را به درستی محاسبه کند، به طوری که حتی عبارات پیچیده مثل a + b * c ** 2 - (b + c)/a یا sqrt(abs(log(exp((x + y)**2)))) دقیقاً همان نتیجه‌ای را بدهند که انتظار می‌رود.

علاوه بر این، eval باید به‌طور کامل ایمن باشد و هیچ‌گونه کد اجرایی یا فراخوانی توابع غیرمجاز را اجازه ندهد؛ اگر عبارت شامل نام‌های ناشناخته یا دستورات خطرناک باشد، باید خطای ValueError بدهد. در پیاده‌سازی، این متد با استفاده از تجزیه‌ی AST عمل می‌کند و هر گره‌ی عبارت (مثل عملیات باینری، یونیاری، فراخوانی تابع یا ثابت) را به صورت کنترل‌شده ارزیابی می‌کند تا علاوه بر دقت عددی، امنیت و پیش‌بینی‌پذیری کامل را نیز تضمین کند. همچنین در ترکیب با دیگر بخش‌های باقرآکادمی مانند PlaceholderProcessor و Renderer، متد eval پایه‌ی محاسبات پارامتری و جایگذاری مقادیر در قالب سوالات را تشکیل خواهد داد.

  • توجه داشته باشید که در پیاده‌سازی این سوال شما به هیچ عنوان مجاز به استفاده از تابع eval پایتونی نیستید و استفاده از این مورد به صورت خودکار، نمره صفر برای گل پاسخ ارائه شده لحاظ خواهد کرد.
import ast
import operator as op
import math
from typing import Any, Dict, Optional
from utils.config_loader import ConfigLoader

config = ConfigLoader()
decimal_places: Optional[int] = config.get("decimal_places", 3)
enabled_functions = config.get(
    "math_functions",
    [
        "sqrt","sin","cos","tan","asin","acos","atan",
        "log","log10","exp","ceil","floor","abs","round"
    ]
)

OPERATORS = {
    ast.Add: op.add,
    ast.Sub: op.sub,
    ast.Mult: op.mul,
    ast.Div: op.truediv,
    ast.FloorDiv: op.floordiv,
    ast.Mod: op.mod,
    ast.Pow: op.pow,
    ast.USub: op.neg,
    ast.UAdd: op.pos,
}

builtins_dict = __builtins__ if isinstance(__builtins__, dict) else __builtins__.__dict__
SAFE_FUNCTIONS: Dict[str, Any] = {}
for name in enabled_functions:
    if name in ("abs", "round"):
        SAFE_FUNCTIONS[name] = builtins_dict[name]
    else:
        SAFE_FUNCTIONS[name] = getattr(math, name)

CONSTANTS = {"pi": math.pi, "e": math.e}

class SafeEvaluator:
    def __init__(self, vars: Optional[Dict[str, Any]] = None):
        pass

    def eval(self, expression: str) -> Any:
        pass

برای درک نقش SafeEvaluator و متد eval، می‌توان دو مثال زیر را در نظر گرفت. در مثال اول، یک عبارت تو در تو از توابع ریاضی با استفاده ازsqrt, abs, log, exp داریم:

ev = SafeEvaluator(vars={'x': 2, 'y': 3})
expr = 'sqrt(abs(log(exp((x + y) ** 2))))'
result = ev.eval(expr)

# sqrt((2 + 3) ** 2) = 5
  • در اینجا، eval باید توانایی پردازش دقیق عملیات تو در تو را داشته باشد و مقادیر متغیرها (x=2, y=3) را جایگذاری کند. ترتیب اعمال تابع‌ها و عملیات ریاضی رعایت می‌شود تا نتیجه‌ی نهایی با مقدار مورد انتظار مطابق باشد.

در مثال دوم، یک عبارت ترکیبی از عملگرهای مختلف داریم که شامل جمع، ضرب، توان، تفریق و تقسیم است:

ev = SafeEvaluator(vars={'a': 2, 'b': 3, 'c': 4})
expr = 'a + b * c ** 2 - (b + c) / a'
result = ev.eval(expr)

# 2 + 3 * 4**2 - (3 + 4)/2 = 46.5
  • در اینجا متد eval باید تقدم عملگرها را رعایت کند و محاسبات ترکیبی را به دقت انجام دهد. این مثال نشان می‌دهد که SafeEvaluator باید قادر باشد تا عبارات پیچیده‌ی عددی را به‌درستی محاسبه کند.

پیاده سازی کلاس PlaceholderProcessor از فایل placeholder.py

کلاس PlaceholderProcessor برای پردازش متن‌هایی طراحی شده است که شامل placeholderهای محاسباتی به شکل {{ ... }} هستند و هدف آن جایگذاری مقادیر محاسبه‌شده به‌طور در متن است. این کلاس هنگام ساخت، یک SafeEvaluator دریافت می‌کند تا هر بار که نیاز به محاسبه‌ی عبارت‌ها باشد، یک نمونه امن و مستقل ایجاد شود و هیچ تداخل یا تغییر ناخواسته‌ای در مقادیر رخ ندهد.

متد process متن ورودی را بررسی می‌کند و تمام بخش‌هایی که با الگوی {{...}} هم‌خوانی دارند را استخراج می‌کند. سپس برای هر placeholder، متغیرهای داده‌شده را جایگذاری کرده و عبارت را با استفاده از SafeEvaluator.eval محاسبه می‌کند. اگر هنگام محاسبات ریاضی، خطایی رخ دهد یا عبارت شامل متغیر یا تابع ناشناخته باشد، کلاس به صورت امن placeholder را بدون تغییر باقی می‌گذارد و از ایجاد خطا جلوگیری می‌کند. در نهایت، متن خروجی شامل همه‌ی placeholderهای جایگزین‌شده با مقادیر محاسبه‌شده است.

from .evaluator import SafeEvaluator

class PlaceholderProcessor:
    def __init__(self, evaluator_factory):
        pass

    def process(self, text: str, vars: Dict[str, float]) -> str:
        pass
  • تابع process از کلاس PlaceholderProcessor مسئول پردازش متن‌هایی است که شامل placeholder یا عبارت‌های محاسباتی در قالب {{ ... }} هستند و با مقادیر متغیرها جایگذاری می‌شوند.

به‌عنوان مثال، اگر متن به شکل زیر باشد:

pp = PlaceholderProcessor(lambda: SafeEvaluator(vars={}))
text = "Sum = {{x + y}}"
result = pp.process(text, {'x': 2, 'y': 3})
# result باید برابر با "Sum = 5" باشد
  • در این حالت، process ** حاصل عبارت داخل** {{x + y}} را پیدا می‌کند، مقادیر متغیرها (x=2, y=3) را جایگذاری می‌کند و با استفاده از SafeEvaluator.eval محاسبه می‌کند. نتیجه‌ی عددی به رشته تبدیل شده و در متن اصلی جایگزین می‌شود.

مثال دیگر که شامل توابع ریاضی و ترکیب‌ها است:

text = "Value={{ sqrt(x**2 + y**2) + log(z) }}"
result = pp.process(text, {'x': 3, 'y': 4, 'z': math.e})
# result باید برابر با "Value=6.0" باشد
  • در اینجا process قادر است عبارات پیچیده و تو در تو را پردازش کند و مقادیر دقیق را جایگزین کند. اگر هرگونه خطا یا عبارت غیرقابل ارزیابی وجود داشته باشد، متن اصلی بدون تغییر حفظ می‌شود، یعنی placeholder باقی می‌ماند.

به طور خلاصه، PlaceholderProcessor پل بین داده‌های متغیر و مانفیست سوال شده است و تضمین می‌کند که همه‌ی placeholderها با مقادیر محاسبه‌شده جایگزین شوند.

پیاده سازی کلاس Renderer از فایل placeholder.py

کلاس Renderer مسئول تبدیل یک سوال با قالب متنی و مقادیر متغیرها به یک خروجی آماده است که شامل متن سوال، گزینه‌ها و پاسخ محاسبه‌شده می‌شود و هدف آن این است که تمام placeholderها به صورت با مقادیر مناسب جایگزین شوند. هنگام مقداردهی اولیه، Renderer یک نمونه از PlaceholderProcessor به همراه نمونه‌ی SafeEvaluator ایجاد می‌کند تا تمام پردازش‌های ریاضی و جایگذاری‌ها به صورت امن و قابل پیش‌بینی انجام شوند و هیچ تغییر ناخواسته‌ای روی مقادیر متغیرها رخ ندهد.

متد render ابتدا متن اصلی سوال یا stem را پردازش می‌کند و تمامی placeholderها را با مقادیر محاسبه‌شده جایگزین می‌کند. سپس اگر سوال شامل گزینه‌ها باشد، هر گزینه نیز با همان مقادیر متغیرها پردازش می‌شود تا تمامی توابع و عبارات ریاضی داخل گزینه‌ها به شکل صحیح جایگزین شوند. در نهایت، اگر سوال شامل پاسخ باشد، render آن را پردازش می‌کند و تلاش می‌کند تا مقادیر عددی را با تعداد رقم‌های اعشاری مشخص‌شده و واحد مناسب نمایش دهد. اگر مقدار قابل تبدیل به عدد نباشد، به همان شکل متنی نمایش داده می‌شود و هیچ خطایی ایجاد نمی‌شود.

from typing import Dict
from .placeholder import PlaceholderProcessor
from .evaluator import SafeEvaluator
from utils.config_loader import ConfigLoader

config = ConfigLoader()
DECIMAL_PLACES = config.get("decimal_places", 3)
DEFAULT_UNITS = config.get("default_units", {})

class Renderer:
    def __init__(self):
        pass

    def render(self, question, vars: Dict[str, float]) -> Dict[str, str]:
        pass

کلاس Renderer مسئول تبدیل یک سوال با مانیفست‌های دارای placeholder و مقادیر متغیر به خروجی نهایی است که شامل stem، گزینه‌ها و پاسخ پردازش‌شده می‌شود و تمامی placeholderها را با مقادیر واقعی جایگزین می‌کند. به عبارت دیگر، این کلاس متن خام سوال و گزینه‌ها را دریافت کرده و با استفاده از PlaceholderProcessor و SafeEvaluator مقادیر متغیرها و محاسبات ریاضی داخل placeholderها را محاسبه و در متن جایگذاری می‌کند.

به‌عنوان مثال، اگر یک سوال داشته باشیم با stem "Compute {{x}} + {{y}}" و گزینه‌های ["Sum={{x+y}}", "Double={{x*2}}"] و مقادیر متغیرهای {'x': 3, 'y': 5}، فراخوانی Renderer().render(question, {'x': 3, 'y': 5}) خروجی زیر را تولید می‌کند:

{
    "stem": "Compute 3 + 5",
    "options": ["Sum=8", "Double=6"]
}
  • در این مثال، تمامی placeholderها با مقادیر محاسبه‌شده جایگزین شده‌اند و بدنه سوال جدید به شکل درست ارائه شده است. همچنین، اگر سوال دارای پاسخ عددی یا متنی باشد، Renderer پاسخ را با تعداد رقم‌های اعشار مشخص‌شده و واحد مناسب قالب‌بندی می‌کند. اگر پاسخ یک عبارت غیرقابل تبدیل به عدد باشد، به همان صورت رشته‌ای باقی می‌ماند.

پیاده سازی کلاس VariableResolver از فایل variable_resolver.py

کلاس VariableResolver مسئول مدیریت و تولید مقادیر تصادفی برای متغیرها در محدوده‌های تعریف شده است و به گونه‌ای طراحی شده که خروجی‌ها دترمنیستیک (Deterministic) باشند زمانی که یک seed مشخص یا seed سراسری در پیکربندی تعریف شده باشد. این کلاس، مقادیر پیش‌فرض بارگذاری شده از فایل پیکربندی را با محدوده‌های ارائه شده توسط کاربر ترکیب می‌کند و برای هر متغیر یک مقدار تصادفی تولید می‌کند که در بازه مشخص شده قرار دارد. این طراحی اجازه می‌دهد که مقادیر تولید شده به صورت قابل پیش‌بینی و هماهنگ با نیازها و اجرای برنامه باشند، حتی وقتی محدوده‌های متغیرها متنوع یا زیاد باشند.

متد resolve قابلیت تنظیم seed محلی برای تولید مقادیر قابل تکرار را دارد و این قابلیت باعث می‌شود هر بار که همان seed استفاده شود، خروجی‌های تولید شده دقیقاً یکسان باشند. اگر seed محلی مشخص نشده باشد، کلاس به طور خودکار از seed سراسری تعریف شده در پیکربندی استفاده می‌کند تا همچنان دترمنیستیک بودن حفظ شود.

در هنگام تولید مقادیر، هر مقدار با استفاده از round و تعداد رقم‌های اعشار مشخص شده در پیکربندی گرد می‌شود تا دقت عددی و قالب‌بندی عدد حفظ شود. این مسئله برای اطمینان از اینکه مقادیر تولید شده در محاسبات بعدی با دقت معین استفاده شوند بسیار مهم است و باعث می‌شود نتایج محاسباتی در تست‌ها یا رندرینگ سوالات دقیق و یکسان باقی بمانند. به عنوان مثال، اگر محدوده متغیرها به شکل {'x': (1, 5), 'y': (10, 20)} باشد و seed برابر با 42 تنظیم شود، فراخوانی VariableResolver({'x': (1,5), 'y':(10,20)}).resolve(seed=42) مقدار مشخص و تکرارپذیر مثل {'x': 3.14, 'y': 16.27} تولید می‌کند و هر بار با همان seed، همان مقادیر بازمی‌گردند.

import random
from typing import Dict, Any, Optional, Tuple
from utils.config_loader import ConfigLoader

config = ConfigLoader()
GLOBAL_SEED = config.get("random_seed", None)
DECIMAL_PLACES = config.get("decimal_places", 3)
CONFIG_RANGES = config.get("variable_ranges", {})

class VariableResolver:
    def __init__(self, ranges: Optional[Dict[str, Tuple[float, float]]] = None):
        pass

    def resolve(self, seed: Optional[int] = None) -> Dict[str, Any]:
        pass

  • به‌عنوان مثال، فرض کنید یک سوال با stem و گزینه‌ها داریم که شامل placeholderهای ریاضی است:
class DummyQuestion:
    stem = "Compute {{x}} + {{y}}"
    options = ["Sum={{x+y}}", "Double={{x*2}}"]

r = Renderer()
rendered = r.render(DummyQuestion(), {'x': 2, 'y': 3})
# rendered['stem'] باید برابر "Compute 2 + 3" باشد
# rendered['options'] باید برابر ["Sum=5", "Double=4"] باشد

+‌ در این مثال، Renderer ابتدا placeholderهای stem را پردازش می‌کند و مقادیر x و y را جایگزین می‌کند. سپس گزینه‌ها را پردازش می‌کند، به طوری که محاسبات داخلی هر گزینه (x+y و x*2) انجام شده و خروجی نهایی جایگزین متن placeholder می‌شود. اگر پاسخ نهایی دارای واحد یا نیاز به گرد کردن باشد، کلاس به‌طور خودکار آن را مدیریت می‌کند.

به طور خلاصه، Renderer مسئول یکپارچه‌سازی و پردازش تمام متون و داده‌های سوال است، به طوری که خروجی دترمنیستیک، دقیق و آماده استفاده در نمایش یا ارزیابی باشد.

پیاده‌سازی پوشه models

توضیحات کلاس BaseQuestion از فایل base_question.py

کلاس BaseQuestion به عنوان یک کلاس انتزاعی طراحی شده تا چارچوب و رابطی مشترک برای تمامی انواع سوالات را فراهم کند. این کلاس شامل متغیرهای پایه‌ای مانند template برای نگهداری ساختار سوال و variable_spec برای تعریف محدوده یا نوع متغیرهای استفاده شده در سوال است. متد to_dict امکان تبدیل سوال به یک دیکشنری استاندارد را فراهم می‌کند تا بتوان آن را ذخیره یا ارسال کرد و متد generate_variables با استفاده از کلاس VariableResolver مقادیر متغیرهای مورد نیاز برای تولید یک نمونه دترمنیستیک از سوال را فراهم می‌کند.

بخش مهم دیگر، متدهای انتزاعی render_variants و validate_template هستند که در کلاس پایه تعریف شده‌اند اما پیاده‌سازی آن‌ها برعهده کلاس‌های فرزند است. render_variants وظیفه تولید یک یا چند نسخه از سوال با مقادیر متفاوت متغیرها را بر اساس variable_spec دارد، در حالی که validate_template مسئول بررسی صحت و کامل بودن قالب سوال است. کلاس‌های فرزند با ارث‌بری از BaseQuestion موظف هستند این دو متد را پیاده‌سازی کنند تا مطمئن شویم که هر نوع سوال جدید قابلیت تولید نمونه‌ها و اعتبارسنجی مانیفست را به طور دترمنیستیک و استاندارد دارد.

from typing import Dict, Any, List
from abc import ABC, abstractmethod
from core.variable_resolver import VariableResolver

class BaseQuestion(ABC):
    def __init__(self, template: Dict[str, Any], variable_spec: Dict[str, Dict[str, Any]]):
        self.template = template
        self.variable_spec = variable_spec

    @abstractmethod
    def render_variants(self, num_variants: int = 1) -> List[Dict[str, Any]]:
        pass

    @abstractmethod
    def validate_template(self) -> None:
        pass

    def to_dict(self) -> Dict[str, Any]:
        return {
            'template': self.template,
            'variable_spec': self.variable_spec
        }
    
    def generate_variables(self) -> Dict[str, Any]:
        resolver = VariableResolver(self.variable_spec)
        return resolver.resolve()

پیاده سازی کلاس MultipleChoiceQuestion از فایل mcq.py

کلاس MultipleChoiceQuestion یک پیاده‌سازی خاص از کلاس انتزاعی BaseQuestion است که مخصوص سوالات چندگزینه‌ای طراحی شده است. متد validate_template تضمین می‌کند که قالب سوال شامل حداقل یک stem و یک لیست options باشد و در صورت نبود یا نادرستی این بخش‌ها خطا صادر می‌کند. این متد در کلاس‌های فرزند باید همیشه پیاده‌سازی شود تا قبل از تولید نمونه‌های سوال، اعتبار قالب بررسی شود و از تولید خروجی نادرست جلوگیری گردد.

from .base_question import BaseQuestion

class MultipleChoiceQuestion(BaseQuestion):
    pass

متد render_variants مسئول تولید یک یا چند نسخه از سوال با مقادیر متفاوت متغیرها است. در این متد ابتدا قالب اعتبارسنجی می‌شود، سپس با استفاده از VariableResolver مقادیر متغیرها تولید و با PlaceholderProcessor در متن stem و گزینه‌ها جایگذاری می‌شوند. خروجی یک لیست از دیکشنری‌هاست که شامل stem، options و مقادیر متغیرهای استفاده شده است.

به عنوان مثال، اگر مانیفست سوال به شکل زیر باشد:

template = {
    'stem': "What is {{a}} + {{b}}?",
    'options': ["{{a+b}}", "{{a*b}}", "{{a-b}}"]
}
variable_spec = {'a': (1, 3), 'b': (2, 4)}
mcq = MultipleChoiceQuestion(template, variable_spec)
variants = mcq.render_variants(num_variants=2)
  • در این مثال، render_variants دو نسخه از سوال تولید می‌کند، هر نسخه دارای stem و گزینه‌های جایگذاری‌شده با مقادیر واقعی متغیرها خواهد بود.

پیاده سازی کلاس NumericRangeQuestion از فایل numeric_range.py

کلاس NumericRangeQuestion یک پیاده‌سازی از کلاس انتزاعی BaseQuestion است که برای سوالاتی با پاسخ عددی در بازه‌ای مشخص طراحی شده است. متد validate_template بررسی می‌کند که قالب سوال شامل حداقل یک stem و یک answer باشد و در صورت نبود یا نادرستی این بخش‌ها خطا صادر می‌کند. این متد تضمین می‌کند که قبل از تولید نسخه‌های سوال، قالب اعتبارسنجی شده و از تولید خروجی نادرست جلوگیری شود.

from .base_question import BaseQuestion

class NumericRangeQuestion(BaseQuestion):
    pass

متد render_variants وظیفه تولید یک یا چند نسخه دترمنیستیک از سوال را بر عهده دارد. در این متد ابتدا قالب اعتبارسنجی می‌شود، سپس با استفاده از VariableResolver مقادیر متغیرها تولید و با PlaceholderProcessor در متن stem و مقدار answer جایگذاری می‌شوند. اگر جایگذاری مقدار answer به هر دلیلی ناموفق باشد، مقدار پیش‌فرض answer استفاده می‌شود. خروجی یک لیست از دیکشنری‌هاست که شامل stem، answer، tolerance و مقادیر متغیرهای استفاده شده است.

به عنوان مثال، اگر مانیفست سوال به شکل زیر باشد:

template = {
    'stem': "Compute {{x}} + {{y}}",
    'answer': "{{x + y}}",
    'tolerance': 0.01
}
variable_spec = {'x': (1, 5), 'y': (2, 6)}
num_question = NumericRangeQuestion(template, variable_spec)
variants = num_question.render_variants(num_variants=2)
  • در این مثال، render_variants دو نسخه از سوال تولید می‌کند، هر نسخه شامل stem جایگذاری‌شده با مقادیر واقعی متغیرها و answer محاسبه‌شده با همان مقادیر خواهد بود و می‌توان از آن برای تولید سوالات دترمنیستیک با پاسخ عددی و محدوده‌ی مجاز استفاده کرد.

پیاده سازی کلاس ShortAnswerQuestion از فایل short_answer.py

کلاس ShortAnswerQuestion یک پیاده‌سازی از کلاس انتزاعی BaseQuestion است که برای سوالاتی با پاسخ کوتاه طراحی شده و می‌تواند هم پاسخ‌های عددی و هم متنی را مدیریت کند. متد validate_template ابتدا بررسی می‌کند که قالب سوال شامل حداقل یک stem و یک answer باشد و در صورت نبود یا نادرستی این بخش‌ها، خطا صادر می‌کند. این مرحله اعتبارسنجی تضمین می‌کند که قبل از تولید نسخه‌های سوال، قالب درست باشد و از تولید خروجی نامعتبر جلوگیری شود.

from .base_question import BaseQuestion

class ShortAnswerQuestion(BaseQuestion):
    pass

متد render_variants وظیفه تولید یک یا چند نسخه دترمنیستیک از سوال را بر عهده دارد. در این متد ابتدا قالب اعتبارسنجی می‌شود، سپس با استفاده از VariableResolver مقادیر متغیرها تولید می‌شوند و با PlaceholderProcessor در متن stem و مقدار answer جایگذاری می‌شوند. اگر پاسخ قابل تبدیل به عدد باشد، از RegexGenerator.numeric_regex برای تولید یک الگوی عددی استفاده می‌شود و در غیر این صورت از RegexGenerator.text_regex برای تولید الگوی متنی بهره گرفته می‌شود. خروجی یک لیست از دیکشنری‌هاست که شامل stem جایگذاری‌شده، answer محاسبه‌شده یا متنی، regex برای بررسی پاسخ و مقادیر متغیرهای استفاده شده است.

به عنوان مثال، اگر مانیفست سوال به شکل زیر باشد:

template = {
    'stem': "Enter the sum of {{x}} and {{y}}",
    'answer': "{{x + y}}"
}
variable_spec = {'x': (1, 5), 'y': (2, 6)}
sa_question = ShortAnswerQuestion(template, variable_spec)
variants = sa_question.render_variants(num_variants=2)
  • در این مثال، render_variants دو نسخه از سوال تولید می‌کند، هر نسخه شامل stem جایگذاری‌شده با مقادیر واقعی متغیرها و answer محاسبه‌شده است. اگر پاسخ عددی باشد، یک regex مناسب برای بررسی پاسخ‌های عددی ایجاد می‌شود و اگر پاسخ متنی باشد، regex برای مقایسه‌ی متن به کار می‌رود. این مکانیزم اطمینان می‌دهد که سوالات کوتاه پاسخ هم دترمنیستیک و هم قابل اعتبارسنجی باشند.

پیاده سازی کلاس TrueFalseQuestion از فایل true_false.py

کلاس TrueFalseQuestion پیاده‌سازی ویژه‌ای از BaseQuestion است که برای سوالات درست/نادرست طراحی شده و می‌تواند مقادیر متغیرها را در متن سوال جایگذاری کند. متد validate_template بررسی می‌کند که قالب شامل حداقل یک stem و یک answer باشد و در صورت نبود آن‌ها خطا صادر می‌کند. این اعتبارسنجی تضمین می‌کند که قبل از تولید نسخه‌های سوال، قالب به صورت دترمنیستیک معتبر باشد و از تولید خروجی نامعتبر جلوگیری شود.

from .base_question import BaseQuestion

class TrueFalseQuestion(BaseQuestion):
    pass

متد render_variants وظیفه تولید یک یا چند نسخه از سوال را بر اساس مقادیر متغیرها بر عهده دارد. ابتدا قالب اعتبارسنجی می‌شود، سپس با VariableResolver مقادیر متغیرها تولید می‌شوند و با PlaceholderProcessor در متن stem جایگذاری می‌شوند. پاسخ درست/نادرست (answer) به همان شکل از قالب گرفته می‌شود و در خروجی قرار می‌گیرد. در نهایت، خروجی یک لیست از دیکشنری‌هاست که شامل stem جایگذاری‌شده، پاسخ و مقادیر متغیرهاست تا سوالات تولید شده قابل استفاده و بررسی باشند.

به عنوان مثال، اگر مانیفست سوال به شکل زیر باشد:

template = {
    'stem': "Is {{x}} greater than {{y}}?",
    'answer': True
}
variable_spec = {'x': (1, 10), 'y': (1, 10)}
tf_question = TrueFalseQuestion(template, variable_spec)
variants = tf_question.render_variants(num_variants=2)
  • در این مثال، render_variants دو نسخه از سوال تولید می‌کند، هر نسخه شامل متن سوال جایگذاری‌شده با مقادیر واقعی متغیرها و پاسخ درست/نادرست مشخص شده در قالب است. این روش اطمینان می‌دهد که سوالات درست/نادرست دترمنیستیک باشند و مقادیر متغیرها به درستی در متن سوال منعکس شوند.

پیاده سازی کلاس MatchingQuestion از فایل matching.py

کلاس MatchingQuestion یک پیاده‌سازی از BaseQuestion است که برای سوالات تطبیقی طراحی شده و امکان جایگذاری مقادیر متغیرها در متن سوال و جفت‌های تطبیقی را فراهم می‌کند. متد validate_template بررسی می‌کند که قالب شامل حداقل یک stem و یک لیست pairs باشد و در صورت نبود آن‌ها خطا صادر می‌کند. این اطمینان می‌دهد که قالب دترمنیستیک و معتبر است و قبل از تولید نسخه‌های سوال هیچ داده ناقصی وارد فرآیند تولید نمی‌شود.

from .base_question import BaseQuestion

class MatchingQuestion(BaseQuestion):
    pass

متد render_variants وظیفه تولید یک یا چند نسخه از سوال تطبیقی را بر اساس مقادیر متغیرها بر عهده دارد. ابتدا قالب اعتبارسنجی می‌شود، سپس با استفاده از VariableResolver مقادیر متغیرها تولید می‌شوند و با PlaceholderProcessor در متن stem و در هر جفت left و right جایگذاری می‌شوند. خروجی یک لیست از دیکشنری‌هاست که شامل متن جایگذاری‌شده، جفت‌های تطبیقی جایگذاری‌شده و مقادیر متغیرهاست تا نسخه‌های تولید شده قابل استفاده و قابل بررسی باشند.

به عنوان مثال، اگر مانیفست سوال به شکل زیر باشد:

template = {
    'stem': "Match the capitals with their countries:",
    'pairs': [
        {"left": "{{country1}}", "right": "{{capital1}}"},
        {"left": "{{country2}}", "right": "{{capital2}}"}
    ]
}
variable_spec = {
    'country1': ('France', 'France'),
    'capital1': ('Paris', 'Paris'),
    'country2': ('Germany', 'Germany'),
    'capital2': ('Berlin', 'Berlin')
}
matching_question = MatchingQuestion(template, variable_spec)
variants = matching_question.render_variants(num_variants=1)
  • در این مثال، render_variants یک نسخه از سوال تولید می‌کند که متن سوال و همه جفت‌ها با مقادیر واقعی متغیرها جایگذاری شده‌اند. این روش تضمین می‌کند که سوالات تطبیقی دترمنیستیک باشند و تمامی placeholderها به درستی با مقادیر مشخص شده جایگزین شوند.

پیاده سازی کلاس QuestionBank از فایل question_bank.py

کلاس QuestionBank مسئول مدیریت مجموعه‌ای از قالب‌های سوال است و وظیفه آن بارگذاری فایل‌های JSON شامل سوالات مختلف و نگهداری آن‌ها در یک دیکشنری داخلی است. هر فایل JSON باید شامل نوع سوال (typeشناسه یکتا (idقالب سوال (template) و مشخصات متغیرها (variables) باشد. هنگام بارگذاری، QuestionBank بر اساس نوع سوال کلاس مناسب را انتخاب می‌کند و نمونه‌ای از آن کلاس را با قالب و متغیرهای مشخص‌شده ایجاد می‌کند. قالب‌های با نوع ناشناخته یا بدون شناسه نادیده گرفته می‌شوند و بارگذاری فقط شامل فایل‌های JSON معتبر است.

class QuestionBank:
    pass

پس از بارگذاری، تمامی سوالات در دیکشنری self.templates نگهداری می‌شوند تا دسترسی سریع به آن‌ها ممکن باشد. متد get امکان دسترسی به یک سوال مشخص بر اساس شناسه آن را فراهم می‌کند و در صورت نبود شناسه موردنظر، مقدار None برمی‌گرداند. این ساختار اطمینان می‌دهد که تمام قالب‌های معتبر با نوع مشخص و شناسه یکتا در بانک سوال قابل دسترس باشند و هرگونه خطا یا داده ناقص هنگام بارگذاری مدیریت شود.

به عنوان مثال، فرض کنید مجموعه‌ای از فایل‌های JSON در مسیر questions/ داریم و می‌خواهیم سوال با شناسه Q101 را بارگذاری کنیم:

qb = QuestionBank("questions/")
question = qb.get("Q101")
if question:
    variants = question.render_variants(num_variants=3)
    for v in variants:
        print(v['stem'])
  • در این مثال، QuestionBank ابتدا فایل‌ها را بررسی می‌کند، قالب‌ها و متغیرها را ایجاد می‌کند، سپس با فراخوانی get سوال مشخص را برمی‌گرداند و امکان تولید نسخه‌های متعدد از هر سوال با مقادیر متغیر مشخص فراهم باشد.

زیرمسئله‌ها

سیستم داوری برای این سوال به زیرمسئله‌های زیر برای نمره‌دهی تقسیم‌بندی شده است که می‌توانید امتیاز مربوط به هر کدام را در جدول زیر مشاهده کنید. زیرمسئله‌های این جدول ابتدا بر اساس اولویت و پیشنیازی پیاده‌سازی و سپس بر اساس امتیاز آن‌ها مرتب‌سازی شده‌اند. لذا پیشنهاد می‌شود در پیاده‌سازی از زیرمسئله‌ی ابتدایی آغاز کنید.

زیرمسئله امتیاز
پیاده‌سازی بخش core 203
پیاده‌سازی بخش models 147

آن‌چه باید آپلود کنید

  • توجه: پس از پیاده‌سازی موارد خواسته شده، کل فایل‌های پروژه را زیپ کرده و ارسال کنید.
  • توجه: شما مجاز به افزودن فایل جدیدی در این ساختار نیستید و تنها باید تغییرات را در فایل‌های موجود اعمال کنید.
  • توجه: که نام فایل Zip اهمیتی ندارد.
ارسال پاسخ برای این سؤال
فایلی انتخاب نشده است.