import json import logging from odoo import http from odoo.http import request from .base import EncoachMixin _logger = logging.getLogger(__name__) class GenerationController(EncoachMixin, http.Controller): # ------------------------------------------------- reading passage @http.route( '/api/exam/reading/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def generate_reading_passage(self, passage, **kwargs): """Generate a reading passage (1, 2, or 3) using GPT-4o.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) if passage not in (1, 2, 3): return self._error_response('passage must be 1, 2, or 3', 400) topic = kwargs.get('topic') word_count = int(kwargs.get('word_count', 500)) GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_reading_passage( passage=passage, topic=topic, word_count=word_count, ) return self._json_response(result) except Exception: _logger.exception("Generate reading passage error") return self._error_response('Internal server error', 500) # ------------------------------------------ reading exercises @http.route('/api/exam/reading/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def generate_reading_exercises(self, **kwargs): """Generate reading exercises from a passage.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() text = body.get('text', '') exercises = body.get('exercises', []) difficulty = body.get('difficulty', ['B1', 'B2']) if not text or not exercises: return self._error_response('text and exercises are required', 400) GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_reading_exercises( text=text, exercises=exercises, difficulty=difficulty, ) return self._json_response({'exercises': result}) except Exception: _logger.exception("Generate reading exercises error") return self._error_response('Internal server error', 500) # ----------------------------------------------- listening dialog @http.route( '/api/exam/listening/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def generate_listening_dialog(self, section, **kwargs): """Generate a listening dialog/monologue (section 1-4) using GPT-4o.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) if section not in (1, 2, 3, 4): return self._error_response('section must be 1, 2, 3, or 4', 400) topic = kwargs.get('topic') difficulty = kwargs.get('difficulty') GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_listening_dialog( section=section, topic=topic, difficulty=difficulty, ) return self._json_response(result) except Exception: _logger.exception("Generate listening dialog error") return self._error_response('Internal server error', 500) # ---------------------------------------- listening exercises @http.route('/api/exam/listening/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def generate_listening_exercises(self, **kwargs): """Generate listening exercises from a dialog transcript.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() text = body.get('text', '') exercises = body.get('exercises', []) difficulty = body.get('difficulty', ['B1']) if not text or not exercises: return self._error_response('text and exercises are required', 400) GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_listening_exercises( text=text, exercises=exercises, difficulty=difficulty, ) return self._json_response({'exercises': result}) except Exception: _logger.exception("Generate listening exercises error") return self._error_response('Internal server error', 500) # ------------------------------------------------- writing task @http.route( '/api/exam/writing/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def generate_writing_task(self, task, **kwargs): """Generate a writing task prompt (task 1 or 2) using GPT-4o.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) if task not in (1, 2): return self._error_response('task must be 1 or 2', 400) topic = kwargs.get('topic') difficulty = kwargs.get('difficulty') GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_writing_task( task=task, topic=topic, difficulty=difficulty, ) return self._json_response(result) except Exception: _logger.exception("Generate writing task error") return self._error_response('Internal server error', 500) # ------------------------------------------------ speaking task @http.route( '/api/exam/speaking/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*', ) def generate_speaking_task(self, task, **kwargs): """Generate a speaking task prompt (part 1, 2, or 3) using GPT-4o.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) if task not in (1, 2, 3): return self._error_response('task must be 1, 2, or 3', 400) topic = kwargs.get('topic') first_topic = kwargs.get('first_topic') second_topic = kwargs.get('second_topic') difficulty = kwargs.get('difficulty') GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_speaking_task( task=task, topic=topic, first_topic=first_topic, second_topic=second_topic, difficulty=difficulty, ) return self._json_response(result) except Exception: _logger.exception("Generate speaking task error") return self._error_response('Internal server error', 500) # --------------------------------------------- level exercises @http.route('/api/exam/level/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def generate_level_exercises(self, **kwargs): """Generate level-test exercises using GPT-4o.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() exercises = body.get('exercises', []) difficulty = body.get('difficulty', ['B1']) if not exercises: return self._error_response('exercises specification is required', 400) GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_level_exercises( exercises=exercises, difficulty=difficulty, ) return self._json_response({'exercises': result}) except Exception: _logger.exception("Generate level exercises error") return self._error_response('Internal server error', 500) # --------------------------------------------- writing attachment (vision) @http.route( '/api/exam/writing//attachment', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*', ) def generate_writing_attachment(self, task, **kwargs): """Academic writing Task 1 with image upload for GPT-4o vision.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) if task not in (1, 2): return self._error_response('task must be 1 or 2', 400) files = request.httprequest.files form = request.httprequest.form image_file = files.get('file') difficulty = form.get('difficulty', 'B2') if not image_file: return self._error_response('file (image) is required', 400) import base64 image_data = base64.b64encode(image_file.read()).decode('utf-8') mime_type = image_file.content_type or 'image/png' GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_writing_with_image( task=task, image_b64=image_data, mime_type=mime_type, difficulty=difficulty, ) return self._json_response(result) except Exception: _logger.exception("Generate writing attachment error") return self._error_response('Internal server error', 500) # -------------------------------------------- pre-built level exam @http.route('/api/exam/level/', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*') def get_level_exam(self, **kwargs): """Return a pre-built level exam (no AI generation).""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) exams = request.env['encoach.exam'].sudo().search([ ('module', '=', 'level'), ('access', '=', 'public'), ], limit=1, order='create_date desc') if not exams: return self._error_response('No level exam found', 404) return self._json_response(exams[0].to_encoach_dict()) except Exception: _logger.exception("Get level exam error") return self._error_response('Internal server error', 500) # -------------------------------------------- UTAS level exam @http.route('/api/exam/level/utas', type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*') def get_utas_level_exam(self, **kwargs): """Return a UTAS-format level exam.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) exams = request.env['encoach.exam'].sudo().search([ ('module', '=', 'level'), ('label', 'ilike', 'utas'), ], limit=1, order='create_date desc') if not exams: return self._error_response('No UTAS level exam found', 404) return self._json_response(exams[0].to_encoach_dict()) except Exception: _logger.exception("Get UTAS level exam error") return self._error_response('Internal server error', 500) # -------------------------------------------- level exam import @http.route('/api/exam/level/import/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def import_level_exam(self, **kwargs): """Import level exam from uploaded file.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) files = request.httprequest.files exercises_file = files.get('exercises') if not exercises_file: return self._error_response('exercises file is required', 400) import json as _json content = exercises_file.read().decode('utf-8') data = _json.loads(content) exam = request.env['encoach.exam'].sudo().create({ 'module': 'level', 'label': data.get('label', 'Imported Level Exam'), 'parts': data.get('parts', []), 'access': 'private', }) return self._json_response(exam.to_encoach_dict()) except Exception: _logger.exception("Import level exam error") return self._error_response('Internal server error', 500) # -------------------------------------------- custom level exam @http.route('/api/exam/level/custom/', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*') def generate_custom_level(self, **kwargs): """Generate custom level exam from JSON specification.""" try: user = self._authenticate() if not user: return self._error_response('Unauthorized', 401) body = self._get_json_body() spec = body.get('spec', body) GenService = request.env['encoach.ai.generation'].sudo() result = GenService.generate_level_exercises( exercises=spec.get('exercises', []), difficulty=spec.get('difficulty', ['B1', 'B2']), ) return self._json_response({'exercises': result}) except Exception: _logger.exception("Generate custom level error") return self._error_response('Internal server error', 500)