openwebui_integration

This commit is contained in:
xtremxpert 2025-01-16 18:42:16 -05:00
parent 064983d18a
commit 77c5b8ee82
22 changed files with 1150 additions and 0 deletions

View file

@ -0,0 +1,2 @@
from . import models
from . import controllers

View file

@ -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',
}

View file

@ -0,0 +1 @@
from . import main

View file

@ -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,
}

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)}"

View file

@ -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,
}
}

View file

@ -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)

View file

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_openwebui_model_user access_openwebui_model_user model_openwebui_model base.group_user 1 0 0 0
3 access_openwebui_model_admin access_openwebui_model_admin model_openwebui_model base.group_system 1 1 1 1
4 access_openwebui_bot_user access_openwebui_bot_user model_openwebui_bot base.group_user 1 0 0 0
5 access_openwebui_bot_admin access_openwebui_bot_admin model_openwebui_bot base.group_system 1 1 1 1

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Règle de sécurité pour le multi-compagnie -->
</data>
</odoo>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="0">
<!-- Catégorie de sécurité pour OpenWebUI -->
<record id="module_category_openwebui" model="ir.module.category">
<field name="name">OpenWebUI</field>
<field name="description">Gère les accès aux fonctionnalités OpenWebUI</field>
<field name="sequence">20</field>
</record>
<!-- Groupe pour les utilisateurs OpenWebUI -->
<record id="group_openwebui_user" model="res.groups">
<field name="name">Utilisateur</field>
<field name="category_id" ref="module_category_openwebui"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
<!-- Groupe pour les administrateurs OpenWebUI -->
<record id="group_openwebui_admin" model="res.groups">
<field name="name">Administrateur</field>
<field name="category_id" ref="module_category_openwebui"/>
<field name="implied_ids" eval="[(4, ref('group_openwebui_user'))]"/>
<field name="users" eval="[(4, ref('base.user_admin'))]"/>
</record>
<!-- Règles de sécurité par société -->
<record id="openwebui_model_company_rule" model="ir.rule">
<field name="name">Modèles OpenWebUI: règle multi-société</field>
<field name="model_id" ref="model_openwebui_model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="openwebui_bot_company_rule" model="ir.rule">
<field name="name">Bots OpenWebUI: règle multi-société</field>
<field name="model_id" ref="model_openwebui_bot"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
</data>
</odoo>

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View file

@ -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;
}

View file

@ -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);

View file

@ -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); }
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="openwebui_integration.ChatMessage" owl="1">
<div class="o_mail_message">
<div class="o_mail_message_content">
<t t-esc="props.message"/>
</div>
</div>
</t>
<t t-name="openwebui_integration.BotTyping" owl="1">
<div class="o_mail_message o_mail_message_typing">
<div class="o_mail_message_content">
<span>Le bot est en train d'écrire...</span>
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</t>
</templates>

View file

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_openwebui_bot_config_list" model="ir.ui.view">
<field name="name">openwebui.bot.config.list</field>
<field name="model">openwebui.bot</field>
<field name="mode">primary</field>
<field name="arch" type="xml">
<list>
<field name="name"/>
<field name="model_id"/>
<field name="is_active"/>
<field name="max_tokens"/>
<field name="temperature"/>
<field name="company_id" optional="hide" groups="base.group_multi_company"/>
</list>
</field>
</record>
<record id="view_openwebui_bot_config_form" model="ir.ui.view">
<field name="name">openwebui.bot.config.form</field>
<field name="model">openwebui.bot</field>
<field name="arch" type="xml">
<form>
<sheet>
<group>
<group>
<field name="name"/>
<field name="model_id"/>
<field name="is_active"/>
<field name="company_id" groups="base.group_multi_company"/>
</group>
<group>
<field name="max_tokens"/>
<field name="temperature"/>
</group>
</group>
<notebook>
<page string="Instructions" name="instructions">
<field name="instructions" placeholder="Specific instructions to guide the bot's behavior..."/>
</page>
<page string="Context" name="context">
<field name="context" placeholder="Additional context for conversations..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record id="action_openwebui_bot_config" model="ir.actions.act_window">
<field name="name">OpenWebUI Bots Configuration</field>
<field name="res_model">openwebui.bot</field>
<field name="view_mode">list,form</field>
</record>
<menuitem id="menu_openwebui_bot"
name="Bots Configuration"
parent="mail.menu_configuration"
action="action_openwebui_bot_config"
sequence="20"/>
</odoo>

View file

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_openwebui_model_form" model="ir.ui.view">
<field name="name">openwebui.model.form</field>
<field name="model">openwebui.model</field>
<field name="arch" type="xml">
<form create="false" edit="false" delete="false">
<sheet>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name"/></h1>
</div>
<group>
<group>
<field name="identifier"/>
<field name="company_id" groups="base.group_multi_company"/>
<field name="is_active" widget="boolean_toggle"/>
</group>
<group>
<field name="description"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
</odoo>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_company_form_inherit_openwebui" model="ir.ui.view">
<field name="name">res.company.form.inherit.openwebui</field>
<field name="model">res.company</field>
<field name="inherit_id" ref="base.view_company_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="OpenWebUI" name="openwebui">
<group>
<group>
<field name="openwebui_enabled"/>
<field name="openwebui_api_url"
invisible="not openwebui_enabled"
required="openwebui_enabled"
placeholder="https://api.openwebui.com"/>
<field name="openwebui_api_key"
invisible="not openwebui_enabled"
required="openwebui_enabled"
password="True"/>
</group>
<group>
<field name="openwebui_verify_ssl"
invisible="not openwebui_enabled"/>
<field name="openwebui_timeout"
invisible="not openwebui_enabled"/>
<button name="test_openwebui_connection"
string="Test Connection"
type="object"
class="oe_highlight"
invisible="not openwebui_enabled"/>
</group>
</group>
<group string="Available Models" invisible="not openwebui_enabled">
<field name="openwebui_models_ids" nolabel="1">
<list create="false" delete="false" edit="true">
<field name="name"/>
<field name="identifier"/>
<field name="description"/>
<field name="is_active" widget="boolean_toggle"/>
</list>
</field>
</group>
<group>
<button name="refresh_model_list"
string="Refresh List"
type="object"
class="btn-secondary"
icon="fa-refresh"/>
</group>
</page>
</xpath>
</field>
</record>
</odoo>