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