""" Grammar Question Generation and Validation Module This module provides endpoints for: - Generating fill-in-the-blank grammar questions at various difficulty levels - Batch validating user answers with AI-powered feedback - Providing hints for incorrect answers All AI operations are powered by Cohere's API v2. """ import logging import os from typing import Optional, Dict, Any, List import requests from flask import Blueprint, jsonify, request, current_app # ------------------------------------------------------------------------------ # Configuration Constants # ------------------------------------------------------------------------------ COHERE_API_URL = 'https://api.cohere.com/v2/chat' COHERE_MODEL = 'command-r-08-2024' # Token limits for different operations TOKEN_LIMITS = { 'validation': 5, 'answer_validation_detailed': 200, 'hint_generation': 250, 'question_generation': 1000 } # Request timeouts (seconds) API_TIMEOUT = 30 # Difficulty levels VALID_DIFFICULTIES = ['basic', 'intermediate', 'expert'] # Validation response types VALIDATION_RESPONSES = ['Grammar', 'Not Grammar', 'ask grammar topics'] # ------------------------------------------------------------------------------ # Blueprint Setup # ------------------------------------------------------------------------------ questions_bp = Blueprint('questions', __name__) # Configure logging logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) # ------------------------------------------------------------------------------ # Helper Functions # ------------------------------------------------------------------------------ def _get_cohere_headers() -> Optional[Dict[str, str]]: """ Get Cohere API headers with authentication. Prefers API key from Flask app config, falls back to environment variable. Returns: Dict containing Authorization and Content-Type headers, or None if key not found. """ api_key = current_app.config.get('COHERE_API_KEY') or os.getenv('COHERE_API_KEY', '') if not api_key: logger.error('COHERE_API_KEY is not configured') return None return { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json', } def _extract_text_from_cohere_v2_response(response_json: Dict[str, Any]) -> str: """ Extract text content from Cohere API v2 response. The v2 /chat endpoint returns: { "message": { "content": [ {"type": "text", "text": "..."} ] } } Args: response_json: The JSON response from Cohere API Returns: Extracted text content or empty string if not found """ message = response_json.get('message', {}) content = message.get('content', []) if isinstance(content, list) and content: first_block = content[0] if isinstance(first_block, dict): return (first_block.get('text') or '').strip() return '' def _call_cohere_api(prompt: str, max_tokens: int, temperature: float = 0.7) -> Optional[str]: """ Make a call to Cohere API with standardized error handling. Args: prompt: The prompt to send to the AI max_tokens: Maximum tokens for the response temperature: Temperature for response generation (0.0-1.0) Returns: The AI response text, or None if an error occurred """ headers = _get_cohere_headers() if not headers: logger.error('Cannot call Cohere API: headers not available') return None payload = { 'model': COHERE_MODEL, 'messages': [ {'role': 'user', 'content': prompt} ], 'max_tokens': max_tokens, 'temperature': temperature } try: response = requests.post( COHERE_API_URL, json=payload, headers=headers, timeout=API_TIMEOUT ) if response.status_code == 200: return _extract_text_from_cohere_v2_response(response.json()) else: logger.error( f'Cohere API returned status {response.status_code}: {response.text}' ) return None except requests.exceptions.Timeout: logger.error(f'Cohere API request timed out after {API_TIMEOUT} seconds') return None except requests.exceptions.RequestException as e: logger.error(f'Cohere API request failed: {str(e)}') return None except Exception as e: logger.error(f'Unexpected error calling Cohere API: {str(e)}') return None def _validate_input_length(text: str, max_length: int = 500) -> bool: """ Validate that input text doesn't exceed maximum length. Args: text: Input text to validate max_length: Maximum allowed length Returns: True if valid, False otherwise """ return len(text.strip()) <= max_length def _get_question_generation_prompt(topic: str, difficulty: str) -> str: """ Get the appropriate prompt for question generation based on difficulty. Args: topic: The grammar topic difficulty: The difficulty level (basic, intermediate, expert) Returns: The formatted prompt string """ if difficulty == 'basic': return f""" Generate five **completely new and unique** very basic-level fill-in-the-blank grammar questions **every time** on the topic '{topic}'. ### Rules: - Generate five unique fill-in-the-blank grammar questions based on the topic '{topic}'. - Each question must have exactly one blank represented by '_______' (not two blanks or underscores inside the sentence). - Each question must have a different theme for variety. - Use different sentence structures; avoid predictable patterns. - Avoid long words or abstract concepts. - Focus on the topic '{topic}', and ensure the blank is the key part of speech. - Each question must include the correct answer in parentheses at the end. - Do not include any explanations or instructions—only the five questions. """ elif difficulty == 'intermediate': return f""" Generate five **completely new and unique** intermediate-level fill-in-the-blank grammar questions **every time** on the topic '{topic}'. ### Rules: - Generate five unique fill-in-the-blank grammar questions based on the topic '{topic}'. - Each question must have exactly one blank represented by '_______'. - Slightly more challenging than basic-level; use a wider range of sentence structures and vocabulary. - Each question must have a different theme. - Sentences should be longer and include more detail. - Focus on the topic '{topic}', and ensure the blank is the key part of speech. - Each question must include the correct answer in parentheses at the end. - Do not include any explanations or instructions—only the five questions. """ else: # expert return f""" Generate five **completely new and unique** advanced-level (C1) fill-in-the-blank grammar questions **every time** on the topic '{topic}'. ### Rules: - Generate five unique fill-in-the-blank grammar questions based on the topic '{topic}'. - Each question must have exactly one blank represented by '_______'. - More challenging than intermediate (C1); require expert-level mastery of grammar and context. - Ensure varied and sophisticated vocabulary; avoid basic words. - Each question should require nuanced comprehension; test advanced grammar patterns. - The blank must be the key part of the sentence (not an obvious answer). - Each question must include the correct answer in parentheses at the end. - Do not include any explanations or instructions—only the five questions. """ # ------------------------------------------------------------------------------ # Core Functions # ------------------------------------------------------------------------------ def validate_topic(topic: str) -> str: """ Validate whether a given topic is related to English grammar. Args: topic: The topic to validate Returns: One of: 'Grammar', 'Not Grammar', 'ask grammar topics', or an error message """ if not _validate_input_length(topic, max_length=200): return 'Not Grammar' validation_prompt = f""" You are a highly knowledgeable AI grammar expert. Your task is to evaluate whether the given topic relates to **English grammar** or not. **Input Topic:** "{topic}" ### **Instructions:** - If the input **exactly refers to** grammar concepts (such as **parts of speech**, **verb tenses**, **sentence structure**, **grammar rules**, etc.), respond with `"Grammar"`. - If the input **seems to be a general question or concept** that is **not directly related to grammar**, such as general knowledge, science, history, or unrelated fields, respond with `"Not Grammar"`. - If the input is in the form of a **question** (e.g., "What is subject-verb agreement?"), respond with `"ask grammar topics"`. - If the topic refers to a **specific grammar concept** (e.g., **noun**, **verb**, **preposition**, **past tense**, etc.), always classify it as `"Grammar"`. - **Do not include any explanations or examples**. Your answer must only be `"Grammar"`, `"Not Grammar"`, or `"ask grammar topics"`, depending on whether the topic is relevant to grammar. - If the input is **unclear**, err on the side of classifying it as `"Not Grammar"` rather than `"Grammar"`. Your response must only be one of these three options: - `"Grammar"` - `"Not Grammar"` - `"ask grammar topics"` No extra text or explanation. """ result = _call_cohere_api( validation_prompt, max_tokens=TOKEN_LIMITS['validation'], temperature=0.3 ) if result is None: return 'Error: Unable to validate topic' if result not in VALIDATION_RESPONSES: return 'Not Grammar' return result def validate_single_answer(topic: str, question: str, user_answer: str) -> str: """ Validate a single answer using AI. Args: topic: The grammar topic question: The question being answered user_answer: The user's answer Returns: Validation response from the AI """ prompt = f""" You are a highly knowledgeable grammar assistant. Validate whether the user's answer to the following question is correct or not based on {topic}. If the answer is incorrect, provide a helpful hint. Topic: {topic} Question: "{question}" User's Answer: "{user_answer}" Is the answer correct? If not, please explain why and give a hint. """ result = _call_cohere_api( prompt, max_tokens=TOKEN_LIMITS['answer_validation_detailed'], temperature=0.7 ) if result is None: return 'Error: Unable to validate answer' return result def generate_hint(topic: str, question: str, user_answer: str) -> str: """ Generate a helpful hint for an incorrect answer. Args: topic: The grammar topic question: The question user_answer: The user's incorrect answer Returns: A helpful hint without revealing the answer """ prompt = f""" You are a highly skilled grammar assistant. Your task is to generate a helpful hint for the user to improve their answer based on the following question. Topic: {topic} Question: "{question}" User's Answer: "{user_answer}" If the user's answer is incorrect, provide a specific, actionable hint to help the user correct their answer. The hint should include: - Explanation of the error made by the user. - A hint on the correct grammatical structure or word form. - A hint on how to structure the sentence correctly **without revealing the exact answer**. Please make sure the hint is **clear** and **helpful** for the user, **without revealing the correct answer**. """ result = _call_cohere_api( prompt, max_tokens=TOKEN_LIMITS['hint_generation'], temperature=0.7 ) if result is None: return 'Error: Unable to generate hint' return result # ------------------------------------------------------------------------------ # API Endpoints # ------------------------------------------------------------------------------ @questions_bp.post('/generate-questions') def generate_questions(): """ Generate grammar questions based on topic and difficulty. Expected JSON payload: { "topic": "string", "difficulty": "basic" | "intermediate" | "expert" } Returns: JSON response with generated questions or error message """ try: data = request.get_json() if not data: return jsonify({'error': 'Request body must be JSON'}), 400 # Extract and validate inputs topic = data.get('topic', '').strip() difficulty = data.get('difficulty', 'basic').lower() if not topic: return jsonify({'error': 'Topic is required'}), 400 if not _validate_input_length(topic, max_length=200): return jsonify({'error': 'Topic exceeds maximum length of 200 characters'}), 400 if difficulty not in VALID_DIFFICULTIES: return jsonify({ 'error': f'Invalid difficulty level. Must be one of: {", ".join(VALID_DIFFICULTIES)}' }), 400 # Validate topic is grammar-related validation_result = validate_topic(topic) if validation_result.startswith('Error:'): logger.error(f'Topic validation error: {validation_result}') return jsonify({'error': 'Unable to validate topic at this time'}), 500 if validation_result != 'Grammar': return jsonify({ 'message': 'Please enter a valid **grammar topic**, not a general word or unrelated question.' }), 400 logger.info(f'Generating {difficulty} questions for topic: {topic}') # Generate questions prompt = _get_question_generation_prompt(topic, difficulty) result = _call_cohere_api( prompt, max_tokens=TOKEN_LIMITS['question_generation'], temperature=0.8 ) if result is None: return jsonify({ 'error': 'Failed to generate questions', 'details': 'Unable to reach AI service' }), 500 return jsonify({'text': result}), 200 except Exception as e: logger.exception(f'Unexpected error in generate_questions: {str(e)}') return jsonify({'error': 'An unexpected error occurred'}), 500 @questions_bp.post('/validate-all-answers') def validate_all_answers(): """ Validate multiple answers at once (batch validation). Expected JSON payload: { "questions": [ { "topic": "string", "question": "string", "user_answer": "string" } ] } Returns: JSON response with validation results for all questions """ try: data = request.get_json() if not data: return jsonify({'error': 'Request body must be JSON'}), 400 questions = data.get('questions', []) if not questions: return jsonify({'error': 'No questions provided'}), 400 if not isinstance(questions, list): return jsonify({'error': 'Questions must be an array'}), 400 if len(questions) > 50: return jsonify({'error': 'Maximum 50 questions allowed per request'}), 400 validation_results = [] for item in questions: if not isinstance(item, dict): validation_results.append({ 'error': 'Invalid question format' }) continue topic = item.get('topic', '').strip() question = item.get('question', '').strip() user_answer = item.get('user_answer', '').strip() if not all([topic, question, user_answer]): validation_results.append({ 'question': question, 'error': 'Missing required fields (topic, question, or user_answer)' }) continue # Validate input lengths if not _validate_input_length(topic, 200) or not _validate_input_length(question, 500) or not _validate_input_length(user_answer, 500): validation_results.append({ 'question': question, 'error': 'Input exceeds maximum length' }) continue # Validate the answer validation_response = validate_single_answer(topic, question, user_answer) # Generate hint if answer is incorrect hint = None if isinstance(validation_response, str) and ( 'incorrect' in validation_response.lower() or 'not correct' in validation_response.lower() ): hint = generate_hint(topic, question, user_answer) validation_results.append({ 'question': question, 'user_answer': user_answer, 'validation_response': validation_response, 'hint': hint }) return jsonify({'results': validation_results}), 200 except Exception as e: logger.exception(f'Unexpected error in validate_all_answers: {str(e)}') return jsonify({'error': 'An unexpected error occurred'}), 500 # ------------------------------------------------------------------------------ # Health Check # ------------------------------------------------------------------------------ @questions_bp.get('/health') def health(): """ Health check endpoint for the questions service. Returns: JSON response indicating service status """ return jsonify({ 'status': 'healthy', 'service': 'grammar-questions', 'cohere_api_configured': bool(_get_cohere_headers()) }), 200