import json import logging import httpx from odoo.addons.encoach_ai.models.constants import GPT_MODELS, TEMPERATURE from odoo.addons.encoach_ai.services.openai_service import EncoachOpenAIService _logger = logging.getLogger(__name__) WRITING_RESPONSE_TEMPLATE = json.dumps({ "comment": "comment about student's response quality", "overall": 0.0, "task_response": { "Task Achievement": {"grade": 0.0, "comment": "..."}, "Coherence and Cohesion": {"grade": 0.0, "comment": "..."}, "Lexical Resource": {"grade": 0.0, "comment": "..."}, "Grammatical Range and Accuracy": {"grade": 0.0, "comment": "..."}, }, }) SPEAKING_RESPONSE_TEMPLATE = json.dumps({ "comment": "extensive comment about answer quality", "overall": 0.0, "task_response": { "Fluency and Coherence": {"grade": 0.0, "comment": "extensive comment..."}, "Lexical Resource": {"grade": 0.0, "comment": "..."}, "Grammatical Range and Accuracy": {"grade": 0.0, "comment": "..."}, "Pronunciation": {"grade": 0.0, "comment": "..."}, }, }) SHORT_ANSWER_TEMPLATE = json.dumps({ "exercises": [ {"id": "exercise-id", "correct": True, "correct_answer": "the correct answer"}, ], }) SUMMARY_TEMPLATE = json.dumps({ "sections": [ { "code": "section-code", "name": "Section Name", "grade": 0.0, "evaluation": "Detailed evaluation text...", "suggestions": "Improvement suggestions...", "bullet_points": ["point 1", "point 2"], }, ], }) SPEAKING_TASK_INSTRUCTIONS = { 1: ( 'Address the student as "you". ' "If the answers are not 2 or 3 sentences long, " "warn the student that they should be." ), 2: 'Address the student as "you".', 3: ( 'Address the student as "you" and pay special attention ' "to coherence between the answers." ), } class EncoachGradingService: def __init__(self, env): self.env = env self.ai = EncoachOpenAIService(env) # ------------------------------------------------------------------ # Writing grading # ------------------------------------------------------------------ def grade_writing(self, question, answer, task, attachment=None): """Grade a writing answer using GPT-4o with IELTS writing rubric. Returns dict with comment, overall, task_response, fixed_text, perfect_answer, and ai_detection results. """ system_msg = ( "You are a helpful assistant designed to output JSON " f"on this format: {WRITING_RESPONSE_TEMPLATE}" ) if task == 1: user_prompt = ( f"Evaluate the given Writing Task {task} response based on the " "IELTS grading system, ensuring a strict assessment that penalizes " "errors. Deduct points for deviations from the task, and assign a " "score of 0 if the response fails to address the question. " "Additionally, provide a detailed commentary highlighting both " "strengths and weaknesses in the response.\n" 'Refer to the parts of the letter as: "Greeting Opener", ' '"bullet 1", "bullet 2", "bullet 3", ' '"closer (restate the purpose of the letter)", "closing greeting".\n' f'Question: "{question}"\nAnswer: "{answer}"' ) else: user_prompt = ( f"Evaluate the given Writing Task {task} response based on the " "IELTS grading system, ensuring a strict assessment that penalizes " "errors. Deduct points for deviations from the task, and assign a " "score of 0 if the response fails to address the question. " "Additionally, provide a detailed commentary highlighting both " "strengths and weaknesses in the response.\n" f'Question: "{question}"\nAnswer: "{answer}"' ) messages = [ {"role": "system", "content": system_msg}, ] if attachment and task == 1: messages.append({ "role": "user", "content": [ {"type": "text", "text": user_prompt}, { "type": "image_url", "image_url": {"url": attachment}, }, ], }) else: messages.append({"role": "user", "content": user_prompt}) result = self.ai.prediction( model=GPT_MODELS["grading"], messages=messages, temperature=TEMPERATURE["grading"], fields_to_check=["comment"], ) if not result: return None result["fixed_text"] = self._get_fixed_text(answer) result["perfect_answer"] = self._get_perfect_answer(question) result["ai_detection"] = self._detect_ai(answer) return result # ------------------------------------------------------------------ # Speaking grading # ------------------------------------------------------------------ def grade_speaking(self, task, qa_pairs): """Grade speaking using GPT-4o with IELTS speaking rubric. qa_pairs: list of dicts with 'question' and 'answer' keys. """ system_msg = ( "You are a helpful assistant designed to output JSON " f"on this format: {SPEAKING_RESPONSE_TEMPLATE}" ) qa_text = "\n".join( f'Question: "{p["question"]}"\nAnswer: "{p["answer"]}"' for p in qa_pairs ) task_instruction = SPEAKING_TASK_INSTRUCTIONS.get(task, "") user_prompt = ( f"Evaluate the given Speaking Part {task} response based on the " "IELTS grading system, ensuring a strict assessment that penalizes " "errors. Deduct points for deviations from the task, and assign a " "score of 0 if the response fails to address the question. " "Additionally, provide detailed commentary highlighting both " f"strengths and weaknesses in the response. {task_instruction}\n" f"{qa_text}" ) messages = [ {"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}, ] result = self.ai.prediction( model=GPT_MODELS["grading"], messages=messages, temperature=TEMPERATURE["grading"], fields_to_check=["comment"], ) if not result: return None if task == 2 and qa_pairs: combined_answer = " ".join(p["answer"] for p in qa_pairs) result["fixed_text"] = self._get_fixed_text(combined_answer) result["perfect_answer"] = self._get_perfect_answer( qa_pairs[0]["question"] ) return result # ------------------------------------------------------------------ # Short-answer grading # ------------------------------------------------------------------ def grade_short_answers(self, text, questions, answers): """Evaluate short answers against a reading/listening passage.""" system_msg = ( "You are a helpful assistant designed to output JSON " f"on this format: {SHORT_ANSWER_TEMPLATE}" ) qa_text = "\n".join( f'Q{i + 1}: "{q}" — Student answer: "{a}"' for i, (q, a) in enumerate(zip(questions, answers)) ) user_prompt = ( "Evaluate each student answer against the passage. For each answer, " "determine if it is correct and provide the correct answer.\n\n" f'Passage: "{text}"\n\n{qa_text}' ) messages = [ {"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}, ] return self.ai.prediction( model=GPT_MODELS["grading"], messages=messages, temperature=TEMPERATURE["grading"], check_blacklisted=False, ) # ------------------------------------------------------------------ # Grading summary # ------------------------------------------------------------------ def generate_grading_summary(self, sections): """Generate a summary for an entire exam session using GPT-3.5-turbo.""" system_msg = ( "You are a helpful assistant designed to output JSON " f"on this format: {SUMMARY_TEMPLATE}" ) user_prompt = ( "Generate a detailed evaluation summary for the following IELTS exam " "sections. For each section, provide an evaluation, suggestions for " "improvement, and key bullet points.\n\n" + json.dumps(sections) ) messages = [ {"role": "system", "content": system_msg}, {"role": "user", "content": user_prompt}, ] return self.ai.prediction( model=GPT_MODELS["secondary"], messages=messages, temperature=TEMPERATURE["grading"], check_blacklisted=False, ) # ------------------------------------------------------------------ # AI detection (GPTZero) # ------------------------------------------------------------------ def _detect_ai(self, text): """Call GPTZero API to detect AI-generated text.""" api_key = ( self.env["ir.config_parameter"] .sudo() .get_param("encoach.gptzero_api_key", "") ) if not api_key: _logger.warning("GPTZero API key not configured") return None try: resp = httpx.post( "https://api.gptzero.me/v2/predict/text", headers={ "x-api-key": api_key, "Content-Type": "application/json", }, json={ "document": text, "version": "", "multilingual": False, }, timeout=30, ) resp.raise_for_status() return resp.json() except Exception: _logger.exception("GPTZero API call failed") return None # ------------------------------------------------------------------ # Helper prompts # ------------------------------------------------------------------ def _get_perfect_answer(self, question): messages = [ {"role": "system", "content": "You are an IELTS writing expert."}, { "role": "user", "content": ( "Write a perfect answer for this IELTS writing task: " f'"{question}"' ), }, ] result = self.ai.prediction( model=GPT_MODELS["secondary"], messages=messages, temperature=TEMPERATURE["grading"], response_format={"type": "json_object"}, check_blacklisted=False, ) if result and "answer" in result: return result["answer"] return result def _get_fixed_text(self, text): messages = [ {"role": "system", "content": "You are a grammar correction assistant. Output JSON."}, { "role": "user", "content": ( "Fix the grammatical and spelling errors in this text, " f'keeping the original meaning: "{text}"' ), }, ] result = self.ai.prediction( model=GPT_MODELS["secondary"], messages=messages, temperature=TEMPERATURE["grading"], response_format={"type": "json_object"}, check_blacklisted=False, ) if result and "text" in result: return result["text"] return result