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:
1
encoach_ai_generation/__init__.py
Normal file
1
encoach_ai_generation/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import services
|
||||
13
encoach_ai_generation/__manifest__.py
Normal file
13
encoach_ai_generation/__manifest__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "EnCoach AI Generation",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Education",
|
||||
"summary": "AI-powered IELTS exam content generation",
|
||||
"description": "Reading passages, listening dialogs, writing/speaking tasks, level exercises.",
|
||||
"depends": ["encoach_ai", "encoach_exam"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
],
|
||||
"installable": True,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
1
encoach_ai_generation/security/ir.model.access.csv
Normal file
1
encoach_ai_generation/security/ir.model.access.csv
Normal file
@@ -0,0 +1 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
1
encoach_ai_generation/services/__init__.py
Normal file
1
encoach_ai_generation/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import generation_service
|
||||
352
encoach_ai_generation/services/generation_service.py
Normal file
352
encoach_ai_generation/services/generation_service.py
Normal file
@@ -0,0 +1,352 @@
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
|
||||
from odoo.addons.encoach_ai.models.constants import (
|
||||
CEFR_LEVELS,
|
||||
GPT_MODELS,
|
||||
TEMPERATURE,
|
||||
TOPICS,
|
||||
)
|
||||
from odoo.addons.encoach_ai.services.openai_service import EncoachOpenAIService
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
PASSAGE_DIFFICULTY = {
|
||||
1: "fairly easy and consist of multiple paragraphs",
|
||||
2: "fairly hard and consist of multiple paragraphs",
|
||||
3: (
|
||||
"very hard, present different points or theories, "
|
||||
"cite different sources, and consist of multiple paragraphs"
|
||||
),
|
||||
}
|
||||
|
||||
LISTENING_SECTION_CONFIG = {
|
||||
1: {
|
||||
"type": "conversation",
|
||||
"format": '{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}',
|
||||
"prompt": (
|
||||
"Compose an authentic conversation between two individuals "
|
||||
'on the topic of "{topic}". Please include random names and genders. '
|
||||
"Include misleading discourse (dates, colors, etc.) and spelling of names. "
|
||||
"Make sure that the generated conversation does not contain "
|
||||
"forbidden subjects in muslim countries."
|
||||
),
|
||||
},
|
||||
2: {
|
||||
"type": "monologue",
|
||||
"format": '{"monologue": "monologue"}',
|
||||
"prompt": (
|
||||
"Generate a comprehensive monologue set in the social context "
|
||||
'of "{topic}". Make sure that the generated monologue does not '
|
||||
"contain forbidden subjects in muslim countries."
|
||||
),
|
||||
},
|
||||
3: {
|
||||
"type": "conversation",
|
||||
"format": '{"conversation": [{"name": "name", "gender": "gender", "text": "text"}]}',
|
||||
"prompt": (
|
||||
"Compose an authentic and elaborate conversation between "
|
||||
'up to four individuals on the topic of "{topic}". '
|
||||
"Please include random names and genders. Make sure that the "
|
||||
"generated conversation does not contain forbidden subjects "
|
||||
"in muslim countries."
|
||||
),
|
||||
},
|
||||
4: {
|
||||
"type": "monologue",
|
||||
"format": '{"monologue": "monologue"}',
|
||||
"prompt": (
|
||||
"Generate a comprehensive and complex monologue on the academic "
|
||||
'subject of "{topic}". Make sure that the generated monologue '
|
||||
"does not contain forbidden subjects in muslim countries."
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class EncoachGenerationService:
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
self.ai = EncoachOpenAIService(env)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Reading
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_reading_passage(self, passage_num, topic=None, word_count=500):
|
||||
"""Generate an IELTS reading passage (passage 1, 2, or 3)."""
|
||||
topic = topic or random.choice(TOPICS)
|
||||
difficulty = PASSAGE_DIFFICULTY.get(passage_num, PASSAGE_DIFFICULTY[1])
|
||||
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON on this format: "
|
||||
'{"title": "title of the text", "text": "generated text"}'
|
||||
)
|
||||
user_msg = (
|
||||
f"Generate an extensive text for IELTS Reading Passage {passage_num}, "
|
||||
f'of at least {word_count} words, on the topic of "{topic}". '
|
||||
"The passage should offer a substantial amount of information "
|
||||
f"relevant to the chosen subject matter. It should be {difficulty}. "
|
||||
"Make sure that the generated text does not contain forbidden "
|
||||
"subjects in muslim countries."
|
||||
)
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
fields_to_check=["title", "text"],
|
||||
)
|
||||
|
||||
def generate_reading_exercises(self, text, exercises_config, difficulty):
|
||||
"""Generate exercises for a reading passage.
|
||||
|
||||
exercises_config: list of dicts with 'type' and 'quantity' keys.
|
||||
difficulty: CEFR level string.
|
||||
"""
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON. "
|
||||
"Generate reading comprehension exercises based on the given text."
|
||||
)
|
||||
config_str = json.dumps(exercises_config)
|
||||
user_msg = (
|
||||
f"Based on the following text, generate exercises according to this "
|
||||
f"configuration: {config_str}. Target CEFR level: {difficulty}.\n\n"
|
||||
f'Text: "{text}"'
|
||||
)
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Listening
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_listening_dialog(self, section, topic=None, difficulty=None):
|
||||
"""Generate a listening dialog/monologue for the given section (1-4)."""
|
||||
topic = topic or random.choice(TOPICS)
|
||||
config = LISTENING_SECTION_CONFIG.get(section, LISTENING_SECTION_CONFIG[1])
|
||||
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON on this format: "
|
||||
+ config["format"]
|
||||
)
|
||||
user_msg = config["prompt"].format(topic=topic)
|
||||
if difficulty:
|
||||
user_msg += f" Target CEFR level: {difficulty}."
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
)
|
||||
|
||||
def generate_listening_exercises(self, text, exercises_config, difficulty):
|
||||
"""Generate exercises for a listening transcript."""
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON. "
|
||||
"Generate listening comprehension exercises based on the given transcript."
|
||||
)
|
||||
config_str = json.dumps(exercises_config)
|
||||
user_msg = (
|
||||
f"Based on the following transcript, generate exercises according to "
|
||||
f"this configuration: {config_str}. Target CEFR level: {difficulty}.\n\n"
|
||||
f'Transcript: "{text}"'
|
||||
)
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Writing
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_writing_task(
|
||||
self, task, topic=None, difficulty=None, task_type="general"
|
||||
):
|
||||
"""Generate a writing task prompt.
|
||||
|
||||
task: 1 or 2
|
||||
task_type: 'general' or 'academic'
|
||||
"""
|
||||
topic = topic or random.choice(TOPICS)
|
||||
difficulty = difficulty or random.choice(CEFR_LEVELS)
|
||||
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON on this format: "
|
||||
'{"question": "the generated writing task prompt"}'
|
||||
)
|
||||
|
||||
if task == 1 and task_type == "general":
|
||||
user_msg = (
|
||||
"Craft a prompt for an IELTS Writing Task 1 General Training "
|
||||
"exercise that instructs the student to compose a letter based "
|
||||
f'on the topic of "{topic}" of {difficulty} CEFR level difficulty. '
|
||||
'The prompt should end with "In the letter you should" followed '
|
||||
"by 3 bullet points. Make sure it does not contain forbidden "
|
||||
"subjects in muslim countries."
|
||||
)
|
||||
elif task == 2:
|
||||
user_msg = (
|
||||
f"Craft a comprehensive question of {difficulty} CEFR level "
|
||||
"difficulty like the ones for IELTS Writing Task 2 General "
|
||||
"Training that directs the candidate to delve into an in-depth "
|
||||
"analysis of contrasting perspectives on the topic of "
|
||||
f'"{topic}". The question should lead to an answer with either '
|
||||
'"theories", "complicated information" or be "very descriptive" '
|
||||
"on the topic."
|
||||
)
|
||||
else:
|
||||
user_msg = (
|
||||
"Analyze the uploaded image and create a detailed IELTS Writing "
|
||||
"Task 1 Academic prompt. Describe the visual type, context, and "
|
||||
f"create a prompt at {difficulty} CEFR level."
|
||||
)
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
fields_to_check=["question"],
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Speaking
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_speaking_task(
|
||||
self,
|
||||
part,
|
||||
topic=None,
|
||||
first_topic=None,
|
||||
second_topic=None,
|
||||
difficulty=None,
|
||||
):
|
||||
"""Generate a speaking task for the given part (1, 2, or 3)."""
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON. "
|
||||
"Generate IELTS speaking task questions."
|
||||
)
|
||||
|
||||
if part == 1:
|
||||
first_topic = first_topic or random.choice(TOPICS)
|
||||
second_topic = second_topic or random.choice(TOPICS)
|
||||
user_msg = (
|
||||
"Craft 5 simple and single questions of easy difficulty for "
|
||||
"IELTS Speaking Part 1 that encourages candidates to delve "
|
||||
"deeply into personal experiences, preferences, or insights "
|
||||
f'on the topic of "{first_topic}" and the topic of '
|
||||
f'"{second_topic}". The questions should lead to the usage '
|
||||
"of 4 verb tenses (present perfect, present, past and future). "
|
||||
"Make sure that the generated question does not contain "
|
||||
"forbidden subjects in muslim countries."
|
||||
)
|
||||
elif part == 2:
|
||||
topic = topic or random.choice(TOPICS)
|
||||
user_msg = (
|
||||
"Create a question of medium difficulty for IELTS Speaking "
|
||||
"Part 2 that encourages candidates to narrate a personal "
|
||||
f'experience or story related to the topic of "{topic}". '
|
||||
"Include 3 prompts that guide the candidate. The prompts "
|
||||
"must not be questions. Also include a suffix like the ones "
|
||||
'in the IELTS exams that start with "And explain why". '
|
||||
"Make sure that the generated question does not contain "
|
||||
"forbidden subjects in muslim countries."
|
||||
)
|
||||
else:
|
||||
topic = topic or random.choice(TOPICS)
|
||||
user_msg = (
|
||||
"Formulate a set of 5 single questions of hard difficulty "
|
||||
"for IELTS Speaking Part 3 that encourage candidates to "
|
||||
"engage in a meaningful discussion on the topic of "
|
||||
f'"{topic}". They must be 1 single question each and not '
|
||||
"be double-barreled questions. Make sure that the generated "
|
||||
"question does not contain forbidden subjects in muslim countries."
|
||||
)
|
||||
|
||||
if difficulty:
|
||||
user_msg += f" Target CEFR level: {difficulty}."
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Level exercises
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def generate_level_exercises(self, exercises_config, difficulty):
|
||||
"""Generate level-exam exercises (multiple choice, fill-blanks, etc.).
|
||||
|
||||
exercises_config: list of dicts with 'type', 'quantity', and optional
|
||||
'topic', 'size' keys.
|
||||
difficulty: CEFR level string.
|
||||
"""
|
||||
system_msg = (
|
||||
"You are a helpful assistant designed to output JSON. "
|
||||
"Generate English level exam exercises."
|
||||
)
|
||||
|
||||
prompts = []
|
||||
for cfg in exercises_config:
|
||||
ex_type = cfg.get("type", "multiple_choice")
|
||||
quantity = cfg.get("quantity", 5)
|
||||
topic = cfg.get("topic") or random.choice(TOPICS)
|
||||
size = cfg.get("size", 200)
|
||||
|
||||
if ex_type == "multiple_choice":
|
||||
prompts.append(
|
||||
f"Generate {quantity} multiple choice questions of 4 options "
|
||||
f"for an english level exam of {difficulty} CEFR level. "
|
||||
"Ensure that the questions cover a range of topics such as "
|
||||
"verb tense, subject-verb agreement, pronoun usage, sentence "
|
||||
"structure, and punctuation. Make sure every question only has "
|
||||
"1 correct answer."
|
||||
)
|
||||
elif ex_type == "fill_blanks":
|
||||
prompts.append(
|
||||
f"Generate a text of at least {size} words about the topic "
|
||||
f"{topic}. From the generated text choose exactly {quantity} "
|
||||
"words (cannot be sequential words), replace each with "
|
||||
"{id}, and generate a JSON object containing: the modified "
|
||||
"text, solutions array, words array with four options per blank."
|
||||
)
|
||||
|
||||
user_msg = "\n\n".join(prompts)
|
||||
|
||||
return self.ai.prediction(
|
||||
model=GPT_MODELS["generation"],
|
||||
messages=[
|
||||
{"role": "system", "content": system_msg},
|
||||
{"role": "user", "content": user_msg},
|
||||
],
|
||||
temperature=TEMPERATURE["generation"],
|
||||
)
|
||||
Reference in New Issue
Block a user