Spaces:
Sleeping
Sleeping
File size: 32,280 Bytes
6a11527 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 |
"""
Personalized Learning Pathway System
Implements adaptive learning capabilities that customize instruction based on comprehensive user profiling
"""
import json
import os
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass, asdict
from collections import defaultdict
import pickle
@dataclass
class UserProfile:
"""User profile data structure"""
user_id: str
knowledge_level: Dict[str, float] # Knowledge level for each topic (0-1)
learning_style: str # "visual", "textual", "practical", "mixed"
learning_pace: str # "slow", "medium", "fast"
preferred_topics: List[str]
weak_areas: List[str]
strong_areas: List[str]
test_scores: Dict[str, List[float]] # Historical test scores
question_history: List[Dict] # Question and answer history
learning_time: Dict[str, float] # Learning time for each topic (minutes)
last_activity: str
total_questions_asked: int
total_tests_completed: int
bloom_level_performance: Dict[str, Dict[str, float]] # Bloom level performance for each topic
created_at: str
updated_at: str
# Cold start related fields
has_completed_onboarding: bool = False # Whether onboarding is completed
background_experience: str = "" # Background experience (e.g., "experienced", "beginner", "intermediate")
learning_goals: List[str] = None # Learning goals, None requires special handling
initial_assessment_completed: bool = False # Whether initial assessment is completed
initial_knowledge_survey: Dict[str, float] = None # Initial knowledge survey results, None requires special handling
@dataclass
class LearningPathNode:
"""Learning path node"""
node_id: str
topic: str
bloom_level: str # "remember", "understand", "apply", "analyze", "evaluate", "create"
difficulty: float # 0-1
prerequisites: List[str] # Prerequisite node IDs
estimated_time: int # Estimated time (minutes)
content_type: str # "reading", "quiz", "practical", "review"
status: str # "pending", "in_progress", "completed", "skipped"
completion_date: Optional[str] = None
score: Optional[float] = None
@dataclass
class LearningPath:
"""Learning path"""
path_id: str
user_id: str
nodes: List[LearningPathNode]
current_node_index: int
completion_percentage: float
created_at: str
updated_at: str
estimated_total_time: int
class UserProfilingSystem:
"""User profiling system"""
def __init__(self, storage_dir: str = "user_data"):
self.storage_dir = storage_dir
os.makedirs(storage_dir, exist_ok=True)
self.profiles_file = os.path.join(storage_dir, "user_profiles.json")
self.profiles = self._load_profiles()
def _load_profiles(self) -> Dict[str, UserProfile]:
"""Load user profiles"""
if os.path.exists(self.profiles_file):
try:
with open(self.profiles_file, 'r', encoding='utf-8') as f:
data = json.load(f)
return {uid: UserProfile(**profile) for uid, profile in data.items()}
except Exception as e:
print(f"Error loading profiles: {e}")
return {}
def _save_profiles(self):
"""Save user profiles"""
try:
with open(self.profiles_file, 'w', encoding='utf-8') as f:
data = {uid: asdict(profile) for uid, profile in self.profiles.items()}
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving profiles: {e}")
def get_or_create_profile(self, user_id: str) -> UserProfile:
"""Get or create user profile (cold start)"""
if user_id not in self.profiles:
self.profiles[user_id] = UserProfile(
user_id=user_id,
knowledge_level={},
learning_style="mixed",
learning_pace="medium",
preferred_topics=[],
weak_areas=[],
strong_areas=[],
test_scores={},
question_history=[],
learning_time={},
last_activity=datetime.now().isoformat(),
total_questions_asked=0,
total_tests_completed=0,
bloom_level_performance={},
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat(),
has_completed_onboarding=False,
background_experience="",
learning_goals=None,
initial_assessment_completed=False,
initial_knowledge_survey=None
)
self._save_profiles()
return self.profiles[user_id]
def is_cold_start(self, user_id: str) -> bool:
"""Check if user is in cold start state"""
if user_id not in self.profiles:
return True
profile = self.profiles[user_id]
return not profile.has_completed_onboarding
def complete_onboarding(self, user_id: str, onboarding_data: Dict):
"""Complete cold start setup and collect initial user information
Information collected during cold start:
1. Learning preferences:
- learning_style: Learning style preference
- learning_pace: Learning pace preference
2. Background information:
- background_experience: Background experience
- learning_goals: List of learning goals
3. Initial knowledge assessment:
- initial_knowledge_survey: Initial familiarity with each topic (0-1)
- initial_assessment_completed: Whether initial assessment is completed
"""
profile = self.get_or_create_profile(user_id)
# Update learning style
if 'learning_style' in onboarding_data:
profile.learning_style = onboarding_data['learning_style']
# Update learning pace
if 'learning_pace' in onboarding_data:
profile.learning_pace = onboarding_data['learning_pace']
# Update background experience
if 'background_experience' in onboarding_data:
profile.background_experience = onboarding_data['background_experience']
# Update learning goals
if 'learning_goals' in onboarding_data:
profile.learning_goals = onboarding_data['learning_goals']
# Update initial knowledge survey
if 'initial_knowledge_survey' in onboarding_data:
profile.initial_knowledge_survey = onboarding_data['initial_knowledge_survey']
# Convert initial survey results to knowledge level
profile.knowledge_level = onboarding_data['initial_knowledge_survey'].copy()
# Update preferred topics (based on initial survey, select topics with lower familiarity)
if 'initial_knowledge_survey' in onboarding_data:
survey = onboarding_data['initial_knowledge_survey']
# Select topics with lower familiarity as learning focus
low_knowledge_topics = [topic for topic, level in survey.items() if level < 0.5]
profile.preferred_topics = low_knowledge_topics[:3] # Take top 3
# Update initial assessment status
if 'initial_assessment_completed' in onboarding_data:
profile.initial_assessment_completed = onboarding_data['initial_assessment_completed']
# Mark cold start as completed
profile.has_completed_onboarding = True
profile.updated_at = datetime.now().isoformat()
self._save_profiles()
return profile
def update_from_test_results(self, user_id: str, topic: str, test_results: List[Dict]):
"""Update user profile from test results"""
profile = self.get_or_create_profile(user_id)
# Calculate average score
scores = [r.get('score', 1.0 if r.get('is_correct', False) else 0.0)
for r in test_results]
avg_score = np.mean(scores) if scores else 0.5
# Update knowledge level
if topic not in profile.knowledge_level:
profile.knowledge_level[topic] = avg_score
else:
# Weighted average (give more weight to latest results)
profile.knowledge_level[topic] = 0.7 * avg_score + 0.3 * profile.knowledge_level[topic]
# Update test score history
if topic not in profile.test_scores:
profile.test_scores[topic] = []
profile.test_scores[topic].append(avg_score)
# Update Bloom level performance
if topic not in profile.bloom_level_performance:
profile.bloom_level_performance[topic] = {}
for result in test_results:
level = result.get('level', 'unknown')
is_correct = result.get('is_correct', False)
score = 1.0 if is_correct else 0.0
if level not in profile.bloom_level_performance[topic]:
profile.bloom_level_performance[topic][level] = []
profile.bloom_level_performance[topic][level].append(score)
# Calculate average performance for each Bloom level
for level in profile.bloom_level_performance[topic]:
scores = profile.bloom_level_performance[topic][level]
profile.bloom_level_performance[topic][level] = np.mean(scores) if scores else 0.0
# Update weak and strong areas
self._update_weak_strong_areas(profile)
# Update learning pace
profile.learning_pace = self._calculate_learning_pace(profile)
profile.total_tests_completed += 1
profile.last_activity = datetime.now().isoformat()
profile.updated_at = datetime.now().isoformat()
self._save_profiles()
return profile
def update_from_question(self, user_id: str, question: str, topic: Optional[str] = None):
"""Update user profile from question history"""
profile = self.get_or_create_profile(user_id)
profile.question_history.append({
"question": question,
"topic": topic,
"timestamp": datetime.now().isoformat()
})
# Analyze question type to infer learning style
profile.learning_style = self._infer_learning_style(profile.question_history)
# Update preferred topics
if topic:
if topic not in profile.preferred_topics:
profile.preferred_topics.append(topic)
# Sort by frequency
topic_counts = defaultdict(int)
for q in profile.question_history:
if q.get('topic'):
topic_counts[q['topic']] += 1
profile.preferred_topics = sorted(topic_counts.items(), key=lambda x: x[1], reverse=True)[:5]
profile.preferred_topics = [t[0] for t in profile.preferred_topics]
profile.total_questions_asked += 1
profile.last_activity = datetime.now().isoformat()
profile.updated_at = datetime.now().isoformat()
self._save_profiles()
return profile
def update_learning_time(self, user_id: str, topic: str, minutes: float):
"""Update learning time"""
profile = self.get_or_create_profile(user_id)
if topic not in profile.learning_time:
profile.learning_time[topic] = 0.0
profile.learning_time[topic] += minutes
profile.last_activity = datetime.now().isoformat()
profile.updated_at = datetime.now().isoformat()
self._save_profiles()
return profile
def _update_weak_strong_areas(self, profile: UserProfile):
"""Update weak and strong areas"""
# Topics with knowledge level below 0.6 are weak areas
weak = [topic for topic, level in profile.knowledge_level.items() if level < 0.6]
# Topics with knowledge level above 0.8 are strong areas
strong = [topic for topic, level in profile.knowledge_level.items() if level >= 0.8]
profile.weak_areas = weak
profile.strong_areas = strong
def _calculate_learning_pace(self, profile: UserProfile) -> str:
"""Calculate learning pace"""
if not profile.test_scores:
return "medium"
# Calculate test completion speed
total_tests = profile.total_tests_completed
if total_tests == 0:
return "medium"
# Analyze performance changes in recent tests
recent_scores = []
for topic_scores in profile.test_scores.values():
if topic_scores:
recent_scores.extend(topic_scores[-3:]) # Last 3 tests
if not recent_scores:
return "medium"
# If recent scores improve quickly, likely a fast-paced learner
if len(recent_scores) >= 3:
improvement = recent_scores[-1] - recent_scores[0]
if improvement > 0.3:
return "fast"
elif improvement < -0.1:
return "slow"
return "medium"
def _infer_learning_style(self, question_history: List[Dict]) -> str:
"""Infer learning style from question history"""
if not question_history:
return "mixed"
# Analyze question keywords
visual_keywords = ["how", "what does", "show", "visual", "diagram", "illustration"]
practical_keywords = ["how to", "step", "procedure", "activate", "use", "operate"]
textual_keywords = ["what is", "explain", "define", "describe", "meaning"]
visual_count = sum(1 for q in question_history
if any(kw in q.get('question', '').lower() for kw in visual_keywords))
practical_count = sum(1 for q in question_history
if any(kw in q.get('question', '').lower() for kw in practical_keywords))
textual_count = sum(1 for q in question_history
if any(kw in q.get('question', '').lower() for kw in textual_keywords))
total = len(question_history)
if total == 0:
return "mixed"
visual_ratio = visual_count / total
practical_ratio = practical_count / total
textual_ratio = textual_count / total
max_ratio = max(visual_ratio, practical_ratio, textual_ratio)
if max_ratio > 0.4:
if visual_ratio == max_ratio:
return "visual"
elif practical_ratio == max_ratio:
return "practical"
else:
return "textual"
return "mixed"
def get_profile_summary(self, user_id: str) -> Dict:
"""Get user profile summary"""
profile = self.get_or_create_profile(user_id)
return {
"user_id": profile.user_id,
"knowledge_level": profile.knowledge_level,
"learning_style": profile.learning_style,
"learning_pace": profile.learning_pace,
"preferred_topics": profile.preferred_topics,
"weak_areas": profile.weak_areas,
"strong_areas": profile.strong_areas,
"total_questions": profile.total_questions_asked,
"total_tests": profile.total_tests_completed,
"overall_progress": self._calculate_overall_progress(profile)
}
def _calculate_overall_progress(self, profile: UserProfile) -> float:
"""Calculate overall progress"""
if not profile.knowledge_level:
return 0.0
return np.mean(list(profile.knowledge_level.values()))
class LearningPathGenerator:
"""Learning path generator"""
def __init__(self, user_profiling: UserProfilingSystem, available_topics: List[str]):
self.user_profiling = user_profiling
self.available_topics = available_topics
self.bloom_levels = ["remember", "understand", "apply", "analyze", "evaluate", "create"]
def generate_path(self, user_id: str, focus_areas: Optional[List[str]] = None) -> LearningPath:
"""Generate personalized learning path"""
profile = self.user_profiling.get_or_create_profile(user_id)
# Determine topics to learn
topics_to_learn = self._determine_topics(profile, focus_areas)
# Generate learning nodes
nodes = []
node_id_counter = 0
for topic in topics_to_learn:
topic_level = profile.knowledge_level.get(topic, 0.0)
bloom_performance = profile.bloom_level_performance.get(topic, {})
# Generate nodes for different Bloom levels for each topic
for bloom_level in self.bloom_levels:
# Check if this node is needed
if not self._needs_node(profile, topic, bloom_level, topic_level, bloom_performance):
continue
node = LearningPathNode(
node_id=f"node_{node_id_counter}",
topic=topic,
bloom_level=bloom_level,
difficulty=self._calculate_difficulty(topic_level, bloom_level),
prerequisites=self._get_prerequisites(nodes, topic, bloom_level),
estimated_time=self._estimate_time(bloom_level, profile.learning_pace),
content_type=self._determine_content_type(bloom_level, profile.learning_style),
status="pending"
)
nodes.append(node)
node_id_counter += 1
# Sort nodes (considering prerequisites)
ordered_nodes = self._topological_sort(nodes)
# Calculate total time
total_time = sum(node.estimated_time for node in ordered_nodes)
path = LearningPath(
path_id=f"path_{user_id}_{datetime.now().strftime('%Y%m%d%H%M%S')}",
user_id=user_id,
nodes=ordered_nodes,
current_node_index=0,
completion_percentage=0.0,
created_at=datetime.now().isoformat(),
updated_at=datetime.now().isoformat(),
estimated_total_time=total_time
)
return path
def _determine_topics(self, profile: UserProfile, focus_areas: Optional[List[str]]) -> List[str]:
"""Determine topics to learn"""
if focus_areas:
return focus_areas
# Prioritize weak areas
topics = profile.weak_areas.copy()
# Add unlearned topics
for topic in self.available_topics:
if topic not in profile.knowledge_level and topic not in topics:
topics.append(topic)
# If no weak areas, recommend preferred or strong area related topics
if not topics:
topics = profile.preferred_topics[:3] if profile.preferred_topics else self.available_topics[:3]
return topics[:5] # Limit to maximum 5 topics
def _needs_node(self, profile: UserProfile, topic: str, bloom_level: str,
topic_level: float, bloom_performance: Dict) -> bool:
"""Determine if a node is needed"""
# If performance at this Bloom level is already good, skip
level_performance = bloom_performance.get(bloom_level, 0.0)
if level_performance >= 0.8:
return False
# Decide based on knowledge level
if topic_level < 0.3 and bloom_level in ["analyze", "evaluate", "create"]:
return False # Insufficient foundational knowledge, learn basics first
return True
def _calculate_difficulty(self, topic_level: float, bloom_level: str) -> float:
"""Calculate node difficulty"""
bloom_weights = {
"remember": 0.2,
"understand": 0.3,
"apply": 0.5,
"analyze": 0.7,
"evaluate": 0.85,
"create": 1.0
}
base_difficulty = bloom_weights.get(bloom_level, 0.5)
# Adjust based on current knowledge level
adjusted = base_difficulty * (1 - topic_level * 0.3)
return min(1.0, max(0.1, adjusted))
def _get_prerequisites(self, existing_nodes: List[LearningPathNode],
topic: str, bloom_level: str) -> List[str]:
"""Get prerequisites"""
prereqs = []
# Lower Bloom levels of the same topic are prerequisites
bloom_order = ["remember", "understand", "apply", "analyze", "evaluate", "create"]
current_index = bloom_order.index(bloom_level) if bloom_level in bloom_order else 0
for node in existing_nodes:
if node.topic == topic:
node_index = bloom_order.index(node.bloom_level) if node.bloom_level in bloom_order else 0
if node_index < current_index:
prereqs.append(node.node_id)
return prereqs
def _estimate_time(self, bloom_level: str, learning_pace: str) -> int:
"""Estimate learning time (minutes)"""
base_times = {
"remember": 10,
"understand": 15,
"apply": 20,
"analyze": 25,
"evaluate": 30,
"create": 35
}
base_time = base_times.get(bloom_level, 20)
pace_multipliers = {
"slow": 1.5,
"medium": 1.0,
"fast": 0.7
}
return int(base_time * pace_multipliers.get(learning_pace, 1.0))
def _determine_content_type(self, bloom_level: str, learning_style: str) -> str:
"""Determine content type"""
# Decide based on learning style and Bloom level
if learning_style == "visual":
if bloom_level in ["remember", "understand"]:
return "reading"
else:
return "practical"
elif learning_style == "practical":
return "practical"
elif learning_style == "textual":
return "reading"
else:
# mixed
if bloom_level in ["apply", "analyze", "evaluate", "create"]:
return "quiz"
return "reading"
def _topological_sort(self, nodes: List[LearningPathNode]) -> List[LearningPathNode]:
"""Topological sort to ensure prerequisites are completed first"""
# Create node mapping
node_map = {node.node_id: node for node in nodes}
# Calculate in-degree
in_degree = {node.node_id: len(node.prerequisites) for node in nodes}
# Find nodes without prerequisites
queue = [node.node_id for node in nodes if in_degree[node.node_id] == 0]
result = []
while queue:
current_id = queue.pop(0)
current_node = node_map[current_id]
result.append(current_node)
# Update in-degree of other nodes
for node in nodes:
if current_id in node.prerequisites:
in_degree[node.node_id] -= 1
if in_degree[node.node_id] == 0:
queue.append(node.node_id)
# Add remaining nodes (may have circular dependencies)
remaining = [node for node in nodes if node not in result]
result.extend(remaining)
return result
class AdaptiveLearningEngine:
"""Adaptive learning engine"""
def __init__(self, user_profiling: UserProfilingSystem, learning_path_generator: LearningPathGenerator):
self.user_profiling = user_profiling
self.learning_path_generator = learning_path_generator
self.paths_file = os.path.join("user_data", "learning_paths.json")
self.paths = self._load_paths()
def _load_paths(self) -> Dict[str, LearningPath]:
"""Load learning paths"""
if os.path.exists(self.paths_file):
try:
with open(self.paths_file, 'r', encoding='utf-8') as f:
data = json.load(f)
paths = {}
for pid, path_data in data.items():
nodes = [LearningPathNode(**node) for node in path_data['nodes']]
path = LearningPath(
path_id=path_data['path_id'],
user_id=path_data['user_id'],
nodes=nodes,
current_node_index=path_data['current_node_index'],
completion_percentage=path_data['completion_percentage'],
created_at=path_data['created_at'],
updated_at=path_data['updated_at'],
estimated_total_time=path_data['estimated_total_time']
)
paths[pid] = path
return paths
except Exception as e:
print(f"Error loading paths: {e}")
return {}
def _save_paths(self):
"""Save learning paths"""
try:
os.makedirs("user_data", exist_ok=True)
with open(self.paths_file, 'w', encoding='utf-8') as f:
data = {}
for pid, path in self.paths.items():
path_dict = asdict(path)
data[pid] = path_dict
json.dump(data, f, indent=2, ensure_ascii=False)
except Exception as e:
print(f"Error saving paths: {e}")
def create_or_update_path(self, user_id: str, focus_areas: Optional[List[str]] = None) -> LearningPath:
"""Create or update learning path"""
# Check if there is an active path
active_path = self.get_active_path(user_id)
if active_path and active_path.completion_percentage < 1.0:
# Update existing path
return self._update_path(active_path)
else:
# Create new path
path = self.learning_path_generator.generate_path(user_id, focus_areas)
self.paths[path.path_id] = path
self._save_paths()
return path
def get_active_path(self, user_id: str) -> Optional[LearningPath]:
"""Get user's current active path"""
user_paths = [p for p in self.paths.values() if p.user_id == user_id]
if not user_paths:
return None
# Return the latest incomplete path
active = [p for p in user_paths if p.completion_percentage < 1.0]
if active:
return max(active, key=lambda p: p.created_at)
# If no incomplete paths, return the latest one
return max(user_paths, key=lambda p: p.created_at)
def complete_node(self, user_id: str, node_id: str, score: float):
"""Complete a node"""
path = self.get_active_path(user_id)
if not path:
return None
# Find node and mark as completed
for node in path.nodes:
if node.node_id == node_id:
node.status = "completed"
node.score = score
node.completion_date = datetime.now().isoformat()
break
# Update path progress
completed = sum(1 for n in path.nodes if n.status == "completed")
path.completion_percentage = completed / len(path.nodes) if path.nodes else 0.0
# Update current node index
for i, node in enumerate(path.nodes):
if node.status not in ["completed", "skipped"]:
path.current_node_index = i
break
path.updated_at = datetime.now().isoformat()
self._save_paths()
# Update user profile
current_node = path.nodes[path.current_node_index] if path.current_node_index < len(path.nodes) else None
if current_node:
self.user_profiling.update_learning_time(
user_id,
current_node.topic,
current_node.estimated_time
)
return path
def _update_path(self, path: LearningPath) -> LearningPath:
"""Update path based on user performance"""
profile = self.user_profiling.get_or_create_profile(path.user_id)
# Check if path needs adjustment
for node in path.nodes:
if node.status == "pending":
# Check if already mastered
topic_level = profile.knowledge_level.get(node.topic, 0.0)
bloom_perf = profile.bloom_level_performance.get(node.topic, {}).get(node.bloom_level, 0.0)
if bloom_perf >= 0.8:
# Already mastered, can skip
node.status = "skipped"
node.completion_date = datetime.now().isoformat()
# Recalculate progress
completed = sum(1 for n in path.nodes if n.status in ["completed", "skipped"])
path.completion_percentage = completed / len(path.nodes) if path.nodes else 0.0
path.updated_at = datetime.now().isoformat()
self._save_paths()
return path
def get_recommendations(self, user_id: str) -> Dict:
"""Get learning recommendations"""
profile = self.user_profiling.get_or_create_profile(user_id)
path = self.get_active_path(user_id)
recommendations = {
"next_node": None,
"suggested_topics": [],
"review_topics": [],
"challenge_topics": []
}
# Recommend next node
if path and path.current_node_index < len(path.nodes):
next_node = path.nodes[path.current_node_index]
recommendations["next_node"] = {
"node_id": next_node.node_id,
"topic": next_node.topic,
"bloom_level": next_node.bloom_level,
"content_type": next_node.content_type,
"estimated_time": next_node.estimated_time
}
# Recommend topics for review
recommendations["review_topics"] = profile.weak_areas[:3]
# Recommend challenge topics (advanced content for strong areas)
for topic in profile.strong_areas[:2]:
if topic not in recommendations["challenge_topics"]:
recommendations["challenge_topics"].append(topic)
# Recommend new topics
all_topics = set(self.learning_path_generator.available_topics)
learned_topics = set(profile.knowledge_level.keys())
new_topics = list(all_topics - learned_topics)[:3]
recommendations["suggested_topics"] = new_topics
return recommendations
def get_path_visualization_data(self, user_id: str) -> Dict:
"""Get path visualization data"""
path = self.get_active_path(user_id)
if not path:
return {"nodes": [], "edges": []}
nodes_data = []
edges_data = []
for node in path.nodes:
nodes_data.append({
"id": node.node_id,
"topic": node.topic,
"bloom_level": node.bloom_level,
"status": node.status,
"difficulty": node.difficulty,
"score": node.score
})
# Add edges (prerequisites)
for prereq_id in node.prerequisites:
edges_data.append({
"source": prereq_id,
"target": node.node_id
})
return {
"nodes": nodes_data,
"edges": edges_data,
"completion_percentage": path.completion_percentage,
"current_node_index": path.current_node_index
}
|