# -*- 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