From d3e2078fb4e61a736bcbc97b2543eafb07f55fb3 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 9 Jun 2025 08:21:53 +0000 Subject: [PATCH 01/61] [5192][ADD] openai_connector --- chatgpt_integration/__init__.py | 2 + chatgpt_integration/__manifest__.py | 20 +++ chatgpt_integration/models/__init__.py | 6 + chatgpt_integration/models/ir_attachment.py | 51 +++++++ .../models/openai_vision_message.py | 40 ++++++ .../models/openai_vision_session.py | 131 ++++++++++++++++++ chatgpt_integration/models/res_company.py | 6 + .../models/res_config_settings.py | 11 ++ .../security/ir.model.access.csv | 3 + .../views/openai_vision_menu.xml | 10 ++ .../views/openai_vision_message.xml | 52 +++++++ .../views/openai_vision_session.xml | 81 +++++++++++ .../views/res_config_settings_views.xml | 19 +++ openai_connector/__init__.py | 2 + openai_connector/__manifest__.py | 16 +++ openai_connector/models/__init__.py | 6 + openai_connector/models/ir_attachment.py | 51 +++++++ .../models/openai_vision_message.py | 40 ++++++ .../models/openai_vision_session.py | 131 ++++++++++++++++++ openai_connector/models/res_company.py | 6 + .../models/res_config_settings.py | 11 ++ openai_connector/security/ir.model.access.csv | 3 + openai_connector/views/openai_vision_menu.xml | 10 ++ .../views/openai_vision_message.xml | 52 +++++++ .../views/openai_vision_session.xml | 81 +++++++++++ .../views/res_config_settings_views.xml | 19 +++ 26 files changed, 860 insertions(+) create mode 100644 chatgpt_integration/__init__.py create mode 100644 chatgpt_integration/__manifest__.py create mode 100644 chatgpt_integration/models/__init__.py create mode 100644 chatgpt_integration/models/ir_attachment.py create mode 100644 chatgpt_integration/models/openai_vision_message.py create mode 100644 chatgpt_integration/models/openai_vision_session.py create mode 100644 chatgpt_integration/models/res_company.py create mode 100644 chatgpt_integration/models/res_config_settings.py create mode 100644 chatgpt_integration/security/ir.model.access.csv create mode 100644 chatgpt_integration/views/openai_vision_menu.xml create mode 100644 chatgpt_integration/views/openai_vision_message.xml create mode 100644 chatgpt_integration/views/openai_vision_session.xml create mode 100644 chatgpt_integration/views/res_config_settings_views.xml create mode 100644 openai_connector/__init__.py create mode 100644 openai_connector/__manifest__.py create mode 100644 openai_connector/models/__init__.py create mode 100644 openai_connector/models/ir_attachment.py create mode 100644 openai_connector/models/openai_vision_message.py create mode 100644 openai_connector/models/openai_vision_session.py create mode 100644 openai_connector/models/res_company.py create mode 100644 openai_connector/models/res_config_settings.py create mode 100644 openai_connector/security/ir.model.access.csv create mode 100644 openai_connector/views/openai_vision_menu.xml create mode 100644 openai_connector/views/openai_vision_message.xml create mode 100644 openai_connector/views/openai_vision_session.xml create mode 100644 openai_connector/views/res_config_settings_views.xml diff --git a/chatgpt_integration/__init__.py b/chatgpt_integration/__init__.py new file mode 100644 index 00000000..f8ea970b --- /dev/null +++ b/chatgpt_integration/__init__.py @@ -0,0 +1,2 @@ +# Initialize the module +from . import models diff --git a/chatgpt_integration/__manifest__.py b/chatgpt_integration/__manifest__.py new file mode 100644 index 00000000..bc0c92c1 --- /dev/null +++ b/chatgpt_integration/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'ChatGPT Integration', + 'version': '15.0.1.0.0', + 'summary': 'Integrate ChatGPT with Odoo', + 'description': 'This module allows integration with ChatGPT by storing API keys in res_config and res_company.', + 'author': 'Your Company', + 'website': 'https://www.quartile.co', + 'category': 'Tools', + 'depends': ['base', 'base_setup', 'web'], + 'data': [ + 'security/ir.model.access.csv', + 'views/res_config_settings_views.xml', + 'views/openai_vision_menu.xml', + 'views/openai_vision_message.xml', + 'views/openai_vision_session.xml', + ], + 'installable': True, + 'application': True, + 'auto_install': False, +} diff --git a/chatgpt_integration/models/__init__.py b/chatgpt_integration/models/__init__.py new file mode 100644 index 00000000..b11e9fb3 --- /dev/null +++ b/chatgpt_integration/models/__init__.py @@ -0,0 +1,6 @@ +# Initialize the models +from . import ir_attachment +from . import res_company +from . import res_config_settings +from . import openai_vision_message +from . import openai_vision_session diff --git a/chatgpt_integration/models/ir_attachment.py b/chatgpt_integration/models/ir_attachment.py new file mode 100644 index 00000000..8f7387a3 --- /dev/null +++ b/chatgpt_integration/models/ir_attachment.py @@ -0,0 +1,51 @@ +from odoo import models, api +from odoo.exceptions import UserError +import base64 +import uuid + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + @api.model + def analyze_with_openai_vision(self, attachment_id, prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?"): + attachment = self.browse(attachment_id) + if not attachment or not attachment.datas: + return "指定された添付ファイルが存在しないか、データがありません。" + + try: + # base64エンコードされた画像URLを作成 + image_data = base64.b64decode(attachment.datas) + base64_str = base64.b64encode(image_data).decode("utf-8") + input_image = f"data:{attachment.mimetype};base64,{base64_str}" + + # セッションの作成 + session = self.env['openai.vision.session'].create({ + 'name': f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", + 'model': 'gpt-4o', + 'temperature': 0.7, + }) + + # メッセージをセッションに関連付けて作成 + Message = self.env['openai.vision.message'] + Message.create({ + 'role': 'user', + 'type': 'input_text', + 'content': prompt_text, + 'sequence': 10, + 'input_session_id': session.id, + }) + Message.create({ + 'role': 'user', + 'type': 'input_image', + 'content': input_image, + 'sequence': 10, + 'input_session_id': session.id, + }) + + # OpenAI APIを呼び出し + return_message = session.call_openAI() + + return return_message.content or session.response_refusal_message or "応答がありませんでした。" + + except Exception as e: + return f"エラー: {str(e)}" diff --git a/chatgpt_integration/models/openai_vision_message.py b/chatgpt_integration/models/openai_vision_message.py new file mode 100644 index 00000000..eef39914 --- /dev/null +++ b/chatgpt_integration/models/openai_vision_message.py @@ -0,0 +1,40 @@ +import json +from odoo import models, fields, api +from odoo.exceptions import UserError + +class OpenAIVisionMessage(models.Model): + _name = 'openai.vision.message' + _description = 'OpenAI Vision Message Line' + + role = fields.Selection([ + ("user", "User"), + ("assistant", "Assistant") + ], default='user', required=True) + type = fields.Selection([ + ("input_text", "Text"), + ("input_image", "Image URL") + ], default='input_text', required=True) + input_session_id = fields.Many2one( + 'openai.vision.session', string="Input Session") + response_session_id = fields.Many2one( + 'openai.vision.session', string="Response Session") + content = fields.Text(string="Content") + sequence = fields.Integer(default=10, string="Sequence") + + @api.model + def to_openai_dict(self): + if self.type == "input_text": + return { + "type": "input_text", + "text": self.content.strip() if self.content else "" + } + elif self.type == "input_image": + url = self.content.strip() if self.content else "" + if not url: + raise UserError("Image URL message content is empty") + return { + "type": "input_image", + "image_url": url + } + else: + raise UserError(f"Unsupported message type: {self.type}") diff --git a/chatgpt_integration/models/openai_vision_session.py b/chatgpt_integration/models/openai_vision_session.py new file mode 100644 index 00000000..1dde926f --- /dev/null +++ b/chatgpt_integration/models/openai_vision_session.py @@ -0,0 +1,131 @@ +import json +from odoo import models, fields, api +from odoo.exceptions import UserError +from openai import OpenAI +from collections import defaultdict + +class OpenAISession(models.Model): + _name = 'openai.vision.session' + _description = 'OpenAI Vision Session' + + name = fields.Char(required=True, string="Session Name") + model = fields.Selection([ + ('gpt-4o', 'GPT-4o'), + ], default='gpt-4o', required=True) + temperature = fields.Float(default=0.7, string="Temperature") + input_message_ids = fields.One2many( + 'openai.vision.message', 'input_session_id', string="Input Messages") + response_message_ids = fields.One2many( + 'openai.vision.message', 'response_session_id', string="Response Messages") + previous_response_id = fields.Char(string="Previous Response ID") + store_response = fields.Boolean(string="Store Response", default=True) + web_search = fields.Boolean(string="Use Web Search", default=False) + response_refusal_message = fields.Text( + string="Refusal Reason", readonly=True) + response_format_enabled = fields.Boolean( + string="Use Structured Output (JSON Schema)", default=False) + response_format_schema = fields.Text( + string="Response Format Schema (JSON)", + default=json.dumps({ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"], + "additionalProperties": False, + "strict": True, + }, + indent=2)) + + @api.onchange('response_format_schema') + def _onchange_response_format_schema(self): + for record in self: + if record.response_format_enabled: + try: + json.loads(record.response_format_schema or "{}") + except json.JSONDecodeError as e: + raise UserError("The JSON schema is invalid:\n%s" % str(e)) + + @api.model + def call_openAI(self): + api_key = self.env.company.openai_api_key + if not api_key: + raise UserError("OpenAI API key is not configured") + + client = OpenAI(api_key=api_key) + + for record in self: + if record.model != 'gpt-4o': + raise UserError( + "Responses API(構造化出力)は現在 'gpt-4o' モデルでのみサポートされています。") + grouped = defaultdict(list) + for msg in record.input_message_ids: + key = (msg.sequence, msg.role) + grouped[key].append(msg) + instructions = [] + inputs = [] + sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) + for seq, role in sorted_keys: + components = grouped[(seq, role)] + roles = {c.role for c in components} + if len(roles) != 1: + raise UserError(f"Multiple roles found at sequence {seq}: {roles}") + contents = [c.to_openai_dict() for c in components if c.content and c.content.strip()] + if not contents: + continue + inputs.append({ + "role": role, + "content": contents + }) + + text_format = None + if record.response_format_enabled: + try: + schema = json.loads(record.response_format_schema or '{}') + text_format = { + "type": "json_schema", + "name": "json_schema", + "schema": schema + } + except json.JSONDecodeError as e: + raise UserError("Invalid JSON schema:\n%s" % str(e)) + + try: + params = { + 'model': record.model, + 'instructions': "\n".join(instructions).strip(), + 'input': inputs, + 'temperature': record.temperature, + 'store': record.store_response, + 'text': {"format": text_format}, + "tools": [{"type": "web_search_preview"}] if record.web_search else [], + } + + if record.previous_response_id: + params['previous_response_id'] = record.previous_response_id + + response = client.responses.create(**params) + if record.store_response: + record.previous_response_id = getattr(response, 'id', False) + + refusal = getattr(response, 'refusal_reason', None) + if refusal: + record.response_refusal_message = refusal + continue + + record.response_refusal_message = False + output = getattr(response, 'output_text', '') or '' + result = output.strip() + + next_seq = max(record.input_message_ids.mapped('sequence') or [0]) + 10 + return_message = record.response_message_ids.create({ + 'response_session_id': record.id, + 'role': 'assistant', + 'type': 'input_text', + 'content': result, + 'sequence': next_seq, + }) + return return_message + + except Exception as e: + raise UserError(f"OpenAI error: {e}") diff --git a/chatgpt_integration/models/res_company.py b/chatgpt_integration/models/res_company.py new file mode 100644 index 00000000..00091ecc --- /dev/null +++ b/chatgpt_integration/models/res_company.py @@ -0,0 +1,6 @@ +from odoo import models, fields + +class ResCompany(models.Model): + _inherit = 'res.company' + + openai_api_key = fields.Char(string='OpenAI API Key') diff --git a/chatgpt_integration/models/res_config_settings.py b/chatgpt_integration/models/res_config_settings.py new file mode 100644 index 00000000..2e7dad0a --- /dev/null +++ b/chatgpt_integration/models/res_config_settings.py @@ -0,0 +1,11 @@ +from odoo import models, fields, api + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + openai_api_key = fields.Char( + string='OpenAI API Key', + related='company_id.openai_api_key', + readonly=False, + password=True + ) diff --git a/chatgpt_integration/security/ir.model.access.csv b/chatgpt_integration/security/ir.model.access.csv new file mode 100644 index 00000000..8830a57c --- /dev/null +++ b/chatgpt_integration/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink +access_openai_vision_message,access_openai_vision_message,model_openai_vision_message,,1,1,1,1 +access_openai_vision_session,access_openai_vision_session,model_openai_vision_session,,1,1,1,1 diff --git a/chatgpt_integration/views/openai_vision_menu.xml b/chatgpt_integration/views/openai_vision_menu.xml new file mode 100644 index 00000000..a91bd473 --- /dev/null +++ b/chatgpt_integration/views/openai_vision_menu.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/chatgpt_integration/views/openai_vision_message.xml b/chatgpt_integration/views/openai_vision_message.xml new file mode 100644 index 00000000..fdecef56 --- /dev/null +++ b/chatgpt_integration/views/openai_vision_message.xml @@ -0,0 +1,52 @@ + + + + openai.vision.message.tree + openai.vision.message + + + + + + + + + + + + openai.vision.message.form + openai.vision.message + +
+ + + + + + + + + + + +
+
+
+ + + Vision Messages + openai.vision.message + tree,form + + + +
diff --git a/chatgpt_integration/views/openai_vision_session.xml b/chatgpt_integration/views/openai_vision_session.xml new file mode 100644 index 00000000..b5e820dd --- /dev/null +++ b/chatgpt_integration/views/openai_vision_session.xml @@ -0,0 +1,81 @@ + + + + openai.vision.session.tree + openai.vision.session + + + + + + + + + + + openai.vision.session.form + openai.vision.session + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Vision Sessions + openai.vision.session + tree,form + + + +
diff --git a/chatgpt_integration/views/res_config_settings_views.xml b/chatgpt_integration/views/res_config_settings_views.xml new file mode 100644 index 00000000..2f4803f5 --- /dev/null +++ b/chatgpt_integration/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ + + + + res.config.settings.view.form.inherit.chatgpt + res.config.settings + + + + +
+
+
+
+
+
+
+
\ No newline at end of file diff --git a/openai_connector/__init__.py b/openai_connector/__init__.py new file mode 100644 index 00000000..f8ea970b --- /dev/null +++ b/openai_connector/__init__.py @@ -0,0 +1,2 @@ +# Initialize the module +from . import models diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py new file mode 100644 index 00000000..5500dfd5 --- /dev/null +++ b/openai_connector/__manifest__.py @@ -0,0 +1,16 @@ +{ + 'name': 'Openai Connector', + 'version': '15.0.1.0.0', + 'author': 'Quatile', + "license": "AGPL-3", + 'website': 'https://www.quartile.co', + 'depends': ['base_setup', 'web'], + 'data': [ + 'security/ir.model.access.csv', + 'views/res_config_settings_views.xml', + 'views/openai_vision_menu.xml', + 'views/openai_vision_message.xml', + 'views/openai_vision_session.xml', + ], + 'installable': True, +} diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py new file mode 100644 index 00000000..b11e9fb3 --- /dev/null +++ b/openai_connector/models/__init__.py @@ -0,0 +1,6 @@ +# Initialize the models +from . import ir_attachment +from . import res_company +from . import res_config_settings +from . import openai_vision_message +from . import openai_vision_session diff --git a/openai_connector/models/ir_attachment.py b/openai_connector/models/ir_attachment.py new file mode 100644 index 00000000..8f7387a3 --- /dev/null +++ b/openai_connector/models/ir_attachment.py @@ -0,0 +1,51 @@ +from odoo import models, api +from odoo.exceptions import UserError +import base64 +import uuid + +class IrAttachment(models.Model): + _inherit = 'ir.attachment' + + @api.model + def analyze_with_openai_vision(self, attachment_id, prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?"): + attachment = self.browse(attachment_id) + if not attachment or not attachment.datas: + return "指定された添付ファイルが存在しないか、データがありません。" + + try: + # base64エンコードされた画像URLを作成 + image_data = base64.b64decode(attachment.datas) + base64_str = base64.b64encode(image_data).decode("utf-8") + input_image = f"data:{attachment.mimetype};base64,{base64_str}" + + # セッションの作成 + session = self.env['openai.vision.session'].create({ + 'name': f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", + 'model': 'gpt-4o', + 'temperature': 0.7, + }) + + # メッセージをセッションに関連付けて作成 + Message = self.env['openai.vision.message'] + Message.create({ + 'role': 'user', + 'type': 'input_text', + 'content': prompt_text, + 'sequence': 10, + 'input_session_id': session.id, + }) + Message.create({ + 'role': 'user', + 'type': 'input_image', + 'content': input_image, + 'sequence': 10, + 'input_session_id': session.id, + }) + + # OpenAI APIを呼び出し + return_message = session.call_openAI() + + return return_message.content or session.response_refusal_message or "応答がありませんでした。" + + except Exception as e: + return f"エラー: {str(e)}" diff --git a/openai_connector/models/openai_vision_message.py b/openai_connector/models/openai_vision_message.py new file mode 100644 index 00000000..eef39914 --- /dev/null +++ b/openai_connector/models/openai_vision_message.py @@ -0,0 +1,40 @@ +import json +from odoo import models, fields, api +from odoo.exceptions import UserError + +class OpenAIVisionMessage(models.Model): + _name = 'openai.vision.message' + _description = 'OpenAI Vision Message Line' + + role = fields.Selection([ + ("user", "User"), + ("assistant", "Assistant") + ], default='user', required=True) + type = fields.Selection([ + ("input_text", "Text"), + ("input_image", "Image URL") + ], default='input_text', required=True) + input_session_id = fields.Many2one( + 'openai.vision.session', string="Input Session") + response_session_id = fields.Many2one( + 'openai.vision.session', string="Response Session") + content = fields.Text(string="Content") + sequence = fields.Integer(default=10, string="Sequence") + + @api.model + def to_openai_dict(self): + if self.type == "input_text": + return { + "type": "input_text", + "text": self.content.strip() if self.content else "" + } + elif self.type == "input_image": + url = self.content.strip() if self.content else "" + if not url: + raise UserError("Image URL message content is empty") + return { + "type": "input_image", + "image_url": url + } + else: + raise UserError(f"Unsupported message type: {self.type}") diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py new file mode 100644 index 00000000..1dde926f --- /dev/null +++ b/openai_connector/models/openai_vision_session.py @@ -0,0 +1,131 @@ +import json +from odoo import models, fields, api +from odoo.exceptions import UserError +from openai import OpenAI +from collections import defaultdict + +class OpenAISession(models.Model): + _name = 'openai.vision.session' + _description = 'OpenAI Vision Session' + + name = fields.Char(required=True, string="Session Name") + model = fields.Selection([ + ('gpt-4o', 'GPT-4o'), + ], default='gpt-4o', required=True) + temperature = fields.Float(default=0.7, string="Temperature") + input_message_ids = fields.One2many( + 'openai.vision.message', 'input_session_id', string="Input Messages") + response_message_ids = fields.One2many( + 'openai.vision.message', 'response_session_id', string="Response Messages") + previous_response_id = fields.Char(string="Previous Response ID") + store_response = fields.Boolean(string="Store Response", default=True) + web_search = fields.Boolean(string="Use Web Search", default=False) + response_refusal_message = fields.Text( + string="Refusal Reason", readonly=True) + response_format_enabled = fields.Boolean( + string="Use Structured Output (JSON Schema)", default=False) + response_format_schema = fields.Text( + string="Response Format Schema (JSON)", + default=json.dumps({ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"], + "additionalProperties": False, + "strict": True, + }, + indent=2)) + + @api.onchange('response_format_schema') + def _onchange_response_format_schema(self): + for record in self: + if record.response_format_enabled: + try: + json.loads(record.response_format_schema or "{}") + except json.JSONDecodeError as e: + raise UserError("The JSON schema is invalid:\n%s" % str(e)) + + @api.model + def call_openAI(self): + api_key = self.env.company.openai_api_key + if not api_key: + raise UserError("OpenAI API key is not configured") + + client = OpenAI(api_key=api_key) + + for record in self: + if record.model != 'gpt-4o': + raise UserError( + "Responses API(構造化出力)は現在 'gpt-4o' モデルでのみサポートされています。") + grouped = defaultdict(list) + for msg in record.input_message_ids: + key = (msg.sequence, msg.role) + grouped[key].append(msg) + instructions = [] + inputs = [] + sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) + for seq, role in sorted_keys: + components = grouped[(seq, role)] + roles = {c.role for c in components} + if len(roles) != 1: + raise UserError(f"Multiple roles found at sequence {seq}: {roles}") + contents = [c.to_openai_dict() for c in components if c.content and c.content.strip()] + if not contents: + continue + inputs.append({ + "role": role, + "content": contents + }) + + text_format = None + if record.response_format_enabled: + try: + schema = json.loads(record.response_format_schema or '{}') + text_format = { + "type": "json_schema", + "name": "json_schema", + "schema": schema + } + except json.JSONDecodeError as e: + raise UserError("Invalid JSON schema:\n%s" % str(e)) + + try: + params = { + 'model': record.model, + 'instructions': "\n".join(instructions).strip(), + 'input': inputs, + 'temperature': record.temperature, + 'store': record.store_response, + 'text': {"format": text_format}, + "tools": [{"type": "web_search_preview"}] if record.web_search else [], + } + + if record.previous_response_id: + params['previous_response_id'] = record.previous_response_id + + response = client.responses.create(**params) + if record.store_response: + record.previous_response_id = getattr(response, 'id', False) + + refusal = getattr(response, 'refusal_reason', None) + if refusal: + record.response_refusal_message = refusal + continue + + record.response_refusal_message = False + output = getattr(response, 'output_text', '') or '' + result = output.strip() + + next_seq = max(record.input_message_ids.mapped('sequence') or [0]) + 10 + return_message = record.response_message_ids.create({ + 'response_session_id': record.id, + 'role': 'assistant', + 'type': 'input_text', + 'content': result, + 'sequence': next_seq, + }) + return return_message + + except Exception as e: + raise UserError(f"OpenAI error: {e}") diff --git a/openai_connector/models/res_company.py b/openai_connector/models/res_company.py new file mode 100644 index 00000000..00091ecc --- /dev/null +++ b/openai_connector/models/res_company.py @@ -0,0 +1,6 @@ +from odoo import models, fields + +class ResCompany(models.Model): + _inherit = 'res.company' + + openai_api_key = fields.Char(string='OpenAI API Key') diff --git a/openai_connector/models/res_config_settings.py b/openai_connector/models/res_config_settings.py new file mode 100644 index 00000000..2e7dad0a --- /dev/null +++ b/openai_connector/models/res_config_settings.py @@ -0,0 +1,11 @@ +from odoo import models, fields, api + +class ResConfigSettings(models.TransientModel): + _inherit = 'res.config.settings' + + openai_api_key = fields.Char( + string='OpenAI API Key', + related='company_id.openai_api_key', + readonly=False, + password=True + ) diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv new file mode 100644 index 00000000..8830a57c --- /dev/null +++ b/openai_connector/security/ir.model.access.csv @@ -0,0 +1,3 @@ +id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink +access_openai_vision_message,access_openai_vision_message,model_openai_vision_message,,1,1,1,1 +access_openai_vision_session,access_openai_vision_session,model_openai_vision_session,,1,1,1,1 diff --git a/openai_connector/views/openai_vision_menu.xml b/openai_connector/views/openai_vision_menu.xml new file mode 100644 index 00000000..a91bd473 --- /dev/null +++ b/openai_connector/views/openai_vision_menu.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/openai_connector/views/openai_vision_message.xml b/openai_connector/views/openai_vision_message.xml new file mode 100644 index 00000000..1ac16f7e --- /dev/null +++ b/openai_connector/views/openai_vision_message.xml @@ -0,0 +1,52 @@ + + + + openai.vision.message.tree + openai.vision.message + + + + + + + + + + + + openai.vision.message.form + openai.vision.message + +
+ + + + + + + + + + + +
+
+
+ + + Vision Messages + openai.vision.message + tree,form + + + +
diff --git a/openai_connector/views/openai_vision_session.xml b/openai_connector/views/openai_vision_session.xml new file mode 100644 index 00000000..b5e820dd --- /dev/null +++ b/openai_connector/views/openai_vision_session.xml @@ -0,0 +1,81 @@ + + + + openai.vision.session.tree + openai.vision.session + + + + + + + + + + + openai.vision.session.form + openai.vision.session + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + Vision Sessions + openai.vision.session + tree,form + + + +
diff --git a/openai_connector/views/res_config_settings_views.xml b/openai_connector/views/res_config_settings_views.xml new file mode 100644 index 00000000..2f4803f5 --- /dev/null +++ b/openai_connector/views/res_config_settings_views.xml @@ -0,0 +1,19 @@ + + + + res.config.settings.view.form.inherit.chatgpt + res.config.settings + + + + +
+
+
+
+
+
+
+
\ No newline at end of file From db7d6f8769095b60617215f6e9257e33781ad119 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 9 Jun 2025 09:15:11 +0000 Subject: [PATCH 02/61] for pre-commit --- openai_connector/__init__.py | 1 - openai_connector/__manifest__.py | 27 ++-- openai_connector/models/ir_attachment.py | 65 +++++--- .../models/openai_vision_message.py | 48 +++--- .../models/openai_vision_session.py | 150 +++++++++--------- openai_connector/models/res_company.py | 7 +- .../models/res_config_settings.py | 11 +- openai_connector/views/openai_vision_menu.xml | 15 +- .../views/openai_vision_message.xml | 50 +++--- .../views/openai_vision_session.xml | 88 +++++----- .../views/res_config_settings_views.xml | 12 +- 11 files changed, 258 insertions(+), 216 deletions(-) diff --git a/openai_connector/__init__.py b/openai_connector/__init__.py index f8ea970b..0650744f 100644 --- a/openai_connector/__init__.py +++ b/openai_connector/__init__.py @@ -1,2 +1 @@ -# Initialize the module from . import models diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py index 5500dfd5..ca4805b1 100644 --- a/openai_connector/__manifest__.py +++ b/openai_connector/__manifest__.py @@ -1,16 +1,19 @@ { - 'name': 'Openai Connector', - 'version': '15.0.1.0.0', - 'author': 'Quatile', + "name": "Openai Connector", + "version": "15.0.1.0.0", + "author": "Quatile", "license": "AGPL-3", - 'website': 'https://www.quartile.co', - 'depends': ['base_setup', 'web'], - 'data': [ - 'security/ir.model.access.csv', - 'views/res_config_settings_views.xml', - 'views/openai_vision_menu.xml', - 'views/openai_vision_message.xml', - 'views/openai_vision_session.xml', + "website": "https://www.quartile.co", + "depends": ["base_setup", "web"], + "data": [ + "security/ir.model.access.csv", + "views/res_config_settings_views.xml", + "views/openai_vision_menu.xml", + "views/openai_vision_message.xml", + "views/openai_vision_session.xml", ], - 'installable': True, + "external_dependencies": { + "python": ["openai"], + }, + "installable": True, } diff --git a/openai_connector/models/ir_attachment.py b/openai_connector/models/ir_attachment.py index 8f7387a3..ac33831c 100644 --- a/openai_connector/models/ir_attachment.py +++ b/openai_connector/models/ir_attachment.py @@ -1,13 +1,18 @@ -from odoo import models, api -from odoo.exceptions import UserError import base64 import uuid +from odoo import api, models + + class IrAttachment(models.Model): - _inherit = 'ir.attachment' + _inherit = "ir.attachment" @api.model - def analyze_with_openai_vision(self, attachment_id, prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?"): + def analyze_with_openai_vision( + self, + attachment_id, + prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?", + ): attachment = self.browse(attachment_id) if not attachment or not attachment.datas: return "指定された添付ファイルが存在しないか、データがありません。" @@ -19,33 +24,43 @@ def analyze_with_openai_vision(self, attachment_id, prompt_text="この画像に input_image = f"data:{attachment.mimetype};base64,{base64_str}" # セッションの作成 - session = self.env['openai.vision.session'].create({ - 'name': f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", - 'model': 'gpt-4o', - 'temperature': 0.7, - }) + session = self.env["openai.vision.session"].create( + { + "name": f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", + "model": "gpt-4o", + "temperature": 0.7, + } + ) # メッセージをセッションに関連付けて作成 - Message = self.env['openai.vision.message'] - Message.create({ - 'role': 'user', - 'type': 'input_text', - 'content': prompt_text, - 'sequence': 10, - 'input_session_id': session.id, - }) - Message.create({ - 'role': 'user', - 'type': 'input_image', - 'content': input_image, - 'sequence': 10, - 'input_session_id': session.id, - }) + Message = self.env["openai.vision.message"] + Message.create( + { + "role": "user", + "type": "input_text", + "content": prompt_text, + "sequence": 10, + "input_session_id": session.id, + } + ) + Message.create( + { + "role": "user", + "type": "input_image", + "content": input_image, + "sequence": 10, + "input_session_id": session.id, + } + ) # OpenAI APIを呼び出し return_message = session.call_openAI() - return return_message.content or session.response_refusal_message or "応答がありませんでした。" + return ( + return_message.content + or session.response_refusal_message + or "応答がありませんでした。" + ) except Exception as e: return f"エラー: {str(e)}" diff --git a/openai_connector/models/openai_vision_message.py b/openai_connector/models/openai_vision_message.py index eef39914..b10107b3 100644 --- a/openai_connector/models/openai_vision_message.py +++ b/openai_connector/models/openai_vision_message.py @@ -1,40 +1,34 @@ -import json -from odoo import models, fields, api -from odoo.exceptions import UserError +from odoo import api, fields, models + class OpenAIVisionMessage(models.Model): - _name = 'openai.vision.message' - _description = 'OpenAI Vision Message Line' + _name = "openai.vision.message" + _description = "OpenAI Vision Message Line" - role = fields.Selection([ - ("user", "User"), - ("assistant", "Assistant") - ], default='user', required=True) - type = fields.Selection([ - ("input_text", "Text"), - ("input_image", "Image URL") - ], default='input_text', required=True) - input_session_id = fields.Many2one( - 'openai.vision.session', string="Input Session") + role = fields.Selection( + [("user", "User"), ("assistant", "Assistant")], default="user", required=True + ) + type = fields.Selection( + [("input_text", "Text"), ("input_image", "Image URL")], + default="input_text", + required=True, + ) + input_session_id = fields.Many2one("openai.vision.session", string="Input Session") response_session_id = fields.Many2one( - 'openai.vision.session', string="Response Session") - content = fields.Text(string="Content") - sequence = fields.Integer(default=10, string="Sequence") + "openai.vision.session", string="Response Session" + ) + content = fields.Text( + required=True, + ) + sequence = fields.Integer(default=10) @api.model def to_openai_dict(self): if self.type == "input_text": return { "type": "input_text", - "text": self.content.strip() if self.content else "" + "text": self.content.strip() if self.content else "", } elif self.type == "input_image": url = self.content.strip() if self.content else "" - if not url: - raise UserError("Image URL message content is empty") - return { - "type": "input_image", - "image_url": url - } - else: - raise UserError(f"Unsupported message type: {self.type}") + return {"type": "input_image", "image_url": url} diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index 1dde926f..c56ac1ca 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -1,131 +1,137 @@ import json -from odoo import models, fields, api -from odoo.exceptions import UserError -from openai import OpenAI from collections import defaultdict +from openai import OpenAI + +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + class OpenAISession(models.Model): - _name = 'openai.vision.session' - _description = 'OpenAI Vision Session' + _name = "openai.vision.session" + _description = "OpenAI Vision Session" - name = fields.Char(required=True, string="Session Name") - model = fields.Selection([ - ('gpt-4o', 'GPT-4o'), - ], default='gpt-4o', required=True) - temperature = fields.Float(default=0.7, string="Temperature") + name = fields.Char(required=True) + model = fields.Selection( + [ + ("gpt-4o", "GPT-4o"), + ], + default="gpt-4o", + required=True, + ) + temperature = fields.Float(default=0.7) + instruction = fields.Text() input_message_ids = fields.One2many( - 'openai.vision.message', 'input_session_id', string="Input Messages") + "openai.vision.message", "input_session_id", string="Input Messages" + ) response_message_ids = fields.One2many( - 'openai.vision.message', 'response_session_id', string="Response Messages") + "openai.vision.message", "response_session_id", string="Response Messages" + ) previous_response_id = fields.Char(string="Previous Response ID") - store_response = fields.Boolean(string="Store Response", default=True) + store_response = fields.Boolean(default=True) web_search = fields.Boolean(string="Use Web Search", default=False) - response_refusal_message = fields.Text( - string="Refusal Reason", readonly=True) + response_refusal_message = fields.Text(string="Refusal Reason", readonly=True) response_format_enabled = fields.Boolean( - string="Use Structured Output (JSON Schema)", default=False) + string="Use Structured Output (JSON Schema)", default=False + ) response_format_schema = fields.Text( string="Response Format Schema (JSON)", - default=json.dumps({ - "type": "object", - "properties": { - "message": {"type": "string"} + default=json.dumps( + { + "type": "object", + "properties": {"message": {"type": "string"}}, + "required": ["message"], + "additionalProperties": False, + "strict": True, }, - "required": ["message"], - "additionalProperties": False, - "strict": True, - }, - indent=2)) + indent=2, + ), + ) - @api.onchange('response_format_schema') + @api.onchange("response_format_schema") def _onchange_response_format_schema(self): for record in self: if record.response_format_enabled: try: json.loads(record.response_format_schema or "{}") except json.JSONDecodeError as e: - raise UserError("The JSON schema is invalid:\n%s" % str(e)) + raise UserError( + _("The JSON schema is invalid:\n{}").format(e) + ) from e @api.model def call_openAI(self): api_key = self.env.company.openai_api_key if not api_key: - raise UserError("OpenAI API key is not configured") - + raise UserError(_("OpenAI API key is not configured")) client = OpenAI(api_key=api_key) - for record in self: - if record.model != 'gpt-4o': - raise UserError( - "Responses API(構造化出力)は現在 'gpt-4o' モデルでのみサポートされています。") grouped = defaultdict(list) for msg in record.input_message_ids: key = (msg.sequence, msg.role) grouped[key].append(msg) - instructions = [] inputs = [] - sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) + sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) for seq, role in sorted_keys: components = grouped[(seq, role)] roles = {c.role for c in components} if len(roles) != 1: - raise UserError(f"Multiple roles found at sequence {seq}: {roles}") - contents = [c.to_openai_dict() for c in components if c.content and c.content.strip()] + raise UserError(_("Multiple roles found at same sequence")) + contents = [ + c.to_openai_dict() + for c in components + if c.content and c.content.strip() + ] if not contents: continue - inputs.append({ - "role": role, - "content": contents - }) - + inputs.append({"role": role, "content": contents}) text_format = None if record.response_format_enabled: try: - schema = json.loads(record.response_format_schema or '{}') + schema = json.loads(record.response_format_schema or "{}") text_format = { "type": "json_schema", "name": "json_schema", - "schema": schema + "schema": schema, } except json.JSONDecodeError as e: - raise UserError("Invalid JSON schema:\n%s" % str(e)) - + raise UserError( + _("The JSON schema is invalid:\n{}").format(e) + ) from e try: params = { - 'model': record.model, - 'instructions': "\n".join(instructions).strip(), - 'input': inputs, - 'temperature': record.temperature, - 'store': record.store_response, - 'text': {"format": text_format}, - "tools": [{"type": "web_search_preview"}] if record.web_search else [], + "model": record.model, + "instructions": record.instruction, + "input": inputs, + "temperature": record.temperature, + "store": record.store_response, + "text": {"format": text_format}, + "tools": [{"type": "web_search_preview"}] + if record.web_search + else [], } - if record.previous_response_id: - params['previous_response_id'] = record.previous_response_id - + params["previous_response_id"] = record.previous_response_id response = client.responses.create(**params) if record.store_response: - record.previous_response_id = getattr(response, 'id', False) - - refusal = getattr(response, 'refusal_reason', None) + record.previous_response_id = getattr(response, "id", False) + refusal = getattr(response, "refusal_reason", None) if refusal: record.response_refusal_message = refusal continue - record.response_refusal_message = False - output = getattr(response, 'output_text', '') or '' + output = getattr(response, "output_text", "") or "" result = output.strip() - - next_seq = max(record.input_message_ids.mapped('sequence') or [0]) + 10 - return_message = record.response_message_ids.create({ - 'response_session_id': record.id, - 'role': 'assistant', - 'type': 'input_text', - 'content': result, - 'sequence': next_seq, - }) + next_seq = max(record.input_message_ids.mapped("sequence") or [0]) + 10 + return_message = record.response_message_ids.create( + { + "response_session_id": record.id, + "role": "assistant", + "type": "input_text", + "content": result, + "sequence": next_seq, + } + ) return return_message - except Exception as e: - raise UserError(f"OpenAI error: {e}") + raise UserError(_("OpenAI error:\n{}").format(e)) from e diff --git a/openai_connector/models/res_company.py b/openai_connector/models/res_company.py index 00091ecc..0b592b11 100644 --- a/openai_connector/models/res_company.py +++ b/openai_connector/models/res_company.py @@ -1,6 +1,7 @@ -from odoo import models, fields +from odoo import fields, models + class ResCompany(models.Model): - _inherit = 'res.company' + _inherit = "res.company" - openai_api_key = fields.Char(string='OpenAI API Key') + openai_api_key = fields.Char(string="OpenAI API Key") diff --git a/openai_connector/models/res_config_settings.py b/openai_connector/models/res_config_settings.py index 2e7dad0a..a2ecd1a9 100644 --- a/openai_connector/models/res_config_settings.py +++ b/openai_connector/models/res_config_settings.py @@ -1,11 +1,12 @@ -from odoo import models, fields, api +from odoo import fields, models + class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' + _inherit = "res.config.settings" openai_api_key = fields.Char( - string='OpenAI API Key', - related='company_id.openai_api_key', + string="OpenAI API Key", + related="company_id.openai_api_key", readonly=False, - password=True + password=True, ) diff --git a/openai_connector/views/openai_vision_menu.xml b/openai_connector/views/openai_vision_menu.xml index a91bd473..13722b59 100644 --- a/openai_connector/views/openai_vision_menu.xml +++ b/openai_connector/views/openai_vision_menu.xml @@ -1,10 +1,11 @@ - + - + diff --git a/openai_connector/views/openai_vision_message.xml b/openai_connector/views/openai_vision_message.xml index 1ac16f7e..c78391c4 100644 --- a/openai_connector/views/openai_vision_message.xml +++ b/openai_connector/views/openai_vision_message.xml @@ -1,16 +1,18 @@ - + openai.vision.message.tree openai.vision.message - - - - - + + + + + @@ -22,16 +24,20 @@
- - - + + + - - + +
@@ -44,9 +50,11 @@ tree,form - +
diff --git a/openai_connector/views/openai_vision_session.xml b/openai_connector/views/openai_vision_session.xml index b5e820dd..5708a93e 100644 --- a/openai_connector/views/openai_vision_session.xml +++ b/openai_connector/views/openai_vision_session.xml @@ -1,13 +1,13 @@ - + openai.vision.session.tree openai.vision.session - - - - + + + + @@ -19,46 +19,58 @@
- - - - - - + + + + + + + + - + - - - - + + + + - + - - - - + + + + - + - + @@ -73,9 +85,11 @@ tree,form - + diff --git a/openai_connector/views/res_config_settings_views.xml b/openai_connector/views/res_config_settings_views.xml index 2f4803f5..18dd93c1 100644 --- a/openai_connector/views/res_config_settings_views.xml +++ b/openai_connector/views/res_config_settings_views.xml @@ -1,19 +1,19 @@ - + res.config.settings.view.form.inherit.chatgpt res.config.settings - - + +
-
-
\ No newline at end of file + From 4e470714da9b41fd6b9635ed5285c28e8c2341c3 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 9 Jun 2025 09:18:01 +0000 Subject: [PATCH 03/61] delete chatgpt_integration --- chatgpt_integration/__init__.py | 2 - chatgpt_integration/__manifest__.py | 20 --- chatgpt_integration/models/__init__.py | 6 - chatgpt_integration/models/ir_attachment.py | 51 ------- .../models/openai_vision_message.py | 40 ------ .../models/openai_vision_session.py | 131 ------------------ chatgpt_integration/models/res_company.py | 6 - .../models/res_config_settings.py | 11 -- .../security/ir.model.access.csv | 3 - .../views/openai_vision_menu.xml | 10 -- .../views/openai_vision_message.xml | 52 ------- .../views/openai_vision_session.xml | 81 ----------- .../views/res_config_settings_views.xml | 19 --- .../odoo/addons/openai_connector | 1 + setup/openai_connector/setup.py | 6 + 15 files changed, 7 insertions(+), 432 deletions(-) delete mode 100644 chatgpt_integration/__init__.py delete mode 100644 chatgpt_integration/__manifest__.py delete mode 100644 chatgpt_integration/models/__init__.py delete mode 100644 chatgpt_integration/models/ir_attachment.py delete mode 100644 chatgpt_integration/models/openai_vision_message.py delete mode 100644 chatgpt_integration/models/openai_vision_session.py delete mode 100644 chatgpt_integration/models/res_company.py delete mode 100644 chatgpt_integration/models/res_config_settings.py delete mode 100644 chatgpt_integration/security/ir.model.access.csv delete mode 100644 chatgpt_integration/views/openai_vision_menu.xml delete mode 100644 chatgpt_integration/views/openai_vision_message.xml delete mode 100644 chatgpt_integration/views/openai_vision_session.xml delete mode 100644 chatgpt_integration/views/res_config_settings_views.xml create mode 120000 setup/openai_connector/odoo/addons/openai_connector create mode 100644 setup/openai_connector/setup.py diff --git a/chatgpt_integration/__init__.py b/chatgpt_integration/__init__.py deleted file mode 100644 index f8ea970b..00000000 --- a/chatgpt_integration/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Initialize the module -from . import models diff --git a/chatgpt_integration/__manifest__.py b/chatgpt_integration/__manifest__.py deleted file mode 100644 index bc0c92c1..00000000 --- a/chatgpt_integration/__manifest__.py +++ /dev/null @@ -1,20 +0,0 @@ -{ - 'name': 'ChatGPT Integration', - 'version': '15.0.1.0.0', - 'summary': 'Integrate ChatGPT with Odoo', - 'description': 'This module allows integration with ChatGPT by storing API keys in res_config and res_company.', - 'author': 'Your Company', - 'website': 'https://www.quartile.co', - 'category': 'Tools', - 'depends': ['base', 'base_setup', 'web'], - 'data': [ - 'security/ir.model.access.csv', - 'views/res_config_settings_views.xml', - 'views/openai_vision_menu.xml', - 'views/openai_vision_message.xml', - 'views/openai_vision_session.xml', - ], - 'installable': True, - 'application': True, - 'auto_install': False, -} diff --git a/chatgpt_integration/models/__init__.py b/chatgpt_integration/models/__init__.py deleted file mode 100644 index b11e9fb3..00000000 --- a/chatgpt_integration/models/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Initialize the models -from . import ir_attachment -from . import res_company -from . import res_config_settings -from . import openai_vision_message -from . import openai_vision_session diff --git a/chatgpt_integration/models/ir_attachment.py b/chatgpt_integration/models/ir_attachment.py deleted file mode 100644 index 8f7387a3..00000000 --- a/chatgpt_integration/models/ir_attachment.py +++ /dev/null @@ -1,51 +0,0 @@ -from odoo import models, api -from odoo.exceptions import UserError -import base64 -import uuid - -class IrAttachment(models.Model): - _inherit = 'ir.attachment' - - @api.model - def analyze_with_openai_vision(self, attachment_id, prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?"): - attachment = self.browse(attachment_id) - if not attachment or not attachment.datas: - return "指定された添付ファイルが存在しないか、データがありません。" - - try: - # base64エンコードされた画像URLを作成 - image_data = base64.b64decode(attachment.datas) - base64_str = base64.b64encode(image_data).decode("utf-8") - input_image = f"data:{attachment.mimetype};base64,{base64_str}" - - # セッションの作成 - session = self.env['openai.vision.session'].create({ - 'name': f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", - 'model': 'gpt-4o', - 'temperature': 0.7, - }) - - # メッセージをセッションに関連付けて作成 - Message = self.env['openai.vision.message'] - Message.create({ - 'role': 'user', - 'type': 'input_text', - 'content': prompt_text, - 'sequence': 10, - 'input_session_id': session.id, - }) - Message.create({ - 'role': 'user', - 'type': 'input_image', - 'content': input_image, - 'sequence': 10, - 'input_session_id': session.id, - }) - - # OpenAI APIを呼び出し - return_message = session.call_openAI() - - return return_message.content or session.response_refusal_message or "応答がありませんでした。" - - except Exception as e: - return f"エラー: {str(e)}" diff --git a/chatgpt_integration/models/openai_vision_message.py b/chatgpt_integration/models/openai_vision_message.py deleted file mode 100644 index eef39914..00000000 --- a/chatgpt_integration/models/openai_vision_message.py +++ /dev/null @@ -1,40 +0,0 @@ -import json -from odoo import models, fields, api -from odoo.exceptions import UserError - -class OpenAIVisionMessage(models.Model): - _name = 'openai.vision.message' - _description = 'OpenAI Vision Message Line' - - role = fields.Selection([ - ("user", "User"), - ("assistant", "Assistant") - ], default='user', required=True) - type = fields.Selection([ - ("input_text", "Text"), - ("input_image", "Image URL") - ], default='input_text', required=True) - input_session_id = fields.Many2one( - 'openai.vision.session', string="Input Session") - response_session_id = fields.Many2one( - 'openai.vision.session', string="Response Session") - content = fields.Text(string="Content") - sequence = fields.Integer(default=10, string="Sequence") - - @api.model - def to_openai_dict(self): - if self.type == "input_text": - return { - "type": "input_text", - "text": self.content.strip() if self.content else "" - } - elif self.type == "input_image": - url = self.content.strip() if self.content else "" - if not url: - raise UserError("Image URL message content is empty") - return { - "type": "input_image", - "image_url": url - } - else: - raise UserError(f"Unsupported message type: {self.type}") diff --git a/chatgpt_integration/models/openai_vision_session.py b/chatgpt_integration/models/openai_vision_session.py deleted file mode 100644 index 1dde926f..00000000 --- a/chatgpt_integration/models/openai_vision_session.py +++ /dev/null @@ -1,131 +0,0 @@ -import json -from odoo import models, fields, api -from odoo.exceptions import UserError -from openai import OpenAI -from collections import defaultdict - -class OpenAISession(models.Model): - _name = 'openai.vision.session' - _description = 'OpenAI Vision Session' - - name = fields.Char(required=True, string="Session Name") - model = fields.Selection([ - ('gpt-4o', 'GPT-4o'), - ], default='gpt-4o', required=True) - temperature = fields.Float(default=0.7, string="Temperature") - input_message_ids = fields.One2many( - 'openai.vision.message', 'input_session_id', string="Input Messages") - response_message_ids = fields.One2many( - 'openai.vision.message', 'response_session_id', string="Response Messages") - previous_response_id = fields.Char(string="Previous Response ID") - store_response = fields.Boolean(string="Store Response", default=True) - web_search = fields.Boolean(string="Use Web Search", default=False) - response_refusal_message = fields.Text( - string="Refusal Reason", readonly=True) - response_format_enabled = fields.Boolean( - string="Use Structured Output (JSON Schema)", default=False) - response_format_schema = fields.Text( - string="Response Format Schema (JSON)", - default=json.dumps({ - "type": "object", - "properties": { - "message": {"type": "string"} - }, - "required": ["message"], - "additionalProperties": False, - "strict": True, - }, - indent=2)) - - @api.onchange('response_format_schema') - def _onchange_response_format_schema(self): - for record in self: - if record.response_format_enabled: - try: - json.loads(record.response_format_schema or "{}") - except json.JSONDecodeError as e: - raise UserError("The JSON schema is invalid:\n%s" % str(e)) - - @api.model - def call_openAI(self): - api_key = self.env.company.openai_api_key - if not api_key: - raise UserError("OpenAI API key is not configured") - - client = OpenAI(api_key=api_key) - - for record in self: - if record.model != 'gpt-4o': - raise UserError( - "Responses API(構造化出力)は現在 'gpt-4o' モデルでのみサポートされています。") - grouped = defaultdict(list) - for msg in record.input_message_ids: - key = (msg.sequence, msg.role) - grouped[key].append(msg) - instructions = [] - inputs = [] - sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) - for seq, role in sorted_keys: - components = grouped[(seq, role)] - roles = {c.role for c in components} - if len(roles) != 1: - raise UserError(f"Multiple roles found at sequence {seq}: {roles}") - contents = [c.to_openai_dict() for c in components if c.content and c.content.strip()] - if not contents: - continue - inputs.append({ - "role": role, - "content": contents - }) - - text_format = None - if record.response_format_enabled: - try: - schema = json.loads(record.response_format_schema or '{}') - text_format = { - "type": "json_schema", - "name": "json_schema", - "schema": schema - } - except json.JSONDecodeError as e: - raise UserError("Invalid JSON schema:\n%s" % str(e)) - - try: - params = { - 'model': record.model, - 'instructions': "\n".join(instructions).strip(), - 'input': inputs, - 'temperature': record.temperature, - 'store': record.store_response, - 'text': {"format": text_format}, - "tools": [{"type": "web_search_preview"}] if record.web_search else [], - } - - if record.previous_response_id: - params['previous_response_id'] = record.previous_response_id - - response = client.responses.create(**params) - if record.store_response: - record.previous_response_id = getattr(response, 'id', False) - - refusal = getattr(response, 'refusal_reason', None) - if refusal: - record.response_refusal_message = refusal - continue - - record.response_refusal_message = False - output = getattr(response, 'output_text', '') or '' - result = output.strip() - - next_seq = max(record.input_message_ids.mapped('sequence') or [0]) + 10 - return_message = record.response_message_ids.create({ - 'response_session_id': record.id, - 'role': 'assistant', - 'type': 'input_text', - 'content': result, - 'sequence': next_seq, - }) - return return_message - - except Exception as e: - raise UserError(f"OpenAI error: {e}") diff --git a/chatgpt_integration/models/res_company.py b/chatgpt_integration/models/res_company.py deleted file mode 100644 index 00091ecc..00000000 --- a/chatgpt_integration/models/res_company.py +++ /dev/null @@ -1,6 +0,0 @@ -from odoo import models, fields - -class ResCompany(models.Model): - _inherit = 'res.company' - - openai_api_key = fields.Char(string='OpenAI API Key') diff --git a/chatgpt_integration/models/res_config_settings.py b/chatgpt_integration/models/res_config_settings.py deleted file mode 100644 index 2e7dad0a..00000000 --- a/chatgpt_integration/models/res_config_settings.py +++ /dev/null @@ -1,11 +0,0 @@ -from odoo import models, fields, api - -class ResConfigSettings(models.TransientModel): - _inherit = 'res.config.settings' - - openai_api_key = fields.Char( - string='OpenAI API Key', - related='company_id.openai_api_key', - readonly=False, - password=True - ) diff --git a/chatgpt_integration/security/ir.model.access.csv b/chatgpt_integration/security/ir.model.access.csv deleted file mode 100644 index 8830a57c..00000000 --- a/chatgpt_integration/security/ir.model.access.csv +++ /dev/null @@ -1,3 +0,0 @@ -id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink -access_openai_vision_message,access_openai_vision_message,model_openai_vision_message,,1,1,1,1 -access_openai_vision_session,access_openai_vision_session,model_openai_vision_session,,1,1,1,1 diff --git a/chatgpt_integration/views/openai_vision_menu.xml b/chatgpt_integration/views/openai_vision_menu.xml deleted file mode 100644 index a91bd473..00000000 --- a/chatgpt_integration/views/openai_vision_menu.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/chatgpt_integration/views/openai_vision_message.xml b/chatgpt_integration/views/openai_vision_message.xml deleted file mode 100644 index fdecef56..00000000 --- a/chatgpt_integration/views/openai_vision_message.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - openai.vision.message.tree - openai.vision.message - - - - - - - - - - - - openai.vision.message.form - openai.vision.message - - - - - - - - - - - - - - - - - - - Vision Messages - openai.vision.message - tree,form - - - - diff --git a/chatgpt_integration/views/openai_vision_session.xml b/chatgpt_integration/views/openai_vision_session.xml deleted file mode 100644 index b5e820dd..00000000 --- a/chatgpt_integration/views/openai_vision_session.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - openai.vision.session.tree - openai.vision.session - - - - - - - - - - - openai.vision.session.form - openai.vision.session - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
- - - Vision Sessions - openai.vision.session - tree,form - - - -
diff --git a/chatgpt_integration/views/res_config_settings_views.xml b/chatgpt_integration/views/res_config_settings_views.xml deleted file mode 100644 index 2f4803f5..00000000 --- a/chatgpt_integration/views/res_config_settings_views.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - res.config.settings.view.form.inherit.chatgpt - res.config.settings - - - - -
-
-
-
-
-
-
-
\ No newline at end of file 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, +) From 2db4688077c9c723fa64c98fa6f9892e3d891e81 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 9 Jun 2025 10:12:00 +0000 Subject: [PATCH 04/61] upd --- openai_connector/__manifest__.py | 1 - openai_connector/models/ir_attachment.py | 2 - .../models/openai_vision_message.py | 11 +---- .../models/openai_vision_session.py | 44 +++++-------------- openai_connector/views/openai_vision_menu.xml | 11 ----- .../views/openai_vision_message.xml | 4 -- .../views/openai_vision_session.xml | 31 ++++--------- requirements.txt | 1 + 8 files changed, 21 insertions(+), 84 deletions(-) delete mode 100644 openai_connector/views/openai_vision_menu.xml diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py index ca4805b1..68fe7635 100644 --- a/openai_connector/__manifest__.py +++ b/openai_connector/__manifest__.py @@ -8,7 +8,6 @@ "data": [ "security/ir.model.access.csv", "views/res_config_settings_views.xml", - "views/openai_vision_menu.xml", "views/openai_vision_message.xml", "views/openai_vision_session.xml", ], diff --git a/openai_connector/models/ir_attachment.py b/openai_connector/models/ir_attachment.py index ac33831c..4dc3c646 100644 --- a/openai_connector/models/ir_attachment.py +++ b/openai_connector/models/ir_attachment.py @@ -36,7 +36,6 @@ def analyze_with_openai_vision( Message = self.env["openai.vision.message"] Message.create( { - "role": "user", "type": "input_text", "content": prompt_text, "sequence": 10, @@ -45,7 +44,6 @@ def analyze_with_openai_vision( ) Message.create( { - "role": "user", "type": "input_image", "content": input_image, "sequence": 10, diff --git a/openai_connector/models/openai_vision_message.py b/openai_connector/models/openai_vision_message.py index b10107b3..30671a57 100644 --- a/openai_connector/models/openai_vision_message.py +++ b/openai_connector/models/openai_vision_message.py @@ -5,22 +5,13 @@ class OpenAIVisionMessage(models.Model): _name = "openai.vision.message" _description = "OpenAI Vision Message Line" - role = fields.Selection( - [("user", "User"), ("assistant", "Assistant")], default="user", required=True - ) type = fields.Selection( [("input_text", "Text"), ("input_image", "Image URL")], default="input_text", required=True, ) input_session_id = fields.Many2one("openai.vision.session", string="Input Session") - response_session_id = fields.Many2one( - "openai.vision.session", string="Response Session" - ) - content = fields.Text( - required=True, - ) - sequence = fields.Integer(default=10) + content = fields.Text(required=True) @api.model def to_openai_dict(self): diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index c56ac1ca..4ff29bae 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -24,9 +24,6 @@ class OpenAISession(models.Model): input_message_ids = fields.One2many( "openai.vision.message", "input_session_id", string="Input Messages" ) - response_message_ids = fields.One2many( - "openai.vision.message", "response_session_id", string="Response Messages" - ) previous_response_id = fields.Char(string="Previous Response ID") store_response = fields.Boolean(default=True) web_search = fields.Boolean(string="Use Web Search", default=False) @@ -66,26 +63,14 @@ def call_openAI(self): raise UserError(_("OpenAI API key is not configured")) client = OpenAI(api_key=api_key) for record in self: - grouped = defaultdict(list) - for msg in record.input_message_ids: - key = (msg.sequence, msg.role) - grouped[key].append(msg) inputs = [] - sorted_keys = sorted(grouped.keys(), key=lambda k: k[0]) - for seq, role in sorted_keys: - components = grouped[(seq, role)] - roles = {c.role for c in components} - if len(roles) != 1: - raise UserError(_("Multiple roles found at same sequence")) - contents = [ - c.to_openai_dict() - for c in components - if c.content and c.content.strip() - ] - if not contents: - continue - inputs.append({"role": role, "content": contents}) - text_format = None + for msg in record.input_message_ids: + if msg.content and msg.content.strip(): + inputs.append({ + "role": "user", + "content": [msg.to_openai_dict()], + }) + text_format = False if record.response_format_enabled: try: schema = json.loads(record.response_format_schema or "{}") @@ -105,11 +90,12 @@ def call_openAI(self): "input": inputs, "temperature": record.temperature, "store": record.store_response, - "text": {"format": text_format}, "tools": [{"type": "web_search_preview"}] if record.web_search else [], } + if text_format: + params["text"] = {"format": text_format} if record.previous_response_id: params["previous_response_id"] = record.previous_response_id response = client.responses.create(**params) @@ -122,16 +108,6 @@ def call_openAI(self): record.response_refusal_message = False output = getattr(response, "output_text", "") or "" result = output.strip() - next_seq = max(record.input_message_ids.mapped("sequence") or [0]) + 10 - return_message = record.response_message_ids.create( - { - "response_session_id": record.id, - "role": "assistant", - "type": "input_text", - "content": result, - "sequence": next_seq, - } - ) - return return_message + return result except Exception as e: raise UserError(_("OpenAI error:\n{}").format(e)) from e diff --git a/openai_connector/views/openai_vision_menu.xml b/openai_connector/views/openai_vision_menu.xml deleted file mode 100644 index 13722b59..00000000 --- a/openai_connector/views/openai_vision_menu.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - diff --git a/openai_connector/views/openai_vision_message.xml b/openai_connector/views/openai_vision_message.xml index c78391c4..24d8063d 100644 --- a/openai_connector/views/openai_vision_message.xml +++ b/openai_connector/views/openai_vision_message.xml @@ -5,8 +5,6 @@ openai.vision.message - - - - diff --git a/openai_connector/views/openai_vision_session.xml b/openai_connector/views/openai_vision_session.xml index 5708a93e..4611636d 100644 --- a/openai_connector/views/openai_vision_session.xml +++ b/openai_connector/views/openai_vision_session.xml @@ -32,30 +32,9 @@ - - - - - - - - - - - - - + + +
diff --git a/requirements.txt b/requirements.txt index 9cd16292..983f3ac6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ # generated from manifests external_dependencies +openai From 979c9767c5a1916c1d2f9dc3894a013d7273604f Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Wed, 11 Jun 2025 07:23:58 +0000 Subject: [PATCH 05/61] add log and creater module, delete message --- openai_connector/__manifest__.py | 2 +- openai_connector/models/__init__.py | 4 +- openai_connector/models/ir_attachment.py | 64 ---------- .../models/openai_vision_message.py | 25 ---- .../models/openai_vision_session.py | 117 ++++++++++-------- .../models/openai_vision_session_log.py | 42 +++++++ openai_connector/security/ir.model.access.csv | 2 +- .../views/openai_vision_message.xml | 56 --------- .../views/openai_vision_session.xml | 18 +-- .../views/openai_vision_session_log.xml | 46 +++++++ product_name_creater/__init__.py | 1 + product_name_creater/__manifest__.py | 12 ++ ...ai_vision_session_product_name_creater.xml | 29 +++++ product_name_creater/models/__init__.py | 1 + product_name_creater/models/ir_attachment.py | 38 ++++++ .../odoo/addons/product_name_creater | 1 + setup/product_name_creater/setup.py | 6 + 17 files changed, 243 insertions(+), 221 deletions(-) delete mode 100644 openai_connector/models/ir_attachment.py delete mode 100644 openai_connector/models/openai_vision_message.py create mode 100644 openai_connector/models/openai_vision_session_log.py delete mode 100644 openai_connector/views/openai_vision_message.xml create mode 100644 openai_connector/views/openai_vision_session_log.xml create mode 100644 product_name_creater/__init__.py create mode 100644 product_name_creater/__manifest__.py create mode 100644 product_name_creater/data/openai_vision_session_product_name_creater.xml create mode 100644 product_name_creater/models/__init__.py create mode 100644 product_name_creater/models/ir_attachment.py create mode 120000 setup/product_name_creater/odoo/addons/product_name_creater create mode 100644 setup/product_name_creater/setup.py diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py index 68fe7635..65fafd08 100644 --- a/openai_connector/__manifest__.py +++ b/openai_connector/__manifest__.py @@ -8,8 +8,8 @@ "data": [ "security/ir.model.access.csv", "views/res_config_settings_views.xml", - "views/openai_vision_message.xml", "views/openai_vision_session.xml", + "views/openai_vision_session_log.xml", ], "external_dependencies": { "python": ["openai"], diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py index b11e9fb3..9733d8c3 100644 --- a/openai_connector/models/__init__.py +++ b/openai_connector/models/__init__.py @@ -1,6 +1,4 @@ -# Initialize the models -from . import ir_attachment from . import res_company from . import res_config_settings -from . import openai_vision_message from . import openai_vision_session +from . import openai_vision_session_log diff --git a/openai_connector/models/ir_attachment.py b/openai_connector/models/ir_attachment.py deleted file mode 100644 index 4dc3c646..00000000 --- a/openai_connector/models/ir_attachment.py +++ /dev/null @@ -1,64 +0,0 @@ -import base64 -import uuid - -from odoo import api, models - - -class IrAttachment(models.Model): - _inherit = "ir.attachment" - - @api.model - def analyze_with_openai_vision( - self, - attachment_id, - prompt_text="この画像に何が写っていますか?またこの写真は見えていますか?見えていない場合は教えてください?", - ): - attachment = self.browse(attachment_id) - if not attachment or not attachment.datas: - return "指定された添付ファイルが存在しないか、データがありません。" - - try: - # base64エンコードされた画像URLを作成 - image_data = base64.b64decode(attachment.datas) - base64_str = base64.b64encode(image_data).decode("utf-8") - input_image = f"data:{attachment.mimetype};base64,{base64_str}" - - # セッションの作成 - session = self.env["openai.vision.session"].create( - { - "name": f"Attachment Analysis: {attachment.name or str(uuid.uuid4())}", - "model": "gpt-4o", - "temperature": 0.7, - } - ) - - # メッセージをセッションに関連付けて作成 - Message = self.env["openai.vision.message"] - Message.create( - { - "type": "input_text", - "content": prompt_text, - "sequence": 10, - "input_session_id": session.id, - } - ) - Message.create( - { - "type": "input_image", - "content": input_image, - "sequence": 10, - "input_session_id": session.id, - } - ) - - # OpenAI APIを呼び出し - return_message = session.call_openAI() - - return ( - return_message.content - or session.response_refusal_message - or "応答がありませんでした。" - ) - - except Exception as e: - return f"エラー: {str(e)}" diff --git a/openai_connector/models/openai_vision_message.py b/openai_connector/models/openai_vision_message.py deleted file mode 100644 index 30671a57..00000000 --- a/openai_connector/models/openai_vision_message.py +++ /dev/null @@ -1,25 +0,0 @@ -from odoo import api, fields, models - - -class OpenAIVisionMessage(models.Model): - _name = "openai.vision.message" - _description = "OpenAI Vision Message Line" - - type = fields.Selection( - [("input_text", "Text"), ("input_image", "Image URL")], - default="input_text", - required=True, - ) - input_session_id = fields.Many2one("openai.vision.session", string="Input Session") - content = fields.Text(required=True) - - @api.model - def to_openai_dict(self): - if self.type == "input_text": - return { - "type": "input_text", - "text": self.content.strip() if self.content else "", - } - elif self.type == "input_image": - url = self.content.strip() if self.content else "" - return {"type": "input_image", "image_url": url} diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index 4ff29bae..a1b24630 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -1,11 +1,13 @@ import json -from collections import defaultdict +import logging from openai import OpenAI from odoo import _, api, fields, models from odoo.exceptions import UserError +_logger = logging.getLogger(__name__) + class OpenAISession(models.Model): _name = "openai.vision.session" @@ -21,19 +23,16 @@ class OpenAISession(models.Model): ) temperature = fields.Float(default=0.7) instruction = fields.Text() - input_message_ids = fields.One2many( - "openai.vision.message", "input_session_id", string="Input Messages" - ) + inputs = fields.Text() previous_response_id = fields.Char(string="Previous Response ID") store_response = fields.Boolean(default=True) web_search = fields.Boolean(string="Use Web Search", default=False) - response_refusal_message = fields.Text(string="Refusal Reason", readonly=True) response_format_enabled = fields.Boolean( string="Use Structured Output (JSON Schema)", default=False ) response_format_schema = fields.Text( string="Response Format Schema (JSON)", - default=json.dumps( + default=lambda self: json.dumps( { "type": "object", "properties": {"message": {"type": "string"}}, @@ -44,6 +43,7 @@ class OpenAISession(models.Model): indent=2, ), ) + request_payload = fields.Text(compute="_compute_request_payload", store=False) @api.onchange("response_format_schema") def _onchange_response_format_schema(self): @@ -56,58 +56,65 @@ def _onchange_response_format_schema(self): _("The JSON schema is invalid:\n{}").format(e) ) from e + def _compute_request_payload(self): + self.ensure_one() + try: + request_payload = { + "model": self.model, + "instructions": self.instruction or "", + "input": json.loads(self.inputs 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 + + self.request_payload = json.dumps(request_payload, indent=2) + + except Exception as e: + _logger.warning( + "Failed to compute request_payload for session %s: %s", self.name, e + ) + self.request_payload = "{}" + @api.model def call_openAI(self): + self.ensure_one() api_key = self.env.company.openai_api_key if not api_key: raise UserError(_("OpenAI API key is not configured")) client = OpenAI(api_key=api_key) - for record in self: - inputs = [] - for msg in record.input_message_ids: - if msg.content and msg.content.strip(): - inputs.append({ - "role": "user", - "content": [msg.to_openai_dict()], - }) - text_format = False - if record.response_format_enabled: - try: - schema = json.loads(record.response_format_schema or "{}") - text_format = { - "type": "json_schema", - "name": "json_schema", - "schema": schema, - } - except json.JSONDecodeError as e: - raise UserError( - _("The JSON schema is invalid:\n{}").format(e) - ) from e - try: - params = { - "model": record.model, - "instructions": record.instruction, - "input": inputs, - "temperature": record.temperature, - "store": record.store_response, - "tools": [{"type": "web_search_preview"}] - if record.web_search - else [], - } - if text_format: - params["text"] = {"format": text_format} - if record.previous_response_id: - params["previous_response_id"] = record.previous_response_id - response = client.responses.create(**params) - if record.store_response: - record.previous_response_id = getattr(response, "id", False) - refusal = getattr(response, "refusal_reason", None) - if refusal: - record.response_refusal_message = refusal - continue - record.response_refusal_message = False - output = getattr(response, "output_text", "") or "" - result = output.strip() - return result - except Exception as e: - raise UserError(_("OpenAI error:\n{}").format(e)) from e + refusal = False + try: + payload = json.loads(self.request_payload) + response = client.responses.create(**payload) + + if self.store_response: + self.previous_response_id = getattr(response, "id", False) + + refusal = getattr(response, "refusal_reason", None) + if refusal: + self.create_error_log("Refused by OpenAI", refusal) + raise UserError(_("OpenAI refused the request:\n{}").format(refusal)) + output = getattr(response, "output_text", "") or "" + return output.strip() + + except Exception as e: + self.create_error_log(e, refusal) + raise UserError(_("OpenAI error:\n{}").format(e)) from e + + @api.model + def create_error_log(self, error, refusal): + self.env["openai.vision.session.log"].create_from_session(self, error, refusal) diff --git a/openai_connector/models/openai_vision_session_log.py b/openai_connector/models/openai_vision_session_log.py new file mode 100644 index 00000000..1c7b318e --- /dev/null +++ b/openai_connector/models/openai_vision_session_log.py @@ -0,0 +1,42 @@ +import json +import logging +from odoo import _, api, fields, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +class OpenAISessionLog(models.Model): + _name = "openai.vision.session.log" + _description = "OpenAI Vision Session Log" + _order = "create_date desc" + + session_name = fields.Char(string="Session Name", required=True) + refusal_message = fields.Text(string="Refusal Message") + error_message = fields.Text(string="Error Message") + request_payload = fields.Text(string="Request Payload (sanitized)", required=True) + + @api.model + def create_from_session(self, session, error, refusal): + log = self.create({ + "session_name": session.name, + "refusal_message": refusal, + "error_message": str(error), + "request_payload": self._strip_images(session.request_payload) or "{}", + }) + _logger.error(_("OpenAI call failed (Session Name: %s): %s"), session.name, str(error)) + return log + + @api.model + def _strip_images(self, request_payload): + try: + clean_payload = json.loads(request_payload) + except Exception: + return "{}" + input_field = clean_payload.get("input", []) + if isinstance(input_field, list): + for message in input_field: + for part in message.get("content", []): + if isinstance(part, dict) and part.get("type") == "image_url": + part["image_url"] = {"url": "REMOVED_FOR_LOG"} + return json.dumps(clean_payload, indent=2) + diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv index 8830a57c..3ccb382e 100644 --- a/openai_connector/security/ir.model.access.csv +++ b/openai_connector/security/ir.model.access.csv @@ -1,3 +1,3 @@ id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink -access_openai_vision_message,access_openai_vision_message,model_openai_vision_message,,1,1,1,1 access_openai_vision_session,access_openai_vision_session,model_openai_vision_session,,1,1,1,1 +access_openai_vision_session_log,access_openai_vision_session_log,model_openai_vision_session_log,,1,1,1,1 diff --git a/openai_connector/views/openai_vision_message.xml b/openai_connector/views/openai_vision_message.xml deleted file mode 100644 index 24d8063d..00000000 --- a/openai_connector/views/openai_vision_message.xml +++ /dev/null @@ -1,56 +0,0 @@ - - - - openai.vision.message.tree - openai.vision.message - - - - - - - - - - openai.vision.message.form - openai.vision.message - -
- - - - - - - - - -
-
-
- - - Vision Messages - openai.vision.message - tree,form - - - -
diff --git a/openai_connector/views/openai_vision_session.xml b/openai_connector/views/openai_vision_session.xml index 4611636d..f67f9bcf 100644 --- a/openai_connector/views/openai_vision_session.xml +++ b/openai_connector/views/openai_vision_session.xml @@ -7,7 +7,6 @@ - @@ -26,23 +25,10 @@ - - - - - - - - + + + + openai.vision.session.log.tree + openai.vision.session.log + + + + + + + + + + + + openai.vision.session.log.form + openai.vision.session.log + +
+ + + + + + + + + +
+
+
+ + + Vision Session Logs + openai.vision.session.log + tree,form + + + + diff --git a/product_name_creater/__init__.py b/product_name_creater/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/product_name_creater/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/product_name_creater/__manifest__.py b/product_name_creater/__manifest__.py new file mode 100644 index 00000000..2ecf7015 --- /dev/null +++ b/product_name_creater/__manifest__.py @@ -0,0 +1,12 @@ +{ + "name": "Product Name Creater", + "version": "15.0.1.0.0", + "author": "Quatile", + "license": "AGPL-3", + "website": "https://www.quartile.co", + "depends": ["openai_connector"], + "data": [ + "data/openai_vision_session_product_name_creater.xml", + ], + "installable": True, +} diff --git a/product_name_creater/data/openai_vision_session_product_name_creater.xml b/product_name_creater/data/openai_vision_session_product_name_creater.xml new file mode 100644 index 00000000..a93bbfe3 --- /dev/null +++ b/product_name_creater/data/openai_vision_session_product_name_creater.xml @@ -0,0 +1,29 @@ + + + + + + Product Name Creater + gpt-4o + 1 + Please create product name. + 1 + + + + 1 + 0 + + + + diff --git a/product_name_creater/models/__init__.py b/product_name_creater/models/__init__.py new file mode 100644 index 00000000..aaf38a16 --- /dev/null +++ b/product_name_creater/models/__init__.py @@ -0,0 +1 @@ +from . import ir_attachment diff --git a/product_name_creater/models/ir_attachment.py b/product_name_creater/models/ir_attachment.py new file mode 100644 index 00000000..8e5a8b75 --- /dev/null +++ b/product_name_creater/models/ir_attachment.py @@ -0,0 +1,38 @@ +#example how to use this module +import base64 +import json +from odoo.exceptions import UserError + +from odoo import api, models + + +class IrAttachment(models.Model): + _inherit = "ir.attachment" + + @api.model + def analyze_with_openai_vision( + self, + attachment_id, + ): + attachment = self.browse(attachment_id) + if not attachment or not attachment.datas: + return "There is no attachment" + + image_data = base64.b64decode(attachment.datas) + base64_str = base64.b64encode(image_data).decode("utf-8") + input_image = f"data:{attachment.mimetype};base64,{base64_str}" + session = self.env.ref("product_name_creater.openai_vision_session_product_name_creater") + session.inputs = json.dumps([ + { + "role": "user", + "content": [ + {"type": "input_image", "image_url": input_image} + ] + } + ], indent=2) + + response_json_str = session.call_openAI() + response_json = json.loads(response_json_str) + product_name = response_json.get("product_name") + + return product_name diff --git a/setup/product_name_creater/odoo/addons/product_name_creater b/setup/product_name_creater/odoo/addons/product_name_creater new file mode 120000 index 00000000..984ffeeb --- /dev/null +++ b/setup/product_name_creater/odoo/addons/product_name_creater @@ -0,0 +1 @@ +../../../../product_name_creater \ No newline at end of file diff --git a/setup/product_name_creater/setup.py b/setup/product_name_creater/setup.py new file mode 100644 index 00000000..28c57bb6 --- /dev/null +++ b/setup/product_name_creater/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +) From a5522cabacad5d4a4c43edbc645242d3136f2a46 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Fri, 13 Jun 2025 08:13:07 +0000 Subject: [PATCH 06/61] delete openai_vission_session_log model --- openai_connector/__manifest__.py | 1 - openai_connector/models/__init__.py | 1 - .../models/openai_vision_session.py | 17 +------ .../models/openai_vision_session_log.py | 42 ----------------- openai_connector/security/ir.model.access.csv | 1 - .../views/openai_vision_session_log.xml | 46 ------------------- product_name_creater/__manifest__.py | 2 +- product_name_creater/models/ir_attachment.py | 10 +--- 8 files changed, 4 insertions(+), 116 deletions(-) delete mode 100644 openai_connector/models/openai_vision_session_log.py delete mode 100644 openai_connector/views/openai_vision_session_log.xml diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py index 65fafd08..233cd5d6 100644 --- a/openai_connector/__manifest__.py +++ b/openai_connector/__manifest__.py @@ -9,7 +9,6 @@ "security/ir.model.access.csv", "views/res_config_settings_views.xml", "views/openai_vision_session.xml", - "views/openai_vision_session_log.xml", ], "external_dependencies": { "python": ["openai"], diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py index 9733d8c3..5528ee5f 100644 --- a/openai_connector/models/__init__.py +++ b/openai_connector/models/__init__.py @@ -1,4 +1,3 @@ from . import res_company from . import res_config_settings from . import openai_vision_session -from . import openai_vision_session_log diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index a1b24630..718f301b 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -77,17 +77,12 @@ def _compute_request_payload(self): "schema": schema, } } - if self.previous_response_id: request_payload["previous_response_id"] = self.previous_response_id - self.request_payload = json.dumps(request_payload, indent=2) - except Exception as e: - _logger.warning( - "Failed to compute request_payload for session %s: %s", self.name, e - ) - self.request_payload = "{}" + raise UserError(_("Failed to compute request_payload for session %s: %s", self.name, e)) + @api.model def call_openAI(self): @@ -100,21 +95,13 @@ def call_openAI(self): try: payload = json.loads(self.request_payload) response = client.responses.create(**payload) - if self.store_response: self.previous_response_id = getattr(response, "id", False) - refusal = getattr(response, "refusal_reason", None) if refusal: - self.create_error_log("Refused by OpenAI", refusal) raise UserError(_("OpenAI refused the request:\n{}").format(refusal)) output = getattr(response, "output_text", "") or "" return output.strip() except Exception as e: - self.create_error_log(e, refusal) raise UserError(_("OpenAI error:\n{}").format(e)) from e - - @api.model - def create_error_log(self, error, refusal): - self.env["openai.vision.session.log"].create_from_session(self, error, refusal) diff --git a/openai_connector/models/openai_vision_session_log.py b/openai_connector/models/openai_vision_session_log.py deleted file mode 100644 index 1c7b318e..00000000 --- a/openai_connector/models/openai_vision_session_log.py +++ /dev/null @@ -1,42 +0,0 @@ -import json -import logging -from odoo import _, api, fields, models -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - -class OpenAISessionLog(models.Model): - _name = "openai.vision.session.log" - _description = "OpenAI Vision Session Log" - _order = "create_date desc" - - session_name = fields.Char(string="Session Name", required=True) - refusal_message = fields.Text(string="Refusal Message") - error_message = fields.Text(string="Error Message") - request_payload = fields.Text(string="Request Payload (sanitized)", required=True) - - @api.model - def create_from_session(self, session, error, refusal): - log = self.create({ - "session_name": session.name, - "refusal_message": refusal, - "error_message": str(error), - "request_payload": self._strip_images(session.request_payload) or "{}", - }) - _logger.error(_("OpenAI call failed (Session Name: %s): %s"), session.name, str(error)) - return log - - @api.model - def _strip_images(self, request_payload): - try: - clean_payload = json.loads(request_payload) - except Exception: - return "{}" - input_field = clean_payload.get("input", []) - if isinstance(input_field, list): - for message in input_field: - for part in message.get("content", []): - if isinstance(part, dict) and part.get("type") == "image_url": - part["image_url"] = {"url": "REMOVED_FOR_LOG"} - return json.dumps(clean_payload, indent=2) - diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv index 3ccb382e..0bd78ae8 100644 --- a/openai_connector/security/ir.model.access.csv +++ b/openai_connector/security/ir.model.access.csv @@ -1,3 +1,2 @@ id,name,model_id:id,group_id,perm_read,perm_write,perm_create,perm_unlink access_openai_vision_session,access_openai_vision_session,model_openai_vision_session,,1,1,1,1 -access_openai_vision_session_log,access_openai_vision_session_log,model_openai_vision_session_log,,1,1,1,1 diff --git a/openai_connector/views/openai_vision_session_log.xml b/openai_connector/views/openai_vision_session_log.xml deleted file mode 100644 index 9255cb60..00000000 --- a/openai_connector/views/openai_vision_session_log.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - openai.vision.session.log.tree - openai.vision.session.log - - - - - - - - - - - - openai.vision.session.log.form - openai.vision.session.log - -
- - - - - - - - - -
-
-
- - - Vision Session Logs - openai.vision.session.log - tree,form - - - -
diff --git a/product_name_creater/__manifest__.py b/product_name_creater/__manifest__.py index 2ecf7015..6406c6a1 100644 --- a/product_name_creater/__manifest__.py +++ b/product_name_creater/__manifest__.py @@ -4,7 +4,7 @@ "author": "Quatile", "license": "AGPL-3", "website": "https://www.quartile.co", - "depends": ["openai_connector"], + "depends": ["openai_connector", "queue_job"], "data": [ "data/openai_vision_session_product_name_creater.xml", ], diff --git a/product_name_creater/models/ir_attachment.py b/product_name_creater/models/ir_attachment.py index 8e5a8b75..e71ee043 100644 --- a/product_name_creater/models/ir_attachment.py +++ b/product_name_creater/models/ir_attachment.py @@ -5,19 +5,13 @@ from odoo import api, models - class IrAttachment(models.Model): _inherit = "ir.attachment" - @api.model - def analyze_with_openai_vision( - self, - attachment_id, - ): + def update_product_name(self, attachment_id): attachment = self.browse(attachment_id) if not attachment or not attachment.datas: return "There is no attachment" - image_data = base64.b64decode(attachment.datas) base64_str = base64.b64encode(image_data).decode("utf-8") input_image = f"data:{attachment.mimetype};base64,{base64_str}" @@ -30,9 +24,7 @@ def analyze_with_openai_vision( ] } ], indent=2) - response_json_str = session.call_openAI() response_json = json.loads(response_json_str) product_name = response_json.get("product_name") - return product_name From 894acb6dda64897c80cd65c06b455aa0bfe845b3 Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Mon, 16 Jun 2025 06:06:07 +0000 Subject: [PATCH 07/61] adj --- .../models/openai_vision_session.py | 5 +- .../views/openai_vision_session.xml | 15 +- product_name_creater/__manifest__.py | 12 - product_name_creater/models/__init__.py | 1 - product_name_creater/models/ir_attachment.py | 30 -- product_name_generator/README.rst | 69 +++ .../__init__.py | 0 product_name_generator/__manifest__.py | 16 + .../data/session.xml | 7 +- product_name_generator/models/__init__.py | 1 + .../models/product_image.py | 51 +++ product_name_generator/readme/CONFIGURE.md | 2 + product_name_generator/readme/DESCRIPTION.md | 2 + product_name_generator/readme/USAGE.md | 3 + .../static/description/index.html | 424 ++++++++++++++++++ .../views/product_image_views.xml | 22 + .../odoo/addons/product_name_creater | 1 - .../odoo/addons/product_name_generator | 1 + .../setup.py | 0 19 files changed, 607 insertions(+), 55 deletions(-) delete mode 100644 product_name_creater/__manifest__.py delete mode 100644 product_name_creater/models/__init__.py delete mode 100644 product_name_creater/models/ir_attachment.py create mode 100644 product_name_generator/README.rst rename {product_name_creater => product_name_generator}/__init__.py (100%) create mode 100644 product_name_generator/__manifest__.py rename product_name_creater/data/openai_vision_session_product_name_creater.xml => product_name_generator/data/session.xml (77%) create mode 100644 product_name_generator/models/__init__.py create mode 100644 product_name_generator/models/product_image.py create mode 100644 product_name_generator/readme/CONFIGURE.md create mode 100644 product_name_generator/readme/DESCRIPTION.md create mode 100644 product_name_generator/readme/USAGE.md create mode 100644 product_name_generator/static/description/index.html create mode 100644 product_name_generator/views/product_image_views.xml delete mode 120000 setup/product_name_creater/odoo/addons/product_name_creater create mode 120000 setup/product_name_generator/odoo/addons/product_name_generator rename setup/{product_name_creater => product_name_generator}/setup.py (100%) diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index 718f301b..a8511f55 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -81,8 +81,9 @@ def _compute_request_payload(self): request_payload["previous_response_id"] = self.previous_response_id self.request_payload = json.dumps(request_payload, indent=2) except Exception as e: - raise UserError(_("Failed to compute request_payload for session %s: %s", self.name, e)) - + raise UserError( + _("Failed to compute request_payload for session %s: %s", self.name, e) + ) @api.model def call_openAI(self): diff --git a/openai_connector/views/openai_vision_session.xml b/openai_connector/views/openai_vision_session.xml index f67f9bcf..8feda55c 100644 --- a/openai_connector/views/openai_vision_session.xml +++ b/openai_connector/views/openai_vision_session.xml @@ -50,6 +50,14 @@ tree,form + + - diff --git a/product_name_creater/__manifest__.py b/product_name_creater/__manifest__.py deleted file mode 100644 index 6406c6a1..00000000 --- a/product_name_creater/__manifest__.py +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "Product Name Creater", - "version": "15.0.1.0.0", - "author": "Quatile", - "license": "AGPL-3", - "website": "https://www.quartile.co", - "depends": ["openai_connector", "queue_job"], - "data": [ - "data/openai_vision_session_product_name_creater.xml", - ], - "installable": True, -} diff --git a/product_name_creater/models/__init__.py b/product_name_creater/models/__init__.py deleted file mode 100644 index aaf38a16..00000000 --- a/product_name_creater/models/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import ir_attachment diff --git a/product_name_creater/models/ir_attachment.py b/product_name_creater/models/ir_attachment.py deleted file mode 100644 index e71ee043..00000000 --- a/product_name_creater/models/ir_attachment.py +++ /dev/null @@ -1,30 +0,0 @@ -#example how to use this module -import base64 -import json -from odoo.exceptions import UserError - -from odoo import api, models - -class IrAttachment(models.Model): - _inherit = "ir.attachment" - - def update_product_name(self, attachment_id): - attachment = self.browse(attachment_id) - if not attachment or not attachment.datas: - return "There is no attachment" - image_data = base64.b64decode(attachment.datas) - base64_str = base64.b64encode(image_data).decode("utf-8") - input_image = f"data:{attachment.mimetype};base64,{base64_str}" - session = self.env.ref("product_name_creater.openai_vision_session_product_name_creater") - session.inputs = json.dumps([ - { - "role": "user", - "content": [ - {"type": "input_image", "image_url": input_image} - ] - } - ], indent=2) - response_json_str = session.call_openAI() - response_json = json.loads(response_json_str) - product_name = response_json.get("product_name") - return product_name diff --git a/product_name_generator/README.rst b/product_name_generator/README.rst new file mode 100644 index 00000000..a2de320a --- /dev/null +++ b/product_name_generator/README.rst @@ -0,0 +1,69 @@ +====================== +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 allows generating the product name by calling the OpenAI +API. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +This module depends on the 'openai_connector'. Therefore, please adhere +to the configuration steps outlined in this module. + +Usage +===== + +- Go to the Product form. +- Click on one of the Extra Product Media items. +- Click the "Generate Product Name" button. + +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_creater/__init__.py b/product_name_generator/__init__.py similarity index 100% rename from product_name_creater/__init__.py rename to product_name_generator/__init__.py diff --git a/product_name_generator/__manifest__.py b/product_name_generator/__manifest__.py new file mode 100644 index 00000000..c486bcf1 --- /dev/null +++ b/product_name_generator/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License LGPL-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", "queue_job", "openai_connector"], + "data": [ + "data/session.xml", + "views/product_image_views.xml", + ], + "installable": True, +} diff --git a/product_name_creater/data/openai_vision_session_product_name_creater.xml b/product_name_generator/data/session.xml similarity index 77% rename from product_name_creater/data/openai_vision_session_product_name_creater.xml rename to product_name_generator/data/session.xml index a93bbfe3..c9955164 100644 --- a/product_name_creater/data/openai_vision_session_product_name_creater.xml +++ b/product_name_generator/data/session.xml @@ -2,8 +2,11 @@ - - Product Name Creater + + Product Name Generator gpt-4o 1 Please create product name. diff --git a/product_name_generator/models/__init__.py b/product_name_generator/models/__init__.py new file mode 100644 index 00000000..702d8f34 --- /dev/null +++ b/product_name_generator/models/__init__.py @@ -0,0 +1 @@ +from . import product_image diff --git a/product_name_generator/models/product_image.py b/product_name_generator/models/product_image.py new file mode 100644 index 00000000..58ef8c3c --- /dev/null +++ b/product_name_generator/models/product_image.py @@ -0,0 +1,51 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +import base64 +import json + +from odoo import _, models +from odoo.exceptions import UserError + +IMAGE_TYPES = ["image/png", "image/jpeg", "image/bmp", "image/tiff"] + + +class ProductImage(models.Model): + _inherit = "product.image" + + def call_openAI(self, session): + response_json_str = session.call_openAI() + response_json = json.loads(response_json_str) + product_name = response_json.get("product_name") + self.product_tmpl_id.name = product_name + + def action_generate_product_name(self): + if not self.image_1920: + raise UserError(_("Please upload the image first.")) + attachment = self.env["ir.attachment"].search( + [ + ("res_id", "=", self.id), + ("res_model", "=", "product.image"), + ("res_field", "=", "image_1920"), + ("mimetype", "in", IMAGE_TYPES), + ], + limit=1, + ) + if not attachment: + raise UserError(_("There is no attachment for this product image.")) + image_data = base64.b64decode(attachment.datas) + base64_str = base64.b64encode(image_data).decode("utf-8") + input_image = f"data:{attachment.mimetype};base64,{base64_str}" + session = self.env.ref( + "product_name_generator.openai_vision_session_product_name_generator" + ) + session.inputs = json.dumps( + [ + { + "role": "user", + "content": [{"type": "input_image", "image_url": input_image}], + } + ], + indent=2, + ) + self.with_delay().call_openAI(session) diff --git a/product_name_generator/readme/CONFIGURE.md b/product_name_generator/readme/CONFIGURE.md new file mode 100644 index 00000000..a3d6b0ec --- /dev/null +++ b/product_name_generator/readme/CONFIGURE.md @@ -0,0 +1,2 @@ +This module depends on the 'openai_connector'. Therefore, please adhere +to the configuration steps outlined in this module. diff --git a/product_name_generator/readme/DESCRIPTION.md b/product_name_generator/readme/DESCRIPTION.md new file mode 100644 index 00000000..9c101f7e --- /dev/null +++ b/product_name_generator/readme/DESCRIPTION.md @@ -0,0 +1,2 @@ +This module allows generating the product name by calling the OpenAI +API. diff --git a/product_name_generator/readme/USAGE.md b/product_name_generator/readme/USAGE.md new file mode 100644 index 00000000..dbcca6fe --- /dev/null +++ b/product_name_generator/readme/USAGE.md @@ -0,0 +1,3 @@ +- Go to the Product form. +- Click on one of the Extra Product Media items. +- Click the "Generate Product Name" button. diff --git a/product_name_generator/static/description/index.html b/product_name_generator/static/description/index.html new file mode 100644 index 00000000..6aec0a8b --- /dev/null +++ b/product_name_generator/static/description/index.html @@ -0,0 +1,424 @@ + + + + + +Product Name Generator + + + +
+

