File size: 22,110 Bytes
87112c5
 
 
5594657
 
87112c5
 
 
 
 
114117d
 
 
 
87112c5
114117d
87112c5
 
 
 
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711a6ba
87112c5
 
 
 
 
114117d
 
87112c5
 
 
114117d
87112c5
 
 
114117d
 
87112c5
 
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87112c5
114117d
 
 
 
 
5594657
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87112c5
114117d
 
 
 
 
08ae6f4
114117d
 
08ae6f4
114117d
 
 
 
 
 
 
87112c5
114117d
 
 
87112c5
08ae6f4
114117d
 
 
 
 
 
 
 
08ae6f4
114117d
 
 
08ae6f4
114117d
 
08ae6f4
114117d
87112c5
114117d
 
 
 
 
87112c5
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87112c5
 
711a6ba
114117d
711a6ba
 
114117d
 
 
711a6ba
114117d
08ae6f4
114117d
08ae6f4
 
 
 
711a6ba
 
114117d
 
 
 
711a6ba
114117d
 
 
 
 
711a6ba
114117d
 
 
711a6ba
114117d
 
 
 
711a6ba
114117d
 
 
711a6ba
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87112c5
 
114117d
 
 
711a6ba
114117d
 
87112c5
114117d
 
 
 
 
 
 
 
 
 
87112c5
114117d
711a6ba
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711a6ba
 
114117d
 
 
711a6ba
114117d
 
711a6ba
114117d
 
 
 
 
 
 
 
 
711a6ba
114117d
711a6ba
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711a6ba
 
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711a6ba
114117d
711a6ba
114117d
711a6ba
114117d
 
 
 
 
711a6ba
 
 
114117d
711a6ba
114117d
 
 
 
 
711a6ba
 
114117d
 
 
5594657
114117d
5594657
114117d
 
 
 
 
 
5594657
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5594657
 
114117d
 
 
711a6ba
114117d
5594657
114117d
 
 
 
 
 
5594657
114117d
5594657
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5594657
711a6ba
114117d
 
 
 
 
711a6ba
59c7938
 
 
 
 
 
 
 
711a6ba
114117d
 
 
 
 
 
 
 
 
 
711a6ba
 
114117d
 
 
711a6ba
59c7938
 
 
 
 
 
 
 
 
711a6ba
5594657
114117d
 
 
 
 
 
 
 
 
5594657
 
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5594657
114117d
 
 
 
 
 
 
85cc3b3
114117d
 
 
5594657
114117d
 
 
 
 
5594657
114117d
5594657
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
711a6ba
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5594657
114117d
 
 
 
 
 
 
 
 
 
5594657
 
114117d
 
 
7e25bba
114117d
 
 
 
 
 
5594657
711a6ba
114117d
 
 
 
 
 
 
 
 
 
5594657
 
87112c5
 
114117d
 
 
 
 
 
 
87112c5
 
5594657
114117d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5594657
 
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
import os
import json
import logging
import re
import time
from pathlib import Path
from dotenv import load_dotenv
from flask import Flask, request, jsonify
from flask_cors import CORS
import nltk
from nltk.tokenize import sent_tokenize
from groq import Groq, RateLimitError
from functools import wraps
from threading import Lock

# Load .env
env_path = Path(__file__).parent.parent / '.env'
if env_path.exists():
    load_dotenv(env_path)

# Groq client (lazy init)
_groq_client = None
_rate_limiter = Lock()
_last_request_time = 0
_min_request_interval = 2.1  # 2.1s = ~28 req/min (safe margin)

def get_groq_client():
    """Get or create Groq client"""
    global _groq_client
    if _groq_client is None:
        if not os.getenv('GROQ_API_KEY'):
            raise ValueError("GROQ_API_KEY not configured")
        _groq_client = Groq(api_key=os.getenv('GROQ_API_KEY'))
    return _groq_client

