improved model management
This commit is contained in:
@@ -12,6 +12,7 @@ class OllamaDownloaderComponent(AsyncCard):
|
|||||||
self.download_status = ''
|
self.download_status = ''
|
||||||
|
|
||||||
async def build(self) -> None:
|
async def build(self) -> None:
|
||||||
|
self.classes('w-full')
|
||||||
with self:
|
with self:
|
||||||
with ui.column().classes('w-full gap-4'):
|
with ui.column().classes('w-full gap-4'):
|
||||||
ui.label('Model Downloader').classes('text-xl font-bold')
|
ui.label('Model Downloader').classes('text-xl font-bold')
|
||||||
@@ -21,8 +22,9 @@ class OllamaDownloaderComponent(AsyncCard):
|
|||||||
placeholder='e.g., TheBloke/Llama-2-7B-GGUF',
|
placeholder='e.g., TheBloke/Llama-2-7B-GGUF',
|
||||||
value='qwen2.5:0.5b'
|
value='qwen2.5:0.5b'
|
||||||
).props('outlined dense').classes('w-full')
|
).props('outlined dense').classes('w-full')
|
||||||
ui.link('Ollama Library', target='https://ollama.com/library/', new_tab=True)
|
with ui.row().classes('items-center gap-2'):
|
||||||
ui.link('Using HF Models', target='https://huggingface.co/docs/hub/en/ollama', new_tab=True)
|
ui.link('Ollama Library', target='https://ollama.com/library/', new_tab=True)
|
||||||
|
ui.link('Using HF Models', target='https://huggingface.co/docs/hub/en/ollama', new_tab=True)
|
||||||
|
|
||||||
with ui.row().classes('items-center gap-2'):
|
with ui.row().classes('items-center gap-2'):
|
||||||
ui.icon('check_circle').props(f'color=positive').bind_visibility_from(self, 'download_status', backward=lambda x: True if x == 'success' else False)
|
ui.icon('check_circle').props(f'color=positive').bind_visibility_from(self, 'download_status', backward=lambda x: True if x == 'success' else False)
|
||||||
|
|||||||
@@ -4,12 +4,6 @@ from pathlib import Path
|
|||||||
from utils import ollama
|
from utils import ollama
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
modelfile_example = """FROM qwen2.5-coder:7b
|
|
||||||
PARAMETER num_ctx 8192
|
|
||||||
PARAMETER temperature 0.1
|
|
||||||
SYSTEM "Du bist ein Python-Experte."
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class OllamaModelCreationComponent(AsyncCard):
|
class OllamaModelCreationComponent(AsyncCard):
|
||||||
model_name = binding.BindableProperty()
|
model_name = binding.BindableProperty()
|
||||||
@@ -74,33 +68,59 @@ class OllamaModelCreationComponent(AsyncCard):
|
|||||||
self.use_seed = False
|
self.use_seed = False
|
||||||
self.use_stop = False
|
self.use_stop = False
|
||||||
|
|
||||||
async def build(self) -> None:
|
async def build(self, existing_model_name: Optional[str] = None) -> None:
|
||||||
self.classes('w-full')
|
self.classes('w-full')
|
||||||
|
|
||||||
|
# Load existing model data if provided
|
||||||
|
if existing_model_name:
|
||||||
|
await self.load_existing_model(existing_model_name)
|
||||||
|
|
||||||
with self:
|
with self:
|
||||||
with ui.column().classes('w-full gap-4'):
|
with ui.column().classes('w-full gap-4'):
|
||||||
ui.label('Create Model').classes('text-xl font-bold')
|
title = 'Edit Model' if existing_model_name else 'Create Model'
|
||||||
|
ui.label(title).classes('text-xl font-bold')
|
||||||
|
|
||||||
# Basic fields
|
# Basic fields
|
||||||
ui.input('Model Name', value='my-custom-model:latest').props('outlined dense').classes('w-full').bind_value(self, 'model_name')
|
model_name_default = existing_model_name if existing_model_name else 'my-custom-model:latest'
|
||||||
ui.input('Base Model', value='llama3.2:3b').props('outlined dense').classes('w-full').bind_value(self, 'model_from')
|
base_model_default = '' if existing_model_name else 'llama3.2:3b'
|
||||||
|
|
||||||
|
ui.input('Model Name', value=model_name_default).props('outlined dense').classes('w-full').bind_value(self, 'model_name')
|
||||||
|
ui.input('Base Model', value=base_model_default).props('outlined dense').classes('w-full').bind_value(self, 'model_from')
|
||||||
|
|
||||||
# System message field (commonly used)
|
# System message field (commonly used)
|
||||||
ui.textarea('System Message', placeholder='You are a helpful assistant...').classes('w-full').props('autogrow outlined').bind_value(self, 'system_message')
|
ui.textarea('System Message', placeholder='You are a helpful assistant...').classes('w-full').props('autogrow outlined').bind_value(self, 'system_message')
|
||||||
|
|
||||||
# Parameters section
|
# Common Parameters section
|
||||||
ui.label('Parameters').classes('text-md font-medium mt-2 mb-3')
|
ui.label('Common Parameters').classes('text-md font-medium mt-2 mb-3')
|
||||||
|
|
||||||
|
# Temperature (always visible)
|
||||||
|
with ui.row().classes('items-center gap-3 w-full mb-3'):
|
||||||
|
ui.switch().bind_value(self, 'use_temperature')
|
||||||
|
ui.label('Temperature').classes('min-w-fit')
|
||||||
|
ui.slider(min=0.0, max=2.0, step=0.1).classes('flex-1').bind_value(self, 'temperature').bind_enabled_from(self, 'use_temperature')
|
||||||
|
ui.label().bind_text_from(self, 'temperature', backward=lambda x: f'{x:.1f}').classes('text-xs text-gray-500 min-w-fit')
|
||||||
|
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('The temperature of the model. Higher values (e.g., 1.2) make output more creative, lower values (e.g., 0.5) more focused. Default: 0.8')
|
||||||
|
|
||||||
|
# Context Length (always visible)
|
||||||
|
with ui.row().classes('items-center gap-3 w-full mb-3'):
|
||||||
|
ui.switch().bind_value(self, 'use_num_ctx')
|
||||||
|
ui.label('Context Length').classes('min-w-fit')
|
||||||
|
ui.number(value=4096, min=1, max=32768).classes('flex-1').bind_value(self, 'num_ctx').bind_enabled_from(self, 'use_num_ctx')
|
||||||
|
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Size of the context window used to generate the next token. Default: 4096')
|
||||||
|
|
||||||
|
# Max Tokens (always visible)
|
||||||
|
with ui.row().classes('items-center gap-3 w-full mb-4'):
|
||||||
|
ui.switch().bind_value(self, 'use_num_predict')
|
||||||
|
ui.label('Max Tokens').classes('min-w-fit')
|
||||||
|
ui.number(value=-1, min=-1, max=4096).classes('flex-1').bind_value(self, 'num_predict').bind_enabled_from(self, 'use_num_predict')
|
||||||
|
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Maximum number of tokens to predict. -1 for infinite generation. Default: -1')
|
||||||
|
|
||||||
|
# Advanced Parameters section
|
||||||
|
ui.label('Advanced Parameters').classes('text-md font-medium mt-2 mb-3')
|
||||||
|
|
||||||
# Generation Parameters
|
# Generation Parameters
|
||||||
with ui.expansion('Generation', icon='tune').classes('w-full mb-2'):
|
with ui.expansion('Generation', icon='tune').classes('w-full mb-2'):
|
||||||
with ui.column().classes('w-full gap-3 pt-2'):
|
with ui.column().classes('w-full gap-3 pt-2'):
|
||||||
# Temperature
|
|
||||||
with ui.row().classes('items-center gap-3 w-full'):
|
|
||||||
ui.switch().bind_value(self, 'use_temperature')
|
|
||||||
ui.label('Temperature').classes('min-w-fit')
|
|
||||||
ui.slider(min=0.0, max=2.0, step=0.1).classes('flex-1').bind_value(self, 'temperature').bind_enabled_from(self, 'use_temperature')
|
|
||||||
ui.label().bind_text_from(self, 'temperature', backward=lambda x: f'{x:.1f}').classes('text-xs text-gray-500 min-w-fit')
|
|
||||||
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('The temperature of the model. Higher values (e.g., 1.2) make output more creative, lower values (e.g., 0.5) more focused. Default: 0.8')
|
|
||||||
|
|
||||||
# Top K
|
# Top K
|
||||||
with ui.row().classes('items-center gap-3 w-full'):
|
with ui.row().classes('items-center gap-3 w-full'):
|
||||||
ui.switch().bind_value(self, 'use_top_k')
|
ui.switch().bind_value(self, 'use_top_k')
|
||||||
@@ -124,23 +144,6 @@ class OllamaModelCreationComponent(AsyncCard):
|
|||||||
ui.label().bind_text_from(self, 'min_p', backward=lambda x: f'{x:.2f}').classes('text-xs text-gray-500 min-w-fit')
|
ui.label().bind_text_from(self, 'min_p', backward=lambda x: f'{x:.2f}').classes('text-xs text-gray-500 min-w-fit')
|
||||||
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Alternative to top_p. Minimum probability for a token relative to the most likely token. Default: 0.0')
|
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Alternative to top_p. Minimum probability for a token relative to the most likely token. Default: 0.0')
|
||||||
|
|
||||||
# Context Parameters
|
|
||||||
with ui.expansion('Context', icon='memory').classes('w-full mb-2'):
|
|
||||||
with ui.column().classes('w-full gap-3 pt-2'):
|
|
||||||
# Context Length
|
|
||||||
with ui.row().classes('items-center gap-3 w-full'):
|
|
||||||
ui.switch().bind_value(self, 'use_num_ctx')
|
|
||||||
ui.label('Context Length').classes('min-w-fit')
|
|
||||||
ui.number(value=4096, min=1, max=32768).classes('flex-1').bind_value(self, 'num_ctx').bind_enabled_from(self, 'use_num_ctx')
|
|
||||||
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Size of the context window used to generate the next token. Default: 4096')
|
|
||||||
|
|
||||||
# Max Tokens
|
|
||||||
with ui.row().classes('items-center gap-3 w-full'):
|
|
||||||
ui.switch().bind_value(self, 'use_num_predict')
|
|
||||||
ui.label('Max Tokens').classes('min-w-fit')
|
|
||||||
ui.number(value=-1, min=-1, max=4096).classes('flex-1').bind_value(self, 'num_predict').bind_enabled_from(self, 'use_num_predict')
|
|
||||||
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Maximum number of tokens to predict. -1 for infinite generation. Default: -1')
|
|
||||||
|
|
||||||
# Repetition Parameters
|
# Repetition Parameters
|
||||||
with ui.expansion('Repetition Control', icon='repeat').classes('w-full mb-2'):
|
with ui.expansion('Repetition Control', icon='repeat').classes('w-full mb-2'):
|
||||||
with ui.column().classes('w-full gap-3 pt-2'):
|
with ui.column().classes('w-full gap-3 pt-2'):
|
||||||
@@ -194,7 +197,82 @@ class OllamaModelCreationComponent(AsyncCard):
|
|||||||
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
|
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
|
||||||
|
|
||||||
# Create button
|
# Create button
|
||||||
self.create_btn = ui.button('Create Model', icon='add', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_name', backward=lambda x: bool(x) and not self.is_downloading)
|
button_text = 'Update Model' if existing_model_name else 'Create Model'
|
||||||
|
self.create_btn = ui.button(button_text, icon='add', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_name', backward=lambda x: bool(x) and not self.is_downloading)
|
||||||
|
|
||||||
|
async def load_existing_model(self, model_name):
|
||||||
|
"""Load existing model data and populate form fields"""
|
||||||
|
try:
|
||||||
|
model_info = await ollama.model_info(model_name)
|
||||||
|
modelfile = model_info.get('modelfile', '')
|
||||||
|
|
||||||
|
# Parse the modelfile content to extract settings
|
||||||
|
for line in modelfile.split('\n'):
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if line.startswith('FROM '):
|
||||||
|
self.model_from = line[5:].strip()
|
||||||
|
elif line.startswith('SYSTEM '):
|
||||||
|
# Extract system message (remove quotes)
|
||||||
|
system_msg = line[7:].strip()
|
||||||
|
if system_msg.startswith('"') and system_msg.endswith('"'):
|
||||||
|
system_msg = system_msg[1:-1]
|
||||||
|
self.system_message = system_msg
|
||||||
|
elif line.startswith('TEMPLATE '):
|
||||||
|
# Extract template (remove quotes)
|
||||||
|
template = line[9:].strip()
|
||||||
|
if template.startswith('"') and template.endswith('"'):
|
||||||
|
template = template[1:-1]
|
||||||
|
self.template = template
|
||||||
|
elif line.startswith('PARAMETER '):
|
||||||
|
# Parse parameter lines
|
||||||
|
param_line = line[10:].strip()
|
||||||
|
try:
|
||||||
|
key, value = param_line.split(' ', 1)
|
||||||
|
|
||||||
|
# Set parameter values and enable toggles
|
||||||
|
if key == 'temperature':
|
||||||
|
self.temperature = float(value)
|
||||||
|
self.use_temperature = True
|
||||||
|
elif key == 'top_k':
|
||||||
|
self.top_k = int(value)
|
||||||
|
self.use_top_k = True
|
||||||
|
elif key == 'top_p':
|
||||||
|
self.top_p = float(value)
|
||||||
|
self.use_top_p = True
|
||||||
|
elif key == 'min_p':
|
||||||
|
self.min_p = float(value)
|
||||||
|
self.use_min_p = True
|
||||||
|
elif key == 'num_ctx':
|
||||||
|
self.num_ctx = int(value)
|
||||||
|
self.use_num_ctx = True
|
||||||
|
elif key == 'num_predict':
|
||||||
|
self.num_predict = int(value)
|
||||||
|
self.use_num_predict = True
|
||||||
|
elif key == 'repeat_last_n':
|
||||||
|
self.repeat_last_n = int(value)
|
||||||
|
self.use_repeat_last_n = True
|
||||||
|
elif key == 'repeat_penalty':
|
||||||
|
self.repeat_penalty = float(value)
|
||||||
|
self.use_repeat_penalty = True
|
||||||
|
elif key == 'seed':
|
||||||
|
self.seed = int(value)
|
||||||
|
self.use_seed = True
|
||||||
|
elif key == 'stop':
|
||||||
|
# Remove quotes from stop sequence
|
||||||
|
stop_value = value.strip()
|
||||||
|
if stop_value.startswith('"') and stop_value.endswith('"'):
|
||||||
|
stop_value = stop_value[1:-1]
|
||||||
|
self.stop = stop_value
|
||||||
|
self.use_stop = True
|
||||||
|
except ValueError:
|
||||||
|
# Skip invalid parameter lines
|
||||||
|
continue
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
ui.notify(f'Error loading model info: {str(e)}', type='negative')
|
||||||
|
|
||||||
async def create_model(self):
|
async def create_model(self):
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from nicegui import ui
|
from nicegui import ui
|
||||||
from utils import ollama
|
from utils import ollama
|
||||||
from typing import Literal, List, Dict
|
from typing import Literal, List, Dict, Optional
|
||||||
from pprint import pprint
|
from pprint import pprint
|
||||||
from niceguiasyncelement import AsyncColumn
|
from niceguiasyncelement import AsyncColumn
|
||||||
from components.ollama_downloader import OllamaDownloaderComponent
|
from components.ollama_downloader import OllamaDownloaderComponent
|
||||||
@@ -62,7 +62,7 @@ class OllamaManagerPage(AsyncColumn):
|
|||||||
self.models_container.refresh()
|
self.models_container.refresh()
|
||||||
|
|
||||||
async def _download_model_dialog(self):
|
async def _download_model_dialog(self):
|
||||||
with ui.dialog() as dialog:
|
with ui.dialog().classes('max-w-none') as dialog:
|
||||||
await OllamaDownloaderComponent.create()
|
await OllamaDownloaderComponent.create()
|
||||||
await dialog
|
await dialog
|
||||||
self.models_container.refresh()
|
self.models_container.refresh()
|
||||||
@@ -96,14 +96,26 @@ class OllamaManagerPage(AsyncColumn):
|
|||||||
# self.quick_test_select.set_options(select_options)
|
# self.quick_test_select.set_options(select_options)
|
||||||
|
|
||||||
for model in self.models:
|
for model in self.models:
|
||||||
self._create_model_item(model)
|
await self._create_model_item(model)
|
||||||
|
|
||||||
def _create_model_item(self, model: Dict):
|
async def _create_model_item(self, model: Dict):
|
||||||
|
model_info = await ollama.model_info(model['name'])
|
||||||
|
base_model: Optional[Literal['ollama', 'huggingface']] = None
|
||||||
|
if len(model_info['details']['parent_model']) == 0:
|
||||||
|
if model['name'].startswith('hf.co/'):
|
||||||
|
base_model = 'huggingface'
|
||||||
|
else:
|
||||||
|
base_model = 'ollama'
|
||||||
with ui.card().classes('w-full'):
|
with ui.card().classes('w-full'):
|
||||||
with ui.row().classes('w-full items-center'):
|
with ui.row().classes('w-full items-center'):
|
||||||
with ui.column().classes('flex-grow gap-1'):
|
with ui.column().classes('flex-grow gap-1'):
|
||||||
# Model name
|
# Model name
|
||||||
ui.label(model['name']).classes('font-bold text-h6')
|
with ui.row().classes('items-center'):
|
||||||
|
ui.label(model['name']).classes('font-bold text-h6')
|
||||||
|
if base_model == 'ollama':
|
||||||
|
ui.html(f'<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/ollama-dark.svg" style="width: 20px; height: 20px; object-fit: contain;" />')
|
||||||
|
if base_model == 'huggingface':
|
||||||
|
ui.html(f'<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/hugging-face.svg" style="width: 20px; height: 20px; object-fit: contain;" />')
|
||||||
|
|
||||||
# Details row with chips
|
# Details row with chips
|
||||||
with ui.row().classes('gap-2 flex-wrap'):
|
with ui.row().classes('gap-2 flex-wrap'):
|
||||||
@@ -151,10 +163,16 @@ class OllamaManagerPage(AsyncColumn):
|
|||||||
|
|
||||||
with ui.row().classes('gap-2'):
|
with ui.row().classes('gap-2'):
|
||||||
ui.button(icon='chat', on_click=lambda m=model['name']: self._test_model_dialog(m)).props('round flat').tooltip('Test Model')
|
ui.button(icon='chat', on_click=lambda m=model['name']: self._test_model_dialog(m)).props('round flat').tooltip('Test Model')
|
||||||
ui.button(icon='play_arrow').props('round flat color=primary').tooltip('Run Model')
|
ui.button(icon='edit', on_click=lambda m=model['name']: self._model_edit_model_dialog(m)).props('round flat').tooltip('Model Info')
|
||||||
ui.button(icon='info', on_click=lambda m=model['name']: self._model_information_dialog(m)).props('round flat').tooltip('Model Info')
|
ui.button(icon='info', on_click=lambda m=model['name']: self._model_information_dialog(m)).props('round flat').tooltip('Model Info')
|
||||||
ui.button(icon='delete', on_click=lambda m=model['name']: self._delete_model(m)).props('round flat color=negative').tooltip('Delete Model')
|
ui.button(icon='delete', on_click=lambda m=model['name']: self._delete_model(m)).props('round flat color=negative').tooltip('Delete Model')
|
||||||
|
|
||||||
|
async def _model_edit_model_dialog(self, model_name):
|
||||||
|
with ui.dialog() as dialog:
|
||||||
|
await OllamaModelCreationComponent.create(model_name)
|
||||||
|
await dialog
|
||||||
|
self.models_container.refresh()
|
||||||
|
|
||||||
async def _print_model_info(self, model_name):
|
async def _print_model_info(self, model_name):
|
||||||
result = await ollama.model_info(model_name)
|
result = await ollama.model_info(model_name)
|
||||||
for key, value in result.items():
|
for key, value in result.items():
|
||||||
|
|||||||
Reference in New Issue
Block a user