Files
encoach_be_odoo19/encoach_training/services/training_service.py
Talal Sharabi f5b627256f 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
2026-03-14 16:46:46 +04:00

178 lines
5.8 KiB
Python

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]]
)