def rate_limited_call(func):
    """Decorator to enforce rate limiting"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        global _last_request_time
        with _rate_limiter:
            current_time = time.time()
            time_since_last = current_time - _last_request_time
            if time_since_last < _min_request_interval:
                sleep_time = _min_request_interval - time_since_last
                time.sleep(sleep_time)
            _last_request_time = time.time()
        return func(*args, **kwargs)
    return wrapper

# Setup Flask
app = Flask(__name__)
CORS(app)

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - [%(levelname)s] - %(name)s - %(message)s',
    datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger(__name__)

# Download NLTK data
try:
    nltk.download('punkt', quiet=True)
    nltk.download('punkt_tab', quiet=True)
except Exception as e:
    logger.warning(f"NLTK download failed: {e}")


@app.route('/health', methods=['GET'])
def health_check():
    """Health check"""
    return jsonify({
        'status': 'healthy',
        'ai_provider': 'Groq (Llama 3.3 70B)',
        'groq_configured': bool(os.getenv('GROQ_API_KEY')),
        'endpoints': {
            'test': '/api/restructure/',
            'content': '/api/study-content/',
            'recommendations': '/api/recommendations/'
        },
        'pipeline': 'Smart Batching (5-8 calls)',
        'version': '8.0 - Production Ready'
    }), 200


@app.route('/api/restructure/', methods=['POST'])
def generate_from_studykit():
    """Test question generation - optimized batching"""
    start = time.time()
    
    try:
        if not request.is_json:
            return jsonify({'status': 'error', 'error': 'Request must be JSON'}), 400

        data = request.get_json(force=True)
        content = data.get('content', '').strip()

        if not content:
            return jsonify({'status': 'error', 'error': 'Content required'}), 400

        logger.info(f"🎯 TEST GENERATION: {len(content)} chars")

        # Tokenize
        sentences = sent_tokenize(content)
        logger.info(f"πŸ“ Tokenized: {len(sentences)} sentences")

        # BiGNN Classification
        from question_api.classify_content import classify_sentences
        classifications = classify_sentences(sentences)
        
        # Fix tuple/dict issue
        if classifications and isinstance(classifications[0], tuple):
            classifications = [{'sentence': s, 'label': l} for s, l in classifications]
        
        logger.info(f"πŸ” Classified: {len(classifications)} sentences")

        # Generate questions (optimized)
        context = "\n".join([f"- {s['sentence']}" for s in classifications[:50]])
        mcqs, theories = generate_questions_optimized(context, 40, 20)
        
        elapsed = time.time() - start
        logger.info(f"βœ… Generated {len(mcqs)} MCQs + {len(theories)} theories in {elapsed:.2f}s")
        
        return jsonify({
            'status': 'success',
            'mcqs': mcqs,
            'theory': theories,
            'counts': {'mcqs': len(mcqs), 'theory': len(theories), 'total': len(mcqs) + len(theories)},
            'processing_time_seconds': round(elapsed, 2)
        }), 200

    except Exception as e:
        logger.exception("Test generation failed")
        return jsonify({'status': 'error', 'error': str(e)}), 500


@app.route('/api/study-content/', methods=['POST'])
def generate_study_content():
    """Study content generation - smart batching"""
    start = time.time()
    
    try:
        if not request.is_json:
            return jsonify({'status': 'error', 'error': 'Request must be JSON'}), 400

        data = request.get_json(force=True)
        topic = data.get('topic', '').strip()
        description = data.get('description', '').strip()

        if not topic:
            return jsonify({'status': 'error', 'error': 'Topic required'}), 400

        logger.info(f"πŸ“š STUDY CONTENT: {topic}")

        # Generate using optimized pipeline
        result = generate_study_content_optimized(topic, description)
        
        if not result:
            return jsonify({'status': 'error', 'error': 'Generation failed'}), 500

        elapsed = time.time() - start
        logger.info(f"βœ… Generated in {elapsed:.2f}s | Subtopics: {len(result['subtopics'])} | Terms: {len(result['terms'])} | Guide: {len(result['guide'])} chars")
        
        return jsonify({
            'status': 'success',
            'subtopics': result['subtopics'],
            'terms': result['terms'],
            'guide': result['guide'],
            'processing_time_seconds': round(elapsed, 2)
        }), 200

    except Exception as e:
        logger.exception("Study content failed")
        return jsonify({'status': 'error', 'error': str(e)}), 500


@app.route('/api/recommendations/', methods=['POST'])
def generate_recommendations():
    """Recommendations - 2 API calls"""
    start = time.time()
    
    try:
        if not request.is_json:
            return jsonify({'status': 'error', 'error': 'Request must be JSON'}), 400

        data = request.get_json(force=True)
        topic = data.get('topic', '').strip()

        if not topic:
            return jsonify({'status': 'error', 'error': 'Topic required'}), 400

        logger.info(f"πŸŽ₯ RECOMMENDATIONS: {topic}")

        result = generate_recommendations_optimized(topic)
        
        if not result:
            return jsonify({'status': 'error', 'error': 'Generation failed'}), 500
        
        elapsed = time.time() - start
        logger.info(f"βœ… Generated {len(result['videos'])} videos + {len(result['blogs'])} blogs in {elapsed:.2f}s")
        
        return jsonify({
            'status': 'success',
            'videos': result['videos'],
            'blogs': result['blogs'],
            'processing_time_seconds': round(elapsed, 2)
        }), 200
        
    except Exception as e:
        logger.exception("Recommendations failed")
        return jsonify({'status': 'error', 'error': str(e)}), 500


# ============================================================================
# OPTIMIZED GENERATION FUNCTIONS
# ============================================================================

@rate_limited_call
def call_groq(messages, temperature=0.5, max_tokens=4000):
    """Centralized Groq call with rate limiting"""
    client = get_groq_client()
    response = client.chat.completions.create(
        model="llama-3.3-70b-versatile",
        messages=messages,
        temperature=temperature,
        max_tokens=max_tokens,
    )
    return response.choices[0].message.content.strip()


def generate_questions_optimized(content, target_mcqs=40, target_theories=20):
    """Generate questions in large batches (4 calls total)"""
    mcqs = []
    theories = []
    
    try:
        # 2 batches for MCQs (20 each)
        logger.info("πŸ“ Generating MCQ batch 1/2...")
        batch1 = generate_mcq_batch(content[:1500], 20)
        mcqs.extend(batch1)
        
        logger.info("πŸ“ Generating MCQ batch 2/2...")
        batch2 = generate_mcq_batch(content[:1500], 20)
        mcqs.extend(batch2)
        
        # 2 batches for theories (10 each)
        logger.info("πŸ“ Generating theory batch 1/2...")
        theory1 = generate_theory_batch(content[:1500], 10)
        theories.extend(theory1)
        
        logger.info("πŸ“ Generating theory batch 2/2...")
        theory2 = generate_theory_batch(content[:1500], 10)
        theories.extend(theory2)
        
        # Ensure counts
        mcqs = mcqs[:target_mcqs]
        theories = theories[:target_theories]
        
        # Pad if needed
        while len(mcqs) < target_mcqs:
            mcqs.append({
                "stem": f"Question {len(mcqs)+1}?",
                "key": "Answer",
                "distractors": ["A", "B", "C"]
            })
        
        while len(theories) < target_theories:
            theories.append({
                "question": f"Question {len(theories)+1}?",
                "answer": "Detailed answer required."
            })
        
        return mcqs, theories
        
    except Exception as e:
        logger.error(f"Question generation failed: {e}")
        return (
            [{"stem": f"Q{i+1}?", "key": "A", "distractors": ["B", "C", "D"]} for i in range(target_mcqs)],
            [{"question": f"Q{i+1}?", "answer": "Answer"} for i in range(target_theories)]
        )


def generate_mcq_batch(content, count):
    """Generate MCQ batch"""
    prompt = f"""Generate EXACTLY {count} multiple-choice questions.

