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
353 lines
14 KiB
Python
353 lines
14 KiB
Python
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"],
|
|
)
|