Spaces:
Sleeping
Sleeping
| import json | |
| import re | |
| import time | |
| CHARS_PER_CHUNK = 5000 # ~2000 tokens of text — fits under both Groq and Gemini free limits | |
| QUESTIONS_PROMPT = """You are extracting problems and questions from a Control Systems textbook PDF. | |
| From the raw extracted text, identify each individual problem or question. | |
| Return a JSON array where each object has exactly these fields: | |
| - "question_number": string (e.g. "P1.1", "E2.3", "1") | |
| - "question": string (complete question text, preserve all sub-parts) | |
| - "type": one of "problem", "theory", "design", "mcq" | |
| - "difficulty": one of "easy", "medium", "hard" | |
| - "topics": array of strings (e.g. ["transfer function", "stability", "PID"]) | |
| - "has_diagram": boolean (true if the question references a figure or diagram) | |
| - "figure_reference": string or null (e.g. "Figure P1.1", "Fig. 3.2") | |
| - "page": number (page where question starts) | |
| - "answer": null | |
| - "images": [] | |
| Return ONLY a JSON array wrapped in ```json fences. No explanation, no other text.""" | |
| SOLUTIONS_PROMPT = """You are extracting solutions from a Control Systems solution manual PDF. | |
| From the raw extracted text, identify each solution entry. | |
| Return a JSON array where each object has exactly these fields: | |
| - "question_number": string matching the question number (e.g. "P1.1", "E2.3") | |
| - "answer": string (complete solution/answer text) | |
| - "page": number | |
| Return ONLY a JSON array wrapped in ```json fences. No explanation, no other text.""" | |
| def chunk_text(text: str, max_chars: int = CHARS_PER_CHUNK) -> list[str]: | |
| pages = re.split(r'(?=\n\n--- PAGE \d+ ---\n)', text) | |
| pages = [p for p in pages if p.strip()] | |
| chunks, current = [], "" | |
| for page in pages: | |
| if len(current) + len(page) > max_chars and current: | |
| chunks.append(current.strip()) | |
| current = page | |
| else: | |
| current += page | |
| if current.strip(): | |
| chunks.append(current.strip()) | |
| return chunks or [text[:max_chars]] | |
| def _parse_json_response(content: str) -> list: | |
| fenced = re.search(r"```(?:json)?\s*([\s\S]*?)```", content) | |
| json_str = fenced.group(1).strip() if fenced else content.strip() | |
| try: | |
| parsed = json.loads(json_str) | |
| return parsed if isinstance(parsed, list) else [] | |
| except json.JSONDecodeError: | |
| arr_match = re.search(r"\[[\s\S]*\]", json_str) | |
| if arr_match: | |
| try: | |
| return json.loads(arr_match.group()) | |
| except Exception: | |
| pass | |
| return [] | |
| def call_groq(client, prompt: str, text: str, max_retries: int = 3) -> list: | |
| for attempt in range(max_retries): | |
| try: | |
| response = client.chat.completions.create( | |
| model="llama-3.1-8b-instant", | |
| messages=[ | |
| {"role": "system", "content": prompt}, | |
| {"role": "user", "content": f"Extract from this text:\n\n{text}"}, | |
| ], | |
| temperature=0.1, | |
| max_tokens=4000, | |
| ) | |
| return _parse_json_response(response.choices[0].message.content or "") | |
| except Exception as e: | |
| if ("429" in str(e) or "rate_limit" in str(e)) and attempt < max_retries - 1: | |
| time.sleep(10 * (attempt + 1)) | |
| else: | |
| raise | |
| return [] | |
| def call_gemini(model, prompt: str, text: str, max_retries: int = 3) -> list: | |
| import google.generativeai as genai | |
| for attempt in range(max_retries): | |
| try: | |
| response = model.generate_content( | |
| f"{prompt}\n\nExtract from this text:\n\n{text}", | |
| generation_config=genai.types.GenerationConfig( | |
| temperature=0.1, | |
| max_output_tokens=4000, | |
| ), | |
| ) | |
| return _parse_json_response(response.text or "") | |
| except Exception as e: | |
| if ("429" in str(e) or "quota" in str(e).lower()) and attempt < max_retries - 1: | |
| time.sleep(30 * (attempt + 1)) | |
| else: | |
| raise | |
| return [] | |
| def match_solutions_to_questions(questions: list, solutions: list) -> list: | |
| sol_map = {s["question_number"]: s["answer"] for s in solutions} | |
| for q in questions: | |
| q["answer"] = sol_map.get(q.get("question_number", "")) | |
| return questions | |