CONTENT:
{content}

Return ONLY valid JSON (no markdown):
{{
  "mcqs": [
    {{
      "stem": "Question?",
      "key": "Correct",
      "distractors": ["Wrong1", "Wrong2", "Wrong3"]
    }}
  ]
}}

EXACTLY {count} questions. Test understanding."""

    try:
        result_text = call_groq(
            [
                {"role": "system", "content": "You generate exam questions. Return only JSON."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.6,
            max_tokens=3000
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        return result.get('mcqs', [])[:count]
        
    except Exception as e:
        logger.error(f"MCQ batch failed: {e}")
        return []


def generate_theory_batch(content, count):
    """Generate theory batch"""
    prompt = f"""Generate EXACTLY {count} theory questions.

CONTENT:
{content}

Return ONLY valid JSON (no markdown):
{{
  "theory": [
    {{
      "question": "Question?",
      "answer": "2-3 paragraph detailed answer."
    }}
  ]
}}

EXACTLY {count} questions."""

    try:
        result_text = call_groq(
            [
                {"role": "system", "content": "You generate exam questions. Return only JSON."},
                {"role": "user", "content": prompt}
            ],
            temperature=0.6,
            max_tokens=4000
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        return result.get('theory', [])[:count]
        
    except Exception as e:
        logger.error(f"Theory batch failed: {e}")
        return []


def generate_study_content_optimized(topic, description):
    """Generate study content (5-7 calls)"""
    try:
        # CALL 1: All subtopics
        logger.info("πŸ“‹ Step 1/5: Generating 20 subtopics...")
        subtopics = generate_all_subtopics(topic, description, 20)
        
        # CALL 2: All terms
        logger.info("πŸ“‹ Step 2/5: Generating 30 terms...")
        terms = generate_all_terms(topic, description, 30)
        
        # CALL 3: Introduction
        logger.info("πŸ“‹ Step 3/5: Generating introduction...")
        intro = generate_introduction(topic, description)
        
        # CALL 4-5: Main content in 2 sections
        logger.info("πŸ“‹ Step 4/5: Generating main content part 1...")
        main1 = generate_guide_section(topic, "foundational concepts and principles", 600)
        
        logger.info("πŸ“‹ Step 5/5: Generating main content part 2...")
        main2 = generate_guide_section(topic, "advanced topics and applications", 600)
        
        # Assemble guide
        guide = f"""# {topic}: Complete Study Guide

