# -*- coding: utf-8 -*- from odoo import api, models, _ from odoo.exceptions import UserError, ValidationError from bs4 import BeautifulSoup as BS # Note: Use the modern OpenAI client # pip install openai try: from openai import OpenAI except Exception: # pragma: no cover OpenAI = None class ChatGptBot(models.AbstractModel): _inherit = 'mail.bot' _description = 'ChatGPT OdooBot' ################################################################################################# # ORM FUNCTIONS # ################################################################################################# def create_content(self): """ Generates HTML content and SEO JSON via OpenAI and writes back to fields if present. Expected optional fields on the record: - text_chatgpt (input prompt) - content (HTML output) - website_meta_title / website_meta_description / website_meta_keywords (SEO) - is_publish_now / is_published / is_elaborate This method is defensive: if fields do not exist on the record, it skips setting them. """ # Ensure running on a single record or abstractly use 'self' as context holder record = self # Validate input field existence text = getattr(record, 'text_chatgpt', None) if not text: raise UserError(_("Please provide a prompt (text_chatgpt) before generating content.")) api_key = self.env['ir.config_parameter'].sudo().get_param('chatgpt_api_key') if not api_key: raise UserError(_("No OpenAI API key configured. Set it in Settings → ChatGPT OdooBot.")) if OpenAI is None: raise UserError(_("The OpenAI Python package is not installed. Please install 'openai'.")) # Define an example JSON schema for SEO guidance ex_json = { "title": "Sample Title", "description": "Short description (max 160 characters)", "keywords": "keyword1, keyword2, keyword3" } client = OpenAI(api_key=api_key) # Prompt instructs the model to output: 1) an HTML
...
, then 2) a pure JSON block system_msg = "You are a helpful assistant that writes clean HTML and valid JSON." user_msg = ( f"{text}\n\n" "Output requirements:\n" "1) First, return an HTML block wrapped strictly between
and
with headings, paragraphs, and links.\n" "2) Immediately after the closing , return ONLY a valid JSON object (no backticks, no labels)\n" f"matching this schema and rules (description max 160 chars): {ex_json}\n" ) try: resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=[ {"role": "system", "content": system_msg}, {"role": "user", "content": user_msg}, ], max_tokens=1200, temperature=0.7, ) full = resp.choices[0].message.content or "" except Exception as e: raise UserError(_("OpenAI request failed: %s") % str(e)) # Split HTML and JSON by the first html_part, json_part = "", "" if "" in full: html_part = full.split("", 1)[0] + "" json_part = full.split("", 1)[1].strip() # Parse SEO JSON if present title = descr = kw = None if json_part: import json try: seo = json.loads(json_part) title = seo.get("title") descr = seo.get("description") kw = seo.get("keywords") except Exception: # We don't hard-fail if JSON is malformed; raise a friendly error raise ValidationError(_("The SEO JSON response is not valid JSON.")) # Assign fields defensively if they exist on the record def safe_set(rec, field, value): if hasattr(rec, field): try: setattr(rec, field, value) except Exception: pass safe_set(record, 'content', html_part or "") if title: safe_set(record, 'website_meta_title', title) if descr: safe_set(record, 'website_meta_description', descr) if kw: safe_set(record, 'website_meta_keywords', kw) # Handle publish flags if the model uses them is_publish_now = getattr(record, 'is_publish_now', False) if is_publish_now and hasattr(record, 'is_published'): safe_set(record, 'is_published', True) elif hasattr(record, 'is_published'): safe_set(record, 'is_published', False) safe_set(record, 'is_elaborate', True) return True ################################################################################################# # CUSTOM FUNCTIONS # ################################################################################################# def _get_answer(self, record, body, values, command=None): """ Odoo 18-compatible signature. Adds #enable / #disable and routes to ChatGPT if enabled. """ res = super()._get_answer(record, body, values, command=command) # Simple toggles if body.strip().lower() == "#enable": self.env.user.odoobot_state = 'chatgpt' return _("ChatGPT enabled") if body.strip().lower() == "#disable": self.env.user.odoobot_state = 'disabled' return _("ChatGPT disabled") # Build a short context from last messages (plaintext only) channel = self.env['mail.channel'].browse(record.id) last_ids = channel.message_ids.ids messages = self.env['mail.message'].search([('id', 'in', last_ids)], order='id desc', limit=2).mapped('body') old_conv = "" for msg in messages: if msg: old_conv += BS(msg, 'html.parser').get_text() + "\n" # Route to ChatGPT if enabled if self.env.user.odoobot_state == 'chatgpt': return self._chatgpt_reply(record, body, old_conv) return res def _chatgpt_reply(self, record, body, context_text=""): api_key = self.env['ir.config_parameter'].sudo().get_param('chatgpt_api_key') if not api_key: raise UserError(_("No OpenAI API key configured. Set it in Settings → ChatGPT OdooBot.")) if OpenAI is None: raise UserError(_("The OpenAI Python package is not installed. Please install 'openai'.")) client = OpenAI(api_key=api_key) try: messages = [] if context_text: messages.append({"role": "system", "content": f"Conversation context:\n{context_text.strip()}"}) messages.append({"role": "user", "content": body}) resp = client.chat.completions.create( model="gpt-3.5-turbo", messages=messages, max_tokens=800, temperature=0.7, ) reply = resp.choices[0].message.content or "" except Exception as e: raise UserError(_("OpenAI request failed: %s") % str(e)) gpt_html = "OpenAI: " + reply author_id = self.env.ref("base.partner_root").id subtype_id = self.env.ref("mail.mt_comment").id self.env['mail.channel'].browse(record.id).message_post( body=gpt_html, message_type='comment', subtype_id=subtype_id, author_id=author_id, ) return gpt_html