diff --git a/openai_connector/README.rst b/openai_connector/README.rst new file mode 100644 index 00000000..8871594e --- /dev/null +++ b/openai_connector/README.rst @@ -0,0 +1,102 @@ +================ +OpenAI Connector +================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:30c53be77f7ad65281a7975a7a3cf834cc69470e0b2c9844b12f0a5a81ebae7d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Frmm--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/rmm-custom/tree/15.0/openai_connector + :alt: qrtl/rmm-custom + +|badge1| |badge2| |badge3| + +This module defines openai.vision.session, which enables programmatic +access to OpenAI. It supports multimodal input (images and text), +optional web search, and structured output via JSON Schema. + +The module is designed as a backend service and is intended to be +invoked by other modules. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +For the OpenAI API configuration: + +- Only the api_key field is required to be set. +- The base_url, token_type, and header_api_key_string are prefilled by + this module. + +Basically, the OpenAI session should be added by a specific module that +depends on this one. Adjust the session attributes: + +- Go to Settings → Technical →OpenAI Vision Sessions. + +For additional options and usage details, refer to the official +documentation: https://platform.openai.com/docs + +Usage +===== + +This module is intended to be used programmatically by other Odoo +modules. You create and configure ``openai.vision.session`` records and +call the method ``call_openai()`` from your own business logic. + +For example, a product name generator module may call this module's +session as follows: + +.. code:: python + + session = self.env["openai.vision.session"].search( + [("reference_code", "=", "product_name_generation")], + limit=1, + ) + input_image = f"{base_url}/web/image/{self._name}/{self.id}/image_1920" + input_data = json.dumps([ + {"role": "user", "content": [{"type": "input_image", "image_url": image_url}]} + ]) + response = session.call_openai(input_data) + +It is advised to delegate execution to background job queues (e.g., +queue.job) for asynchronous and fault-tolerant processing. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quartile + +Maintainers +----------- + +This module is part of the `qrtl/rmm-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/openai_connector/__init__.py b/openai_connector/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/openai_connector/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py new file mode 100644 index 00000000..26a232d7 --- /dev/null +++ b/openai_connector/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "OpenAI Connector", + "version": "15.0.1.0.0", + "author": "Quartile", + "license": "AGPL-3", + "website": "https://www.quartile.co", + "depends": ["base_api_connection"], + "data": [ + "data/api_config.xml", + "security/ir.model.access.csv", + "views/openai_vision_session_views.xml", + ], + "installable": True, +} diff --git a/openai_connector/data/api_config.xml b/openai_connector/data/api_config.xml new file mode 100644 index 00000000..a948d38f --- /dev/null +++ b/openai_connector/data/api_config.xml @@ -0,0 +1,10 @@ + + + OpenAI + https://api.openai.com/v1 + openai + Authorization + Bearer + Dummy Key + + diff --git a/openai_connector/i18n/ja.po b/openai_connector/i18n/ja.po new file mode 100644 index 00000000..1806fd8c --- /dev/null +++ b/openai_connector/i18n/ja.po @@ -0,0 +1,215 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * openai_connector +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-08-20 03:08+0000\n" +"PO-Revision-Date: 2025-08-20 03:08+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__create_uid +msgid "Created by" +msgstr "作成者" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__create_date +msgid "Created on" +msgstr "作成日" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__reference_code +msgid "Defines the specific purpose of the OpenAI Vision session." +msgstr "OpenAI Vissionセッションの利用方法を目的を定義します" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__display_name +msgid "Display Name" +msgstr "表示名" + +#. module: openai_connector +#: model:ir.model.fields.selection,name:openai_connector.selection__openai_vision_session__llm_model__gpt-4o +msgid "GPT-4o" +msgstr "GPT-4o" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__id +msgid "ID" +msgstr "ID" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__previous_response_id +msgid "ID of previous response to continue chat (expires in 30 days)." +msgstr "チャット継続用の前回のレスポンスID(30日で期限が切れる)" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__store_response +msgid "" +"If enabled, the API response is stored and the response ID is saved for " +"session continuity, allowing the conversation context to be preserved across" +" multiple calls." +msgstr "" +"有効にすると、APIレスポンスが保存され、レスポンスIDも保持されます。これにより複数回の呼び出しにまたがって会話のコンテキストが維持されます。" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__web_search +msgid "" +"If enabled, the model can use web search to find information beyond its " +"training data." +msgstr "有効にすると、モデルはトレーニングデータ以外の情報をウェブ検索で取得できます。" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__response_format_enabled +msgid "" +"If enabled, the response will be structured according to the Response Format" +" Schema (JSON)" +msgstr "有効にすると、レスポンスはJSON形式のスキーマに従って構造化されます。" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__instruction +msgid "Instruction" +msgstr "システム指示" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session____last_update +msgid "Last Modified on" +msgstr "最終変更日" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__write_uid +msgid "Last Updated by" +msgstr "最終更新者" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__write_date +msgid "Last Updated on" +msgstr "最終更新日" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__llm_model +msgid "Model" +msgstr "モデル" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__name +msgid "Name" +msgstr "名称" + +#. module: openai_connector +#: model:ir.ui.menu,name:openai_connector.menu_openai_root +msgid "OpenAI" +msgstr "OpenAI" + +#. module: openai_connector +#: model:ir.model,name:openai_connector.model_openai_vision_session +msgid "OpenAI Vision Session" +msgstr "OpenAI Visionセッション" + +#. module: openai_connector +#: model:ir.ui.menu,name:openai_connector.menu_openai_vision_sessions +msgid "OpenAI Vision Sessions" +msgstr "OpenAI Visionセッション" + +#. module: openai_connector +#: code:addons/openai_connector/models/openai_vision_session.py:0 +#, python-format +msgid "" +"OpenAI refused the request:\n" +"{}" +msgstr "" +"OpenAIがリクエストを拒否しました:\n" +"{}" + +#. module: openai_connector +#: code:addons/openai_connector/models/openai_vision_session.py:0 +#, python-format +msgid "OpenAI returned invalid JSON." +msgstr "OpenAI から無効な JSON が返されました" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__previous_response_id +msgid "Previous Response ID" +msgstr "前回のレスポンスID" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__reference_code +msgid "Reference Code" +msgstr "参照コード" + +#. module: openai_connector +#: model_terms:ir.ui.view,arch_db:openai_connector.view_openai_vision_session_form +msgid "Response Format" +msgstr "レスポンス形式" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__response_format_schema +msgid "Response Format Schema (JSON)" +msgstr "レスポンス形式スキーマ(JSON)" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__temperature +msgid "" +"Sets response randomness (0–2). Higher is more creative, lower is more " +"focused." +msgstr "レスポンスのランダム性を設定します(0〜2)。数値が高いほど創造的になり、低いほど正確な応答になります。" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__store_response +msgid "Store Response" +msgstr "レスポンスを保存" + +#. module: openai_connector +#: model:ir.model.fields,help:openai_connector.field_openai_vision_session__instruction +msgid "" +"System-level instruction for the assistant. Example: You are a helpful " +"assistant that answers in Japanese." +msgstr "OpenAIへのシステムレベルの指示。例:「あなたは日本語で答える親切なアシスタントです。」" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__temperature +msgid "Temperature" +msgstr "創造性" + +#. module: openai_connector +#: code:addons/openai_connector/models/openai_vision_session.py:0 +#, python-format +msgid "" +"The JSON schema is invalid:\n" +"{}" +msgstr "" +"JSON スキーマが無効です:\n" +"{}" + +#. module: openai_connector +#: code:addons/openai_connector/models/openai_vision_session.py:0 +#, python-format +msgid "The inputs are invalid: %s" +msgstr "Inputsが無効です: %s" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__response_format_enabled +msgid "Use Structured Output (JSON Schema)" +msgstr "構造化出力(JSON スキーマ)を使用" + +#. module: openai_connector +#: model:ir.model.fields,field_description:openai_connector.field_openai_vision_session__web_search +msgid "Use Web Search" +msgstr "ウェブ検索を使用" + +#. module: openai_connector +#: model_terms:ir.ui.view,arch_db:openai_connector.view_openai_vision_session_form +msgid "Vision Session" +msgstr "Visionセッション" + +#. module: openai_connector +#: model:ir.actions.act_window,name:openai_connector.action_openai_vision_sessions +msgid "Vision Sessions" +msgstr "Visionセッション" diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py new file mode 100644 index 00000000..2cfb4831 --- /dev/null +++ b/openai_connector/models/__init__.py @@ -0,0 +1 @@ +from . import openai_vision_session diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py new file mode 100644 index 00000000..42ba6c34 --- /dev/null +++ b/openai_connector/models/openai_vision_session.py @@ -0,0 +1,130 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import json +import logging + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + + +class OpenaiVisionSession(models.Model): + _name = "openai.vision.session" + _inherit = ["api.call.mixin"] + _description = "OpenAI Vision Session" + + name = fields.Char(required=True) + reference_code = fields.Selection( + selection=[], + required=True, + help="Defines the specific purpose of the OpenAI Vision session.", + ) + llm_model = fields.Selection( + [("gpt-4o", "GPT-4o")], + string="Model", + default="gpt-4o", + required=True, + ) + temperature = fields.Float( + default=0.7, + help="Sets response randomness (0–2)." + " Higher is more creative, lower is more focused.", + ) + instruction = fields.Text( + help="System-level instruction for the assistant." + " Example: You are a helpful assistant that answers in Japanese." + ) + previous_response_id = fields.Char( + string="Previous Response ID", + help="ID of previous response to continue chat (expires in 30 days).", + ) + store_response = fields.Boolean( + help="If enabled, the API response is stored and the response ID is saved for" + " session continuity, allowing the conversation context to be preserved across" + " multiple calls." + ) + web_search = fields.Boolean( + string="Use Web Search", + help="If enabled, the model can use web search to find information beyond " + "its training data.", + ) + response_format_enabled = fields.Boolean( + string="Use Structured Output (JSON Schema)", + help="If enabled, the response will be structured according to " + "the Response Format Schema (JSON)", + ) + response_format_schema = fields.Text( + string="Response Format Schema (JSON)", + default=lambda self: json.dumps( + { + "type": "object", + "properties": {"message": {"type": "string"}}, + "required": ["message"], + "additionalProperties": False, + "strict": True, + }, + indent=2, + ), + ) + + @api.constrains("response_format_schema") + def _constrains_response_format_schema(self): + for record in self: + try: + json.loads(record.response_format_schema or "{}") + except json.JSONDecodeError as e: + raise UserError(_("The JSON schema is invalid:\n{}").format(e)) from e + + def _get_request_payload(self, input_datas): + self.ensure_one() + try: + json.loads(input_datas or "[]") + except json.JSONDecodeError as e: + raise UserError(_("The inputs are invalid: %s") % e) from e + request_payload = { + "model": self.llm_model, + "instructions": self.instruction or "", + "input": json.loads(input_datas) or [], + "temperature": self.temperature, + "store": self.store_response, + "tools": [{"type": "web_search_preview"}] if self.web_search else [], + } + if self.response_format_enabled: + schema = json.loads(self.response_format_schema or "{}") + request_payload["text"] = { + "format": { + "type": "json_schema", + "name": "json_schema", + "schema": schema, + } + } + if self.previous_response_id: + request_payload["previous_response_id"] = self.previous_response_id + return request_payload + + def call_openai(self, input_datas): + self.ensure_one() + payload = self._get_request_payload(input_datas) + response = self.make_api_call( + "openai", + endpoint="responses", + json=payload, + http_method="post", + ) + try: + response_json = response.json() + except json.decoder.JSONDecodeError as e: + _logger.exception("Malformed JSON from OpenAI") + raise UserError(_("OpenAI returned invalid JSON.")) from e + refusal = response_json.get("refusal_reason") + if refusal: + raise UserError(_("OpenAI refused the request:\n{}").format(refusal)) + if self.store_response: + self.previous_response_id = response_json.get("id") + output = response_json.get("output", []) + contents = [] + for item in output: + contents.extend(item.get("content", [])) + return contents[0].get("text") diff --git a/openai_connector/readme/CONFIGURE.md b/openai_connector/readme/CONFIGURE.md new file mode 100644 index 00000000..e2b375cf --- /dev/null +++ b/openai_connector/readme/CONFIGURE.md @@ -0,0 +1,11 @@ +For the OpenAI API configuration: +- Only the api_key field is required to be set. +- The base_url, token_type, and header_api_key_string are prefilled by this module. + +Basically, the OpenAI session should be added by a specific module that depends on this +one. +Adjust the session attributes: +- Go to Settings → Technical →OpenAI Vision Sessions. + +For additional options and usage details, refer to the official documentation: + diff --git a/openai_connector/readme/DESCRIPTION.md b/openai_connector/readme/DESCRIPTION.md new file mode 100644 index 00000000..caf5143d --- /dev/null +++ b/openai_connector/readme/DESCRIPTION.md @@ -0,0 +1,6 @@ +This module defines openai.vision.session, which enables programmatic access to OpenAI. +It supports multimodal input (images and text), optional web search, and structured +output via JSON Schema. + +The module is designed as a backend service and is intended to be invoked by other +modules. diff --git a/openai_connector/readme/USAGE.md b/openai_connector/readme/USAGE.md new file mode 100644 index 00000000..5e7b279a --- /dev/null +++ b/openai_connector/readme/USAGE.md @@ -0,0 +1,20 @@ +This module is intended to be used programmatically by other Odoo modules. You create +and configure `openai.vision.session` records and call the method `call_openai()` from +your own business logic. + +For example, a product name generator module may call this module's session as follows: + +```python +session = self.env["openai.vision.session"].search( + [("reference_code", "=", "product_name_generation")], + limit=1, +) +input_image = f"{base_url}/web/image/{self._name}/{self.id}/image_1920" +input_data = json.dumps([ + {"role": "user", "content": [{"type": "input_image", "image_url": image_url}]} +]) +response = session.call_openai(input_data) +``` + +It is advised to delegate execution to background job queues (e.g., +queue.job) for asynchronous and fault-tolerant processing. diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv new file mode 100644 index 00000000..f97b6295 --- /dev/null +++ b/openai_connector/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_openai_vision_session_user,access_openai_vision_session_user,model_openai_vision_session,base.group_user,1,0,0,0 +access_openai_vision_session_manager,access_openai_vision_session_manager,model_openai_vision_session,base.group_system,1,1,1,1 diff --git a/openai_connector/static/description/index.html b/openai_connector/static/description/index.html new file mode 100644 index 00000000..18d80901 --- /dev/null +++ b/openai_connector/static/description/index.html @@ -0,0 +1,451 @@ + + + + + +OpenAI Connector + + + +
+

