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 @@
+
+
+