diff --git a/odoo_to_odoo_sync/models/sync_log.py b/odoo_to_odoo_sync/models/sync_log.py
index e6e90ba..9e29d7e 100644
--- a/odoo_to_odoo_sync/models/sync_log.py
+++ b/odoo_to_odoo_sync/models/sync_log.py
@@ -23,7 +23,6 @@ class OdooSyncLog(models.Model):
string='Name',
compute='_compute_name'
)
-
queue_id = fields.Many2one(
comodel_name='odoo.sync.queue',
string='Queue Entry',
@@ -38,7 +37,7 @@ class OdooSyncLog(models.Model):
required=True
)
message = fields.Text(
- string='Message',
+ string='Message'
)
details = fields.Text(
string='Technical Details'
diff --git a/openwebui_integration/Spécifications.md b/openwebui_integration/Spécifications.md
new file mode 100644
index 0000000..0a5b5c1
--- /dev/null
+++ b/openwebui_integration/Spécifications.md
@@ -0,0 +1,110 @@
+# 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/__manifest__.py b/openwebui_integration/__manifest__.py
index a25db3f..a502e12 100644
--- a/openwebui_integration/__manifest__.py
+++ b/openwebui_integration/__manifest__.py
@@ -2,20 +2,20 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': 'OpenWebUI Integration',
- 'version': '1.0',
- 'summary': 'Integration with OpenWebUI for chatbots',
+ 'version': '18.0.1.0.1',
+ 'summary': 'Core integration with OpenWebUI',
'sequence': 10,
'description': """
OpenWebUI Integration
=====================
-This module provides integration with OpenWebUI to create and manage chatbots.
+This module provides core integration with OpenWebUI.
Features:
---------
* Connect to OpenWebUI API
* Manage models and configurations
-* Create chatbots with specific behaviors
-* Control access to bots by channels and users
+* Base bot functionality and configuration
+* Authentication and access control
""",
'category': 'Discuss',
'website': 'https://www.odoo.com/app/discuss',
@@ -24,10 +24,10 @@ Features:
'security/security.xml',
'security/ir.model.access.csv',
'views/res_company_views.xml',
- 'views/openwebui_bot_views.xml',
],
'installable': True,
'application': False,
'auto_install': False,
'license': 'LGPL-3',
+ 'i18n': True,
}
diff --git a/openwebui_integration/migrations/18.0.1.0.1/__init__.py b/openwebui_integration/migrations/18.0.1.0.1/__init__.py
new file mode 100644
index 0000000..c533a53
--- /dev/null
+++ b/openwebui_integration/migrations/18.0.1.0.1/__init__.py
@@ -0,0 +1 @@
+from . import pre-migration
diff --git a/openwebui_integration/migrations/18.0.1.0.1/pre-migration.py b/openwebui_integration/migrations/18.0.1.0.1/pre-migration.py
new file mode 100644
index 0000000..4b11e54
--- /dev/null
+++ b/openwebui_integration/migrations/18.0.1.0.1/pre-migration.py
@@ -0,0 +1,17 @@
+def migrate(cr, version):
+ """Add products_per_request column to res_company table."""
+ if not version:
+ return
+
+ # Add products_per_request column if it doesn't exist
+ cr.execute("""
+ SELECT column_name
+ FROM information_schema.columns
+ WHERE table_name='res_company'
+ AND column_name='openwebui_products_per_request'
+ """)
+ if not cr.fetchone():
+ cr.execute("""
+ ALTER TABLE res_company
+ ADD COLUMN openwebui_products_per_request integer DEFAULT 10
+ """)
diff --git a/openwebui_integration/models/__init__.py b/openwebui_integration/models/__init__.py
index 8c7db0c..aab818f 100644
--- a/openwebui_integration/models/__init__.py
+++ b/openwebui_integration/models/__init__.py
@@ -3,7 +3,4 @@
from . import openwebui_bot_mixin
from . import openwebui_model
-from . import openwebui_bot
-from . import discuss_channel
from . import res_company
-from . import res_users
diff --git a/openwebui_integration/models/openwebui_bot_mixin.py b/openwebui_integration/models/openwebui_bot_mixin.py
index 8721724..52c4663 100644
--- a/openwebui_integration/models/openwebui_bot_mixin.py
+++ b/openwebui_integration/models/openwebui_bot_mixin.py
@@ -1,7 +1,38 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
-from odoo import models, fields, api, _
+"""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.
@@ -14,51 +45,106 @@ class OpenWebUIBotMixin(models.AbstractModel):
_name = 'openwebui.bot.mixin'
_description = 'OpenWebUI Bot Mixin'
- def _get_bot_context(self, bot):
- """Gets the bot context with its parameters"""
- return {
- 'instructions': bot.instructions,
- 'model': bot.model_id,
- 'context': bot.context or '',
- 'max_tokens': bot.max_tokens,
- 'temperature': bot.temperature,
- }
+ def _get_model(self, model):
+ """Gets the model to use"""
+ return model
def _apply_logic(self, record, values, command=None):
- """Applies the bot logic to a record.
-
- Args:
- record: The record to apply the logic to
- values: The values to use for message generation
- command: Optional command for the bot
-
- Returns:
- dict: The updated values
- """
- bot = values.get('bot')
- if not bot or not bot.model_id:
+ """Applies the model logic to a record."""
+ model = values.get('bot')
+ if not model:
return values
- # Prepare the bot context
- bot_context = self._get_bot_context(bot)
+ # Get the model
+ model = self._get_model(model)
- # Generate the message for the bot
+ # Generate the message for the model
message = self._generate_bot_message(record, values, command)
- # Send the message to the model
- response = bot_context['model'].send_message(
- message=message,
- context=bot_context['context'],
- max_tokens=bot_context['max_tokens'],
- temperature=bot_context['temperature'],
- instructions=bot_context['instructions']
- )
+ # 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)
+ response = model.send_message(message=message, timeout=timeout)
+
+ # Si la réponse contient une erreur de connexion, on la traite comme une exception
+ if isinstance(response, str) and "Connection error" in response:
+ raise ConnectionError(response)
+
+ _logger.info('Received response from model: %r', response)
- if response:
- # Update the values with the bot response
- values = self._process_bot_response(values, response)
+ if not response:
+ raise ValueError("Empty response from model")
- return values
+ if not isinstance(response, str):
+ raise ValueError(f"Invalid response type: {type(response)}")
+
+ # Clean up the response
+ # Supprimer les blocs de code markdown
+ response = re.sub(r'```json\n|\n```', '', response.strip())
+ _logger.debug('Response after markdown cleanup: %r', response)
+
+ try:
+ # Essayer de parser directement la réponse nettoyée
+ parsed_response = json.loads(response)
+ clean_response = response
+ except json.JSONDecodeError as e:
+ _logger.warning('Failed to parse response directly: %s', e)
+ # Si échec, chercher une liste JSON dans la réponse
+ 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)
+
+ try:
+ parsed_response = json.loads(clean_response)
+ except json.JSONDecodeError as e:
+ _logger.error('Failed to parse extracted JSON: %s', e)
+ raise ValueError("Invalid JSON list in 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.
diff --git a/openwebui_integration/models/openwebui_model.py b/openwebui_integration/models/openwebui_model.py
index 40d3869..df606f2 100644
--- a/openwebui_integration/models/openwebui_model.py
+++ b/openwebui_integration/models/openwebui_model.py
@@ -1,12 +1,19 @@
-# -*- coding: utf-8 -*-
+# -*-
+"""
+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 requests
-import logging
import json
-from requests.exceptions import RequestException
+import logging
+import requests
from odoo import models, fields, api, _
-from odoo.exceptions import UserError
+from odoo.exceptions import UserError, AccessError
+
+_logger = logging.getLogger(__name__)
_logger = logging.getLogger(__name__)
@@ -59,7 +66,11 @@ class OpenWebUIModel(models.Model):
)
_sql_constraints = [
- ('unique_identifier', 'unique(identifier, company_id)', 'The identifier must be unique per company!')
+ (
+ 'unique_identifier',
+ 'unique(identifier, company_id)',
+ 'The identifier must be unique per company!'
+ )
]
@api.model_create_multi
@@ -69,11 +80,10 @@ class OpenWebUIModel(models.Model):
# Mark model as temporary if it starts with test_ or refresh_
if vals.get('identifier', '').startswith(('test_', 'refresh_')):
vals['is_temp'] = True
-
- # Allow creation if it's a temporary model or in installation mode
- if not (vals.get('is_temp') or self.env.context.get('install_mode')):
+
+ # 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):
@@ -90,8 +100,62 @@ class OpenWebUIModel(models.Model):
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()
+
+ # Initialize messages list with history if provided
+ messages = []
+ if message_history:
+ messages.extend(message_history)
+
+ # Add the current message
+ messages.append({'role': 'user', 'content': message})
+
+ data = {
+ 'model': self.identifier,
+ 'messages': messages,
+ }
+
+ if context:
+ data['context'] = context
+ if instructions:
+ data['system'] = instructions
+
+ success, result = self._make_request('chat/completions', method='POST', data=data, timeout=timeout)
+ if not success:
+ return f"Error: {result}"
+
+ try:
+ return result['choices'][0]['message']['content']
+ except (KeyError, IndexError) as e:
+ return f"Error processing response: {str(e)}"
+
def cleanup_temp_models(self):
- """Cleans up temporary models"""
+ """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),
'|',
@@ -104,7 +168,18 @@ class OpenWebUIModel(models.Model):
temp_models.with_context(sync_models=True).unlink()
def _get_company_config(self):
- """Gets the configuration of the current company"""
+ """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,
@@ -115,7 +190,27 @@ class OpenWebUIModel(models.Model):
}
def _make_request(self, endpoint, method='GET', data=None, files=None, timeout=None):
- """Makes a request to the OpenWebUI API"""
+ """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']:
@@ -152,24 +247,51 @@ class OpenWebUIModel(models.Model):
except requests.exceptions.SSLError:
return False, "SSL/TLS verification failed"
- except requests.exceptions.ConnectionError:
- return False, "Could not connect to server"
- except requests.exceptions.Timeout:
- return False, "Connection timed out"
except requests.exceptions.RequestException as e:
return False, f"Connection error: {str(e)}"
except (json.JSONDecodeError, ValueError) as e:
return False, f"Invalid response from server: {str(e)}"
- except Exception as e:
- return False, f"Unexpected error: {str(e)}"
+ 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):
- """Tests the connection to OpenWebUI"""
+ """Test the connection to the OpenWebUI API.
+
+ This method performs a simple request to the /api/models 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('models', timeout=5)
@api.model
def get_available_models(self):
- """Gets the list of available models from the API"""
+ """Get the list of available models from the OpenWebUI API.
+
+ This method attempts to retrieve the list of models from the API.
+ If it fails, it returns a default list of common models.
+
+ Returns:
+ list: A list of tuples (id, name) of available models.
+ Example: [('gpt-3.5-turbo', 'GPT-3.5 Turbo'), ('gpt-4', 'GPT-4')]
+
+ Note:
+ - Handles both possible API response formats (list or dict)
+ - Returns DEFAULT_MODELS in case of error or unexpected format
+ - Errors are logged but don't interrupt execution
+ """
success, result = self._make_request('models')
if not success:
@@ -184,19 +306,43 @@ class OpenWebUIModel(models.Model):
else:
_logger.warning("Unexpected response format from OpenWebUI API")
return DEFAULT_MODELS
- except Exception as e:
+ except (KeyError, TypeError, AttributeError) as e:
_logger.error(f"Error processing models response: {str(e)}")
return DEFAULT_MODELS
def sync_models(self):
- """Synchronizes models from OpenWebUI"""
+ """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):
- """Synchronizes models from OpenWebUI"""
+ """Internal method to synchronize models from OpenWebUI.
+
+ 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.
+ """
# First clean up temporary models
self.cleanup_temp_models()
@@ -215,48 +361,9 @@ class OpenWebUIModel(models.Model):
'description': model_data.get('description', ''),
})
return True, None
- except Exception as e:
- _logger.error(f"Error synchronizing models: {str(e)}")
- return False, str(e)
-
- def send_message(self, message, message_history=None, context=None, instructions=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
-
- Returns:
- str: The model's response or error message
- """
- self.ensure_one()
-
- # Initialize messages list with history if provided
- messages = []
- if message_history:
- messages.extend(message_history)
-
- # Add the current message
- messages.append({'role': 'user', 'content': message})
-
- data = {
- 'model': self.identifier,
- 'messages': messages,
- }
-
- if context:
- data['context'] = context
- if instructions:
- data['system'] = instructions
-
- success, result = self._make_request('chat/completions', method='POST', data=data)
- if not success:
- return f"Error: {result}"
-
- try:
- return result['choices'][0]['message']['content']
- except (KeyError, IndexError) as e:
- return f"Error processing response: {str(e)}"
+ except (KeyError, TypeError) as e:
+ _logger.error(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
index cd7f5c9..29b539d 100644
--- a/openwebui_integration/models/res_company.py
+++ b/openwebui_integration/models/res_company.py
@@ -1,9 +1,15 @@
# -*- 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, _
+from odoo import models, fields, _
from odoo.exceptions import UserError
+
class ResCompany(models.Model):
_inherit = 'res.company'
@@ -31,71 +37,135 @@ class ResCompany(models.Model):
openwebui_timeout = fields.Integer(
string='Timeout (seconds)',
- default=30,
+ default=60,
help="Maximum wait time for API calls"
)
+
+ 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"
+ )
def test_openwebui_connection(self):
- """Tests the connection to OpenWebUI"""
+ """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."))
- model = self.env['openwebui.model'].create({
- 'name': 'test_connection',
- 'identifier': 'test_connection',
- 'company_id': self.id,
- 'is_temp': True,
- })
-
- success, message = model.test_connection()
+ success, result = self.env['openwebui.model'].test_connection()
if not success:
- raise UserError(message)
-
+ raise UserError(_("Connection test failed: %s") % result)
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
- 'message': _('Connection to OpenWebUI was successful.'),
- 'type': 'success',
+ 'message': _('Connection to OpenWebUI successful!'),
'sticky': False,
+ 'type': 'success',
}
}
def refresh_model_list(self):
- """Refreshes the list of models from OpenWebUI"""
+ """Synchronize available AI models from OpenWebUI with Odoo.
+
+ This method performs the following operations:
+ 1. Verifies that OpenWebUI integration is enabled for the company
+ 2. Connects to the OpenWebUI API to fetch the latest model list
+ 3. Updates the local database:
+ - Creates records for new models
+ - Updates existing model information
+ - Archives models that are no longer available
+
+ Technical Details:
+ - Uses the sync_models context to trigger a full synchronization
+ - Performs all operations in a single transaction
+ - Handles API connection errors gracefully
+
+ Returns:
+ dict: An action dictionary with the following structure:
+ {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': str,
+ 'message': str,
+ 'sticky': bool,
+ 'type': str,
+ }
+ }
+
+ Raises:
+ UserError: In the following cases:
+ - OpenWebUI is not enabled for the company
+ - API connection fails
+ - Model synchronization fails
+
+ Example:
+ >>> company = env['res.company'].browse(1)
+ >>> result = company.refresh_model_list()
+ >>> print(result['params']['message'])
+ 'Models list has been refreshed successfully!'
+ """
self.ensure_one()
if not self.openwebui_enabled:
raise UserError(_("OpenWebUI is not enabled for this company."))
- # Create a temporary model to perform synchronization
- model = self.env['openwebui.model'].create({
- 'name': 'refresh_models',
- 'identifier': 'refresh_models',
- 'company_id': self.id,
- 'is_temp': True,
- })
+ Model = self.env['openwebui.model'].with_context(sync_models=True)
+ success, result = Model._sync_models()
- success, message = model.sync_models()
if not success:
- raise UserError(message)
-
+ raise UserError(_("Failed to refresh models: %s") % result)
+
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Success'),
- 'message': _('Model list has been successfully updated.'),
- 'type': 'success',
+ 'message': _('Models list has been refreshed successfully!'),
'sticky': False,
+ 'type': 'success',
}
}
diff --git a/openwebui_integration/security/ir.model.access.csv b/openwebui_integration/security/ir.model.access.csv
index a5108db..b4484fa 100644
--- a/openwebui_integration/security/ir.model.access.csv
+++ b/openwebui_integration/security/ir.model.access.csv
@@ -1,5 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
-access_openwebui_model_user,access_openwebui_model_user,model_openwebui_model,base.group_user,1,0,0,0
-access_openwebui_model_admin,access_openwebui_model_admin,model_openwebui_model,base.group_system,1,1,1,1
-access_openwebui_bot_user,access_openwebui_bot_user,model_openwebui_bot,base.group_user,1,0,0,0
-access_openwebui_bot_admin,access_openwebui_bot_admin,model_openwebui_bot,base.group_system,1,1,1,1
+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/security.xml b/openwebui_integration/security/security.xml
index 7680c40..714424f 100644
--- a/openwebui_integration/security/security.xml
+++ b/openwebui_integration/security/security.xml
@@ -31,11 +31,6 @@
-
- Bots OpenWebUI: règle multi-société
-
- [('company_id', 'in', company_ids)]
-
-
+
diff --git a/openwebui_integration/views/openwebui_model_views.xml b/openwebui_integration/views/openwebui_model_views.xml
deleted file mode 100644
index 54cbf1e..0000000
--- a/openwebui_integration/views/openwebui_model_views.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
- openwebui.model.form
- openwebui.model
-
-
-
-
-
diff --git a/openwebui_integration/views/res_company_views.xml b/openwebui_integration/views/res_company_views.xml
index cdbd6b2..bfffc75 100644
--- a/openwebui_integration/views/res_company_views.xml
+++ b/openwebui_integration/views/res_company_views.xml
@@ -24,6 +24,9 @@
invisible="not openwebui_enabled"/>
+
+
+
+
diff --git a/openwebui_integration_chat/Spécifications.md b/openwebui_integration_chat/Spécifications.md
new file mode 100644
index 0000000..e69de29
diff --git a/openwebui_integration_chat/__init__.py b/openwebui_integration_chat/__init__.py
new file mode 100644
index 0000000..8c67f89
--- /dev/null
+++ b/openwebui_integration_chat/__init__.py
@@ -0,0 +1,5 @@
+# -*- 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
new file mode 100644
index 0000000..6727881
--- /dev/null
+++ b/openwebui_integration_chat/__manifest__.py
@@ -0,0 +1,35 @@
+# -*- 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
new file mode 100644
index 0000000..9382e1d
--- /dev/null
+++ b/openwebui_integration_chat/models/__init__.py
@@ -0,0 +1,6 @@
+# -*- 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/models/discuss_channel.py b/openwebui_integration_chat/models/discuss_channel.py
similarity index 100%
rename from openwebui_integration/models/discuss_channel.py
rename to openwebui_integration_chat/models/discuss_channel.py
diff --git a/openwebui_integration/models/openwebui_bot.py b/openwebui_integration_chat/models/openwebui_bot.py
similarity index 100%
rename from openwebui_integration/models/openwebui_bot.py
rename to openwebui_integration_chat/models/openwebui_bot.py
diff --git a/openwebui_integration/models/res_users.py b/openwebui_integration_chat/models/res_users.py
similarity index 100%
rename from openwebui_integration/models/res_users.py
rename to openwebui_integration_chat/models/res_users.py
diff --git a/openwebui_integration_chat/security/ir.model.access.csv b/openwebui_integration_chat/security/ir.model.access.csv
new file mode 100644
index 0000000..d0eab75
--- /dev/null
+++ b/openwebui_integration_chat/security/ir.model.access.csv
@@ -0,0 +1,3 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_openwebui_bot_user,access_openwebui_bot_user,model_openwebui_bot,openwebui_integration.group_openwebui_user,1,0,0,0
+access_openwebui_bot_admin,access_openwebui_bot_admin,model_openwebui_bot,openwebui_integration.group_openwebui_admin,1,1,1,1
diff --git a/openwebui_integration_chat/security/security.xml b/openwebui_integration_chat/security/security.xml
new file mode 100644
index 0000000..6bff117
--- /dev/null
+++ b/openwebui_integration_chat/security/security.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+ Bots OpenWebUI: règle multi-société
+
+ [('company_id', 'in', company_ids)]
+
+
+
+
diff --git a/openwebui_integration/views/openwebui_bot_views.xml b/openwebui_integration_chat/views/openwebui_bot_views.xml
similarity index 100%
rename from openwebui_integration/views/openwebui_bot_views.xml
rename to openwebui_integration_chat/views/openwebui_bot_views.xml
diff --git a/openwebui_integration_product/Spécifications.md b/openwebui_integration_product/Spécifications.md
new file mode 100644
index 0000000..004e6af
--- /dev/null
+++ b/openwebui_integration_product/Spécifications.md
@@ -0,0 +1,123 @@
+# Spécifications du module OpenWebUI Integration Product
+
+## Objectif
+Ce module étend les fonctionnalités d'OpenWebUI Integration pour ajouter des capacités d'IA spécifiques à la gestion des produits dans Odoo. Il permet d'automatiser et d'optimiser différents aspects de la gestion des produits grâce à l'intelligence artificielle.
+
+## Architecture du Module
+
+### Structure des Répertoires
+```
+openwebui_integration_product/
+├── controllers/ # Contrôleurs pour les endpoints web spécifiques aux produits
+├── models/ # Modèles de données pour les produits et l'historique
+├── security/ # Fichiers de sécurité et accès
+├── static/ # Ressources statiques (JS, CSS)
+└── views/ # Vues XML Odoo pour les produits
+```
+
+## Composants Principaux
+
+### 1. Intégration Produit (`product.integration`)
+Extension du modèle `product.template` pour ajouter les fonctionnalités d'IA.
+
+#### Fonctionnalités clés:
+- Héritage du mixin `openwebui.bot.mixin`
+- Gestion des suggestions de catégories
+- Historique des suggestions
+- Interface utilisateur intégrée
+
+#### Méthodes principales:
+- `_get_product_context()`: Prépare le contexte produit pour l'IA
+- `suggest_category()`: Déclenche l'analyse IA pour les suggestions
+- `apply_suggestion()`: Applique une suggestion validée
+- `_process_category_suggestion()`: Traite la réponse de l'IA
+
+### 2. Historique des Suggestions (`category.suggestion.history`)
+Gère l'historique des suggestions de catégories.
+
+#### Caractéristiques:
+- Traçabilité des suggestions
+- Scores de confiance
+- Statut des suggestions (appliquée/rejetée)
+- Métadonnées de contexte
+
+#### Champs principaux:
+- `product_id`: Produit concerné
+- `suggested_category_id`: Catégorie suggérée
+- `confidence_score`: Score de confiance
+- `status`: État de la suggestion
+- `applied_date`: Date d'application
+- `user_id`: Utilisateur ayant traité la suggestion
+
+### 3. Configuration Spécifique
+Extension des paramètres de configuration d'OpenWebUI.
+
+#### Paramètres configurables:
+- `product_suggestion_threshold`: Seuil minimum de confiance
+- `max_suggestions_per_product`: Nombre maximum de suggestions
+- `suggestion_retention_days`: Durée de conservation de l'historique
+- `auto_apply_threshold`: Seuil pour application automatique
+
+## Processus de Suggestion de Catégorie
+
+### Flux de Données
+1. Collecte des données produit
+ - Informations de base (nom, description)
+ - Caractéristiques techniques
+ - Historique des achats
+ - Catégories existantes
+
+2. Préparation du Contexte
+ - Formatage des données
+ - Ajout du contexte entreprise
+ - Inclusion des paramètres de configuration
+
+3. Analyse IA
+ - Envoi à OpenWebUI
+ - Traitement de la réponse
+ - Calcul des scores de confiance
+
+4. Validation et Application
+ - Vérification des seuils
+ - Présentation à l'utilisateur
+ - Enregistrement dans l'historique
+
+## Sécurité et Gestion des Erreurs
+
+### Sécurité
+- Droits d'accès spécifiques aux produits
+- Validation des suggestions
+- Protection contre les suggestions malveillantes
+- Traçabilité des modifications
+
+### Gestion des Erreurs
+- Validation des données produit
+- Gestion des timeouts
+- Traitement des réponses invalides
+- Rollback en cas d'échec
+
+## Intégration et Utilisation
+
+### Installation
+1. Installation des dépendances
+2. Configuration des paramètres produit
+3. Attribution des droits d'accès
+4. Test initial des suggestions
+
+### Développement d'Extensions
+1. Héritage des modèles existants
+2. Ajout de nouveaux critères d'analyse
+3. Personnalisation des suggestions
+4. Extension des fonctionnalités
+
+## Dépendances
+- Module `openwebui_integration`
+- Module `product`
+- Python 3.x
+- Bibliothèques de traitement de texte
+
+## Notes Techniques
+- Optimisation des requêtes IA
+- Cache des suggestions fréquentes
+- Support multi-langue
+- Extensible pour d'autres types d'analyses
diff --git a/openwebui_integration_product/__init__.py b/openwebui_integration_product/__init__.py
new file mode 100644
index 0000000..9b42961
--- /dev/null
+++ b/openwebui_integration_product/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/openwebui_integration_product/__manifest__.py b/openwebui_integration_product/__manifest__.py
new file mode 100644
index 0000000..e0634d6
--- /dev/null
+++ b/openwebui_integration_product/__manifest__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+{
+ 'name': 'OpenWebUI Integration Product',
+ 'version': '18.0.1.0.1',
+ 'summary': "AI-powered actions integration for Odoo products",
+ 'description': """
+OpenWebUI Integration Product Module
+====================================
+
+This module extends OpenWebUI Integration to add AI-powered features to product management.
+""",
+ 'author': 'Benoît Vézina',
+ 'license': 'LGPL-3',
+ 'website': 'https://www.bemade.org',
+ 'depends': ['product', 'openwebui_integration'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/product_template_view.xml',
+ 'views/product_template_action.xml',
+ 'wizard/category_suggestion_wizard_view.xml',
+ ],
+ 'installable': True,
+ 'application': False,
+ 'auto_install': False,
+}
diff --git a/openwebui_integration_product/migrations/18.0.1.0.1/__init__.py b/openwebui_integration_product/migrations/18.0.1.0.1/__init__.py
new file mode 100644
index 0000000..c533a53
--- /dev/null
+++ b/openwebui_integration_product/migrations/18.0.1.0.1/__init__.py
@@ -0,0 +1 @@
+from . import pre-migration
diff --git a/openwebui_integration_product/migrations/18.0.1.0.1/pre-migration.py b/openwebui_integration_product/migrations/18.0.1.0.1/pre-migration.py
new file mode 100644
index 0000000..580af3e
--- /dev/null
+++ b/openwebui_integration_product/migrations/18.0.1.0.1/pre-migration.py
@@ -0,0 +1,17 @@
+def migrate(cr, version):
+ """Add explanation column to product_category_suggestion_history table."""
+ if not version:
+ return
+
+ # Add explanation column if it doesn't exist
+ cr.execute("""
+ SELECT column_name
+ FROM information_schema.columns
+ WHERE table_name='product_category_suggestion_history'
+ AND column_name='explanation'
+ """)
+ if not cr.fetchone():
+ cr.execute("""
+ ALTER TABLE product_category_suggestion_history
+ ADD COLUMN explanation text
+ """)
diff --git a/openwebui_integration_product/models/__init__.py b/openwebui_integration_product/models/__init__.py
new file mode 100644
index 0000000..4e7e8f4
--- /dev/null
+++ b/openwebui_integration_product/models/__init__.py
@@ -0,0 +1,2 @@
+from . import product_template
+from . import category_suggestion_history
diff --git a/openwebui_integration_product/models/category_suggestion_history.py b/openwebui_integration_product/models/category_suggestion_history.py
new file mode 100644
index 0000000..c62d118
--- /dev/null
+++ b/openwebui_integration_product/models/category_suggestion_history.py
@@ -0,0 +1,56 @@
+# -*- coding: utf-8 -*-
+
+from odoo import models, fields
+
+class CategorySuggestionHistory(models.Model):
+ _name = 'product.category.suggestion.history'
+ _description = 'Category Suggestion History'
+ _order = 'create_date desc'
+
+ product_id = fields.Many2one(
+ comodel_name='product.template',
+ string='Product',
+ required=True,
+ ondelete='cascade',
+ help='Product for which the category was suggested'
+ )
+
+ suggested_category_id = fields.Many2one(
+ comodel_name='product.category',
+ string='Suggested Category',
+ required=True,
+ help='Category suggested by AI analysis'
+ )
+
+ suggestion_confidence = fields.Float(
+ string='Confidence Score',
+ help='Confidence score of the suggestion (0-100)'
+ )
+
+ applied = fields.Boolean(
+ string='Applied',
+ help='Indicates if the suggestion was applied to the product'
+ )
+
+ suggestion_date = fields.Datetime(
+ string='Suggestion Date',
+ readonly=True,
+ help='Date and time of the suggestion'
+ )
+
+ suggestion_uid = fields.Many2one(
+ comodel_name='res.users',
+ string='Created By',
+ readonly=True,
+ help='User who triggered the suggestion'
+ )
+
+ input_data = fields.Text(
+ string='Analyzed Data',
+ help='Data used by AI to generate the suggestion'
+ )
+
+ explanation = fields.Text(
+ string='Explanation',
+ help='Detailed explanation of category choice'
+ )
\ No newline at end of file
diff --git a/openwebui_integration_product/models/product_template.py b/openwebui_integration_product/models/product_template.py
new file mode 100644
index 0000000..bf1ac14
--- /dev/null
+++ b/openwebui_integration_product/models/product_template.py
@@ -0,0 +1,203 @@
+# -*- coding: utf-8 -*-
+"""OpenWebUI Product Integration Module
+
+This module extends product.template functionality to integrate
+OpenWebUI artificial intelligence into product management.
+It enables automatic product category suggestions
+based on description and characteristics analysis.
+
+Main features:
+- AI-powered category suggestions
+- Suggestion history tracking
+- Integrated user interface
+"""
+
+import json
+import logging
+from datetime import datetime
+
+from odoo import models, fields, _
+from odoo.exceptions import UserError
+from odoo.tools import float_round
+from odoo.addons.openwebui_integration.models.openwebui_bot_mixin import OpenWebUIBotMixin
+
+_logger = logging.getLogger(__name__)
+
+class ProductTemplate(models.Model, OpenWebUIBotMixin):
+ _inherit = "product.template"
+
+ suggested_category_id = fields.Many2one(
+ comodel_name='product.category',
+ string="Suggested Category",
+ readonly=True,
+ help="Category suggested by AI based on product information analysis"
+ )
+
+ suggestion_confidence = fields.Float(
+ string="Confidence",
+ readonly=True,
+ help="Confidence score (0-100) indicating how sure the AI is about the suggested category"
+ )
+
+ suggestion_date = fields.Datetime(
+ string="Suggestion Date",
+ readonly=True,
+ help="Date and time when the category was suggested by the AI"
+ )
+
+ suggestion_history_ids = fields.One2many(
+ comodel_name='product.category.suggestion.history',
+ inverse_name='product_id',
+ string="Suggestion History",
+ help="History of all category suggestions made by AI for this product"
+ )
+
+ def _generate_bot_message(self, records, values, command=None):
+ """Generates the message to send to the bot to get category suggestions."""
+ # Préparer les données des produits
+ products_data = [{
+ 'odoo_id': record.id, # ID interne Odoo
+ 'name': record.name,
+ 'description': record.description or '',
+ 'description_sale': record.description_sale or '',
+ 'default_code': record.default_code or '',
+ 'current_category': record.categ_id.display_name,
+ 'sellers': [
+ {
+ 'name': seller.partner_id.display_name,
+ 'product_code': seller.product_code or '',
+ 'product_name': seller.product_name or ''
+ } for seller in record.seller_ids
+ ]
+ } for record in records]
+
+ # Récupérer toutes les catégories disponibles
+ Category = self.env['product.category']
+ categories = Category.search([('parent_id', '!=', False)], order='complete_name')
+ available_categories = [{
+ 'id': cat.id,
+ 'name': cat.name,
+ 'complete_name': cat.complete_name or cat.name,
+ 'level': len(cat.parent_path.split('/')) - 1 if cat.parent_path else 0
+ } for cat in categories]
+
+ # Construire le message pour l'IA
+ message = {
+ 'task': 'product_categorization',
+ 'products': products_data,
+ 'available_categories': available_categories,
+ 'instructions': """For each product in the products list, analyze the product information and suggest the most appropriate product category from the available list.
+ Consider each product's name, description, and supplier information to make the best match.
+ Return a list of JSON objects, one for each product, with:
+ - 'odoo_id' (integer): The internal Odoo ID of the product
+ - 'category_id' (integer): The ID of the most appropriate category
+ - 'confidence' (float between 0 and 100): How confident you are about this suggestion
+ - 'explanation' (string): A detailed explanation of why this category was chosen, including analysis of the product name, description, and other relevant information.""",
+ 'format': 'json'
+ }
+ return json.dumps(message)
+
+ def _process_bot_response(self, values, response):
+ """Process the bot response to extract the suggested category."""
+ try:
+ results = json.loads(response)
+ if not isinstance(results, list):
+ raise ValueError("La réponse n'est pas une liste JSON valide")
+
+ for result in results:
+ category_id = result.get('category_id')
+ confidence = result.get('confidence', 0.0)
+ product_id = result.get('odoo_id')
+
+ if not category_id:
+ raise ValueError("No category ID in response")
+
+ if not product_id:
+ raise ValueError("No product ID in response")
+
+ # Vérifier que la catégorie existe
+ category = self.env['product.category'].browse(category_id).exists()
+ if not category:
+ raise ValueError(f"Category {category_id} not found")
+
+ # Trouver le produit concerné
+ product = self.filtered(lambda p: p.id == product_id)
+ if not product:
+ raise ValueError(f"Product {product_id} not found in selection")
+
+ # Mettre à jour les valeurs pour ce produit
+ product.write({
+ 'suggested_category_id': category_id,
+ 'suggestion_confidence': confidence,
+ 'suggestion_date': fields.Datetime.now(),
+ })
+
+ # Créer l'historique
+ self.env['product.category.suggestion.history'].create({
+ 'product_id': product_id,
+ 'suggested_category_id': category_id,
+ 'suggestion_confidence': confidence,
+ 'input_data': json.dumps(result),
+ 'explanation': result.get('explanation', ''),
+ 'applied': False
+ })
+
+ except Exception as e:
+ raise UserError(_("Erreur lors du traitement de la réponse de l'IA: %s") % str(e))
+
+ return values
+
+ def action_suggest_category(self):
+ """Request category suggestions from AI."""
+ if len(self) > 800:
+ raise UserError(_("For performance reasons, you cannot analyze more than 800 products at once."))
+
+ # Get company settings
+ company = self.env.company
+ if not company.openwebui_enabled:
+ raise UserError(_("OpenWebUI is not enabled for your company. Please enable it in company settings."))
+
+ model = company.openwebui_default_model_id
+ if not model:
+ raise UserError(_("No default OpenWebUI model configured. Please configure it in company settings."))
+
+ # Process products in batches
+ batch_size = company.openwebui_products_per_request
+ successful_products = self.env['product.template']
+
+ for i in range(0, len(self), batch_size):
+ batch = self[i:i + batch_size]
+
+ # Créer une nouvelle transaction pour ce batch
+ with self.env.cr.savepoint():
+ try:
+ values = {'bot': model}
+ self._apply_logic(batch, values)
+ # Si on arrive ici, le batch a réussi
+ successful_products |= batch
+ _logger.info('Successfully processed batch of %d products: %s',
+ len(batch), batch.mapped('default_code'))
+ except Exception as e:
+ _logger.error('Batch processing failed for products %s: %s',
+ batch.mapped('default_code'), str(e))
+ # Le savepoint sera rollback automatiquement
+ continue
+
+ # Si aucun produit n'a été traité avec succès
+ if not successful_products:
+ raise UserError(_('No suggestions could be generated by AI.'))
+
+ # Ouvrir l'assistant si des suggestions ont été générées
+ if any(product.suggested_category_id for product in successful_products):
+ wizard = self.env['product.category.suggestion.wizard'].create({})
+ return {
+ 'name': _('Category Suggestions'),
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'product.category.suggestion.wizard',
+ 'res_id': wizard.id,
+ 'view_mode': 'form',
+ 'target': 'new',
+ 'context': self.env.context,
+ }
+ else:
+ raise UserError(_('No valid suggestions could be generated by AI.'))
\ No newline at end of file
diff --git a/openwebui_integration_product/security/ir.model.access.csv b/openwebui_integration_product/security/ir.model.access.csv
new file mode 100644
index 0000000..b705df6
--- /dev/null
+++ b/openwebui_integration_product/security/ir.model.access.csv
@@ -0,0 +1,4 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_product_category_suggestion_history_user,product.category.suggestion.history user,model_product_category_suggestion_history,base.group_user,1,1,1,1
+access_product_category_suggestion_wizard_user,product.category.suggestion.wizard user,model_product_category_suggestion_wizard,base.group_user,1,1,1,1
+access_product_category_suggestion_wizard_line_user,product.category.suggestion.wizard.line user,model_product_category_suggestion_wizard_line,base.group_user,1,1,1,1
diff --git a/openwebui_integration_product/views/product_template_action.xml b/openwebui_integration_product/views/product_template_action.xml
new file mode 100644
index 0000000..bdf533b
--- /dev/null
+++ b/openwebui_integration_product/views/product_template_action.xml
@@ -0,0 +1,14 @@
+
+
+
+
+ AI Suggested Category
+
+
+ list
+ code
+
+action = model.browse(env.context.get('active_ids', [])).action_suggest_category()
+
+
+
diff --git a/openwebui_integration_product/views/product_template_view.xml b/openwebui_integration_product/views/product_template_view.xml
new file mode 100644
index 0000000..06e4d8a
--- /dev/null
+++ b/openwebui_integration_product/views/product_template_view.xml
@@ -0,0 +1,62 @@
+
+
+
+
+ product.category.suggestion.history.tree
+ product.category.suggestion.history
+ list
+
+
+
+
+
+
+
+
+
+
+
+
+
+ product.template.form.inherit.openwebui
+ product.template
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ product.template.tree.inherit.openwebui
+ product.template
+
+
+
+ action_suggest_category_multi
+
+
+
+
+
diff --git a/openwebui_integration_product/wizard/__init__.py b/openwebui_integration_product/wizard/__init__.py
new file mode 100644
index 0000000..91f745b
--- /dev/null
+++ b/openwebui_integration_product/wizard/__init__.py
@@ -0,0 +1 @@
+from . import category_suggestion_wizard
diff --git a/openwebui_integration_product/wizard/category_suggestion_wizard.py b/openwebui_integration_product/wizard/category_suggestion_wizard.py
new file mode 100644
index 0000000..020e413
--- /dev/null
+++ b/openwebui_integration_product/wizard/category_suggestion_wizard.py
@@ -0,0 +1,74 @@
+from odoo import models, fields, api, _
+from odoo.exceptions import UserError
+import json
+
+class CategorySuggestionWizard(models.TransientModel):
+ _name = 'product.category.suggestion.wizard'
+ _description = 'Assistant de suggestions de catégories'
+
+ product_suggestion_ids = fields.One2many('product.category.suggestion.wizard.line', 'wizard_id',
+ string='Suggestions de produits')
+
+ @api.model
+ def default_get(self, fields):
+ res = super().default_get(fields)
+ context = self.env.context
+ if context.get('active_model') == 'product.template' and context.get('active_ids'):
+ products = self.env['product.template'].browse(context['active_ids'])
+ suggestion_lines = []
+ for product in products:
+ if product.suggested_category_id:
+ suggestion_lines.append((0, 0, {
+ 'product_id': product.id,
+ 'current_category_id': product.categ_id.id,
+ 'suggested_category_id': product.suggested_category_id.id,
+ 'confidence': product.suggestion_confidence,
+ 'apply_suggestion': False
+ }))
+ res['product_suggestion_ids'] = suggestion_lines
+ return res
+
+ def action_apply_selected_suggestions(self):
+ """Applique les suggestions sélectionnées aux produits."""
+ selected_lines = self.product_suggestion_ids.filtered(lambda l: l.apply_suggestion)
+ if not selected_lines:
+ raise UserError(_('Veuillez sélectionner au moins une suggestion à appliquer.'))
+
+ for line in selected_lines:
+ # Mise à jour du produit
+ line.product_id.write({
+ 'categ_id': line.suggested_category_id.id
+ })
+
+ # Mise à jour de l'historique
+ history = self.env['product.category.suggestion.history'].search([
+ ('product_id', '=', line.product_id.id),
+ ('suggested_category_id', '=', line.suggested_category_id.id)
+ ], limit=1)
+ if history:
+ history.write({'applied': True})
+
+ # Message de confirmation
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': _('Succès'),
+ 'message': _('%d catégories ont été mises à jour.') % len(selected_lines),
+ 'type': 'success',
+ 'sticky': False,
+ }
+ }
+
+
+class CategorySuggestionWizardLine(models.TransientModel):
+ _name = 'product.category.suggestion.wizard.line'
+ _description = 'Ligne de suggestion de catégorie'
+ _order = 'confidence desc, product_id'
+
+ wizard_id = fields.Many2one('product.category.suggestion.wizard', string='Assistant')
+ product_id = fields.Many2one('product.template', string='Produit', required=True, readonly=True)
+ current_category_id = fields.Many2one('product.category', string='Catégorie actuelle', readonly=True)
+ suggested_category_id = fields.Many2one('product.category', string='Catégorie suggérée', readonly=True)
+ confidence = fields.Float(string='Confiance (%)', readonly=True)
+ apply_suggestion = fields.Boolean(string='Appliquer', default=False)
diff --git a/openwebui_integration_product/wizard/category_suggestion_wizard_view.xml b/openwebui_integration_product/wizard/category_suggestion_wizard_view.xml
new file mode 100644
index 0000000..b39c9d8
--- /dev/null
+++ b/openwebui_integration_product/wizard/category_suggestion_wizard_view.xml
@@ -0,0 +1,36 @@
+
+
+
+ product.category.suggestion.wizard.form
+ product.category.suggestion.wizard
+
+
+
+
+