OpenAI Connector

+ + +

Beta License: AGPL-3 qrtl/rmm-custom

+

This module defines openai.vision.session, which enables programmatic +access to OpenAI. It supports multimodal input (images and text), +optional web search, and structured output via JSON Schema.

+

The module is designed as a backend service and is intended to be +invoked by other modules.

+

Table of contents

+ +
+

Configuration

+

For the OpenAI API configuration:

+
    +
  • Only the api_key field is required to be set.
  • +
  • The base_url, token_type, and header_api_key_string are prefilled by +this module.
  • +
+

Basically, the OpenAI session should be added by a specific module that +depends on this one. Adjust the session attributes:

+
    +
  • Go to Settings → Technical →OpenAI Vision Sessions.
  • +
+

For additional options and usage details, refer to the official +documentation: https://platform.openai.com/docs

+
+
+

Usage

+

This module is intended to be used programmatically by other Odoo +modules. You create and configure openai.vision.session records and +call the method call_openai() from your own business logic.

+

For example, a product name generator module may call this module’s +session as follows:

+
+session = self.env["openai.vision.session"].search(
+    [("reference_code", "=", "product_name_generation")],
+    limit=1,
+)
+input_image = f"{base_url}/web/image/{self._name}/{self.id}/image_1920"
+input_data = json.dumps([
+    {"role": "user", "content": [{"type": "input_image", "image_url": image_url}]}
+])
+response = session.call_openai(input_data)
+
+

