MCP-EdTech / src /edtech_extensions /progress_tracking.py
Engmhabib's picture
Upload 30 files
942216e verified
"""
EdTech Extensions - Learning Progress Tracking
This module provides functionality for tracking student learning progress
in the MCP EdTech system, including assessment results and skill development.
"""
import os
from datetime import datetime
from typing import Dict, List, Optional, Any, Set, Tuple
from uuid import uuid4
import json
from enum import Enum
from pydantic import BaseModel, Field
class AssessmentType(str, Enum):
"""Types of assessments supported by the system."""
QUIZ = "quiz"
TEST = "test"
ASSIGNMENT = "assignment"
PROJECT = "project"
PRACTICE = "practice"
class AssessmentResult(BaseModel):
"""Model representing an assessment result."""
id: str = Field(..., description="Unique identifier for this assessment result")
student_id: str = Field(..., description="ID of the student")
assessment_type: AssessmentType = Field(..., description="Type of assessment")
title: str = Field(..., description="Title of the assessment")
score: float = Field(..., description="Score achieved (0-100)")
max_score: float = Field(..., description="Maximum possible score")
objectives: List[str] = Field(default_factory=list, description="Learning objectives covered")
completed_at: str = Field(..., description="Completion timestamp")
feedback: str = Field("", description="Feedback on the assessment")
details: Dict[str, Any] = Field(default_factory=dict, description="Detailed assessment data")
class SkillLevel(str, Enum):
"""Skill proficiency levels."""
NOVICE = "novice"
BEGINNER = "beginner"
INTERMEDIATE = "intermediate"
ADVANCED = "advanced"
EXPERT = "expert"
class SkillProgress(BaseModel):
"""Model representing progress in a specific skill."""
skill_id: str = Field(..., description="Unique identifier for this skill")
name: str = Field(..., description="Name of the skill")
level: SkillLevel = Field(SkillLevel.NOVICE, description="Current skill level")
progress_percentage: float = Field(0.0, description="Progress towards next level (0-100)")
last_updated: str = Field(..., description="Last update timestamp")
related_objectives: List[str] = Field(default_factory=list, description="Related learning objectives")
class ProgressTracker:
"""
Tracks student learning progress in the MCP EdTech system.
This class provides functionality for recording and analyzing student
progress, including assessment results and skill development.
"""
def __init__(self, storage_dir: str = "./storage/progress"):
"""
Initialize a new ProgressTracker.
Args:
storage_dir: Directory to store progress data in.
"""
self.storage_dir = storage_dir
self.assessments_dir = os.path.join(storage_dir, "assessments")
self.skills_dir = os.path.join(storage_dir, "skills")
os.makedirs(self.assessments_dir, exist_ok=True)
os.makedirs(self.skills_dir, exist_ok=True)
def _get_student_assessments_dir(self, student_id: str) -> str:
"""
Get the directory for a student's assessment results.
Args:
student_id: The ID of the student.
Returns:
The directory path.
"""
path = os.path.join(self.assessments_dir, student_id)
os.makedirs(path, exist_ok=True)
return path
def _get_student_skills_dir(self, student_id: str) -> str:
"""
Get the directory for a student's skill progress.
Args:
student_id: The ID of the student.
Returns:
The directory path.
"""
path = os.path.join(self.skills_dir, student_id)
os.makedirs(path, exist_ok=True)
return path
async def record_assessment(
self,
student_id: str,
assessment_type: AssessmentType,
title: str,
score: float,
max_score: float,
objectives: List[str],
feedback: str = "",
details: Dict[str, Any] = None
) -> AssessmentResult:
"""
Record a new assessment result.
Args:
student_id: ID of the student.
assessment_type: Type of assessment.
title: Title of the assessment.
score: Score achieved.
max_score: Maximum possible score.
objectives: Learning objectives covered.
feedback: Feedback on the assessment.
details: Detailed assessment data.
Returns:
The newly created AssessmentResult.
"""
assessment_id = str(uuid4())
completed_at = datetime.utcnow().isoformat()
result = AssessmentResult(
id=assessment_id,
student_id=student_id,
assessment_type=assessment_type,
title=title,
score=score,
max_score=max_score,
objectives=objectives,
completed_at=completed_at,
feedback=feedback,
details=details or {}
)
# Save to disk
await self._save_assessment(result)
# Update skill progress based on assessment
for objective_id in objectives:
await self._update_skills_for_objective(
student_id,
objective_id,
score / max_score
)
return result
async def _save_assessment(self, assessment: AssessmentResult) -> bool:
"""
Save an assessment result to disk.
Args:
assessment: The AssessmentResult to save.
Returns:
True if the save was successful, False otherwise.
"""
try:
dir_path = self._get_student_assessments_dir(assessment.student_id)
file_path = os.path.join(dir_path, f"{assessment.id}.json")
with open(file_path, 'w') as f:
json.dump(assessment.dict(), f, indent=2)
return True
except Exception as e:
print(f"Error saving assessment result: {e}")
return False
async def get_assessment(
self,
student_id: str,
assessment_id: str
) -> Optional[AssessmentResult]:
"""
Get an assessment result by ID.
Args:
student_id: ID of the student.
assessment_id: ID of the assessment.
Returns:
The AssessmentResult if found, None otherwise.
"""
try:
dir_path = self._get_student_assessments_dir(student_id)
file_path = os.path.join(dir_path, f"{assessment_id}.json")
if not os.path.exists(file_path):
return None
with open(file_path, 'r') as f:
data = json.load(f)
return AssessmentResult(**data)
except Exception as e:
print(f"Error loading assessment result: {e}")
return None
async def list_assessments(
self,
student_id: str,
assessment_type: Optional[AssessmentType] = None
) -> List[AssessmentResult]:
"""
List assessment results for a student.
Args:
student_id: ID of the student.
assessment_type: Optional filter by assessment type.
Returns:
List of AssessmentResult objects.
"""
results = []
try:
dir_path = self._get_student_assessments_dir(student_id)
for filename in os.listdir(dir_path):
if filename.endswith('.json'):
file_path = os.path.join(dir_path, filename)
with open(file_path, 'r') as f:
data = json.load(f)
result = AssessmentResult(**data)
# Apply filter if specified
if assessment_type is None or result.assessment_type == assessment_type:
results.append(result)
except Exception as e:
print(f"Error listing assessment results: {e}")
# Sort by completion date, newest first
results.sort(key=lambda x: x.completed_at, reverse=True)
return results
async def _update_skills_for_objective(
self,
student_id: str,
objective_id: str,
performance: float
) -> None:
"""
Update skill progress based on performance in an objective.
Args:
student_id: ID of the student.
objective_id: ID of the learning objective.
performance: Performance level (0-1).
"""
# In a real implementation, this would map objectives to skills
# and update skill progress accordingly.
# For this demo, we'll create/update a skill with the same ID as the objective.
skill = await self.get_skill_progress(student_id, objective_id)
if skill:
# Update existing skill
new_progress = skill.progress_percentage + (performance * 20) # Increase by up to 20%
# Check if we should level up
if new_progress >= 100:
# Level up and reset progress
levels = list(SkillLevel)
current_index = levels.index(skill.level)
if current_index < len(levels) - 1:
# Move to next level
skill.level = levels[current_index + 1]
skill.progress_percentage = new_progress - 100
else:
# Already at max level
skill.progress_percentage = 100
else:
skill.progress_percentage = new_progress
skill.last_updated = datetime.utcnow().isoformat()
else:
# Create new skill
skill = SkillProgress(
skill_id=objective_id,
name=f"Skill for objective {objective_id}", # In a real app, would use actual skill name
level=SkillLevel.NOVICE,
progress_percentage=performance * 20, # Initial progress based on performance
last_updated=datetime.utcnow().isoformat(),
related_objectives=[objective_id]
)
await self._save_skill_progress(student_id, skill)
async def _save_skill_progress(
self,
student_id: str,
skill: SkillProgress
) -> bool:
"""
Save skill progress to disk.
Args:
student_id: ID of the student.
skill: The SkillProgress to save.
Returns:
True if the save was successful, False otherwise.
"""
try:
dir_path = self._get_student_skills_dir(student_id)
file_path = os.path.join(dir_path, f"{skill.skill_id}.json")
with open(file_path, 'w') as f:
json.dump(skill.dict(), f, indent=2)
return True
except Exception as e:
print(f"Error saving skill progress: {e}")
return False
async def get_skill_progress(
self,
student_id: str,
skill_id: str
) -> Optional[SkillProgress]:
"""
Get skill progress by ID.
Args:
student_id: ID of the student.
skill_id: ID of the skill.
Returns:
The SkillProgress if found, None otherwise.
"""
try:
dir_path = self._get_student_skills_dir(student_id)
file_path = os.path.join(dir_path, f"{skill_id}.json")
if not os.path.exists(file_path):
return None
with open(file_path, 'r') as f:
data = json.load(f)
return SkillProgress(**data)
except Exception as e:
print(f"Error loading skill progress: {e}")
return None
async def list_skills(self, student_id: str) -> List[SkillProgress]:
"""
List skill progress for a student.
Args:
student_id: ID of the student.
Returns:
List of SkillProgress objects.
"""
skills = []
try:
dir_path = self._get_student_skills_dir(student_id)
if os.path.exists(dir_path):
for filename in os.listdir(dir_path):
if filename.endswith('.json'):
file_path = os.path.join(dir_path, filename)
with open(file_path, 'r') as f:
data = json.load(f)
skills.append(SkillProgress(**data))
except Exception as e:
print(f"Error listing skill progress: {e}")
# Sort by level and then by progress percentage
skills.sort(
key=lambda x: (list(SkillLevel).index(x.level), x.progress_percentage),
reverse=True
)
return skills
async def get_progress_summary(self, student_id: str) -> Dict[str, Any]:
"""
Get a summary of a student's learning progress.
Args:
student_id: ID of the student.
Returns:
Dictionary containing progress summary.
"""
assessments = await self.list_assessments(student_id)
skills = await self.list_skills(student_id)
# Calculate assessment statistics
total_assessments = len(assessments)
average_score = 0
assessment_types_count = {}
recent_assessments = []
if total_assessments > 0:
total_score_percentage = 0
for assessment in assessments:
score_percentage = (assessment.score / assessment.max_score) * 100
total_score_percentage += score_percentage
# Count by type
assessment_type = assessment.assessment_type
assessment_types_count[assessment_type] = assessment_types_count.get(assessment_type, 0) + 1
average_score = total_score_percentage / total_assessments
recent_assessments = [a.dict() for a in assessments[:5]] # 5 most recent
# Summarize skills
skill_levels = {}
for skill in skills:
level = skill.level
skill_levels[level] = skill_levels.get(level, 0) + 1
# Create summary
return {
"student_id": student_id,
"total_assessments": total_assessments,
"average_score": round(average_score, 2),
"assessment_types_count": assessment_types_count,
"recent_assessments": recent_assessments,
"total_skills": len(skills),
"skill_levels": skill_levels,
"top_skills": [s.dict() for s in skills[:3]], # 3 highest-level skills
"generated_at": datetime.utcnow().isoformat()
}