diff --git a/openai_connector/__init__.py b/openai_connector/__init__.py
deleted file mode 100644
index 9a7e03e..0000000
--- a/openai_connector/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import models
\ No newline at end of file
diff --git a/openai_connector/__manifest__.py b/openai_connector/__manifest__.py
deleted file mode 100644
index 3832c8f..0000000
--- a/openai_connector/__manifest__.py
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- 'name': 'OpenAI Connector',
- 'version': '1.0',
- 'category': 'Tools',
- 'summary': 'Manage OpenAI connection settings',
- 'license': 'AGPL-3',
- 'description': '''
- This module allows the configuration of OpenAI API connection
- settings, including API key and Organization ID.
- ''',
- 'author': 'Bemade Inc.',
- 'depends': ['base_setup'],
- 'data': [
- 'views/res_config_settings_views.xml',
- 'security/ir.model.access.csv',
- ],
- 'external_dependencies': {
- 'python': ['openai'],
- },
- 'installable': True,
- 'application': False,
-}
\ No newline at end of file
diff --git a/openai_connector/models/__init__.py b/openai_connector/models/__init__.py
deleted file mode 100644
index 3468e42..0000000
--- a/openai_connector/models/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from . import res_config_settings
-from . import res_company
\ No newline at end of file
diff --git a/openai_connector/models/res_company.py b/openai_connector/models/res_company.py
deleted file mode 100644
index 0945476..0000000
--- a/openai_connector/models/res_company.py
+++ /dev/null
@@ -1,9 +0,0 @@
-
-from odoo import models, fields
-
-class ResCompany(models.Model):
- _inherit = 'res.company'
-
- api_key = fields.Char(string="API Key", help="API Key for OpenAI specific to this company. It should start with 'sk-'")
- organization = fields.Char(string="Organization ID", help="Organization ID for OpenAI specific to this company. It should start with 'org-'")
-
\ No newline at end of file
diff --git a/openai_connector/models/res_config_settings.py b/openai_connector/models/res_config_settings.py
deleted file mode 100644
index e8438f0..0000000
--- a/openai_connector/models/res_config_settings.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from odoo import models, fields, api, _
-from odoo.exceptions import UserError
-import openai
-import logging
-
-_logger = logging.getLogger(__name__)
-
-class ResConfigSettings(models.TransientModel):
- _inherit = 'res.config.settings'
-
- api_key = fields.Char(
- string="API Key",
- config_parameter='openai_connector.api_key',
- help="API Key for OpenAI, used as a default if the company-specific key is not set. Format should start with 'sk-'"
- )
- organization = fields.Char(
- string="Organization ID",
- config_parameter='openai_connector.organization',
- help="Organization ID for OpenAI, used as a default if the company-specific ID is not set. Format should start with 'org-'"
- )
-
- connection_status = fields.Char(
- string="Connection Status",
- compute='_compute_connection_status',
- help="Displays the current connection status with OpenAI."
- )
-
- @api.depends('api_key', 'organization')
- def _compute_connection_status(self):
- for record in self:
- try:
- if record.api_key and record.organization: # Vérifie que les champs ne sont pas vides
- record._test_openai_connection()
- record.connection_status = "Connected"
- else:
- record.connection_status = "Disconnected"
- except Exception as e:
- record.connection_status = "Disconnected"
- _logger.error(f"OpenAI connection test failed: {str(e)}")
-
- def set_values(self):
- super(ResConfigSettings, self).set_values()
- self.env['ir.config_parameter'].sudo().set_param(
- 'openai_connector.api_key', self.api_key)
- self.env['ir.config_parameter'].sudo().set_param(
- 'openai_connector.organization', self.organization)
-
- # Test de connexion automatique lors de l'enregistrement si les champs sont remplis
- if self.api_key and self.organization:
- self._test_openai_connection()
-
- @api.model
- def get_values(self):
- res = super(ResConfigSettings, self).get_values()
- res.update(
- api_key=self.env['ir.config_parameter'].sudo().get_param(
- 'openai_connector.api_key', default=''),
- organization=self.env['ir.config_parameter'].sudo().get_param(
- 'openai_connector.organization', default='')
- )
- return res
-
- def _test_openai_connection(self):
- """Method to test connection to OpenAI."""
- if not self.api_key or not self.organization:
- return # Ne fait rien si l'un des champs est vide
-
- try:
- client = openai.OpenAI(organization=self.organization, api_key=self.api_key)
- client.models.list() # Test basique de la connexion
- except Exception as e:
- raise UserError(_("Failed to connect to OpenAI API. Please check the API Key and Organization ID.\nError: %s") % str(e))
-
- def action_test_openai_connection(self):
- """Action to test connection manually and display the result."""
- try:
- self._test_openai_connection()
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _("Connection Test Successful"),
- 'message': _("The connection to OpenAI was successful."),
- 'sticky': False,
- },
- }
- except UserError as e:
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _("Connection Test Failed"),
- 'message': str(e),
- 'sticky': True,
- },
- }
\ No newline at end of file
diff --git a/openai_connector/security/ir.model.access.csv b/openai_connector/security/ir.model.access.csv
deleted file mode 100644
index 1d9caaa..0000000
--- a/openai_connector/security/ir.model.access.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_res_config_settings_openai,res.config.settings.openai,model_res_config_settings,base.group_system,1,1,1,1
diff --git a/openai_connector/views/res_config_settings_views.xml b/openai_connector/views/res_config_settings_views.xml
deleted file mode 100644
index 3d4131e..0000000
--- a/openai_connector/views/res_config_settings_views.xml
+++ /dev/null
@@ -1,46 +0,0 @@
-
-
-
- openai.connector.config.settings.form
- res.config.settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/__init__.py b/openai_partner_purchase_analysis/__init__.py
deleted file mode 100644
index c536983..0000000
--- a/openai_partner_purchase_analysis/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from . import models
-from . import wizard
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/__manifest__.py b/openai_partner_purchase_analysis/__manifest__.py
deleted file mode 100644
index 684df61..0000000
--- a/openai_partner_purchase_analysis/__manifest__.py
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- 'name': 'Partner Purchase Analysis with Optional OpenAI and Queue Job',
- 'version': '1.0',
- 'category': 'Tools',
- 'summary': 'Manage OpenAI connection settings',
- 'license': 'AGPL-3',
- 'description': '''
- This module allows the configuration of OpenAI API connection
- settings, including API key and Organization ID.
- ''',
- 'author': 'Bemade Inc.',
- 'depends': [
- 'base',
- 'sale',
- 'product',
- 'openai_connector',
- 'sale_management',
- ],
- 'data': [
- 'security/ir.model.access.csv', # Fichier de sécurité mis à jour
- 'data/queue_job_group.xml',
- 'views/res_config_settings_view.xml',
- 'views/res_partner_view.xml',
- 'wizard/partner_purchase_analysis_wizard_view.xml',
- ],
- 'external_dependencies': {
- 'python': ['openai'],
- },
- 'installable': True,
- 'application': False,
-}
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/data/queue_job_group.xml b/openai_partner_purchase_analysis/data/queue_job_group.xml
deleted file mode 100644
index a2ac0e3..0000000
--- a/openai_partner_purchase_analysis/data/queue_job_group.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
- Use Queue Job for Asynchronous Processing
-
-
-
diff --git a/openai_partner_purchase_analysis/models/__init__.py b/openai_partner_purchase_analysis/models/__init__.py
deleted file mode 100644
index b861208..0000000
--- a/openai_partner_purchase_analysis/models/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from . import res_partner
-# from . import partner_purchase_analysis_wizard
-from . import res_config_settings
-from . import res_company
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py b/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py
deleted file mode 100644
index b1a6faa..0000000
--- a/openai_partner_purchase_analysis/models/partner_purchase_analysis_wizard.py
+++ /dev/null
@@ -1,93 +0,0 @@
-from odoo import models, fields, api, _
-from odoo.exceptions import UserError
-import openai
-
-try:
- from odoo.addons.queue_job.job import job
-except ImportError:
- job = None # Si queue_job n'est pas disponible, job reste None
-
-
-class PartnerPurchaseAnalysisWizard(models.TransientModel):
- _name = 'partner.purchase.analysis.wizard'
- _description = 'Wizard for Partner Purchase Analysis with OpenAI'
-
- partner_id = fields.Many2one('res.partner', string="Customer", required=True, readonly=True)
- date_start = fields.Date(string="Start Date")
- date_end = fields.Date(string="End Date")
- analysis_result = fields.Text(string="Analysis Result", readonly=True)
-
- def start_analysis(self):
- use_queue = self.env['ir.config_parameter'].sudo().get_param('my_module.use_queue_job')
- if use_queue and job:
- return self.with_delay().perform_analysis()
- else:
- return self.perform_analysis()
-
- def perform_analysis(self):
- """Effectue l'analyse des ventes pour le partenaire sélectionné."""
- selected_categories = self.env.company.product_categories_analyzed
- if selected_categories:
- category_ids = selected_categories.ids
- domain = [
- ('order_id.partner_id', '=', self.partner_id.id),
- ('product_id.categ_id', 'child_of', category_ids),
- ]
- else:
- domain = [('order_id.partner_id', '=', self.partner_id.id)]
-
- if self.date_start:
- domain.append(('order_id.date_order', '>=', self.date_start))
- if self.date_end:
- domain.append(('order_id.date_order', '<=', self.date_end))
-
- sale_order_lines = self.env['sale.order.line'].search(domain)
-
- if not sale_order_lines:
- raise UserError(_("No relevant purchase history found for this customer based on the selected filters."))
-
- # Préparation des données pour l'API d'OpenAI
- purchase_data = {}
- for line in sale_order_lines:
- category_name = line.product_id.categ_id.name or "Uncategorized"
- if category_name not in purchase_data:
- purchase_data[category_name] = {}
- product_name = line.product_id.display_name
- if product_name not in purchase_data[category_name]:
- purchase_data[category_name][product_name] = []
- purchase_data[category_name][product_name].append({
- 'date': line.order_id.date_order,
- 'quantity': line.product_uom_qty,
- 'unit_price': line.price_unit,
- })
-
- # Construire le prompt pour OpenAI
- user_lang = self.env.user.lang or 'en_US'
- user_lang_name = self.env['res.lang'].search([('code', '=', user_lang)], limit=1).name or "English"
-
- purchase_details = f"Customer Purchase Analysis Grouped by Product Category and Product (Response in {user_lang_name}):\n\n"
- for category, products in purchase_data.items():
- purchase_details += f"Category: {category}\n"
- for product, entries in products.items():
- purchase_details += f" Product: {product}\n"
- for entry in entries:
- purchase_details += (
- f" - Date: {entry['date']}, "
- f"Qty: {entry['quantity']}, "
- f"U.Price: {entry['unit_price']}\n"
- )
- purchase_details += "\n"
- purchase_details += "\n"
-
- try:
- response = openai.ChatCompletion.create(
- model="gpt-4",
- messages=[
- {"role": "system",
- "content": f"Analyze the following customer purchase history. Identify trends, product category preferences, and any significant deviations. Respond in {user_lang_name}."},
- {"role": "user", "content": purchase_details}
- ]
- )
- self.analysis_result = response.choices[0].message['content']
- except Exception as e:
- raise UserError(_("Failed to get response from OpenAI. Error: %s") % str(e))
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/res_company.py b/openai_partner_purchase_analysis/models/res_company.py
deleted file mode 100644
index 2582c6c..0000000
--- a/openai_partner_purchase_analysis/models/res_company.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from odoo import models, fields
-
-class ResCompany(models.Model):
- _inherit = 'res.company'
-
- product_categories_analyzed = fields.Many2many(
- 'product.category',
- string="Product Categories Analyzed",
- help="Select product categories to include in the purchase analysis. Only products in these categories and their subcategories will be analyzed."
- )
-
diff --git a/openai_partner_purchase_analysis/models/res_config_settings.py b/openai_partner_purchase_analysis/models/res_config_settings.py
deleted file mode 100644
index b7b1aec..0000000
--- a/openai_partner_purchase_analysis/models/res_config_settings.py
+++ /dev/null
@@ -1,36 +0,0 @@
-from odoo import models, fields, api
-from odoo.modules.module import get_module_resource
-
-class ResConfigSettings(models.TransientModel):
- _inherit = 'res.config.settings'
-
- use_queue_job = fields.Boolean(
- string="Use Queue Job for Asynchronous Processing",
- help="If enabled, the system will use queue jobs for background tasks. Requires the queue_job module.",
- default=False
- )
- product_categories_analyzed = fields.Many2many(
- related='company_id.product_categories_analyzed',
- comodel_name='product.category',
- string="Product Categories Analyzed",
- help="Select product categories to include in the purchase analysis."
- )
-
- @api.model
- def get_values(self):
- res = super().get_values()
- res.update(
- use_queue_job=self.env['ir.config_parameter'].sudo().get_param('my_module.use_queue_job', default=False)
- )
- return res
-
- def set_values(self):
- super().set_values()
- self.env['ir.config_parameter'].sudo().set_param('my_module.use_queue_job', self.use_queue_job)
-
- @api.model
- def enable_queue_job_group(self):
- """Enable the group if the queue_job module is installed"""
- group = self.env.ref('partner_purchase_analysis_with_openai_filtered.group_use_queue_job', raise_if_not_found=False)
- if group and get_module_resource('queue_job'):
- group.sudo().write({'users': [(4, self.env.user.id)]})
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/models/res_partner.py b/openai_partner_purchase_analysis/models/res_partner.py
deleted file mode 100644
index a37f8cf..0000000
--- a/openai_partner_purchase_analysis/models/res_partner.py
+++ /dev/null
@@ -1,20 +0,0 @@
-from odoo import models, fields
-
-class ResPartner(models.Model):
- _inherit = 'res.partner'
-
- sale_analysis = fields.Html("Sale Analysis", help="Analysis of the partner's sales.")
- sale_analysis_date = fields.Date("Sale Analysis Date", help="Date of the latest sale analysis.")
-
- def action_open_purchase_analysis(self):
- """Ouvre le wizard d'analyse des achats pour le client actuel."""
- return {
- 'name': 'Purchase Analysis',
- 'type': 'ir.actions.act_window',
- 'res_model': 'partner.purchase.analysis.wizard',
- 'view_mode': 'form',
- 'target': 'new',
- 'context': {
- 'default_partner_id': self.id,
- },
- }
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/openai_connector b/openai_partner_purchase_analysis/openai_connector
deleted file mode 120000
index a280bd5..0000000
--- a/openai_partner_purchase_analysis/openai_connector
+++ /dev/null
@@ -1 +0,0 @@
-../.repos/bemade-addons/openai_connector
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/security/ir.model.access.csv b/openai_partner_purchase_analysis/security/ir.model.access.csv
deleted file mode 100644
index fe746dd..0000000
--- a/openai_partner_purchase_analysis/security/ir.model.access.csv
+++ /dev/null
@@ -1,2 +0,0 @@
-id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_partner_purchase_analysis_wizard_sales,partner.purchase.analysis.wizard access,model_partner_purchase_analysis_wizard,sales_team.group_sale_salesman,1,1,1,1
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/views/res_config_settings_view.xml b/openai_partner_purchase_analysis/views/res_config_settings_view.xml
deleted file mode 100644
index d2b2269..0000000
--- a/openai_partner_purchase_analysis/views/res_config_settings_view.xml
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
- res.config.settings.view.form.inherit.partner.analysis
- res.config.settings
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/views/res_partner_view.xml b/openai_partner_purchase_analysis/views/res_partner_view.xml
deleted file mode 100644
index 9fb4018..0000000
--- a/openai_partner_purchase_analysis/views/res_partner_view.xml
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- res.partner.form.inherit.purchase.analysis
- res.partner
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/__init__.py b/openai_partner_purchase_analysis/wizard/__init__.py
deleted file mode 100644
index 7e6aa79..0000000
--- a/openai_partner_purchase_analysis/wizard/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import partner_purchase_analysis_wizard
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
deleted file mode 100644
index d4d8fcb..0000000
--- a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard.py
+++ /dev/null
@@ -1,141 +0,0 @@
-from odoo import models, fields, api, _
-from odoo.exceptions import UserError
-from openai import OpenAI
-from datetime import datetime
-from bs4 import BeautifulSoup
-from markupsafe import escape
-
-# Import conditionnel de queue_job pour gérer les files d'attente si elles sont disponibles
-try:
- from odoo.addons.queue_job.job import job
-except ImportError:
- job = None # Si queue_job n'est pas disponible, job reste None
-
-class PartnerPurchaseAnalysisWizard(models.TransientModel):
- _name = 'partner.purchase.analysis.wizard'
- _description = 'Wizard for Partner Purchase Analysis with OpenAI'
-
- partner_id = fields.Many2one('res.partner', string="Customer", required=True, readonly=True)
- date_start = fields.Date(string="Start Date", required=True)
- date_end = fields.Date(string="End Date", default=fields.Date.today, required=True)
-
- def start_analysis(self):
- """Lance l'analyse en arrière-plan si `queue_job` est disponible, sinon exécute immédiatement."""
- use_queue = self.env['ir.config_parameter'].sudo().get_param('my_module.use_queue_job')
- if use_queue and job:
- return self.with_delay().perform_analysis()
- else:
- return self.perform_analysis()
-
- def perform_analysis(self):
- """Effectue l'analyse des ventes pour le partenaire sélectionné."""
-
- # Récupération de l'organisation et de la clé API OpenAI dans les paramètres
- organization = self.env['ir.config_parameter'].sudo().get_param('openai_connector.organization')
- api_key = self.env['ir.config_parameter'].sudo().get_param('openai_connector.api_key')
-
- if not api_key or not organization:
- raise UserError(_("API Key or Organization ID for OpenAI is missing in settings."))
-
- client = OpenAI(
- api_key=api_key,
- organization=organization
- )
-
- selected_categories = self.env.company.product_categories_analyzed
- if selected_categories:
- category_ids = selected_categories.ids
- domain = [
- ('order_id.partner_id', '=', self.partner_id.id),
- ('product_id.categ_id', 'child_of', category_ids),
- ]
- else:
- domain = [('order_id.partner_id', '=', self.partner_id.id)]
-
- if self.date_start:
- domain.append(('order_id.date_order', '>=', self.date_start))
- if self.date_end:
- domain.append(('order_id.date_order', '<=', self.date_end))
-
- sale_order_lines = self.env['sale.order.line'].search(domain)
-
- if not sale_order_lines:
- raise UserError(_("No relevant purchase history found for this customer based on the selected filters."))
-
- # Préparation des données pour le prompt OpenAI
- purchase_data = {}
- for line in sale_order_lines:
- category_name = line.product_id.categ_id.name or "Uncategorized"
- if category_name not in purchase_data:
- purchase_data[category_name] = {}
- product_name = line.product_id.display_name
- if product_name not in purchase_data[category_name]:
- purchase_data[category_name][product_name] = []
- purchase_data[category_name][product_name].append({
- 'date': line.order_id.date_order,
- 'quantity': line.product_uom_qty,
- 'unit_price': line.price_unit,
- })
-
- # Prompt défini en plusieurs lignes pour lisibilité
- user_lang = self.env.user.lang or 'en_US'
- user_lang_name = self.env['res.lang'].search([('code', '=', user_lang)], limit=1).name or "English"
-
- prompt = (
- "Analyze the following customer purchase history. Identify trends, product "
- "category preferences, and any significant deviations. Respond in "
- f"{user_lang_name}. Produce graph and table of the analysis. You output all "
- "in html format."
- )
-
- # Construction de `purchase_details` pour le contenu du prompt
- purchase_details = f"Customer Purchase Analysis Grouped by Product Category and Product:\n\n"
- for category, products in purchase_data.items():
- purchase_details += f"Category: {category}\n"
- for product, entries in products.items():
- purchase_details += f" Product: {product}\n"
- for entry in entries:
- purchase_details += (
- f" - Date: {entry['date']}, "
- f"Qty: {entry['quantity']}, "
- f"U.Price: {entry['unit_price']}\n"
- )
- purchase_details += "\n"
- purchase_details += "\n"
-
- # Appel à l'API OpenAI
- try:
- response = client.chat.completions.create(
- messages=[
- {
- "role": "system",
- "content": prompt
- },
- {
- "role": "user",
- "content": purchase_details
- }
- ],
- model="gpt-4o",
- )
-
- # Supposons que `response` contient la réponse complète au format HTML
- response_text = response.choices[0].message.content
-
- # Parse avec BeautifulSoup pour extraire le contenu du
- soup = BeautifulSoup(response_text, "html.parser")
- body_content = soup.body
-
- # Si body est présent, on utilise son contenu ; sinon, on utilise tout le texte
- cleaned_text = escape(body_content.get_text()) if body_content else escape(response_text)
-
- # Ajouter des balises de base pour structurer le texte en HTML simple
- html_content = f"
{cleaned_text.replace('\n', ' ')}
"
-
- self.partner_id.sale_analysis = body_content
- self.partner_id.sale_analysis_date = fields.Datetime.today()
-# self.sale_analysis = response.choices[0].message.content
-# self.sale_analysis_date = fields.Datetime.today()
- print(response.choices[0].message.content)
- except Exception as e:
- raise UserError(_("Failed to get response from OpenAI. Error: %s") % str(e))
\ No newline at end of file
diff --git a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml b/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml
deleted file mode 100644
index 75f0238..0000000
--- a/openai_partner_purchase_analysis/wizard/partner_purchase_analysis_wizard_view.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
- partner.purchase.analysis.wizard.form
- partner.purchase.analysis.wizard
-
-
-
-
-
-
-
- Analyze Purchases
-
- code
- action
-
-
- action = {
- 'name': 'Purchase Analysis',
- 'type': 'ir.actions.act_window',
- 'res_model': 'partner.purchase.analysis.wizard',
- 'view_mode': 'form',
- 'target': 'new',
- 'context': {
- 'default_partner_id': env.context.get('active_id'),
- }
- }
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/openwebui_integration/Docs/OllamaAPI.md b/openwebui_integration/Docs/OllamaAPI.md
deleted file mode 100644
index 603b15b..0000000
--- a/openwebui_integration/Docs/OllamaAPI.md
+++ /dev/null
@@ -1,1627 +0,0 @@
-# API
-
-## Endpoints
-
-- [Generate a completion](#generate-a-completion)
-- [Generate a chat completion](#generate-a-chat-completion)
-- [Create a Model](#create-a-model)
-- [List Local Models](#list-local-models)
-- [Show Model Information](#show-model-information)
-- [Copy a Model](#copy-a-model)
-- [Delete a Model](#delete-a-model)
-- [Pull a Model](#pull-a-model)
-- [Push a Model](#push-a-model)
-- [Generate Embeddings](#generate-embeddings)
-- [List Running Models](#list-running-models)
-- [Version](#version)
-
-## Conventions
-
-### Model names
-
-Model names follow a `model:tag` format, where `model` can have an optional namespace such as `example/model`. Some examples are `orca-mini:3b-q4_1` and `llama3:70b`. The tag is optional and, if not provided, will default to `latest`. The tag is used to identify a specific version.
-
-### Durations
-
-All durations are returned in nanoseconds.
-
-### Streaming responses
-
-Certain endpoints stream responses as JSON objects. Streaming can be disabled by providing `{"stream": false}` for these endpoints.
-
-## Generate a completion
-
-```
-POST /api/generate
-```
-
-Generate a response for a given prompt with a provided model. This is a streaming endpoint, so there will be a series of responses. The final response object will include statistics and additional data from the request.
-
-### Parameters
-
-- `model`: (required) the [model name](#model-names)
-- `prompt`: the prompt to generate a response for
-- `suffix`: the text after the model response
-- `images`: (optional) a list of base64-encoded images (for multimodal models such as `llava`)
-
-Advanced parameters (optional):
-
-- `format`: the format to return a response in. Format can be `json` or a JSON schema
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
-- `system`: system message to (overrides what is defined in the `Modelfile`)
-- `template`: the prompt template to use (overrides what is defined in the `Modelfile`)
-- `stream`: if `false` the response will be returned as a single response object, rather than a stream of objects
-- `raw`: if `true` no formatting will be applied to the prompt. You may choose to use the `raw` parameter if you are specifying a full templated prompt in your request to the API
-- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
-- `context` (deprecated): the context parameter returned from a previous request to `/generate`, this can be used to keep a short conversational memory
-
-#### Structured outputs
-
-Structured outputs are supported by providing a JSON schema in the `format` parameter. The model will generate a response that matches the schema. See the [structured outputs](#request-structured-outputs) example below.
-
-#### JSON mode
-
-Enable JSON mode by setting the `format` parameter to `json`. This will structure the response as a valid JSON object. See the JSON mode [example](#request-json-mode) below.
-
-> [!IMPORTANT]
-> It's important to instruct the model to use JSON in the `prompt`. Otherwise, the model may generate large amounts whitespace.
-
-### Examples
-
-#### Generate request (Streaming)
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2",
- "prompt": "Why is the sky blue?"
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T08:52:19.385406455-07:00",
- "response": "The",
- "done": false
-}
-```
-
-The final response in the stream also includes additional data about the generation:
-
-- `total_duration`: time spent generating the response
-- `load_duration`: time spent in nanoseconds loading the model
-- `prompt_eval_count`: number of tokens in the prompt
-- `prompt_eval_duration`: time spent in nanoseconds evaluating the prompt
-- `eval_count`: number of tokens in the response
-- `eval_duration`: time in nanoseconds spent generating the response
-- `context`: an encoding of the conversation used in this response, this can be sent in the next request to keep a conversational memory
-- `response`: empty if the response was streamed, if not streamed, this will contain the full response
-
-To calculate how fast the response is generated in tokens per second (token/s), divide `eval_count` / `eval_duration` * `10^9`.
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T19:22:45.499127Z",
- "response": "",
- "done": true,
- "context": [1, 2, 3],
- "total_duration": 10706818083,
- "load_duration": 6338219291,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 130079000,
- "eval_count": 259,
- "eval_duration": 4232710000
-}
-```
-
-#### Request (No streaming)
-
-##### Request
-
-A response can be received in one reply when streaming is off.
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2",
- "prompt": "Why is the sky blue?",
- "stream": false
-}'
-```
-
-##### Response
-
-If `stream` is set to `false`, the response will be a single JSON object:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T19:22:45.499127Z",
- "response": "The sky is blue because it is the color of the sky.",
- "done": true,
- "context": [1, 2, 3],
- "total_duration": 5043500667,
- "load_duration": 5025959,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 325953000,
- "eval_count": 290,
- "eval_duration": 4709213000
-}
-```
-
-#### Request (with suffix)
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "codellama:code",
- "prompt": "def compute_gcd(a, b):",
- "suffix": " return result",
- "options": {
- "temperature": 0
- },
- "stream": false
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "codellama:code",
- "created_at": "2024-07-22T20:47:51.147561Z",
- "response": "\n if a == 0:\n return b\n else:\n return compute_gcd(b % a, a)\n\ndef compute_lcm(a, b):\n result = (a * b) / compute_gcd(a, b)\n",
- "done": true,
- "done_reason": "stop",
- "context": [...],
- "total_duration": 1162761250,
- "load_duration": 6683708,
- "prompt_eval_count": 17,
- "prompt_eval_duration": 201222000,
- "eval_count": 63,
- "eval_duration": 953997000
-}
-```
-
-#### Request (Structured outputs)
-
-##### Request
-
-```shell
-curl -X POST http://localhost:11434/api/generate -H "Content-Type: application/json" -d '{
- "model": "llama3.1:8b",
- "prompt": "Ollama is 22 years old and is busy saving the world. Respond using JSON",
- "stream": false,
- "format": {
- "type": "object",
- "properties": {
- "age": {
- "type": "integer"
- },
- "available": {
- "type": "boolean"
- }
- },
- "required": [
- "age",
- "available"
- ]
- }
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.1:8b",
- "created_at": "2024-12-06T00:48:09.983619Z",
- "response": "{\n \"age\": 22,\n \"available\": true\n}",
- "done": true,
- "done_reason": "stop",
- "context": [1, 2, 3],
- "total_duration": 1075509083,
- "load_duration": 567678166,
- "prompt_eval_count": 28,
- "prompt_eval_duration": 236000000,
- "eval_count": 16,
- "eval_duration": 269000000
-}
-```
-
-#### Request (JSON mode)
-
-> [!IMPORTANT]
-> When `format` is set to `json`, the output will always be a well-formed JSON object. It's important to also instruct the model to respond in JSON.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2",
- "prompt": "What color is the sky at different times of the day? Respond using JSON",
- "format": "json",
- "stream": false
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-11-09T21:07:55.186497Z",
- "response": "{\n\"morning\": {\n\"color\": \"blue\"\n},\n\"noon\": {\n\"color\": \"blue-gray\"\n},\n\"afternoon\": {\n\"color\": \"warm gray\"\n},\n\"evening\": {\n\"color\": \"orange\"\n}\n}\n",
- "done": true,
- "context": [1, 2, 3],
- "total_duration": 4648158584,
- "load_duration": 4071084,
- "prompt_eval_count": 36,
- "prompt_eval_duration": 439038000,
- "eval_count": 180,
- "eval_duration": 4196918000
-}
-```
-
-The value of `response` will be a string containing JSON similar to:
-
-```json
-{
- "morning": {
- "color": "blue"
- },
- "noon": {
- "color": "blue-gray"
- },
- "afternoon": {
- "color": "warm gray"
- },
- "evening": {
- "color": "orange"
- }
-}
-```
-
-#### Request (with images)
-
-To submit images to multimodal models such as `llava` or `bakllava`, provide a list of base64-encoded `images`:
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llava",
- "prompt":"What is in this picture?",
- "stream": false,
- "images": ["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"]
-}'
-```
-
-#### Response
-
-```json
-{
- "model": "llava",
- "created_at": "2023-11-03T15:36:02.583064Z",
- "response": "A happy cartoon character, which is cute and cheerful.",
- "done": true,
- "context": [1, 2, 3],
- "total_duration": 2938432250,
- "load_duration": 2559292,
- "prompt_eval_count": 1,
- "prompt_eval_duration": 2195557000,
- "eval_count": 44,
- "eval_duration": 736432000
-}
-```
-
-#### Request (Raw Mode)
-
-In some cases, you may wish to bypass the templating system and provide a full prompt. In this case, you can use the `raw` parameter to disable templating. Also note that raw mode will not return a context.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "mistral",
- "prompt": "[INST] why is the sky blue? [/INST]",
- "raw": true,
- "stream": false
-}'
-```
-
-#### Request (Reproducible outputs)
-
-For reproducible outputs, set `seed` to a number:
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "mistral",
- "prompt": "Why is the sky blue?",
- "options": {
- "seed": 123
- }
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "mistral",
- "created_at": "2023-11-03T15:36:02.583064Z",
- "response": " The sky appears blue because of a phenomenon called Rayleigh scattering.",
- "done": true,
- "total_duration": 8493852375,
- "load_duration": 6589624375,
- "prompt_eval_count": 14,
- "prompt_eval_duration": 119039000,
- "eval_count": 110,
- "eval_duration": 1779061000
-}
-```
-
-#### Generate request (With options)
-
-If you want to set custom options for the model at runtime rather than in the Modelfile, you can do so with the `options` parameter. This example sets every available option, but you can set any of them individually and omit the ones you do not want to override.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2",
- "prompt": "Why is the sky blue?",
- "stream": false,
- "options": {
- "num_keep": 5,
- "seed": 42,
- "num_predict": 100,
- "top_k": 20,
- "top_p": 0.9,
- "min_p": 0.0,
- "typical_p": 0.7,
- "repeat_last_n": 33,
- "temperature": 0.8,
- "repeat_penalty": 1.2,
- "presence_penalty": 1.5,
- "frequency_penalty": 1.0,
- "mirostat": 1,
- "mirostat_tau": 0.8,
- "mirostat_eta": 0.6,
- "penalize_newline": true,
- "stop": ["\n", "user:"],
- "numa": false,
- "num_ctx": 1024,
- "num_batch": 2,
- "num_gpu": 1,
- "main_gpu": 0,
- "low_vram": false,
- "vocab_only": false,
- "use_mmap": true,
- "use_mlock": false,
- "num_thread": 8
- }
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T19:22:45.499127Z",
- "response": "The sky is blue because it is the color of the sky.",
- "done": true,
- "context": [1, 2, 3],
- "total_duration": 4935886791,
- "load_duration": 534986708,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 107345000,
- "eval_count": 237,
- "eval_duration": 4289432000
-}
-```
-
-#### Load a model
-
-If an empty prompt is provided, the model will be loaded into memory.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2"
-}'
-```
-
-##### Response
-
-A single JSON object is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-12-18T19:52:07.071755Z",
- "response": "",
- "done": true
-}
-```
-
-#### Unload a model
-
-If an empty prompt is provided and the `keep_alive` parameter is set to `0`, a model will be unloaded from memory.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/generate -d '{
- "model": "llama3.2",
- "keep_alive": 0
-}'
-```
-
-##### Response
-
-A single JSON object is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2024-09-12T03:54:03.516566Z",
- "response": "",
- "done": true,
- "done_reason": "unload"
-}
-```
-
-## Generate a chat completion
-
-```
-POST /api/chat
-```
-
-Generate the next message in a chat with a provided model. This is a streaming endpoint, so there will be a series of responses. Streaming can be disabled using `"stream": false`. The final response object will include statistics and additional data from the request.
-
-### Parameters
-
-- `model`: (required) the [model name](#model-names)
-- `messages`: the messages of the chat, this can be used to keep a chat memory
-- `tools`: list of tools in JSON for the model to use if supported
-
-The `message` object has the following fields:
-
-- `role`: the role of the message, either `system`, `user`, `assistant`, or `tool`
-- `content`: the content of the message
-- `images` (optional): a list of images to include in the message (for multimodal models such as `llava`)
-- `tool_calls` (optional): a list of tools in JSON that the model wants to use
-
-Advanced parameters (optional):
-
-- `format`: the format to return a response in. Format can be `json` or a JSON schema.
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
-- `stream`: if `false` the response will be returned as a single response object, rather than a stream of objects
-- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
-
-### Structured outputs
-
-Structured outputs are supported by providing a JSON schema in the `format` parameter. The model will generate a response that matches the schema. See the [Chat request (Structured outputs)](#chat-request-structured-outputs) example below.
-
-### Examples
-
-#### Chat Request (Streaming)
-
-##### Request
-
-Send a chat message with a streaming response.
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "user",
- "content": "why is the sky blue?"
- }
- ]
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T08:52:19.385406455-07:00",
- "message": {
- "role": "assistant",
- "content": "The",
- "images": null
- },
- "done": false
-}
-```
-
-Final response:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T19:22:45.499127Z",
- "done": true,
- "total_duration": 4883583458,
- "load_duration": 1334875,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 342546000,
- "eval_count": 282,
- "eval_duration": 4535599000
-}
-```
-
-#### Chat request (No streaming)
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "user",
- "content": "why is the sky blue?"
- }
- ],
- "stream": false
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-12-12T14:13:43.416799Z",
- "message": {
- "role": "assistant",
- "content": "Hello! How are you today?"
- },
- "done": true,
- "total_duration": 5191566416,
- "load_duration": 2154458,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 383809000,
- "eval_count": 298,
- "eval_duration": 4799921000
-}
-```
-
-#### Chat request (Structured outputs)
-
-##### Request
-
-```shell
-curl -X POST http://localhost:11434/api/chat -H "Content-Type: application/json" -d '{
- "model": "llama3.1",
- "messages": [{"role": "user", "content": "Ollama is 22 years old and busy saving the world. Return a JSON object with the age and availability."}],
- "stream": false,
- "format": {
- "type": "object",
- "properties": {
- "age": {
- "type": "integer"
- },
- "available": {
- "type": "boolean"
- }
- },
- "required": [
- "age",
- "available"
- ]
- },
- "options": {
- "temperature": 0
- }
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.1",
- "created_at": "2024-12-06T00:46:58.265747Z",
- "message": { "role": "assistant", "content": "{\"age\": 22, \"available\": false}" },
- "done_reason": "stop",
- "done": true,
- "total_duration": 2254970291,
- "load_duration": 574751416,
- "prompt_eval_count": 34,
- "prompt_eval_duration": 1502000000,
- "eval_count": 12,
- "eval_duration": 175000000
-}
-```
-
-#### Chat request (With History)
-
-Send a chat message with a conversation history. You can use this same approach to start the conversation using multi-shot or chain-of-thought prompting.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "user",
- "content": "why is the sky blue?"
- },
- {
- "role": "assistant",
- "content": "due to rayleigh scattering."
- },
- {
- "role": "user",
- "content": "how is that different than mie scattering?"
- }
- ]
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T08:52:19.385406455-07:00",
- "message": {
- "role": "assistant",
- "content": "The"
- },
- "done": false
-}
-```
-
-Final response:
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-08-04T19:22:45.499127Z",
- "done": true,
- "total_duration": 8113331500,
- "load_duration": 6396458,
- "prompt_eval_count": 61,
- "prompt_eval_duration": 398801000,
- "eval_count": 468,
- "eval_duration": 7701267000
-}
-```
-
-#### Chat request (with images)
-
-##### Request
-
-Send a chat message with images. The images should be provided as an array, with the individual images encoded in Base64.
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llava",
- "messages": [
- {
- "role": "user",
- "content": "what is in this image?",
- "images": ["iVBORw0KGgoAAAANSUhEUgAAAG0AAABmCAYAAADBPx+VAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAA3VSURBVHgB7Z27r0zdG8fX743i1bi1ikMoFMQloXRpKFFIqI7LH4BEQ+NWIkjQuSWCRIEoULk0gsK1kCBI0IhrQVT7tz/7zZo888yz1r7MnDl7z5xvsjkzs2fP3uu71nNfa7lkAsm7d++Sffv2JbNmzUqcc8m0adOSzZs3Z+/XES4ZckAWJEGWPiCxjsQNLWmQsWjRIpMseaxcuTKpG/7HP27I8P79e7dq1ars/yL4/v27S0ejqwv+cUOGEGGpKHR37tzJCEpHV9tnT58+dXXCJDdECBE2Ojrqjh071hpNECjx4cMHVycM1Uhbv359B2F79+51586daxN/+pyRkRFXKyRDAqxEp4yMlDDzXG1NPnnyJKkThoK0VFd1ELZu3TrzXKxKfW7dMBQ6bcuWLW2v0VlHjx41z717927ba22U9APcw7Nnz1oGEPeL3m3p2mTAYYnFmMOMXybPPXv2bNIPpFZr1NHn4HMw0KRBjg9NuRw95s8PEcz/6DZELQd/09C9QGq5RsmSRybqkwHGjh07OsJSsYYm3ijPpyHzoiacg35MLdDSIS/O1yM778jOTwYUkKNHWUzUWaOsylE00MyI0fcnOwIdjvtNdW/HZwNLGg+sR1kMepSNJXmIwxBZiG8tDTpEZzKg0GItNsosY8USkxDhD0Rinuiko2gfL/RbiD2LZAjU9zKQJj8RDR0vJBR1/Phx9+PHj9Z7REF4nTZkxzX4LCXHrV271qXkBAPGfP/atWvu/PnzHe4C97F48eIsRLZ9+3a3f/9+87dwP1JxaF7/3r17ba+5l4EcaVo0lj3SBq5kGTJSQmLWMjgYNei2GPT1MuMqGTDEFHzeQSP2wi/jGnkmPJ/nhccs44jvDAxpVcxnq0F6eT8h4ni/iIWpR5lPyA6ETkNXoSukvpJAD3AsXLiwpZs49+fPn5ke4j10TqYvegSfn0OnafC+Tv9ooA/JPkgQysqQNBzagXY55nO/oa1F7qvIPWkRL12WRpMWUvpVDYmxAPehxWSe8ZEXL20sadYIozfmNch4QJPAfeJgW3rNsnzphBKNJM2KKODo1rVOMRYik5ETy3ix4qWNI81qAAirizgMIc+yhTytx0JWZuNI03qsrgWlGtwjoS9XwgUhWGyhUaRZZQNNIEwCiXD16tXcAHUs79co0vSD8rrJCIW98pzvxpAWyyo3HYwqS0+H0BjStClcZJT5coMm6D2LOF8TolGJtK9fvyZpyiC5ePFi9nc/oJU4eiEP0jVoAnHa9wyJycITMP78+eMeP37sXrx44d6+fdt6f82aNdkx1pg9e3Zb5W+RSRE+n+VjksQWifvVaTKFhn5O8my63K8Qabdv33b379/PiAP//vuvW7BggZszZ072/+TJk91YgkafPn166zXB1rQHFvouAWHq9z3SEevSUerqCn2/dDCeta2jxYbr69evk4MHDyY7d+7MjhMnTiTPnz9Pfv/+nfQT2ggpO2dMF8cghuoM7Ygj5iWCqRlGFml0QC/ftGmTmzt3rmsaKDsgBSPh0/8yPeLLBihLkOKJc0jp8H8vUzcxIA1k6QJ/c78tWEyj5P3o4u9+jywNPdJi5rAH9x0KHcl4Hg570eQp3+vHXGyrmEeigzQsQsjavXt38ujRo44LQuDDhw+TW7duRS1HGgMxhNXHgflaNTOsHyKvHK5Ijo2jbFjJBQK9YwFd6RVMzfgRBmEfP37suBBm/p49e1qjEP2mwTViNRo0VJWH1deMXcNK08uUjVUu7s/zRaL+oLNxz1bpANco4npUgX4G2eFbpDFyQoQxojBCpEGSytmOH8qrH5Q9vuzD6ofQylkCUmh8DBAr+q8JCyVNtWQIidKQE9wNtLSQnS4jDSsxNHogzFuQBw4cyM61UKVsjfr3ooBkPSqqQHesUPWVtzi9/vQi1T+rJj7WiTz4Pt/l3LxUkr5P2VYZaZ4URpsE+st/dujQoaBBYokbrz/8TJNQYLSonrPS9kUaSkPeZyj1AWSj+d+VBoy1pIWVNed8P0Ll/ee5HdGRhrHhR5GGN0r4LGZBaj8oFDJitBTJzIZgFcmU0Y8ytWMZMzJOaXUSrUs5RxKnrxmbb5YXO9VGUhtpXldhEUogFr3IzIsvlpmdosVcGVGXFWp2oU9kLFL3dEkSz6NHEY1sjSRdIuDFWEhd8KxFqsRi1uM/nz9/zpxnwlESONdg6dKlbsaMGS4EHFHtjFIDHwKOo46l4TxSuxgDzi+rE2jg+BaFruOX4HXa0Nnf1lwAPufZeF8/r6zD97WK2qFnGjBxTw5qNGPxT+5T/r7/7RawFC3j4vTp09koCxkeHjqbHJqArmH5UrFKKksnxrK7FuRIs8STfBZv+luugXZ2pR/pP9Ois4z+TiMzUUkUjD0iEi1fzX8GmXyuxUBRcaUfykV0YZnlJGKQpOiGB76x5GeWkWWJc3mOrK6S7xdND+W5N6XyaRgtWJFe13GkaZnKOsYqGdOVVVbGupsyA/l7emTLHi7vwTdirNEt0qxnzAvBFcnQF16xh/TMpUuXHDowhlA9vQVraQhkudRdzOnK+04ZSP3DUhVSP61YsaLtd/ks7ZgtPcXqPqEafHkdqa84X6aCeL7YWlv6edGFHb+ZFICPlljHhg0bKuk0CSvVznWsotRu433alNdFrqG45ejoaPCaUkWERpLXjzFL2Rpllp7PJU2a/v7Ab8N05/9t27Z16KUqoFGsxnI9EosS2niSYg9SpU6B4JgTrvVW1flt1sT+0ADIJU2maXzcUTraGCRaL1Wp9rUMk16PMom8QhruxzvZIegJjFU7LLCePfS8uaQdPny4jTTL0dbee5mYokQsXTIWNY46kuMbnt8Kmec+LGWtOVIl9cT1rCB0V8WqkjAsRwta93TbwNYoGKsUSChN44lgBNCoHLHzquYKrU6qZ8lolCIN0Rh6cP0Q3U6I6IXILYOQI513hJaSKAorFpuHXJNfVlpRtmYBk1Su1obZr5dnKAO+L10Hrj3WZW+E3qh6IszE37F6EB+68mGpvKm4eb9bFrlzrok7fvr0Kfv727dvWRmdVTJHw0qiiCUSZ6wCK+7XL/AcsgNyL74DQQ730sv78Su7+t/A36MdY0sW5o40ahslXr58aZ5HtZB8GH64m9EmMZ7FpYw4T6QnrZfgenrhFxaSiSGXtPnz57e9TkNZLvTjeqhr734CNtrK41L40sUQckmj1lGKQ0rC37x544r8eNXRpnVE3ZZY7zXo8NomiO0ZUCj2uHz58rbXoZ6gc0uA+F6ZeKS/jhRDUq8MKrTho9fEkihMmhxtBI1DxKFY9XLpVcSkfoi8JGnToZO5sU5aiDQIW716ddt7ZLYtMQlhECdBGXZZMWldY5BHm5xgAroWj4C0hbYkSc/jBmggIrXJWlZM6pSETsEPGqZOndr2uuuR5rF169a2HoHPdurUKZM4CO1WTPqaDaAd+GFGKdIQkxAn9RuEWcTRyN2KSUgiSgF5aWzPTeA/lN5rZubMmR2bE4SIC4nJoltgAV/dVefZm72AtctUCJU2CMJ327hxY9t7EHbkyJFseq+EJSY16RPo3Dkq1kkr7+q0bNmyDuLQcZBEPYmHVdOBiJyIlrRDq41YPWfXOxUysi5fvtyaj+2BpcnsUV/oSoEMOk2CQGlr4ckhBwaetBhjCwH0ZHtJROPJkyc7UjcYLDjmrH7ADTEBXFfOYmB0k9oYBOjJ8b4aOYSe7QkKcYhFlq3QYLQhSidNmtS2RATwy8YOM3EQJsUjKiaWZ+vZToUQgzhkHXudb/PW5YMHD9yZM2faPsMwoc7RciYJXbGuBqJ1UIGKKLv915jsvgtJxCZDubdXr165mzdvtr1Hz5LONA8jrUwKPqsmVesKa49S3Q4WxmRPUEYdTjgiUcfUwLx589ySJUva3oMkP6IYddq6HMS4o55xBJBUeRjzfa4Zdeg56QZ43LhxoyPo7Lf1kNt7oO8wWAbNwaYjIv5lhyS7kRf96dvm5Jah8vfvX3flyhX35cuX6HfzFHOToS1H4BenCaHvO8pr8iDuwoUL7tevX+b5ZdbBair0xkFIlFDlW4ZknEClsp/TzXyAKVOmmHWFVSbDNw1l1+4f90U6IY/q4V27dpnE9bJ+v87QEydjqx/UamVVPRG+mwkNTYN+9tjkwzEx+atCm/X9WvWtDtAb68Wy9LXa1UmvCDDIpPkyOQ5ZwSzJ4jMrvFcr0rSjOUh+GcT4LSg5ugkW1Io0/SCDQBojh0hPlaJdah+tkVYrnTZowP8iq1F1TgMBBauufyB33x1v+NWFYmT5KmppgHC+NkAgbmRkpD3yn9QIseXymoTQFGQmIOKTxiZIWpvAatenVqRVXf2nTrAWMsPnKrMZHz6bJq5jvce6QK8J1cQNgKxlJapMPdZSR64/UivS9NztpkVEdKcrs5alhhWP9NeqlfWopzhZScI6QxseegZRGeg5a8C3Re1Mfl1ScP36ddcUaMuv24iOJtz7sbUjTS4qBvKmstYJoUauiuD3k5qhyr7QdUHMeCgLa1Ear9NquemdXgmum4fvJ6w1lqsuDhNrg1qSpleJK7K3TF0Q2jSd94uSZ60kK1e3qyVpQK6PVWXp2/FC3mp6jBhKKOiY2h3gtUV64TWM6wDETRPLDfSakXmH3w8g9Jlug8ZtTt4kVF0kLUYYmCCtD/DrQ5YhMGbA9L3ucdjh0y8kOHW5gU/VEEmJTcL4Pz/f7mgoAbYkAAAAAElFTkSuQmCC"]
- }
- ]
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llava",
- "created_at": "2023-12-13T22:42:50.203334Z",
- "message": {
- "role": "assistant",
- "content": " The image features a cute, little pig with an angry facial expression. It's wearing a heart on its shirt and is waving in the air. This scene appears to be part of a drawing or sketching project.",
- "images": null
- },
- "done": true,
- "total_duration": 1668506709,
- "load_duration": 1986209,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 359682000,
- "eval_count": 83,
- "eval_duration": 1303285000
-}
-```
-
-#### Chat request (Reproducible outputs)
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "user",
- "content": "Hello!"
- }
- ],
- "options": {
- "seed": 101,
- "temperature": 0
- }
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2023-12-12T14:13:43.416799Z",
- "message": {
- "role": "assistant",
- "content": "Hello! How are you today?"
- },
- "done": true,
- "total_duration": 5191566416,
- "load_duration": 2154458,
- "prompt_eval_count": 26,
- "prompt_eval_duration": 383809000,
- "eval_count": 298,
- "eval_duration": 4799921000
-}
-```
-
-#### Chat request (with tools)
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [
- {
- "role": "user",
- "content": "What is the weather today in Paris?"
- }
- ],
- "stream": false,
- "tools": [
- {
- "type": "function",
- "function": {
- "name": "get_current_weather",
- "description": "Get the current weather for a location",
- "parameters": {
- "type": "object",
- "properties": {
- "location": {
- "type": "string",
- "description": "The location to get the weather for, e.g. San Francisco, CA"
- },
- "format": {
- "type": "string",
- "description": "The format to return the weather in, e.g. 'celsius' or 'fahrenheit'",
- "enum": ["celsius", "fahrenheit"]
- }
- },
- "required": ["location", "format"]
- }
- }
- }
- ]
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at": "2024-07-22T20:33:28.123648Z",
- "message": {
- "role": "assistant",
- "content": "",
- "tool_calls": [
- {
- "function": {
- "name": "get_current_weather",
- "arguments": {
- "format": "celsius",
- "location": "Paris, FR"
- }
- }
- }
- ]
- },
- "done_reason": "stop",
- "done": true,
- "total_duration": 885095291,
- "load_duration": 3753500,
- "prompt_eval_count": 122,
- "prompt_eval_duration": 328493000,
- "eval_count": 33,
- "eval_duration": 552222000
-}
-```
-
-#### Load a model
-
-If the messages array is empty, the model will be loaded into memory.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": []
-}'
-```
-
-##### Response
-
-```json
-{
- "model": "llama3.2",
- "created_at":"2024-09-12T21:17:29.110811Z",
- "message": {
- "role": "assistant",
- "content": ""
- },
- "done_reason": "load",
- "done": true
-}
-```
-
-#### Unload a model
-
-If the messages array is empty and the `keep_alive` parameter is set to `0`, a model will be unloaded from memory.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/chat -d '{
- "model": "llama3.2",
- "messages": [],
- "keep_alive": 0
-}'
-```
-
-##### Response
-
-A single JSON object is returned:
-
-```json
-{
- "model": "llama3.2",
- "created_at":"2024-09-12T21:33:17.547535Z",
- "message": {
- "role": "assistant",
- "content": ""
- },
- "done_reason": "unload",
- "done": true
-}
-```
-
-## Create a Model
-
-```
-POST /api/create
-```
-
-Create a model from:
- * another model;
- * a safetensors directory; or
- * a GGUF file.
-
-If you are creating a model from a safetensors directory or from a GGUF file, you must [create a blob](#create-a-blob) for each of the files and then use the file name and SHA256 digest associated with each blob in the `files` field.
-
-### Parameters
-
-- `model`: name of the model to create
-- `from`: (optional) name of an existing model to create the new model from
-- `files`: (optional) a dictionary of file names to SHA256 digests of blobs to create the model from
-- `adapters`: (optional) a dictionary of file names to SHA256 digests of blobs for LORA adapters
-- `template`: (optional) the prompt template for the model
-- `license`: (optional) a string or list of strings containing the license or licenses for the model
-- `system`: (optional) a string containing the system prompt for the model
-- `parameters`: (optional) a dictionary of parameters for the model (see [Modelfile](./modelfile.md#valid-parameters-and-values) for a list of parameters)
-- `messages`: (optional) a list of message objects used to create a conversation
-- `stream`: (optional) if `false` the response will be returned as a single response object, rather than a stream of objects
-- `quantize` (optional): quantize a non-quantized (e.g. float16) model
-
-#### Quantization types
-
-| Type | Recommended |
-| --- | :-: |
-| q2_K | |
-| q3_K_L | |
-| q3_K_M | |
-| q3_K_S | |
-| q4_0 | |
-| q4_1 | |
-| q4_K_M | * |
-| q4_K_S | |
-| q5_0 | |
-| q5_1 | |
-| q5_K_M | |
-| q5_K_S | |
-| q6_K | |
-| q8_0 | * |
-
-### Examples
-
-#### Create a new model
-
-Create a new model from an existing model.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/create -d '{
- "model": "mario",
- "from": "llama3.2",
- "system": "You are Mario from Super Mario Bros."
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{"status":"reading model metadata"}
-{"status":"creating system layer"}
-{"status":"using already created layer sha256:22f7f8ef5f4c791c1b03d7eb414399294764d7cc82c7e94aa81a1feb80a983a2"}
-{"status":"using already created layer sha256:8c17c2ebb0ea011be9981cc3922db8ca8fa61e828c5d3f44cb6ae342bf80460b"}
-{"status":"using already created layer sha256:7c23fb36d80141c4ab8cdbb61ee4790102ebd2bf7aeff414453177d4f2110e5d"}
-{"status":"using already created layer sha256:2e0493f67d0c8c9c68a8aeacdf6a38a2151cb3c4c1d42accf296e19810527988"}
-{"status":"using already created layer sha256:2759286baa875dc22de5394b4a925701b1896a7e3f8e53275c36f75a877a82c9"}
-{"status":"writing layer sha256:df30045fe90f0d750db82a058109cecd6d4de9c90a3d75b19c09e5f64580bb42"}
-{"status":"writing layer sha256:f18a68eb09bf925bb1b669490407c1b1251c5db98dc4d3d81f3088498ea55690"}
-{"status":"writing manifest"}
-{"status":"success"}
-```
-
-#### Quantize a model
-
-Quantize a non-quantized model.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/create -d '{
- "model": "llama3.1:quantized",
- "from": "llama3.1:8b-instruct-fp16",
- "quantize": "q4_K_M"
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{"status":"quantizing F16 model to Q4_K_M"}
-{"status":"creating new layer sha256:667b0c1932bc6ffc593ed1d03f895bf2dc8dc6df21db3042284a6f4416b06a29"}
-{"status":"using existing layer sha256:11ce4ee3e170f6adebac9a991c22e22ab3f8530e154ee669954c4bc73061c258"}
-{"status":"using existing layer sha256:0ba8f0e314b4264dfd19df045cde9d4c394a52474bf92ed6a3de22a4ca31a177"}
-{"status":"using existing layer sha256:56bb8bd477a519ffa694fc449c2413c6f0e1d3b1c88fa7e3c9d88d3ae49d4dcb"}
-{"status":"creating new layer sha256:455f34728c9b5dd3376378bfb809ee166c145b0b4c1f1a6feca069055066ef9a"}
-{"status":"writing manifest"}
-{"status":"success"}
-```
-
-#### Create a model from GGUF
-
-Create a model from a GGUF file. The `files` parameter should be filled out with the file name and SHA256 digest of the GGUF file you wish to use. Use [/api/blobs/:digest](#push-a-blob) to push the GGUF file to the server before calling this API.
-
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/create -d '{
- "model": "my-gguf-model",
- "files": {
- "test.gguf": "sha256:432f310a77f4650a88d0fd59ecdd7cebed8d684bafea53cbff0473542964f0c3"
- }
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```json
-{"status":"parsing GGUF"}
-{"status":"using existing layer sha256:432f310a77f4650a88d0fd59ecdd7cebed8d684bafea53cbff0473542964f0c3"}
-{"status":"writing manifest"}
-{"status":"success"}
-```
-
-
-#### Create a model from a Safetensors directory
-
-The `files` parameter should include a dictionary of files for the safetensors model which includes the file names and SHA256 digest of each file. Use [/api/blobs/:digest](#push-a-blob) to first push each of the files to the server before calling this API. Files will remain in the cache until the Ollama server is restarted.
-
-##### Request
-
-```shell
-curl http://localhost:11434/api/create -d '{
- "model": "fred",
- "files": {
- "config.json": "sha256:dd3443e529fb2290423a0c65c2d633e67b419d273f170259e27297219828e389",
- "generation_config.json": "sha256:88effbb63300dbbc7390143fbbdd9d9fa50587b37e8bfd16c8c90d4970a74a36",
- "special_tokens_map.json": "sha256:b7455f0e8f00539108837bfa586c4fbf424e31f8717819a6798be74bef813d05",
- "tokenizer.json": "sha256:bbc1904d35169c542dffbe1f7589a5994ec7426d9e5b609d07bab876f32e97ab",
- "tokenizer_config.json": "sha256:24e8a6dc2547164b7002e3125f10b415105644fcf02bf9ad8b674c87b1eaaed6",
- "model.safetensors": "sha256:1ff795ff6a07e6a68085d206fb84417da2f083f68391c2843cd2b8ac6df8538f"
- }
-}'
-```
-
-##### Response
-
-A stream of JSON objects is returned:
-
-```shell
-{"status":"converting model"}
-{"status":"creating new layer sha256:05ca5b813af4a53d2c2922933936e398958855c44ee534858fcfd830940618b6"}
-{"status":"using autodetected template llama3-instruct"}
-{"status":"using existing layer sha256:56bb8bd477a519ffa694fc449c2413c6f0e1d3b1c88fa7e3c9d88d3ae49d4dcb"}
-{"status":"writing manifest"}
-{"status":"success"}
-```
-
-## Check if a Blob Exists
-
-```shell
-HEAD /api/blobs/:digest
-```
-
-Ensures that the file blob (Binary Large Object) used with create a model exists on the server. This checks your Ollama server and not ollama.com.
-
-### Query Parameters
-
-- `digest`: the SHA256 digest of the blob
-
-### Examples
-
-#### Request
-
-```shell
-curl -I http://localhost:11434/api/blobs/sha256:29fdb92e57cf0827ded04ae6461b5931d01fa595843f55d36f5b275a52087dd2
-```
-
-#### Response
-
-Return 200 OK if the blob exists, 404 Not Found if it does not.
-
-## Push a Blob
-
-```
-POST /api/blobs/:digest
-```
-
-Push a file to the Ollama server to create a "blob" (Binary Large Object).
-
-### Query Parameters
-
-- `digest`: the expected SHA256 digest of the file
-
-### Examples
-
-#### Request
-
-```shell
-curl -T model.gguf -X POST http://localhost:11434/api/blobs/sha256:29fdb92e57cf0827ded04ae6461b5931d01fa595843f55d36f5b275a52087dd2
-```
-
-#### Response
-
-Return 201 Created if the blob was successfully created, 400 Bad Request if the digest used is not expected.
-
-## List Local Models
-
-```
-GET /api/tags
-```
-
-List models that are available locally.
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/tags
-```
-
-#### Response
-
-A single JSON object will be returned.
-
-```json
-{
- "models": [
- {
- "name": "codellama:13b",
- "modified_at": "2023-11-04T14:56:49.277302595-07:00",
- "size": 7365960935,
- "digest": "9f438cb9cd581fc025612d27f7c1a6669ff83a8bb0ed86c94fcf4c5440555697",
- "details": {
- "format": "gguf",
- "family": "llama",
- "families": null,
- "parameter_size": "13B",
- "quantization_level": "Q4_0"
- }
- },
- {
- "name": "llama3:latest",
- "modified_at": "2023-12-07T09:32:18.757212583-08:00",
- "size": 3825819519,
- "digest": "fe938a131f40e6f6d40083c9f0f430a515233eb2edaa6d72eb85c50d64f2300e",
- "details": {
- "format": "gguf",
- "family": "llama",
- "families": null,
- "parameter_size": "7B",
- "quantization_level": "Q4_0"
- }
- }
- ]
-}
-```
-
-## Show Model Information
-
-```
-POST /api/show
-```
-
-Show information about a model including details, modelfile, template, parameters, license, system prompt.
-
-### Parameters
-
-- `model`: name of the model to show
-- `verbose`: (optional) if set to `true`, returns full data for verbose response fields
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/show -d '{
- "model": "llama3.2"
-}'
-```
-
-#### Response
-
-```json
-{
- "modelfile": "# Modelfile generated by \"ollama show\"\n# To build a new Modelfile based on this one, replace the FROM line with:\n# FROM llava:latest\n\nFROM /Users/matt/.ollama/models/blobs/sha256:200765e1283640ffbd013184bf496e261032fa75b99498a9613be4e94d63ad52\nTEMPLATE \"\"\"{{ .System }}\nUSER: {{ .Prompt }}\nASSISTANT: \"\"\"\nPARAMETER num_ctx 4096\nPARAMETER stop \"\u003c/s\u003e\"\nPARAMETER stop \"USER:\"\nPARAMETER stop \"ASSISTANT:\"",
- "parameters": "num_keep 24\nstop \"<|start_header_id|>\"\nstop \"<|end_header_id|>\"\nstop \"<|eot_id|>\"",
- "template": "{{ if .System }}<|start_header_id|>system<|end_header_id|>\n\n{{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>\n\n{{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>\n\n{{ .Response }}<|eot_id|>",
- "details": {
- "parent_model": "",
- "format": "gguf",
- "family": "llama",
- "families": [
- "llama"
- ],
- "parameter_size": "8.0B",
- "quantization_level": "Q4_0"
- },
- "model_info": {
- "general.architecture": "llama",
- "general.file_type": 2,
- "general.parameter_count": 8030261248,
- "general.quantization_version": 2,
- "llama.attention.head_count": 32,
- "llama.attention.head_count_kv": 8,
- "llama.attention.layer_norm_rms_epsilon": 0.00001,
- "llama.block_count": 32,
- "llama.context_length": 8192,
- "llama.embedding_length": 4096,
- "llama.feed_forward_length": 14336,
- "llama.rope.dimension_count": 128,
- "llama.rope.freq_base": 500000,
- "llama.vocab_size": 128256,
- "tokenizer.ggml.bos_token_id": 128000,
- "tokenizer.ggml.eos_token_id": 128009,
- "tokenizer.ggml.merges": [], // populates if `verbose=true`
- "tokenizer.ggml.model": "gpt2",
- "tokenizer.ggml.pre": "llama-bpe",
- "tokenizer.ggml.token_type": [], // populates if `verbose=true`
- "tokenizer.ggml.tokens": [] // populates if `verbose=true`
- }
-}
-```
-
-## Copy a Model
-
-```
-POST /api/copy
-```
-
-Copy a model. Creates a model with another name from an existing model.
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/copy -d '{
- "source": "llama3.2",
- "destination": "llama3-backup"
-}'
-```
-
-#### Response
-
-Returns a 200 OK if successful, or a 404 Not Found if the source model doesn't exist.
-
-## Delete a Model
-
-```
-DELETE /api/delete
-```
-
-Delete a model and its data.
-
-### Parameters
-
-- `model`: model name to delete
-
-### Examples
-
-#### Request
-
-```shell
-curl -X DELETE http://localhost:11434/api/delete -d '{
- "model": "llama3:13b"
-}'
-```
-
-#### Response
-
-Returns a 200 OK if successful, 404 Not Found if the model to be deleted doesn't exist.
-
-## Pull a Model
-
-```
-POST /api/pull
-```
-
-Download a model from the ollama library. Cancelled pulls are resumed from where they left off, and multiple calls will share the same download progress.
-
-### Parameters
-
-- `model`: name of the model to pull
-- `insecure`: (optional) allow insecure connections to the library. Only use this if you are pulling from your own library during development.
-- `stream`: (optional) if `false` the response will be returned as a single response object, rather than a stream of objects
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/pull -d '{
- "model": "llama3.2"
-}'
-```
-
-#### Response
-
-If `stream` is not specified, or set to `true`, a stream of JSON objects is returned:
-
-The first object is the manifest:
-
-```json
-{
- "status": "pulling manifest"
-}
-```
-
-Then there is a series of downloading responses. Until any of the download is completed, the `completed` key may not be included. The number of files to be downloaded depends on the number of layers specified in the manifest.
-
-```json
-{
- "status": "downloading digestname",
- "digest": "digestname",
- "total": 2142590208,
- "completed": 241970
-}
-```
-
-After all the files are downloaded, the final responses are:
-
-```json
-{
- "status": "verifying sha256 digest"
-}
-{
- "status": "writing manifest"
-}
-{
- "status": "removing any unused layers"
-}
-{
- "status": "success"
-}
-```
-
-if `stream` is set to false, then the response is a single JSON object:
-
-```json
-{
- "status": "success"
-}
-```
-
-## Push a Model
-
-```
-POST /api/push
-```
-
-Upload a model to a model library. Requires registering for ollama.ai and adding a public key first.
-
-### Parameters
-
-- `model`: name of the model to push in the form of `/:`
-- `insecure`: (optional) allow insecure connections to the library. Only use this if you are pushing to your library during development.
-- `stream`: (optional) if `false` the response will be returned as a single response object, rather than a stream of objects
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/push -d '{
- "model": "mattw/pygmalion:latest"
-}'
-```
-
-#### Response
-
-If `stream` is not specified, or set to `true`, a stream of JSON objects is returned:
-
-```json
-{ "status": "retrieving manifest" }
-```
-
-and then:
-
-```json
-{
- "status": "starting upload",
- "digest": "sha256:bc07c81de745696fdf5afca05e065818a8149fb0c77266fb584d9b2cba3711ab",
- "total": 1928429856
-}
-```
-
-Then there is a series of uploading responses:
-
-```json
-{
- "status": "starting upload",
- "digest": "sha256:bc07c81de745696fdf5afca05e065818a8149fb0c77266fb584d9b2cba3711ab",
- "total": 1928429856
-}
-```
-
-Finally, when the upload is complete:
-
-```json
-{"status":"pushing manifest"}
-{"status":"success"}
-```
-
-If `stream` is set to `false`, then the response is a single JSON object:
-
-```json
-{ "status": "success" }
-```
-
-## Generate Embeddings
-
-```
-POST /api/embed
-```
-
-Generate embeddings from a model
-
-### Parameters
-
-- `model`: name of model to generate embeddings from
-- `input`: text or list of text to generate embeddings for
-
-Advanced parameters:
-
-- `truncate`: truncates the end of each input to fit within context length. Returns error if `false` and context length is exceeded. Defaults to `true`
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
-- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/embed -d '{
- "model": "all-minilm",
- "input": "Why is the sky blue?"
-}'
-```
-
-#### Response
-
-```json
-{
- "model": "all-minilm",
- "embeddings": [[
- 0.010071029, -0.0017594862, 0.05007221, 0.04692972, 0.054916814,
- 0.008599704, 0.105441414, -0.025878139, 0.12958129, 0.031952348
- ]],
- "total_duration": 14143917,
- "load_duration": 1019500,
- "prompt_eval_count": 8
-}
-```
-
-#### Request (Multiple input)
-
-```shell
-curl http://localhost:11434/api/embed -d '{
- "model": "all-minilm",
- "input": ["Why is the sky blue?", "Why is the grass green?"]
-}'
-```
-
-#### Response
-
-```json
-{
- "model": "all-minilm",
- "embeddings": [[
- 0.010071029, -0.0017594862, 0.05007221, 0.04692972, 0.054916814,
- 0.008599704, 0.105441414, -0.025878139, 0.12958129, 0.031952348
- ],[
- -0.0098027075, 0.06042469, 0.025257962, -0.006364387, 0.07272725,
- 0.017194884, 0.09032035, -0.051705178, 0.09951512, 0.09072481
- ]]
-}
-```
-
-## List Running Models
-```
-GET /api/ps
-```
-
-List models that are currently loaded into memory.
-
-#### Examples
-
-### Request
-
-```shell
-curl http://localhost:11434/api/ps
-```
-
-#### Response
-
-A single JSON object will be returned.
-
-```json
-{
- "models": [
- {
- "name": "mistral:latest",
- "model": "mistral:latest",
- "size": 5137025024,
- "digest": "2ae6f6dd7a3dd734790bbbf58b8909a606e0e7e97e94b7604e0aa7ae4490e6d8",
- "details": {
- "parent_model": "",
- "format": "gguf",
- "family": "llama",
- "families": [
- "llama"
- ],
- "parameter_size": "7.2B",
- "quantization_level": "Q4_0"
- },
- "expires_at": "2024-06-04T14:38:31.83753-07:00",
- "size_vram": 5137025024
- }
- ]
-}
-```
-
-## Generate Embedding
-
-> Note: this endpoint has been superseded by `/api/embed`
-
-```
-POST /api/embeddings
-```
-
-Generate embeddings from a model
-
-### Parameters
-
-- `model`: name of model to generate embeddings from
-- `prompt`: text to generate embeddings for
-
-Advanced parameters:
-
-- `options`: additional model parameters listed in the documentation for the [Modelfile](./modelfile.md#valid-parameters-and-values) such as `temperature`
-- `keep_alive`: controls how long the model will stay loaded into memory following the request (default: `5m`)
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/embeddings -d '{
- "model": "all-minilm",
- "prompt": "Here is an article about llamas..."
-}'
-```
-
-#### Response
-
-```json
-{
- "embedding": [
- 0.5670403838157654, 0.009260174818336964, 0.23178744316101074, -0.2916173040866852, -0.8924556970596313,
- 0.8785552978515625, -0.34576427936553955, 0.5742510557174683, -0.04222835972905159, -0.137906014919281
- ]
-}
-```
-
-## Version
-
-```
-GET /api/version
-```
-
-Retrieve the Ollama version
-
-### Examples
-
-#### Request
-
-```shell
-curl http://localhost:11434/api/version
-```
-
-#### Response
-
-```json
-{
- "version": "0.5.1"
-}
-```
-
diff --git a/openwebui_integration/Spécifications.md b/openwebui_integration/Spécifications.md
deleted file mode 100644
index 0a5b5c1..0000000
--- a/openwebui_integration/Spécifications.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Spécifications du module OpenWebUI Integration
-
-## Objectif
-Ce module fournit une intégration complète entre Odoo et OpenWebUI, permettant l'utilisation de modèles d'IA avancés dans diverses fonctionnalités d'Odoo. Il sert de base pour tous les modules qui souhaitent utiliser les capacités d'IA d'OpenWebUI.
-
-## Architecture du Module
-
-### Structure des Répertoires
-```
-openwebui_integration/
-├── controllers/ # Contrôleurs pour les endpoints web
-├── models/ # Modèles de données
-├── security/ # Fichiers de sécurité et accès
-├── static/ # Ressources statiques
-└── views/ # Vues XML Odoo
-```
-
-## Composants Principaux
-
-### 1. OpenWebUI Bot Mixin (`openwebui.bot.mixin`)
-Mixin permettant d'ajouter des fonctionnalités de bot à n'importe quel modèle Odoo.
-
-#### Fonctionnalités clés:
-- Gestion du contexte du bot
-- Génération et traitement des messages
-- Gestion des réponses
-- Points de personnalisation spécifiques au modèle
-
-#### Méthodes principales:
-- `_get_bot_context()`: Récupère le contexte et les paramètres du bot
-- `_apply_logic()`: Applique la logique du bot à un enregistrement
-- `_generate_bot_message()`: Génère le message à envoyer (à surcharger)
-- `_process_bot_response()`: Traite la réponse du bot (à surcharger)
-
-### 2. Modèle OpenWebUI (`openwebui.model`)
-Gère les modèles d'IA disponibles via OpenWebUI.
-
-#### Caractéristiques:
-- Modèles par défaut: GPT-3.5 Turbo, GPT-4, Claude 2
-- Identifiant unique par entreprise
-- Gestion de l'état actif/inactif
-- Support des modèles temporaires pour les tests
-
-#### Contraintes de sécurité:
-- Création manuelle interdite (sauf modèles temporaires)
-- Modifications limitées (activation/désactivation uniquement)
-- Suppression contrôlée
-
-### 3. Configuration par Entreprise
-Extension du modèle `res.company` pour la configuration OpenWebUI.
-
-#### Paramètres configurables:
-- `openwebui_enabled`: Activation de l'intégration
-- `openwebui_api_url`: URL de l'API
-- `openwebui_api_key`: Clé d'API
-- `openwebui_verify_ssl`: Vérification du certificat SSL
-- `openwebui_timeout`: Délai d'attente des requêtes
-- `openwebui_default_model_id`: Modèle d'IA par défaut à utiliser
- - Sélectionnable uniquement parmi les modèles actifs
- - Utilisé comme modèle par défaut pour toutes les requêtes IA
- - Peut être surchargé au niveau des modules spécifiques
-
-#### Fonctionnalités de gestion:
-- Test de connexion à l'API
-- Synchronisation des modèles disponibles
-- Gestion des modèles par entreprise
-
-## Sécurité et Gestion des Erreurs
-
-### Sécurité
-- Authentification API via clé
-- Vérification SSL configurable
-- Contrôle d'accès par entreprise
-- Protection contre la création/modification non autorisée
-
-### Gestion des Erreurs
-- Validation des paramètres de connexion
-- Gestion des timeouts
-- Traitement des erreurs API
-- Validation des réponses
-
-## Intégration et Utilisation
-
-### Étapes d'installation
-1. Installation du module via Odoo
-2. Configuration des paramètres OpenWebUI dans la configuration de l'entreprise
-3. Test de la connexion API
-4. Synchronisation initiale des modèles
-
-### Développement d'Extensions
-1. Hériter du mixin `openwebui.bot.mixin`
-2. Implémenter les méthodes de génération et traitement
-3. Configurer les paramètres spécifiques au modèle
-4. Gérer les réponses selon les besoins
-
-### Maintenance
-- Nettoyage périodique des modèles temporaires
-- Surveillance des timeouts et erreurs
-- Mise à jour des modèles disponibles
-
-## Dépendances
-- Module `mail` d'Odoo
-- Accès à une instance OpenWebUI
-- Python 3.x avec support SSL
-
-## Notes Techniques
-- Utilisation de requêtes HTTP asynchrones
-- Cache des réponses API pour optimisation
-- Support multi-entreprises
-- Extensible pour différents cas d'usage
\ No newline at end of file
diff --git a/openwebui_integration/__init__.py b/openwebui_integration/__init__.py
deleted file mode 100644
index f7209b1..0000000
--- a/openwebui_integration/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from . import models
-from . import controllers
diff --git a/openwebui_integration/__manifest__.py b/openwebui_integration/__manifest__.py
deleted file mode 100644
index b7c0964..0000000
--- a/openwebui_integration/__manifest__.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-{
- 'name': 'OpenWebUI Integration',
- 'version': '18.0.1.1.0',
- 'summary': 'Core integration with OpenWebUI',
- 'sequence': 10,
- 'description': """
-OpenWebUI Integration
-=====================
-This module provides core integration with OpenWebUI.
-
-Features:
----------
-* Connect to OpenWebUI API
-* Manage models and configurations
-* Base bot functionality and configuration
-* Authentication and access control
-""",
- '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',
- ],
- 'installable': True,
- 'application': False,
- 'auto_install': False,
- 'license': 'LGPL-3',
- 'i18n': True,
- 'post_init_hook': 'post_init_hook',
-}
diff --git a/openwebui_integration/controllers/__init__.py b/openwebui_integration/controllers/__init__.py
deleted file mode 100644
index 12a7e52..0000000
--- a/openwebui_integration/controllers/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-from . import main
diff --git a/openwebui_integration/controllers/main.py b/openwebui_integration/controllers/main.py
deleted file mode 100644
index 89753a1..0000000
--- a/openwebui_integration/controllers/main.py
+++ /dev/null
@@ -1,22 +0,0 @@
-from odoo import http, fields
-from odoo.http import request
-
-class OpenWebUIController(http.Controller):
- @http.route('/openwebui/config', type='json', auth='user')
- def get_config(self):
- company = request.env.company
- user_companies = request.env.user.company_ids.ids
-
- return {
- 'version': '1.0',
- 'enabled': company.openwebui_enabled,
- 'company_id': company.id,
- 'allowed_company_ids': user_companies,
- 'company_name': company.name,
- 'model': company.openwebui_model,
- 'api_url': company.openwebui_api_url,
- 'max_tokens': company.openwebui_max_tokens,
- 'temperature': company.openwebui_temperature,
- 'timeout': company.openwebui_timeout,
- 'use_ssl': company.openwebui_use_ssl,
- }
diff --git a/openwebui_integration/hooks.py b/openwebui_integration/hooks.py
deleted file mode 100644
index 9ea29cc..0000000
--- a/openwebui_integration/hooks.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-
-def post_init_hook(cr, registry):
- """Post-init hook for initializing new columns."""
- # Initialize ollama_port with default value
- cr.execute("""
- ALTER TABLE res_company
- ADD COLUMN IF NOT EXISTS ollama_port integer DEFAULT 11434;
- """)
\ No newline at end of file
diff --git a/openwebui_integration/models/__init__.py b/openwebui_integration/models/__init__.py
deleted file mode 100644
index aab818f..0000000
--- a/openwebui_integration/models/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- 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 res_company
diff --git a/openwebui_integration/models/openwebui_bot_mixin.py b/openwebui_integration/models/openwebui_bot_mixin.py
deleted file mode 100644
index 41fd8ea..0000000
--- a/openwebui_integration/models/openwebui_bot_mixin.py
+++ /dev/null
@@ -1,223 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-"""OpenWebUI Bot Mixin Module
-
-This module provides a mixin class that can be used to add OpenWebUI bot
-functionality to any Odoo model. It handles the integration between Odoo
-models and OpenWebUI's AI models, providing:
-
-- Bot context management
-- Message generation and processing
-- Response handling
-- Model-specific customization points
-
-Example usage:
- class MyModel(models.Model, OpenWebUIBotMixin):
- _name = 'my.model'
-
- def _generate_bot_message(self, record, values, command=None):
- return f"Process this: {record.name}"
-
- def _process_bot_response(self, values, response):
- values['processed_text'] = response
- return values
-"""
-
-import json
-import logging
-import re
-import time
-import requests
-from odoo import models, _
-from odoo.exceptions import UserError
-
-_logger = logging.getLogger(__name__)
-
-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_model(self, model):
- """Gets the model to use"""
- return model
-
- def _apply_logic(self, record, values, command=None):
- """Applies the model logic to a record."""
- model = values.get('bot')
- if not model:
- return values
-
- # Get the model
- model = self._get_model(model)
-
- # Generate the message for the model
- message = self._generate_bot_message(record, values, command)
-
- # Paramètres de retry
- max_retries = 3
- base_delay = 2 # délai initial en secondes
-
- # Obtenir le timeout de la configuration de la compagnie
- company = self.env.company
- timeout = company.openwebui_timeout
-
- last_error = None
- for attempt in range(max_retries):
- try:
- # Send the message to the model
- _logger.info('Attempt %d/%d: Sending message to model (timeout=%ds)',
- attempt + 1, max_retries, timeout)
- _logger.info('Sending message to model: %r', message)
- response = model.send_message(message=message, timeout=timeout)
- _logger.info('Raw response from model: %r', response)
-
- # Si la réponse contient une erreur de connexion, on la traite comme une exception
- if isinstance(response, str) and "Connection error" in response:
- _logger.error('Connection error in response: %r', response)
- raise ConnectionError(response)
-
- _logger.info('Processed response from model: %r', response)
-
- if not response:
- raise ValueError("Empty response from model")
-
- if not isinstance(response, str):
- raise ValueError(f"Invalid response type: {type(response)}")
-
- # Debug de la réponse brute
- _logger.info('Raw response from model: %r', response)
-
- # Clean up the response
- # Supprimer les blocs de code markdown et autres caractères problématiques
- response = re.sub(r'```json\n|\n```', '', response.strip())
- response = re.sub(r'\\([^\\])', r'\1', response) # Supprimer les backslashes simples
- response = re.sub(r'">\\n', '",', response) # Corriger le format des fins de lignes
- response = re.sub(r'\\n\s*]\\n"}$', ']}', response) # Corriger la fin du JSON
- _logger.info('Response after cleanup: %r', response)
-
- # Debug des lignes individuelles
- lines = response.strip().split('\n')
- _logger.info('Number of response lines: %d', len(lines))
- for i, line in enumerate(lines):
- _logger.info('Line %d: %r', i + 1, line)
-
- # Paramètres de retry pour le parsing JSON
- max_json_retries = 3
- json_retry_delay = 2 # secondes
- last_json_error = None
-
- for json_attempt in range(max_json_retries):
- try:
- # Pour Ollama, la réponse peut être une série de JSON séparés par des newlines
- # On prend le dernier JSON qui devrait être la réponse finale
- json_responses = [json.loads(line) for line in response.strip().split('\n') if line.strip()]
- if json_responses:
- parsed_response = json_responses[-1] # Prendre le dernier JSON
- else:
- parsed_response = json.loads(response)
-
- # Si on arrive ici, le parsing a réussi
- break
-
- except json.JSONDecodeError as e:
- last_json_error = e
- if json_attempt < max_json_retries - 1:
- _logger.warning('JSON parsing attempt %d/%d failed: %s. Retrying in %d seconds...',
- json_attempt + 1, max_json_retries, str(e), json_retry_delay)
- time.sleep(json_retry_delay)
- else:
- _logger.error('All %d JSON parsing attempts failed. Last error: %s',
- max_json_retries, str(e))
- raise ValueError(f"Failed to parse JSON after {max_json_retries} attempts: {str(e)}")
-
- # Si c'est un dictionnaire avec une clé 'response', extraire la valeur
- if isinstance(parsed_response, dict):
- if 'response' in parsed_response:
- # Pour Ollama, la réponse est directement dans la clé 'response'
- clean_response = parsed_response['response']
- return self._process_bot_response(values, clean_response)
- clean_response = response
- # Si le parsing initial a échoué, essayer d'extraire une liste JSON
- if last_json_error is not None:
- _logger.warning('Failed to parse response directly, trying to extract JSON list')
- start = response.find('[')
- end = response.rfind(']')
-
- if start == -1 or end == -1:
- _logger.error('No JSON list markers found in response: %r', response)
- raise ValueError("No JSON list found in response")
-
- clean_response = response[start:end + 1]
- _logger.debug('Extracted JSON list: %r', clean_response)
-
- # Nouveau cycle de retry pour le JSON extrait
- for json_attempt in range(max_json_retries):
- try:
- parsed_response = json.loads(clean_response)
- # Si on arrive ici, le parsing a réussi
- break
- except json.JSONDecodeError as e:
- last_json_error = e
- if json_attempt < max_json_retries - 1:
- _logger.warning('Extracted JSON parsing attempt %d/%d failed: %s. Retrying in %d seconds...',
- json_attempt + 1, max_json_retries, str(e), json_retry_delay)
- time.sleep(json_retry_delay)
- else:
- _logger.error('All %d extracted JSON parsing attempts failed. Last error: %s',
- max_json_retries, str(e))
- raise ValueError("Invalid JSON list in response")
-
- # Extraire la liste de produits si présente
- if isinstance(parsed_response, dict):
- if 'products' in parsed_response:
- parsed_response = parsed_response['products']
- elif 'response' in parsed_response:
- # Cas précédent où la réponse est dans la clé 'response'
- parsed_response = json.loads(parsed_response['response'])
-
- if not isinstance(parsed_response, list):
- _logger.error('Response is not a list: %r', parsed_response)
- raise ValueError("Response must be a JSON list")
-
- _logger.info('Successfully parsed response as list with %d items', len(parsed_response))
-
- # Si on arrive ici, tout s'est bien passé
- # Update the values with the cleaned response
- values = self._process_bot_response(values, clean_response)
- return values
-
- except (ConnectionError, TimeoutError) as e:
- last_error = e
- if attempt < max_retries - 1: # Ne pas attendre après la dernière tentative
- delay = base_delay * (2 ** attempt) # Délai exponentiel: 2s, 4s, 8s
- _logger.warning('Connection error on attempt %d: %s. Retrying in %d seconds...',
- attempt + 1, str(e), delay)
- time.sleep(delay)
- else:
- _logger.error('All %d connection attempts failed. Last error: %s', max_retries, str(e))
-
- except Exception as e:
- # Pour les autres erreurs, on ne réessaie pas
- _logger.error('Non-connection error occurred: %s', str(e))
- raise e
-
- # Si on arrive ici, toutes les tentatives ont échoué
- raise last_error
-
- def _generate_bot_message(self, record, values, command=None):
- """Generates the message to send to the bot.
- To be overridden in child classes."""
- return ''
-
- def _process_bot_response(self, values, response):
- """Handles the bot response.
- To be overridden in child classes."""
- return values
diff --git a/openwebui_integration/models/openwebui_model.py b/openwebui_integration/models/openwebui_model.py
deleted file mode 100644
index 2fd418c..0000000
--- a/openwebui_integration/models/openwebui_model.py
+++ /dev/null
@@ -1,535 +0,0 @@
-# -*-
-"""
-OpenWebUI Model Module
-
-This module defines the models needed for OpenWebUI integration in Odoo.
-It includes user management, roles, and customizable interfaces.
-"""
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-import json
-import logging
-import requests
-from odoo import models, fields, api, _
-from odoo.exceptions import UserError, AccessError
-
-_logger = logging.getLogger(__name__)
-
-_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, in installation mode, or during sync
- if not (vals.get('is_temp') or self.env.context.get('install_mode') or self.env.context.get('sync_models')):
- 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 send_message(self, message, message_history=None, context=None, instructions=None, timeout=None):
- """Sends a message to the model and returns its response
-
- Args:
- message (str): The message to send
- message_history (list): Optional list of previous messages in the format
- [{'role': 'user'|'assistant', 'content': 'message'}, ...]
- context (dict): Optional context to pass to the model
- instructions (str): Optional system instructions
- timeout (int): Optional timeout in seconds for this request
-
- Returns:
- str: The model's response or error message
- """
- self.ensure_one()
- company = self.env.company
-
- # Base data structure
- if company.ai_provider == 'ollama':
- # Format for Ollama API
- data = {
- 'model': self.identifier,
- 'stream': False, # We want a single response
- 'options': {
- 'num_ctx': company.openwebui_context_size,
- 'temperature': 0.7 # Default temperature
- }
- }
-
- # Handle message history and current message
- if message_history:
- messages = []
- for msg in message_history:
- messages.append({
- 'role': msg['role'],
- 'content': msg['content']
- })
- messages.append({'role': 'user', 'content': message})
- data['messages'] = messages
- else:
- # For single message, use simple prompt
- if instructions:
- # If system instructions are provided, use chat format
- data['messages'] = [
- {'role': 'system', 'content': instructions},
- {'role': 'user', 'content': message}
- ]
- else:
- # For simple queries, use generate endpoint
- data = {
- 'model': self.identifier,
- 'prompt': message,
- 'stream': False,
- 'options': data['options']
- }
- endpoint = 'api/generate'
-
- if not 'endpoint' in locals():
- endpoint = 'api/chat'
-
- else: # openwebui
- # Format for OpenWebUI API
- data = {
- 'model': self.identifier,
- 'messages': message_history or []
- }
- data['messages'].append({'role': 'user', 'content': message})
-
- if instructions:
- data['system'] = instructions
- if context:
- data['context'] = context
-
- data['options'] = {
- 'num_ctx': company.openwebui_context_size
- }
- endpoint = 'chat/completions'
-
- _logger.info('Sending request to %s with data: %r', endpoint, data)
- success, result = self._make_request(endpoint, method='POST', data=data, timeout=timeout)
-
- if not success:
- _logger.error('Request failed: %s', result)
- return f"Error: {result}"
-
- _logger.info('Raw API response: %r', result)
-
- try:
- # Handle Ollama response format
- if isinstance(result, dict):
- if 'response' in result: # Ollama chat/generate response
- return result['response']
- elif 'message' in result: # Ollama chat response alternative format
- return result['message']['content']
- elif 'choices' in result: # OpenWebUI format
- return result['choices'][0]['message']['content']
- else:
- _logger.error('Unexpected API response format: %r', result)
- return f"Error: Unexpected response format from API"
-
- # Handle streaming response (should not happen with stream=False)
- elif isinstance(result, str):
- try:
- # Parse the last line of a streaming response
- lines = [line.strip() for line in result.split('\n') if line.strip()]
- if lines:
- last_response = json.loads(lines[-1])
- if 'response' in last_response:
- return last_response['response']
- except json.JSONDecodeError as e:
- _logger.error('Failed to parse streaming response: %s', e)
- return result # Return raw string if parsing fails
-
- return f"Error: Unexpected response type: {type(result)}"
-
- except Exception as e:
- _logger.error('Error processing API response: %s', str(e))
- return f"Error processing response: {str(e)}"
-
- def cleanup_temp_models(self):
- """Clean up temporary models from the database.
-
- This method automatically removes models marked as temporary,
- including:
- - Models with identifier starting with 'test_'
- - Models with identifier starting with 'refresh_'
- - Temporary models created before the current date
-
- Note:
- Deletion is performed with 'sync_models' context to bypass
- standard deletion restrictions.
- """
- 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):
- """Get the OpenWebUI configuration for the current company.
-
- Returns:
- dict: A dictionary containing the OpenWebUI configuration:
- {
- 'enabled': bool, # Whether integration is enabled
- 'api_url': str, # OpenWebUI API URL
- 'api_key': str, # API key for authentication
- 'verify_ssl': bool, # SSL certificate verification
- 'timeout': int # Request timeout in seconds
- }
- """
- 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):
- """Make a request to the OpenWebUI API.
-
- Args:
- endpoint (str): API endpoint (without /api/ prefix)
- method (str, optional): HTTP method to use. Defaults to 'GET'.
- data (dict, optional): Data to send for POST/PUT/PATCH methods.
- files (dict, optional): Files to send as multipart/form-data.
- timeout (int, optional): Custom request timeout in seconds.
-
- Returns:
- tuple: A tuple (success, result) where:
- - success (bool): True if request succeeded, False otherwise
- - result (dict|str): JSON response if success, error message if failure
-
- Note:
- The method automatically handles:
- - API key authentication
- - SSL verification
- - Content-Type headers
- - Timeouts and connection errors
- """
- config = self._get_company_config()
-
- if not config['enabled']:
- return False, "OpenWebUI is not enabled"
-
- try:
- api_url = config['api_url']
- if not api_url:
- return False, "OpenWebUI API URL is not configured"
-
- # Add scheme if missing
- if not api_url.startswith(('http://', 'https://')):
- api_url = f'http://{api_url}'
-
- # Check if port is included in URL
- from urllib.parse import urlparse
- parsed_url = urlparse(api_url)
- if not parsed_url.port:
- # Use Ollama port from company configuration
- company = self.env.company
- netloc = parsed_url.netloc
- if ':' in netloc:
- host = netloc.split(':')[0]
- else:
- host = netloc
- api_url = f"{parsed_url.scheme}://{host}:{company.ollama_port}"
-
- # Pour Ollama, les endpoints incluent déjà /api/
- if endpoint.startswith('api/'):
- url = f"{api_url.rstrip('/')}/{endpoint.lstrip('/')}"
- else:
- url = f"{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
-
- # Send the request
- response = requests.request(method=method, url=url, **kwargs)
- response.raise_for_status()
-
- # Log raw response
- response_text = response.text.strip()
- _logger.info('Raw response text: %r', response_text)
-
- # Split response into lines (Ollama may send multiple JSON objects)
- response_lines = [line.strip() for line in response_text.split('\n') if line.strip()]
-
- # Try to parse each line as JSON
- parsed_responses = []
- for line in response_lines:
- try:
- parsed_json = json.loads(line)
- if isinstance(parsed_json, dict):
- # Handle Ollama message format
- if 'message' in parsed_json and 'content' in parsed_json['message']:
- content = parsed_json['message']['content']
- if content:
- try:
- # Try to parse content as JSON
- content_json = json.loads(content)
- parsed_responses.append(content_json)
- except json.JSONDecodeError:
- # Content is not JSON
- parsed_responses.append(content)
- else:
- parsed_responses.append(parsed_json)
- except json.JSONDecodeError:
- # Skip invalid JSON lines
- continue
-
- # Return the last valid parsed response
- if parsed_responses:
- return True, parsed_responses[-1]
-
- # If no valid JSON found, return the raw text
- return True, response_text
-
- except requests.exceptions.SSLError:
- return False, "SSL/TLS verification failed"
- 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 (TypeError, AttributeError) as e:
- return False, f"Invalid request parameters: {str(e)}"
- except KeyError as e:
- return False, f"Missing required configuration: {str(e)}"
-
-
- def test_connection(self):
- """Test the connection to the OpenWebUI API.
-
- This method performs a simple request to the /api/chat endpoint
- to verify that:
- 1. The API URL is accessible
- 2. The credentials are valid
- 3. The SSL configuration is correct
-
- Returns:
- tuple: A tuple (success, result) where:
- - success (bool): True if test succeeded
- - result (dict|str): Models list if success, error message if failure
-
- Note:
- Uses a reduced timeout of 5 seconds to avoid long waits.
- """
- return self._make_request('api/chat', timeout=5)
-
- @api.model
- def get_available_models(self):
- """Get the list of available models from the OpenWebUI/Ollama API.
-
- This method attempts to retrieve the list of models from the API.
- For Ollama, it uses the /api/tags endpoint.
- If it fails, it returns a default list of common models.
-
- Returns:
- list: A list of tuples (id, name) of available models.
- Example: [('llama2', 'Llama 2'), ('mistral', 'Mistral')]
-
- Note:
- - Handles both Ollama and OpenWebUI API response formats
- - Returns DEFAULT_MODELS in case of error or unexpected format
- - Errors are logged but don't interrupt execution
- """
- # Try Ollama endpoint first
- success, result = self._make_request('api/tags')
-
- if not success:
- # Try OpenWebUI endpoint as fallback
- success, result = self._make_request('models')
- if not success:
- _logger.error(f"Error fetching models: {result}")
- return DEFAULT_MODELS
-
- try:
- # Handle Ollama response format
- if isinstance(result, dict) and 'models' in result:
- return [(model['name'], model.get('name', model['name'])) for model in result['models']]
- # Handle OpenWebUI response format
- elif 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 API")
- return DEFAULT_MODELS
- except (KeyError, TypeError, AttributeError) as e:
- _logger.error(f"Error processing models response: {str(e)}")
- return DEFAULT_MODELS
-
- def sync_models(self):
- """Synchronize models from OpenWebUI.
-
- This method ensures that local models match those available in OpenWebUI.
- It will create new models and update existing ones as needed.
-
- Returns:
- tuple: A tuple (success, result) where:
- - success (bool): True if synchronization succeeded
- - result (str|None): Error message if failed, None if succeeded
- """
- self.ensure_one()
- success, result = self._sync_models()
- return success, result
-
- @api.model
- def _sync_models(self):
- """Internal method to synchronize models from OpenWebUI/Ollama.
-
- This method performs the actual synchronization work:
- 1. Cleans up temporary models
- 2. Fetches current models from the API
- 3. Creates new models that don't exist locally
-
- Returns:
- tuple: A tuple (success, result) where:
- - success (bool): True if synchronization succeeded
- - result (str|None): Error message if failed, None if succeeded
-
- Note:
- This is an internal method called by sync_models().
- It should not be called directly unless you need fine-grained control.
- Supports both Ollama and OpenWebUI API formats.
- """
- # First clean up temporary models
- self.cleanup_temp_models()
-
- # Try Ollama endpoint first
- success, result = self._make_request('api/tags')
-
- if not success:
- # Try OpenWebUI endpoint as fallback
- success, result = self._make_request('models')
- if not success:
- return False, result
-
- try:
- # Handle Ollama response format
- if isinstance(result, dict) and 'models' in result:
- models_data = result['models']
- for model_data in models_data:
- existing = self.search([('identifier', '=', model_data['name'])])
- if not existing:
- self.with_context(sync_models=True).create({
- 'name': model_data['name'],
- 'identifier': model_data['name'],
- 'description': model_data.get('description', ''),
- })
- # Handle OpenWebUI response format
- else:
- 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 (KeyError, TypeError) as e:
- _logger.error(f"Error processing model data: {str(e)}")
- return False, f"Error processing model data: {str(e)}"
- return False, f"Invalid model data format: {str(e)}"
- except odoo.exceptions.AccessError as e:
- _logger.error(f"Access error while creating model: {str(e)}")
- return False, f"Permission denied: {str(e)}"
diff --git a/openwebui_integration/models/res_company.py b/openwebui_integration/models/res_company.py
deleted file mode 100644
index b3ce479..0000000
--- a/openwebui_integration/models/res_company.py
+++ /dev/null
@@ -1,300 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-This module extends the res.company model to add OpenWebUI configuration.
-It manages OpenWebUI integration settings at the company level,
-including service activation, API keys, and other configuration parameters.
-"""
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, fields, api, tools, _
-from odoo.exceptions import UserError
-
-
-class ResCompany(models.Model):
- _inherit = 'res.company'
-
-
-
- ai_provider = fields.Selection([
- ('openwebui', 'OpenWebUI'),
- ('ollama', 'Ollama')
- ],
- string='AI Provider',
- default='openwebui',
- required=True,
- help="Select the AI provider to use"
- )
-
- # Ollama Configuration
- ollama_port = fields.Integer(
- string='Ollama Port',
- default=11434,
- required=True,
- help="Port number for the Ollama server (default: 11434)"
- )
-
- # OpenWebUI Configuration
- 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=60,
- help="Maximum wait time for API calls"
- )
-
- openwebui_context_size = fields.Integer(
- string='Context Window Size',
- default=4096,
- help="Maximum number of tokens in the context window (default: 4096)"
- )
-
- openwebui_max_products = fields.Integer(
- string='Maximum Products per Batch',
- default=800,
- help="Maximum number of products that can be processed at once (default: 800)"
- )
-
- def test_ollama_connection(self):
- """Test the connection to Ollama server.
-
- Returns:
- dict: A notification action with the test result
- """
- self.ensure_one()
-
- if self.ai_provider != 'ollama':
- raise UserError(_("Please select Ollama as the AI provider first."))
-
- try:
- import requests
- # Test connection to Ollama server
- url = f'http://localhost:{self.ollama_port}/api/tags'
- response = requests.get(url, timeout=5)
- response.raise_for_status()
-
- # If we get here, the connection was successful
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Successfully connected to Ollama server on port %s', self.ollama_port),
- 'sticky': False,
- 'type': 'success',
- }
- }
-
- except requests.exceptions.ConnectionError:
- raise UserError(_("Could not connect to Ollama server. Please check if Ollama is running and the port number is correct."))
- except requests.exceptions.Timeout:
- raise UserError(_("Connection to Ollama server timed out. Please check your network settings."))
- except requests.exceptions.RequestException as e:
- raise UserError(_("Error connecting to Ollama server: %s", str(e)))
-
- openwebui_products_per_request = fields.Integer(
- string='Products per Request',
- default=10,
- help="Number of products to process in a single API request"
- )
-
- openwebui_models_ids = fields.One2many(
- comodel_name='openwebui.model',
- inverse_name='company_id',
- string='OpenWebUI Models',
- )
-
- openwebui_default_model_id = fields.Many2one(
- comodel_name='openwebui.model',
- string='Default Model',
- domain="[('company_id', '=', id), ('is_active', '=', True)]",
- help="Default OpenWebUI model to use for AI requests"
- )
-
- openwebui_context_size = fields.Integer(
- string='Context Window Size',
- default=2048,
- help="Size of the context window in tokens (e.g., 2048, 4096, 8192)"
- )
-
- def test_openwebui_connection(self):
- """Test the connection to OpenWebUI API.
-
- This method verifies the connection to OpenWebUI by:
- 1. Checking if OpenWebUI integration is enabled
- 2. Attempting to connect to the configured API endpoint
- 3. Validating the API credentials
-
- Returns:
- dict: A notification action with the test results:
- {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': 'Success',
- 'message': str,
- 'type': 'success',
- 'sticky': False
- }
- }
-
- Raises:
- UserError: If any of the following conditions occur:
- - OpenWebUI is not enabled for the company
- - API endpoint is not reachable
- - Invalid API credentials
- - Connection timeout
- """
- self.ensure_one()
-
- if not self.openwebui_enabled:
- raise UserError(_("OpenWebUI is not enabled for this company."))
-
- success, result = self.env['openwebui.model'].test_connection()
- if not success:
- raise UserError(_("Connection test failed: %s") % result)
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Connection to OpenWebUI successful!'),
- 'sticky': False,
- 'type': 'success',
- }
- }
-
- def refresh_model_list(self):
- """Synchronize available AI models with Odoo.
-
- This method performs the following operations based on the selected AI provider:
-
- For OpenWebUI:
- 1. Verifies that OpenWebUI integration is enabled
- 2. Connects to the OpenWebUI API to fetch the latest model list
- 3. Updates the local database with model information
-
- For Ollama:
- 1. Connects to the Ollama server
- 2. Fetches available models using the Ollama API
- 3. Updates the local database with model information
-
- Returns:
- dict: A notification action with the sync results
-
- Raises:
- UserError: If there are connection or synchronization issues
- """
- self.ensure_one()
-
- if self.ai_provider == 'ollama':
- return self._refresh_ollama_models()
- elif self.ai_provider == 'openwebui':
- if not self.openwebui_enabled:
- raise UserError(_("OpenWebUI is not enabled for this company."))
- return self._refresh_openwebui_models()
- else:
- raise UserError(_("Unknown AI provider: %s", self.ai_provider))
-
- def _refresh_ollama_models(self):
- """Fetch and sync available models from Ollama server."""
- try:
- import requests
- url = f'http://localhost:{self.ollama_port}/api/tags'
- response = requests.get(url, timeout=5)
- response.raise_for_status()
- models_data = response.json()
-
- # Get the OpenWebUI model object
- Model = self.env['openwebui.model'].with_context(sync_models=True)
-
- # Process each model from Ollama
- for model in models_data.get('models', []):
- name = model.get('name', '')
- if not name:
- continue
-
- # Split name and tag if present (format: name:tag)
- identifier = name
- if ':' in name:
- name, tag = name.split(':', 1)
- identifier = f"{name}:{tag}"
- else:
- identifier = f"{name}:latest"
-
- # Check if model already exists
- existing_model = Model.search([
- ('identifier', '=', identifier),
- ('company_id', '=', self.id)
- ], limit=1)
-
- model_vals = {
- 'name': name,
- 'identifier': identifier,
- 'is_active': True,
- 'description': f"Ollama model: {name}",
- 'company_id': self.id
- }
-
- if existing_model:
- # Update existing model
- existing_model.write(model_vals)
- else:
- # Create new model
- Model.create(model_vals)
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Successfully synchronized Ollama models'),
- 'sticky': False,
- 'type': 'success',
- }
- }
-
- except requests.exceptions.ConnectionError:
- raise UserError(_("Could not connect to Ollama server. Please check if Ollama is running and the port number is correct."))
- except requests.exceptions.Timeout:
- raise UserError(_("Connection to Ollama server timed out. Please check your network settings."))
- except requests.exceptions.RequestException as e:
- raise UserError(_("Error connecting to Ollama server: %s", str(e)))
- except Exception as e:
- raise UserError(_("Error synchronizing Ollama models: %s", str(e)))
-
- def _refresh_openwebui_models(self):
- """Fetch and sync available models from OpenWebUI."""
- success, result = self.env['openwebui.model'].sync_models()
- if not success:
- raise UserError(_("Failed to refresh models: %s") % result)
-
- return {
- 'type': 'ir.actions.client',
- 'tag': 'display_notification',
- 'params': {
- 'title': _('Success'),
- 'message': _('Successfully synchronized OpenWebUI models'),
- 'sticky': False,
- 'type': 'success',
- }
- }
diff --git a/openwebui_integration/security/ir.model.access.csv b/openwebui_integration/security/ir.model.access.csv
deleted file mode 100644
index b4484fa..0000000
--- a/openwebui_integration/security/ir.model.access.csv
+++ /dev/null
@@ -1,3 +0,0 @@
-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,openwebui_integration.group_openwebui_user,1,0,0,0
-access_openwebui_model_admin,access_openwebui_model_admin,model_openwebui_model,openwebui_integration.group_openwebui_admin,1,1,1,1
diff --git a/openwebui_integration/security/openwebui_security.xml b/openwebui_integration/security/openwebui_security.xml
deleted file mode 100644
index 4f5b5c9..0000000
--- a/openwebui_integration/security/openwebui_security.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
-
-
diff --git a/openwebui_integration/security/security.xml b/openwebui_integration/security/security.xml
deleted file mode 100644
index 714424f..0000000
--- a/openwebui_integration/security/security.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-
-
-
- OpenWebUI
- Gère les accès aux fonctionnalités OpenWebUI
- 20
-
-
-
-
- Utilisateur
-
-
-
-
-
-
- Administrateur
-
-
-
-
-
-
-
- Modèles OpenWebUI: règle multi-société
-
- [('company_id', 'in', company_ids)]
-
-
-
-
-
-
diff --git a/openwebui_integration/static/description/icon.png b/openwebui_integration/static/description/icon.png
deleted file mode 100644
index ce62a5a..0000000
Binary files a/openwebui_integration/static/description/icon.png and /dev/null differ
diff --git a/openwebui_integration/static/src/css/openwebui.css b/openwebui_integration/static/src/css/openwebui.css
deleted file mode 100644
index fdddc22..0000000
--- a/openwebui_integration/static/src/css/openwebui.css
+++ /dev/null
@@ -1,12 +0,0 @@
-.o_openwebui_container {
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
-}
-
-.o_openwebui_content {
- flex: 1;
- padding: 16px;
- overflow: auto;
-}
diff --git a/openwebui_integration/static/src/js/openwebui.js b/openwebui_integration/static/src/js/openwebui.js
deleted file mode 100644
index c8c9852..0000000
--- a/openwebui_integration/static/src/js/openwebui.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/** @odoo-module **/
-
-import { registry } from "@web/core/registry";
-import { session } from "@web/session";
-import { Component } from "@odoo/owl";
-
-export class OpenWebUIAction extends Component {
- static template = "openwebui_integration.ClientAction";
-
- setup() {
- super.setup();
- this.state = {
- openwebui_enabled: false,
- openwebui_model: '',
- openwebui_api_url: ''
- };
- this.loadCompanyConfig();
- }
-
- async loadCompanyConfig() {
- const company = await this.env.services.orm.read(
- 'res.company',
- [session.company_id],
- ['openwebui_enabled', 'openwebui_api_url', 'openwebui_model']
- );
- this.state = company[0];
- }
-}
-
-registry.category("actions").add("openwebui_action", OpenWebUIAction);
diff --git a/openwebui_integration/static/src/scss/openwebui.scss b/openwebui_integration/static/src/scss/openwebui.scss
deleted file mode 100644
index 377aa89..0000000
--- a/openwebui_integration/static/src/scss/openwebui.scss
+++ /dev/null
@@ -1,26 +0,0 @@
-.o_mail_message_typing {
- opacity: 0.7;
-
- .typing-indicator {
- display: inline-block;
- margin-left: 8px;
-
- span {
- display: inline-block;
- width: 8px;
- height: 8px;
- margin: 0 2px;
- background-color: currentColor;
- border-radius: 50%;
- animation: typing 1s infinite;
-
- &:nth-child(2) { animation-delay: 0.2s; }
- &:nth-child(3) { animation-delay: 0.4s; }
- }
- }
-}
-
-@keyframes typing {
- 0%, 100% { transform: translateY(0); }
- 50% { transform: translateY(-4px); }
-}
diff --git a/openwebui_integration/static/src/xml/templates.xml b/openwebui_integration/static/src/xml/templates.xml
deleted file mode 100644
index 330e595..0000000
--- a/openwebui_integration/static/src/xml/templates.xml
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
- Le bot est en train d'écrire...
-
-
-
-
-
-
-
-
-
diff --git a/openwebui_integration/views/res_company_views.xml b/openwebui_integration/views/res_company_views.xml
deleted file mode 100644
index 8138acf..0000000
--- a/openwebui_integration/views/res_company_views.xml
+++ /dev/null
@@ -1,98 +0,0 @@
-
-
-
- res.company.form.inherit.openwebui
- res.company
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/openwebui_integration_chat/Spécifications.md b/openwebui_integration_chat/Spécifications.md
deleted file mode 100644
index e69de29..0000000
diff --git a/openwebui_integration_chat/__init__.py b/openwebui_integration_chat/__init__.py
deleted file mode 100644
index 8c67f89..0000000
--- a/openwebui_integration_chat/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from . import models
-from . import controllers
diff --git a/openwebui_integration_chat/__manifest__.py b/openwebui_integration_chat/__manifest__.py
deleted file mode 100644
index 6727881..0000000
--- a/openwebui_integration_chat/__manifest__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-{
- 'name': 'OpenWebUI Integration Chat',
- 'version': '1.0',
- 'summary': 'Chat integration with OpenWebUI for Discuss',
- 'sequence': 11,
- 'description': """
-OpenWebUI Integration Chat
-=========================
-This module extends OpenWebUI Integration to provide chatbot features in Discuss.
-
-Features:
----------
-* Integrate chatbots with Discuss channels
-* Control access to bots by channels and users
-* Chat-specific bot configurations and behaviors
-""",
- 'category': 'Discuss',
- 'website': 'https://www.odoo.com/app/discuss',
- 'depends': [
- 'mail',
- 'openwebui_integration'
- ],
- 'data': [
- 'security/security.xml',
- 'security/ir.model.access.csv',
- 'views/discuss_channel_views.xml',
- 'views/openwebui_bot_views.xml',
- ],
- 'installable': True,
- 'application': False,
- 'auto_install': False,
- 'license': 'LGPL-3',
-}
diff --git a/openwebui_integration_chat/models/__init__.py b/openwebui_integration_chat/models/__init__.py
deleted file mode 100644
index 9382e1d..0000000
--- a/openwebui_integration_chat/models/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from . import discuss_channel
-from . import res_users
-from . import openwebui_bot
\ No newline at end of file
diff --git a/openwebui_integration_chat/models/discuss_channel.py b/openwebui_integration_chat/models/discuss_channel.py
deleted file mode 100644
index 3dbf791..0000000
--- a/openwebui_integration_chat/models/discuss_channel.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, fields, api, _
-
-class DiscussChannel(models.Model):
- _inherit = 'discuss.channel'
-
- bot_id = fields.Many2one(
- comodel_name='openwebui.bot',
- string='AI Bot',
- help="AI Bot associated with this channel"
- )
-
- def _get_bot_domain(self, company_id):
- """Get domain for available bots."""
- return [
- ('company_id', '=', company_id),
- ('is_active', '=', True),
- ]
-
- @api.model
- def channel_get(self, partners_to):
- """Override channel_get to handle bot channels."""
- channel = super().channel_get(partners_to)
-
- # Check if any of the partners is a bot
- if len(partners_to) == 2: # One-to-one chat
- bot_partner_ids = self.env['openwebui.bot'].search([('partner_id', 'in', partners_to)]).mapped('partner_id').ids
- if bot_partner_ids:
- bot = self.env['openwebui.bot'].search([('partner_id', 'in', bot_partner_ids)], limit=1)
- if bot:
- channel.write({'bot_id': bot.id})
-
- return channel
-
- @api.returns('mail.message', lambda value: value.id)
- def message_post(self, **kwargs):
- """Override message_post to handle bot messages."""
- message = super().message_post(**kwargs)
-
- # If this is a bot channel, let the bot handle the message
- if self.bot_id and message.author_id != self.bot_id.partner_id:
- self.bot_id._handle_message(message)
-
- return message
diff --git a/openwebui_integration_chat/models/openwebui_bot.py b/openwebui_integration_chat/models/openwebui_bot.py
deleted file mode 100644
index 855a337..0000000
--- a/openwebui_integration_chat/models/openwebui_bot.py
+++ /dev/null
@@ -1,545 +0,0 @@
-# -*- coding: utf-8 -*-
-# Part of Odoo. See LICENSE file for full copyright and licensing details.
-
-from odoo import models, fields, api, _, modules
-from odoo.exceptions import ValidationError
-import logging
-from markupsafe import Markup
-import base64
-
-_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."""
- # Get OdooBot's image
- image_path = modules.get_module_resource('mail', 'static/src/img', 'odoobot.png')
- image_base64 = False
- if image_path:
- with open(image_path, 'rb') as f:
- image_base64 = base64.b64encode(f.read())
-
- 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,
- 'image_1920': image_base64,
- 'active': False, # Archivé comme OdooBot
- })
-
- @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
-
- if vals.get('instructions'):
- vals['instructions'] = self._convert_markdown_to_html(vals['instructions'])
-
- # 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 name changes and activation/deactivation."""
- # Store old name for channel updates
- old_names = {bot.id: bot.name for bot in self} if 'name' in vals else {}
-
- if vals.get('instructions'):
- vals['instructions'] = self._convert_markdown_to_html(vals['instructions'])
-
- res = super().write(vals)
-
- # Handle name change
- if 'name' in vals:
- for bot in self:
- # Update partner name
- if bot.partner_id:
- # Get OdooBot's image if partner doesn't have one
- if not bot.partner_id.image_1920:
- odoobot = self.env.ref('base.partner_root', raise_if_not_found=False)
- image_1920 = odoobot.image_1920 if odoobot else None
- else:
- image_1920 = bot.partner_id.image_1920
-
- bot.partner_id.sudo().write({
- 'name': vals['name'],
- 'email': f"{vals['name'].lower().replace(' ', '.')}@bot.internal",
- 'image_1920': image_1920,
- })
-
- # Update channel names
- old_name = old_names.get(bot.id)
- if old_name:
- channels = self.env['discuss.channel'].sudo().search([
- ('bot_id', '=', bot.id),
- ('channel_type', '=', 'chat')
- ])
- for channel in channels:
- # Only update if the channel name contains the old bot name
- if old_name in channel.name:
- new_name = channel.name.replace(old_name, vals['name'])
- channel.write({'name': new_name})
- _logger.info(
- "Updated channel name from '%s' to '%s' for bot %s",
- channel.name, new_name, bot.name
- )
-
- # Handle activation
- 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)
- )
-
- # Get conversation history from the channel
- domain = [
- ('model', '=', 'discuss.channel'),
- ('res_id', '=', channel.id),
- ('message_type', '=', 'comment'),
- ('id', '<=', message.id) # Only get messages up to current message
- ]
-
- # Limit to last 10 messages for context
- history_messages = self.env['mail.message'].search(domain, limit=10, order='id desc')
-
- # Build message history in the format expected by the API
- message_history = []
- for msg in reversed(history_messages[:-1]): # Exclude current message
- role = 'assistant' if msg.author_id == self.partner_id else 'user'
- message_history.append({
- 'role': role,
- 'content': msg.body
- })
-
- # Process message with OpenWebUI
- response = self.model_id.send_message(
- message=message.body,
- message_history=message_history,
- context=self.context,
- instructions=self.instructions
- )
-
- _logger.info("Got response from OpenWebUI: %s", bool(response))
-
- if response:
- # Convert markdown response to HTML
- html_response = self._convert_markdown_to_odoo_html(response)
-
- # Post the response
- _logger.info("Posting response to channel %s", channel.name)
- channel.sudo().message_post(
- body=html_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)
-
- def _convert_markdown_to_odoo_html(self, text):
- """Convert markdown response to Odoo-compatible HTML."""
- if not text:
- return ""
-
- lines = text.split('\n')
- html = []
- in_list = False
-
- for line in lines:
- line = line.strip()
- if not line:
- continue
-
- # Section separator
- if line.startswith('---'):
- html.append(Markup(''))
- continue
-
- # Headers
- if line.startswith('###'):
- line = line.replace('###', '').strip()
- line = line.replace('**', '') # Remove bold from headers
- html.append(Markup('
{}
').format(line))
- continue
-
- # Lists
- if line.startswith('- '):
- if not in_list:
- html.append(Markup('
'))
- in_list = True
- line = line[2:] # Remove the '- '
- line = line.replace('**', '', 1).replace('**', '', 1)
- html.append(Markup('
{}
').format(Markup(line)))
- continue
-
- # Numbered lists
- if line[0].isdigit() and line[1:].startswith('. '):
- if not in_list:
- html.append(Markup(''))
- in_list = True
- line = line[line.find(' ')+1:] # Remove the number and dot
- line = line.replace('**', '', 1).replace('**', '', 1)
- html.append(Markup('
{}
').format(Markup(line)))
- continue
-
- # End list if line doesn't match list format
- if in_list and not (line.startswith('- ') or (line[0].isdigit() and line[1:].startswith('. '))):
- html.append(Markup('
') if str(html[-2]).startswith('
'))
- in_list = False
-
- # Regular text with bold
- if not in_list:
- line = line.replace('**', '', 1).replace('**', '', 1)
- html.append(Markup('
{}
').format(Markup(line)))
-
- # Close any open list
- if in_list:
- html.append(Markup('
') if str(html[-2]).startswith('
'))
-
- return Markup('\n').join(html)
-
- def _convert_markdown_to_html(self, text):
- """Convert markdown-like text to Odoo-compatible HTML."""
- if not text:
- return ""
-
- html_parts = []
- current_section = []
- in_list = False
-
- for line in text.split('\n'):
- line = line.strip()
-
- # Skip empty lines
- if not line:
- if current_section:
- html_parts.append('
' + '\n'.join(current_section) + '
')
- current_section = []
- continue
-
- # Section separator
- if line.startswith('---'):
- if current_section:
- html_parts.append('
' + '\n'.join(current_section) + '
')
- current_section = []
- continue
-
- # Headers
- if line.startswith('###'):
- if current_section:
- html_parts.append('
' + '\n'.join(current_section) + '
')
- current_section = []
- line = line.replace('###', '').strip()
- if '**' in line:
- line = line.replace('**', '')
- html_parts.append(f'
{line}
')
- continue
-
- # Lists
- if line.startswith('- '):
- if not in_list:
- if current_section:
- html_parts.append('