feat: Generation Page AI workflows + AI/Vector modules + exam session fixes

Generation Page (complete rebuild):
- Full production-parity exam generation wizard with 4 IELTS modules
- Reading: AI passage gen, 5 exercise types (MCQ, Fill, Write, T/F, Match)
- Listening: 4 section types, AI context gen, TTS audio gen (ElevenLabs)
- Writing: Task 1/2, AI instruction gen, word limits, marks
- Speaking: 3 parts, AI script gen, avatar video gen (7 avatars)
- Per-module config: timer, CEFR difficulty, access, approval, rubrics
- Exam submission workflow (draft/published)

Exam Structures:
- New encoach.exam.structure model + CRUD controller
- ExamStructuresPage wired to real API

AI Module (encoach_ai):
- OpenAI service, ElevenLabs TTS, AWS Polly, ELAI avatars
- AI settings model with Odoo config parameters
- 7 generation endpoints (passage, exercises, instructions, scripts, context)

Vector Module (encoach_vector):
- pgvector integration for RAG-based content search
- Embedding service with sentence-transformers

Exam Session Fixes:
- Fixed ExamSession.tsx field mapping (question_type→type, exam_title→title)
- Fixed submit payload to include attempt_id and answers
- Fixed normalizeType to handle null/undefined

Tested: 12/12 API tests passed, browser-verified with real OpenAI calls
Made-with: Cursor
This commit is contained in:
Yamen Ahmad
2026-04-11 14:27:03 +04:00
parent 0c8443256d
commit 6a62a43d61
34 changed files with 2261 additions and 77 deletions

View File

@@ -5,7 +5,7 @@
'summary': 'AI content generation pipelines for General English and IELTS courses',
'author': 'EnCoach',
'license': 'LGPL-3',
'depends': ['encoach_core', 'encoach_exam_template', 'encoach_course_gen'],
'depends': ['encoach_core', 'encoach_exam_template', 'encoach_course_gen', 'encoach_ai'],
'data': [
'security/ir.model.access.csv',
'views/ai_generation_log_views.xml',

View File

@@ -263,6 +263,171 @@ class EncoachAiCourseController(http.Controller):
_logger.exception('validation check failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/<int:course_id>
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def get_course(self, course_id, **kw):
try:
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
IeltsLog = request.env['encoach.ai.ielts.generation.log'].sudo()
ielts = IeltsLog.browse(course_id)
if not ielts.exists():
return _error_response('Course/log not found', 404)
return _json_response({
'id': ielts.id,
'type': 'ielts',
'skill': ielts.skill or '',
'status': ielts.status or '',
'review_status': getattr(ielts, 'review_status', ''),
'created_at': ielts.create_date.isoformat() if ielts.create_date else '',
})
brief = {}
try:
brief = json.loads(log.brief or '{}')
except (json.JSONDecodeError, TypeError):
pass
return _json_response({
'id': log.id,
'type': 'general_english',
'status': log.status or '',
'course_type': log.course_type or '',
'brief': brief,
'attempts': log.attempts,
'student_id': log.student_id.id if log.student_id else None,
'created_at': log.create_date.isoformat() if log.create_date else '',
})
except Exception as e:
_logger.exception('get_course failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/<int:course_id>/tracks
# ------------------------------------------------------------------
@http.route('/api/ai-course/<int:course_id>/tracks', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def get_tracks(self, course_id, **kw):
try:
Log = request.env['encoach.ai.generation.log'].sudo()
log = Log.browse(course_id)
if not log.exists():
return _error_response('Course not found', 404)
generated = {}
try:
generated = json.loads(log.generated_content or '{}')
except (json.JSONDecodeError, TypeError):
pass
tracks = []
modules = generated.get('modules', [])
for i, mod in enumerate(modules):
tracks.append({
'index': i,
'title': mod.get('title', f'Module {i+1}'),
'skill': mod.get('skill', ''),
'status': 'completed' if i == 0 else 'locked',
'progress': 100 if i == 0 else 0,
})
if not tracks:
tracks = [{
'index': 0,
'title': 'Course content pending generation',
'skill': '',
'status': 'pending',
'progress': 0,
}]
return _json_response({'tracks': tracks})
except Exception as e:
_logger.exception('get_tracks failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/english/taxonomy
# ------------------------------------------------------------------
@http.route('/api/ai-course/english/taxonomy', type='http', auth='none',
methods=['GET'], csrf=False)
@jwt_required
def english_taxonomy(self, **kw):
try:
taxonomy = {
'skills': ['reading', 'listening', 'writing', 'speaking', 'grammar', 'vocabulary'],
'cefr_levels': ['A1', 'A2', 'B1', 'B2', 'C1', 'C2'],
'content_types': ['lesson', 'exercise', 'assessment', 'review'],
'topic_domains': [
'daily_life', 'work', 'education', 'travel',
'technology', 'environment', 'health', 'culture',
],
}
Taxonomy = request.env.get('encoach.taxonomy.domain')
if Taxonomy:
domains = Taxonomy.sudo().search([])
if domains:
taxonomy['topic_domains'] = [
{'id': d.id, 'name': d.name, 'description': getattr(d, 'description', '')}
for d in domains
]
return _json_response(taxonomy)
except Exception as e:
_logger.exception('english_taxonomy failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# POST /api/ai-course/examiner-review
# ------------------------------------------------------------------
@http.route('/api/ai-course/examiner-review', type='http', auth='none',
methods=['POST'], csrf=False)
@jwt_required
def examiner_review(self, **kw):
try:
body = _get_json_body()
log_id = body.get('log_id')
action = body.get('action')
examiner_notes = body.get('examiner_notes', '')
if not log_id:
return _error_response('log_id is required', 400)
if action not in ('approve', 'reject', 'revise'):
return _error_response('action must be approve, reject, or revise', 400)
IeltsLog = request.env['encoach.ai.ielts.generation.log'].sudo()
log = IeltsLog.browse(int(log_id))
if not log.exists():
return _error_response('Log not found', 404)
status_map = {
'approve': 'approved',
'reject': 'rejected',
'revise': 'revision_needed',
}
log.write({
'review_status': status_map[action],
'examiner_id': request.env.user.id,
'examiner_notes': examiner_notes,
'reviewed_at': fields.Datetime.now(),
})
return _json_response({'status': status_map[action], 'log_id': log_id})
except Exception as e:
_logger.exception('examiner_review failed')
return _error_response(str(e), 500)
# ------------------------------------------------------------------
# GET /api/ai-course/review-queue
# ------------------------------------------------------------------