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
This commit is contained in:
Talal Sharabi
2026-03-14 16:46:46 +04:00
commit f5b627256f
168 changed files with 13428 additions and 0 deletions

View File

@@ -0,0 +1 @@
from . import grading_service

View File

@@ -0,0 +1,340 @@
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