Product Name Generator

+ + +

Beta License: AGPL-3 qrtl/rmm-custom

+

This module allows generating the product name by calling the OpenAI +API.

+

Table of contents

+ +
+

Configuration

+

This module depends on the ‘openai_connector’. Therefore, please adhere +to the configuration steps outlined in this module.

+
+
+

Usage

+
    +
  • Go to the Product form.
  • +
  • Click on one of the Extra Product Media items.
  • +
  • Click the “Generate Product Name” button.
  • +
+
+
+

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_image_views.xml b/product_name_generator/views/product_image_views.xml new file mode 100644 index 00000000..5dff9f39 --- /dev/null +++ b/product_name_generator/views/product_image_views.xml @@ -0,0 +1,22 @@ + + + product.image.view.form.inherit.custom.button + product.image + + + +
+
+
+
+
+
diff --git a/setup/product_name_creater/odoo/addons/product_name_creater b/setup/product_name_creater/odoo/addons/product_name_creater deleted file mode 120000 index 984ffeeb..00000000 --- a/setup/product_name_creater/odoo/addons/product_name_creater +++ /dev/null @@ -1 +0,0 @@ -../../../../product_name_creater \ No newline at end of file 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_creater/setup.py b/setup/product_name_generator/setup.py similarity index 100% rename from setup/product_name_creater/setup.py rename to setup/product_name_generator/setup.py From f8a00e43020a859cfcb16a831220250fa2e0156c Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 16 Jun 2025 09:47:01 +0000 Subject: [PATCH 08/61] add readme openai_connector and fix some ci error --- openai_connector/README.rst | 77 ++++ .../models/openai_vision_session.py | 8 +- openai_connector/readme/CONFIGURE.rst | 10 + openai_connector/readme/DESCRIPTION.rst | 10 + .../static/description/index.html | 423 ++++++++++++++++++ product_name_generator/data/session.xml | 5 +- .../models/product_image.py | 2 +- 7 files changed, 528 insertions(+), 7 deletions(-) create mode 100644 openai_connector/README.rst create mode 100644 openai_connector/readme/CONFIGURE.rst create mode 100644 openai_connector/readme/DESCRIPTION.rst create mode 100644 openai_connector/static/description/index.html diff --git a/openai_connector/README.rst b/openai_connector/README.rst new file mode 100644 index 00000000..3b1e574a --- /dev/null +++ b/openai_connector/README.rst @@ -0,0 +1,77 @@ +================ +Openai Connector +================ + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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-OCA%2Frmm--custom-lightgray.png?logo=github + :target: https://github.com/OCA/rmm-custom/tree/15.0/openai_connector + :alt: OCA/rmm-custom +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/rmm-custom-15-0/rmm-custom-15-0-openai_connector + :alt: Translate me on Weblate + +|badge1| |badge2| |badge3| |badge4| + +This module defines openai.vision.session to call OpenAI’s GPT-4o with structured +payloads. It supports image/text inputs, web search, and optional JSON schema output. +Designed to be used by other modules, not standalone. Use call_openAI() to trigger +requests. Enable store_response to keep previous_response_id for chaining. + +Using a job queue (e.g., queue.job) is recommended for asynchronous execution. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +Set your company’s openai_api_key in Odoo +Install the OpenAI Python SDK: pip install openai +See official configuration + +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 smashing 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 maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/rmm-custom `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index a8511f55..86d7e8fa 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -82,8 +82,12 @@ def _compute_request_payload(self): self.request_payload = json.dumps(request_payload, indent=2) except Exception as e: raise UserError( - _("Failed to compute request_payload for session %s: %s", self.name, e) - ) + _("Failed to compute request_payload for session %(id)s: %(error)s") + % { + "id": self.id, + "error": e, + } + ) from e @api.model def call_openAI(self): diff --git a/openai_connector/readme/CONFIGURE.rst b/openai_connector/readme/CONFIGURE.rst new file mode 100644 index 00000000..7dad4bf8 --- /dev/null +++ b/openai_connector/readme/CONFIGURE.rst @@ -0,0 +1,10 @@ +To use this module, configure the OpenAI API key under the Company settings in Odoo. + +The official Python SDK from OpenAI is required. Install it using: + +.. code-block:: bash + + pip install openai + +For additional options and usage details, refer to the official documentation: +https://platform.openai.com/docs diff --git a/openai_connector/readme/DESCRIPTION.rst b/openai_connector/readme/DESCRIPTION.rst new file mode 100644 index 00000000..339e6fe0 --- /dev/null +++ b/openai_connector/readme/DESCRIPTION.rst @@ -0,0 +1,10 @@ +This module defines `openai.vision.session`, which enables programmatic access to OpenAI’s +GPT-4o model using structured payloads. 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. +Requests can be triggered using the method `call_openAI()`. When `store_response` is enabled, +the session retains the `previous_response_id` to support continuation of conversations. + +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/static/description/index.html b/openai_connector/static/description/index.html new file mode 100644 index 00000000..6eddbb27 --- /dev/null +++ b/openai_connector/static/description/index.html @@ -0,0 +1,423 @@ + + + + + + +Openai Connector + + + +
+

