from flask import Flask, request, jsonify, session from flask_cors import CORS from models import db, User, Topic, KnowledgeState, LearningSession, QuizAttempt from knowledge_tracker import KnowledgeTracker from llm_service import LLMService from config import Config from agents.coordinator_agent import CoordinatorAgent import os from datetime import datetime import re app = Flask(__name__) app.config.from_object(Config) # UPDATED CORS CONFIGURATION CORS(app, supports_credentials=True, origins=['http://localhost:3000', 'https://adaptive-e-learning-system.vercel.app'], # placeholder - will improve with regex or wildcard allow_headers=['Content-Type', 'Authorization'], methods=['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']) db.init_app(app) # Initialize agents and services knowledge_tracker = KnowledgeTracker() llm_service = LLMService() coordinator = CoordinatorAgent() # Initialize database and sample data def init_db(): with app.app_context(): db.create_all() # Create sample topics if none exist if Topic.query.count() == 0: sample_topics = [ Topic(name="Python Basics", category="Programming", difficulty="beginner", description="Introduction to Python programming"), Topic(name="Data Structures", category="Programming", difficulty="intermediate", description="Learn about arrays, lists, stacks, and queues", prerequisites="1"), Topic(name="Object-Oriented Programming", category="Programming", difficulty="intermediate", description="Classes, objects, inheritance, and polymorphism", prerequisites="1"), Topic(name="Algorithms", category="Programming", difficulty="advanced", description="Sorting, searching, and algorithm complexity", prerequisites="2"), Topic(name="Machine Learning Basics", category="AI/ML", difficulty="intermediate", description="Introduction to ML concepts and algorithms", prerequisites="1,2"), Topic(name="Web Development", category="Web", difficulty="beginner", description="HTML, CSS, and JavaScript fundamentals"), Topic(name="Database Design", category="Database", difficulty="intermediate", description="SQL and database normalization", prerequisites="1"), Topic(name="Neural Networks", category="AI/ML", difficulty="advanced", description="Deep learning and neural network architectures", prerequisites="5"), ] db.session.bulk_save_objects(sample_topics) db.session.commit() # UPDATED: Root route now returns API info instead of template @app.route('/') def index(): return jsonify({ 'message': 'Adaptive E-Learning API', 'version': '1.0.0', 'status': 'running', 'ai_agents': '5 autonomous agents active', 'endpoints': { 'auth': '/api/login, /api/register, /api/logout, /api/current-user', 'topics': '/api/topics, /api/topics/', 'learning': '/api/generate-lesson', 'quiz': '/api/generate-quiz, /api/submit-answer', 'progress': '/api/progress-summary, /api/knowledge-state/', 'utility': '/api/check-code, /api/ask-challenge-hint, /api/agent-status' } }) # REMOVED: All HTML template routes (dashboard, learn, quiz, progress) # React frontend handles all routing # API Routes @app.route('/api/register', methods=['POST']) def register(): data = request.json existing_user = User.query.filter_by(username=data['username']).first() if existing_user: return jsonify({'error': 'Username already exists'}), 400 user = User( username=data['username'], email=data['email'] ) db.session.add(user) db.session.commit() session['user_id'] = user.id session['username'] = user.username return jsonify({ 'message': 'User registered successfully', 'user_id': user.id, 'username': user.username }) @app.route('/api/login', methods=['POST']) def login(): data = request.json user = User.query.filter_by(username=data['username']).first() if user: session['user_id'] = user.id session['username'] = user.username return jsonify({ 'message': 'Login successful', 'user_id': user.id, 'username': user.username }) else: return jsonify({'error': 'User not found'}), 404 @app.route('/api/current-user', methods=['GET']) def current_user(): if 'user_id' in session: return jsonify({ 'user_id': session['user_id'], 'username': session['username'] }) return jsonify({'error': 'Not logged in'}), 401 @app.route('/api/logout', methods=['POST']) def logout(): session.clear() return jsonify({'message': 'Logged out successfully'}) @app.route('/api/topics', methods=['GET']) def get_topics(): topics = Topic.query.all() return jsonify([{ 'id': t.id, 'name': t.name, 'category': t.category, 'difficulty': t.difficulty, 'description': t.description } for t in topics]) @app.route('/api/topics/', methods=['GET']) def get_topic(topic_id): topic = Topic.query.get_or_404(topic_id) return jsonify({ 'id': topic.id, 'name': topic.name, 'category': topic.category, 'difficulty': topic.difficulty, 'description': topic.description }) def format_lesson_content(content): """Format lesson content for better display""" import re # Remove multiple consecutive blank lines content = re.sub(r'\n\s*\n\s*\n+', '\n\n', content) # Remove extra whitespace from each line content = '\n'.join(line.strip() for line in content.split('\n')) # Convert markdown code blocks to HTML content = re.sub(r'```python\n(.*?)\n```', r'
\1
', content, flags=re.DOTALL) content = re.sub(r'```\n(.*?)\n```', r'
\1
', content, flags=re.DOTALL) # Convert markdown headers to HTML content = re.sub(r'^### (.*?)$', r'

