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:
2
encoach_ai/__init__.py
Normal file
2
encoach_ai/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import services
|
||||
16
encoach_ai/__manifest__.py
Normal file
16
encoach_ai/__manifest__.py
Normal 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"],
|
||||
},
|
||||
}
|
||||
1
encoach_ai/models/__init__.py
Normal file
1
encoach_ai/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import constants
|
||||
59
encoach_ai/models/constants.py
Normal file
59
encoach_ai/models/constants.py
Normal 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,
|
||||
}
|
||||
1
encoach_ai/security/ir.model.access.csv
Normal file
1
encoach_ai/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/services/__init__.py
Normal file
1
encoach_ai/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import openai_service
|
||||
103
encoach_ai/services/openai_service.py
Normal file
103
encoach_ai/services/openai_service.py
Normal 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))
|
||||
Reference in New Issue
Block a user