""" 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() }