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