Files
encoach_be_odoo19/encoach_api/controllers/evaluations.py
Talal Sharabi f5b627256f EnCoach Odoo 19 custom modules
Full backend implementation with custom Odoo modules:
- encoach_api: Core API, user management, JWT auth
- encoach_exam: Exam generation (reading, writing, listening, speaking)
- encoach_evaluate: AI-powered evaluation (writing, speaking)
- encoach_training: Training tips and walkthrough
- encoach_storage: File storage management
- encoach_payment: Stripe, PayPal, Paymob integration
- encoach_mail: Email notifications

Made-with: Cursor
2026-03-14 16:46:46 +04:00

294 lines
11 KiB
Python

import json
import logging
import threading
import odoo
from odoo import api, SUPERUSER_ID, http
from odoo.http import request
from .base import EncoachMixin
_logger = logging.getLogger(__name__)
def _run_grading_background(dbname, evaluation_id, input_data, grading_type):
"""Execute AI grading in a background thread with its own cursor."""
try:
registry = odoo.registry(dbname)
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
service = env['encoach.ai.grading']
if grading_type == 'writing':
service.grade_writing(evaluation_id, input_data)
elif grading_type == 'speaking':
service.grade_speaking(evaluation_id, input_data)
elif grading_type == 'interactive_speaking':
service.grade_interactive_speaking(evaluation_id, input_data)
except Exception:
_logger.exception("Background grading failed for evaluation %s", evaluation_id)
try:
registry = odoo.registry(dbname)
with registry.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env['encoach.evaluation'].browse(evaluation_id).write({
'status': 'error',
'error_message': 'Grading failed unexpectedly',
})
except Exception:
_logger.exception("Failed to update evaluation status after error")
class EvaluationsController(EncoachMixin, http.Controller):
# ------------------------------------------------- evaluate writing
@http.route('/api/evaluate/writing', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def evaluate_writing(self, **kwargs):
try:
user = self._authenticate()
if not user:
return self._error_response('Unauthorized', 401)
body = self._get_json_body()
required = ('sessionId', 'exerciseId', 'question', 'answer', 'task')
for field in required:
if not body.get(field):
return self._error_response(f'{field} is required', 400)
Evaluation = request.env['encoach.evaluation'].sudo()
evaluation = Evaluation.create({
'user_id': int(body.get('userId', user.id)),
'session_id': int(body['sessionId']),
'exercise_id': body['exerciseId'],
'eval_type': 'writing',
'status': 'pending',
'input_data': json.dumps({
'question': body['question'],
'answer': body['answer'],
'task': body['task'],
'attachment': body.get('attachment'),
}),
})
dbname = request.env.cr.dbname
input_data = {
'question': body['question'],
'answer': body['answer'],
'task': body['task'],
'attachment': body.get('attachment'),
}
thread = threading.Thread(
target=_run_grading_background,
args=(dbname, evaluation.id, input_data, 'writing'),
daemon=True,
)
thread.start()
return self._json_response({'ok': True})
except Exception:
_logger.exception("Evaluate writing error")
return self._error_response('Internal server error', 500)
# ------------------------------------------------- evaluate speaking
@http.route('/api/evaluate/speaking', type='http', auth='public', methods=['POST', 'OPTIONS'], csrf=False, cors='*')
def evaluate_speaking(self, **kwargs):
"""Accept multipart form with audio files and question fields."""
try:
user = self._authenticate()
if not user:
return self._error_response('Unauthorized', 401)
form = request.httprequest.form
files = request.httprequest.files
user_id = form.get('userId', str(user.id))
session_id = form.get('sessionId')
exercise_id = form.get('exerciseId')
task = form.get('task')
if not session_id or not exercise_id or not task:
return self._error_response('sessionId, exerciseId, and task are required', 400)
audio_entries = []
idx = 1
while True:
audio_key = f'audio_{idx}'
question_key = f'question_{idx}'
audio_file = files.get(audio_key)
if not audio_file:
break
audio_entries.append({
'audio_data': audio_file.read(),
'audio_filename': audio_file.filename,
'question': form.get(question_key, ''),
})
idx += 1
if not audio_entries:
return self._error_response('At least one audio file is required', 400)
Attachment = request.env['ir.attachment'].sudo()
saved_audios = []
for entry in audio_entries:
import base64
att = Attachment.create({
'name': entry['audio_filename'],
'datas': base64.b64encode(entry['audio_data']),
'res_model': 'encoach.evaluation',
'type': 'binary',
})
saved_audios.append({
'attachment_id': att.id,
'question': entry['question'],
})
Evaluation = request.env['encoach.evaluation'].sudo()
evaluation = Evaluation.create({
'user_id': int(user_id),
'session_id': int(session_id),
'exercise_id': exercise_id,
'eval_type': 'speaking',
'status': 'pending',
'input_data': json.dumps({
'task': task,
'audios': [{'attachment_id': a['attachment_id'], 'question': a['question']} for a in saved_audios],
}),
})
dbname = request.env.cr.dbname
input_data = {
'task': task,
'audios': saved_audios,
}
thread = threading.Thread(
target=_run_grading_background,
args=(dbname, evaluation.id, input_data, 'speaking'),
daemon=True,
)
thread.start()
return self._json_response({'ok': True})
except Exception:
_logger.exception("Evaluate speaking error")
return self._error_response('Internal server error', 500)
# ---------------------------------------- evaluate interactive speaking
@http.route(
'/api/evaluate/interactiveSpeaking', type='http', auth='public',
methods=['POST', 'OPTIONS'], csrf=False, cors='*',
)
def evaluate_interactive_speaking(self, **kwargs):
"""Same as speaking but for interactive Q&A pairs."""
try:
user = self._authenticate()
if not user:
return self._error_response('Unauthorized', 401)
form = request.httprequest.form
files = request.httprequest.files
user_id = form.get('userId', str(user.id))
session_id = form.get('sessionId')
exercise_id = form.get('exerciseId')
task = form.get('task')
if not session_id or not exercise_id or not task:
return self._error_response('sessionId, exerciseId, and task are required', 400)
audio_entries = []
idx = 1
while True:
audio_key = f'audio_{idx}'
question_key = f'question_{idx}'
audio_file = files.get(audio_key)
if not audio_file:
break
audio_entries.append({
'audio_data': audio_file.read(),
'audio_filename': audio_file.filename,
'question': form.get(question_key, ''),
})
idx += 1
if not audio_entries:
return self._error_response('At least one audio file is required', 400)
import base64
Attachment = request.env['ir.attachment'].sudo()
saved_audios = []
for entry in audio_entries:
att = Attachment.create({
'name': entry['audio_filename'],
'datas': base64.b64encode(entry['audio_data']),
'res_model': 'encoach.evaluation',
'type': 'binary',
})
saved_audios.append({
'attachment_id': att.id,
'question': entry['question'],
})
Evaluation = request.env['encoach.evaluation'].sudo()
evaluation = Evaluation.create({
'user_id': int(user_id),
'session_id': int(session_id),
'exercise_id': exercise_id,
'eval_type': 'interactive_speaking',
'status': 'pending',
'input_data': json.dumps({
'task': task,
'audios': [{'attachment_id': a['attachment_id'], 'question': a['question']} for a in saved_audios],
}),
})
dbname = request.env.cr.dbname
input_data = {
'task': task,
'audios': saved_audios,
}
thread = threading.Thread(
target=_run_grading_background,
args=(dbname, evaluation.id, input_data, 'interactive_speaking'),
daemon=True,
)
thread.start()
return self._json_response({'ok': True})
except Exception:
_logger.exception("Evaluate interactive speaking error")
return self._error_response('Internal server error', 500)
# ----------------------------------------------------- poll status
@http.route(['/api/evaluate/<int:session_id>/<string:exercise_id>', '/api/evaluate/status'], type='http', auth='public', methods=['GET', 'OPTIONS'], csrf=False, cors='*')
def evaluation_status(self, session_id=None, exercise_id=None, **kwargs):
try:
user = self._authenticate()
if not user:
return self._error_response('Unauthorized', 401)
if not session_id:
session_id = kwargs.get('sessionId')
if not exercise_id:
exercise_id = kwargs.get('exerciseId')
if not session_id or not exercise_id:
return self._error_response('sessionId and exerciseId are required', 400)
evaluation = request.env['encoach.evaluation'].sudo().search([
('session_id', '=', int(session_id)),
('exercise_id', '=', str(exercise_id)),
], limit=1, order='create_date desc')
if not evaluation:
return self._error_response('Evaluation not found', 404)
result = None
if evaluation.status == 'completed' and evaluation.result:
result = json.loads(evaluation.result)
return self._json_response({
'status': evaluation.status,
'result': result,
})
except Exception:
_logger.exception("Evaluation status error")
return self._error_response('Internal server error', 500)