diff --git a/openwebui_integration/__init__.py b/openwebui_integration/__init__.py new file mode 100644 index 0000000..f7209b1 --- /dev/null +++ b/openwebui_integration/__init__.py @@ -0,0 +1,2 @@ +from . import models +from . import controllers diff --git a/openwebui_integration/__manifest__.py b/openwebui_integration/__manifest__.py new file mode 100644 index 0000000..a25db3f --- /dev/null +++ b/openwebui_integration/__manifest__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. +{ + 'name': 'OpenWebUI Integration', + 'version': '1.0', + 'summary': 'Integration with OpenWebUI for chatbots', + 'sequence': 10, + 'description': """ +OpenWebUI Integration +===================== +This module provides integration with OpenWebUI to create and manage chatbots. + +Features: +--------- +* Connect to OpenWebUI API +* Manage models and configurations +* Create chatbots with specific behaviors +* Control access to bots by channels and users +""", + 'category': 'Discuss', + 'website': 'https://www.odoo.com/app/discuss', + 'depends': ['mail'], + 'data': [ + 'security/security.xml', + 'security/ir.model.access.csv', + 'views/res_company_views.xml', + 'views/openwebui_bot_views.xml', + ], + 'installable': True, + 'application': False, + 'auto_install': False, + 'license': 'LGPL-3', +} diff --git a/openwebui_integration/controllers/__init__.py b/openwebui_integration/controllers/__init__.py new file mode 100644 index 0000000..12a7e52 --- /dev/null +++ b/openwebui_integration/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/openwebui_integration/controllers/main.py b/openwebui_integration/controllers/main.py new file mode 100644 index 0000000..89753a1 --- /dev/null +++ b/openwebui_integration/controllers/main.py @@ -0,0 +1,22 @@ +from odoo import http, fields +from odoo.http import request + +class OpenWebUIController(http.Controller): + @http.route('/openwebui/config', type='json', auth='user') + def get_config(self): + company = request.env.company + user_companies = request.env.user.company_ids.ids + + return { + 'version': '1.0', + 'enabled': company.openwebui_enabled, + 'company_id': company.id, + 'allowed_company_ids': user_companies, + 'company_name': company.name, + 'model': company.openwebui_model, + 'api_url': company.openwebui_api_url, + 'max_tokens': company.openwebui_max_tokens, + 'temperature': company.openwebui_temperature, + 'timeout': company.openwebui_timeout, + 'use_ssl': company.openwebui_use_ssl, + } diff --git a/openwebui_integration/models/__init__.py b/openwebui_integration/models/__init__.py new file mode 100644 index 0000000..8c7db0c --- /dev/null +++ b/openwebui_integration/models/__init__.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import openwebui_bot_mixin +from . import openwebui_model +from . import openwebui_bot +from . import discuss_channel +from . import res_company +from . import res_users diff --git a/openwebui_integration/models/discuss_channel.py b/openwebui_integration/models/discuss_channel.py new file mode 100644 index 0000000..3dbf791 --- /dev/null +++ b/openwebui_integration/models/discuss_channel.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, _ + +class DiscussChannel(models.Model): + _inherit = 'discuss.channel' + + bot_id = fields.Many2one( + comodel_name='openwebui.bot', + string='AI Bot', + help="AI Bot associated with this channel" + ) + + def _get_bot_domain(self, company_id): + """Get domain for available bots.""" + return [ + ('company_id', '=', company_id), + ('is_active', '=', True), + ] + + @api.model + def channel_get(self, partners_to): + """Override channel_get to handle bot channels.""" + channel = super().channel_get(partners_to) + + # Check if any of the partners is a bot + if len(partners_to) == 2: # One-to-one chat + bot_partner_ids = self.env['openwebui.bot'].search([('partner_id', 'in', partners_to)]).mapped('partner_id').ids + if bot_partner_ids: + bot = self.env['openwebui.bot'].search([('partner_id', 'in', bot_partner_ids)], limit=1) + if bot: + channel.write({'bot_id': bot.id}) + + return channel + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + """Override message_post to handle bot messages.""" + message = super().message_post(**kwargs) + + # If this is a bot channel, let the bot handle the message + if self.bot_id and message.author_id != self.bot_id.partner_id: + self.bot_id._handle_message(message) + + return message diff --git a/openwebui_integration/models/openwebui_bot.py b/openwebui_integration/models/openwebui_bot.py new file mode 100644 index 0000000..9b65571 --- /dev/null +++ b/openwebui_integration/models/openwebui_bot.py @@ -0,0 +1,318 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +import logging + +_logger = logging.getLogger(__name__) + +class OpenWebUIBot(models.Model): + """OpenWebUI Bot that can be configured and used in channels. + + This class allows to: + - Configure a bot with a name, model and parameters + - Manage permissions (channels and users) + - Send messages to the model with the configured parameters + """ + _name = 'openwebui.bot' + _description = 'OpenWebUI Bot' + _inherit = ['mail.thread', 'mail.activity.mixin', 'openwebui.bot.mixin'] + + name = fields.Char( + string='Name', + required=True, + tracking=True + ) + + description = fields.Text( + string='Description', + tracking=True + ) + + model_id = fields.Many2one( + comodel_name='openwebui.model', + string='Model', + required=True, + tracking=True, + domain="[('company_id', '=', company_id), ('is_active', '=', True), ('is_temp', '=', False)]" + ) + + company_id = fields.Many2one( + comodel_name='res.company', + string='Company', + required=True, + default=lambda self: self.env.company + ) + + partner_id = fields.Many2one( + comodel_name='res.partner', + string='Partner', + readonly=True, + required=True + ) + + is_active = fields.Boolean( + string='Active', + default=True, + tracking=True + ) + + max_tokens = fields.Integer( + string='Max Tokens', + default=2048, + tracking=True, + help="Maximum number of tokens to generate in the response" + ) + + temperature = fields.Float( + string='Temperature', + default=0.7, + tracking=True, + help="Controls the creativity of the responses. Higher values give more creative but potentially less accurate responses." + ) + + channel_ids = fields.One2many( + comodel_name='discuss.channel', + inverse_name='bot_id', + string='Channels' + ) + + user_ids = fields.Many2many( + comodel_name='res.users', + string='Authorized Users', + help="Users authorized to interact with the bot" + ) + + instructions = fields.Text( + string='Instructions', + tracking=True, + help="Specific instructions to guide the bot's behavior" + ) + + context = fields.Text( + string='Context', + tracking=True, + help="Additional context for the bot" + ) + + _sql_constraints = [ + ('name_company_uniq', 'unique(name, company_id)', 'Bot name must be unique per company!') + ] + + @api.constrains('max_tokens') + def _check_max_tokens(self): + """Checks that the number of tokens is valid""" + for bot in self: + if bot.max_tokens < 1: + raise ValidationError("The maximum number of tokens must be greater than 0") + + @api.constrains('temperature') + def _check_temperature(self): + """Checks that the temperature is valid""" + for bot in self: + if not (0 <= bot.temperature <= 2): + raise ValidationError("The temperature must be between 0 and 2") + + def _create_bot_partner(self, name=None, company_id=None): + """Create a partner for the bot.""" + return self.env['res.partner'].sudo().create({ + 'name': name or 'New Bot', + 'email': f"{(name or 'new.bot').lower().replace(' ', '.')}@bot.internal", + 'type': 'contact', + 'company_id': company_id or self.env.company.id, + 'is_company': False, + }) + + @api.model_create_multi + def create(self, vals_list): + """Override create to ensure bot partner is created and discussions initialized with internal users.""" + for vals in vals_list: + if not vals.get('partner_id'): + partner = self._create_bot_partner( + name=vals.get('name'), + company_id=vals.get('company_id', self.env.company.id) + ) + vals['partner_id'] = partner.id + + # Create bots + bots = super().create(vals_list) + + # Initialize discussions for each bot with internal users + for bot in bots: + bot._init_bot_for_users() + + return bots + + def write(self, vals): + """Override write to handle activation/deactivation.""" + res = super().write(vals) + if 'is_active' in vals and vals['is_active']: + for bot in self: + _logger.info("Bot %s (ID: %s) activated, initializing discussions with internal users", bot.name, bot.id) + bot._init_bot_for_users() + return res + + def _init_bot_for_users(self): + """Initialize discussions with all internal users (non-portal, non-public users).""" + self.ensure_one() + if not self.is_active: + return + + # Get all internal users (employees, not portal or public users) + internal_users = self.env['res.users'].sudo().search([ + ('share', '=', False), # Excludes portal and public users + ('active', '=', True), # Only active users + ('partner_id', '!=', False) # Must have a partner + ]) + + _logger.info( + "Initializing discussions for bot %s (ID: %s) with %s internal users", + self.name, self.id, len(internal_users) + ) + + for user in internal_users: + try: + # Create discussion only for internal users + self.init_bot_discussion(user.id) + _logger.debug( + "Created discussion between bot %s and internal user %s (ID: %s)", + self.name, user.name, user.id + ) + except Exception as e: + _logger.error( + "Failed to create discussion between bot %s and user %s (ID: %s): %s", + self.name, user.name, user.id, str(e) + ) + + def init_bot_discussion(self, user_id): + """Initialize one-to-one discussion between the bot and a user.""" + self.ensure_one() + user = self.env['res.users'].browse(user_id) + + # Create or get channel + channel = self.env['discuss.channel'].channel_get([self.partner_id.id, user.partner_id.id]) + + # Send welcome message + welcome_msg = _( + "Hello! I'm %(bot_name)s, your AI assistant. " + "I'm here to help you with any questions or tasks you might have. " + "Feel free to start our conversation!" + ) % {'bot_name': self.name} + + channel.sudo().message_post( + body=welcome_msg, + author_id=self.partner_id.id, + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + + return channel + + def _handle_message(self, message): + """Handle incoming messages in bot channels.""" + self.ensure_one() + + # Get the channel from message's model and res_id + channel = self.env[message.model].browse(message.res_id) if message.model == 'discuss.channel' else None + + _logger.info( + "Received message for bot %s (ID: %s). Message ID: %s, Author: %s, Channel: %s", + self.name, self.id, message.id, + message.author_id.name if message.author_id else 'No Author', + channel.name if channel else 'No Channel' + ) + + # Skip if message is from the bot itself + if message.author_id == self.partner_id: + _logger.info("Skipping message as it's from the bot itself") + return + + # Verify channel + if not channel or message.model != 'discuss.channel': + _logger.warning("Message %s is not from a discuss channel", message.id) + return + + if self.partner_id not in channel.channel_member_ids.partner_id: + _logger.warning( + "Bot %s (ID: %s) is not a member of channel %s", + self.name, self.id, channel.name + ) + return + + try: + _logger.info( + "Processing message with OpenWebUI. Model: %s, Context: %s, Instructions: %s", + self.model_id.identifier, bool(self.context), bool(self.instructions) + ) + + # Process message with OpenWebUI + response = self.model_id.send_message( + message=message.body, + context=self.context, + instructions=self.instructions + ) + + _logger.info("Got response from OpenWebUI: %s", bool(response)) + + if response: + # Post the response + _logger.info("Posting response to channel %s", channel.name) + channel.sudo().message_post( + body=response, + author_id=self.partner_id.id, + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + else: + _logger.warning("Empty response from OpenWebUI") + error_msg = _("I apologize, but I couldn't generate a response at this time.") + channel.sudo().message_post( + body=error_msg, + author_id=self.partner_id.id, + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + + except Exception as e: + _logger.error( + "Error processing message with OpenWebUI: %s\nBot: %s (ID: %s)\nModel: %s", + str(e), self.name, self.id, self.model_id.identifier, + exc_info=True + ) + error_msg = _("I apologize, but I encountered an error processing your message. Please try again later.") + channel.sudo().message_post( + body=error_msg, + author_id=self.partner_id.id, + message_type="comment", + subtype_xmlid="mail.mt_comment", + ) + + def send_message(self, message, context=None): + """Sends a message to the bot and returns its response. + + Args: + message: The message to send + context: Additional context for the conversation + + Returns: + tuple: (success, result) + """ + self.ensure_one() + + if not self.is_active: + return False, "This bot is not active" + + if not self.model_id: + return False, "No model is configured for this bot" + + # Prepare the conversation context + conversation_context = { + 'max_tokens': self.max_tokens, + 'temperature': self.temperature, + } + if context: + conversation_context.update(context) + + # Send the message to the model + return self.model_id.send_message(message, conversation_context) diff --git a/openwebui_integration/models/openwebui_bot_mixin.py b/openwebui_integration/models/openwebui_bot_mixin.py new file mode 100644 index 0000000..8721724 --- /dev/null +++ b/openwebui_integration/models/openwebui_bot_mixin.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, _ + +class OpenWebUIBotMixin(models.AbstractModel): + """Mixin to add OpenWebUI bot functionality to any model. + + This mixin provides methods to: + - Get bot parameters and context + - Send messages to models + - Handle responses + """ + _name = 'openwebui.bot.mixin' + _description = 'OpenWebUI Bot Mixin' + + def _get_bot_context(self, bot): + """Gets the bot context with its parameters""" + return { + 'instructions': bot.instructions, + 'model': bot.model_id, + 'context': bot.context or '', + 'max_tokens': bot.max_tokens, + 'temperature': bot.temperature, + } + + def _apply_logic(self, record, values, command=None): + """Applies the bot logic to a record. + + Args: + record: The record to apply the logic to + values: The values to use for message generation + command: Optional command for the bot + + Returns: + dict: The updated values + """ + bot = values.get('bot') + if not bot or not bot.model_id: + return values + + # Prepare the bot context + bot_context = self._get_bot_context(bot) + + # Generate the message for the bot + message = self._generate_bot_message(record, values, command) + + # Send the message to the model + response = bot_context['model'].send_message( + message=message, + context=bot_context['context'], + max_tokens=bot_context['max_tokens'], + temperature=bot_context['temperature'], + instructions=bot_context['instructions'] + ) + + if response: + # Update the values with the bot response + values = self._process_bot_response(values, response) + + return values + + def _generate_bot_message(self, record, values, command=None): + """Generates the message to send to the bot. + To be overridden in child classes.""" + return '' + + def _process_bot_response(self, values, response): + """Handles the bot response. + To be overridden in child classes.""" + return values diff --git a/openwebui_integration/models/openwebui_model.py b/openwebui_integration/models/openwebui_model.py new file mode 100644 index 0000000..d18539d --- /dev/null +++ b/openwebui_integration/models/openwebui_model.py @@ -0,0 +1,243 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import requests +import logging +import json +from requests.exceptions import RequestException +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +DEFAULT_MODELS = [ + ('gpt-3.5-turbo', 'GPT-3.5 Turbo'), + ('gpt-4', 'GPT-4'), + ('claude-2', 'Claude 2'), +] + +class OpenWebUIModel(models.Model): + _name = 'openwebui.model' + _description = 'OpenWebUI Model' + + name = fields.Char( + string='Name', + required=True, + readonly=True + ) + + identifier = fields.Char( + string='Identifier', + required=True, + readonly=True + ) + + description = fields.Text( + string='Description', + readonly=True + ) + + is_active = fields.Boolean( + string='Active', + default=True, + help="If disabled, this model will not be available for bots" + ) + + company_id = fields.Many2one( + comodel_name='res.company', + string='Company', + required=True, + readonly=True, + default=lambda self: self.env.company + ) + + is_temp = fields.Boolean( + string='Temporary', + default=False, + readonly=True, + help="Indicates if this model is temporary (used for testing)" + ) + + _sql_constraints = [ + ('unique_identifier', 'unique(identifier, company_id)', 'The identifier must be unique per company!') + ] + + @api.model_create_multi + def create(self, vals_list): + """Prevents manual creation of models except for temporary models""" + for vals in vals_list: + # Mark model as temporary if it starts with test_ or refresh_ + if vals.get('identifier', '').startswith(('test_', 'refresh_')): + vals['is_temp'] = True + + # Allow creation if it's a temporary model or in installation mode + if not (vals.get('is_temp') or self.env.context.get('install_mode')): + raise UserError(_("OpenWebUI models cannot be created manually. Use the 'Refresh List' button to synchronize models from OpenWebUI.")) + + return super().create(vals_list) + + def write(self, vals): + """Prevents manual modification of models""" + if self.env.context.get('install_mode') or self.env.context.get('sync_models'): + return super().write(vals) + if 'is_active' in vals: + return super().write(vals) + raise UserError(_("OpenWebUI models cannot be modified manually. Use the 'Refresh List' button to synchronize models from OpenWebUI.")) + + def unlink(self): + """Prevents manual deletion of models""" + if self.env.context.get('install_mode') or self.env.context.get('sync_models') or all(model.is_temp for model in self): + return super().unlink() + raise UserError(_("OpenWebUI models cannot be deleted manually. They are managed automatically during synchronization with OpenWebUI.")) + + def cleanup_temp_models(self): + """Cleans up temporary models""" + temp_models = self.search([ + ('is_temp', '=', True), + '|', + ('create_date', '<', fields.Datetime.now()), + '|', + ('identifier', '=like', 'test_%'), + ('identifier', '=like', 'refresh_%') + ]) + if temp_models: + temp_models.with_context(sync_models=True).unlink() + + def _get_company_config(self): + """Gets the configuration of the current company""" + company = self.env.company + return { + 'enabled': company.openwebui_enabled, + 'api_url': company.openwebui_api_url, + 'api_key': company.openwebui_api_key, + 'verify_ssl': company.openwebui_verify_ssl, + 'timeout': company.openwebui_timeout, + } + + def _make_request(self, endpoint, method='GET', data=None, files=None, timeout=None): + """Makes a request to the OpenWebUI API""" + config = self._get_company_config() + + if not config['enabled']: + return False, "OpenWebUI is not enabled" + + try: + url = f"{config['api_url'].rstrip('/')}/api/{endpoint.lstrip('/')}" + headers = { + 'Accept': 'application/json' + } + if config['api_key']: + headers['Authorization'] = f'Bearer {config["api_key"]}' + + if method in ['POST', 'PUT', 'PATCH'] and not files: + headers['Content-Type'] = 'application/json' + + timeout = timeout or config['timeout'] + + kwargs = { + 'headers': headers, + 'verify': config['verify_ssl'], + 'timeout': timeout + } + + if files: + kwargs['files'] = files + elif method in ['POST', 'PUT', 'PATCH']: + kwargs['json'] = data + + response = requests.request(method=method, url=url, **kwargs) + response.raise_for_status() + + return True, response.json() + + except requests.exceptions.SSLError: + return False, "SSL/TLS verification failed" + except requests.exceptions.ConnectionError: + return False, "Could not connect to server" + except requests.exceptions.Timeout: + return False, "Connection timed out" + except requests.exceptions.RequestException as e: + return False, f"Connection error: {str(e)}" + except (json.JSONDecodeError, ValueError) as e: + return False, f"Invalid response from server: {str(e)}" + except Exception as e: + return False, f"Unexpected error: {str(e)}" + + def test_connection(self): + """Tests the connection to OpenWebUI""" + return self._make_request('models', timeout=5) + + @api.model + def get_available_models(self): + """Gets the list of available models from the API""" + success, result = self._make_request('models') + + if not success: + _logger.error(f"Error fetching models: {result}") + return DEFAULT_MODELS + + try: + if isinstance(result, list): + return [(model['id'], model.get('name', model['id'])) for model in result] + elif isinstance(result, dict) and 'data' in result: + return [(model['id'], model.get('name', model['id'])) for model in result['data']] + else: + _logger.warning("Unexpected response format from OpenWebUI API") + return DEFAULT_MODELS + except Exception as e: + _logger.error(f"Error processing models response: {str(e)}") + return DEFAULT_MODELS + + def sync_models(self): + """Synchronizes models from OpenWebUI""" + self.ensure_one() + success, result = self._sync_models() + return success, result + + @api.model + def _sync_models(self): + """Synchronizes models from OpenWebUI""" + # First clean up temporary models + self.cleanup_temp_models() + + success, result = self._make_request('models') + if not success: + return False, result + + try: + models_data = result if isinstance(result, list) else result.get('data', []) + for model_data in models_data: + existing = self.search([('identifier', '=', model_data['id'])]) + if not existing: + self.with_context(sync_models=True).create({ + 'name': model_data.get('name', model_data['id']), + 'identifier': model_data['id'], + 'description': model_data.get('description', ''), + }) + return True, None + except Exception as e: + _logger.error(f"Error synchronizing models: {str(e)}") + return False, str(e) + + def send_message(self, message, context=None, instructions=None): + """Sends a message to the model and returns its response""" + self.ensure_one() + + data = { + 'model': self.identifier, + 'messages': [{'role': 'user', 'content': message}], + } + + if context: + data['context'] = context + if instructions: + data['system'] = instructions + + success, result = self._make_request('chat/completions', method='POST', data=data) + if not success: + return f"Error: {result}" + + try: + return result['choices'][0]['message']['content'] + except (KeyError, IndexError) as e: + return f"Error processing response: {str(e)}" diff --git a/openwebui_integration/models/res_company.py b/openwebui_integration/models/res_company.py new file mode 100644 index 0000000..cd7f5c9 --- /dev/null +++ b/openwebui_integration/models/res_company.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +class ResCompany(models.Model): + _inherit = 'res.company' + + openwebui_enabled = fields.Boolean( + string='Enable OpenWebUI', + default=False, + help="Enable integration with OpenWebUI" + ) + + openwebui_api_url = fields.Char( + string='OpenWebUI API URL', + help="Base URL of the OpenWebUI API (e.g. http://localhost:8080)" + ) + + openwebui_api_key = fields.Char( + string='OpenWebUI API Key', + help="API key for authentication with OpenWebUI" + ) + + openwebui_verify_ssl = fields.Boolean( + string='Verify SSL Certificate', + default=True, + help="Verify the SSL certificate when making API calls" + ) + + openwebui_timeout = fields.Integer( + string='Timeout (seconds)', + default=30, + help="Maximum wait time for API calls" + ) + + openwebui_models_ids = fields.One2many( + comodel_name='openwebui.model', + inverse_name='company_id', + string='OpenWebUI Models', + ) + + def test_openwebui_connection(self): + """Tests the connection to OpenWebUI""" + self.ensure_one() + + if not self.openwebui_enabled: + raise UserError(_("OpenWebUI is not enabled for this company.")) + + model = self.env['openwebui.model'].create({ + 'name': 'test_connection', + 'identifier': 'test_connection', + 'company_id': self.id, + 'is_temp': True, + }) + + success, message = model.test_connection() + if not success: + raise UserError(message) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Connection to OpenWebUI was successful.'), + 'type': 'success', + 'sticky': False, + } + } + + def refresh_model_list(self): + """Refreshes the list of models from OpenWebUI""" + self.ensure_one() + + if not self.openwebui_enabled: + raise UserError(_("OpenWebUI is not enabled for this company.")) + + # Create a temporary model to perform synchronization + model = self.env['openwebui.model'].create({ + 'name': 'refresh_models', + 'identifier': 'refresh_models', + 'company_id': self.id, + 'is_temp': True, + }) + + success, message = model.sync_models() + if not success: + raise UserError(message) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Model list has been successfully updated.'), + 'type': 'success', + 'sticky': False, + } + } diff --git a/openwebui_integration/models/res_users.py b/openwebui_integration/models/res_users.py new file mode 100644 index 0000000..a8bc39d --- /dev/null +++ b/openwebui_integration/models/res_users.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models, Command, _ + +class ResUsers(models.Model): + _inherit = 'res.users' + + @api.model + def _init_messaging(self, store): + """Initialize messaging for the user, including OpenWebUI bot.""" + super()._init_messaging(store) + if not self.env.user._is_public(): + # Initialize chat with active bots + bots = self.env['openwebui.bot'].sudo().search([('is_active', '=', True)]) + for bot in bots: + bot.init_bot_discussion(self.env.user.id) diff --git a/openwebui_integration/security/ir.model.access.csv b/openwebui_integration/security/ir.model.access.csv new file mode 100644 index 0000000..a5108db --- /dev/null +++ b/openwebui_integration/security/ir.model.access.csv @@ -0,0 +1,5 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_openwebui_model_user,access_openwebui_model_user,model_openwebui_model,base.group_user,1,0,0,0 +access_openwebui_model_admin,access_openwebui_model_admin,model_openwebui_model,base.group_system,1,1,1,1 +access_openwebui_bot_user,access_openwebui_bot_user,model_openwebui_bot,base.group_user,1,0,0,0 +access_openwebui_bot_admin,access_openwebui_bot_admin,model_openwebui_bot,base.group_system,1,1,1,1 diff --git a/openwebui_integration/security/openwebui_security.xml b/openwebui_integration/security/openwebui_security.xml new file mode 100644 index 0000000..4f5b5c9 --- /dev/null +++ b/openwebui_integration/security/openwebui_security.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/openwebui_integration/security/security.xml b/openwebui_integration/security/security.xml new file mode 100644 index 0000000..7680c40 --- /dev/null +++ b/openwebui_integration/security/security.xml @@ -0,0 +1,41 @@ + + + + + + OpenWebUI + Gère les accès aux fonctionnalités OpenWebUI + 20 + + + + + Utilisateur + + + + + + + Administrateur + + + + + + + + Modèles OpenWebUI: règle multi-société + + [('company_id', 'in', company_ids)] + + + + + Bots OpenWebUI: règle multi-société + + [('company_id', 'in', company_ids)] + + + + diff --git a/openwebui_integration/static/description/icon.png b/openwebui_integration/static/description/icon.png new file mode 100644 index 0000000..ce62a5a Binary files /dev/null and b/openwebui_integration/static/description/icon.png differ diff --git a/openwebui_integration/static/src/css/openwebui.css b/openwebui_integration/static/src/css/openwebui.css new file mode 100644 index 0000000..fdddc22 --- /dev/null +++ b/openwebui_integration/static/src/css/openwebui.css @@ -0,0 +1,12 @@ +.o_openwebui_container { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.o_openwebui_content { + flex: 1; + padding: 16px; + overflow: auto; +} diff --git a/openwebui_integration/static/src/js/openwebui.js b/openwebui_integration/static/src/js/openwebui.js new file mode 100644 index 0000000..c8c9852 --- /dev/null +++ b/openwebui_integration/static/src/js/openwebui.js @@ -0,0 +1,30 @@ +/** @odoo-module **/ + +import { registry } from "@web/core/registry"; +import { session } from "@web/session"; +import { Component } from "@odoo/owl"; + +export class OpenWebUIAction extends Component { + static template = "openwebui_integration.ClientAction"; + + setup() { + super.setup(); + this.state = { + openwebui_enabled: false, + openwebui_model: '', + openwebui_api_url: '' + }; + this.loadCompanyConfig(); + } + + async loadCompanyConfig() { + const company = await this.env.services.orm.read( + 'res.company', + [session.company_id], + ['openwebui_enabled', 'openwebui_api_url', 'openwebui_model'] + ); + this.state = company[0]; + } +} + +registry.category("actions").add("openwebui_action", OpenWebUIAction); diff --git a/openwebui_integration/static/src/scss/openwebui.scss b/openwebui_integration/static/src/scss/openwebui.scss new file mode 100644 index 0000000..377aa89 --- /dev/null +++ b/openwebui_integration/static/src/scss/openwebui.scss @@ -0,0 +1,26 @@ +.o_mail_message_typing { + opacity: 0.7; + + .typing-indicator { + display: inline-block; + margin-left: 8px; + + span { + display: inline-block; + width: 8px; + height: 8px; + margin: 0 2px; + background-color: currentColor; + border-radius: 50%; + animation: typing 1s infinite; + + &:nth-child(2) { animation-delay: 0.2s; } + &:nth-child(3) { animation-delay: 0.4s; } + } + } +} + +@keyframes typing { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-4px); } +} diff --git a/openwebui_integration/static/src/xml/templates.xml b/openwebui_integration/static/src/xml/templates.xml new file mode 100644 index 0000000..330e595 --- /dev/null +++ b/openwebui_integration/static/src/xml/templates.xml @@ -0,0 +1,23 @@ + + + +
+
+ +
+
+
+ + +
+
+ Le bot est en train d'écrire... +
+ + + +
+
+
+
+
diff --git a/openwebui_integration/views/openwebui_bot_views.xml b/openwebui_integration/views/openwebui_bot_views.xml new file mode 100644 index 0000000..a37cfdd --- /dev/null +++ b/openwebui_integration/views/openwebui_bot_views.xml @@ -0,0 +1,61 @@ + + + + openwebui.bot.config.list + openwebui.bot + primary + + + + + + + + + + + + + + openwebui.bot.config.form + openwebui.bot + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + + OpenWebUI Bots Configuration + openwebui.bot + list,form + + + +
diff --git a/openwebui_integration/views/openwebui_model_views.xml b/openwebui_integration/views/openwebui_model_views.xml new file mode 100644 index 0000000..54cbf1e --- /dev/null +++ b/openwebui_integration/views/openwebui_model_views.xml @@ -0,0 +1,27 @@ + + + + openwebui.model.form + openwebui.model + +
+ +
+
+ + + + + + + + + + +
+
+
+
+
diff --git a/openwebui_integration/views/res_company_views.xml b/openwebui_integration/views/res_company_views.xml new file mode 100644 index 0000000..cdbd6b2 --- /dev/null +++ b/openwebui_integration/views/res_company_views.xml @@ -0,0 +1,55 @@ + + + + res.company.form.inherit.openwebui + res.company + + + + + + + + + + + + + +