It is advised to delegate execution to background job queues (e.g., +queue.job) for asynchronous and fault-tolerant processing.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quartile
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/rmm-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/openai_connector/views/openai_vision_session_views.xml b/openai_connector/views/openai_vision_session_views.xml new file mode 100644 index 00000000..de49cf28 --- /dev/null +++ b/openai_connector/views/openai_vision_session_views.xml @@ -0,0 +1,63 @@ + + + + openai.vision.session.tree + openai.vision.session + + + + + + + + + openai.vision.session.form + openai.vision.session + +
+ + + + + + + + + + + + + + + + + + + + +
+
+
+ + Vision Sessions + openai.vision.session + tree,form + + + +
diff --git a/product_name_generator/README.rst b/product_name_generator/README.rst new file mode 100644 index 00000000..2cd32c6e --- /dev/null +++ b/product_name_generator/README.rst @@ -0,0 +1,84 @@ +====================== +Product Name Generator +====================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3b3bfcff6fa979b65d0bce27a9e055008e8bafbf2e6feeaed06b1dc14e164c09 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-qrtl%2Frmm--custom-lightgray.png?logo=github + :target: https://github.com/qrtl/rmm-custom/tree/15.0/product_name_generator + :alt: qrtl/rmm-custom + +|badge1| |badge2| |badge3| + +This module adds the openai_generated_name field to the product template +to store product names generated from product images using the OpenAI +API. + +This module creates an OpenAI Vision session for Product Name Generator. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +To customize the prompt sent to OpenAI along with the product image: + +- Go to Settings → Technical → OpenAI Vision Sessions. +- Select the record named "Product Name Generator". +- Modify the Instruction field as needed. + +For other parameters, please refer to the OpenAI official documentation. + +Usage +===== + +To generate product names using the OpenAI API: openai_generated_name + +- Go to product template list view. +- Select the products for which you want to generate names. +- Click "Generate Product Name Using OpenAI" from the Action dropdown. +- OpenAI Generated Name will be updated once the session is complete. + (Note: You may need to refresh the page to see the updated names.) + +If the request with OpenAI fails, you can check the Queue Job records to +investigate. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Quatile + +Maintainers +----------- + +This module is part of the `qrtl/rmm-custom `_ project on GitHub. + +You are welcome to contribute. diff --git a/product_name_generator/__init__.py b/product_name_generator/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/product_name_generator/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_name_generator/__manifest__.py b/product_name_generator/__manifest__.py new file mode 100644 index 00000000..a9039cf1 --- /dev/null +++ b/product_name_generator/__manifest__.py @@ -0,0 +1,17 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +{ + "name": "Product Name Generator", + "version": "15.0.1.0.0", + "author": "Quatile", + "license": "AGPL-3", + "website": "https://www.quartile.co", + "depends": ["website_sale", "openai_connector", "queue_job"], + "data": [ + "data/ir_config_parameter.xml", + "data/openai_vision_session.xml", + "views/product_template_views.xml", + ], + "installable": True, +} diff --git a/product_name_generator/data/ir_config_parameter.xml b/product_name_generator/data/ir_config_parameter.xml new file mode 100644 index 00000000..0d6f6e5b --- /dev/null +++ b/product_name_generator/data/ir_config_parameter.xml @@ -0,0 +1,6 @@ + + + product_name_generator.openai_retry_message_substring + Timeout while downloading + + diff --git a/product_name_generator/data/openai_vision_session.xml b/product_name_generator/data/openai_vision_session.xml new file mode 100644 index 00000000..e0885291 --- /dev/null +++ b/product_name_generator/data/openai_vision_session.xml @@ -0,0 +1,28 @@ + + + Product Name Generator + product_name_generation + gpt-4o + 1 + Please create product name. + 1 + + + + 1 + 0 + + diff --git a/product_name_generator/i18n/ja.po b/product_name_generator/i18n/ja.po new file mode 100644 index 00000000..aec4891c --- /dev/null +++ b/product_name_generator/i18n/ja.po @@ -0,0 +1,69 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * product_name_generator +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 15.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-07-30 05:27+0000\n" +"PO-Revision-Date: 2025-07-30 05:27+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: product_name_generator +#: model:ir.model.fields,help:product_name_generator.field_openai_vision_session__session_purpose +msgid "Defines the specific purpose of the OpenAI Vision session." +msgstr "" + +#. module: product_name_generator +#: model:ir.actions.server,name:product_name_generator.action_product_template_name_generator +msgid "Generate Product Name Using OpenAI" +msgstr "OpenAIを使ってプロダクト名を生成" + +#. module: product_name_generator +#: model_terms:ir.ui.view,arch_db:product_name_generator.product_template_form_view +msgid "OpenAI Generated Name" +msgstr "OpenAI生成名" + +#. module: product_name_generator +#: model:ir.model,name:product_name_generator.model_openai_vision_session +msgid "OpenAI Vision Session" +msgstr "" + +#. module: product_name_generator +#: code:addons/product_name_generator/models/product_template.py:0 +#, python-format +msgid "OpenAI Vision Session not found." +msgstr "OpenAI Vision Sessionが見つかりません。" + +#. module: product_name_generator +#: model:ir.model.fields,field_description:product_name_generator.field_product_product__openai_generated_name +#: model:ir.model.fields,field_description:product_name_generator.field_product_template__openai_generated_name +msgid "Openai Generated Name" +msgstr "OpenAI生成名" + +#. module: product_name_generator +#: code:addons/product_name_generator/models/product_template.py:0 +#, python-format +msgid "Please upload the image first." +msgstr "先にプロダクトの画像をアップロードしてください。" + +#. module: product_name_generator +#: model:ir.model.fields.selection,name:product_name_generator.selection__openai_vision_session__session_purpose__product_name_generation +msgid "Product Name Generation" +msgstr "プロダクト名生成" + +#. module: product_name_generator +#: model:ir.model,name:product_name_generator.model_product_template +msgid "Product Template" +msgstr "プロダクトテンプレート" + +#. module: product_name_generator +#: model:ir.model.fields,field_description:product_name_generator.field_openai_vision_session__session_purpose +msgid "Session Purpose" +msgstr "" diff --git a/product_name_generator/models/__init__.py b/product_name_generator/models/__init__.py new file mode 100644 index 00000000..f75385ee --- /dev/null +++ b/product_name_generator/models/__init__.py @@ -0,0 +1,2 @@ +from . import openai_vision_session +from . import product_template diff --git a/product_name_generator/models/openai_vision_session.py b/product_name_generator/models/openai_vision_session.py new file mode 100644 index 00000000..7ccc5fd0 --- /dev/null +++ b/product_name_generator/models/openai_vision_session.py @@ -0,0 +1,13 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class OpenAIVisionSession(models.Model): + _inherit = "openai.vision.session" + + reference_code = fields.Selection( + selection_add=[("product_name_generation", "Product Name Generation")], + ondelete={"product_name_generation": "cascade"}, + ) diff --git a/product_name_generator/models/product_template.py b/product_name_generator/models/product_template.py new file mode 100644 index 00000000..1d2693af --- /dev/null +++ b/product_name_generator/models/product_template.py @@ -0,0 +1,69 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import json + +from odoo import _, fields, models +from odoo.exceptions import UserError + +from odoo.addons.queue_job.exception import RetryableJobError + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + openai_generated_name = fields.Char() + + def generate_name(self, session, base_url, is_published): + self.ensure_one() + input_image = f"{base_url}/web/image/{self._name}/{self.id}/image_1920" + input_datas = json.dumps( + [ + { + "role": "user", + "content": [{"type": "input_image", "image_url": input_image}], + } + ] + ) + try: + response_json_str = session.call_openai(input_datas) + except UserError as e: + # HTTP 400 Error about "Timeout while downloading" occurs when calling the + # OpenAI API to fetch an image. This usually happens if the image takes too + # long to load or the download is interrupted. In such cases, we treat it as + # a temporary issue and retry instead of failing permanently. + # "retry_msg_substr " is used here because the error message text can be + # changed from the GUI when the wording changes + cause = e.__cause__ + retry_msg_substr = ( + self.env["ir.config_parameter"] + .sudo() + .get_param("product_name_generator.openai_retry_message_substring") + ) + error_detail = getattr(getattr(cause, "response", None), "text", "")[:500] + if retry_msg_substr in error_detail: + raise RetryableJobError( + _("Retry due to OpenAI temporary error"), seconds=60 + ) from None + raise + response_json = json.loads(response_json_str) + product_name = response_json.get("product_name") + self.openai_generated_name = product_name + self.is_published = is_published + + def action_generate_product_name(self): + session = self.env["openai.vision.session"].search( + [("reference_code", "=", "product_name_generation")], + limit=1, + ) + if not session: + raise UserError(_("OpenAI Vision Session not found.")) + base_url = self.env["ir.config_parameter"].sudo().get_param("web.base.url") + for rec in self: + if not rec.image_1920: + raise UserError(_("Please upload the image first.")) + is_published = rec.is_published + if not is_published: + # Need to be published to be accessible to the public. + rec.is_published = True + rec.with_delay().generate_name(session, base_url, is_published) diff --git a/product_name_generator/readme/CONFIGURE.md b/product_name_generator/readme/CONFIGURE.md new file mode 100644 index 00000000..a40b4e7c --- /dev/null +++ b/product_name_generator/readme/CONFIGURE.md @@ -0,0 +1,7 @@ +To customize the prompt sent to OpenAI along with the product image: + +- Go to Settings → Technical → OpenAI Vision Sessions. +- Select the record named "Product Name Generator". +- Modify the Instruction field as needed. + +For other parameters, please refer to the OpenAI official documentation. diff --git a/product_name_generator/readme/DESCRIPTION.md b/product_name_generator/readme/DESCRIPTION.md new file mode 100644 index 00000000..f042a7b9 --- /dev/null +++ b/product_name_generator/readme/DESCRIPTION.md @@ -0,0 +1,4 @@ +This module adds the openai_generated_name field to the product template to store +product names generated from product images using the OpenAI API. + +This module creates an OpenAI Vision session for Product Name Generator. diff --git a/product_name_generator/readme/USAGE.md b/product_name_generator/readme/USAGE.md new file mode 100644 index 00000000..17a1cf99 --- /dev/null +++ b/product_name_generator/readme/USAGE.md @@ -0,0 +1,10 @@ +To generate product names using the OpenAI API: +openai_generated_name + +- Go to product template list view. +- Select the products for which you want to generate names. +- Click "Generate Product Name Using OpenAI" from the Action dropdown. +- OpenAI Generated Name will be updated once the session is complete. + (Note: You may need to refresh the page to see the updated names.) + +If the request with OpenAI fails, you can check the Queue Job records to investigate. diff --git a/product_name_generator/static/description/index.html b/product_name_generator/static/description/index.html new file mode 100644 index 00000000..a1777646 --- /dev/null +++ b/product_name_generator/static/description/index.html @@ -0,0 +1,436 @@ + + + + + +Product Name Generator + + + +
+

