“shubhamdhamal”
Deploy Flask app with Docker
7644eac
"""
Path Modification Service
Phase 3: Dynamic Learning Path Updates
This service handles:
- Modifying learning paths based on user requests
- Adding/removing/updating resources
- Splitting/merging milestones
- Adjusting difficulty and duration
- Tracking all modifications
"""
from typing import Dict, List, Optional, Any
import json
import copy
from datetime import datetime
from web_app import db
from web_app.models import UserLearningPath, PathModification
from src.ml.model_orchestrator import ModelOrchestrator
class PathModifier:
"""
Handles dynamic modifications to learning paths.
Modification Types:
- add_resource: Add a new resource to a milestone
- remove_resource: Remove a resource from a milestone
- modify_milestone: Update milestone properties
- split_milestone: Split one milestone into multiple
- merge_milestones: Combine multiple milestones
- adjust_difficulty: Make content easier or harder
- adjust_duration: Change time estimates
"""
def __init__(self):
"""Initialize the path modifier."""
self.orchestrator = ModelOrchestrator()
def modify_path(
self,
learning_path_id: str,
user_id: int,
modification_request: str,
entities: Dict,
chat_message_id: Optional[int] = None
) -> Dict:
"""
Modify a learning path based on user request.
Args:
learning_path_id: Learning path ID
user_id: User ID
modification_request: User's modification request
entities: Extracted entities from intent classification
chat_message_id: Optional chat message ID that triggered this
Returns:
Dictionary with modification result
"""
# Get the learning path
learning_path = UserLearningPath.query.get(learning_path_id)
if not learning_path or learning_path.user_id != user_id:
return {
'success': False,
'error': 'Learning path not found or access denied'
}
# Get current path data
path_data = learning_path.path_data_json
# Determine modification type and generate changes
modification_plan = self._generate_modification_plan(
modification_request,
entities,
path_data
)
if not modification_plan['success']:
return modification_plan
# Apply the modification
try:
modified_path = self._apply_modification(
path_data,
modification_plan
)
# Validate the modified path
if not self._validate_path(modified_path):
return {
'success': False,
'error': 'Modified path failed validation'
}
# Save the modification
old_path_data = copy.deepcopy(path_data)
learning_path.path_data_json = modified_path
learning_path.last_accessed_at = datetime.utcnow()
# Record the modification
path_modification = PathModification(
learning_path_id=learning_path_id,
user_id=user_id,
chat_message_id=chat_message_id,
modification_type=modification_plan['type'],
target_path=modification_plan.get('target_path'),
change_description=modification_plan['description'],
old_value=old_path_data,
new_value=modified_path
)
db.session.add(path_modification)
db.session.commit()
return {
'success': True,
'modification_type': modification_plan['type'],
'description': modification_plan['description'],
'changes': modification_plan.get('changes', {}),
'modified_path': modified_path
}
except Exception as e:
db.session.rollback()
print(f"Path modification error: {e}")
import traceback
traceback.print_exc()
return {
'success': False,
'error': f'Failed to apply modification: {str(e)}'
}
def _generate_modification_plan(
self,
request: str,
entities: Dict,
current_path: Dict
) -> Dict:
"""
Generate a modification plan using AI.
Args:
request: User's modification request
entities: Extracted entities
current_path: Current learning path data
Returns:
Modification plan dictionary
"""
# Build prompt for AI to generate modification plan
prompt = f"""You are a learning path modification assistant. Generate a specific modification plan.
User request: "{request}"
Extracted entities: {json.dumps(entities, indent=2)}
Current learning path summary:
- Title: {current_path.get('title', 'Unknown')}
- Total milestones: {len(current_path.get('milestones', []))}
- Duration: {current_path.get('duration_weeks', 'Unknown')} weeks
Milestones:
{self._format_milestones_for_prompt(current_path.get('milestones', []))}
Generate a modification plan that includes:
1. Modification type (add_resource, remove_resource, modify_milestone, split_milestone, adjust_difficulty, etc.)
2. Target (which milestone/resource to modify)
3. Specific changes to make
4. Human-readable description of the change
Be specific and actionable."""
schema = """
{
"success": true,
"type": "string (modification type)",
"target_path": "string (JSON path to target, e.g., 'milestones[2]')",
"description": "string (human-readable description)",
"changes": {
"action": "string (add, remove, update, split, etc.)",
"target_index": "integer or null (milestone index)",
"data": "object (specific changes to apply)"
}
}
"""
try:
response = self.orchestrator.generate_structured_response(
prompt=prompt,
output_schema=schema,
temperature=0.4,
use_cache=False # Don't cache modifications
)
plan = json.loads(response)
return plan
except Exception as e:
print(f"Modification plan generation error: {e}")
return {
'success': False,
'error': f'Failed to generate modification plan: {str(e)}'
}
def _apply_modification(self, path_data: Dict, plan: Dict) -> Dict:
"""
Apply the modification plan to the path data.
Args:
path_data: Current path data
plan: Modification plan
Returns:
Modified path data
"""
modified_path = copy.deepcopy(path_data)
changes = plan.get('changes', {})
action = changes.get('action', '')
if plan['type'] == 'add_resource':
modified_path = self._add_resource(modified_path, changes)
elif plan['type'] == 'remove_resource':
modified_path = self._remove_resource(modified_path, changes)
elif plan['type'] == 'modify_milestone':
modified_path = self._modify_milestone(modified_path, changes)
elif plan['type'] == 'split_milestone':
modified_path = self._split_milestone(modified_path, changes)
elif plan['type'] == 'adjust_difficulty':
modified_path = self._adjust_difficulty(modified_path, changes)
elif plan['type'] == 'adjust_duration':
modified_path = self._adjust_duration(modified_path, changes)
return modified_path
def _add_resource(self, path_data: Dict, changes: Dict) -> Dict:
"""Add a resource to a milestone."""
milestone_index = changes.get('target_index')
new_resources = changes.get('data', {}).get('resources', [])
if milestone_index is not None and 0 <= milestone_index < len(path_data.get('milestones', [])):
if 'resources' not in path_data['milestones'][milestone_index]:
path_data['milestones'][milestone_index]['resources'] = []
path_data['milestones'][milestone_index]['resources'].extend(new_resources)
return path_data
def _remove_resource(self, path_data: Dict, changes: Dict) -> Dict:
"""Remove a resource from a milestone."""
milestone_index = changes.get('target_index')
resource_index = changes.get('data', {}).get('resource_index')
if milestone_index is not None and resource_index is not None:
milestones = path_data.get('milestones', [])
if 0 <= milestone_index < len(milestones):
resources = milestones[milestone_index].get('resources', [])
if 0 <= resource_index < len(resources):
resources.pop(resource_index)
return path_data
def _modify_milestone(self, path_data: Dict, changes: Dict) -> Dict:
"""Modify milestone properties."""
milestone_index = changes.get('target_index')
updates = changes.get('data', {})
if milestone_index is not None and 0 <= milestone_index < len(path_data.get('milestones', [])):
milestone = path_data['milestones'][milestone_index]
# Apply updates
for key, value in updates.items():
if key in milestone:
milestone[key] = value
return path_data
def _split_milestone(self, path_data: Dict, changes: Dict) -> Dict:
"""Split a milestone into multiple smaller milestones."""
milestone_index = changes.get('target_index')
new_milestones = changes.get('data', {}).get('new_milestones', [])
if milestone_index is not None and new_milestones:
milestones = path_data.get('milestones', [])
if 0 <= milestone_index < len(milestones):
# Remove original milestone and insert new ones
milestones.pop(milestone_index)
for i, new_milestone in enumerate(new_milestones):
milestones.insert(milestone_index + i, new_milestone)
return path_data
def _adjust_difficulty(self, path_data: Dict, changes: Dict) -> Dict:
"""Adjust difficulty of content."""
milestone_index = changes.get('target_index')
difficulty_change = changes.get('data', {}).get('difficulty') # 'easier' or 'harder'
if milestone_index is not None:
milestone = path_data['milestones'][milestone_index]
if difficulty_change == 'easier':
# Reduce estimated hours, add more beginner resources
current_hours = milestone.get('estimated_hours', 10)
milestone['estimated_hours'] = max(2, int(current_hours * 0.7))
elif difficulty_change == 'harder':
# Increase estimated hours, add advanced resources
current_hours = milestone.get('estimated_hours', 10)
milestone['estimated_hours'] = int(current_hours * 1.3)
return path_data
def _adjust_duration(self, path_data: Dict, changes: Dict) -> Dict:
"""Adjust overall duration."""
new_duration = changes.get('data', {}).get('duration_weeks')
if new_duration:
path_data['duration_weeks'] = new_duration
# Recalculate total hours
total_hours = sum(
m.get('estimated_hours', 0)
for m in path_data.get('milestones', [])
)
path_data['total_hours'] = total_hours
return path_data
def _validate_path(self, path_data: Dict) -> bool:
"""
Validate that the modified path has all required fields.
Args:
path_data: Path data to validate
Returns:
True if valid, False otherwise
"""
required_fields = ['title', 'description', 'milestones']
for field in required_fields:
if field not in path_data:
return False
# Validate milestones
for milestone in path_data.get('milestones', []):
if 'title' not in milestone or 'description' not in milestone:
return False
return True
def _format_milestones_for_prompt(self, milestones: List[Dict]) -> str:
"""Format milestones for AI prompt."""
formatted = []
for i, milestone in enumerate(milestones):
formatted.append(
f"{i+1}. {milestone.get('title', 'Untitled')} "
f"({milestone.get('estimated_hours', '?')} hours)"
)
return '\n'.join(formatted)
def get_modification_history(
self,
learning_path_id: str,
limit: int = 10
) -> List[PathModification]:
"""
Get modification history for a learning path.
Args:
learning_path_id: Learning path ID
limit: Maximum number of modifications to return
Returns:
List of PathModification objects
"""
return PathModification.query.filter(
PathModification.learning_path_id == learning_path_id
).order_by(
PathModification.timestamp.desc()
).limit(limit).all()
def undo_modification(
self,
modification_id: int,
user_id: int
) -> Dict:
"""
Undo a previous modification.
Args:
modification_id: Modification ID to undo
user_id: User ID (for authorization)
Returns:
Result dictionary
"""
modification = PathModification.query.get(modification_id)
if not modification or modification.user_id != user_id:
return {
'success': False,
'error': 'Modification not found or access denied'
}
if modification.is_reverted:
return {
'success': False,
'error': 'Modification already reverted'
}
# Restore old value
learning_path = UserLearningPath.query.get(modification.learning_path_id)
if learning_path:
learning_path.path_data_json = modification.old_value
modification.is_reverted = True
db.session.commit()
return {
'success': True,
'message': 'Modification reverted successfully'
}
return {
'success': False,
'error': 'Learning path not found'
}