{intro}

## Part 1: Foundational Concepts

{main1}

## Part 2: Advanced Applications

{main2}

## Summary

This guide covers the essential aspects of {topic}. Focus on understanding core principles and connecting theoretical knowledge to practical applications. Regular review and active engagement lead to mastery."""

        return {'subtopics': subtopics, 'terms': terms, 'guide': guide}
        
    except Exception as e:
        logger.error(f"Study content failed: {e}")
        return None


def generate_all_subtopics(topic, description, count):
    """Generate all subtopics in one call"""
    context = f" Context: {description}" if description else ""
    
    prompt = f"""Generate EXACTLY {count} subtopics for "{topic}".{context}

Return ONLY valid JSON:
{{
  "subtopics": [
    "Title - Subtitle: 2-3 sentences (40-70 words) explaining what students learn and why it matters."
  ]
}}

EXACTLY {count} subtopics."""

    try:
        result_text = call_groq(
            [{"role": "system", "content": "You are a curriculum designer. Return only JSON."},
             {"role": "user", "content": prompt}],
            temperature=0.5,
            max_tokens=3000
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        subtopics = result.get('subtopics', [])
        
        while len(subtopics) < count:
            subtopics.append(f"Topic {len(subtopics)+1} - Study Area: Explore {topic}")
        
        return subtopics[:count]
        
    except Exception as e:
        logger.error(f"Subtopics failed: {e}")
        return [f"Subtopic {i+1}: Explore {topic}" for i in range(count)]


def generate_all_terms(topic, description, count):
    """Generate all terms in one call"""
    context = f" Context: {description}" if description else ""
    
    prompt = f"""Generate EXACTLY {count} key terms for "{topic}".{context}

