File size: 4,781 Bytes
87112c5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
import logging
from pathlib import Path
from dotenv import load_dotenv
import google.generativeai as genai
from concurrent.futures import ThreadPoolExecutor, as_completed
import sys

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

sys.path.append(os.path.dirname(__file__))
from utils import parse_restructured_text

logger = logging.getLogger(__name__)

# Get Gemini API key
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
if not GEMINI_API_KEY:
    raise EnvironmentError("GEMINI_API_KEY not set")

# Configure Gemini
genai.configure(api_key=GEMINI_API_KEY)
model = genai.GenerativeModel("gemini-1.5-flash-latest")


def build_batch_prompt(mcqs, theories, batch_num):
    """Build OPTIMIZED prompt for one batch."""
    prompt = f"""Reformat into {len(mcqs)} MCQs and {len(theories)} theory questions. Be CONCISE.

MCQs ({len(mcqs)} total):
{chr(10).join(f'{i+1}. {q}' for i, q in enumerate(mcqs))}

Theory ({len(theories)} total):
{chr(10).join(f'{i+1}. {q}' for i, q in enumerate(theories))}

FORMAT (NO extra text):

MCQ 1
Stem: [question]
Key: [correct answer]
Distractors:
- [wrong 1]
- [wrong 2]
- [wrong 3]

Theory 1
Question: [question]
Answer: [2-3 sentences]

Continue for all questions. NO commentary.
"""
    return prompt


def process_batch(batch_data):
    """Process one batch through Gemini with SPEED optimization."""
    batch_num, mcqs, theories = batch_data
    
    try:
        logger.info(f"Processing batch {batch_num}: {len(mcqs)} MCQs, {len(theories)} theories")
        
        prompt = build_batch_prompt(mcqs, theories, batch_num)
        
        # SPEED OPTIMIZED: Lower temperature, higher top_p, reduced tokens
        response = model.generate_content(
            prompt,
            generation_config={
                'temperature': 0.5,      # REDUCED from 0.7 for faster, more deterministic output
                'top_p': 0.9,            # REDUCED from 0.95
                'max_output_tokens': 3072,  # REDUCED from 4096
            },
            request_options={'timeout': 30}  # 30 second timeout per batch
        )
        
        text = getattr(response, "text", "").strip()
        
        if not text:
            raise ValueError(f"Empty response from Gemini for batch {batch_num}")
        
        # Parse the structured output
        mcqs_parsed, theories_parsed = parse_restructured_text(text)
        
        logger.info(f"Batch {batch_num} complete: {len(mcqs_parsed)} MCQs, {len(theories_parsed)} theories")
        
        return mcqs_parsed, theories_parsed
        
    except Exception as e:
        logger.error(f"Batch {batch_num} failed: {str(e)}")
        raise


def restructure_questions_parallel(generated_mcqs, generated_theory):
    """
    OPTIMIZED: Restructure 40 MCQs + 20 Theory questions faster.
    
    Uses 2 parallel batches with optimized Gemini settings.
    Target time: 20-30 seconds (down from 60+ seconds)
    """
    if len(generated_mcqs) < 40:
        logger.warning(f"Expected 40 MCQs, got {len(generated_mcqs)}")
    if len(generated_theory) < 20:
        logger.warning(f"Expected 20 theories, got {len(generated_theory)}")
    
    # Ensure we have exactly 40 MCQs and 20 theories
    generated_mcqs = generated_mcqs[:40]
    generated_theory = generated_theory[:20]
    
    # Split into 2 batches
    batches = [
        (1, generated_mcqs[:20], generated_theory[:10]),
        (2, generated_mcqs[20:40], generated_theory[10:20]),
    ]
    
    final_mcqs = []
    final_theory = []
    
    logger.info("Starting parallel Gemini processing (2 batches)")
    
    try:
        # OPTIMIZED: Process both batches in parallel with 60s total timeout
        with ThreadPoolExecutor(max_workers=2) as executor:
            future_to_batch = {
                executor.submit(process_batch, batch): batch[0] 
                for batch in batches
            }
            
            for future in as_completed(future_to_batch, timeout=60):  # REDUCED from 120s
                batch_num = future_to_batch[future]
                try:
                    mcqs, theories = future.result()
                    final_mcqs.extend(mcqs)
                    final_theory.extend(theories)
                    logger.info(f"Collected batch {batch_num} results")
                except Exception as e:
                    logger.error(f"Failed to collect batch {batch_num}: {str(e)}")
                    raise
    
    except TimeoutError:
        raise RuntimeError("Gemini processing timeout after 60 seconds")
    
    logger.info(f"Complete: {len(final_mcqs)} MCQs, {len(final_theory)} theories")
    
    return {
        "mcqs": final_mcqs,
        "theory": final_theory
    }