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

2
encoach_ai/__init__.py Normal file
View File

@@ -0,0 +1,2 @@
from . import models
from . import services

View File

@@ -0,0 +1,16 @@
{
"name": "EnCoach AI",
"version": "19.0.1.0.0",
"category": "Education",
"summary": "Core AI services for the EnCoach IELTS platform",
"description": "OpenAI integration, constants, blacklist validation, token counting.",
"depends": ["encoach_core"],
"data": [
"security/ir.model.access.csv",
],
"installable": True,
"license": "LGPL-3",
"external_dependencies": {
"python": ["openai", "tiktoken", "httpx", "tenacity", "numpy"],
},
}

View File

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

View File

@@ -0,0 +1,59 @@
BLACKLISTED_WORDS = [
"alcohol", "atheism", "beer", "bible", "blasphemy",
"buddha", "buddhism", "cannabis", "casino", "christ",
"christianity", "church", "cocaine", "communist", "condom",
"corruption", "crucifixion", "cult", "drug", "drugs",
"erotic", "extremism", "gambling", "gay", "god",
"gods", "gospel", "haram", "heresy", "heroin",
"hindu", "hinduism", "homosexual", "idol", "infidel",
"islam", "islamic", "israeli", "jesus", "jew",
"jewish", "jihad", "judaism", "lesbian", "liquor",
"marijuana", "mosque", "muhammad", "muslim", "naked",
"nazi", "nude", "pagan", "pig", "pork",
"porn", "pornography", "prayer", "priest", "prophet",
"prostitution", "quran", "rabbi", "rape", "religion",
"religious", "satan", "sex", "sexual", "sharia",
"shinto", "sikh", "strip", "suicide", "synagogue",
"temple", "terrorism", "terrorist", "torah", "vodka",
"whiskey", "wine", "worship", "zionism",
]
TOPICS = [
"Technology", "Education", "Health", "Environment", "Travel",
"Food and Nutrition", "Sports", "Music", "Art", "Science",
"History", "Geography", "Business", "Economy", "Media",
"Communication", "Fashion", "Architecture", "Transportation", "Energy",
"Water Resources", "Agriculture", "Space Exploration", "Wildlife",
"Climate Change", "Urbanization", "Rural Life", "Population Growth",
"Immigration", "Cultural Diversity", "Globalization", "Tourism",
"Employment", "Workplace Culture", "Leadership", "Innovation",
"Artificial Intelligence", "Social Media", "Advertising", "Cinema",
"Literature", "Languages", "Volunteering", "Charity",
"Mental Health", "Fitness", "Aging Population", "Childhood Development",
"Higher Education", "Online Learning", "Public Libraries",
"Museums", "Public Transport", "Cycling", "Renewable Energy",
"Recycling", "Waste Management", "Deforestation", "Ocean Conservation",
"Animal Rights", "Genetic Engineering", "Robotics", "Cybersecurity",
"Privacy", "Freedom of Speech", "Democracy", "Law Enforcement",
"Crime Prevention", "Housing", "Homelessness", "Poverty",
"Income Inequality", "Consumer Culture", "Minimalism", "Traditions",
"Festivals", "Cooking", "Gardening", "Photography",
"Podcasts", "Board Games", "Pet Ownership", "Astronomy",
"Archaeology", "Philosophy", "Psychology", "Sociology",
"Ecology", "Marine Biology", "Meteorology", "Geology",
"Time Management", "Stress Management", "Work-Life Balance",
]
CEFR_LEVELS = ["A1", "A2", "B1", "B2", "C1", "C2"]
GPT_MODELS = {
"grading": "gpt-4o",
"generation": "gpt-4o",
"secondary": "gpt-3.5-turbo",
}
TEMPERATURE = {
"grading": 0.1,
"generation": 0.7,
"tips": 0.2,
}

View File

@@ -0,0 +1 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink

View File

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

View File

@@ -0,0 +1,103 @@
import json
import logging
import tiktoken
from openai import OpenAI
from tenacity import retry, stop_after_attempt, wait_exponential
from ..models.constants import BLACKLISTED_WORDS
_logger = logging.getLogger(__name__)
TOKEN_RESERVE = 300
MODEL_TOKEN_LIMIT = 4097
class EncoachOpenAIService:
def __init__(self, env):
self.env = env
api_key = (
env["ir.config_parameter"]
.sudo()
.get_param("encoach.openai_api_key", "")
)
if not api_key:
_logger.warning("OpenAI API key not configured (encoach.openai_api_key)")
self.client = OpenAI(api_key=api_key)
def prediction(
self,
model,
messages,
temperature=0.7,
response_format=None,
fields_to_check=None,
check_blacklisted=True,
max_retries=2,
):
"""Call OpenAI chat completion with validation and blacklist filtering.
Returns parsed JSON dict on success or None on failure.
"""
if response_format is None:
response_format = {"type": "json_object"}
input_tokens = self._count_tokens(
" ".join(m.get("content", "") for m in messages if isinstance(m.get("content"), str)),
model,
)
max_tokens = max(MODEL_TOKEN_LIMIT - input_tokens - TOKEN_RESERVE, 256)
attempt = 0
while attempt <= max_retries:
try:
resp = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens,
response_format=response_format,
)
content = resp.choices[0].message.content
data = json.loads(content)
if check_blacklisted:
text_to_check = content
if fields_to_check and isinstance(data, dict):
text_to_check = " ".join(
str(data.get(f, "")) for f in fields_to_check
)
if self._check_blacklisted(text_to_check):
_logger.info(
"Blacklisted content detected (attempt %d/%d)",
attempt + 1,
max_retries + 1,
)
attempt += 1
continue
return data
except json.JSONDecodeError:
_logger.warning("Invalid JSON from OpenAI (attempt %d)", attempt + 1)
attempt += 1
except Exception:
_logger.exception("OpenAI API error (attempt %d)", attempt + 1)
attempt += 1
_logger.error("OpenAI prediction failed after %d attempts", max_retries + 1)
return None
@staticmethod
def _check_blacklisted(text):
lower = text.lower()
return any(word in lower for word in BLACKLISTED_WORDS)
@staticmethod
def _count_tokens(text, model="gpt-4o"):
try:
encoding = tiktoken.encoding_for_model(model)
except KeyError:
encoding = tiktoken.get_encoding("cl100k_base")
return len(encoding.encode(text))