Product Name Generator

+ + +

Beta License: AGPL-3 qrtl/rmm-custom

+

This module adds the openai_generated_name field to the product template +to store product names generated from product images using the OpenAI +API.

+

This module creates an OpenAI Vision session for Product Name Generator.

+

Table of contents

+ +
+

Configuration

+

To customize the prompt sent to OpenAI along with the product image:

+
    +
  • Go to Settings → Technical → OpenAI Vision Sessions.
  • +
  • Select the record named “Product Name Generator”.
  • +
  • Modify the Instruction field as needed.
  • +
+

For other parameters, please refer to the OpenAI official documentation.

+
+
+

Usage

+

To generate product names using the OpenAI API: openai_generated_name

+
    +
  • Go to product template list view.
  • +
  • Select the products for which you want to generate names.
  • +
  • Click “Generate Product Name Using OpenAI” from the Action dropdown.
  • +
  • OpenAI Generated Name will be updated once the session is complete. +(Note: You may need to refresh the page to see the updated names.)
  • +
+

If the request with OpenAI fails, you can check the Queue Job records to +investigate.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub Issues. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Quatile
  • +
+
+
+

Maintainers

+

This module is part of the qrtl/rmm-custom project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/product_name_generator/views/product_template_views.xml b/product_name_generator/views/product_template_views.xml new file mode 100644 index 00000000..8c393e24 --- /dev/null +++ b/product_name_generator/views/product_template_views.xml @@ -0,0 +1,35 @@ + + + product.template.product.tree + product.template + + + + + + + + + product.template.common.form + product.template + + + +
+
+
+
+
+ + Generate Product Name Using OpenAI + + + code + action = records.action_generate_product_name() + +
diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 9cd16292..00000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -# generated from manifests external_dependencies diff --git a/setup/openai_connector/odoo/addons/openai_connector b/setup/openai_connector/odoo/addons/openai_connector new file mode 120000 index 00000000..da2986d8 --- /dev/null +++ b/setup/openai_connector/odoo/addons/openai_connector @@ -0,0 +1 @@ +../../../../openai_connector \ No newline at end of file diff --git a/setup/openai_connector/setup.py b/setup/openai_connector/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/openai_connector/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/setup/product_name_generator/odoo/addons/product_name_generator b/setup/product_name_generator/odoo/addons/product_name_generator new file mode 120000 index 00000000..46497df1 --- /dev/null +++ b/setup/product_name_generator/odoo/addons/product_name_generator @@ -0,0 +1 @@ +../../../../product_name_generator \ No newline at end of file diff --git a/setup/product_name_generator/setup.py b/setup/product_name_generator/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/product_name_generator/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) diff --git a/test-requirements.txt b/test-requirements.txt index de9b87ca..274dae5a 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -4,3 +4,4 @@ git+https://github.com/qrtl/rmm-oca@15.0#subdirectory=setup/sale_commission git+https://github.com/qrtl/rmm-oca@15.0#subdirectory=setup/base_custom_filter git+https://github.com/qrtl/rmm-oca@15.0#subdirectory=setup/base_group_backend git+https://github.com/qrtl/rmm-oca@15.0#subdirectory=setup/l10n_jp_partner_zip_address +git+https://github.com/qrtl/rmm-oca@15.0#subdirectory=setup/base_api_connection