updated creation dialog

This commit is contained in:
2025-09-20 10:42:27 +02:00
parent fc9f982c96
commit b1be9afa78
3 changed files with 232 additions and 40 deletions

View File

@@ -55,18 +55,18 @@ class ModelInfoComponent(AsyncCard):
with ui.tab_panel(basic_tab).classes('p-4'): with ui.tab_panel(basic_tab).classes('p-4'):
with ui.scroll_area().classes('w-full').style('height: 400px; max-height: 60vh'): with ui.scroll_area().classes('w-full').style('height: 400px; max-height: 60vh'):
with ui.column().classes('gap-4'): with ui.column().classes('gap-4'):
if 'license' in model_info: if 'system' in model_info:
with ui.row().classes('items-start gap-4'): ui.label('System Prompt:').classes('text-sm font-bold text-white mb-2')
ui.label('License:').classes('text-sm font-bold text-white min-w-24') ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["system"]}</pre>').classes('w-full')
ui.label(model_info['license']).classes('text-sm text-grey-5 flex-1')
if 'template' in model_info: if 'template' in model_info:
ui.label('Template:').classes('text-sm font-bold text-white mb-2') ui.label('Template:').classes('text-sm font-bold text-white mb-2')
ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["template"]}</pre>').classes('w-full') ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["template"]}</pre>').classes('w-full')
if 'system' in model_info: if 'license' in model_info:
ui.label('System Prompt:').classes('text-sm font-bold text-white mb-2') with ui.row().classes('items-start gap-4'):
ui.html(f'<pre class="text-xs text-grey-5 whitespace-pre-wrap font-mono bg-gray-900 p-3 rounded border max-w-full overflow-x-auto">{model_info["system"]}</pre>').classes('w-full') ui.label('License:').classes('text-sm font-bold text-white min-w-24')
ui.label(model_info['license']).classes('text-sm text-grey-5 flex-1')
# Parameters Tab # Parameters Tab
if has_params and params_tab: if has_params and params_tab:

View File

