[IMP] website: generate text with a LLM for configurator pages
When building a website with the website configurator, you get pages with a nice layout. However, the text inside is not adapted to your company, industry or even, most of the time, language. Using a LLM, we could easily get text much more relevant to customer needs. The scope of this work is static pages generated with the website configurator (homepage, about us, pricing, ...). Later, in other tasks, we also want users to be able to generate text inside the website builder. This commit implement the bare minimum to generate and replace a website content by AI generated sentences (based on the information provided by the user). Related IAP PR at https://github.com/odoo/iap-apps/pull/656 task-3248852 closes odoo/odoo#121021 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com> Co-authored-by: qsm-odoo <qsm@odoo.com>
This commit is contained in:
committed by
qsm-odoo
co-authored by
qsm-odoo
parent
bd456ec5b5
commit
0d448d950c
@@ -14,7 +14,6 @@ from psycopg2 import sql
|
||||
from werkzeug import urls
|
||||
from werkzeug.datastructures import OrderedMultiDict
|
||||
from werkzeug.exceptions import NotFound
|
||||
from markupsafe import Markup
|
||||
|
||||
from odoo import api, fields, models, tools, http, release, registry
|
||||
from odoo.addons.http_routing.models.ir_http import RequestUID, slugify, url_for
|
||||
@@ -26,7 +25,7 @@ from odoo.exceptions import AccessError, MissingError, UserError, ValidationErro
|
||||
from odoo.http import request
|
||||
from odoo.modules.module import get_resource_path, get_manifest
|
||||
from odoo.osv.expression import AND, OR, FALSE_DOMAIN, get_unaccent_wrapper
|
||||
from odoo.tools.translate import _
|
||||
from odoo.tools.translate import _, xml_translate
|
||||
from odoo.tools import escape_psql, pycompat
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -42,7 +41,8 @@ DEFAULT_CDN_FILTERS = [
|
||||
"^/website/image/",
|
||||
]
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://website.api.odoo.com'
|
||||
DEFAULT_WEBSITE_ENDPOINT = 'https://website.api.odoo.com'
|
||||
DEFAULT_OLG_ENDPOINT = 'https://olg.api.odoo.com'
|
||||
|
||||
|
||||
class Website(models.Model):
|
||||
@@ -333,12 +333,20 @@ class Website(models.Model):
|
||||
# ----------------------------------------------------------
|
||||
# Configurator
|
||||
# ----------------------------------------------------------
|
||||
def _website_api_rpc(self, route, params):
|
||||
|
||||
def _api_rpc(self, route, params, endpoint_param_name, default_endpoint, **kwargs):
|
||||
params['version'] = release.version
|
||||
IrConfigParameter = self.env['ir.config_parameter'].sudo()
|
||||
website_api_endpoint = IrConfigParameter.get_param('website.website_api_endpoint', DEFAULT_ENDPOINT)
|
||||
endpoint = website_api_endpoint + route
|
||||
return iap_tools.iap_jsonrpc(endpoint, params=params)
|
||||
api_endpoint = IrConfigParameter.get_param(endpoint_param_name, default_endpoint)
|
||||
return iap_tools.iap_jsonrpc(api_endpoint + route, params=params, **kwargs)
|
||||
|
||||
def _website_api_rpc(self, route, params):
|
||||
# For industries, theme suggestions, ...
|
||||
return self._api_rpc(route, params, 'website.website_api_endpoint', DEFAULT_WEBSITE_ENDPOINT)
|
||||
|
||||
def _OLG_api_rpc(self, route, params):
|
||||
# For text content generation
|
||||
return self._api_rpc(route, params, 'website.olg_api_endpoint', DEFAULT_OLG_ENDPOINT, timeout=300)
|
||||
|
||||
def get_cta_data(self, website_purpose, website_type):
|
||||
return {'cta_btn_text': False, 'cta_btn_href': '/contactus'}
|
||||
@@ -577,9 +585,55 @@ class Website(models.Model):
|
||||
{'theme': theme_name}
|
||||
)
|
||||
|
||||
# Configure the pages
|
||||
requested_pages = list(pages_views.keys()) + ['homepage']
|
||||
# Generate text for the pages
|
||||
requested_pages = set(pages_views.keys()).union({'homepage'})
|
||||
snippet_lists = website.get_theme_snippet_lists(theme_name)
|
||||
industry = kwargs['industry_name']
|
||||
|
||||
IrQweb = self.env['ir.qweb'].with_context(website_id=website.id, lang=website.default_lang_id.code)
|
||||
snippets_cache = {}
|
||||
|
||||
def _compute_placeholder(term):
|
||||
return xml_translate.get_text_content(term).strip()
|
||||
|
||||
def _render_snippet(key):
|
||||
# Using this avoids rendering the same snippet multiple times
|
||||
data = snippets_cache.get(key)
|
||||
if data:
|
||||
return data
|
||||
|
||||
render = IrQweb._render(key, cta_data)
|
||||
|
||||
terms = []
|
||||
xml_translate(terms.append, render)
|
||||
placeholders = [_compute_placeholder(term) for term in terms]
|
||||
|
||||
data = (render, placeholders)
|
||||
snippets_cache[key] = data
|
||||
return data
|
||||
|
||||
generated_content = {}
|
||||
for page_code in requested_pages - {'privacy_policy'}:
|
||||
snippet_list = snippet_lists.get(page_code, [])
|
||||
for snippet in snippet_list:
|
||||
render, placeholders = _render_snippet(f'website.{snippet}')
|
||||
for placeholder in placeholders:
|
||||
generated_content[placeholder] = ''
|
||||
try:
|
||||
response = self._OLG_api_rpc('/api/olg/1/generate_placeholder', {
|
||||
'placeholders': list(generated_content.keys()),
|
||||
'lang': website.default_lang_id.name,
|
||||
'industry': industry,
|
||||
})
|
||||
name_replace_parser = re.compile(r"XXXX", re.MULTILINE)
|
||||
for key in generated_content:
|
||||
if response.get(key):
|
||||
generated_content[key] = (name_replace_parser.sub(website.name, response[key], 0))
|
||||
except AccessError:
|
||||
# If IAP is broken continue normally (without generating text)
|
||||
pass
|
||||
|
||||
# Configure the pages
|
||||
for page_code in requested_pages:
|
||||
snippet_list = snippet_lists.get(page_code, [])
|
||||
if page_code == 'homepage':
|
||||
@@ -590,8 +644,14 @@ class Website(models.Model):
|
||||
nb_snippets = len(snippet_list)
|
||||
for i, snippet in enumerate(snippet_list, start=1):
|
||||
try:
|
||||
IrQweb = self.env['ir.qweb'].with_context(website_id=website.id, lang=website.default_lang_id.code)
|
||||
render = IrQweb._render('website.' + snippet, cta_data)
|
||||
render, placeholders = _render_snippet(f'website.{snippet}')
|
||||
|
||||
# Fill rendered block with AI text
|
||||
render = xml_translate(
|
||||
lambda x: generated_content.get(_compute_placeholder(x), x),
|
||||
render
|
||||
)
|
||||
|
||||
el = html.fromstring(render)
|
||||
|
||||
# Add the data-snippet attribute to identify the snippet
|
||||
|
||||
@@ -362,6 +362,7 @@ class ApplyConfiguratorScreen extends Component {
|
||||
const data = {
|
||||
'selected_features': selectedFeatures,
|
||||
'industry_id': this.state.selectedIndustry.id,
|
||||
'industry_name': this.state.selectedIndustry.label,
|
||||
'selected_palette': selectedPalette,
|
||||
'theme_name': themeName,
|
||||
'website_purpose': WEBSITE_PURPOSES[
|
||||
|
||||
@@ -39,6 +39,8 @@ class TestConfiguratorCommon(odoo.tests.HttpCase):
|
||||
return []
|
||||
elif '/api/website/2/configurator/custom_resources/' in endpoint:
|
||||
return {'images': {}}
|
||||
elif '/api/olg/1/generate_placeholder' in endpoint:
|
||||
return {"a non existing placeholder": "😠", 'Catchy Headline': 'Welcome to XXXX - Your Super test'}
|
||||
|
||||
iap_patch = patch('odoo.addons.iap.tools.iap_tools.iap_jsonrpc', iap_jsonrpc_mocked_configurator)
|
||||
self.startPatcher(iap_patch)
|
||||
|
||||
Reference in New Issue
Block a user