From d295f38355cd90780a5c7297aef596e67d020219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20V=C3=A9zina?= Date: Wed, 19 Feb 2025 15:12:15 -0500 Subject: [PATCH] next try --- ai_integration/__manifest__.py | 3 +- ai_integration/models/__init__.py | 2 +- ai_integration/models/ai_generation_params.py | 18 +- ai_integration/models/ai_model.py | 4 +- ai_integration/models/ai_provider_instance.py | 3 +- ai_integration/models/doc_models.md | 90 ++++++++++ ai_integration/models/mixins/__init__.py | 3 +- ai_integration/models/mixins/ai_base_mixin.py | 167 ++++++++++++++++++ .../models/mixins/ai_generation_params.py | 64 ------- ai_integration/models/mixins/ai_mixin.py | 150 ---------------- ai_integration/security/ir.model.access.csv | 2 + ollama_ai_integration/__manifest__.py | 8 +- ollama_ai_integration/models/__init__.py | 26 ++- .../models/ai_provider_instance.py | 131 ++++++++++++++ ...er_ollama.py => ai_provider_ollama.py.bak} | 3 +- .../models/ollama_model_stats.py | 38 +++- .../models/ollama_provider.py | 57 +++++- .../models/ollama_provider_mixin.py | 85 ++++++++- .../security/ir.model.access.csv | 2 + ollama_ai_integration/views/ollama_views.xml | 3 + 20 files changed, 616 insertions(+), 243 deletions(-) create mode 100644 ai_integration/models/doc_models.md create mode 100644 ai_integration/models/mixins/ai_base_mixin.py delete mode 100644 ai_integration/models/mixins/ai_generation_params.py delete mode 100644 ai_integration/models/mixins/ai_mixin.py create mode 100644 ollama_ai_integration/models/ai_provider_instance.py rename ollama_ai_integration/models/{ai_provider_ollama.py => ai_provider_ollama.py.bak} (89%) diff --git a/ai_integration/__manifest__.py b/ai_integration/__manifest__.py index 2fe11aa..793b339 100644 --- a/ai_integration/__manifest__.py +++ b/ai_integration/__manifest__.py @@ -5,7 +5,7 @@ 'summary': 'Base module for AI integration', 'description': """ AI Integration Base -================== +=================== This module provides the base framework for integrating various AI providers into Odoo. It includes: @@ -18,6 +18,7 @@ into Odoo. It includes: 'depends': [ 'base', 'web', + 'mail', ], 'data': [ 'security/ai_security.xml', diff --git a/ai_integration/models/__init__.py b/ai_integration/models/__init__.py index 72eaf6c..4c00cbb 100644 --- a/ai_integration/models/__init__.py +++ b/ai_integration/models/__init__.py @@ -1,4 +1,4 @@ -from .mixins import ai_mixin +from .mixins.ai_base_mixin import AIBaseMixin from . import ai_generation_params from . import ai_model from . import ai_provider_interface diff --git a/ai_integration/models/ai_generation_params.py b/ai_integration/models/ai_generation_params.py index 83a6bd5..3cc7079 100644 --- a/ai_integration/models/ai_generation_params.py +++ b/ai_integration/models/ai_generation_params.py @@ -8,29 +8,35 @@ class AIGenerationParams(models.AbstractModel): temperature = fields.Float( string='Temperature', help='Controls randomness in generation. Higher values make output more random, lower values more deterministic.', - default=0.7) + default=0.7 + ) repeat_penalty = fields.Float( string='Repeat Penalty', help='Penalty for repeating tokens. Higher values make repetition less likely.', - default=1.1) + default=1.1 + ) max_tokens = fields.Integer( string='Max Tokens', help='Maximum number of tokens to generate.', - default=2048) + default=2048 + ) stop_sequences = fields.Char( string='Stop Sequences', help='Comma-separated list of sequences where generation should stop.', - default='') + default='' + ) frequency_penalty = fields.Float( string='Frequency Penalty', help='Penalty for using frequent tokens. Higher values encourage using less frequent tokens.', - default=0.0) + default=0.0 + ) presence_penalty = fields.Float( string='Presence Penalty', help='Penalty for using tokens already in the text. Higher values encourage using new tokens.', - default=0.0) + default=0.0 + ) diff --git a/ai_integration/models/ai_model.py b/ai_integration/models/ai_model.py index 0487c7e..04666a6 100644 --- a/ai_integration/models/ai_model.py +++ b/ai_integration/models/ai_model.py @@ -54,9 +54,9 @@ class AIModel(models.Model): ) is_active = fields.Boolean( - string='Active', + string='Model Active', default=True, - help='Whether this model is active' + help='Whether this model is currently active and available for use' ) context_window = fields.Integer( diff --git a/ai_integration/models/ai_provider_instance.py b/ai_integration/models/ai_provider_instance.py index 14622c3..4469c6f 100644 --- a/ai_integration/models/ai_provider_instance.py +++ b/ai_integration/models/ai_provider_instance.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from odoo import models, fields, api, _ +from odoo.addons.mail.models.mail_thread import MailThread from odoo.exceptions import UserError @@ -8,7 +9,7 @@ class AIProviderInstance(models.Model): _description = 'AI Provider Instance' _order = 'name' _check_company = False # Disable automatic company checks - _inherit = ['ai.generation.params'] + _inherit = ['mail.thread', 'ai.base.mixin'] active = fields.Boolean( string='Active', diff --git a/ai_integration/models/doc_models.md b/ai_integration/models/doc_models.md new file mode 100644 index 0000000..960f0c7 --- /dev/null +++ b/ai_integration/models/doc_models.md @@ -0,0 +1,90 @@ +# Documentation des Modèles AI Integration + +## Vue d'ensemble + +Le module AI Integration fournit une infrastructure flexible pour intégrer différents fournisseurs d'IA dans Odoo. Il est conçu pour être extensible et permettre l'ajout facile de nouveaux fournisseurs. + +## Modèles Principaux + +### 1. AI Provider (`ai.provider`) +- **Description**: Configuration de base des fournisseurs d'IA +- **Champs principaux**: + - `name`: Nom du fournisseur + - `code`: Code technique unique + - `description`: Description détaillée + - `default_host`: Hôte par défaut + - `active`: État actif/inactif + +### 2. AI Provider Instance (`ai.provider.instance`) +- **Description**: Instance spécifique d'un fournisseur d'IA +- **Champs principaux**: + - `name`: Nom de l'instance + - `provider_id`: Fournisseur associé + - `provider_type`: Type de fournisseur + - `host`: Adresse de l'hôte + - `api_key`: Clé API (si nécessaire) + - `active`: État actif/inactif + +### 3. AI Model (`ai.model`) +- **Description**: Modèles d'IA disponibles +- **Champs principaux**: + - `name`: Nom du modèle + - `identifier`: Identifiant technique + - `provider_instance_id`: Instance du fournisseur + - `active`: État actif/inactif + +### 4. AI Model Stats (`ai.model.stats`) +- **Description**: Statistiques d'utilisation des modèles +- **Champs principaux**: + - `model_id`: Modèle associé + - `total_tokens`: Nombre total de tokens + - `total_requests`: Nombre total de requêtes + - `average_latency`: Latence moyenne + +## Mixin de Base + +### AI Base Mixin (`ai.base.mixin`) +- **Description**: Mixin unifié pour l'intégration IA et les paramètres de génération +- **Champs principaux**: + - `temperature`: Contrôle de la créativité (0.0 - 2.0) + - `top_p`: Sampling nucleus (0.0 - 1.0) + - `max_tokens`: Limite de tokens (1 - 32768) + - `stop_sequences`: Séquences d'arrêt + - `timeout`: Délai d'attente (1 - 300s) + - `retry_count`: Nombre de tentatives (0 - 5) + - `stream_response`: Activation du streaming +- **Méthodes principales**: + - `_get_ai_provider_instance`: Obtenir l'instance du fournisseur + - `_get_ai_model`: Obtenir le modèle à utiliser + - `send_ai_message`: Envoyer un message à l'IA + - `_get_base_generation_params`: Obtenir les paramètres de génération + +## Configuration + +### 1. Res Config Settings +- **Description**: Paramètres de configuration globaux +- **Champs principaux**: + - `default_provider_instance_id`: Instance de fournisseur par défaut + - `default_model_id`: Modèle par défaut + - `ai_batch_size`: Taille du lot pour le traitement + +### 2. Res Company +- **Description**: Extensions des paramètres de société +- **Méthodes principales**: + - `_get_default_provider_instance`: Obtenir l'instance par défaut + +## Interfaces + +### AI Provider Interface (`ai.provider.interface`) +- **Description**: Interface abstraite pour les fournisseurs d'IA +- **Méthodes requises**: + - `send_message`: Envoyer un message + - `get_models`: Obtenir la liste des modèles + - `test_connection`: Tester la connexion + +## Notes d'Implémentation + +1. Tous les fournisseurs d'IA doivent implémenter `ai.provider.interface` +2. Les instances de fournisseur héritent des paramètres de génération via `ai.generation.params` +3. La configuration est hiérarchique : Global > Société > Instance +4. Les statistiques sont collectées automatiquement pour chaque modèle diff --git a/ai_integration/models/mixins/__init__.py b/ai_integration/models/mixins/__init__.py index 6d1b50e..ca864bb 100644 --- a/ai_integration/models/mixins/__init__.py +++ b/ai_integration/models/mixins/__init__.py @@ -1,2 +1 @@ -from . import ai_mixin -from . import ai_generation_params +from . import ai_base_mixin diff --git a/ai_integration/models/mixins/ai_base_mixin.py b/ai_integration/models/mixins/ai_base_mixin.py new file mode 100644 index 0000000..cd590fb --- /dev/null +++ b/ai_integration/models/mixins/ai_base_mixin.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +from typing import List, Dict, Any, Optional +from odoo import models, api, fields, _ +from odoo.exceptions import UserError +import logging + +_logger = logging.getLogger(__name__) + +class AIBaseMixin(models.AbstractModel): + """Base mixin for AI integration providing both provider interaction and generation parameters. + + This mixin combines the functionality of message handling and generation parameters + into a single, cohesive interface for AI integration. + """ + _name = 'ai.base.mixin' + _description = 'AI Integration Base Mixin' + + # Basic Generation Parameters + temperature = fields.Float( + string='Temperature', + help='Sampling temperature. Range: [0.0 - 2.0]. Higher values make output more random, ' + 'lower values more deterministic.', + default=0.7, + digits=(3, 2)) + + top_p = fields.Float( + string='Top P', + help='Nucleus sampling: limits cumulative probability of tokens to sample from. ' + 'Range: [0.0 - 1.0].', + default=0.9, + digits=(3, 2)) + + max_tokens = fields.Integer( + string='Max Tokens', + help='Maximum number of tokens to generate. Range: [1 - 32768].', + default=2048) + + stop_sequences = fields.Char( + string='Stop Sequences', + help='Comma-separated list of sequences where the model should stop generating') + + # System Settings + timeout = fields.Integer( + string='Timeout', + help='Request timeout in seconds. Range: [1 - 300].', + default=30) + + retry_count = fields.Integer( + string='Retry Count', + help='Number of times to retry failed requests. Range: [0 - 5].', + default=3) + + stream_response = fields.Boolean( + string='Stream Response', + help='Enable response streaming for real-time output.', + default=False) + + def _get_base_generation_params(self): + """Get common generation parameters as a dictionary. + + Returns: + dict: Dictionary containing all generation parameters + """ + self.ensure_one() + return { + 'temperature': self.temperature, + 'top_p': self.top_p, + 'max_tokens': self.max_tokens, + 'stop_sequences': self.stop_sequences.split(',') if self.stop_sequences else None, + 'timeout': self.timeout, + 'retry_count': self.retry_count, + 'stream_response': self.stream_response, + } + + def _get_ai_provider_instance(self, provider_instance_id=None): + """Get the AI provider instance to use. + + Args: + provider_instance_id: Optional specific provider instance to use + + Returns: + ai.provider.instance: The provider instance to use + + Raises: + UserError: If no provider instance is configured or available + """ + if provider_instance_id: + instance = self.env['ai.provider.instance'].browse(provider_instance_id) + if not instance.exists(): + raise UserError(_("Invalid provider instance")) + else: + provider_id = self.env['ir.config_parameter'].sudo().get_param( + 'ai_integration.default_provider_instance_id') + if not provider_id: + raise UserError(_("No default AI provider instance configured")) + instance = self.env['ai.provider.instance'].browse(int(provider_id)) + if not instance.exists(): + raise UserError(_("Default provider instance not found")) + + if not instance.is_active: + raise UserError(_("The selected AI provider instance is not active")) + + return instance + + def _get_ai_model(self, model_id=None, provider_instance=None): + """Get the AI model to use. + + Args: + model_id: Optional specific model to use + provider_instance: Optional provider instance (to avoid duplicate lookup) + + Returns: + ai.model: The model to use + + Raises: + UserError: If no model is configured or available + """ + if not provider_instance: + provider_instance = self._get_ai_provider_instance() + + if model_id: + model = self.env['ai.model'].browse(model_id) + if not model.exists(): + raise UserError(_("Invalid model")) + if model.provider_instance_id != provider_instance: + raise UserError(_("Model does not belong to the selected provider instance")) + else: + model_id = self.env['ir.config_parameter'].sudo().get_param( + 'ai_integration.default_model_id') + if not model_id: + raise UserError(_("No default AI model configured")) + model = self.env['ai.model'].browse(int(model_id)) + if not model.exists(): + raise UserError(_("Default model not found")) + + if not model.is_active: + raise UserError(_("The selected AI model is not active")) + + return model + + def send_ai_message(self, message: Dict[str, Any], provider_instance_id: Optional[int] = None, + model_id: Optional[int] = None, **kwargs): + """Send a message to an AI provider instance. + + Args: + message: The message to send + provider_instance_id: Optional specific provider instance to use + model_id: Optional specific model to use + **kwargs: Additional provider-specific parameters + + Returns: + str: The response from the AI provider + + Raises: + UserError: If there's an error with the AI provider + """ + provider_instance = self._get_ai_provider_instance(provider_instance_id) + model = self._get_ai_model(model_id, provider_instance) + + # Merge generation parameters with provider-specific parameters + params = {**self._get_base_generation_params(), **kwargs} + + try: + return provider_instance.send_message(message, model=model, **params) + except Exception as e: + _logger.error("Error sending message to AI provider: %s", str(e)) + raise UserError(_("Failed to send message to AI provider: %s") % str(e)) diff --git a/ai_integration/models/mixins/ai_generation_params.py b/ai_integration/models/mixins/ai_generation_params.py deleted file mode 100644 index 71792aa..0000000 --- a/ai_integration/models/mixins/ai_generation_params.py +++ /dev/null @@ -1,64 +0,0 @@ -# -*- coding: utf-8 -*- -from odoo import models, fields, api, _ - -class AIGenerationParams(models.AbstractModel): - """Mixin for common AI generation parameters across different providers.""" - _name = 'ai.generation.params' - _description = 'Common AI Generation Parameters' - - # Basic Generation Parameters - temperature = fields.Float( - string='Temperature', - help='Sampling temperature. Range: [0.0 - 2.0]. Higher values make output more random, lower values more deterministic.', - default=0.7, - digits=(3, 2)) - - top_p = fields.Float( - string='Top P', - help='Nucleus sampling: limits cumulative probability of tokens to sample from. Range: [0.0 - 1.0].', - default=0.9, - digits=(3, 2)) - - max_tokens = fields.Integer( - string='Max Tokens', - help='Maximum number of tokens to generate. Range: [1 - 32768].', - default=2048) - - stop_sequences = fields.Char( - string='Stop Sequences', - help='Comma-separated list of sequences where the model should stop generating') - - # System Settings - timeout = fields.Integer( - string='Timeout', - help='Request timeout in seconds. Range: [1 - 300].', - default=30) - - retry_count = fields.Integer( - string='Retry Count', - help='Number of times to retry failed requests. Range: [0 - 5].', - default=3) - - stream_response = fields.Boolean( - string='Stream Response', - help='Enable response streaming for real-time output.', - default=False) - - def _get_base_generation_params(self): - """Get common generation parameters as a dictionary.""" - self.ensure_one() - - params = { - 'temperature': self.temperature, - 'top_p': self.top_p, - 'max_tokens': self.max_tokens, - 'stream': self.stream_response, - } - - if self.stop_sequences: - params['stop'] = [ - seq.strip() - for seq in self.stop_sequences.split(',') - ] - - return params diff --git a/ai_integration/models/mixins/ai_mixin.py b/ai_integration/models/mixins/ai_mixin.py deleted file mode 100644 index f8516ea..0000000 --- a/ai_integration/models/mixins/ai_mixin.py +++ /dev/null @@ -1,150 +0,0 @@ -# -*- coding: utf-8 -*- -import logging -from typing import List, Dict, Any, Optional -from odoo import models, api, fields, _ -from odoo.exceptions import UserError - -_logger = logging.getLogger(__name__) - - -class AIMixin(models.AbstractModel): - _name = 'ai.mixin' - _description = 'AI Integration Mixin' - - def _get_ai_provider_instance(self, provider_instance_id=None): - """Get the AI provider instance to use. - - Args: - provider_instance_id: Optional specific provider instance to use - - Returns: - ai.provider.instance: The provider instance to use - - Raises: - UserError: If no provider instance is configured or available - """ - if provider_instance_id: - instance = self.env['ai.provider.instance'].browse(provider_instance_id) - if not instance.exists(): - raise UserError(_("Invalid provider instance")) - else: - provider_id = self.env['ir.config_parameter'].sudo().get_param('ai_integration.default_provider_instance_id') - if not provider_id: - raise UserError(_("No default AI provider instance configured")) - instance = self.env['ai.provider.instance'].browse(int(provider_id)) - if not instance.exists(): - raise UserError(_("Default provider instance not found")) - - if not instance.is_active: - raise UserError(_("The selected AI provider instance is not active")) - - return instance - - def _get_ai_model(self, model_id=None, provider_instance=None): - """Get the AI model to use. - - Args: - model_id: Optional specific model to use - provider_instance: Optional provider instance (to avoid duplicate lookup) - - Returns: - ai.model: The model to use - - Raises: - UserError: If no model is configured or available - """ - provider_instance = provider_instance or self._get_ai_provider_instance() - - if model_id: - model = self.env['ai.model'].browse(model_id) - if not model.exists(): - raise UserError(_("Invalid AI model")) - if model.provider_instance_id != provider_instance: - raise UserError(_("The specified model does not belong to the selected provider instance")) - else: - model_id = self.env['ir.config_parameter'].sudo().get_param('ai_integration.default_model_id') - if not model_id: - raise UserError(_("No default AI model configured")) - model = self.env['ai.model'].browse(int(model_id)) - if not model.exists(): - raise UserError(_("Default AI model not found")) - - if not model.is_active: - raise UserError(_("The selected AI model is not active")) - - return model - - def send_ai_message(self, message: Dict[str, Any], provider_instance_id: Optional[int] = None, - model_id: Optional[int] = None, **kwargs) -> str: - """Send a message to an AI provider instance. - - Args: - message: The message to send - provider_instance_id: Optional specific provider instance to use - model_id: Optional specific model to use - **kwargs: Additional provider-specific parameters - - Returns: - str: The response from the AI provider - - Raises: - UserError: If there's an error with the AI provider - """ - try: - instance = self._get_ai_provider_instance(provider_instance_id) - model = self._get_ai_model(model_id, instance) - return instance.send_message(message, model, **kwargs) - except Exception as e: - _logger.error("Error sending AI message: %s", str(e)) - raise UserError(_("Error communicating with AI provider: %s", str(e))) - - def process_batch_ai(self, items: List[Any], processor_func: callable, - provider_instance_id: Optional[int] = None, - model_id: Optional[int] = None, **kwargs) -> List[Any]: - """Process a batch of items using AI. - - Args: - items: List of items to process - processor_func: Function that processes each item and returns AI message - provider_instance_id: Optional specific provider instance to use - model_id: Optional specific model to use - **kwargs: Additional parameters passed to processor_func - - Returns: - List[Any]: List of processed results - - Example: - def _process_item(item, **kwargs): - return {'role': 'user', 'content': f'Analyze: {item.name}'} - - results = self.process_batch_ai(items, _process_item) - """ - if not items: - return [] - - company = self.env.company - batch_size = company.ai_batch_size or 10 - results = [] - - for i in range(0, len(items), batch_size): - batch = items[i:i + batch_size] - batch_messages = [processor_func(item, **kwargs) for item in batch] - - for message in batch_messages: - result = self.send_ai_message( - message, - provider_instance_id=provider_instance_id, - model_id=model_id - ) - results.append(result) - - return results - - def _prepare_ai_message(self, **kwargs): - """Prepare a message to send to the AI provider. - This method should be implemented by models using this mixin. - - Returns: - dict: The prepared message - """ - raise NotImplementedError(_("Method _prepare_ai_message must be implemented by models using AI mixin")) diff --git a/ai_integration/security/ir.model.access.csv b/ai_integration/security/ir.model.access.csv index bee26f6..8498e38 100644 --- a/ai_integration/security/ir.model.access.csv +++ b/ai_integration/security/ir.model.access.csv @@ -7,3 +7,5 @@ access_ai_model_stats_user,ai.model.stats.user,model_ai_model_stats,base.group_u access_ai_model_stats_system,ai.model.stats.system,model_ai_model_stats,base.group_system,1,1,1,1 access_ai_generation_params_user,ai.generation.params.user,model_ai_generation_params,base.group_user,1,0,0,0 access_ai_generation_params_system,ai.generation.params.system,model_ai_generation_params,base.group_system,1,1,1,1 +access_ai_provider_user,ai.provider.user,model_ai_provider,base.group_user,1,0,0,0 +access_ai_provider_system,ai.provider.system,model_ai_provider,base.group_system,1,1,1,1 diff --git a/ollama_ai_integration/__manifest__.py b/ollama_ai_integration/__manifest__.py index 6d98e35..ba48dc8 100644 --- a/ollama_ai_integration/__manifest__.py +++ b/ollama_ai_integration/__manifest__.py @@ -5,7 +5,7 @@ 'summary': 'Integration with Ollama AI models', 'description': """ Ollama Integration -================= +================== This module provides integration with Ollama, allowing you to use local AI models in your Odoo instance. Features include: @@ -16,10 +16,12 @@ in your Odoo instance. Features include: """, 'author': 'Bemade', 'website': 'https://www.bemade.org', - 'depends': ['ai_integration'], + 'depends': [ + 'ai_integration' + ], 'data': [ 'data/ollama_provider.xml', - 'views/ollama_views.xml', + # 'views/ollama_views.xml', 'views/ollama_stats_views.xml', 'security/ir.model.access.csv', ], diff --git a/ollama_ai_integration/models/__init__.py b/ollama_ai_integration/models/__init__.py index 06829c9..bfd61d5 100644 --- a/ollama_ai_integration/models/__init__.py +++ b/ollama_ai_integration/models/__init__.py @@ -1,4 +1,26 @@ +"""Ollama AI Integration Models Package. + +This package contains all the model definitions required for integrating +Ollama AI with Odoo's AI framework. The models are loaded in a specific +order to handle dependencies correctly. + +Module Structure: +1. ollama_provider_mixin - Base configuration and parameter definitions +2. ollama_provider - Core Ollama API integration implementation +3. ollama_model_stats - Usage statistics and performance tracking +4. ai_provider_instance - Instance-specific configuration and management + +Note: The import order is important to avoid circular dependencies. +""" + +# Base Configuration from . import ollama_provider_mixin -from . import ai_provider_ollama -from . import ollama_instance + +# Core Implementation +from . import ollama_provider + +# Statistics and Monitoring from . import ollama_model_stats + +# Instance Management +from . import ai_provider_instance diff --git a/ollama_ai_integration/models/ai_provider_instance.py b/ollama_ai_integration/models/ai_provider_instance.py new file mode 100644 index 0000000..19c3b3f --- /dev/null +++ b/ollama_ai_integration/models/ai_provider_instance.py @@ -0,0 +1,131 @@ +from odoo import models, fields, api, _ + +class AIProviderInstance(models.Model): + """Extends the AI Provider Instance model to support Ollama-specific configuration. + + This model inherits from both ai.provider.instance and ollama.provider.mixin to: + 1. Add Ollama-specific fields (num_ctx, temperature, etc.) + 2. Handle field visibility based on provider_type + 3. Manage field cleanup when switching providers + + Note: This extends the base ai.provider.instance model instead of creating + a new one to ensure seamless integration with the core AI framework. + """ + _name = 'ai.provider.instance' + _inherit = ['ollama.provider.mixin', 'mail.thread'] + _description = 'AI Provider Instance' + + # Basic Fields + name = fields.Char( + string='Name', + required=True, + tracking=True, + help='Name of this AI provider instance') + + active = fields.Boolean( + string='Active', + default=True, + tracking=True, + help='Whether this provider instance is active') + + host = fields.Char( + string='Host', + required=True, + default='http://localhost:11434', + tracking=True, + help='Ollama server host URL') + + company_id = fields.Many2one( + 'res.company', + string='Company', + required=True, + default=lambda self: self.env.company, + help='Company this provider instance belongs to') + + @api.onchange('provider_type') + def _onchange_provider_type(self): + """Automatically clear Ollama-specific fields when switching provider type. + + This ensures that Ollama configuration is only kept when the provider + type is 'ollama'. When switching to another provider, all Ollama-specific + fields are reset to their default values to avoid confusion. + """ + if self.provider_type != 'ollama': + self.update({ + 'num_ctx': False, # Context length + 'temperature': False, # Sampling temperature + 'top_p': False, # Nucleus sampling threshold + 'top_k': False, # Top-k sampling threshold + 'repeat_penalty': False, # Penalty for repeated tokens + }) + + def test_connection(self): + """Test the connection to the Ollama server. + + This method attempts to connect to the Ollama server and verify + that it is responding correctly. It will raise a user-friendly + error if the connection fails. + + Returns: + dict: Action to display success message + """ + self.ensure_one() + if self.provider_type != 'ollama': + return + + try: + # Try to list models as a basic connectivity test + self.env['ai.provider.ollama']._get_models(self) + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Successfully connected to Ollama server'), + 'sticky': False, + 'type': 'success', + } + } + except Exception as e: + raise UserError(_('Connection test failed: %s', str(e))) + + def sync_models(self): + """Synchronize available models from the Ollama server. + + This method fetches the list of available models from the Ollama + server and creates or updates the corresponding AI model records + in Odoo. + + Returns: + dict: Action to display success message + """ + self.ensure_one() + if self.provider_type != 'ollama': + return + + try: + provider = self.env['ai.provider.ollama'] + models = provider._get_models(self) + + for model_data in models: + # Create or update AI model record + self.env['ai.model'].create_or_update({ + 'name': model_data['name'], + 'identifier': model_data['id'], + 'provider_instance_id': self.id, + 'model_type': 'text', + 'active': True, + }) + + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': _('Success'), + 'message': _('Successfully synchronized %d models', len(models)), + 'sticky': False, + 'type': 'success', + } + } + except Exception as e: + raise UserError(_('Model synchronization failed: %s', str(e))) diff --git a/ollama_ai_integration/models/ai_provider_ollama.py b/ollama_ai_integration/models/ai_provider_ollama.py.bak similarity index 89% rename from ollama_ai_integration/models/ai_provider_ollama.py rename to ollama_ai_integration/models/ai_provider_ollama.py.bak index 3f6645e..6d6a39f 100644 --- a/ollama_ai_integration/models/ai_provider_ollama.py +++ b/ollama_ai_integration/models/ai_provider_ollama.py.bak @@ -1,6 +1,7 @@ from odoo import models, fields, api, _ +from .ollama_provider_mixin import OllamaProviderMixin -class OllamaProvider(models.Model): +class OllamaAIProvider(models.Model, OllamaProviderMixin): _name = 'ai.provider.ollama' _description = 'Ollama AI Provider' _inherit = ['ai.provider.interface'] diff --git a/ollama_ai_integration/models/ollama_model_stats.py b/ollama_ai_integration/models/ollama_model_stats.py index d9f93df..996da76 100644 --- a/ollama_ai_integration/models/ollama_model_stats.py +++ b/ollama_ai_integration/models/ollama_model_stats.py @@ -2,10 +2,28 @@ from odoo import models, fields, api from datetime import datetime, timedelta class OllamaModelStats(models.Model): + """Tracks and stores daily usage statistics for Ollama AI models. + + This model maintains detailed daily statistics for each Ollama model, + including request counts, token usage, response times, and error rates. + It inherits from ai.model.stats for base statistics functionality. + + Key Features: + - Daily usage tracking per model + - Performance metrics collection + - Error rate monitoring + - Version tracking for model updates + + Technical Details: + - One stat entry per model per day (enforced by SQL constraint) + - Automatic version tracking from Ollama API + - Aggregated statistics calculation + - Ordered by date for easy historical analysis + """ _name = 'ollama.model.stats' _description = 'Ollama Model Usage Statistics' _inherit = ['ai.model.stats'] - _order = 'date desc' + _order = 'date desc' # Most recent stats first model_id = fields.Many2one('ai.model', string='Model', required=True, ondelete='cascade') date = fields.Date(string='Date', required=True, default=fields.Date.context_today) @@ -20,7 +38,23 @@ class OllamaModelStats(models.Model): ] def _update_stats(self, model, tokens, response_time, error=False): - """Update statistics for a model.""" + """Update daily statistics for a specific model. + + This method handles the creation or update of daily statistics entries. + It maintains running averages and cumulative counts for various metrics. + + Args: + model (ai.model): The model record being tracked + tokens (int): Number of tokens in the current request + response_time (float): Response time in milliseconds + error (bool): Whether this request resulted in an error + + Technical Notes: + - Creates new stat entry if none exists for today + - Updates running averages for response time + - Fetches and stores model version from Ollama API + - Maintains cumulative counts for requests and errors + """ today = fields.Date.context_today(self) stats = self.search([ ('model_id', '=', model.id), diff --git a/ollama_ai_integration/models/ollama_provider.py b/ollama_ai_integration/models/ollama_provider.py index c307cd2..6abf869 100644 --- a/ollama_ai_integration/models/ollama_provider.py +++ b/ollama_ai_integration/models/ollama_provider.py @@ -1,15 +1,70 @@ import json import logging import requests -from odoo import models, fields, _ +from odoo import models, fields, api, _ from odoo.exceptions import UserError _logger = logging.getLogger(__name__) class OllamaProvider(models.Model): + """Main Ollama AI Provider implementation. + + This model implements the core functionality for interacting with Ollama's API, + including model management, text generation, and error handling. + + Key Responsibilities: + - Model discovery and validation + - API communication and error handling + - Request formatting and response parsing + - Resource management and cleanup + + Technical Details: + - Implements the ai.provider.interface for standardized AI provider integration + - Uses Ollama's HTTP API for all operations + - Handles both synchronous and asynchronous requests + - Provides detailed error messages for troubleshooting + """ _name = 'ai.provider.ollama' _description = 'Ollama AI Provider' _inherit = ['ai.provider.interface'] + + @api.model + def _get_models(self, instance): + """Get list of available models from Ollama server. + + Args: + instance (ai.provider.instance): Provider instance to get models for + + Returns: + list: List of model dictionaries with keys: + - name: Model name + - id: Model identifier + - details: Additional model metadata + + Raises: + UserError: If unable to connect or retrieve models + """ + try: + response = requests.get(f"{instance.host}/api/tags") + response.raise_for_status() + + models_data = response.json().get('models', []) + return [{ + 'name': model['name'], + 'id': model['name'], + 'details': model + } for model in models_data] + + except requests.exceptions.RequestException as e: + raise UserError(_('Failed to connect to Ollama server: %s', str(e))) + except (KeyError, ValueError) as e: + raise UserError(_('Invalid response from Ollama server: %s', str(e))) + + # API Configuration + timeout = fields.Integer( + string='Timeout', + default=30, + help='API request timeout in seconds') def _get_provider_type(self): return 'ollama' diff --git a/ollama_ai_integration/models/ollama_provider_mixin.py b/ollama_ai_integration/models/ollama_provider_mixin.py index 45d4a3b..126d6f8 100644 --- a/ollama_ai_integration/models/ollama_provider_mixin.py +++ b/ollama_ai_integration/models/ollama_provider_mixin.py @@ -1,25 +1,96 @@ from odoo import models, fields, api, _ class OllamaProviderMixin(models.AbstractModel): + """Mixin model that provides Ollama-specific configuration parameters. + + This mixin is designed to be inherited by models that need to interact with + the Ollama AI provider. It provides all the necessary fields and methods + for configuring and interacting with Ollama's API. + + Key Features: + - Provider type selection and validation + - Context window configuration + - Advanced sampling parameters (temperature, top-k, top-p) + - Token generation controls + + Technical Details: + - Inherits from ai.generation.params for base AI generation parameters + - Implements Ollama-specific API parameters + - Provides default values optimized for general use cases + """ _name = 'ollama.provider.mixin' _description = 'Ollama Provider Configuration Mixin' _inherit = ['ai.generation.params'] + # Provider Configuration provider_type = fields.Selection( - selection_add=[('ollama', 'Ollama')], - ondelete={'ollama': 'cascade'}) + selection=[('ollama', 'Ollama')], + string='Provider Type', + required=True, + default='ollama', + help='Type of AI provider - Must be Ollama for this configuration') - # Ollama-specific Parameters + # Model Parameters + model_name = fields.Char( + string='Model Name', + help='Name of the Ollama model to use (e.g. llama2, mistral, codellama)', + required=True, + default='llama2') + + # Context Window Configuration num_ctx = fields.Integer( string='Context Length', - help='Maximum number of tokens to consider for context. Range: [0 - 32768].', + help='Maximum number of tokens to consider for context. A larger context window allows ' + 'the model to access more historical information but requires more memory. ' + 'Range: [0 - 32768].', default=4096) - - # Advanced Sampling Parameters + + # Generation Parameters + temperature = fields.Float( + string='Temperature', + help='Controls randomness in the output. Higher values make the output more random, ' + 'while lower values make it more focused and deterministic. ' + 'Range: [0.0 - 2.0]', + default=0.8) + + top_p = fields.Float( + string='Top P', + help='Nucleus sampling: only consider the tokens whose cumulative probability exceeds ' + 'this value. Lower values make the output more focused. ' + 'Range: [0.0 - 1.0]', + default=0.9) + top_k = fields.Integer( string='Top K', - help='Limits the number of tokens to sample from. Range: [1 - 100].', + help='Only consider the top K tokens for text generation. Lower values make the ' + 'output more focused. Set to 0 to disable. ' + 'Range: [0 - 100]', default=40) + + repeat_penalty = fields.Float( + string='Repeat Penalty', + help='Penalty for repeating tokens. Higher values make the output less repetitive. ' + 'Range: [0.0 - 2.0]', + default=1.1) + + # Advanced Configuration + stop_sequences = fields.Char( + string='Stop Sequences', + help='Comma-separated list of sequences where the model should stop generating further tokens.') + + top_k = fields.Integer( + string='Top K', + help='Limits the cumulative probability of tokens to sample from. Only the top K ' + 'most likely tokens are considered for sampling at each step. ' + 'Range: [1 - 100].', + default=40) + + top_p = fields.Float( + string='Top P (Nucleus Sampling)', + help='Limits the cumulative probability of tokens to sample from. Only the most likely ' + 'tokens with total probability mass of top_p are considered. ' + 'Range: [0.0 - 1.0].', + default=0.9) min_p = fields.Float( string='Min P', diff --git a/ollama_ai_integration/security/ir.model.access.csv b/ollama_ai_integration/security/ir.model.access.csv index ad2ea39..1cebd2d 100644 --- a/ollama_ai_integration/security/ir.model.access.csv +++ b/ollama_ai_integration/security/ir.model.access.csv @@ -1,3 +1,5 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink access_ollama_model_stats_user,ollama.model.stats.user,model_ollama_model_stats,base.group_user,1,0,0,0 access_ollama_model_stats_manager,ollama.model.stats.manager,model_ollama_model_stats,base.group_system,1,1,1,1 +access_ai_provider_ollama_user,ai.provider.ollama.user,model_ai_provider_ollama,base.group_user,1,0,0,0 +access_ai_provider_ollama_manager,ai.provider.ollama.manager,model_ai_provider_ollama,base.group_system,1,1,1,1 diff --git a/ollama_ai_integration/views/ollama_views.xml b/ollama_ai_integration/views/ollama_views.xml index 15ff3f5..c459b12 100644 --- a/ollama_ai_integration/views/ollama_views.xml +++ b/ollama_ai_integration/views/ollama_views.xml @@ -15,6 +15,9 @@ + + +