@@ -2,7 +2,7 @@ from nicegui import ui, binding
from niceguiasyncelement import AsyncCard from niceguiasyncelement import AsyncCard
from pathlib import Path from pathlib import Path
from utils import ollama from utils import ollama
from typing import Optional, Dict from typing import Optional
modelfile_example = """FROM qwen2.5-coder:7b modelfile_example = """FROM qwen2.5-coder:7b
PARAMETER num_ctx 8192 PARAMETER num_ctx 8192
@@ -14,14 +14,65 @@ SYSTEM "Du bist ein Python-Experte."
class OllamaModelCreationComponent(AsyncCard): class OllamaModelCreationComponent(AsyncCard):
model_name = binding.BindableProperty() model_name = binding.BindableProperty()
model_from = binding.BindableProperty() model_from = binding.BindableProperty()
parameters = binding.BindableProperty() system_message = binding.BindableProperty()
quantize = binding.BindableProperty() quantize = binding.BindableProperty()
template = binding.BindableProperty()
show_advanced = binding.BindableProperty()
# Parameter toggles
use_temperature = binding.BindableProperty()
use_top_k = binding.BindableProperty()
use_top_p = binding.BindableProperty()
use_min_p = binding.BindableProperty()
use_num_ctx = binding.BindableProperty()
use_num_predict = binding.BindableProperty()
use_repeat_last_n = binding.BindableProperty()
use_repeat_penalty = binding.BindableProperty()
use_seed = binding.BindableProperty()
use_stop = binding.BindableProperty()
# Parameter values
temperature = binding.BindableProperty()
top_k = binding.BindableProperty()
top_p = binding.BindableProperty()
min_p = binding.BindableProperty()
num_ctx = binding.BindableProperty()
num_predict = binding.BindableProperty()
repeat_last_n = binding.BindableProperty()
repeat_penalty = binding.BindableProperty()
seed = binding.BindableProperty()
stop = binding.BindableProperty()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.is_downloading = False self.is_downloading = False
self.download_progress = 0 self.download_progress = 0
self.download_status = '' self.download_status = ''
self.show_advanced = False
# Initialize parameter defaults
self.temperature = 0.8
self.top_k = 40
self.top_p = 0.9
self.min_p = 0.0
self.num_ctx = 4096
self.num_predict = -1
self.repeat_last_n = 64
self.repeat_penalty = 1.1
self.seed = 0
self.stop = ""
# Initialize toggles (all off by default)
self.use_temperature = False
self.use_top_k = False
self.use_top_p = False
self.use_min_p = False
self.use_num_ctx = False
self.use_num_predict = False
self.use_repeat_last_n = False
self.use_repeat_penalty = False
self.use_seed = False
self.use_stop = False
async def build(self) -> None: async def build(self) -> None:
self.classes('w-full') self.classes('w-full')
@@ -29,57 +80,195 @@ class OllamaModelCreationComponent(AsyncCard):
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') ui.label('Create Model').classes('text-xl font-bold')
ui.input('Model Name', value='qwen2.5-coder-32k-python:latest').props('outlined dense').classes('w-full').bind_value(self, 'model_name') # Basic fields
ui.input('From', value='qwen2.5-coder:7b').props('outlined dense').classes('w-full').bind_value(self, 'model_from') ui.input('Model Name', value='my-custom-model:latest').props('outlined dense').classes('w-full').bind_value(self, 'model_name')
ui.textarea(placeholder='Parameters').classes('w-full').props('autogrow').bind_value(self, 'parameters') ui.input('Base Model', value='llama3.2:3b').props('outlined dense').classes('w-full').bind_value(self, 'model_from')
ui.select(['q4_K_M', 'q4_K_S', 'q8_0'], label='quantize', clearable=True).props('outlined dense').classes('w-full').bind_value(self, 'quantize') # 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')
# Parameters section
ui.label('Parameters').classes('text-md font-medium mt-2 mb-3')
# Generation Parameters
with ui.expansion('Generation', icon='tune').classes('w-full mb-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
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_top_k')
ui.label('Top K').classes('min-w-fit')
ui.number(value=40, min=1, max=200).classes('flex-1').bind_value(self, 'top_k').bind_enabled_from(self, 'use_top_k')
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Reduces probability of generating nonsense. Higher values (e.g., 100) give more diverse answers, lower values (e.g., 10) are more conservative. Default: 40')
# Top P
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_top_p')
ui.label('Top P').classes('min-w-fit')
ui.slider(min=0.0, max=1.0, step=0.05).classes('flex-1').bind_value(self, 'top_p').bind_enabled_from(self, 'use_top_p')
ui.label().bind_text_from(self, 'top_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('Works with top-k. Higher values (e.g., 0.95) lead to more diverse text, lower values (e.g., 0.5) generate more focused text. Default: 0.9')
# Min P
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_min_p')
ui.label('Min P').classes('min-w-fit')
ui.slider(min=0.0, max=1.0, step=0.01).classes('flex-1').bind_value(self, 'min_p').bind_enabled_from(self, 'use_min_p')
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')
# 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
with ui.expansion('Repetition Control', icon='repeat').classes('w-full mb-2'):
with ui.column().classes('w-full gap-3 pt-2'):
# Repeat Last N
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_repeat_last_n')
ui.label('Repeat Last N').classes('min-w-fit')
ui.number(value=64, min=-1, max=512).classes('flex-1').bind_value(self, 'repeat_last_n').bind_enabled_from(self, 'use_repeat_last_n')
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('How far back the model looks to prevent repetition. 0=disabled, -1=num_ctx. Default: 64')
# Repeat Penalty
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_repeat_penalty')
ui.label('Repeat Penalty').classes('min-w-fit')
ui.slider(min=0.5, max=2.0, step=0.1).classes('flex-1').bind_value(self, 'repeat_penalty').bind_enabled_from(self, 'use_repeat_penalty')
ui.label().bind_text_from(self, 'repeat_penalty', 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('How strongly to penalize repetitions. Higher values (e.g., 1.5) penalize more, lower values (e.g., 0.9) are more lenient. Default: 1.1')
# Control Parameters
with ui.expansion('Control', icon='settings').classes('w-full mb-2'):
with ui.column().classes('w-full gap-3 pt-2'):
# Seed
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_seed')
ui.label('Seed').classes('min-w-fit')
ui.number(value=0, min=0, max=999999).classes('flex-1').bind_value(self, 'seed').bind_enabled_from(self, 'use_seed')
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Random number seed for generation. Same seed produces same output for same prompt. Default: 0')
# Stop Sequences
with ui.row().classes('items-center gap-3 w-full'):
ui.switch().bind_value(self, 'use_stop')
ui.label('Stop Sequence').classes('min-w-fit')
ui.input(placeholder='AI assistant:').classes('flex-1').bind_value(self, 'stop').bind_enabled_from(self, 'use_stop')
ui.icon('info', size='sm').classes('text-gray-500 cursor-help').tooltip('Text pattern where the model stops generating. Default: none')
# Advanced section (collapsible)
with ui.expansion('Advanced Settings', icon='settings').classes('w-full').bind_value(self, 'show_advanced'):
with ui.column().classes('w-full gap-4 pt-2'):
# Quantization
ui.select(['q4_K_M', 'q4_K_S', 'q8_0'],
label='Quantization', clearable=True).props('outlined dense').classes('w-full').bind_value(self, 'quantize')
# Template field
ui.textarea('Template',
placeholder='{{ if .System }}<|im_start|>system\n{{ .System }}<|im_end|>\n{{ end }}...').classes('w-full').props('autogrow outlined').bind_value(self, 'template')
# Status and progress
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)
self.status_label = ui.label().bind_text_from(self, 'download_status') self.status_label = ui.label().bind_text_from(self, 'download_status')
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')
self.create_btn = ui.button('Create Model', on_click=self.create_model).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_id', backward=lambda x: bool(x) and not self.is_downloading)
# 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)
async def create_model(self): async def create_model(self):
self.parameters = self.parameters.strip() # Validate required fields
model_parameters: Optional[Dict[str, str | int | float]] = None if not self.model_name or not self.model_name.strip():
if self.parameters: ui.notify('Model name is required', type='negative')
model_parameters = {} return
for line in self.parameters.split('\n'):
line = line.strip() if not self.model_from or not self.model_from.strip():
try: ui.notify('Base model is required', type='negative')
key, value = line.split(' ') return
except:
ui.notify(f'Not a valid format. {line}') # Build parameters from toggleable controls
return model_parameters = {}
if key in ['num_ctx', 'repeat_last_n', 'seed', 'num_predict', 'top_k']:
model_parameters[key] = int(value) # Only include parameters that are enabled
elif key in ['repeat_penalty', 'temperature', 'top_p', 'min_p']: if self.use_temperature:
model_parameters[key] = float(value) model_parameters['temperature'] = float(self.temperature)
elif key == 'stop': if self.use_top_k:
model_parameters[key] = value.strip() model_parameters['top_k'] = int(self.top_k)
else: if self.use_top_p:
ui.notify(f'Unknown parameter: {key}') model_parameters['top_p'] = float(self.top_p)
return if self.use_min_p:
model_parameters['min_p'] = float(self.min_p)
if self.use_num_ctx:
model_parameters['num_ctx'] = int(self.num_ctx)
if self.use_num_predict:
model_parameters['num_predict'] = int(self.num_predict)
if self.use_repeat_last_n:
model_parameters['repeat_last_n'] = int(self.repeat_last_n)
if self.use_repeat_penalty:
model_parameters['repeat_penalty'] = float(self.repeat_penalty)
if self.use_seed:
model_parameters['seed'] = int(self.seed)
if self.use_stop and self.stop.strip():
model_parameters['stop'] = self.stop.strip()
# If no parameters are enabled, set to None
if not model_parameters:
model_parameters = None
self.create_btn.set_enabled(False) self.create_btn.set_enabled(False)
self.download_status = 'Preparing...'
self.download_progress = 0
try: try:
async for chunk in ollama.create_ollama_model(self.model_name, self.model_from, model_parameters, self.quantize): # Use the updated create_ollama_model function
async for chunk in ollama.create_ollama_model(
self.model_name.strip(),
self.model_from.strip(),
parameters=model_parameters,
system=self.system_message.strip() if self.system_message else None,
template=self.template.strip() if self.template else None,
quantizie=self.quantize if self.quantize else None
):
if chunk.strip(): if chunk.strip():
# Parse the JSON chunk and extract content # Parse the JSON chunk and extract content
import json import json
try: try:
chunk_data = json.loads(chunk) chunk_data = json.loads(chunk)
self.download_status = chunk_data['status'] self.download_status = chunk_data.get('status', 'Processing...')
if 'total' in chunk_data and 'completed' in chunk_data: if 'total' in chunk_data and 'completed' in chunk_data:
self.download_progress = chunk_data['completed'] / chunk_data['total'] self.download_progress = chunk_data['completed'] / chunk_data['total']
print(self.download_progress)
else: else:
self.download_progress = 0 self.download_progress = 0
except json.JSONDecodeError: except json.JSONDecodeError:
pass # Skip malformed chunks pass # Skip malformed chunks
# Success
self.download_status = 'success'
self.download_progress = 1.0
ui.notify(f'Model "{self.model_name}" created successfully!', type='positive')
except Exception as e: except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative') self.download_status = f'Error: {str(e)}'
ui.notify(f'Error creating model: {str(e)}', type='negative')
finally: finally:
self.create_btn.set_enabled(True) self.create_btn.set_enabled(True)

View File

@@ -32,13 +32,16 @@ async def active_models(url='http://127.0.0.1:11434'):
return response.json()["models"] return response.json()["models"]
async def create_ollama_model(model_name, model_from, parameters=None, quantizie=None, url='http://127.0.0.1:11434'): async def create_ollama_model(model_name, model_from, parameters=None, system=None, template=None, quantizie=None, url='http://127.0.0.1:11434'):
data = { data = {
"model": model_name, "model": model_name,
"from": model_from, "from": model_from,
"stream": True "stream": True
} }
if system:
data['system'] = system
if template:
data['template'] = template
if parameters: if parameters:
data['parameters'] = parameters data['parameters'] = parameters
if quantizie: if quantizie: