196 lines
7.8 KiB
Python
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
|