This commit is contained in:
Benoît Vézina 2025-02-19 15:12:15 -05:00
parent 0676cf1e7f
commit d295f38355
20 changed files with 616 additions and 243 deletions

View file

@ -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',

View file

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

View file

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

View file

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

View file

@ -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',

View 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

View file

@ -1,2 +1 @@
from . import ai_mixin
from . import ai_generation_params
from . import ai_base_mixin

View 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))

View file

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

View file

@ -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"))

View file

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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
7 access_ai_model_stats_system ai.model.stats.system model_ai_model_stats base.group_system 1 1 1 1
8 access_ai_generation_params_user ai.generation.params.user model_ai_generation_params base.group_user 1 0 0 0
9 access_ai_generation_params_system ai.generation.params.system model_ai_generation_params base.group_system 1 1 1 1
10 access_ai_provider_user ai.provider.user model_ai_provider base.group_user 1 0 0 0
11 access_ai_provider_system ai.provider.system model_ai_provider base.group_system 1 1 1 1

View file

@ -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',
],

View file

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

View 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)))

View file

@ -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']

View file

@ -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),

View file

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

View file

@ -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',

View file

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

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_ollama_model_stats_user ollama.model.stats.user model_ollama_model_stats base.group_user 1 0 0 0
3 access_ollama_model_stats_manager ollama.model.stats.manager model_ollama_model_stats base.group_system 1 1 1 1
4 access_ai_provider_ollama_user ai.provider.ollama.user model_ai_provider_ollama base.group_user 1 0 0 0
5 access_ai_provider_ollama_manager ai.provider.ollama.manager model_ai_provider_ollama base.group_system 1 1 1 1

View file

@ -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'">