Openai Connector

+ + +

Beta License: AGPL-3 OCA/rmm-custom Translate me on Weblate

+

This module defines openai.vision.session to call OpenAI’s GPT-4o with structured +payloads. It supports image/text inputs, web search, and optional JSON schema output. +Designed to be used by other modules, not standalone. Use call_openAI() to trigger +requests. Enable store_response to keep previous_response_id for chaining.

+

Using a job queue (e.g., queue.job) is recommended for asynchronous execution.

+

Table of contents

+ +
+

Configuration

+

Set your company’s openai_api_key in Odoo +Install the OpenAI Python SDK: pip install openai +See official configuration

+
+
+

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 smashing 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 maintained by the OCA.

+Odoo Community Association +

OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use.

+

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

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/product_name_generator/data/session.xml b/product_name_generator/data/session.xml index c9955164..3f1df6dc 100644 --- a/product_name_generator/data/session.xml +++ b/product_name_generator/data/session.xml @@ -1,7 +1,5 @@ - - - + 1
0 - diff --git a/product_name_generator/models/product_image.py b/product_name_generator/models/product_image.py index 58ef8c3c..37c304cb 100644 --- a/product_name_generator/models/product_image.py +++ b/product_name_generator/models/product_image.py @@ -17,7 +17,7 @@ def call_openAI(self, session): response_json_str = session.call_openAI() response_json = json.loads(response_json_str) product_name = response_json.get("product_name") - self.product_tmpl_id.name = product_name + self.with_context(lang="ja_JP").product_tmpl_id.name = product_name def action_generate_product_name(self): if not self.image_1920: From f8ea52dc6b31eb419b1822c71092e4bfcec79e38 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Mon, 16 Jun 2025 10:09:32 +0000 Subject: [PATCH 09/61] fix product_name_generator data error --- product_name_generator/data/session.xml | 30 ++++++++++++------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/product_name_generator/data/session.xml b/product_name_generator/data/session.xml index 3f1df6dc..13517844 100644 --- a/product_name_generator/data/session.xml +++ b/product_name_generator/data/session.xml @@ -1,15 +1,14 @@ - - - Product Name Generator - gpt-4o - 1 - Please create product name. - 1 - + + Product Name Generator + gpt-4o + 1 + Please create product name. + 1 + - - 1 - 0 - - +
+ 1 + 0 + From f21583e5fec0ba8f4644ba15e1a5123f9a822777 Mon Sep 17 00:00:00 2001 From: nobuQuartile Date: Wed, 18 Jun 2025 06:26:54 +0000 Subject: [PATCH 10/61] fix --- openai_connector/README.rst | 70 +++++++++--------- openai_connector/__manifest__.py | 7 +- .../models/openai_vision_session.py | 30 ++++---- openai_connector/readme/CONFIGURE.md | 11 +++ openai_connector/readme/CONFIGURE.rst | 10 --- openai_connector/readme/DESCRIPTION.md | 12 ++++ openai_connector/readme/DESCRIPTION.rst | 10 --- .../static/description/index.html | 72 ++++++++++--------- ...ion.xml => openai_vision_session_view.xml} | 5 -- .../models/product_image.py | 20 ++---- 10 files changed, 127 insertions(+), 120 deletions(-) create mode 100644 openai_connector/readme/CONFIGURE.md delete mode 100644 openai_connector/readme/CONFIGURE.rst create mode 100644 openai_connector/readme/DESCRIPTION.md delete mode 100644 openai_connector/readme/DESCRIPTION.rst rename openai_connector/views/{openai_vision_session.xml => openai_vision_session_view.xml} (99%) diff --git a/openai_connector/README.rst b/openai_connector/README.rst index 3b1e574a..1e9e991e 100644 --- a/openai_connector/README.rst +++ b/openai_connector/README.rst @@ -2,10 +2,13 @@ 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 @@ -13,21 +16,24 @@ Openai Connector .. |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-OCA%2Frmm--custom-lightgray.png?logo=github - :target: https://github.com/OCA/rmm-custom/tree/15.0/openai_connector - :alt: OCA/rmm-custom -.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/rmm-custom-15-0/rmm-custom-15-0-openai_connector - :alt: Translate me on Weblate +.. |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| -|badge1| |badge2| |badge3| |badge4| +This module defines openai.vision.session, which enables programmatic +access to OpenAI’s GPT-4o model using structured payloads. It supports +multimodal input (images and text), optional web search, and structured +output via JSON Schema. -This module defines openai.vision.session to call OpenAI’s GPT-4o with structured -payloads. It supports image/text inputs, web search, and optional JSON schema output. -Designed to be used by other modules, not standalone. Use call_openAI() to trigger -requests. Enable store_response to keep previous_response_id for chaining. +The module is designed as a backend service and is intended to be +invoked by other modules. Requests can be triggered using the method +call_openAI(). When store_response is enabled, the session retains the +previous_response_id to support continuation of conversations. -Using a job queue (e.g., queue.job) is recommended for asynchronous execution. +It is advised to delegate execution to background job queues (e.g., +queue.job) for asynchronous and fault-tolerant processing. **Table of contents** @@ -37,17 +43,25 @@ Using a job queue (e.g., queue.job) is recommended for asynchronous execution. Configuration ============= -Set your company’s openai_api_key in Odoo -Install the OpenAI Python SDK: pip install openai -See official configuration +To use this module, configure the OpenAI API key under the Company +settings in Odoo. + +The official Python SDK from OpenAI is required. Install it using: + +.. code:: bash + + pip install openai + +For additional options and usage details, refer to the official +documentation: https://platform.openai.com/docs Bug Tracker =========== -Bugs are tracked on `GitHub Issues `_. +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 smashing it by providing a detailed and welcomed -`feedback `_. +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. @@ -55,23 +69,13 @@ Credits ======= Authors -~~~~~~~ +------- * Quatile Maintainers -~~~~~~~~~~~ - -This module is maintained by the OCA. - -.. image:: https://odoo-community.org/logo.png - :alt: Odoo Community Association - :target: https://odoo-community.org - -OCA, or the Odoo Community Association, is a nonprofit organization whose -mission is to support the collaborative development of Odoo features and -promote its widespread use. +----------- -This module is part of the `OCA/rmm-custom `_ project on GitHub. +This module is part of the `qrtl/rmm-custom `_ project on GitHub. -You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. +You are welcome to contribute. diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py index 233cd5d6..06c45836 100644 --- a/openai_connector/__manifest__.py +++ b/openai_connector/__manifest__.py @@ -1,14 +1,17 @@ +# Copyright 2025 Quartile (https://www.quartile.co) +# License LGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + { "name": "Openai Connector", "version": "15.0.1.0.0", "author": "Quatile", "license": "AGPL-3", "website": "https://www.quartile.co", - "depends": ["base_setup", "web"], + "depends": ["web"], "data": [ "security/ir.model.access.csv", "views/res_config_settings_views.xml", - "views/openai_vision_session.xml", + "views/openai_vision_session_view.xml", ], "external_dependencies": { "python": ["openai"], diff --git a/openai_connector/models/openai_vision_session.py b/openai_connector/models/openai_vision_session.py index 86d7e8fa..759c1313 100644 --- a/openai_connector/models/openai_vision_session.py +++ b/openai_connector/models/openai_vision_session.py @@ -9,7 +9,7 @@ _logger = logging.getLogger(__name__) -class OpenAISession(models.Model): +class OpenAIVisionSession(models.Model): _name = "openai.vision.session" _description = "OpenAI Vision Session" @@ -21,14 +21,20 @@ class OpenAISession(models.Model): default="gpt-4o", required=True, ) - temperature = fields.Float(default=0.7) - instruction = fields.Text() + 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." + "\nExample: 'You are a helpful assistant that answers in Japanese." + ) inputs = fields.Text() previous_response_id = fields.Char(string="Previous Response ID") store_response = fields.Boolean(default=True) - web_search = fields.Boolean(string="Use Web Search", default=False) + web_search = fields.Boolean(string="Use Web Search") response_format_enabled = fields.Boolean( - string="Use Structured Output (JSON Schema)", default=False + string="Use Structured Output (JSON Schema)" ) response_format_schema = fields.Text( string="Response Format Schema (JSON)", @@ -45,7 +51,7 @@ class OpenAISession(models.Model): ) request_payload = fields.Text(compute="_compute_request_payload", store=False) - @api.onchange("response_format_schema") + @api.constrains("response_format_schema") def _onchange_response_format_schema(self): for record in self: if record.response_format_enabled: @@ -57,17 +63,15 @@ def _onchange_response_format_schema(self): ) from e def _compute_request_payload(self): - self.ensure_one() try: request_payload = { "model": self.model, "instructions": self.instruction or "", - "input": json.loads(self.inputs or "[]"), + "input": self.inputs 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"] = { @@ -94,12 +98,11 @@ def call_openAI(self): self.ensure_one() api_key = self.env.company.openai_api_key if not api_key: - raise UserError(_("OpenAI API key is not configured")) - client = OpenAI(api_key=api_key) - refusal = False + raise UserError(_("OpenAI API key is not configured.")) + openai_client = OpenAI(api_key=api_key) try: payload = json.loads(self.request_payload) - response = client.responses.create(**payload) + response = openai_client.responses.create(**payload) if self.store_response: self.previous_response_id = getattr(response, "id", False) refusal = getattr(response, "refusal_reason", None) @@ -107,6 +110,5 @@ def call_openAI(self): raise UserError(_("OpenAI refused the request:\n{}").format(refusal)) output = getattr(response, "output_text", "") or "" return output.strip() - except Exception as e: raise UserError(_("OpenAI error:\n{}").format(e)) from e diff --git a/openai_connector/readme/CONFIGURE.md b/openai_connector/readme/CONFIGURE.md new file mode 100644 index 00000000..56f3757b --- /dev/null +++ b/openai_connector/readme/CONFIGURE.md @@ -0,0 +1,11 @@ +To use this module, configure the OpenAI API key under the Company +settings in Odoo. + +The official Python SDK from OpenAI is required. Install it using: + +``` bash +pip install openai +``` + +For additional options and usage details, refer to the official +documentation: diff --git a/openai_connector/readme/CONFIGURE.rst b/openai_connector/readme/CONFIGURE.rst deleted file mode 100644 index 7dad4bf8..00000000 --- a/openai_connector/readme/CONFIGURE.rst +++ /dev/null @@ -1,10 +0,0 @@ -To use this module, configure the OpenAI API key under the Company settings in Odoo. - -The official Python SDK from OpenAI is required. Install it using: - -.. code-block:: bash - - pip install openai - -For additional options and usage details, refer to the official documentation: -https://platform.openai.com/docs diff --git a/openai_connector/readme/DESCRIPTION.md b/openai_connector/readme/DESCRIPTION.md new file mode 100644 index 00000000..531937ec --- /dev/null +++ b/openai_connector/readme/DESCRIPTION.md @@ -0,0 +1,12 @@ +This module defines openai.vision.session, which enables programmatic +access to OpenAI’s GPT-4o model using structured payloads. 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. Requests can be triggered using the method +call_openAI(). When store_response is enabled, the session retains the +previous_response_id to support continuation of conversations. + +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/readme/DESCRIPTION.rst b/openai_connector/readme/DESCRIPTION.rst deleted file mode 100644 index 339e6fe0..00000000 --- a/openai_connector/readme/DESCRIPTION.rst +++ /dev/null @@ -1,10 +0,0 @@ -This module defines `openai.vision.session`, which enables programmatic access to OpenAI’s -GPT-4o model using structured payloads. 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. -Requests can be triggered using the method `call_openAI()`. When `store_response` is enabled, -the session retains the `previous_response_id` to support continuation of conversations. - -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/static/description/index.html b/openai_connector/static/description/index.html index 6eddbb27..c4c38e7e 100644 --- a/openai_connector/static/description/index.html +++ b/openai_connector/static/description/index.html @@ -1,20 +1,19 @@ - - + Openai Connector