From c507e5d416f2aa9b8bdaf5e9bae9b7548ec9ca31 Mon Sep 17 00:00:00 2001 From: xtremxpert Date: Wed, 26 Feb 2025 10:44:58 -0500 Subject: [PATCH] ai ?? --- ai_integration/models/ai_provider_instance.py | 23 ++- ai_integration_ollama_api/__manifest__.py | 3 +- .../data/ai_provider_instance_data.xml | 31 ++++ .../migrations/1.0.0/post-migration.py | 17 +++ .../models/ai_provider_instance.py | 71 ++++----- .../models/ollama_provider_mixin.py | 136 +++++++++++++----- 6 files changed, 196 insertions(+), 85 deletions(-) create mode 100644 ai_integration_ollama_api/data/ai_provider_instance_data.xml create mode 100644 ai_integration_ollama_api/migrations/1.0.0/post-migration.py diff --git a/ai_integration/models/ai_provider_instance.py b/ai_integration/models/ai_provider_instance.py index ae1035d..121c31f 100644 --- a/ai_integration/models/ai_provider_instance.py +++ b/ai_integration/models/ai_provider_instance.py @@ -48,14 +48,20 @@ class BaseAIProviderInstance(models.Model): api_key = fields.Char( string='API Key', help='API key if required by the provider', - invisible=lambda self: self.provider_type == 'ollama' + invisible="[('provider_type', '=', 'ollama')]" # Hide when provider type is ollama ) - - @api.onchange('provider_id') - def _onchange_provider_id(self): - if self.provider_id: - self.provider_type = self.provider_id.code + @api.model + def get_default_instance(self): + """Get the default AI provider instance to use. + + Returns: + ai.provider.instance: The default instance to use, or raises UserError if none found + """ + instance = self.env['ai.provider.instance'].search([('active', '=', True)], limit=1) + if not instance: + raise UserError(_('No active AI provider instance found. Please configure one in the settings.')) + return instance model_ids = fields.One2many( 'ai.model', @@ -76,6 +82,10 @@ class BaseAIProviderInstance(models.Model): help='Maximum number of retry attempts for failed API calls' ) + @api.model + def _valid_field_parameter(self, field, name): + return name == 'invisible' or super()._valid_field_parameter(field, name) + _sql_constraints = [ ('name_uniq', 'unique(name)', @@ -94,4 +104,3 @@ class BaseAIProviderInstance(models.Model): self.ensure_one() if self.provider_type == 'none': raise UserError(_('Please select a provider type')) - diff --git a/ai_integration_ollama_api/__manifest__.py b/ai_integration_ollama_api/__manifest__.py index a724267..e119cbc 100644 --- a/ai_integration_ollama_api/__manifest__.py +++ b/ai_integration_ollama_api/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Ollama Integration', - 'version': '1.0', + 'version': '1.0.0', 'category': 'Technical', 'summary': 'Integration with Ollama AI models', 'description': """ @@ -21,6 +21,7 @@ in your Odoo instance. Features include: ], 'data': [ 'data/ai_provider_data.xml', + 'data/ai_provider_instance_data.xml', 'views/ollama_stats_views.xml', 'views/ai_provider_instance_views.xml', 'security/ir.model.access.csv', diff --git a/ai_integration_ollama_api/data/ai_provider_instance_data.xml b/ai_integration_ollama_api/data/ai_provider_instance_data.xml new file mode 100644 index 0000000..7097bb4 --- /dev/null +++ b/ai_integration_ollama_api/data/ai_provider_instance_data.xml @@ -0,0 +1,31 @@ + + + + + + Ollama Local + ollama + http://localhost:11434 + llama3.2 + 0.7 + 40 + 0.9 + 1.1 + 4096 + 1024 + 0.05 + 64 + 0 + 1 + 8 + 0 + 5.0 + 0.1 + 8 + 0 + 1.0 + + + + + diff --git a/ai_integration_ollama_api/migrations/1.0.0/post-migration.py b/ai_integration_ollama_api/migrations/1.0.0/post-migration.py new file mode 100644 index 0000000..0998710 --- /dev/null +++ b/ai_integration_ollama_api/migrations/1.0.0/post-migration.py @@ -0,0 +1,17 @@ +def migrate(cr, version): + # Add num_predict column if it doesn't exist + cr.execute(""" + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name='ai_provider_instance' + AND column_name='num_predict' + ) THEN + ALTER TABLE ai_provider_instance + ADD COLUMN num_predict integer DEFAULT 1024; + END IF; + END + $$; + """) diff --git a/ai_integration_ollama_api/models/ai_provider_instance.py b/ai_integration_ollama_api/models/ai_provider_instance.py index dd9d041..466ceb3 100644 --- a/ai_integration_ollama_api/models/ai_provider_instance.py +++ b/ai_integration_ollama_api/models/ai_provider_instance.py @@ -36,16 +36,16 @@ class OllamaAIProviderInstance(models.Model): else: # Clear Ollama-specific fields self.update({ - 'num_ctx': False, - 'temperature': False, - 'top_p': False, - 'top_k': False, - 'repeat_penalty': False, - 'repeat_last_n': False, - 'num_thread': False, - 'num_gpu': False, - 'num_batch': False, - 'model_name': False, + '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 + 'repeat_last_n': False, # Number of tokens to consider for repeat penalty + 'num_thread': False, # Number of CPU threads to use + 'num_gpu': False, # Number of GPUs to use + 'num_batch': False, # Batch size for inference + 'model_name': False, # Model name/path }) # Override default host for Ollama @@ -53,34 +53,6 @@ class OllamaAIProviderInstance(models.Model): default='http://localhost:11434', help='Ollama server host URL') - @api.onchange('provider_type') - def _onchange_provider_type(self): - """Handle provider type changes. - - When switching to 'ollama': - - Set the provider_id to the Ollama provider - - Set default host if empty - - When switching away from 'ollama': - - Clear Ollama-specific fields - """ - if self.provider_type == 'ollama': - # Find and set the Ollama provider - ollama_provider = self.env['ai.provider'].search([('code', '=', 'ollama')], limit=1) - if ollama_provider: - self.provider_id = ollama_provider.id - if not self.host: - self.host = ollama_provider.default_host - else: - # Clear Ollama-specific fields - 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. @@ -155,16 +127,27 @@ class OllamaAIProviderInstance(models.Model): existing.write(vals) else: self.env['ai.model'].create(vals) - + + # Invalidate the cache to force reload of related records + self.invalidate_recordset(['model_ids']) + + # Return action to reload the view completely return { - 'type': 'ir.actions.client', - 'tag': 'display_notification', - 'params': { + 'type': 'ir.actions.act_window', + 'res_model': 'ai.provider.instance', + 'res_id': self.id, + 'view_mode': 'form', + 'target': 'current', + 'flags': { + 'mode': 'readonly', + 'reload': True, # Force reload + }, + 'context': {'notification': { + 'type': 'success', '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/ai_integration_ollama_api/models/ollama_provider_mixin.py b/ai_integration_ollama_api/models/ollama_provider_mixin.py index b48399b..c50a4fe 100644 --- a/ai_integration_ollama_api/models/ollama_provider_mixin.py +++ b/ai_integration_ollama_api/models/ollama_provider_mixin.py @@ -1,4 +1,10 @@ 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. @@ -27,7 +33,7 @@ class OllamaProviderMixin(models.AbstractModel): string='Model Name', help='Name of the Ollama model to use (e.g. llama2, mistral, codellama)', required=True, - default='llama2') + default='deepseek-r1:32b') # Context Window Configuration num_ctx = fields.Integer( @@ -35,7 +41,7 @@ class OllamaProviderMixin(models.AbstractModel): 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) + default=8192) # Generation Parameters temperature = fields.Float( @@ -43,32 +49,14 @@ class OllamaProviderMixin(models.AbstractModel): 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) + default=0.7) 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]', + 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='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', @@ -77,13 +65,6 @@ class OllamaProviderMixin(models.AbstractModel): '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', help='Sets a minimum probability threshold for token selection. Range: [0.0 - 1.0].', @@ -96,10 +77,97 @@ class OllamaProviderMixin(models.AbstractModel): 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) + 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( @@ -174,6 +242,7 @@ class OllamaProviderMixin(models.AbstractModel): """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, @@ -192,6 +261,7 @@ class OllamaProviderMixin(models.AbstractModel): 'num_keep': self.num_keep, 'tfs_z': self.tfs_z, 'skip_special_tokens': self.skip_special_tokens, + 'stream': False } if self.stop_sequences: