openwebui_integration
This commit is contained in:
parent
064983d18a
commit
77c5b8ee82
22 changed files with 1150 additions and 0 deletions
2
openwebui_integration/__init__.py
Normal file
2
openwebui_integration/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
from . import models
|
||||||
|
from . import controllers
|
||||||
33
openwebui_integration/__manifest__.py
Normal file
33
openwebui_integration/__manifest__.py
Normal 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',
|
||||||
|
}
|
||||||
1
openwebui_integration/controllers/__init__.py
Normal file
1
openwebui_integration/controllers/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import main
|
||||||
22
openwebui_integration/controllers/main.py
Normal file
22
openwebui_integration/controllers/main.py
Normal 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,
|
||||||
|
}
|
||||||
9
openwebui_integration/models/__init__.py
Normal file
9
openwebui_integration/models/__init__.py
Normal 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
|
||||||
46
openwebui_integration/models/discuss_channel.py
Normal file
46
openwebui_integration/models/discuss_channel.py
Normal 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
|
||||||
318
openwebui_integration/models/openwebui_bot.py
Normal file
318
openwebui_integration/models/openwebui_bot.py
Normal 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)
|
||||||
71
openwebui_integration/models/openwebui_bot_mixin.py
Normal file
71
openwebui_integration/models/openwebui_bot_mixin.py
Normal 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
|
||||||
243
openwebui_integration/models/openwebui_model.py
Normal file
243
openwebui_integration/models/openwebui_model.py
Normal 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)}"
|
||||||
101
openwebui_integration/models/res_company.py
Normal file
101
openwebui_integration/models/res_company.py
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
17
openwebui_integration/models/res_users.py
Normal file
17
openwebui_integration/models/res_users.py
Normal 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)
|
||||||
5
openwebui_integration/security/ir.model.access.csv
Normal file
5
openwebui_integration/security/ir.model.access.csv
Normal 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
|
||||||
|
7
openwebui_integration/security/openwebui_security.xml
Normal file
7
openwebui_integration/security/openwebui_security.xml
Normal 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>
|
||||||
41
openwebui_integration/security/security.xml
Normal file
41
openwebui_integration/security/security.xml
Normal 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>
|
||||||
BIN
openwebui_integration/static/description/icon.png
Normal file
BIN
openwebui_integration/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
12
openwebui_integration/static/src/css/openwebui.css
Normal file
12
openwebui_integration/static/src/css/openwebui.css
Normal 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;
|
||||||
|
}
|
||||||
30
openwebui_integration/static/src/js/openwebui.js
Normal file
30
openwebui_integration/static/src/js/openwebui.js
Normal 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);
|
||||||
26
openwebui_integration/static/src/scss/openwebui.scss
Normal file
26
openwebui_integration/static/src/scss/openwebui.scss
Normal 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); }
|
||||||
|
}
|
||||||
23
openwebui_integration/static/src/xml/templates.xml
Normal file
23
openwebui_integration/static/src/xml/templates.xml
Normal 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>
|
||||||
61
openwebui_integration/views/openwebui_bot_views.xml
Normal file
61
openwebui_integration/views/openwebui_bot_views.xml
Normal 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>
|
||||||
27
openwebui_integration/views/openwebui_model_views.xml
Normal file
27
openwebui_integration/views/openwebui_model_views.xml
Normal 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>
|
||||||
55
openwebui_integration/views/res_company_views.xml
Normal file
55
openwebui_integration/views/res_company_views.xml
Normal 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>
|
||||||
Loading…
Reference in a new issue