Return ONLY valid JSON:
{{
  "terms": [
    "Term - Category: Definition + Context + Example + Significance (50-100 words)"
  ]
}}

EXACTLY {count} terms."""

    try:
        result_text = call_groq(
            [{"role": "system", "content": "You are a terminology expert. Return only JSON."},
             {"role": "user", "content": prompt}],
            temperature=0.5,
            max_tokens=4000
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        terms = result.get('terms', [])
        
        while len(terms) < count:
            terms.append(f"Term {len(terms)+1} - Concept: Important concept in {topic}")
        
        return terms[:count]
        
    except Exception as e:
        logger.error(f"Terms failed: {e}")
        return [f"Term {i+1}: Key concept in {topic}" for i in range(count)]


def generate_introduction(topic, description):
    """Generate introduction"""
    context = f"\n\nContext: {description}" if description else ""
    
    prompt = f"""Write a comprehensive introduction for "{topic}".{context}

FORMAT REQUIREMENTS:
- Markdown only
- Start with ## Introduction
- Use many short paragraphs
- Each paragraph 3-5 sentences
- Blank line between paragraphs
- 300-500 words
"""

    try:
        return call_groq(
            [{"role": "system", "content": "You are an academic writer."},
             {"role": "user", "content": prompt}],
            temperature=0.6,
            max_tokens=800
        )
    except Exception as e:
        logger.error(f"Introduction failed: {e}")
        return f"## Introduction\n\nThis guide covers {topic}."


def generate_guide_section(topic, focus, target_words):
    """Generate guide section"""
    prompt = f"""Write educational content about {topic}, focusing on {focus}.

FORMAT REQUIREMENTS:
- Markdown only
- Start with ## {focus}
- Exactly 3 subsections
- Each subsection starts with ###
- Each subsection has 3–4 paragraphs
- Blank line between every paragraph
- No bullets
"""

    try:
        return call_groq(
            [{"role": "system", "content": "You are an expert educator."},
             {"role": "user", "content": prompt}],
            temperature=0.6,
            max_tokens=2000
        )
    except Exception as e:
        logger.error(f"Section failed: {e}")
        return f"### Content\n\nDetailed exploration of {focus} in {topic}."


def generate_recommendations_optimized(topic):
    """Generate recommendations (2 calls)"""
    try:
        logger.info("πŸŽ₯ Generating 6 videos...")
        videos = generate_all_videos(topic, 6)
        
        logger.info("πŸ“° Generating 4 blogs...")
        blogs = generate_all_blogs(topic, 4)
        
        return {'videos': videos, 'blogs': blogs}
    except Exception as e:
        logger.error(f"Recommendations failed: {e}")
        return None


def generate_all_videos(topic, count):
    """Generate all videos in one call"""
    prompt = f"""Generate EXACTLY {count} YouTube video recommendations for "{topic}".

Return ONLY valid JSON:
{{
  "videos": [
    {{
      "title": "Video Title",
      "url": "https://youtube.com/watch?v=ID",
      "channel": "Channel Name",
      "thumbnail": "https://img.youtube.com/vi/ID/maxresdefault.jpg",
      "description": "40-60 word description"
    }}
  ]
}}

