stuff
This commit is contained in:
236
CLAUDE.md
236
CLAUDE.md
@@ -3,10 +3,28 @@
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Context
|
||||
This is a NiceGUI-based web frontend for managing an Arch Linux system with AMD GPU and Ollama. The application serves as:
|
||||
1. System resource monitor (CPU, GPU, memory usage)
|
||||
2. Ollama model manager (list, download, delete models via Ollama API on port 11434)
|
||||
3. Platform for additional system tools
|
||||
This is a NiceGUI-based web platform for testing and managing AI models through Ollama on Arch Linux systems with GPU support. The application serves as an AI model testing environment featuring:
|
||||
|
||||
### Core Purpose
|
||||
A streamlined interface for testing AI models locally, managing Ollama models, and running various AI-related testing tools.
|
||||
|
||||
### Main Features:
|
||||
1. **Model Manager** - Complete Ollama model management interface
|
||||
- Download, delete, create, and test models
|
||||
- Support for Hugging Face models via Ollama pull syntax
|
||||
- Rich model metadata display
|
||||
- Quick in-app chat testing
|
||||
|
||||
2. **System Monitoring** - Resource tracking for AI workloads
|
||||
- Real-time GPU monitoring (AMD/NVIDIA) to track model performance
|
||||
- CPU and memory usage during model inference
|
||||
- System metrics dashboard
|
||||
|
||||
3. **AI Testing Tools**:
|
||||
- **Censor** - Text content filtering/censoring tool for testing AI outputs
|
||||
- Additional testing tools to be added as needed
|
||||
|
||||
4. **Settings** - Application configuration and refresh intervals
|
||||
|
||||
## Development Commands
|
||||
|
||||
@@ -41,36 +59,212 @@ uv sync
|
||||
- **UI Framework**: NiceGUI (async web framework based on FastAPI/Vue.js)
|
||||
- **Python Version**: 3.13+
|
||||
- **Ollama API**: Running on localhost:11434
|
||||
- **Dependencies**:
|
||||
- `nicegui` - Main UI framework
|
||||
- `niceguiasyncelement` - Custom async component framework (from git)
|
||||
- `psutil` - System monitoring
|
||||
- `httpx` - Async HTTP client for Ollama API
|
||||
- `python-dotenv` - Environment configuration
|
||||
|
||||
### Project Structure
|
||||
- `src/main.py`: Entry point, configures NiceGUI app with environment variables
|
||||
- `src/pages/`: Page components inheriting from NiceGUI elements
|
||||
- `src/components/`: Reusable UI components (currently empty, ready for implementation)
|
||||
- `src/static/`: Static assets (CSS, images)
|
||||
- `.env`: Configuration variables (APP_TITLE, APP_PORT, APP_STORAGE_SECRET, APP_SHOW)
|
||||
```
|
||||
src/
|
||||
├── main.py # Entry point, NiceGUI app configuration with all routes
|
||||
├── pages/ # Page components (inheriting NiceGUI elements)
|
||||
│ ├── dashboard.py # Main dashboard with system metrics
|
||||
│ ├── ollama_manager.py # Ollama model management interface (AsyncColumn)
|
||||
│ ├── system_overview.py # System information page
|
||||
│ └── welcome.py # Welcome/landing page
|
||||
├── components/ # Reusable UI components
|
||||
│ ├── circular_progress.py # Circular progress indicators
|
||||
│ ├── header.py # App header with live status
|
||||
│ ├── sidebar.py # Navigation sidebar with updated menu structure
|
||||
│ ├── bottom_nav.py # Mobile bottom navigation
|
||||
│ ├── ollama_downloader.py # Ollama model downloader component (AsyncCard)
|
||||
│ ├── ollama_model_creation.py # Model creation component (AsyncCard)
|
||||
│ └── ollama_quick_test.py # Model testing component (AsyncCard)
|
||||
├── utils/ # Utility modules
|
||||
│ ├── gpu_monitor.py # GPU monitoring (AMD/NVIDIA auto-detect)
|
||||
│ ├── system_monitor.py # System resource monitoring
|
||||
│ ├── ollama_monitor.py # Ollama status monitoring (bindable dataclass)
|
||||
│ └── ollama.py # Ollama API client functions
|
||||
└── static/ # Static assets (CSS, images)
|
||||
└── style.css # Custom dark theme styles
|
||||
```
|
||||
|
||||
### Key Design Patterns
|
||||
1. **Page Classes**: Pages are implemented as classes inheriting from NiceGUI elements (e.g., `WelcomePage` extends `ui.column`)
|
||||
2. **Environment Configuration**: All app settings are managed via `.env` file and loaded with python-dotenv
|
||||
3. **Async Support**: Main page handlers use async functions for potential async operations
|
||||
1. **Async Components**: Uses custom `niceguiasyncelement` framework for async page/component construction
|
||||
- `AsyncColumn`, `AsyncCard` base classes for complex components
|
||||
- `OllamaManagerPage(AsyncColumn)` for full page async initialization
|
||||
- Async component dialogs with `await component.create()` pattern
|
||||
2. **Bindable Dataclasses**: Monitor classes use `@binding.bindable_dataclass` for reactive data binding
|
||||
- `SystemMonitor`, `GPUMonitor`, `OllamaMonitor` for real-time data updates
|
||||
3. **Environment Configuration**: All app settings are managed via `.env` file and loaded with python-dotenv
|
||||
4. **Centralized Routing**: All routes defined in main.py with layout creation pattern
|
||||
5. **Real-time Updates**: Timer-based updates every 2 seconds for all monitor instances
|
||||
|
||||
## Component Architecture
|
||||
|
||||
### Monitor Classes (Supporting AI Testing)
|
||||
- **SystemMonitor**: Tracks system resources during AI model testing
|
||||
- CPU usage during model inference
|
||||
- Memory consumption by loaded models
|
||||
- Disk I/O for model loading
|
||||
- Process statistics for Ollama and GPU processes
|
||||
|
||||
- **GPUMonitor**: Critical for AI workload monitoring
|
||||
- Auto-detects AMD/NVIDIA GPUs
|
||||
- Tracks GPU usage during model inference
|
||||
- Memory usage by loaded models
|
||||
- Temperature monitoring during extended testing
|
||||
- Power draw under AI workloads
|
||||
|
||||
- **OllamaMonitor**: Core service monitoring
|
||||
- Ollama service status and version
|
||||
- Currently loaded/active models
|
||||
- Real-time model state tracking
|
||||
|
||||
### UI Components
|
||||
- **MetricCircle**: Small circular progress indicator with icon
|
||||
- **LargeMetricCircle**: Large circular progress for primary metrics
|
||||
- **ColorfulMetricCard**: Action cards with gradient backgrounds
|
||||
- **Sidebar**: Navigation menu with updated structure:
|
||||
- Main: Dashboard, System Overview
|
||||
- Tools: Censor (content filtering)
|
||||
- Bottom: Model Manager, Settings
|
||||
- **Header**: Top bar with system status indicators
|
||||
|
||||
### Ollama-Specific Components (AsyncCard-based):
|
||||
- **OllamaDownloaderComponent**: Model downloading with progress tracking (supports HF models via Ollama's pull syntax)
|
||||
- **OllamaModelCreationComponent**: Custom model creation from Modelfile
|
||||
- **ModelQuickTestComponent**: Interactive model testing interface
|
||||
|
||||
## Ollama Integration
|
||||
The Ollama API is available at `http://localhost:11434/api/`. Key endpoints:
|
||||
The Ollama API client (`src/utils/ollama.py`) provides async functions:
|
||||
- `status()`: Check if Ollama is online and get version
|
||||
- `available_models()`: List installed models with detailed metadata
|
||||
- `active_models()`: Get currently loaded/running models
|
||||
- `delete_model()`: Remove a model
|
||||
- `model_info()`: Get detailed model information and Modelfile
|
||||
- `stream_chat()`: Stream chat responses
|
||||
|
||||
### AI Model Testing Features:
|
||||
- **Model Discovery & Management**:
|
||||
- Browse and pull models from Ollama library
|
||||
- Support for HuggingFace models via Ollama syntax
|
||||
- Rich metadata display (size, quantization, parameters, format)
|
||||
- Time tracking for model versions
|
||||
|
||||
- **Testing Capabilities**:
|
||||
- Quick chat interface for immediate model testing
|
||||
- Model information and Modelfile inspection
|
||||
- Custom model creation from Modelfiles
|
||||
- Real-time resource monitoring during inference
|
||||
|
||||
- **Testing Tools**:
|
||||
- Censor tool for output filtering analysis
|
||||
- Extensible framework for adding new testing tools
|
||||
|
||||
API endpoints at `http://localhost:11434/api/`:
|
||||
- `/api/version`: Get Ollama version
|
||||
- `/api/tags`: List available models
|
||||
- `/api/pull`: Download models
|
||||
- `/api/delete`: Remove models
|
||||
- `/api/generate`: Generate text
|
||||
- `/api/chat`: Chat completion
|
||||
- `/api/ps`: List running models
|
||||
- `/api/show`: Show model details
|
||||
|
||||
## System Monitoring
|
||||
For AMD GPU monitoring on Arch Linux:
|
||||
- Use `rocm-smi` for GPU stats (temperature, usage, memory)
|
||||
- Use `psutil` for CPU and memory monitoring
|
||||
- Consider `py3nvml` or direct sysfs reading for additional GPU metrics
|
||||
|
||||
### GPU Monitoring Strategy
|
||||
The application uses a hierarchical approach for GPU monitoring:
|
||||
|
||||
1. **NVIDIA GPUs** (via `nvidia-smi`):
|
||||
- Temperature, usage, memory, power draw
|
||||
- CUDA version and driver info
|
||||
- Multi-GPU support
|
||||
|
||||
2. **AMD GPUs** (multiple fallbacks):
|
||||
- Primary: `rocm-smi` for full metrics
|
||||
- Fallback: `/sys/class/drm` filesystem
|
||||
- Reads hwmon for temperature data
|
||||
- Supports both server and consumer GPUs
|
||||
|
||||
### CPU & System Monitoring
|
||||
- Real-time CPU usage and per-core statistics
|
||||
- Memory (RAM and swap) usage
|
||||
- Disk usage and I/O statistics
|
||||
- Network traffic monitoring
|
||||
- Process tracking with top processes by CPU/memory
|
||||
- System uptime and kernel information
|
||||
|
||||
## UI/UX Features
|
||||
|
||||
### Dark Theme
|
||||
Custom dark theme with:
|
||||
- Background: `#1a1d2e` (main), `#252837` (sidebar)
|
||||
- Card backgrounds: `rgba(26, 29, 46, 0.7)` with backdrop blur
|
||||
- Accent colors: Cyan (`#06b6d4`) for primary actions
|
||||
- Metric colors: Purple (CPU), Green (Memory), Orange (GPU), Cyan (Temp)
|
||||
|
||||
### Responsive Design
|
||||
- Desktop: Full sidebar navigation
|
||||
- Mobile: Bottom navigation bar
|
||||
- Adaptive grid layouts for different screen sizes
|
||||
- Viewport-aware content scaling
|
||||
|
||||
### Real-time Updates
|
||||
- System metrics update every 2 seconds (configurable via `MONITORING_UPDATE_INTERVAL`)
|
||||
- Live data binding for all metrics
|
||||
- Smooth transitions and animations
|
||||
|
||||
## NiceGUI Patterns
|
||||
- Use `ui.dark_mode()` for theme toggling
|
||||
- Implement pages as classes extending NiceGUI elements for better organization
|
||||
- Use `ui.timer()` for periodic updates (system stats)
|
||||
- Leverage `ui.refreshable` decorator for dynamic content updates
|
||||
- **Data Binding**: Use `bind_text_from()` and `bind_value_from()` for reactive updates
|
||||
- **Page Routing**: Navigation via `ui.navigate.to(route)` with centralized route handling
|
||||
- **Async Components**: Custom `niceguiasyncelement` framework for complex async initialization
|
||||
- `AsyncColumn.create()` for async page construction
|
||||
- `AsyncCard.create()` for dialog components
|
||||
- `@ui.refreshable` decorators for dynamic content updates
|
||||
- **Timer Updates**: `app.timer()` for periodic data refresh (2-second intervals)
|
||||
- **Dialog Patterns**: Modal dialogs with `await dialog` for user interactions
|
||||
- **Component Layout**: `create_layout(route)` pattern for consistent page structure
|
||||
- **Dark Mode**: Forced dark mode with custom CSS overrides
|
||||
|
||||
## Environment Variables
|
||||
Configured in `.env`:
|
||||
- `MONITORING_UPDATE_INTERVAL`: Update frequency in seconds (default: 2)
|
||||
- `APP_PORT`: Web server port (default: 8080, use 8081 for testing)
|
||||
- `APP_TITLE`: Application title
|
||||
- `APP_STORAGE_SECRET`: Session storage encryption key
|
||||
- `APP_SHOW`: Auto-open browser on startup
|
||||
|
||||
## Testing & Development
|
||||
- Run on port 8081 to avoid conflicts: `APP_PORT=8081 uv run python src/main.py`
|
||||
- Monitor GPU detection in console logs
|
||||
- Check Ollama connectivity at startup
|
||||
- Use browser DevTools for WebSocket debugging
|
||||
|
||||
## Current Route Structure
|
||||
From main.py routing:
|
||||
- `/` - Dashboard (system metrics for monitoring AI workloads)
|
||||
- `/system` - System Overview (detailed resource information)
|
||||
- `/ollama` - Model Manager (primary interface for AI model testing)
|
||||
- `/censor` - Censor tool (AI output filtering/testing)
|
||||
- `/settings` - Settings (refresh intervals, app configuration)
|
||||
|
||||
### Placeholder Routes (may be repurposed for AI tools):
|
||||
- `/processes` - Reserved for future AI tools
|
||||
- `/network` - Reserved for future AI tools
|
||||
- `/packages` - Reserved for future AI tools
|
||||
- `/logs` - Reserved for future AI tools
|
||||
- `/info` - Reserved for future AI tools
|
||||
|
||||
## Future Enhancements
|
||||
- Enhanced model chat interface with conversation history
|
||||
- Model performance benchmarking tools
|
||||
- Batch testing capabilities for multiple models
|
||||
- Output comparison tools between different models
|
||||
- Integration with more AI model formats
|
||||
- Advanced prompt testing and optimization tools
|
||||
- Model fine-tuning interface
|
||||
@@ -1,4 +1,29 @@
|
||||
from nicegui import ui
|
||||
from utils import SystemMonitor, GPUMonitor
|
||||
from typing import Optional, Literal
|
||||
|
||||
|
||||
class MetricCircleAdv:
|
||||
def __init__(self, label: str, monitor: SystemMonitor | GPUMonitor,
|
||||
target_value: str,
|
||||
target_max_value: str,
|
||||
color: str,
|
||||
formatting: Literal['percent', 'units', 'degree'],
|
||||
icon: Optional[str] = None):
|
||||
with ui.card().classes('metric-card p-4 text-center'):
|
||||
with ui.column().classes('items-center gap-2'):
|
||||
# Icon at top
|
||||
with ui.row().classes('items-center gap-1'):
|
||||
if icon:
|
||||
ui.icon(icon, size='sm', color=color)
|
||||
|
||||
# Title
|
||||
ui.label(label).classes('text-sm text-grey-5 font-medium')
|
||||
|
||||
# Circular progress - simplified
|
||||
with ui.circular_progress(size='60px', color=color, show_value=False).bind_value_from(monitor, target_value):
|
||||
if formatting == 'percent':
|
||||
ui.label().classes('text-lg font-bold text-white').bind_text_from(monitor, target_value, backward=lambda x: f"{int(x * 100)} %")
|
||||
|
||||
|
||||
class MetricCircle:
|
||||
@@ -46,4 +71,4 @@ class ColorfulMetricCard:
|
||||
with ui.card().classes(f'p-4 text-center animate-fade-in').style(f'background: linear-gradient(135deg, {color}20 0%, {color}10 100%); border: 1px solid {color}40'):
|
||||
with ui.column().classes('items-center gap-2'):
|
||||
ui.icon(icon, size='xl').style(f'color: {color}')
|
||||
ui.label(title).classes('text-sm font-medium text-white')
|
||||
ui.label(title).classes('text-sm font-medium text-white')
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
from nicegui import ui
|
||||
from utils import SystemMonitor, GPUMonitor
|
||||
from nicegui import ui, binding
|
||||
from utils import SystemMonitor, GPUMonitor, OllamaMonitor
|
||||
|
||||
|
||||
class Header(ui.header):
|
||||
def __init__(self, system_monitor: SystemMonitor, gpu_monitor: GPUMonitor):
|
||||
def __init__(self, system_monitor: SystemMonitor, gpu_monitor: GPUMonitor, ollama_monitor: OllamaMonitor):
|
||||
super().__init__(fixed=True, elevated=False)
|
||||
|
||||
with self.classes('bg-transparent'):
|
||||
with ui.row().classes('w-full items-center justify-between px-6 py-3'):
|
||||
with ui.row().classes('w-full items-center justify-between px-6'):
|
||||
# Left side - minimal branding
|
||||
with ui.row().classes('items-center gap-3'):
|
||||
ui.label('ArchGPU Frontend').classes('text-xl font-bold text-white')
|
||||
ui.chip('Live', icon='circle', color='green').props('size=sm outline')
|
||||
chip = ui.chip('Live', icon='circle').props('size=sm outline')
|
||||
chip.bind_text_from(ollama_monitor, 'status', backward=lambda x: self.update_ollama_running_chip(chip, x))
|
||||
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
print(ollama_monitor.active_models)
|
||||
|
||||
ui.label().bind_text_from(ollama_monitor, 'active_models', backward=lambda x: self.update_active_models(x))
|
||||
|
||||
# Right side - system status only
|
||||
with ui.row().classes('items-center gap-4'):
|
||||
@@ -31,3 +37,14 @@ class Header(ui.header):
|
||||
ui.icon('thermostat', size='sm', color='red')
|
||||
ui.label().classes('text-sm text-white').bind_text_from(gpu_monitor, 'temperature',
|
||||
lambda x: f'{x:.1f}°C')
|
||||
|
||||
def update_ollama_running_chip(self, obj: ui.chip, state):
|
||||
obj.classes(remove='text-red' if state else 'text-green')
|
||||
obj.classes(add='text-green' if state else 'text-red')
|
||||
return 'Ollama Running' if state else 'Ollama stopped'
|
||||
|
||||
def update_active_models(self, active_models):
|
||||
used_vram = 0
|
||||
for active_model in active_models:
|
||||
used_vram += active_model['size_vram']
|
||||
return f'{len(active_models)} Active Models using {(used_vram / 1024**3):.2f} GB'
|
||||
|
||||
57
src/components/ollama_downloader.py
Normal file
57
src/components/ollama_downloader.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from nicegui import ui
|
||||
from niceguiasyncelement import AsyncCard
|
||||
from pathlib import Path
|
||||
from utils import ollama
|
||||
|
||||
|
||||
class OllamaDownloaderComponent(AsyncCard):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_downloading = False
|
||||
self.download_progress = 0
|
||||
self.download_status = ''
|
||||
|
||||
async def build(self) -> None:
|
||||
with self:
|
||||
with ui.column().classes('w-full gap-4'):
|
||||
ui.label('Model Downloader').classes('text-xl font-bold')
|
||||
|
||||
model_input = ui.input(
|
||||
'Model ID',
|
||||
placeholder='e.g., TheBloke/Llama-2-7B-GGUF',
|
||||
value='qwen2.5:0.5b'
|
||||
).props('outlined dense').classes('w-full')
|
||||
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'):
|
||||
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')
|
||||
ui.linear_progress(value=0, show_value=False).props('buffer=0.0 animation-speed=0').bind_value_from(self, 'download_progress')
|
||||
|
||||
self.download_btn = ui.button(
|
||||
'Download Model',
|
||||
on_click=lambda m=model_input: self.download_model(m.value) # type: ignore
|
||||
).props('color=primary').classes('w-full').bind_enabled_from(self, 'model_id', backward=lambda x: bool(x) and not self.is_downloading)
|
||||
|
||||
async def download_model(self, model):
|
||||
self.download_btn.set_enabled(False)
|
||||
try:
|
||||
async for chunk in ollama.download_model(model):
|
||||
if chunk.strip():
|
||||
# Parse the JSON chunk and extract content
|
||||
import json
|
||||
try:
|
||||
chunk_data = json.loads(chunk)
|
||||
self.download_status = chunk_data['status']
|
||||
if 'total' in chunk_data and 'completed' in chunk_data:
|
||||
self.download_progress = chunk_data['completed'] / chunk_data['total']
|
||||
print(self.download_progress)
|
||||
else:
|
||||
self.download_progress = 0
|
||||
except json.JSONDecodeError:
|
||||
pass # Skip malformed chunks
|
||||
except Exception as e:
|
||||
ui.notify(f'Error: {str(e)}', type='negative')
|
||||
finally:
|
||||
self.download_btn.set_enabled(True)
|
||||
85
src/components/ollama_model_creation.py
Normal file
85
src/components/ollama_model_creation.py
Normal file
@@ -0,0 +1,85 @@
|
||||
from nicegui import ui, binding
|
||||
from niceguiasyncelement import AsyncCard
|
||||
from pathlib import Path
|
||||
from utils import ollama
|
||||
from typing import Optional, Dict
|
||||
|
||||
modelfile_example = """FROM qwen2.5-coder:7b
|
||||
PARAMETER num_ctx 8192
|
||||
PARAMETER temperature 0.1
|
||||
SYSTEM "Du bist ein Python-Experte."
|
||||
"""
|
||||
|
||||
|
||||
class OllamaModelCreationComponent(AsyncCard):
|
||||
model_name = binding.BindableProperty()
|
||||
model_from = binding.BindableProperty()
|
||||
parameters = binding.BindableProperty()
|
||||
quantize = binding.BindableProperty()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.is_downloading = False
|
||||
self.download_progress = 0
|
||||
self.download_status = ''
|
||||
|
||||
async def build(self) -> None:
|
||||
self.classes('w-full')
|
||||
with self:
|
||||
with ui.column().classes('w-full gap-4'):
|
||||
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')
|
||||
ui.input('From', value='qwen2.5-coder:7b').props('outlined dense').classes('w-full').bind_value(self, 'model_from')
|
||||
ui.textarea(placeholder='Parameters').classes('w-full').props('autogrow').bind_value(self, 'parameters')
|
||||
|
||||
ui.select(['q4_K_M', 'q4_K_S', 'q8_0'], label='quantize', clearable=True).props('outlined dense').classes('w-full').bind_value(self, 'quantize')
|
||||
|
||||
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)
|
||||
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')
|
||||
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)
|
||||
|
||||
async def create_model(self):
|
||||
self.parameters = self.parameters.strip()
|
||||
model_parameters: Optional[Dict[str, str | int | float]] = None
|
||||
if self.parameters:
|
||||
model_parameters = {}
|
||||
for line in self.parameters.split('\n'):
|
||||
line = line.strip()
|
||||
try:
|
||||
key, value = line.split(' ')
|
||||
except:
|
||||
ui.notify(f'Not a valid format. {line}')
|
||||
return
|
||||
if key in ['num_ctx', 'repeat_last_n', 'seed', 'num_predict', 'top_k']:
|
||||
model_parameters[key] = int(value)
|
||||
elif key in ['repeat_penalty', 'temperature', 'top_p', 'min_p']:
|
||||
model_parameters[key] = float(value)
|
||||
elif key == 'stop':
|
||||
model_parameters[key] = value.strip()
|
||||
else:
|
||||
ui.notify(f'Unknown parameter: {key}')
|
||||
return
|
||||
|
||||
self.create_btn.set_enabled(False)
|
||||
try:
|
||||
async for chunk in ollama.create_ollama_model(self.model_name, self.model_from, model_parameters, self.quantize):
|
||||
if chunk.strip():
|
||||
# Parse the JSON chunk and extract content
|
||||
import json
|
||||
try:
|
||||
chunk_data = json.loads(chunk)
|
||||
self.download_status = chunk_data['status']
|
||||
if 'total' in chunk_data and 'completed' in chunk_data:
|
||||
self.download_progress = chunk_data['completed'] / chunk_data['total']
|
||||
print(self.download_progress)
|
||||
else:
|
||||
self.download_progress = 0
|
||||
except json.JSONDecodeError:
|
||||
pass # Skip malformed chunks
|
||||
except Exception as e:
|
||||
ui.notify(f'Error: {str(e)}', type='negative')
|
||||
finally:
|
||||
self.create_btn.set_enabled(True)
|
||||
65
src/components/ollama_quick_test.py
Normal file
65
src/components/ollama_quick_test.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from nicegui import ui
|
||||
from niceguiasyncelement import AsyncCard
|
||||
from pathlib import Path
|
||||
from utils import ollama
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ModelQuickTestComponent(AsyncCard):
|
||||
model: Optional[str]
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.model = None
|
||||
|
||||
async def build(self, model: str) -> None:
|
||||
self.model = model
|
||||
with self:
|
||||
with ui.card().classes('w-full'):
|
||||
ui.label(f'Quick Chat Test with {model}').classes('text-h6 font-bold mb-4')
|
||||
|
||||
self.quick_test_textarea = ui.textarea(
|
||||
label='Prompt',
|
||||
placeholder='Enter your prompt here...',
|
||||
value='Hello! Tell me a fun fact about AMD GPUs.'
|
||||
).classes('w-full').props('autogrow outlined')
|
||||
|
||||
self.quick_test_send = ui.button('Send', icon='send', on_click=self._quick_test).props('color=primary')
|
||||
|
||||
with ui.row():
|
||||
ui.icon('message', size='sm')
|
||||
ui.label('Response')
|
||||
self.quick_test_response = ui.label('Response will appear here...').classes('text-grey-7')
|
||||
|
||||
async def _quick_test(self):
|
||||
if not self.model:
|
||||
ui.notify('Select a model first.', type='warning')
|
||||
return
|
||||
|
||||
self.quick_test_response.set_text('')
|
||||
prompt = self.quick_test_textarea.value
|
||||
|
||||
# calling stream_ollama_chat
|
||||
data = {
|
||||
"model": self.model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": True
|
||||
}
|
||||
self.quick_test_send.set_enabled(False)
|
||||
try:
|
||||
async for chunk in ollama.stream_chat(data):
|
||||
if chunk.strip():
|
||||
# Parse the JSON chunk and extract content
|
||||
import json
|
||||
try:
|
||||
chunk_data = json.loads(chunk)
|
||||
if 'message' in chunk_data and 'content' in chunk_data['message']:
|
||||
content = chunk_data['message']['content']
|
||||
current_text = self.quick_test_response.text
|
||||
self.quick_test_response.set_text(current_text + content)
|
||||
except json.JSONDecodeError:
|
||||
pass # Skip malformed chunks
|
||||
except Exception as e:
|
||||
ui.notify(f'Error: {str(e)}', type='negative')
|
||||
finally:
|
||||
self.quick_test_send.set_enabled(True)
|
||||
@@ -8,29 +8,17 @@ class Sidebar:
|
||||
|
||||
with ui.column().classes('w-full h-full p-4'):
|
||||
|
||||
# Navigation sections
|
||||
ui.label('MAIN').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Dashboard', 'dashboard', '/', active=(current_route == '/'))
|
||||
self._nav_item('System Overview', 'monitor', '/system', active=(current_route == '/system'))
|
||||
|
||||
ui.label('MANAGEMENT').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Ollama Manager', 'smart_toy', '/ollama', active=(current_route == '/ollama'))
|
||||
self._nav_item('Process Manager', 'terminal', '/processes', active=(current_route == '/processes'))
|
||||
self._nav_item('Network Monitor', 'router', '/network', active=(current_route == '/network'))
|
||||
self._nav_item('Package Manager', 'inventory_2', '/packages', active=(current_route == '/packages'))
|
||||
|
||||
ui.label('TOOLS').classes('text-xs text-grey-5 font-bold tracking-wide mb-2')
|
||||
|
||||
with ui.column().classes('gap-1 mb-6'):
|
||||
self._nav_item('Log Viewer', 'description', '/logs', active=(current_route == '/logs'))
|
||||
self._nav_item('System Info', 'info', '/info', active=(current_route == '/info'))
|
||||
self._nav_item('Censor', 'description', '/censor', active=(current_route == '/censor'))
|
||||
|
||||
ui.space()
|
||||
|
||||
self._nav_item('Model Manager', 'view_in_ar', '/ollama', active=(current_route == '/ollama'))
|
||||
# Bottom section
|
||||
ui.separator().classes('my-4')
|
||||
self._nav_item('Settings', 'settings', '/settings', active=(current_route == '/settings'))
|
||||
|
||||
@@ -5,7 +5,7 @@ from nicegui import ui, app
|
||||
|
||||
from components import Header, Sidebar
|
||||
from pages import DashboardPage, OllamaManagerPage
|
||||
from utils import GPUMonitor, SystemMonitor
|
||||
from utils import GPUMonitor, SystemMonitor, OllamaMonitor
|
||||
import logging
|
||||
|
||||
logging.basicConfig(
|
||||
@@ -22,9 +22,11 @@ app.add_static_files('/static', 'src/static')
|
||||
# Create monitor instances (bindable dataclasses)
|
||||
system_monitor = SystemMonitor()
|
||||
gpu_monitor = GPUMonitor()
|
||||
ollama_monitor = OllamaMonitor()
|
||||
|
||||
app.timer(2.0, system_monitor.update)
|
||||
app.timer(2.0, gpu_monitor.update)
|
||||
app.timer(2.0, ollama_monitor.update)
|
||||
|
||||
|
||||
def create_layout(current_route='/'):
|
||||
@@ -35,7 +37,7 @@ def create_layout(current_route='/'):
|
||||
# Add custom CSS
|
||||
ui.add_head_html('<link rel="stylesheet" type="text/css" href="/static/style.css">')
|
||||
|
||||
Header(system_monitor, gpu_monitor)
|
||||
Header(system_monitor, gpu_monitor, ollama_monitor)
|
||||
Sidebar(current_route)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from typing import Literal
|
||||
from nicegui import ui
|
||||
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard
|
||||
from components.circular_progress import MetricCircle, LargeMetricCircle, ColorfulMetricCard, MetricCircleAdv
|
||||
from utils import SystemMonitor, GPUMonitor
|
||||
|
||||
"""
|
||||
@@ -58,6 +58,8 @@ class DashboardPage(ui.column):
|
||||
# Main content area with proper viewport handling
|
||||
with self:
|
||||
with ui.column().classes('w-full max-w-6xl mx-auto p-6 gap-6'):
|
||||
with ui.grid(columns=4).classes('w-full gap-4'):
|
||||
MetricCircleAdv('CPU', system_monitor, 'cpu_percent', '', icon='memory', formatting='percent', color='#e879f9')
|
||||
# Top stats grid
|
||||
with ui.grid(columns=4).classes('w-full gap-4'):
|
||||
# CPU metric with binding
|
||||
|
||||
@@ -3,6 +3,9 @@ from utils import ollama
|
||||
from typing import Literal, List, Dict
|
||||
from pprint import pprint
|
||||
from niceguiasyncelement import AsyncColumn
|
||||
from components.ollama_downloader import OllamaDownloaderComponent
|
||||
from components.ollama_model_creation import OllamaModelCreationComponent
|
||||
from components.ollama_quick_test import ModelQuickTestComponent
|
||||
|
||||
|
||||
class OllamaManagerPage(AsyncColumn):
|
||||
@@ -44,53 +47,32 @@ class OllamaManagerPage(AsyncColumn):
|
||||
with ui.row().classes('w-full items-center mb-4'):
|
||||
ui.label('Installed Models').classes('text-h6 font-bold')
|
||||
ui.space()
|
||||
ui.button('Create New Model', icon='create', on_click=self._create_model).props('color=primary')
|
||||
ui.button('Pull New Model', icon='download').props('color=primary')
|
||||
ui.button('Create New Model', icon='create', on_click=self._create_model_dialog).props('color=primary')
|
||||
ui.button('Pull New Model', icon='download', on_click=self._download_model_dialog).props('color=primary')
|
||||
|
||||
with ui.column().classes('w-full gap-2'):
|
||||
await self.models_container() # type: ignore
|
||||
|
||||
# Quick test
|
||||
with ui.card().classes('w-full'):
|
||||
ui.label('Quick Chat Test').classes('text-h6 font-bold mb-4')
|
||||
async def _create_model_dialog(self):
|
||||
|
||||
with ui.row().classes('w-full gap-2 mb-2'):
|
||||
self.quick_test_select = ui.select(
|
||||
[],
|
||||
label='Model'
|
||||
).classes('flex-grow').props('outlined')
|
||||
with ui.dialog() as dialog:
|
||||
await OllamaModelCreationComponent.create()
|
||||
await dialog
|
||||
self.models_container.refresh()
|
||||
|
||||
self.quick_test_textarea = ui.textarea(
|
||||
label='Prompt',
|
||||
placeholder='Enter your prompt here...',
|
||||
value='Hello! Tell me a fun fact about AMD GPUs.'
|
||||
).classes('w-full').props('outlined')
|
||||
async def _download_model_dialog(self):
|
||||
with ui.dialog() as dialog:
|
||||
await OllamaDownloaderComponent.create()
|
||||
await dialog
|
||||
self.models_container.refresh()
|
||||
|
||||
self.quick_test_send = ui.button('Send', icon='send', on_click=self._quick_test).props('color=primary')
|
||||
|
||||
with ui.row():
|
||||
ui.icon('message', size='sm')
|
||||
ui.label('Response')
|
||||
self.quick_test_response = ui.label('Response will appear here...').classes('text-grey-7')
|
||||
await self._quick_test_populate_options()
|
||||
|
||||
async def _create_model(self):
|
||||
modelfile = """FROM qwen2.5-coder:7b
|
||||
PARAMETER num_ctx 32768
|
||||
PARAMETER temperature 0.1
|
||||
SYSTEM "Du bist ein Python-Experte."
|
||||
"""
|
||||
print('creating model')
|
||||
result = await ollama.create_ollama_model(
|
||||
"qwen2.5-coder-32k-python",
|
||||
modelfile
|
||||
)
|
||||
print('finished.')
|
||||
print(result)
|
||||
await self.models_container.refresh()
|
||||
async def _test_model_dialog(self, model):
|
||||
with ui.dialog() as dialog:
|
||||
await ModelQuickTestComponent.create(model)
|
||||
await dialog
|
||||
|
||||
async def _loaded_models(self):
|
||||
loaded = await ollama.loaded_models()
|
||||
loaded = await ollama.active_models()
|
||||
print(loaded)
|
||||
|
||||
async def _delete_model(self, model):
|
||||
@@ -114,8 +96,6 @@ class OllamaManagerPage(AsyncColumn):
|
||||
|
||||
for model in self.models:
|
||||
self._create_model_item(model)
|
||||
if hasattr(self, 'quick_test_select'):
|
||||
await self._quick_test_populate_options()
|
||||
|
||||
def _create_model_item(self, model: Dict):
|
||||
with ui.card().classes('w-full'):
|
||||
@@ -169,48 +149,14 @@ class OllamaManagerPage(AsyncColumn):
|
||||
ui.space()
|
||||
|
||||
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='play_arrow').props('round flat color=primary').tooltip('Run Model')
|
||||
ui.button(icon='info', on_click=lambda m=model['name']: self._print_model_info(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')
|
||||
|
||||
async def _print_model_info(self, model_name):
|
||||
result = await ollama.model_info(model_name)
|
||||
print(result)
|
||||
for key, value in result.items():
|
||||
print(key)
|
||||
|
||||
async def _quick_test_populate_options(self):
|
||||
select_options = [model['name'] for model in self.models]
|
||||
self.quick_test_select.set_options(select_options)
|
||||
|
||||
async def _quick_test(self):
|
||||
model = self.quick_test_select.value
|
||||
if not model:
|
||||
ui.notify('Select a model first.', type='warning')
|
||||
return
|
||||
|
||||
self.quick_test_response.set_text('')
|
||||
prompt = self.quick_test_textarea.value
|
||||
|
||||
# calling stream_ollama_chat
|
||||
data = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"stream": True
|
||||
}
|
||||
self.quick_test_send.set_enabled(False)
|
||||
try:
|
||||
async for chunk in ollama.stream_chat(data):
|
||||
if chunk.strip():
|
||||
# Parse the JSON chunk and extract content
|
||||
import json
|
||||
try:
|
||||
chunk_data = json.loads(chunk)
|
||||
if 'message' in chunk_data and 'content' in chunk_data['message']:
|
||||
content = chunk_data['message']['content']
|
||||
current_text = self.quick_test_response.text
|
||||
self.quick_test_response.set_text(current_text + content)
|
||||
except json.JSONDecodeError:
|
||||
pass # Skip malformed chunks
|
||||
except Exception as e:
|
||||
ui.notify(f'Error: {str(e)}', type='negative')
|
||||
finally:
|
||||
self.quick_test_send.set_enabled(True)
|
||||
print(result['modelfile'])
|
||||
|
||||
@@ -168,7 +168,7 @@ body,
|
||||
}
|
||||
|
||||
.main-content {
|
||||
height: calc(100vh - 64px) !important;
|
||||
height: calc(100vh - 0px) !important;
|
||||
width: calc(100vw - 256px) !important;
|
||||
overflow-y: auto !important;
|
||||
margin-left: 256px !important;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from .system_monitor import SystemMonitor
|
||||
from .gpu_monitor import GPUMonitor
|
||||
from .ollama_monitor import OllamaMonitor
|
||||
|
||||
__all__ = ['SystemMonitor', 'GPUMonitor']
|
||||
__all__ = ['SystemMonitor', 'GPUMonitor', 'OllamaMonitor']
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import httpx
|
||||
from nicegui import ui
|
||||
from typing import Tuple
|
||||
from typing import Tuple, Dict
|
||||
|
||||
|
||||
async def status(url='http://127.0.0.1:11434') -> Tuple[bool, str]:
|
||||
@@ -24,27 +24,30 @@ async def available_models(url='http://127.0.0.1:11434'):
|
||||
return response.json()["models"]
|
||||
|
||||
|
||||
async def loaded_models(url='http://127.0.0.1:11434'):
|
||||
async def active_models(url='http://127.0.0.1:11434'):
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"{url}/api/ps")
|
||||
response.raise_for_status()
|
||||
|
||||
return response.json()
|
||||
return response.json()["models"]
|
||||
|
||||
|
||||
async def create_ollama_model(name, modelfile_content, url='http://127.0.0.1:11434'):
|
||||
async def create_ollama_model(model_name, model_from, parameters=None, quantizie=None, url='http://127.0.0.1:11434'):
|
||||
data = {
|
||||
"name": name,
|
||||
"from": "qwen2.5-coder:7b",
|
||||
"modelfile": modelfile_content,
|
||||
"stream": False
|
||||
"model": model_name,
|
||||
"from": model_from,
|
||||
"stream": True
|
||||
}
|
||||
|
||||
if parameters:
|
||||
data['parameters'] = parameters
|
||||
if quantizie:
|
||||
data['quantizie'] = quantizie
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.post(f"{url}/api/create", json=data)
|
||||
response.raise_for_status()
|
||||
print(response.text)
|
||||
return response.json()
|
||||
async with client.stream('POST', f"{url}/api/create", json=data) as response:
|
||||
async for chunk in response.aiter_text():
|
||||
yield chunk
|
||||
|
||||
|
||||
async def delete_model(name, url='http://127.0.0.1:11434') -> bool:
|
||||
@@ -79,3 +82,11 @@ async def stream_chat(data, url='http://127.0.0.1:11434'):
|
||||
async with client.stream('POST', f"{url}/api/chat", json=data) as response:
|
||||
async for chunk in response.aiter_text():
|
||||
yield chunk
|
||||
|
||||
|
||||
async def download_model(model, url='http://127.0.0.1:11434'):
|
||||
data = {'name': model, 'stream': True}
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with client.stream('POST', f"{url}/api/pull", json=data) as response:
|
||||
async for chunk in response.aiter_text():
|
||||
yield chunk
|
||||
|
||||
21
src/utils/ollama_monitor.py
Normal file
21
src/utils/ollama_monitor.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import psutil
|
||||
import platform
|
||||
import time
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, Any, List, Optional, Literal
|
||||
from nicegui import binding
|
||||
from utils import ollama
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@binding.bindable_dataclass
|
||||
class OllamaMonitor:
|
||||
status: bool = False
|
||||
version: str = 'Unknown'
|
||||
active_models: Dict = field(default_factory=dict)
|
||||
|
||||
async def update(self):
|
||||
self.status, self.version = await ollama.status()
|
||||
self.active_models = await ollama.active_models() if self.status else {}
|
||||
Reference in New Issue
Block a user