talking to ollama

This commit is contained in:
xtremxpert 2025-02-20 09:43:35 -05:00
parent e48e6d9aef
commit 2df5651f12
35 changed files with 276 additions and 146 deletions

View file

@ -24,12 +24,13 @@ into Odoo. It includes:
'security/ai_security.xml',
'security/ir_rule.xml',
'security/ir.model.access.csv',
'views/menu.xml',
'views/res_company_views.xml',
'views/res_config_settings_views.xml',
'views/ai_provider_views.xml',
'views/ai_provider_instance_views.xml',
'views/ai_model_views.xml',
'views/ai_model_stats_views.xml',
'views/menu.xml',
],
'installable': True,
'application': False,

View file

@ -9,19 +9,6 @@ class AIModel(models.Model):
_order = 'sequence, name'
_check_company = False # Disable automatic company checks
@api.model
def _has_provider_modules(self):
"""Check if any AI provider modules are installed."""
modules = ['ollama_ai_integration', 'chatgpt_ai_integration']
return any(self.env['ir.module.module'].search([('name', 'in', modules), ('state', '=', 'installed')]))
@api.model
def default_get(self, fields_list):
"""Override default_get to prevent creation if no provider modules are installed."""
if not self._has_provider_modules():
raise UserError(_('No AI provider modules are installed. Please install at least one provider module (e.g., Ollama or ChatGPT) before creating an AI model.'))
return super().default_get(fields_list)
active = fields.Boolean(
string='Active',
default=True,

View file

@ -4,25 +4,20 @@ from odoo.addons.mail.models.mail_thread import MailThread
from odoo.exceptions import UserError
class AIProviderInstance(models.Model):
class BaseAIProviderInstance(models.Model):
_name = 'ai.provider.instance'
_description = 'AI Provider Instance'
_order = 'name'
_check_company = False # Disable automatic company checks
_inherit = ['mail.thread', 'ai.base.mixin']
@api.model
def _has_provider_modules(self):
"""Check if any AI provider modules are installed."""
modules = ['ollama_ai_integration', 'chatgpt_ai_integration']
return any(self.env['ir.module.module'].search([('name', 'in', modules), ('state', '=', 'installed')]))
@api.model
def default_get(self, fields_list):
"""Override default_get to prevent creation if no provider modules are installed."""
if not self._has_provider_modules():
raise UserError(_('No AI provider modules are installed. Please install at least one provider module (e.g., Ollama or ChatGPT) before creating a provider instance.'))
return super().default_get(fields_list)
defaults = super().default_get(fields_list)
if defaults.get('provider_type', 'none') == 'none':
defaults['provider_type'] = 'ollama'
return defaults
active = fields.Boolean(
string='Active',
@ -35,23 +30,13 @@ class AIProviderInstance(models.Model):
help='Name of this provider instance (e.g., "OpenWebUI Production", "Ollama Local")'
)
provider_id = fields.Many2one(
'ai.provider',
string='Provider',
required=True,
ondelete='restrict',
help='The AI provider configuration'
)
provider_type = fields.Selection(
selection=[
('none', 'None') # Base selection, will be extended by provider modules
],
[('none', 'None')], # Base selection, will be extended by provider modules
string='Provider Type',
required=True,
ondelete={'none': 'set default'},
default='none',
help='Type of AI provider'
help='The type of AI provider for this instance',
ondelete={'none': lambda r: r.write({'provider_type': 'none'})}
)
host = fields.Char(
@ -62,7 +47,8 @@ class AIProviderInstance(models.Model):
api_key = fields.Char(
string='API Key',
help='API key if required by the provider'
help='API key if required by the provider',
invisible=lambda self: self.provider_type == 'ollama'
)
@ -99,17 +85,13 @@ class AIProviderInstance(models.Model):
def test_connection(self):
"""Test the connection to this provider instance."""
self.ensure_one()
provider_model = f'ai.provider.{self.provider_type}'
if provider_model not in self.env:
raise UserError(_("Provider type %s is not installed", self.provider_type))
return self.env[provider_model].test_connection(self)
if self.provider_type == 'none':
raise UserError(_('Please select a provider type'))
return {'type': 'ir.actions.act_window_close'}
def sync_models(self):
"""Synchronize models from this provider instance."""
self.ensure_one()
provider_model = f'ai.provider.{self.provider_type}'
if provider_model not in self.env:
raise UserError(_("Provider type %s is not installed", self.provider_type))
return self.env[provider_model].sync_models(self)
if self.provider_type == 'none':
raise UserError(_('Please select a provider type'))

View file

@ -17,49 +17,39 @@ Le module AI Integration fournit une infrastructure flexible pour intégrer diff
### 2. AI Provider Instance (`ai.provider.instance`)
- **Description**: Instance spécifique d'un fournisseur d'IA
- **Héritage**: `mail.thread`, `ai.base.mixin`
- **Champs principaux**:
- `name`: Nom de l'instance
- `name`: Nom de l'instance (ex: "OpenWebUI Production", "Ollama Local")
- `provider_id`: Fournisseur associé
- `provider_type`: Type de fournisseur
- `host`: Adresse de l'hôte
- `api_key`: Clé API (si nécessaire)
- `provider_type`: Type de fournisseur (extensible par modules)
- `active`: État actif/inactif
- **Validation**:
- Vérifie la présence d'au moins un module fournisseur installé
### 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
- `identifier`: Identifiant technique (ex: gpt-3.5-turbo, mistral-7b)
- `provider_instance_id`: Instance du fournisseur (cascade)
- `provider_type`: Type de fournisseur (relié à l'instance)
- `active`: État actif/inactif
- `sequence`: Ordre d'affichage
- **Validation**:
- Vérifie la présence d'au moins un module fournisseur installé
### 4. AI Model Stats (`ai.model.stats`)
- **Description**: Statistiques d'utilisation des modèles
### 4. AI Generation Parameters (`ai.generation.params`)
- **Description**: Paramètres de génération pour les modèles d'IA
- **Type**: Modèle abstrait
- **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)
- `temperature`: Contrôle de l'aléatoire (défaut: 0.7)
- `repeat_penalty`: Pénalité de répétition (défaut: 1.1)
- `max_tokens`: Nombre maximum de tokens (défaut: 2048)
- `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
- `frequency_penalty`: Pénalité de fréquence (défaut: 0.0)
- `presence_penalty`: Pénalité de présence (défaut: 0.0)
## Configuration
## Configuration et Interfaces
### 1. Res Config Settings
- **Description**: Paramètres de configuration globaux
@ -73,9 +63,7 @@ Le module AI Integration fournit une infrastructure flexible pour intégrer diff
- **Méthodes principales**:
- `_get_default_provider_instance`: Obtenir l'instance par défaut
## Interfaces
### AI Provider Interface (`ai.provider.interface`)
### 3. AI Provider Interface (`ai.provider.interface`)
- **Description**: Interface abstraite pour les fournisseurs d'IA
- **Méthodes requises**:
- `send_message`: Envoyer un message
@ -84,7 +72,19 @@ Le module AI Integration fournit une infrastructure flexible pour intégrer diff
## 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. **Architecture Modulaire**:
- Modules fournisseurs disponibles: `ollama_ai_integration`, `chatgpt_ai_integration`
- Vérification de la présence d'au moins un module fournisseur avant création d'instances
2. **Héritage et Extensions**:
- Les instances de fournisseur héritent de `mail.thread` et `ai.base.mixin`
- Les paramètres de génération sont définis dans le modèle abstrait `ai.generation.params`
3. **Configuration Hiérarchique**:
- Configuration globale > Paramètres société > Instance
- Paramètres de génération personnalisables à plusieurs niveaux
4. **Sécurité et Validation**:
- Vérifications de sécurité intégrées
- Validation des modules requis
- Gestion des paramètres de génération avec valeurs par défaut

View file

@ -20,9 +20,3 @@ class ResConfigSettings(models.TransientModel):
config_parameter='ai_integration.ai_batch_size',
default=100,
default_model='ai.provider.instance')
@api.onchange('default_provider_instance_id')
def _onchange_default_provider_instance(self):
"""Reset model when provider instance changes"""
if self.default_provider_instance_id:
self.default_model_id = False

View file

@ -5,7 +5,7 @@
<field name="name">ai.model.stats.list</field>
<field name="model">ai.model.stats</field>
<field name="arch" type="xml">
<list string="Model Statistics">
<list string="Model Statistics" create="false">
<field name="date"/>
<field name="model_id"/>
<field name="provider_instance_id"/>

View file

@ -6,7 +6,7 @@
<field name="model">ai.model</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="AI Models">
<list string="AI Models" create="false">
<field name="sequence" widget="handle"/>
<field name="name"/>
<field name="provider_instance_id"/>

View file

@ -5,7 +5,7 @@
<field name="name">ai.provider.instance.list</field>
<field name="model">ai.provider.instance</field>
<field name="arch" type="xml">
<list string="AI Provider Instances">
<list name="ai_instance" string="AI Provider Instances" create="true">
<field name="name"/>
<field name="provider_type"/>
<field name="host"/>
@ -44,7 +44,7 @@
<group>
<field name="provider_type"/>
<field name="host"/>
<field name="api_key" password="True"/>
<field name="api_key"/>
</group>
<group>
<field name="timeout"/>

View file

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Tree View -->
<record id="ai_provider_view_tree" model="ir.ui.view">
<field name="name">ai.provider.tree</field>
<field name="model">ai.provider</field>
<field name="type">list</field>
<field name="arch" type="xml">
<list string="AI Providers" create="false">
<field name="name"/>
<field name="code"/>
<field name="default_host"/>
<field name="active"/>
</list>
</field>
</record>
<!-- Form View -->
<record id="ai_provider_view_form" model="ir.ui.view">
<field name="name">ai.provider.form</field>
<field name="model">ai.provider</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="AI Provider">
<sheet>
<div class="oe_button_box" name="button_box">
<button name="test_connection" type="object"
string="Test Connection" class="oe_stat_button"
icon="fa-plug"/>
<button name="get_models" type="object"
string="Get Models" class="oe_stat_button"
icon="fa-list"/>
</div>
<div class="oe_title">
<label for="name" class="oe_edit_only"/>
<h1><field name="name" placeholder="e.g. Ollama"/></h1>
</div>
<group>
<group>
<field name="code"/>
<field name="default_host"/>
</group>
<group>
<field name="active"/>
</group>
</group>
<notebook>
<page string="Description" name="description">
<field name="description" nolabel="1" placeholder="Enter a description..."/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- Search View -->
<record id="ai_provider_view_search" model="ir.ui.view">
<field name="name">ai.provider.search</field>
<field name="model">ai.provider</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Search AI Providers">
<field name="name"/>
<field name="code"/>
<filter string="Active" name="active" domain="[('active', '=', True)]"/>
<filter string="Archived" name="inactive" domain="[('active', '=', False)]"/>
</search>
</field>
</record>
<!-- Action -->
<record id="ai_provider_action" model="ir.actions.act_window">
<field name="name">AI Providers</field>
<field name="res_model">ai.provider</field>
<field name="view_mode">list,form</field>
<field name="context">{'search_default_active': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
No AI providers found
</p>
<p>
AI providers are automatically created when you install AI integration modules.
</p>
</field>
</record>
</odoo>

View file

@ -16,12 +16,20 @@
<menuitem id="menu_ai_settings"
name="Settings"
parent="menu_ai_config"
action="action_ai_integration_configuration"
action="ai_integration.action_ai_integration_configuration"
sequence="10"
groups="base.group_system"/>
<!-- Provider Types menu -->
<menuitem id="menu_ai_provider_types"
name="Provider Types"
parent="menu_ai_config"
action="ai_provider_action"
sequence="10"
groups="base.group_system"/>
<!-- Provider Instances menu -->
<menuitem id="menu_ai_providers"
<menuitem id="menu_ai_provider_instances"
name="Provider Instances"
parent="menu_ai_config"
action="ai_provider_instance_action"

View file

@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Settings Action -->
<record id="action_ai_integration_configuration" model="ir.actions.act_window">
<field name="name">AI Integration Settings</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">res.config.settings</field>
<field name="view_mode">form</field>
<field name="target">inline</field>
<field name="context">{"module" : "ai_integration"}</field>
</record>
<record id="res_config_settings_view_form" model="ir.ui.view">
<field name="name">res.config.settings.view.form.inherit.ai.integration</field>
<field name="model">res.config.settings</field>

View file

@ -20,9 +20,9 @@ in your Odoo instance. Features include:
'ai_integration'
],
'data': [
'data/ollama_provider.xml',
# 'views/ollama_views.xml',
'data/ai_provider_data.xml',
'views/ollama_stats_views.xml',
'views/ai_provider_instance_views.xml',
'security/ir.model.access.csv',
],
'external_dependencies': {

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- Ollama Provider -->
<record id="ai_provider_ollama" model="ai.provider">
<field name="name">Ollama</field>
<field name="code">ollama</field>
<field name="description">Ollama is a local AI model provider that allows you to run various open-source models locally.</field>
<field name="default_host">http://localhost:11434</field>
<field name="active" eval="True"/>
</record>
</data>
</odoo>

View file

@ -18,6 +18,7 @@ from . import ollama_provider_mixin
# Core Implementation
from . import ollama_provider
from . import ai_provider_instance
# Statistics and Monitoring
from . import ollama_model_stats

View file

@ -1,56 +1,78 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import requests
class AIProviderInstance(models.Model):
class OllamaAIProviderInstance(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.
"""
_inherit = ['ai.provider.instance', 'ollama.provider.mixin']
_name = 'ai.provider.instance'
_inherit = ['ollama.provider.mixin', 'mail.thread']
_description = 'AI Provider Instance'
_description = 'Ollama AI Provider Instance'
# Override provider_type to add Ollama option
provider_type = fields.Selection(
selection_add=[('ollama', 'Ollama')],
ondelete={'ollama': lambda r: r.write({'provider_type': 'none'})}
)
# Basic Fields
name = fields.Char(
string='Name',
required=True,
tracking=True,
help='Name of this AI provider instance')
@api.onchange('provider_type')
def _onchange_provider_type(self):
"""Handle provider type changes.
active = fields.Boolean(
string='Active',
default=True,
tracking=True,
help='Whether this provider instance is active')
When switching to 'ollama':
- Set default host if empty
When switching away from 'ollama':
- Clear Ollama-specific fields
"""
if self.provider_type == 'ollama':
if not self.host:
self.host = 'http://localhost:11434'
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,
})
# Override default host for Ollama
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.
"""Handle provider type changes.
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.
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':
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
@ -75,7 +97,8 @@ class AIProviderInstance(models.Model):
try:
# Try to list models as a basic connectivity test
self.env['ai.provider.ollama']._get_models(self)
response = requests.get(f'{self.host}/api/tags')
response.raise_for_status()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
@ -104,18 +127,34 @@ class AIProviderInstance(models.Model):
return
try:
provider = self.env['ai.provider.ollama']
models = provider._get_models(self)
# Get models from Ollama API
response = requests.get(f'{self.host}/api/tags')
response.raise_for_status()
# Parse response
models = [{
'name': model['name'],
'id': model['name'],
} for model in response.json()['models']]
for model_data in models:
# Create or update AI model record
self.env['ai.model'].create_or_update({
vals = {
'name': model_data['name'],
'identifier': model_data['id'],
'provider_instance_id': self.id,
'model_type': 'text',
'active': True,
})
}
# Search for existing model
existing = self.env['ai.model'].search([
('identifier', '=', model_data['id']),
('provider_instance_id', '=', self.id)
], limit=1)
if existing:
existing.write(vals)
else:
self.env['ai.model'].create(vals)
return {
'type': 'ir.actions.client',

View file

@ -22,14 +22,6 @@ class OllamaProviderMixin(models.AbstractModel):
_description = 'Ollama Provider Configuration Mixin'
_inherit = ['ai.generation.params']
# Provider Configuration
provider_type = fields.Selection(
selection=[('ollama', 'Ollama')],
string='Provider Type',
required=True,
default='ollama',
help='Type of AI provider - Must be Ollama for this configuration')
# Model Parameters
model_name = fields.Char(
string='Model Name',

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Inherit AI Provider Instance List View -->
<record id="ollama_ai_provider_instance_view_list" model="ir.ui.view">
<field name="name">ai.provider.instance.list.ollama</field>
<field name="model">ai.provider.instance</field>
<field name="inherit_id" ref="ai_integration.ai_provider_instance_view_list"/>
<field name="mode">primary</field>
<field name="arch" type="xml">
<xpath expr="//list[@name='ai_instance']" position="attributes">
<attribute name="create">true</attribute>
</xpath>
</field>
</record>
</odoo>

View file

@ -112,7 +112,7 @@
<!-- Menu Item -->
<menuitem id="menu_ollama_model_stats"
name="Model Statistics"
parent="ai_integration.menu_ai_integration"
parent="ai_integration.menu_ai_config"
action="action_ollama_model_stats"
sequence="30"/>
</odoo>