Compare commits
5 commits
18.0
...
Feature/AI
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
014293c548 | ||
|
|
7815fb55a6 | ||
|
|
794e800d7c | ||
|
|
c5904a51d7 | ||
|
|
ed9dda9bea |
32 changed files with 2094 additions and 140 deletions
|
|
@ -1,3 +1,39 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import models
|
||||
from . import views
|
||||
|
||||
def post_init_hook(cr, registry=None):
|
||||
"""Initialize default AI prompt template after module installation
|
||||
|
||||
Note: This function can be called with either one argument (env) or two arguments (cr, registry)
|
||||
to accommodate different Odoo hook calling conventions.
|
||||
"""
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
_logger.info("=== START: post_init_hook ====")
|
||||
|
||||
try:
|
||||
from odoo import api, SUPERUSER_ID
|
||||
|
||||
# Handle different calling conventions
|
||||
if hasattr(cr, 'env'): # cr is actually an env
|
||||
env = cr
|
||||
_logger.info("Using provided environment")
|
||||
elif registry: # We have both cr and registry
|
||||
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||
_logger.info("Created environment from cursor and registry")
|
||||
else:
|
||||
_logger.warning("Cannot create environment, skipping template creation")
|
||||
return
|
||||
|
||||
_logger.info("Calling _ensure_default_template")
|
||||
# Use our bridge model which will call the correct underlying model
|
||||
env['ai.openwebui.prompt.template']._ensure_default_template('helpdesk')
|
||||
_logger.info("Default template ensured successfully")
|
||||
_logger.info("=== END: post_init_hook ====")
|
||||
except Exception as e:
|
||||
_logger.error(f"ERROR in post_init_hook: {str(e)}")
|
||||
_logger.exception("Exception traceback:")
|
||||
# Don't raise the exception to prevent installation failure
|
||||
return
|
||||
|
|
|
|||
|
|
@ -16,12 +16,17 @@
|
|||
'maintainer': 'it@bemade.org',
|
||||
'depends': [
|
||||
'helpdesk_sale_order',
|
||||
'openai_connector', # Supposant qu'un module de connexion OpenAI existe
|
||||
'openwebui_base',
|
||||
],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/helpdesk_team_views.xml',
|
||||
'views/helpdesk_ticket_views.xml',
|
||||
'views/helpdesk_ticket_button_views.xml',
|
||||
'views/res_config_settings_views.xml',
|
||||
'views/ai_prompt_template_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'post_init_hook': 'post_init_hook',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,3 +2,10 @@
|
|||
|
||||
from . import helpdesk_ticket
|
||||
from . import helpdesk_team
|
||||
from . import sale_order
|
||||
from . import ai_openwebui_client # Bridge model for OpenWebUI client
|
||||
from . import ai_openwebui_prompt_template # Bridge model for OpenWebUI prompt templates
|
||||
from . import res_config_settings
|
||||
|
||||
# Import views after all models are loaded
|
||||
from .. import views
|
||||
|
|
|
|||
114
helpdesk_sale_order_ai/models/ai_openwebui_client.py
Normal file
114
helpdesk_sale_order_ai/models/ai_openwebui_client.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
import requests
|
||||
import json
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class AIOpenWebUIClient(models.AbstractModel):
|
||||
"""
|
||||
Abstract model to provide a bridge between the helpdesk_sale_order_ai module
|
||||
and the openwebui_base module.
|
||||
|
||||
This ensures proper integration with the OpenWebUI API using configuration
|
||||
from the settings.
|
||||
"""
|
||||
_name = "helpdesk_sale_order_ai.openwebui.client"
|
||||
_description = "OpenWebUI Client"
|
||||
|
||||
@api.model
|
||||
def chat_completion(self, messages, model=None, **kwargs):
|
||||
"""
|
||||
Send a chat completion request to the OpenWebUI API
|
||||
|
||||
Args:
|
||||
messages: List of message dictionaries with 'role' and 'content'
|
||||
model: The model to use for the completion (default: anthropic.claude-3-7-sonnet-latest)
|
||||
**kwargs: Additional parameters to pass to the OpenWebUI client
|
||||
|
||||
Returns:
|
||||
The response from the OpenWebUI API
|
||||
"""
|
||||
# Check if openwebui_base module is installed
|
||||
module_obj = self.env['ir.module.module']
|
||||
openwebui_base_module = module_obj.search([('name', '=', 'openwebui_base'), ('state', '=', 'installed')])
|
||||
|
||||
if not openwebui_base_module:
|
||||
_logger.error("The openwebui_base module is not installed. Cannot use AI features.")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Get configuration from settings
|
||||
config = self.env['ir.config_parameter'].sudo()
|
||||
company = self.env.company
|
||||
|
||||
# Get provider configuration
|
||||
provider = company.openwebui_provider_id
|
||||
if not provider:
|
||||
_logger.error("No OpenWebUI provider configured for this company")
|
||||
return False
|
||||
|
||||
# Get API key and base URL from provider
|
||||
api_key = provider.api_key
|
||||
base_url = provider.base_url
|
||||
|
||||
if not api_key or not base_url:
|
||||
_logger.error("Missing API key or base URL in OpenWebUI provider configuration")
|
||||
return False
|
||||
|
||||
# Get model from company settings or use provided model or default
|
||||
if not model and company.openwebui_default_model_id:
|
||||
model = company.openwebui_default_model_id.technical_name
|
||||
|
||||
_logger.info(f"Using OpenWebUI API with model={model} and {len(messages)} messages")
|
||||
|
||||
# Log first few characters of the prompt for debugging
|
||||
if messages and len(messages) > 0:
|
||||
first_content = messages[0].get('content', '')[:100]
|
||||
_logger.info(f"First message content (truncated): {first_content}...")
|
||||
|
||||
# Prepare the request
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': f'Bearer {api_key}'
|
||||
}
|
||||
|
||||
data = {
|
||||
'model': model,
|
||||
'messages': messages,
|
||||
**kwargs
|
||||
}
|
||||
|
||||
# Make the API call
|
||||
endpoint = f"{base_url.rstrip('/')}/chat/completions"
|
||||
_logger.info(f"Sending request to {endpoint}")
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
headers=headers,
|
||||
json=data,
|
||||
timeout=60 # Reasonable timeout for AI responses
|
||||
)
|
||||
|
||||
# Check for successful response
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
# Log response summary
|
||||
if result:
|
||||
_logger.info(f"Received response from OpenWebUI API: {str(result)[:200]}...")
|
||||
else:
|
||||
_logger.warning("Received empty response from OpenWebUI API")
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
_logger.error(f"API request error in chat_completion: {str(e)}")
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
_logger.error(f"JSON decode error in chat_completion: {str(e)}")
|
||||
return False
|
||||
except Exception as e:
|
||||
_logger.error(f"Unexpected error in chat_completion: {str(e)}")
|
||||
return False
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class AIOpenWebUIPromptTemplate(models.AbstractModel):
|
||||
"""
|
||||
Abstract model to provide a bridge between the old ai.openwebui.prompt.template model
|
||||
and the new openwebui.prompt.template model from the openwebui_base module.
|
||||
|
||||
This ensures backward compatibility while leveraging the new centralized infrastructure.
|
||||
"""
|
||||
_name = "ai.openwebui.prompt.template"
|
||||
_description = "OpenWebUI Prompt Template Bridge"
|
||||
|
||||
@api.model
|
||||
def _ensure_default_template(self, template_type='helpdesk'):
|
||||
"""Ensure that a default template exists for the given type"""
|
||||
return self.env['openwebui.prompt.template']._ensure_default_template(template_type)
|
||||
|
||||
@api.model
|
||||
def get_default_template(self, template_type='helpdesk'):
|
||||
"""Get the default template for the given type"""
|
||||
return self.env['openwebui.prompt.template'].get_default_template(template_type)
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HelpdeskTeam(models.Model):
|
||||
|
|
@ -7,19 +10,59 @@ class HelpdeskTeam(models.Model):
|
|||
|
||||
use_ai_sale_orders = fields.Boolean(
|
||||
string='Use AI for Sale Orders',
|
||||
help='If checked, the system will use AI to automatically generate sale orders from ticket descriptions.',
|
||||
help='If checked, the system will use AI to automatically generate sale orders from ticket descriptions for this team. This overrides the global setting.',
|
||||
default=False,
|
||||
)
|
||||
|
||||
ai_prompt_template = fields.Text(
|
||||
ai_prompt_template_id = fields.Many2one(
|
||||
'openwebui.prompt.template',
|
||||
domain="[('template_type', '=', 'helpdesk')]",
|
||||
string='AI Prompt Template',
|
||||
help='Template for the prompt sent to the AI. Use placeholders like {description}, {customer}, etc.',
|
||||
default="""Based on the following helpdesk ticket description, identify products and services that should be included in a sales order:
|
||||
|
||||
Ticket Description: {description}
|
||||
Customer: {customer}
|
||||
|
||||
Please provide a list of products/services with quantities and descriptions in the following format:
|
||||
Product/Service Name | Quantity | Description
|
||||
"""
|
||||
help='Template for the prompt sent to the AI. If empty, the global template will be used.',
|
||||
)
|
||||
|
||||
def _get_use_ai_sale_orders(self):
|
||||
"""Get whether to use AI sale orders, considering both team and global settings"""
|
||||
self.ensure_one()
|
||||
# If team has specific setting, use that
|
||||
if self.use_ai_sale_orders:
|
||||
return True
|
||||
|
||||
# Otherwise, check global setting (default to True)
|
||||
param_value = self.env['ir.config_parameter'].sudo().get_param('helpdesk_sale_order_ai.use_ai_sale_orders', 'True')
|
||||
return param_value.lower() == 'true' if isinstance(param_value, str) else bool(param_value)
|
||||
|
||||
def _get_ai_prompt_template(self):
|
||||
"""Get the AI prompt template to use for this team"""
|
||||
self.ensure_one()
|
||||
|
||||
try:
|
||||
# Ensure template model is initialized
|
||||
self.env['openwebui.prompt.template']._ensure_default_template('helpdesk')
|
||||
|
||||
# If team has a specific template, use it
|
||||
if self.ai_prompt_template_id and self.ai_prompt_template_id.exists():
|
||||
_logger.info(f"Using team-specific prompt template: {self.ai_prompt_template_id.name}")
|
||||
return self.ai_prompt_template_id.content
|
||||
|
||||
# Otherwise, get the global default template
|
||||
default_template = self.env['openwebui.prompt.template'].get_default_template('helpdesk')
|
||||
if default_template:
|
||||
_logger.info(f"Using default helpdesk prompt template: {default_template.name}")
|
||||
return default_template.content
|
||||
except Exception as e:
|
||||
_logger.error(f"Error retrieving prompt template: {str(e)}")
|
||||
|
||||
# Fallback to hardcoded template if all else fails
|
||||
_logger.warning("Using fallback hardcoded prompt template")
|
||||
return """
|
||||
Based on the following helpdesk ticket description, identify products and services that should be included in a sales order:
|
||||
|
||||
Ticket Description: {description}
|
||||
Chatter Messages: {chatter_messages}
|
||||
Attachments: {attachments_info}
|
||||
Attachment Contents: {attachment_contents}
|
||||
|
||||
Please provide a list of products/services with quantities and descriptions in the following format:
|
||||
Product/Service Name | Quantity | Description
|
||||
"""
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
69
helpdesk_sale_order_ai/models/res_config_settings.py
Normal file
69
helpdesk_sale_order_ai/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# Inherit the fields from openwebui_base module
|
||||
# These fields are already defined in the openwebui_base module
|
||||
|
||||
# Add a field to select the prompt template directly
|
||||
helpdesk_ai_prompt_template_id = fields.Many2one(
|
||||
'openwebui.prompt.template',
|
||||
string='Default AI Prompt Template',
|
||||
help='Default template for the prompt sent to the AI.',
|
||||
ondelete='set null', # This helps avoid constraint errors
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
"""Get values for the settings form"""
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
|
||||
# Get the template ID from the system parameter
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
template_id = IrConfigParam.get_param('helpdesk_sale_order_ai.default_prompt_template_id', False)
|
||||
|
||||
if template_id:
|
||||
try:
|
||||
template_id_int = int(template_id) if isinstance(template_id, str) else template_id
|
||||
res['helpdesk_ai_prompt_template_id'] = template_id_int
|
||||
except (ValueError, TypeError):
|
||||
_logger.error(f"Invalid template ID in system parameter: {template_id}")
|
||||
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
"""Set values from the settings form"""
|
||||
super(ResConfigSettings, self).set_values()
|
||||
|
||||
# Save the template ID to the system parameter
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
if self.helpdesk_ai_prompt_template_id:
|
||||
IrConfigParam.set_param('helpdesk_sale_order_ai.default_prompt_template_id', str(self.helpdesk_ai_prompt_template_id.id))
|
||||
|
||||
@api.model
|
||||
def get_prompt_template(self):
|
||||
"""Get the prompt template content from the selected template or default"""
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
template_id = IrConfigParam.get_param('helpdesk_sale_order_ai.default_prompt_template_id', False)
|
||||
|
||||
if template_id:
|
||||
try:
|
||||
# Make sure the template model exists first
|
||||
self.env['ai.openwebui.prompt.template']._ensure_default_template('helpdesk')
|
||||
# The template_id is already an integer in the database now
|
||||
template_id_int = int(template_id) if isinstance(template_id, str) else template_id
|
||||
template = self.env['ai.openwebui.prompt.template'].browse(template_id_int)
|
||||
if template.exists():
|
||||
return template.content
|
||||
except (ValueError, TypeError) as e:
|
||||
_logger.error(f"Error retrieving prompt template: {e}")
|
||||
pass
|
||||
|
||||
# Fallback to default template
|
||||
default_template = self.env['ai.openwebui.prompt.template'].get_default_template('helpdesk')
|
||||
return default_template.content if default_template else ""
|
||||
85
helpdesk_sale_order_ai/models/sale_order.py
Normal file
85
helpdesk_sale_order_ai/models/sale_order.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
# Add missing_product_count field to avoid view errors
|
||||
# This is needed because some views expect this field
|
||||
missing_product_count = fields.Integer(
|
||||
string='Missing Products Count',
|
||||
default=0,
|
||||
help='Number of products that could not be found in the database'
|
||||
)
|
||||
|
||||
# Add has_missing_products field to avoid view errors
|
||||
has_missing_products = fields.Boolean(
|
||||
string='Has Missing Products',
|
||||
default=False,
|
||||
help='Whether this sale order has products that could not be found in the database'
|
||||
)
|
||||
|
||||
# Add ticket_id field to link sale orders to helpdesk tickets
|
||||
ticket_id = fields.Many2one(
|
||||
'helpdesk.ticket',
|
||||
string='Helpdesk Ticket',
|
||||
readonly=True,
|
||||
copy=False,
|
||||
help='Helpdesk ticket from which this sale order was created'
|
||||
)
|
||||
|
||||
def action_confirm(self):
|
||||
"""Override to delete missing products message when sale order is confirmed"""
|
||||
# Call the original method first
|
||||
result = super(SaleOrder, self).action_confirm()
|
||||
|
||||
# Delete missing products message if exists
|
||||
if self.has_missing_products:
|
||||
self._delete_missing_products_message()
|
||||
|
||||
# Reset missing products flags
|
||||
self.write({
|
||||
'missing_product_count': 0,
|
||||
'has_missing_products': False
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
def _delete_missing_products_message(self):
|
||||
"""Delete the missing products message from the chatter"""
|
||||
# Find the message with the unique identifier
|
||||
domain = [
|
||||
('res_id', '=', self.id),
|
||||
('model', '=', 'sale.order'),
|
||||
('body', 'ilike', '%<!-- MISSING_PRODUCTS_MESSAGE -->%'),
|
||||
]
|
||||
|
||||
messages = self.env['mail.message'].sudo().search(domain)
|
||||
|
||||
if messages:
|
||||
_logger.info(f"Found {len(messages)} missing products message(s) for sale order {self.id} with IDs: {messages.ids}")
|
||||
try:
|
||||
# Make sure we're actually deleting the message
|
||||
message_ids = messages.ids # Store IDs before deletion
|
||||
messages.sudo().unlink()
|
||||
_logger.info(f"Successfully deleted missing products message(s) with IDs: {message_ids} for sale order {self.id}")
|
||||
except Exception as e:
|
||||
_logger.error(f"Error deleting missing products message: {e}")
|
||||
else:
|
||||
_logger.warning(f"No missing products message found for sale order {self.id} - check HTML comment marker")
|
||||
|
||||
# Debug: Try a broader search to see if there are any messages at all
|
||||
all_messages = self.env['mail.message'].sudo().search([
|
||||
('res_id', '=', self.id),
|
||||
('model', '=', 'sale.order'),
|
||||
], limit=5)
|
||||
|
||||
if all_messages:
|
||||
_logger.info(f"Found {len(all_messages)} recent messages for this sale order. First message body sample: {all_messages[0].body[:100] if all_messages[0].body else 'Empty'}...")
|
||||
else:
|
||||
_logger.info(f"No messages found at all for sale order {self.id}")
|
||||
|
||||
|
||||
3
helpdesk_sale_order_ai/security/ir.model.access.csv
Normal file
3
helpdesk_sale_order_ai/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_ai_openwebui_prompt_template_helpdesk_user,ai.openwebui.prompt.template.helpdesk.user,openwebui_base.model_openwebui_prompt_template,helpdesk.group_helpdesk_user,1,0,0,0
|
||||
access_ai_openwebui_prompt_template_helpdesk_manager,ai.openwebui.prompt.template.helpdesk.manager,openwebui_base.model_openwebui_prompt_template,helpdesk.group_helpdesk_manager,1,1,1,1
|
||||
|
4
helpdesk_sale_order_ai/views/__init__.py
Normal file
4
helpdesk_sale_order_ai/views/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# These imports are handled differently in Odoo 18
|
||||
# XML files are loaded automatically by the manifest
|
||||
82
helpdesk_sale_order_ai/views/ai_prompt_template_views.xml
Normal file
82
helpdesk_sale_order_ai/views/ai_prompt_template_views.xml
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- AI Prompt Template Form View -->
|
||||
<record id="view_helpdesk_ai_prompt_template_form" model="ir.ui.view">
|
||||
<field name="name">openwebui.prompt.template.form</field>
|
||||
<field name="model">openwebui.prompt.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="AI Prompt Template">
|
||||
<field name="template_type" invisible="0"/>
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<h1><field name="name" placeholder="Template Name"/></h1>
|
||||
</div>
|
||||
<group>
|
||||
<group>
|
||||
<field name="is_default"/>
|
||||
</group>
|
||||
</group>
|
||||
<notebook>
|
||||
<page string="Template Content">
|
||||
<field name="content" nolabel="1" placeholder="Enter the prompt template content here..."/>
|
||||
</page>
|
||||
</notebook>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Prompt Template List View -->
|
||||
<record id="view_helpdesk_ai_prompt_template_list" model="ir.ui.view">
|
||||
<field name="name">openwebui.prompt.template.list</field>
|
||||
<field name="model">openwebui.prompt.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<list>
|
||||
<field name="template_type"/>
|
||||
<field name="name"/>
|
||||
<field name="is_default" string="Default"/>
|
||||
</list>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Prompt Template Search View -->
|
||||
<record id="view_helpdesk_ai_prompt_template_search" model="ir.ui.view">
|
||||
<field name="name">openwebui.prompt.template.search</field>
|
||||
<field name="model">openwebui.prompt.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Search AI Prompt Templates">
|
||||
<field name="template_type"/>
|
||||
<field name="name"/>
|
||||
<filter string="Default" name="default" domain="[('is_default', '=', True)]"/>
|
||||
<filter string="Helpdesk Templates" name="helpdesk" domain="[('template_type', '=', 'helpdesk')]"/>
|
||||
<filter string="General Templates" name="general" domain="[('template_type', '=', 'general')]"/>
|
||||
<filter string="Custom Templates" name="custom" domain="[('template_type', '=', 'custom')]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- AI Prompt Template Action -->
|
||||
<record id="action_helpdesk_ai_prompt_template" model="ir.actions.act_window">
|
||||
<field name="name">AI Prompt Templates</field>
|
||||
<field name="res_model">openwebui.prompt.template</field>
|
||||
<field name="domain">[('template_type', '=', 'helpdesk')]</field>
|
||||
<field name="context">{"default_template_type": "helpdesk"}</field>
|
||||
<field name="view_mode">list,form</field>
|
||||
<field name="search_view_id" ref="view_helpdesk_ai_prompt_template_search"/>
|
||||
<field name="help" type="html">
|
||||
<p class="o_view_nocontent_smiling_face">
|
||||
Create your first AI prompt template!
|
||||
</p>
|
||||
<p>
|
||||
AI prompt templates are used to generate product suggestions from helpdesk tickets.
|
||||
</p>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- Menu Item -->
|
||||
<menuitem id="menu_helpdesk_ai_prompt_template"
|
||||
name="AI Prompt Templates"
|
||||
parent="helpdesk.helpdesk_menu_config"
|
||||
action="action_helpdesk_ai_prompt_template"
|
||||
sequence="50"/>
|
||||
</odoo>
|
||||
|
|
@ -5,9 +5,9 @@
|
|||
<field name="model">helpdesk.team</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_team_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_use_sale_orders']" position="after">
|
||||
<field name="use_ai_sale_orders" attrs="{'invisible': [('team_use_sale_orders', '=', False)]}"/>
|
||||
<field name="ai_prompt_template" attrs="{'invisible': [('use_ai_sale_orders', '=', False)]}" widget="text_field"/>
|
||||
<xpath expr="//field[@name='use_sale_orders']" position="after">
|
||||
<field name="use_ai_sale_orders" invisible="not use_sale_orders"/>
|
||||
<field name="ai_prompt_template_id" invisible="not use_ai_sale_orders"/>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_ticket_form_view_inherit_ai_button" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.form.view.inherit.ai.button</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="helpdesk_sale_order.helpdesk_ticket_form_view_inherit_saleorder"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Update the Convert to Quotation button to be visible when either team_use_sale_orders OR team_use_ai_sale_orders is True -->
|
||||
<xpath expr="//button[@name='action_convert_to_sale_order']" position="attributes">
|
||||
<attribute name="invisible">not team_use_sale_orders and not team_use_ai_sale_orders</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
21
helpdesk_sale_order_ai/views/helpdesk_ticket_views.xml
Normal file
21
helpdesk_sale_order_ai/views/helpdesk_ticket_views.xml
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="helpdesk_ticket_form_view_inherit_ai" model="ir.ui.view">
|
||||
<field name="name">helpdesk.ticket.form.view.inherit.ai</field>
|
||||
<field name="model">helpdesk.ticket</field>
|
||||
<field name="inherit_id" ref="helpdesk.helpdesk_ticket_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//field[@name='team_id']" position="after">
|
||||
<field name="team_use_sale_orders" invisible="1"/>
|
||||
<field name="team_use_ai_sale_orders" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="AI Suggestions" invisible="not team_use_ai_sale_orders">
|
||||
<group>
|
||||
<field name="ai_generated_products" widget="text" readonly="1"/>
|
||||
</group>
|
||||
</page>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
15
helpdesk_sale_order_ai/views/res_config_settings_views.xml
Normal file
15
helpdesk_sale_order_ai/views/res_config_settings_views.xml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- Add the hidden field to the base settings form -->
|
||||
<record id="res_config_settings_view_form_inherit_helpdesk_sale_order_ai" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.helpdesk.sale.order.ai</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<!-- Add the hidden field at the root level of the form -->
|
||||
<field name="company_id" position="after">
|
||||
<field name="helpdesk_ai_prompt_template_id" invisible="1"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
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,
|
||||
}
|
||||
5
openwebui_base/models/__init__.py
Normal file
5
openwebui_base/models/__init__.py
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
from . import res_company
|
||||
from . import res_config_settings
|
||||
from . import openwebui_model
|
||||
from . import openwebui_provider
|
||||
from . import openwebui_prompt_template
|
||||
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",
|
||||
)
|
||||
]
|
||||
85
openwebui_base/models/openwebui_prompt_template.py
Normal file
85
openwebui_base/models/openwebui_prompt_template.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
class OpenWebUIPromptTemplate(models.Model):
|
||||
_name = "openwebui.prompt.template"
|
||||
_description = "OpenWebUI Prompt Template"
|
||||
|
||||
name = fields.Char(string="Name", required=True)
|
||||
template_type = fields.Selection([
|
||||
('helpdesk', 'Helpdesk'),
|
||||
('general', 'General'),
|
||||
('custom', 'Custom'),
|
||||
], string="Template Type", default='general', required=True)
|
||||
content = fields.Text(string="Template Content", required=True)
|
||||
is_default = fields.Boolean(string="Is Default", default=False)
|
||||
|
||||
@api.model
|
||||
def _ensure_default_template(self, template_type='helpdesk'):
|
||||
"""Ensure that a default template exists for the given type"""
|
||||
default_template = self.search([
|
||||
('template_type', '=', template_type),
|
||||
('is_default', '=', True)
|
||||
], limit=1)
|
||||
|
||||
if not default_template:
|
||||
# Create a default template based on the type
|
||||
if template_type == 'helpdesk':
|
||||
self.create({
|
||||
'name': 'Default Helpdesk Template',
|
||||
'template_type': 'helpdesk',
|
||||
'content': """You are a helpful assistant analyzing helpdesk tickets to create sales orders.
|
||||
|
||||
IMPORTANT: In this system, product names are often formatted as the product reference number in square brackets followed by the product name (for example: [REF123] Widget). You can use the reference code inside the brackets to identify products in the database. If you are unsure, select the product with the highest matching reference code.
|
||||
|
||||
Please analyze the following ticket information and extract:
|
||||
|
||||
1. Client reference number (e.g., PO-12345, REF-678)
|
||||
2. Products or services needed with quantities and descriptions
|
||||
3. Any special requirements or notes
|
||||
4. Delivery date expectations
|
||||
5. Payment terms if mentioned
|
||||
|
||||
Ticket Information:
|
||||
{ticket_description}
|
||||
|
||||
{ticket_messages}
|
||||
|
||||
Please format your response as a structured JSON with the following format:
|
||||
{
|
||||
"sale_order_fields": {
|
||||
"client_order_ref": "Reference number if found",
|
||||
"commitment_date": "YYYY-MM-DD if found",
|
||||
"payment_term_id": "Payment terms if found"
|
||||
},
|
||||
"order_lines": [
|
||||
{
|
||||
"product_name": "Product name or description",
|
||||
"quantity": 1.0,
|
||||
"notes": "Any special instructions for this line"
|
||||
}
|
||||
]
|
||||
}""",
|
||||
'is_default': True
|
||||
})
|
||||
elif template_type == 'general':
|
||||
self.create({
|
||||
'name': 'Default General Template',
|
||||
'template_type': 'general',
|
||||
'content': """You are a helpful assistant. Please provide a detailed and accurate response to the following query:
|
||||
|
||||
{query}""",
|
||||
'is_default': True
|
||||
})
|
||||
|
||||
@api.model
|
||||
def get_default_template(self, template_type='helpdesk'):
|
||||
"""Get the default template for the given type"""
|
||||
self._ensure_default_template(template_type)
|
||||
return self.search([
|
||||
('template_type', '=', template_type),
|
||||
('is_default', '=', True)
|
||||
], limit=1)
|
||||
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")],
|
||||
)
|
||||
44
openwebui_base/models/res_config_settings.py
Normal file
44
openwebui_base/models/res_config_settings.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
from odoo import models, fields, api
|
||||
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
use_ai_sale_orders = fields.Boolean(
|
||||
string='Use AI for Sale Orders',
|
||||
help='If checked, the system will use AI to automatically generate sale orders from ticket descriptions.',
|
||||
)
|
||||
|
||||
@api.model
|
||||
def get_values(self):
|
||||
"""Get values for the settings form"""
|
||||
res = super(ResConfigSettings, self).get_values()
|
||||
|
||||
# Get the AI sale orders setting from the system parameter
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
param_value = IrConfigParam.get_param('helpdesk_sale_order_ai.use_ai_sale_orders', 'True')
|
||||
res['use_ai_sale_orders'] = param_value.lower() == 'true' if isinstance(param_value, str) else bool(param_value)
|
||||
|
||||
return res
|
||||
|
||||
def set_values(self):
|
||||
"""Set values from the settings form"""
|
||||
super(ResConfigSettings, self).set_values()
|
||||
|
||||
# Save the AI sale orders setting to the system parameter
|
||||
IrConfigParam = self.env['ir.config_parameter'].sudo()
|
||||
IrConfigParam.set_param('helpdesk_sale_order_ai.use_ai_sale_orders', str(self.use_ai_sale_orders))
|
||||
7
openwebui_base/security/ir.model.access.csv
Normal file
7
openwebui_base/security/ir.model.access.csv
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
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
|
||||
access_openwebui_prompt_template_admin,openwebui.prompt.template admin,model_openwebui_prompt_template,base.group_system,1,1,1,1
|
||||
access_openwebui_prompt_template_user,openwebui.prompt.template user,model_openwebui_prompt_template,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>
|
||||
39
openwebui_base/views/res_config_settings.xml
Normal file
39
openwebui_base/views/res_config_settings.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?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>
|
||||
<block title="Helpdesk Integration" id="openwebui_helpdesk_integration">
|
||||
<setting id="use_ai_sale_orders" help="If enabled, the system will use AI to automatically generate sale orders from helpdesk ticket descriptions.">
|
||||
<field name="use_ai_sale_orders"/>
|
||||
<div class="text-muted">
|
||||
Enable AI-powered sale order generation from helpdesk tickets
|
||||
</div>
|
||||
</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