""" Utility functions for the AI Study Assistant """ import re import os from typing import List, Dict, Tuple import json from datetime import datetime def create_directory(path: str) -> None: """Create a directory if it doesn't exist.""" if not os.path.exists(path): os.makedirs(path) print(f"✓ Created directory: {path}") def read_file(filepath: str) -> str: """Read and return file contents.""" try: with open(filepath, 'r', encoding='utf-8') as f: return f.read() except FileNotFoundError: raise FileNotFoundError(f"File not found: {filepath}") except Exception as e: raise Exception(f"Error reading file {filepath}: {str(e)}") def write_file(filepath: str, content: str) -> None: """Write content to a file.""" try: os.makedirs(os.path.dirname(filepath) or '.', exist_ok=True) with open(filepath, 'w', encoding='utf-8') as f: f.write(content) except Exception as e: raise Exception(f"Error writing to file {filepath}: {str(e)}") def load_prompt_template(prompt_name: str, mode: str = "normal") -> str: """ Load a prompt template from file. Args: prompt_name: Name of the prompt (e.g., 'summary', 'quiz') mode: Mode for the prompt (e.g., 'normal', 'detailed') Returns: The prompt template content """ from config import PROMPTS_DIR, MODES_DIR # Try mode-specific prompt first mode_path = os.path.join(MODES_DIR, f"{mode}.txt") base_path = os.path.join(PROMPTS_DIR, f"{prompt_name}_prompt.txt") try: return read_file(base_path) except FileNotFoundError: raise FileNotFoundError( f"Prompt template not found: {prompt_name}_prompt.txt\n" f"Expected at: {base_path}" ) def extract_text_from_pdf(pdf_path: str) -> str: """ Extract text from a PDF file. Args: pdf_path: Path to the PDF file Returns: Extracted text content """ try: from PyPDF2 import PdfReader text = [] with open(pdf_path, 'rb') as file: pdf_reader = PdfReader(file) for page_num, page in enumerate(pdf_reader.pages): page_text = page.extract_text() if page_text: text.append(page_text) return '\n'.join(text) except ImportError: raise ImportError("PyPDF2 not installed. Install with: pip install PyPDF2") except Exception as e: raise Exception(f"Error extracting PDF: {str(e)}") def extract_text_from_file(file_path: str) -> str: """ Extract text from various file types. Args: file_path: Path to the file Returns: Extracted text content """ file_ext = os.path.splitext(file_path)[1].lower() if file_ext == '.pdf': return extract_text_from_pdf(file_path) elif file_ext in ['.txt', '.md']: return read_file(file_path) else: raise ValueError(f"Unsupported file type: {file_ext}") def truncate_text(text: str, max_length: int) -> str: """ Truncate text to maximum length while preserving words. Args: text: The text to truncate max_length: Maximum number of characters Returns: Truncated text """ if len(text) <= max_length: return text # Truncate at word boundary truncated = text[:max_length].rsplit(' ', 1)[0] return truncated + "..." def clean_text(text: str) -> str: """ Clean and normalize text. Args: text: Text to clean Returns: Cleaned text """ # Remove excess whitespace text = re.sub(r'\s+', ' ', text) # Remove special characters but keep punctuation text = re.sub(r'[^\w\s.,!?;:\-\'"()]', '', text) return text.strip() def extract_json_from_text(text: str) -> Dict: """ Extract JSON object from text response. Args: text: Text potentially containing JSON Returns: Parsed JSON object or empty dict if not found """ try: # Look for JSON-like patterns json_match = re.search(r'\{.*\}', text, re.DOTALL) if json_match: json_str = json_match.group(0) return json.loads(json_str) except json.JSONDecodeError: pass return {} def format_quiz_response(quiz_text: str) -> List[Dict]: """ Parse and format quiz response into structured questions. Args: quiz_text: Raw quiz text from LLM Returns: List of question dictionaries """ questions = [] # Split by question markers q_pattern = r'(?:Q\d+\.|Question \d+:|^\d+\.)' raw_questions = re.split(q_pattern, quiz_text)[1:] # Skip first empty part for idx, q_text in enumerate(raw_questions, 1): questions.append({ 'id': idx, 'question': q_text.strip()[:200], # First 200 chars 'raw_text': q_text.strip() }) return questions[:5] # Return max 5 questions def format_summary(text: str) -> str: """ Format summary response for better readability. Args: text: Raw summary text Returns: Formatted summary """ # Add line breaks between paragraphs paragraphs = text.split('\n\n') formatted = '\n\n'.join(p.strip() for p in paragraphs if p.strip()) return formatted def log_event(event_type: str, details: str) -> None: """ Log events for debugging and monitoring. Args: event_type: Type of event (e.g., 'API_CALL', 'ERROR', 'VALIDATION') details: Event details """ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") log_message = f"[{timestamp}] {event_type}: {details}" print(log_message) # Optionally save to log file try: with open("app.log", "a", encoding='utf-8') as log_file: log_file.write(log_message + "\n") except: pass def validate_api_response(response: str, min_length: int = 10) -> Tuple[bool, str]: """ Validate API response quality. Args: response: The response to validate min_length: Minimum acceptable length Returns: Tuple of (is_valid, message) """ if not response: return False, "Empty response from API" if len(response.strip()) < min_length: return False, f"Response too short (minimum {min_length} characters)" if response.lower().startswith("error"): return False, "API returned an error" return True, "Valid response" def format_error_message(error: Exception) -> str: """ Format error messages for display to user. Args: error: The exception Returns: User-friendly error message """ error_str = str(error) if "API" in error_str: return f"🔴 API Error: {error_str}\n\nPlease check your API key in config.py" elif "file" in error_str.lower(): return f"🔴 File Error: {error_str}\n\nEnsure the file exists and is readable" else: return f"🔴 Error: {error_str}"