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
108 lines
4.2 KiB
Python
108 lines
4.2 KiB
Python
"""REST endpoints for AI coaching — matches frontend coaching.service.ts."""
|
|
|
|
import json
|
|
import logging
|
|
from odoo import http
|
|
from odoo.http import request, Response
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _json_response(data, status=200):
|
|
return Response(json.dumps(data, default=str), status=status, content_type="application/json")
|
|
|
|
|
|
def _get_json():
|
|
try:
|
|
return json.loads(request.httprequest.data or "{}")
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
class CoachController(http.Controller):
|
|
"""Handles /api/coach/* endpoints consumed by frontend AI coaching components."""
|
|
|
|
def _get_coach(self):
|
|
from odoo.addons.encoach_ai.services.coach_service import CoachService
|
|
return CoachService(request.env)
|
|
|
|
# ── POST /api/coach/chat — AiAssistantDrawer.tsx ──
|
|
@http.route("/api/coach/chat", type="http", auth="user", methods=["POST"], csrf=False)
|
|
def coach_chat(self, **kw):
|
|
body = _get_json()
|
|
try:
|
|
coach = self._get_coach()
|
|
result = coach.chat(
|
|
body.get("message", ""),
|
|
history=body.get("history", []),
|
|
student_context=body.get("context"),
|
|
)
|
|
return _json_response(result)
|
|
except Exception as e:
|
|
_logger.exception("Coach chat failed")
|
|
return _json_response({"reply": f"I'm having trouble right now. Error: {e}"})
|
|
|
|
# ── GET /api/coach/tip — AiTipBanner.tsx ──
|
|
@http.route("/api/coach/tip", type="http", auth="user", methods=["GET"], csrf=False)
|
|
def coach_tip(self, **kw):
|
|
context = request.params.get("context", "general")
|
|
try:
|
|
coach = self._get_coach()
|
|
return _json_response(coach.get_tip(context))
|
|
except Exception as e:
|
|
return _json_response({"tip": "Keep practising every day — consistency beats intensity!", "category": "general"})
|
|
|
|
# ── POST /api/coach/explain — AiGradeExplainer.tsx ──
|
|
@http.route("/api/coach/explain", type="http", auth="user", methods=["POST"], csrf=False)
|
|
def coach_explain(self, **kw):
|
|
body = _get_json()
|
|
try:
|
|
coach = self._get_coach()
|
|
result = coach.explain(
|
|
body.get("score_data", {}),
|
|
body.get("student_context", ""),
|
|
)
|
|
return _json_response(result)
|
|
except Exception as e:
|
|
return _json_response({"explanation": f"Could not generate explanation: {e}"})
|
|
|
|
# ── POST /api/coach/suggest — AiStudyCoach.tsx ──
|
|
@http.route("/api/coach/suggest", type="http", auth="user", methods=["POST"], csrf=False)
|
|
def coach_suggest(self, **kw):
|
|
body = _get_json()
|
|
try:
|
|
coach = self._get_coach()
|
|
return _json_response(coach.suggest(body))
|
|
except Exception as e:
|
|
return _json_response({
|
|
"suggestion": "Focus on your weakest skill for 30 minutes daily.",
|
|
"focus_areas": ["writing", "speaking"],
|
|
"daily_plan": [],
|
|
"motivation": "Every expert was once a beginner!",
|
|
})
|
|
|
|
# ── POST /api/coach/writing-help — AiWritingHelper.tsx ──
|
|
@http.route("/api/coach/writing-help", type="http", auth="user", methods=["POST"], csrf=False)
|
|
def coach_writing_help(self, **kw):
|
|
body = _get_json()
|
|
try:
|
|
coach = self._get_coach()
|
|
result = coach.writing_help(
|
|
body.get("task", ""),
|
|
body.get("draft", ""),
|
|
body.get("help_type", "improve"),
|
|
)
|
|
return _json_response(result)
|
|
except Exception as e:
|
|
return _json_response({"improved_text": "", "changes": [], "tips": [str(e)]})
|
|
|
|
# ── POST /api/coach/hint — (unused component, wired for completeness) ──
|
|
@http.route("/api/coach/hint", type="http", auth="user", methods=["POST"], csrf=False)
|
|
def coach_hint(self, **kw):
|
|
body = _get_json()
|
|
try:
|
|
coach = self._get_coach()
|
|
return _json_response(coach.get_hint(body))
|
|
except Exception as e:
|
|
return _json_response({"hint": "Think about the key words in the question.", "strategy": "keyword_focus"})
|