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
104 lines
3.3 KiB
Python
104 lines
3.3 KiB
Python
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))
|