File size: 20,529 Bytes
25732fb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
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/<id>',
            'learning': '/api/generate-lesson',
            'quiz': '/api/generate-quiz, /api/submit-answer',
            'progress': '/api/progress-summary, /api/knowledge-state/<id>',
            '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/<int:topic_id>', 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'<pre><code class="python">\1</code></pre>', content, flags=re.DOTALL)
    content = re.sub(r'```\n(.*?)\n```', r'<pre><code>\1</code></pre>', content, flags=re.DOTALL)
    
    # Convert markdown headers to HTML
    content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', content, flags=re.MULTILINE)
    content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', content, flags=re.MULTILINE)
    content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', content, flags=re.MULTILINE)
    
    # Convert markdown bold to HTML
    content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', content)
    
    # Convert inline code to HTML (before lists to preserve code in lists)
    content = re.sub(r'`([^`]+)`', r'<code>\1</code>', 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('</ul>')
                in_list = False
            continue
        
        # Handle list items
        if line.startswith('- '):
            if not in_list:
                result_lines.append('<ul>')
                in_list = True
            result_lines.append(f'<li>{line[2:]}</li>')
        else:
            if in_list:
                result_lines.append('</ul>')
                in_list = False
            result_lines.append(line)
    
    if in_list:
        result_lines.append('</ul>')
    
    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 '<pre>' in line:
            in_pre = True
            formatted_lines.append(line)
            continue
        elif '</pre>' 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('<h') or line.startswith('<ul>') or line.startswith('</ul>') or line.startswith('<li>'):
            formatted_lines.append(line)
        else:
            # Wrap non-HTML content in paragraph
            formatted_lines.append(f'<p>{line}</p>')
    
    # 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'<strong>\1</strong>', 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('</ul>')
                in_list = False
            formatted_lines.append(f'<h3 style="color: var(--primary-color); margin-top: 1.5rem; margin-bottom: 0.75rem;">{line}</h3>')
        # Check if it's a bullet point
        elif line.startswith('- ') or line.startswith('• '):
            if not in_list:
                formatted_lines.append('<ul style="list-style-type: disc; margin-left: 1.5rem; line-height: 1.8;">')
                in_list = True
            # Remove the bullet character
            text = line[2:] if line.startswith('- ') else line[2:]
            formatted_lines.append(f'<li style="margin-bottom: 1rem; color: #4b5563;">{text}</li>')
        else:
            if in_list:
                formatted_lines.append('</ul>')
                in_list = False
            formatted_lines.append(f'<p style="margin-bottom: 0.75rem; color: #4b5563;">{line}</p>')
    
    if in_list:
        formatted_lines.append('</ul>')
    
    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/<int:topic_id>', 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)