Files
encoach_be_odoo19/encoach_ai/services/openai_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

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