openwebui_base: new module for using openwebui with Odoo
This commit is contained in:
parent
c0834eb327
commit
eff17d674d
15 changed files with 437 additions and 0 deletions
1
openwebui_base/__init__.py
Normal file
1
openwebui_base/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import models
|
||||
30
openwebui_base/__manifest__.py
Normal file
30
openwebui_base/__manifest__.py
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# Bemade Inc.
|
||||
#
|
||||
# Copyright (C) 2023-June Bemade Inc. (<https://www.bemade.org>).
|
||||
# Author: Marc Durepos (Contact : mdurepos@durpro.com)
|
||||
#
|
||||
# This program is under the terms of the GNU Lesser General Public License (LGPL-3)
|
||||
# For details, visit https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||
|
||||
{
|
||||
"name": "OpenWebUI Base",
|
||||
"version": "18.0.0.1.0",
|
||||
"license": "LGPL-3",
|
||||
"category": "Tools",
|
||||
"summary": "Base module for OpenWebUI integration",
|
||||
"author": "Bemade Inc.",
|
||||
"website": "https://www.bemade.org",
|
||||
"depends": ["base", "base_setup"],
|
||||
"external_dependencies": {
|
||||
"python": ["openwebui-client>=0.3.0"],
|
||||
},
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"views/openwebui_provider_views.xml",
|
||||
"views/openwebui_model_views.xml",
|
||||
"views/res_config_settings.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"application": False,
|
||||
"auto_install": False,
|
||||
}
|
||||
4
openwebui_base/models/__init__.py
Normal file
4
openwebui_base/models/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import openwebui_model
|
||||
from . import openwebui_provider
|
||||
29
openwebui_base/models/openwebui_model.py
Normal file
29
openwebui_base/models/openwebui_model.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class OpenWebUIModel(models.Model):
|
||||
_name = "openwebui.model"
|
||||
_description = "OpenWebUI Model"
|
||||
|
||||
technical_name = fields.Char(
|
||||
required=True,
|
||||
help="Technical name of the model on the OpenWebUI server.",
|
||||
readonly=True,
|
||||
)
|
||||
name = fields.Char(
|
||||
required=True,
|
||||
help="User-friendly name of the model.",
|
||||
readonly=True,
|
||||
)
|
||||
provider_id = fields.Many2one(
|
||||
"openwebui.provider",
|
||||
required=True,
|
||||
)
|
||||
|
||||
_sql_constraints = [
|
||||
(
|
||||
"technical_name_provider_unique",
|
||||
"unique(technical_name, provider_id)",
|
||||
"Technical name must be unique per provider",
|
||||
)
|
||||
]
|
||||
66
openwebui_base/models/openwebui_provider.py
Normal file
66
openwebui_base/models/openwebui_provider.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
from odoo import models, fields, api
|
||||
from .openwebui_model import OpenWebUIModel
|
||||
import openwebui_client
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class OpenWebUIProvider(models.Model):
|
||||
_name = "openwebui.provider"
|
||||
_description = "OpenWebUI Provider"
|
||||
|
||||
name = fields.Char(string="Name", required=True)
|
||||
base_url = fields.Char(
|
||||
string="Base URL",
|
||||
required=True,
|
||||
help="Base URL of the OpenWebUI server.",
|
||||
)
|
||||
api_key = fields.Char(
|
||||
string="API Key",
|
||||
required=True,
|
||||
help="API key for authentication.",
|
||||
groups="base.group_system",
|
||||
)
|
||||
model_ids = fields.One2many(
|
||||
comodel_name="openwebui.model",
|
||||
inverse_name="provider_id",
|
||||
)
|
||||
default_model_id = fields.Many2one(
|
||||
comodel_name="openwebui.model",
|
||||
)
|
||||
|
||||
def get_client(
|
||||
self, model: Optional[OpenWebUIModel] = None
|
||||
) -> openwebui_client.OpenWebUIClient:
|
||||
if not self.base_url or not self.api_key:
|
||||
raise ValueError("Base URL and API key are required")
|
||||
model = model or self.default_model_id
|
||||
return openwebui_client.OpenWebUIClient(
|
||||
base_url=self.base_url,
|
||||
api_key=self.api_key,
|
||||
default_model=model.technical_name if model else None,
|
||||
)
|
||||
|
||||
def sync_models(self):
|
||||
client = self.get_client()
|
||||
local_models = self.model_ids
|
||||
remote_models = {model.id: model.name for model in client.models.list()}
|
||||
for model in local_models:
|
||||
if model.technical_name not in remote_models.keys():
|
||||
model.unlink()
|
||||
elif model.name != remote_models[model.technical_name]:
|
||||
model.name = remote_models[model.technical_name]
|
||||
for model_id, model_name in remote_models.items():
|
||||
if model_id not in local_models.mapped("technical_name"):
|
||||
self.env["openwebui.model"].create(
|
||||
{
|
||||
"name": model_name,
|
||||
"technical_name": model_id,
|
||||
"provider_id": self.id,
|
||||
}
|
||||
)
|
||||
|
||||
def create(self, vals_list):
|
||||
providers = super().create(vals_list)
|
||||
for provider in providers:
|
||||
provider.sync_models()
|
||||
return providers
|
||||
15
openwebui_base/models/res_company.py
Normal file
15
openwebui_base/models/res_company.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class Company(models.Model):
|
||||
_inherit = "res.company"
|
||||
|
||||
openwebui_provider_id = fields.Many2one(
|
||||
"openwebui.provider",
|
||||
string="OpenWebUI Provider",
|
||||
)
|
||||
openwebui_default_model_id = fields.Many2one(
|
||||
"openwebui.model",
|
||||
string="OpenWebUI Default Model",
|
||||
domain=[("provider_id", "=", "openwebui_provider_id")],
|
||||
)
|
||||
19
openwebui_base/models/res_config_settings.py
Normal file
19
openwebui_base/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from odoo import models, fields
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = "res.config.settings"
|
||||
|
||||
openwebui_provider_id = fields.Many2one(
|
||||
comodel_name="openwebui.provider",
|
||||
related="company_id.openwebui_provider_id",
|
||||
readonly=False,
|
||||
company_dependent=True,
|
||||
)
|
||||
|
||||
openwebui_default_model_id = fields.Many2one(
|
||||
comodel_name="openwebui.model",
|
||||
related="company_id.openwebui_default_model_id",
|
||||
readonly=False,
|
||||
company_dependent=True,
|
||||
)
|
||||
5
openwebui_base/security/ir.model.access.csv
Normal file
5
openwebui_base/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_provider_admin,openwebui.provider admin,model_openwebui_provider,base.group_system,1,1,1,1
|
||||
access_openwebui_provider_user,openwebui.provider user,model_openwebui_provider,base.group_user,1,0,0,0
|
||||
access_openwebui_model_admin,openwebui.model admin,model_openwebui_model,base.group_system,1,1,1,1
|
||||
access_openwebui_model_user,openwebui.model user,model_openwebui_model,base.group_user,1,0,0,0
|
||||
|
BIN
openwebui_base/static/description/icon.png
Normal file
BIN
openwebui_base/static/description/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
7
openwebui_base/static/description/icon.svg
Normal file
7
openwebui_base/static/description/icon.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Background with rounded corners -->
|
||||
<rect width="128" height="128" rx="24" ry="24" fill="#1a1a1a"/>
|
||||
|
||||
<!-- OI Text -->
|
||||
<text x="64" y="80" font-family="Arial, sans-serif" font-size="48" font-weight="bold" text-anchor="middle" fill="#f5f5dc">OI</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
1
openwebui_base/tests/__init__.py
Normal file
1
openwebui_base/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from . import test_openwebui
|
||||
52
openwebui_base/tests/test_openwebui.py
Normal file
52
openwebui_base/tests/test_openwebui.py
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
from odoo.tests import TransactionCase, Form
|
||||
import os
|
||||
|
||||
|
||||
class TestOpenWebUI(TransactionCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.provider = self.env["openwebui.provider"].create(
|
||||
{
|
||||
"name": "Test Provider",
|
||||
"base_url": os.getenv("OPENWEBUI_BASE_URL"),
|
||||
"api_key": os.getenv("OPENWEBUI_API_KEY"),
|
||||
}
|
||||
)
|
||||
|
||||
def test_sync_models(self):
|
||||
self.assertTrue(self.provider.model_ids)
|
||||
self.assertIn(
|
||||
"MS.qwen3:32b-q8_0", self.provider.model_ids.mapped("technical_name")
|
||||
)
|
||||
|
||||
def test_get_client(self):
|
||||
client = self.provider.get_client()
|
||||
self.assertTrue(client)
|
||||
|
||||
def test_settings(self):
|
||||
model = self.provider.model_ids.filtered(
|
||||
lambda model: model.technical_name == "MS.qwen3:32b-q8_0"
|
||||
)
|
||||
wizard = self.env["res.config.settings"].create({})
|
||||
with Form(wizard) as form:
|
||||
form.openwebui_provider_id = self.provider
|
||||
form.openwebui_default_model_id = model
|
||||
self.assertEqual(self.env.company.openwebui_provider_id, self.provider)
|
||||
self.assertEqual(self.env.company.openwebui_default_model_id, model)
|
||||
|
||||
def test_openwebui_chat(self):
|
||||
model = self.provider.model_ids.filtered(
|
||||
lambda model: model.technical_name == "MS.qwen3:32b-q8_0"
|
||||
)
|
||||
self.env.company.openwebui_default_model_id = model
|
||||
self.env.company.openwebui_provider_id = self.provider
|
||||
response = self.provider.get_client().chat.completions.create(
|
||||
model=model.technical_name,
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, how are you?",
|
||||
}
|
||||
],
|
||||
)
|
||||
self.assertTrue(response and response.choices)
|
||||
78
openwebui_base/views/openwebui_model_views.xml
Normal file
78
openwebui_base/views/openwebui_model_views.xml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for OpenWebUI Model -->
|
||||
<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 string="AI Model">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Model Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="technical_name" readonly="1"/>
|
||||
<field name="provider_id" readonly="1"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View for OpenWebUI Model -->
|
||||
<record id="view_openwebui_model_tree" model="ir.ui.view">
|
||||
<field name="name">openwebui.model.tree</field>
|
||||
<field name="model">openwebui.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="AI Models">
|
||||
<field name="name"/>
|
||||
<field name="technical_name"/>
|
||||
<field name="provider_id"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for OpenWebUI Model -->
|
||||
<record id="view_openwebui_model_search" model="ir.ui.view">
|
||||
<field name="name">openwebui.model.search</field>
|
||||
<field name="model">openwebui.model</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search AI Models">
|
||||
<field name="name"/>
|
||||
<field name="technical_name"/>
|
||||
<field name="provider_id"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Provider" name="group_by_provider" context="{'group_by': 'provider_id'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for OpenWebUI Model -->
|
||||
<record id="action_openwebui_model" model="ir.actions.act_window">
|
||||
<field name="name">AI Models</field>
|
||||
<field name="res_model">openwebui.model</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
No AI models found
|
||||
</p>
|
||||
<p>
|
||||
AI models are synchronized from your providers.
|
||||
Add a provider and sync models to see them here.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item for OpenWebUI Model -->
|
||||
<menuitem id="menu_openwebui_model"
|
||||
name="AI Models"
|
||||
parent="menu_openwebui_configuration"
|
||||
action="action_openwebui_model"
|
||||
sequence="20"
|
||||
groups="base.group_system"/>
|
||||
</odoo>
|
||||
99
openwebui_base/views/openwebui_provider_views.xml
Normal file
99
openwebui_base/views/openwebui_provider_views.xml
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Form View for OpenWebUI Provider -->
|
||||
<record id="view_openwebui_provider_form" model="ir.ui.view">
|
||||
<field name="name">openwebui.provider.form</field>
|
||||
<field name="model">openwebui.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="OpenWebUI Provider">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" placeholder="Provider Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="base_url" placeholder="https://api.example.com"/>
|
||||
<field name="api_key" password="True" groups="base.group_system"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="default_model_id" domain="[('provider_id', '=', id)]"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Models" name="models">
|
||||
<field name="model_ids" readonly="1">
|
||||
<list>
|
||||
<field name="name"/>
|
||||
<field name="technical_name"/>
|
||||
</list>
|
||||
</field>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button name="sync_models" string="Sync Models" type="object" class="oe_highlight" groups="base.group_system"/>
|
||||
</div>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Tree View for OpenWebUI Provider -->
|
||||
<record id="view_openwebui_provider_tree" model="ir.ui.view">
|
||||
<field name="name">openwebui.provider.tree</field>
|
||||
<field name="model">openwebui.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<list string="OpenWebUI Providers">
|
||||
<field name="name"/>
|
||||
<field name="base_url"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Search View for OpenWebUI Provider -->
|
||||
<record id="view_openwebui_provider_search" model="ir.ui.view">
|
||||
<field name="name">openwebui.provider.search</field>
|
||||
<field name="model">openwebui.provider</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search OpenWebUI Providers">
|
||||
<field name="name"/>
|
||||
<field name="base_url"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Action for OpenWebUI Provider -->
|
||||
<record id="action_openwebui_provider" model="ir.actions.act_window">
|
||||
<field name="name">AI Providers</field>
|
||||
<field name="res_model">openwebui.provider</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first AI provider
|
||||
</p>
|
||||
<p>
|
||||
Configure AI providers to connect with OpenWebUI.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item for OpenWebUI Provider -->
|
||||
<menuitem id="menu_openwebui_root"
|
||||
name="OpenWebUI"
|
||||
sequence="100"
|
||||
groups="base.group_user"/>
|
||||
|
||||
<menuitem id="menu_openwebui_configuration"
|
||||
name="Configuration"
|
||||
parent="menu_openwebui_root"
|
||||
sequence="100"
|
||||
groups="base.group_system"/>
|
||||
|
||||
<menuitem id="menu_openwebui_provider"
|
||||
name="AI Providers"
|
||||
parent="menu_openwebui_configuration"
|
||||
action="action_openwebui_provider"
|
||||
sequence="10"
|
||||
groups="base.group_system"/>
|
||||
</odoo>
|
||||
31
openwebui_base/views/res_config_settings.xml
Normal file
31
openwebui_base/views/res_config_settings.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.openwebui.base</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app string="OpenWebUI" name="openwebui_base" groups="base.group_system">
|
||||
<block title="AI Configuration" id="openwebui_config_container">
|
||||
<setting id="ai_provider" help="Select the AI provider to use with OpenWebUI">
|
||||
<field name="openwebui_provider_id"/>
|
||||
</setting>
|
||||
<setting id="default_model" help="Select the default model to use with OpenWebUI">
|
||||
<field name="openwebui_default_model_id"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_openwebui_config_settings" model="ir.actions.act_window">
|
||||
<field name="name">Settings</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_id" ref="res_config_settings_view_form"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">inline</field>
|
||||
<field name="context">{'module' : 'openwebui_base', 'bin_size': False}</field>
|
||||
</record>
|
||||
</odoo>
|
||||
Loading…
Reference in a new issue