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_training/__init__.py
Normal file
2
encoach_training/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import models
|
||||
from . import services
|
||||
17
encoach_training/__manifest__.py
Normal file
17
encoach_training/__manifest__.py
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "EnCoach Training",
|
||||
"version": "19.0.1.0.0",
|
||||
"category": "Education",
|
||||
"summary": "Personalized training content and tips for EnCoach",
|
||||
"description": "FAISS-powered tip retrieval, GPT personalized training, walkthroughs.",
|
||||
"depends": ["encoach_core", "encoach_ai"],
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"security/rules.xml",
|
||||
"views/training_views.xml",
|
||||
"views/tip_views.xml",
|
||||
"views/walkthrough_views.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"license": "LGPL-3",
|
||||
}
|
||||
3
encoach_training/models/__init__.py
Normal file
3
encoach_training/models/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from . import training
|
||||
from . import training_tip
|
||||
from . import walkthrough
|
||||
28
encoach_training/models/training.py
Normal file
28
encoach_training/models/training.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class EncoachTraining(models.Model):
|
||||
_name = "encoach.training"
|
||||
_description = "EnCoach Training Record"
|
||||
|
||||
user_id = fields.Many2one(
|
||||
"res.users", string="User", required=True, ondelete="cascade",
|
||||
)
|
||||
exams = fields.Json()
|
||||
tips = fields.Json()
|
||||
weak_areas = fields.Json()
|
||||
created_at = fields.Datetime(default=fields.Datetime.now)
|
||||
legacy_id = fields.Char(index=True)
|
||||
|
||||
def to_encoach_dict(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"id": self.id,
|
||||
"userId": self.user_id.id if self.user_id else None,
|
||||
"exams": self.exams,
|
||||
"tips": self.tips,
|
||||
"weakAreas": self.weak_areas,
|
||||
"createdAt": (
|
||||
self.created_at.isoformat() if self.created_at else None
|
||||
),
|
||||
}
|
||||
22
encoach_training/models/training_tip.py
Normal file
22
encoach_training/models/training_tip.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class EncoachTrainingTip(models.Model):
|
||||
_name = "encoach.training.tip"
|
||||
_description = "EnCoach Training Tip"
|
||||
|
||||
tip_id = fields.Char(required=True, index=True)
|
||||
category = fields.Selection(
|
||||
[
|
||||
("ct_focus", "CT Focus"),
|
||||
("language_for_writing", "Language for Writing"),
|
||||
("reading_skill", "Reading Skill"),
|
||||
("strategy", "Strategy"),
|
||||
("writing_skill", "Writing Skill"),
|
||||
("word_link", "Word Link"),
|
||||
("word_partners", "Word Partners"),
|
||||
],
|
||||
string="Category",
|
||||
)
|
||||
content = fields.Text(required=True)
|
||||
embedding = fields.Binary(string="FAISS Embedding Vector")
|
||||
19
encoach_training/models/walkthrough.py
Normal file
19
encoach_training/models/walkthrough.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from odoo import models, fields
|
||||
|
||||
|
||||
class EncoachWalkthrough(models.Model):
|
||||
_name = "encoach.walkthrough"
|
||||
_description = "EnCoach Walkthrough"
|
||||
|
||||
name = fields.Char()
|
||||
content = fields.Json()
|
||||
module = fields.Selection(
|
||||
[
|
||||
("reading", "Reading"),
|
||||
("listening", "Listening"),
|
||||
("writing", "Writing"),
|
||||
("speaking", "Speaking"),
|
||||
("level", "Level"),
|
||||
],
|
||||
string="Module",
|
||||
)
|
||||
10
encoach_training/security/ir.model.access.csv
Normal file
10
encoach_training/security/ir.model.access.csv
Normal file
@@ -0,0 +1,10 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_encoach_training_student,encoach.training.student,model_encoach_training,encoach_core.group_encoach_student,1,0,0,0
|
||||
access_encoach_training_admin,encoach.training.admin,model_encoach_training,encoach_core.group_encoach_admin,1,1,1,1
|
||||
access_encoach_training_tip_student,encoach.training.tip.student,model_encoach_training_tip,encoach_core.group_encoach_student,1,0,0,0
|
||||
access_encoach_training_tip_admin,encoach.training.tip.admin,model_encoach_training_tip,encoach_core.group_encoach_admin,1,1,1,1
|
||||
access_encoach_walkthrough_student,encoach.walkthrough.student,model_encoach_walkthrough,encoach_core.group_encoach_student,1,0,0,0
|
||||
access_encoach_walkthrough_admin,encoach.walkthrough.admin,model_encoach_walkthrough,encoach_core.group_encoach_admin,1,1,1,1
|
||||
access_encoach_training_sysadmin,encoach.training.sysadmin,model_encoach_training,base.group_system,1,1,1,1
|
||||
access_encoach_training_tip_sysadmin,encoach.training.tip.sysadmin,model_encoach_training_tip,base.group_system,1,1,1,1
|
||||
access_encoach_walkthrough_sysadmin,encoach.walkthrough.sysadmin,model_encoach_walkthrough,base.group_system,1,1,1,1
|
||||
|
17
encoach_training/security/rules.xml
Normal file
17
encoach_training/security/rules.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="rule_training_student_own" model="ir.rule">
|
||||
<field name="name">Students see own training</field>
|
||||
<field name="model_id" ref="model_encoach_training"/>
|
||||
<field name="groups" eval="[(4, ref('encoach_core.group_encoach_student'))]"/>
|
||||
<field name="domain_force">[('user_id', '=', user.id)]</field>
|
||||
</record>
|
||||
<record id="rule_training_admin" model="ir.rule">
|
||||
<field name="name">Admins see all training</field>
|
||||
<field name="model_id" ref="model_encoach_training"/>
|
||||
<field name="groups" eval="[(4, ref('encoach_core.group_encoach_admin'))]"/>
|
||||
<field name="domain_force">[(1, '=', 1)]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
1
encoach_training/services/__init__.py
Normal file
1
encoach_training/services/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import training_service
|
||||
177
encoach_training/services/training_service.py
Normal file
177
encoach_training/services/training_service.py
Normal file
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
import logging
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
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__)
|
||||
|
||||
TIPS_PROMPT = (
|
||||
"You are an IELTS tutor. Based on the context, the question, the "
|
||||
"student's wrong answer, and the correct answer, provide a concise, "
|
||||
"helpful tip to help the student improve. Use the following reference "
|
||||
"tips to inform your response but adapt them to the specific situation.\n\n"
|
||||
"Reference tips:\n{tips}\n\n"
|
||||
"Context: {context}\n"
|
||||
"Question: {question}\n"
|
||||
"Student's answer: {answer}\n"
|
||||
"Correct answer: {correct_answer}\n\n"
|
||||
"Provide your response as JSON: "
|
||||
'{{"tips": "your personalized tip text"}}'
|
||||
)
|
||||
|
||||
TRAINING_PROMPT = (
|
||||
"You are an IELTS training advisor. Analyze the student's exam history "
|
||||
"and generate personalized training recommendations.\n\n"
|
||||
"Student stats:\n{stats}\n\n"
|
||||
"Generate a JSON response with:\n"
|
||||
'{{"exams": [recommended practice exams], '
|
||||
'"tips": [personalized tips], '
|
||||
'"weakAreas": [identified weak areas with suggestions]}}'
|
||||
)
|
||||
|
||||
|
||||
class EncoachTrainingService:
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
self.ai = EncoachOpenAIService(env)
|
||||
self._faiss_index = None
|
||||
self._tip_ids = None
|
||||
|
||||
def fetch_tips(self, context, question, answer, correct_answer):
|
||||
"""Retrieve relevant tips using FAISS + generate personalized tip via GPT."""
|
||||
query = f"{context} {question} {answer}"
|
||||
similar_tips = self._query_faiss(query, category=None, top_k=5)
|
||||
|
||||
tips_text = "\n".join(
|
||||
f"- [{t.category}] {t.content}" for t in similar_tips
|
||||
)
|
||||
|
||||
prompt = TIPS_PROMPT.format(
|
||||
tips=tips_text,
|
||||
context=context,
|
||||
question=question,
|
||||
answer=answer,
|
||||
correct_answer=correct_answer,
|
||||
)
|
||||
|
||||
result = self.ai.prediction(
|
||||
model=GPT_MODELS["grading"],
|
||||
messages=[
|
||||
{"role": "system", "content": "You are a helpful IELTS tutor. Output JSON."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=TEMPERATURE["tips"],
|
||||
check_blacklisted=False,
|
||||
)
|
||||
return result
|
||||
|
||||
def get_training_content(self, user_id, stats):
|
||||
"""Generate full training content for a user based on their exam stats."""
|
||||
prompt = TRAINING_PROMPT.format(stats=json.dumps(stats))
|
||||
|
||||
result = self.ai.prediction(
|
||||
model=GPT_MODELS["grading"],
|
||||
messages=[
|
||||
{"role": "system", "content": "You are an IELTS training advisor. Output JSON."},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=TEMPERATURE["tips"],
|
||||
check_blacklisted=False,
|
||||
)
|
||||
|
||||
if result:
|
||||
training = self.env["encoach.training"].sudo().create({
|
||||
"user_id": user_id,
|
||||
"exams": result.get("exams"),
|
||||
"tips": result.get("tips"),
|
||||
"weak_areas": result.get("weakAreas"),
|
||||
})
|
||||
return training
|
||||
return None
|
||||
|
||||
def _query_faiss(self, query, category=None, top_k=5):
|
||||
"""Search FAISS index for similar training tips.
|
||||
|
||||
Falls back to keyword search if FAISS is not available.
|
||||
"""
|
||||
try:
|
||||
import faiss
|
||||
from sentence_transformers import SentenceTransformer
|
||||
except ImportError:
|
||||
_logger.warning("FAISS/sentence-transformers not installed, falling back to keyword search")
|
||||
return self._keyword_fallback(query, category, top_k)
|
||||
|
||||
self._ensure_faiss_index()
|
||||
if self._faiss_index is None:
|
||||
return self._keyword_fallback(query, category, top_k)
|
||||
|
||||
model = SentenceTransformer("all-MiniLM-L6-v2")
|
||||
query_vec = model.encode([query]).astype(np.float32)
|
||||
|
||||
distances, indices = self._faiss_index.search(query_vec, top_k * 2)
|
||||
|
||||
tip_ids = [self._tip_ids[i] for i in indices[0] if i < len(self._tip_ids)]
|
||||
domain = [("id", "in", tip_ids)]
|
||||
if category:
|
||||
domain.append(("category", "=", category))
|
||||
|
||||
tips = self.env["encoach.training.tip"].sudo().search(domain, limit=top_k)
|
||||
return tips
|
||||
|
||||
def _ensure_faiss_index(self):
|
||||
"""Build or load the FAISS index from stored tip embeddings."""
|
||||
if self._faiss_index is not None:
|
||||
return
|
||||
|
||||
try:
|
||||
import faiss
|
||||
except ImportError:
|
||||
return
|
||||
|
||||
tips = self.env["encoach.training.tip"].sudo().search(
|
||||
[("embedding", "!=", False)]
|
||||
)
|
||||
if not tips:
|
||||
return
|
||||
|
||||
vectors = []
|
||||
ids = []
|
||||
for tip in tips:
|
||||
vec = pickle.loads(tip.embedding)
|
||||
vectors.append(vec)
|
||||
ids.append(tip.id)
|
||||
|
||||
matrix = np.array(vectors, dtype=np.float32)
|
||||
|
||||
dim = matrix.shape[1]
|
||||
index = faiss.IndexFlatL2(dim)
|
||||
index.add(matrix)
|
||||
|
||||
self._faiss_index = index
|
||||
self._tip_ids = ids
|
||||
|
||||
def _keyword_fallback(self, query, category, top_k):
|
||||
"""Simple keyword-based search when FAISS is unavailable."""
|
||||
domain = []
|
||||
if category:
|
||||
domain.append(("category", "=", category))
|
||||
|
||||
tips = self.env["encoach.training.tip"].sudo().search(domain, limit=200)
|
||||
|
||||
query_words = set(query.lower().split())
|
||||
scored = []
|
||||
for tip in tips:
|
||||
content_words = set(tip.content.lower().split())
|
||||
overlap = len(query_words & content_words)
|
||||
if overlap > 0:
|
||||
scored.append((overlap, tip))
|
||||
|
||||
scored.sort(key=lambda x: x[0], reverse=True)
|
||||
return self.env["encoach.training.tip"].browse(
|
||||
[t.id for _, t in scored[:top_k]]
|
||||
)
|
||||
54
encoach_training/views/tip_views.xml
Normal file
54
encoach_training/views/tip_views.xml
Normal file
@@ -0,0 +1,54 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_training_tip_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.training.tip.tree</field>
|
||||
<field name="model">encoach.training.tip</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Training Tips">
|
||||
<field name="tip_id"/>
|
||||
<field name="category"/>
|
||||
<field name="content"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_training_tip_form" model="ir.ui.view">
|
||||
<field name="name">encoach.training.tip.form</field>
|
||||
<field name="model">encoach.training.tip</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Training Tip">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="tip_id"/>
|
||||
<field name="category"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="content" widget="text"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_training_tip" model="ir.actions.act_window">
|
||||
<field name="name">Training Tips</field>
|
||||
<field name="res_model">encoach.training.tip</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first training tip.
|
||||
</p>
|
||||
<p>
|
||||
Training tips provide personalized advice for users based on categories like strategy, writing skills, and reading skills.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_training_tips"
|
||||
name="Training Tips"
|
||||
parent="encoach_core.menu_encoach_training"
|
||||
action="action_encoach_training_tip"
|
||||
sequence="20"/>
|
||||
</odoo>
|
||||
65
encoach_training/views/training_views.xml
Normal file
65
encoach_training/views/training_views.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_training_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.training.tree</field>
|
||||
<field name="model">encoach.training</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Training Records">
|
||||
<field name="user_id"/>
|
||||
<field name="created_at"/>
|
||||
<field name="legacy_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_training_form" model="ir.ui.view">
|
||||
<field name="name">encoach.training.form</field>
|
||||
<field name="model">encoach.training</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Training Record">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="user_id"/>
|
||||
<field name="created_at"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="legacy_id"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="exams" widget="json"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="tips" widget="json"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="weak_areas" widget="json"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_training" model="ir.actions.act_window">
|
||||
<field name="name">Training Records</field>
|
||||
<field name="res_model">encoach.training</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first training record.
|
||||
</p>
|
||||
<p>
|
||||
Training records store personalized exam data, tips, and weak areas for each user.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_training_records"
|
||||
name="Training Records"
|
||||
parent="encoach_core.menu_encoach_training"
|
||||
action="action_encoach_training"
|
||||
sequence="10"/>
|
||||
</odoo>
|
||||
53
encoach_training/views/walkthrough_views.xml
Normal file
53
encoach_training/views/walkthrough_views.xml
Normal file
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
<record id="view_encoach_walkthrough_tree" model="ir.ui.view">
|
||||
<field name="name">encoach.walkthrough.tree</field>
|
||||
<field name="model">encoach.walkthrough</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="Walkthroughs">
|
||||
<field name="name"/>
|
||||
<field name="module"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_encoach_walkthrough_form" model="ir.ui.view">
|
||||
<field name="name">encoach.walkthrough.form</field>
|
||||
<field name="model">encoach.walkthrough</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Walkthrough">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="module"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="content" widget="json"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_encoach_walkthrough" model="ir.actions.act_window">
|
||||
<field name="name">Walkthroughs</field>
|
||||
<field name="res_model">encoach.walkthrough</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first walkthrough.
|
||||
</p>
|
||||
<p>
|
||||
Walkthroughs provide guided content for Reading, Listening, Writing, Speaking, and Level modules.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_encoach_walkthroughs"
|
||||
name="Walkthroughs"
|
||||
parent="encoach_core.menu_encoach_training"
|
||||
action="action_encoach_walkthrough"
|
||||
sequence="30"/>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user