Engmhabib's picture
Upload 30 files
942216e verified
"""
EdTech Extensions - Assessment and Feedback System
This module provides functionality for creating and managing assessments,
as well as generating personalized feedback for students.
"""
from typing import Dict, List, Optional, Any, Union
import os
import json
from datetime import datetime
from enum import Enum
from uuid import uuid4
from pydantic import BaseModel, Field
class QuestionType(str, Enum):
"""Types of questions supported in assessments."""
MULTIPLE_CHOICE = "multiple_choice"
TRUE_FALSE = "true_false"
SHORT_ANSWER = "short_answer"
ESSAY = "essay"
MATCHING = "matching"
FILL_BLANK = "fill_blank"
class Question(BaseModel):
"""Model representing an assessment question."""
id: str = Field(..., description="Unique identifier for this question")
type: QuestionType = Field(..., description="Type of question")
text: str = Field(..., description="Question text")
options: List[Dict[str, Any]] = Field(default_factory=list, description="Answer options for multiple choice, etc.")
correct_answer: Any = Field(..., description="Correct answer or answers")
points: int = Field(1, description="Points for this question")
difficulty: str = Field("medium", description="Question difficulty")
tags: List[str] = Field(default_factory=list, description="Tags for categorizing questions")
explanation: str = Field("", description="Explanation of the correct answer")
class Assessment(BaseModel):
"""Model representing an assessment."""
id: str = Field(..., description="Unique identifier for this assessment")
title: str = Field(..., description="Assessment title")
description: str = Field("", description="Assessment description")
questions: List[Question] = Field(..., description="Questions in this assessment")
time_limit_minutes: Optional[int] = Field(None, description="Time limit in minutes, if any")
passing_score: float = Field(70.0, description="Passing score percentage")
tags: List[str] = Field(default_factory=list, description="Tags for categorizing assessments")
learning_objectives: List[str] = Field(default_factory=list, description="Learning objectives covered")
created_at: str = Field(..., description="Creation timestamp")
class StudentResponse(BaseModel):
"""Model representing a student's response to a question."""
question_id: str = Field(..., description="ID of the question")
student_answer: Any = Field(..., description="Student's answer")
is_correct: Optional[bool] = Field(None, description="Whether the answer is correct")
points_earned: Optional[float] = Field(None, description="Points earned for this response")
feedback: str = Field("", description="Feedback on this response")
class AssessmentSubmission(BaseModel):
"""Model representing a student's assessment submission."""
id: str = Field(..., description="Unique identifier for this submission")
assessment_id: str = Field(..., description="ID of the assessment")
student_id: str = Field(..., description="ID of the student")
responses: List[StudentResponse] = Field(..., description="Student's responses")
start_time: str = Field(..., description="Start timestamp")
end_time: str = Field(..., description="End timestamp")
score: Optional[float] = Field(None, description="Overall score")
feedback: str = Field("", description="Overall feedback")
graded: bool = Field(False, description="Whether the submission has been graded")
class AssessmentSystem:
"""
Manages assessments and student submissions.
This class provides functionality for creating, retrieving, and grading
assessments, as well as generating personalized feedback.
"""
def __init__(self, storage_dir: str = "./storage/assessments"):
"""
Initialize a new AssessmentSystem.
Args:
storage_dir: Directory to store assessment data in.
"""
self.storage_dir = storage_dir
self.assessments_dir = os.path.join(storage_dir, "definitions")
self.submissions_dir = os.path.join(storage_dir, "submissions")
os.makedirs(self.assessments_dir, exist_ok=True)
os.makedirs(self.submissions_dir, exist_ok=True)
def _get_assessment_path(self, assessment_id: str) -> str:
"""
Get the file path for an assessment.
Args:
assessment_id: The ID of the assessment.
Returns:
The file path for the assessment.
"""
return os.path.join(self.assessments_dir, f"{assessment_id}.json")
def _get_submission_path(self, submission_id: str) -> str:
"""
Get the file path for a submission.
Args:
submission_id: The ID of the submission.
Returns:
The file path for the submission.
"""
return os.path.join(self.submissions_dir, f"{submission_id}.json")
async def create_assessment(
self,
title: str,
questions: List[Question],
description: str = "",
time_limit_minutes: Optional[int] = None,
passing_score: float = 70.0,
tags: List[str] = None,
learning_objectives: List[str] = None
) -> Assessment:
"""
Create a new assessment.
Args:
title: Assessment title.
questions: List of questions.
description: Assessment description.
time_limit_minutes: Time limit in minutes, if any.
passing_score: Passing score percentage.
tags: Tags for categorizing the assessment.
learning_objectives: Learning objectives covered.
Returns:
The newly created Assessment.
"""
assessment_id = str(uuid4())
created_at = datetime.utcnow().isoformat()
assessment = Assessment(
id=assessment_id,
title=title,
description=description,
questions=questions,
time_limit_minutes=time_limit_minutes,
passing_score=passing_score,
tags=tags or [],
learning_objectives=learning_objectives or [],
created_at=created_at
)
# Save to disk
await self._save_assessment(assessment)
return assessment
async def _save_assessment(self, assessment: Assessment) -> bool:
"""
Save an assessment to disk.
Args:
assessment: The Assessment to save.
Returns:
True if the save was successful, False otherwise.
"""
try:
with open(self._get_assessment_path(assessment.id), 'w') as f:
json.dump(assessment.dict(), f, indent=2)
return True
except Exception as e:
print(f"Error saving assessment: {e}")
return False
async def get_assessment(self, assessment_id: str) -> Optional[Assessment]:
"""
Get an assessment by ID.
Args:
assessment_id: The ID of the assessment.
Returns:
The Assessment if found, None otherwise.
"""
try:
path = self._get_assessment_path(assessment_id)
if not os.path.exists(path):
return None
with open(path, 'r') as f:
data = json.load(f)
return Assessment(**data)
except Exception as e:
print(f"Error loading assessment: {e}")
return None
async def list_assessments(
self,
tags: Optional[List[str]] = None,
learning_objectives: Optional[List[str]] = None
) -> List[Assessment]:
"""
List assessments, optionally filtered.
Args:
tags: Optional filter by tags.
learning_objectives: Optional filter by learning objectives.
Returns:
List of Assessment objects.
"""
assessments = []
try:
for filename in os.listdir(self.assessments_dir):
if filename.endswith('.json'):
path = os.path.join(self.assessments_dir, filename)
with open(path, 'r') as f:
data = json.load(f)
assessment = Assessment(**data)
# Apply filters
include = True
if tags:
# Include if any tag matches
tag_match = any(tag in assessment.tags for tag in tags)
if not tag_match:
include = False
if learning_objectives:
# Include if any learning objective matches
obj_match = any(obj in assessment.learning_objectives for obj in learning_objectives)
if not obj_match:
include = False
if include:
assessments.append(assessment)
except Exception as e:
print(f"Error listing assessments: {e}")
return assessments
async def submit_assessment(
self,
assessment_id: str,
student_id: str,
responses: List[StudentResponse]
) -> AssessmentSubmission:
"""
Submit an assessment.
Args:
assessment_id: ID of the assessment.
student_id: ID of the student.
responses: List of student responses.
Returns:
The AssessmentSubmission.
"""
submission_id = str(uuid4())
start_time = datetime.utcnow().isoformat()
end_time = datetime.utcnow().isoformat()
submission = AssessmentSubmission(
id=submission_id,
assessment_id=assessment_id,
student_id=student_id,
responses=responses,
start_time=start_time,
end_time=end_time,
score=None,
feedback="",
graded=False
)
# Save to disk
await self._save_submission(submission)
# Grade the submission
graded_submission = await self.grade_submission(submission_id)
return graded_submission
async def _save_submission(self, submission: AssessmentSubmission) -> bool:
"""
Save a submission to disk.
Args:
submission: The AssessmentSubmission to save.
Returns:
True if the save was successful, False otherwise.
"""
try:
with open(self._get_submission_path(submission.id), 'w') as f:
json.dump(submission.dict(), f, indent=2)
return True
except Exception as e:
print(f"Error saving submission: {e}")
return False
async def get_submission(self, submission_id: str) -> Optional[AssessmentSubmission]:
"""
Get a submission by ID.
Args:
submission_id: The ID of the submission.
Returns:
The AssessmentSubmission if found, None otherwise.
"""
try:
path = self._get_submission_path(submission_id)
if not os.path.exists(path):
return None
with open(path, 'r') as f:
data = json.load(f)
return AssessmentSubmission(**data)
except Exception as e:
print(f"Error loading submission: {e}")
return None
async def grade_submission(self, submission_id: str) -> Optional[AssessmentSubmission]:
"""
Grade a submission.
Args:
submission_id: The ID of the submission.
Returns:
The graded AssessmentSubmission if found, None otherwise.
"""
submission = await self.get_submission(submission_id)
if not submission:
return None
assessment = await self.get_assessment(submission.assessment_id)
if not assessment:
return None
# Create a map of questions by ID for easy lookup
questions_map = {q.id: q for q in assessment.questions}
total_points = 0
earned_points = 0
# Grade each response
for response in submission.responses:
question = questions_map.get(response.question_id)
if not question:
continue
total_points += question.points
# Grade based on question type
if question.type == QuestionType.MULTIPLE_CHOICE or question.type == QuestionType.TRUE_FALSE:
# Simple equality check
is_correct = response.student_answer == question.correct_answer
points_earned = question.points if is_correct else 0
response.is_correct = is_correct
response.points_earned = points_earned
response.feedback = question.explanation if question.explanation else (
"Correct!" if is_correct else f"Incorrect. The correct answer is: {question.correct_answer}"
)
elif question.type == QuestionType.FILL_BLANK:
# Case-insensitive equality check
is_correct = str(response.student_answer).lower() == str(question.correct_answer).lower()
points_earned = question.points if is_correct else 0
response.is_correct = is_correct
response.points_earned = points_earned
response.feedback = question.explanation if question.explanation else (
"Correct!" if is_correct else f"Incorrect. The correct answer is: {question.correct_answer}"
)
elif question.type == QuestionType.SHORT_ANSWER:
# For short answer, we'll use a simple keyword check
# In a real implementation, this would use more sophisticated NLP techniques
student_answer = str(response.student_answer).lower()
correct_keywords = [str(k).lower() for k in question.correct_answer]
# Check how many keywords are present
matches = sum(1 for k in correct_keywords if k in student_answer)
match_ratio = matches / len(correct_keywords) if correct_keywords else 0
# Full points if all keywords present, partial otherwise
is_correct = match_ratio >= 0.7 # 70% or more keywords
points_earned = question.points * match_ratio
response.is_correct = is_correct
response.points_earned = points_earned
response.feedback = question.explanation if question.explanation else (
"Good answer!" if is_correct else
f"Your answer could be improved. Important concepts to include: {', '.join(correct_keywords)}"
)
elif question.type == QuestionType.ESSAY:
# For essays, we'll mark as needing manual grading
# In a real implementation, this might use AI for initial grading
response.is_correct = None
response.points_earned = None
response.feedback = "This response requires manual grading."
elif question.type == QuestionType.MATCHING:
# Check if all matches are correct
correct_matches = question.correct_answer
student_matches = response.student_answer
# Count correct matches
correct_count = sum(1 for k, v in student_matches.items() if k in correct_matches and correct_matches[k] == v)
total_matches = len(correct_matches)
match_ratio = correct_count / total_matches if total_matches else 0
is_correct = match_ratio >= 0.7 # 70% or more correct
points_earned = question.points * match_ratio
response.is_correct = is_correct
response.points_earned = points_earned
response.feedback = question.explanation if question.explanation else (
"Good matching!" if is_correct else
f"Some matches were incorrect. Please review the correct answers."
)
# Add to total points
if response.points_earned is not None:
earned_points += response.points_earned
# Calculate overall score
score_percentage = (earned_points / total_points * 100) if total_points > 0 else 0
# Generate overall feedback
if score_percentage >= assessment.passing_score:
overall_feedback = f"Congratulations! You passed with a score of {score_percentage:.1f}%."
else:
overall_feedback = f"You scored {score_percentage:.1f}%, which is below the passing score of {assessment.passing_score}%. Please review the material and try again."
# Update submission
submission.score = score_percentage
submission.feedback = overall_feedback
submission.graded = True
# Save updated submission
await self._save_submission(submission)
return submission
async def list_student_submissions(self, student_id: str) -> List[AssessmentSubmission]:
"""
List submissions for a student.
Args:
student_id: ID of the student.
Returns:
List of AssessmentSubmission objects.
"""
submissions = []
try:
for filename in os.listdir(self.submissions_dir):
if filename.endswith('.json'):
path = os.path.join(self.submissions_dir, filename)
with open(path, 'r') as f:
data = json.load(f)
if data.get("student_id") == student_id:
submissions.append(AssessmentSubmission(**data))
except Exception as e:
print(f"Error listing submissions: {e}")
# Sort by end time, newest first
submissions.sort(key=lambda x: x.end_time, reverse=True)
return submissions
async def generate_feedback_report(self, submission_id: str) -> Dict[str, Any]:
"""
Generate a detailed feedback report for a submission.
Args:
submission_id: ID of the submission.
Returns:
Dictionary containing the feedback report.
"""
submission = await self.get_submission(submission_id)
if not submission:
return {"error": "Submission not found"}
assessment = await self.get_assessment(submission.assessment_id)
if not assessment:
return {"error": "Assessment not found"}
# Create a map of questions by ID for easy lookup
questions_map = {q.id: q for q in assessment.questions}
# Group questions and responses by tags
tag_groups = {}
for response in submission.responses:
question = questions_map.get(response.question_id)
if not question:
continue
for tag in question.tags:
if tag not in tag_groups:
tag_groups[tag] = {
"total": 0,
"correct": 0,
"questions": []
}
tag_groups[tag]["total"] += 1
if response.is_correct:
tag_groups[tag]["correct"] += 1
tag_groups[tag]["questions"].append({
"question_text": question.text,
"student_answer": response.student_answer,
"correct_answer": question.correct_answer,
"is_correct": response.is_correct,
"feedback": response.feedback
})
# Generate strengths and weaknesses
strengths = []
weaknesses = []
for tag, data in tag_groups.items():
score = data["correct"] / data["total"] if data["total"] > 0 else 0
if score >= 0.8: # 80% or better
strengths.append({
"topic": tag,
"score": score * 100,
"comment": f"You show strong understanding of {tag}."
})
elif score <= 0.6: # 60% or worse
weaknesses.append({
"topic": tag,
"score": score * 100,
"comment": f"You may need additional practice with {tag}."
})
# Generate recommended resources (in a real app, these would come from a database)
recommended_resources = []
for weakness in weaknesses:
recommended_resources.append({
"topic": weakness["topic"],
"title": f"Review material on {weakness['topic']}",
"type": "article",
"url": f"https://example.com/resources/{weakness['topic'].lower().replace(' ', '-')}"
})
# Create the report
report = {
"submission_id": submission_id,
"assessment_id": submission.assessment_id,
"assessment_title": assessment.title,
"student_id": submission.student_id,
"score": submission.score,
"passing_score": assessment.passing_score,
"passed": submission.score >= assessment.passing_score if submission.score is not None else False,
"strengths": strengths,
"weaknesses": weaknesses,
"topic_breakdown": [
{
"topic": tag,
"score": (data["correct"] / data["total"] * 100) if data["total"] > 0 else 0,
"questions_count": data["total"]
}
for tag, data in tag_groups.items()
],
"recommended_resources": recommended_resources,
"detailed_feedback": submission.feedback,
"generated_at": datetime.utcnow().isoformat()
}
return report