\1

', content, flags=re.MULTILINE) content = re.sub(r'^## (.*?)$', r'

\1

', content, flags=re.MULTILINE) content = re.sub(r'^# (.*?)$', r'

\1

', content, flags=re.MULTILINE) # Convert markdown bold to HTML content = re.sub(r'\*\*(.*?)\*\*', r'\1', content) # Convert inline code to HTML (before lists to preserve code in lists) content = re.sub(r'`([^`]+)`', r'\1', content) # Convert markdown lists to HTML - IMPROVED lines = content.split('\n') in_list = False result_lines = [] for i, line in enumerate(lines): line = line.strip() # Skip empty lines if not line: if in_list: result_lines.append('') in_list = False continue # Handle list items if line.startswith('- '): if not in_list: result_lines.append('
    ') in_list = True result_lines.append(f'
  • {line[2:]}
  • ') else: if in_list: result_lines.append('
') in_list = False result_lines.append(line) if in_list: result_lines.append('') content = '\n'.join(result_lines) # Convert paragraphs - NO NEWLINES lines = content.split('\n') formatted_lines = [] in_pre = False for line in lines: line = line.strip() # Skip completely empty lines if not line: continue # Handle pre blocks if '
' in line:
            in_pre = True
            formatted_lines.append(line)
            continue
        elif '
' in line: in_pre = False formatted_lines.append(line) continue elif in_pre: formatted_lines.append(line) continue # Keep HTML tags as-is if line.startswith('') or line.startswith('') or line.startswith('
  • '): formatted_lines.append(line) else: # Wrap non-HTML content in paragraph formatted_lines.append(f'

    {line}

    ') # Join WITHOUT newlines to eliminate all spacing return ''.join(formatted_lines) def format_study_tips(content): """Format study tips markdown to HTML""" import re # Convert **bold** to HTML content = re.sub(r'\*\*(.*?)\*\*', r'\1', content) # Convert bullet points to HTML list lines = content.split('\n') formatted_lines = [] in_list = False for line in lines: line = line.strip() if not line: continue # Check if it's a header if line.endswith(':') and not line.startswith('-') and not line.startswith('•'): if in_list: formatted_lines.append('') in_list = False formatted_lines.append(f'

    {line}

    ') # Check if it's a bullet point elif line.startswith('- ') or line.startswith('• '): if not in_list: formatted_lines.append('
      ') in_list = True # Remove the bullet character text = line[2:] if line.startswith('- ') else line[2:] formatted_lines.append(f'
    • {text}
    • ') else: if in_list: formatted_lines.append('
    ') in_list = False formatted_lines.append(f'

    {line}

    ') if in_list: formatted_lines.append('') return ''.join(formatted_lines) @app.route('/api/generate-lesson', methods=['POST']) def generate_lesson(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 data = request.json topic_id = data['topic_id'] # Use Coordinator Agent result = coordinator.perceive({ 'task': 'generate_lesson', 'user_id': session['user_id'], 'context': { 'topic_id': topic_id, 'learning_history': [] } }).decide().act() if not result['success']: return jsonify({'error': result.get('error')}), 500 # Extract teaching agent result teaching_result = result['results'].get('TeachingAgent', {}) # Format content content = format_lesson_content(teaching_result.get('content', '')) # Save to database topic = Topic.query.get(topic_id) learning_session = LearningSession( user_id=session['user_id'], topic_id=topic_id, content=content, difficulty=teaching_result.get('metadata', {}).get('complexity', 'beginner') ) db.session.add(learning_session) db.session.commit() return jsonify({ 'content': content, 'difficulty': teaching_result.get('metadata', {}).get('complexity'), 'knowledge_level': teaching_result.get('metadata', {}).get('knowledge_level', 0), 'topic_name': topic.name, 'agent_metadata': teaching_result.get('metadata', {}) }) @app.route('/api/generate-quiz', methods=['POST']) def generate_quiz(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 data = request.json topic_id = data['topic_id'] # Use Coordinator Agent result = coordinator.perceive({ 'task': 'generate_quiz', 'user_id': session['user_id'], 'context': { 'topic_id': topic_id, 'recent_performance': [] } }).decide().act() if not result['success']: return jsonify({'error': result.get('error')}), 500 assessment_result = result['results'].get('AssessmentAgent', {}) topic = Topic.query.get(topic_id) return jsonify({ 'questions': assessment_result.get('questions', []), 'difficulty': 'adaptive', 'topic_name': topic.name, 'topic_id': topic_id, 'agent_metadata': assessment_result.get('metadata', {}) }) @app.route('/api/submit-answer', methods=['POST']) def submit_answer(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 data = request.json topic_id = data['topic_id'] question = data['question'] user_answer = data['user_answer'] correct_answer = data['correct_answer'] difficulty = data['difficulty'] is_correct = user_answer == correct_answer # Save quiz attempt attempt = QuizAttempt( user_id=session['user_id'], topic_id=topic_id, question=question, user_answer=user_answer, correct_answer=correct_answer, is_correct=is_correct, difficulty=difficulty ) db.session.add(attempt) db.session.commit() # Update knowledge state state = knowledge_tracker.update_knowledge( session['user_id'], topic_id, is_correct, difficulty ) # Generate explanation topic = Topic.query.get(topic_id) explanation = llm_service.explain_answer( question, user_answer, correct_answer, topic.name ) return jsonify({ 'is_correct': is_correct, 'explanation': explanation, 'new_knowledge_level': state.knowledge_level, 'confidence': state.confidence }) @app.route('/api/knowledge-state/', methods=['GET']) def get_knowledge_state(topic_id): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 state = knowledge_tracker.get_or_create_knowledge_state( session['user_id'], topic_id ) topic = Topic.query.get(topic_id) return jsonify({ 'topic_id': topic_id, 'topic_name': topic.name, 'knowledge_level': state.knowledge_level, 'confidence': state.confidence, 'practice_count': state.practice_count, 'last_practiced': state.last_practiced.isoformat() }) @app.route('/api/progress-summary', methods=['GET']) def get_progress_summary(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 summary = knowledge_tracker.get_progress_summary(session['user_id']) # Enhance with topic names for topic_list in ['weak_topics', 'strong_topics']: for item in summary[topic_list]: topic = Topic.query.get(item['id']) if topic: item['name'] = topic.name return jsonify(summary) @app.route('/api/next-topic', methods=['GET']) def get_next_topic(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 current_topic_id = request.args.get('current_topic_id', type=int) # Use Coordinator Agent result = coordinator.perceive({ 'task': 'recommend_topic', 'user_id': session['user_id'], 'context': { 'current_topic_id': current_topic_id } }).decide().act() if not result['success']: return jsonify({'message': 'No recommendations'}), 404 recommendation_result = result['results'].get('RecommendationAgent', {}) next_best = recommendation_result.get('next_best') if next_best: return jsonify({ 'id': next_best['topic_id'], 'name': next_best['name'], 'category': next_best['category'], 'difficulty': next_best['difficulty'], 'description': next_best['reason'], 'agent_recommendation': True }) return jsonify({'message': 'No recommendations available'}), 404 @app.route('/api/study-tips', methods=['GET']) def get_study_tips(): if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 summary = knowledge_tracker.get_progress_summary(session['user_id']) weak_topic_names = [] strong_topic_names = [] for item in summary['weak_topics']: topic = Topic.query.get(item['id']) if topic: weak_topic_names.append(topic.name) for item in summary['strong_topics']: topic = Topic.query.get(item['id']) if topic: strong_topic_names.append(topic.name) tips = llm_service.generate_study_tips(weak_topic_names, strong_topic_names) # Format the tips formatted_tips = format_study_tips(tips) return jsonify({'tips': formatted_tips}) @app.route('/api/check-code', methods=['POST']) def check_code(): """Execute Python code safely and return output""" if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 data = request.json user_code = data.get('code', '') # Security: limit execution time and dangerous operations if any(keyword in user_code.lower() for keyword in ['import os', 'import sys', 'eval', 'exec', 'open', 'file', '__import__']): return jsonify({ 'success': False, 'output': 'Error: Restricted operations detected. Please use only basic Python syntax.', 'error': True }) try: # Capture output import io import sys from contextlib import redirect_stdout output_buffer = io.StringIO() with redirect_stdout(output_buffer): # Create a restricted namespace namespace = {'__builtins__': { 'print': print, 'input': lambda x='': '', 'len': len, 'range': range, 'str': str, 'int': int, 'float': float, 'list': list, 'dict': dict, 'set': set, 'tuple': tuple, 'sum': sum, 'max': max, 'min': min, 'abs': abs, 'round': round, }} # Execute code exec(user_code, namespace) output = output_buffer.getvalue() return jsonify({ 'success': True, 'output': output if output else 'Code executed successfully (no output)', 'error': False }) except Exception as e: return jsonify({ 'success': False, 'output': f'Error: {str(e)}', 'error': True }) @app.route('/api/ask-challenge-hint', methods=['POST']) def ask_challenge_hint(): """Get hint for practice challenge using LLM""" if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 data = request.json # Use Coordinator Agent with Tutor Agent result = coordinator.perceive({ 'task': 'provide_hint', 'user_id': session['user_id'], 'context': { 'question': data.get('question'), 'challenge': data.get('challenge'), 'attempt_count': data.get('attempt_count', 1), 'frustration_level': 'normal' } }).decide().act() if not result['success']: return jsonify({'hint': 'Break the problem into smaller steps!'}), 200 tutor_result = result['results'] return jsonify({ 'hint': tutor_result.get('hint'), 'hint_level': tutor_result.get('hint_level'), 'agent': tutor_result.get('agent') }) @app.route('/api/agent-status', methods=['GET']) def get_agent_status(): """Get status of all agents""" if 'user_id' not in session: return jsonify({'error': 'Not logged in'}), 401 status = coordinator.get_agent_status() return jsonify(status) if __name__ == '__main__': init_db() port = int(os.environ.get("PORT", 7860)) app.run(debug=True, host='0.0.0.0', port=port)