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