Spaces:
Sleeping
Sleeping
| """ | |
| 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}" | |