odex30_standard/chatgpt_bot/models/mail_bot.py

196 lines
7.8 KiB
Python

# -*- 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 <div> ... </div>, 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 <div> and </div> with headings, paragraphs, and links.\n"
"2) Immediately after the closing </div>, 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 </div>
html_part, json_part = "", ""
if "</div>" in full:
html_part = full.split("</div>", 1)[0] + "</div>"
json_part = full.split("</div>", 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 = "<strong>OpenAI:</strong> " + 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