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/ai_security.xml',
'security/ir_rule.xml', 'security/ir_rule.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
'views/menu.xml',
'views/res_company_views.xml', 'views/res_company_views.xml',
'views/res_config_settings_views.xml', 'views/res_config_settings_views.xml',
'views/ai_provider_views.xml',
'views/ai_provider_instance_views.xml', 'views/ai_provider_instance_views.xml',
'views/ai_model_views.xml', 'views/ai_model_views.xml',
'views/ai_model_stats_views.xml', 'views/ai_model_stats_views.xml',
'views/menu.xml',
], ],
'installable': True, 'installable': True,
'application': False, 'application': False,

View file

@ -9,19 +9,6 @@ class AIModel(models.Model):
_order = 'sequence, name' _order = 'sequence, name'
_check_company = False # Disable automatic company checks _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( active = fields.Boolean(
string='Active', string='Active',
default=True, default=True,

View file

@ -4,25 +4,20 @@ from odoo.addons.mail.models.mail_thread import MailThread
from odoo.exceptions import UserError from odoo.exceptions import UserError
class AIProviderInstance(models.Model): class BaseAIProviderInstance(models.Model):
_name = 'ai.provider.instance' _name = 'ai.provider.instance'
_description = 'AI Provider Instance' _description = 'AI Provider Instance'
_order = 'name' _order = 'name'
_check_company = False # Disable automatic company checks _check_company = False # Disable automatic company checks
_inherit = ['mail.thread', 'ai.base.mixin'] _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 @api.model
def default_get(self, fields_list): def default_get(self, fields_list):
"""Override default_get to prevent creation if no provider modules are installed.""" """Override default_get to prevent creation if no provider modules are installed."""
if not self._has_provider_modules(): defaults = super().default_get(fields_list)
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.')) if defaults.get('provider_type', 'none') == 'none':
return super().default_get(fields_list) defaults['provider_type'] = 'ollama'
return defaults
active = fields.Boolean( active = fields.Boolean(
string='Active', string='Active',
@ -35,23 +30,13 @@ class AIProviderInstance(models.Model):
help='Name of this provider instance (e.g., "OpenWebUI Production", "Ollama Local")' 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( 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', string='Provider Type',
required=True, required=True,
ondelete={'none': 'set default'},
default='none', 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( host = fields.Char(
@ -62,7 +47,8 @@ class AIProviderInstance(models.Model):
api_key = fields.Char( api_key = fields.Char(
string='API Key', 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): def test_connection(self):
"""Test the connection to this provider instance.""" """Test the connection to this provider instance."""
self.ensure_one() self.ensure_one()
provider_model = f'ai.provider.{self.provider_type}' if self.provider_type == 'none':
if provider_model not in self.env: raise UserError(_('Please select a provider type'))
raise UserError(_("Provider type %s is not installed", self.provider_type)) return {'type': 'ir.actions.act_window_close'}
return self.env[provider_model].test_connection(self)
def sync_models(self): def sync_models(self):
"""Synchronize models from this provider instance.""" """Synchronize models from this provider instance."""
self.ensure_one() self.ensure_one()
provider_model = f'ai.provider.{self.provider_type}' if self.provider_type == 'none':
if provider_model not in self.env: raise UserError(_('Please select a provider type'))
raise UserError(_("Provider type %s is not installed", self.provider_type))
return self.env[provider_model].sync_models(self)

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`) ### 2. AI Provider Instance (`ai.provider.instance`)
- **Description**: Instance spécifique d'un fournisseur d'IA - **Description**: Instance spécifique d'un fournisseur d'IA
- **Héritage**: `mail.thread`, `ai.base.mixin`
- **Champs principaux**: - **Champs principaux**:
- `name`: Nom de l'instance - `name`: Nom de l'instance (ex: "OpenWebUI Production", "Ollama Local")
- `provider_id`: Fournisseur associé - `provider_id`: Fournisseur associé
- `provider_type`: Type de fournisseur - `provider_type`: Type de fournisseur (extensible par modules)
- `host`: Adresse de l'hôte
- `api_key`: Clé API (si nécessaire)
- `active`: État actif/inactif - `active`: État actif/inactif
- **Validation**:
- Vérifie la présence d'au moins un module fournisseur installé
### 3. AI Model (`ai.model`) ### 3. AI Model (`ai.model`)
- **Description**: Modèles d'IA disponibles - **Description**: Modèles d'IA disponibles
- **Champs principaux**: - **Champs principaux**:
- `name`: Nom du modèle - `name`: Nom du modèle
- `identifier`: Identifiant technique - `identifier`: Identifiant technique (ex: gpt-3.5-turbo, mistral-7b)
- `provider_instance_id`: Instance du fournisseur - `provider_instance_id`: Instance du fournisseur (cascade)
- `provider_type`: Type de fournisseur (relié à l'instance)
- `active`: État actif/inactif - `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`) ### 4. AI Generation Parameters (`ai.generation.params`)
- **Description**: Statistiques d'utilisation des modèles - **Description**: Paramètres de génération pour les modèles d'IA
- **Type**: Modèle abstrait
- **Champs principaux**: - **Champs principaux**:
- `model_id`: Modèle associé - `temperature`: Contrôle de l'aléatoire (défaut: 0.7)
- `total_tokens`: Nombre total de tokens - `repeat_penalty`: Pénalité de répétition (défaut: 1.1)
- `total_requests`: Nombre total de requêtes - `max_tokens`: Nombre maximum de tokens (défaut: 2048)
- `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 - `stop_sequences`: Séquences d'arrêt
- `timeout`: Délai d'attente (1 - 300s) - `frequency_penalty`: Pénalité de fréquence (défaut: 0.0)
- `retry_count`: Nombre de tentatives (0 - 5) - `presence_penalty`: Pénalité de présence (défaut: 0.0)
- `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 ## Configuration et Interfaces
### 1. Res Config Settings ### 1. Res Config Settings
- **Description**: Paramètres de configuration globaux - **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**: - **Méthodes principales**:
- `_get_default_provider_instance`: Obtenir l'instance par défaut - `_get_default_provider_instance`: Obtenir l'instance par défaut
## Interfaces ### 3. AI Provider Interface (`ai.provider.interface`)
### AI Provider Interface (`ai.provider.interface`)
- **Description**: Interface abstraite pour les fournisseurs d'IA - **Description**: Interface abstraite pour les fournisseurs d'IA
- **Méthodes requises**: - **Méthodes requises**:
- `send_message`: Envoyer un message - `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 ## Notes d'Implémentation
1. Tous les fournisseurs d'IA doivent implémenter `ai.provider.interface` 1. **Architecture Modulaire**:
2. Les instances de fournisseur héritent des paramètres de génération via `ai.generation.params` - Modules fournisseurs disponibles: `ollama_ai_integration`, `chatgpt_ai_integration`
3. La configuration est hiérarchique : Global > Société > Instance - Vérification de la présence d'au moins un module fournisseur avant création d'instances
4. Les statistiques sont collectées automatiquement pour chaque modèle
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', config_parameter='ai_integration.ai_batch_size',
default=100, default=100,
default_model='ai.provider.instance') 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="name">ai.model.stats.list</field>
<field name="model">ai.model.stats</field> <field name="model">ai.model.stats</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<list string="Model Statistics"> <list string="Model Statistics" create="false">
<field name="date"/> <field name="date"/>
<field name="model_id"/> <field name="model_id"/>
<field name="provider_instance_id"/> <field name="provider_instance_id"/>

View file

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

View file

@ -5,7 +5,7 @@
<field name="name">ai.provider.instance.list</field> <field name="name">ai.provider.instance.list</field>
<field name="model">ai.provider.instance</field> <field name="model">ai.provider.instance</field>
<field name="arch" type="xml"> <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="name"/>
<field name="provider_type"/> <field name="provider_type"/>
<field name="host"/> <field name="host"/>
@ -44,7 +44,7 @@
<group> <group>
<field name="provider_type"/> <field name="provider_type"/>
<field name="host"/> <field name="host"/>
<field name="api_key" password="True"/> <field name="api_key"/>
</group> </group>
<group> <group>
<field name="timeout"/> <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" <menuitem id="menu_ai_settings"
name="Settings" name="Settings"
parent="menu_ai_config" 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" sequence="10"
groups="base.group_system"/> groups="base.group_system"/>
<!-- Provider Instances menu --> <!-- Provider Instances menu -->
<menuitem id="menu_ai_providers" <menuitem id="menu_ai_provider_instances"
name="Provider Instances" name="Provider Instances"
parent="menu_ai_config" parent="menu_ai_config"
action="ai_provider_instance_action" action="ai_provider_instance_action"

View file

@ -1,5 +1,15 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <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"> <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="name">res.config.settings.view.form.inherit.ai.integration</field>
<field name="model">res.config.settings</field> <field name="model">res.config.settings</field>

View file

@ -20,9 +20,9 @@ in your Odoo instance. Features include:
'ai_integration' 'ai_integration'
], ],
'data': [ 'data': [
'data/ollama_provider.xml', 'data/ai_provider_data.xml',
# 'views/ollama_views.xml',
'views/ollama_stats_views.xml', 'views/ollama_stats_views.xml',
'views/ai_provider_instance_views.xml',
'security/ir.model.access.csv', 'security/ir.model.access.csv',
], ],
'external_dependencies': { '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 # Core Implementation
from . import ollama_provider from . import ollama_provider
from . import ai_provider_instance
# Statistics and Monitoring # Statistics and Monitoring
from . import ollama_model_stats from . import ollama_model_stats

View file

@ -1,56 +1,78 @@
from odoo import models, fields, api, _ 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. """Extends the AI Provider Instance model to support Ollama-specific configuration.
This model inherits from both ai.provider.instance and ollama.provider.mixin to: This model inherits from both ai.provider.instance and ollama.provider.mixin to:
1. Add Ollama-specific fields (num_ctx, temperature, etc.) 1. Add Ollama-specific fields (num_ctx, temperature, etc.)
2. Handle field visibility based on provider_type 2. Handle field visibility based on provider_type
3. Manage field cleanup when switching providers 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' _name = 'ai.provider.instance'
_inherit = ['ollama.provider.mixin', 'mail.thread'] _description = 'Ollama AI Provider Instance'
_description = 'AI Provider Instance'
# Basic Fields # Override provider_type to add Ollama option
name = fields.Char( provider_type = fields.Selection(
string='Name', selection_add=[('ollama', 'Ollama')],
required=True, ondelete={'ollama': lambda r: r.write({'provider_type': 'none'})}
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') @api.onchange('provider_type')
def _onchange_provider_type(self): 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 When switching to 'ollama':
type is 'ollama'. When switching to another provider, all Ollama-specific - Set default host if empty
fields are reset to their default values to avoid confusion.
When switching away from 'ollama':
- Clear Ollama-specific fields
""" """
if self.provider_type != 'ollama': 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(
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({ self.update({
'num_ctx': False, # Context length 'num_ctx': False, # Context length
'temperature': False, # Sampling temperature 'temperature': False, # Sampling temperature
@ -75,7 +97,8 @@ class AIProviderInstance(models.Model):
try: try:
# Try to list models as a basic connectivity test # 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 { return {
'type': 'ir.actions.client', 'type': 'ir.actions.client',
'tag': 'display_notification', 'tag': 'display_notification',
@ -104,18 +127,34 @@ class AIProviderInstance(models.Model):
return return
try: try:
provider = self.env['ai.provider.ollama'] # Get models from Ollama API
models = provider._get_models(self) 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: for model_data in models:
# Create or update AI model record # Create or update AI model record
self.env['ai.model'].create_or_update({ vals = {
'name': model_data['name'], 'name': model_data['name'],
'identifier': model_data['id'], 'identifier': model_data['id'],
'provider_instance_id': self.id, 'provider_instance_id': self.id,
'model_type': 'text',
'active': True, '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 { return {
'type': 'ir.actions.client', 'type': 'ir.actions.client',

View file

@ -22,14 +22,6 @@ class OllamaProviderMixin(models.AbstractModel):
_description = 'Ollama Provider Configuration Mixin' _description = 'Ollama Provider Configuration Mixin'
_inherit = ['ai.generation.params'] _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 Parameters
model_name = fields.Char( model_name = fields.Char(
string='Model Name', 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 --> <!-- Menu Item -->
<menuitem id="menu_ollama_model_stats" <menuitem id="menu_ollama_model_stats"
name="Model Statistics" name="Model Statistics"
parent="ai_integration.menu_ai_integration" parent="ai_integration.menu_ai_config"
action="action_ollama_model_stats" action="action_ollama_model_stats"
sequence="30"/> sequence="30"/>
</odoo> </odoo>