next try
This commit is contained in:
parent
0676cf1e7f
commit
d295f38355
20 changed files with 616 additions and 243 deletions
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
90
ai_integration/models/doc_models.md
Normal file
90
ai_integration/models/doc_models.md
Normal file
|
|
@ -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
|
||||
|
|
@ -1,2 +1 @@
|
|||
from . import ai_mixin
|
||||
from . import ai_generation_params
|
||||
from . import ai_base_mixin
|
||||
|
|
|
|||
167
ai_integration/models/mixins/ai_base_mixin.py
Normal file
167
ai_integration/models/mixins/ai_base_mixin.py
Normal file
|
|
@ -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))
|
||||
|
|
@ -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
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -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',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
131
ollama_ai_integration/models/ai_provider_instance.py
Normal file
131
ollama_ai_integration/models/ai_provider_instance.py
Normal file
|
|
@ -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)))
|
||||
|
|
@ -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']
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
|
@ -15,6 +15,9 @@
|
|||
</button>
|
||||
</div>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='api_key']" position="replace">
|
||||
<field name="api_key" invisible="1"/>
|
||||
</xpath>
|
||||
<xpath expr="//notebook" position="inside">
|
||||
<page string="Ollama Settings" name="ollama_settings"
|
||||
invisible="provider_type != 'ollama'">
|
||||
|
|
|
|||
Loading…
Reference in a new issue