bemade-addons/ai_integration_ollama_api/models/ollama_provider_mixin.py
2025-05-29 21:35:47 -04:00

274 lines
9.6 KiB
Python

from odoo import models, fields, api, _
from odoo.exceptions import UserError
import requests
import logging
import json
_logger = logging.getLogger(__name__)
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']
# 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='deepseek-r1:32b')
# Context Window Configuration
num_ctx = fields.Integer(
string='Context Length',
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=8192)
# 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.7)
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)
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)
min_p = fields.Float(
string='Min P',
help='Sets a minimum probability threshold for token selection. Range: [0.0 - 1.0].',
default=0.05,
digits=(3, 2))
repeat_penalty = fields.Float(
string='Repeat Penalty',
help='Penalty for repeating tokens. Range: [1.0 - 2.0]. Higher values make repetition less likely.',
default=1.1,
digits=(3, 2))
# Advanced Configuration
stop_sequences = fields.Char(
string='Stop Sequences',
help='Comma-separated list of sequences where the model should stop generating further tokens.')
num_predict = fields.Integer(
string='Maximum Tokens',
help='Maximum number of tokens to predict. Set to -1 for unlimited.',
default=2048)
repeat_last_n = fields.Integer(
string='Repeat Last N',
help='Sets the context window for repeat penalty. Range: [0 - 4096]. Default is 64, 0 disables.',
default=64
)
def generate_text(self, prompt, **kwargs):
"""Generate text using the Ollama API.
Args:
prompt (str): The prompt to generate text from
**kwargs: Additional parameters to pass to the API
Returns:
str: The generated text
"""
self.ensure_one()
# Prepare the request
url = f"{self.host}/api/generate"
# Build the request data
data = {
'model': self.model_name,
'prompt': prompt,
'stream': False,
'num_ctx': self.num_ctx,
'temperature': self.temperature,
'top_k': self.top_k,
'top_p': self.top_p,
'repeat_penalty': self.repeat_penalty,
'repeat_last_n': self.repeat_last_n,
'num_predict': self.num_predict,
'min_p': self.min_p,
'seed': self.seed,
'num_gpu': self.num_gpu,
'num_thread': self.num_thread,
'mirostat': int(self.mirostat),
'mirostat_tau': self.mirostat_tau,
'mirostat_eta': self.mirostat_eta,
'num_batch': self.num_batch,
'num_keep': self.num_keep,
'tfs_z': self.tfs_z,
'skip_special_tokens': self.skip_special_tokens
}
# Add any additional parameters
if kwargs:
data.update(kwargs)
# Make the request
try:
_logger = logging.getLogger(__name__)
_logger.info("Sending request to Ollama API with data: %s", data)
response = requests.post(url, json=data, timeout=self.timeout)
response.raise_for_status()
# Log the raw response
_logger.info("Raw API response: %s", response.text)
# Parse the response
result = response.json()
response_text = result.get('response', '')
# Si la réponse est une chaîne JSON, la parser
try:
if isinstance(response_text, str):
parsed_response = json.loads(response_text)
_logger.info("Parsed nested JSON response: %s", parsed_response)
return parsed_response
else:
_logger.info("Direct response: %s", response_text)
return response_text
except json.JSONDecodeError:
# Si ce n'est pas du JSON valide, retourner le texte tel quel
_logger.info("Non-JSON response: %s", response_text)
return response_text
except requests.exceptions.RequestException as e:
raise UserError(_('Failed to generate text: %s') % str(e))
# Advanced Generation Parameters
seed = fields.Integer(
string='Random Seed',
help='Sets the random seed for generation. Range: [0 - 2147483647]. Use 0 for random.',
default=0)
num_gpu = fields.Integer(
string='Number of GPUs',
help='Number of GPUs to use for generation. Range: [0 - 8]. 0 means CPU only.',
default=1)
num_thread = fields.Integer(
string='Number of Threads',
help='Number of CPU threads to use for generation. Range: [1 - 32].',
default=8)
mirostat = fields.Selection([
('0', 'Disabled'),
('1', 'Mirostat'),
('2', 'Mirostat 2.0')],
string='Mirostat Mode',
help='Enable Mirostat sampling for controlling perplexity',
default='0')
mirostat_tau = fields.Float(
string='Mirostat Tau',
help='Mirostat target entropy. Range: [0.0 - 10.0].',
default=5.0,
digits=(3, 2))
mirostat_eta = fields.Float(
string='Mirostat Eta',
help='Mirostat learning rate. Range: [0.0 - 1.0].',
default=0.1,
digits=(3, 2))
# Ollama-specific Response Control
tfs_z = fields.Float(
string='Tail Free Sampling Z',
help='Tail free sampling parameter. Range: [0.0 - 2.0]. Higher value = more focused.',
default=1.0,
digits=(3, 2))
# System Settings
num_batch = fields.Integer(
string='Batch Size',
help='Number of prompts to batch together',
default=8)
num_keep = fields.Integer(
string='Keep Last N Tokens',
help='Number of tokens to keep from initial prompt',
default=0)
skip_special_tokens = fields.Boolean(
string='Skip Special Tokens',
help='Skip special tokens in generation',
default=True)
@api.model
def default_get(self, fields_list):
defaults = super().default_get(fields_list)
if 'provider_type' in fields_list:
defaults['provider_type'] = 'ollama'
if 'host' in fields_list and not defaults.get('host'):
defaults['host'] = 'http://localhost:11434'
return defaults
def _get_provider_options(self):
"""Get Ollama-specific options for API calls."""
self.ensure_one()
options = {
'model': self.model_name,
'temperature': self.temperature,
'num_ctx': self.num_ctx,
'num_predict': self.num_predict,
'top_k': self.top_k,
'top_p': self.top_p,
'min_p': self.min_p,
'repeat_penalty': self.repeat_penalty,
'repeat_last_n': self.repeat_last_n,
'seed': self.seed,
'num_gpu': self.num_gpu,
'num_thread': self.num_thread,
'mirostat': int(self.mirostat),
'mirostat_tau': self.mirostat_tau,
'mirostat_eta': self.mirostat_eta,
'num_batch': self.num_batch,
'num_keep': self.num_keep,
'tfs_z': self.tfs_z,
'skip_special_tokens': self.skip_special_tokens,
'stream': False
}
if self.stop_sequences:
options['stop'] = [
seq.strip()
for seq in self.stop_sequences.split(',')
if seq.strip()
]
return options