Spaces:
Running
Running
| """ | |
| 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 | |
| # ------------------------------------------------------------------------------ | |
| 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 | |
| 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 | |
| # ------------------------------------------------------------------------------ | |
| 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 |