Use real channels: 3Blue1Brown, Khan Academy, Crash Course, etc.
EXACTLY {count} videos."""

    try:
        result_text = call_groq(
            [{"role": "system", "content": "You are a resource curator. Return only JSON."},
             {"role": "user", "content": prompt}],
            temperature=0.4,
            max_tokens=2000
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        videos = result.get('videos', [])
        
        for video in videos:
            if 'url' in video and 'youtube.com' in video['url']:
                video_id = extract_youtube_id(video['url'])
                if video_id:
                    video['thumbnail'] = f"https://img.youtube.com/vi/{video_id}/maxresdefault.jpg"
        
        return videos[:count]
        
    except Exception as e:
        logger.error(f"Videos failed: {e}")
        return [
            {
                "title": f"Learn {topic} - Video {i+1}",
                "url": f"https://youtube.com/results?search_query={topic.replace(' ', '+')}",
                "channel": "Educational Channel",
                "thumbnail": "https://img.youtube.com/vi/PLACEHOLDER/maxresdefault.jpg",
                "description": f"Educational video about {topic}."
            }
            for i in range(count)
        ]


def generate_all_blogs(topic, count):
    """Generate all blogs in one call"""
    prompt = f"""Generate EXACTLY {count} blog recommendations for "{topic}".

Return ONLY valid JSON:
{{
  "blogs": [
    {{
      "title": "Article Title",
      "url": "https://site.com/article",
      "site": "Site Name",
      "description": "40-60 word description"
    }}
  ]
}}

Use real sites: Medium, Towards Data Science, etc.
EXACTLY {count} articles."""

    try:
        result_text = call_groq(
            [{"role": "system", "content": "You are a resource curator. Return only JSON."},
             {"role": "user", "content": prompt}],
            temperature=0.4,
            max_tokens=1500
        )
        
        result_text = clean_json(result_text)
        result = json.loads(result_text)
        blogs = result.get('blogs', [])
        
        for blog in blogs:
            if 'site' not in blog and 'url' in blog:
                blog['site'] = extract_domain(blog['url'])
        
        return blogs[:count]
        
    except Exception as e:
        logger.error(f"Blogs failed: {e}")
        return [
            {
                "title": f"Article {i+1}: {topic}",
                "url": f"https://www.google.com/search?q={topic.replace(' ', '+')}",
                "site": "Educational Website",
                "description": f"Article about {topic}."
            }
            for i in range(count)
        ]


# ============================================================================
# UTILITY FUNCTIONS
# ============================================================================

def clean_json(text):
    """Remove markdown from JSON"""
    text = re.sub(r'^```json\s*\n?', '', text, flags=re.MULTILINE)
    text = re.sub(r'^```\s*\n?', '', text, flags=re.MULTILINE)
    text = re.sub(r'\n?```\s*$', '', text, flags=re.MULTILINE)
    return text.strip()


def extract_youtube_id(url):
    """Extract YouTube ID"""
    match = re.search(r'(?:v=|/)([a-zA-Z0-9_-]{11})', url)
    return match.group(1) if match else None


def extract_domain(url):
    """Extract domain"""
    match = re.search(r'https?://(?:www\.)?([^/]+)', url)
    return match.group(1) if match else "Website"


@app.errorhandler(404)
def not_found(e):
    return jsonify({'status': 'error', 'error': 'Not found'}), 404


@app.errorhandler(500)
def internal_error(e):
    logger.error(f"500 error: {e}")
    return jsonify({'status': 'error', 'error': 'Internal error'}), 500


if __name__ == '__main__':
    if not os.getenv('GROQ_API_KEY'):
        print("\n⚠️  WARNING: GROQ_API_KEY not found!\n")
    
    print("\n" + "="*60)
    print("πŸš€ GROQ OPTIMIZED API v8.0 - PRODUCTION READY")
    print("="*60)
    print("Architecture: Smart Batching (5-8 calls)")
    print("Rate Limit: 2.1s spacing (~28 req/min)")
    print("Expected Time: 15-25s per request")
    print("\nEndpoints:")
    print("  POST /api/restructure/ - Test generation")
    print("  POST /api/study-content/ - Study content")
    print("  POST /api/recommendations/ - Recommendations")
    print("  GET /health - Health check")
    print("="*60 + "\n")
    
    port = int(os.environ.get('PORT', 7860))
    app.run(host='0.0.0.0', port=port, debug=False, threaded=True)