Files
ArchGPUFrontend/docs/TOOL_CREATION.md
2025-09-18 22:54:40 +02:00

17 KiB

Tool Creation Guide

This guide walks you through creating custom tools for the ArchGPU Frontend platform. Tools are plugins that extend the application with custom AI testing capabilities.

🎯 Overview

Tools in ArchGPU Frontend are:

  • Auto-discovered from the src/tools/ directory
  • Self-contained with their own routing and pages
  • Context-aware with access to system monitors
  • Easily toggleable via enable/disable properties

🚀 Quick Start

1. Create Tool Structure

mkdir src/tools/my_tool
touch src/tools/my_tool/__init__.py
touch src/tools/my_tool/tool.py

2. Basic Tool Implementation

Create src/tools/my_tool/tool.py:

from typing import Dict, Callable, Awaitable
from nicegui import ui
from tools.base_tool import BaseTool, BasePage

class MyTool(BaseTool):
    @property
    def name(self) -> str:
        return "My Testing Tool"

    @property
    def description(self) -> str:
        return "A custom tool for AI testing"

    @property
    def icon(self) -> str:
        return "science"  # Material Design icon name

    @property
    def enabled(self) -> bool:
        return True  # Set to False to disable

    @property
    def routes(self) -> Dict[str, Callable[[], Awaitable]]:
        return {
            '': lambda: MainPage().create(self),
        }

class MainPage(BasePage):
    async def content(self):
        ui.label(f"Welcome to {self.tool.name}!").classes('text-2xl font-bold text-white')

        # Access system monitors via context
        cpu_usage = self.tool.context.system_monitor.cpu_percent
        ui.label(f"Current CPU usage: {cpu_usage:.1f}%").classes('text-white')

3. Run and Test

Start the application:

APP_PORT=8081 uv run python src/main.py

Your tool will automatically appear in the sidebar under "TOOLS" section!

📚 Detailed Guide

Tool Base Class

Every tool must inherit from BaseTool and implement these required properties:

class MyTool(BaseTool):
    @property
    def name(self) -> str:
        """Display name in the sidebar"""
        return "My Tool"

    @property
    def description(self) -> str:
        """Tool description for documentation"""
        return "What this tool does"

    @property
    def icon(self) -> str:
        """Material Design icon name"""
        return "build"

    @property
    def routes(self) -> Dict[str, Callable[[], Awaitable]]:
        """Route mapping for sub-pages"""
        return {'': lambda: MainPage().create(self)}

Optional properties:

    @property
    def enabled(self) -> bool:
        """Enable/disable tool (default: True)"""
        return True  # or False to disable

Route System

Tools can have multiple pages using sub-routes:

@property
def routes(self):
    return {
        '': lambda: MainPage().create(self),           # /my-tool
        '/settings': lambda: SettingsPage().create(self), # /my-tool/settings
        '/results': lambda: ResultsPage().create(self),   # /my-tool/results
        '/history': lambda: HistoryPage().create(self),   # /my-tool/history
    }

Route naming: Tool directory my_tool becomes route /my-tool (underscores → hyphens)

Page Classes

All pages should inherit from BasePage which provides:

  • Standard layout structure
  • main-content CSS class
  • Access to the tool instance via self.tool
class MainPage(BasePage):
    async def content(self):
        # This method contains your page content
        ui.label("Page title").classes('text-2xl font-bold text-white')

        with ui.card().classes('metric-card p-6'):
            ui.label("Card content").classes('text-white')

Accessing System Data

Use the tool context to access system monitors:

class MainPage(BasePage):
    async def content(self):
        # Access system monitors
        sys_mon = self.tool.context.system_monitor
        gpu_mon = self.tool.context.gpu_monitor
        ollama_mon = self.tool.context.ollama_monitor

        # Display live data
        ui.label().classes('text-white').bind_text_from(
            sys_mon, 'cpu_percent',
            backward=lambda x: f'CPU: {x:.1f}%'
        )

        ui.label().classes('text-white').bind_text_from(
            gpu_mon, 'temperature',
            backward=lambda x: f'GPU: {x:.0f}°C'
        )

        ui.label().classes('text-white').bind_text_from(
            ollama_mon, 'active_models',
            backward=lambda x: f'Models: {len(x)}'
        )

Navigation Between Pages

Create navigation buttons to move between tool pages:

class MainPage(BasePage):
    async def content(self):
        ui.label("Main Page").classes('text-2xl font-bold text-white')

        # Navigation buttons
        with ui.row().classes('gap-2'):
            ui.button('Settings', icon='settings',
                     on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/settings'))
            ui.button('Results', icon='analytics',
                     on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/results'))

class SettingsPage(BasePage):
    async def content(self):
        # Back button
        with ui.row().classes('items-center gap-4 mb-4'):
            ui.button(icon='arrow_back',
                     on_click=lambda: ui.navigate.to(self.tool.baseroute)).props('flat round')
            ui.label("Settings").classes('text-2xl font-bold text-white')

🛠️ Advanced Features

Dynamic Content with Refreshable

Use @ui.refreshable for content that updates periodically:

class MainPage(BasePage):
    async def content(self):
        ui.label("Live Model Status").classes('text-xl font-bold text-white mb-4')

        @ui.refreshable
        def model_status():
            models = self.tool.context.ollama_monitor.active_models
            if not models:
                ui.label("No models running").classes('text-gray-400')
            else:
                for model in models:
                    with ui.row().classes('items-center gap-2'):
                        ui.icon('circle', color='green', size='sm')
                        ui.label(model.get('name', 'Unknown')).classes('text-white')

        model_status()
        ui.timer(2.0, model_status.refresh)  # Update every 2 seconds

Form Handling

Create interactive forms for user input:

class SettingsPage(BasePage):
    async def content(self):
        ui.label("Tool Settings").classes('text-xl font-bold text-white mb-4')

        with ui.card().classes('metric-card p-6'):
            with ui.column().classes('gap-4'):
                # Text input
                prompt_input = ui.input('Custom Prompt').props('outlined')

                # Number input
                batch_size = ui.number('Batch Size', value=10, min=1, max=100).props('outlined')

                # Select dropdown
                model_select = ui.select(
                    options=['gpt-3.5-turbo', 'gpt-4', 'claude-3'],
                    value='gpt-3.5-turbo'
                ).props('outlined')

                # Checkbox
                enable_logging = ui.checkbox('Enable Logging', value=True)

                # Save button
                ui.button('Save Settings', icon='save',
                         on_click=lambda: self.save_settings(
                             prompt_input.value,
                             batch_size.value,
                             model_select.value,
                             enable_logging.value
                         )).props('color=primary')

    def save_settings(self, prompt, batch_size, model, logging):
        # Handle form submission
        ui.notify(f'Settings saved: {prompt}, {batch_size}, {model}, {logging}')

File Operations

Handle file uploads and downloads:

class MainPage(BasePage):
    async def content(self):
        ui.label("File Operations").classes('text-xl font-bold text-white mb-4')

        with ui.card().classes('metric-card p-6'):
            # File upload
            ui.upload(
                label='Upload Test Data',
                on_upload=self.handle_upload,
                max_file_size=10_000_000  # 10MB
            ).props('accept=.txt,.json,.csv')

            # Download button
            ui.button('Download Results', icon='download',
                     on_click=self.download_results)

    def handle_upload(self, e):
        """Handle file upload"""
        with open(f'uploads/{e.name}', 'wb') as f:
            f.write(e.content.read())
        ui.notify(f'Uploaded {e.name}')

    def download_results(self):
        """Generate and download results"""
        content = "Sample results data..."
        ui.download(content.encode(), 'results.txt')

Working with Ollama Models

Interact with Ollama models in your tool:

from utils import ollama

class MainPage(BasePage):
    async def content(self):
        ui.label("Model Testing").classes('text-xl font-bold text-white mb-4')

        with ui.card().classes('metric-card p-6'):
            # Model selection
            models = await ollama.available_models()
            model_names = [m['name'] for m in models]
            selected_model = ui.select(model_names, label='Select Model').props('outlined')

            # Prompt input
            prompt_input = ui.textarea('Enter prompt').props('outlined')

            # Test button
            ui.button('Test Model', icon='play_arrow',
                     on_click=lambda: self.test_model(selected_model.value, prompt_input.value))

            # Results display
            self.results_area = ui.html()

    async def test_model(self, model_name, prompt):
        """Test a model with the given prompt"""
        if not model_name or not prompt:
            ui.notify('Please select a model and enter a prompt', type='warning')
            return

        try:
            # Call Ollama API
            response = await ollama.generate(model_name, prompt)
            self.results_area.content = f'<pre class="text-white">{response}</pre>'
        except Exception as e:
            ui.notify(f'Error: {str(e)}', type='negative')

🎨 Styling Guidelines

CSS Classes

Use these standard classes for consistent styling:

# Text styles
ui.label("Title").classes('text-2xl font-bold text-white')
ui.label("Subtitle").classes('text-lg font-bold text-white')
ui.label("Body text").classes('text-sm text-white')
ui.label("Muted text").classes('text-xs text-grey-5')

# Cards and containers
ui.card().classes('metric-card p-6')
ui.row().classes('items-center gap-4')
ui.column().classes('gap-4')

# Buttons
ui.button('Primary').props('color=primary')
ui.button('Secondary').props('color=secondary')
ui.button('Icon', icon='icon_name').props('round flat')

Color Scheme

The application uses a dark theme with these accent colors:

  • Primary: Cyan (#06b6d4)
  • Success: Green (#10b981)
  • Warning: Orange (#f97316)
  • Error: Red (#ef4444)
  • Purple: (#e879f9)

🔧 Tool Configuration

Environment-Based Enabling

Enable tools based on environment variables:

import os

class MyTool(BaseTool):
    @property
    def enabled(self) -> bool:
        return os.getenv('ENABLE_EXPERIMENTAL_TOOLS', 'false').lower() == 'true'

Configuration Files

Store tool-specific configuration:

import json
import os

class MyTool(BaseTool):
    def __init__(self):
        self.config_file = 'config/my_tool.json'
        self.config = self.load_config()

    def load_config(self):
        if os.path.exists(self.config_file):
            with open(self.config_file, 'r') as f:
                return json.load(f)
        return {'enabled': True, 'max_batch_size': 100}

    def save_config(self):
        os.makedirs(os.path.dirname(self.config_file), exist_ok=True)
        with open(self.config_file, 'w') as f:
            json.dump(self.config, f, indent=2)

🐛 Debugging

Logging

Add logging to your tools:

import logging

logger = logging.getLogger(__name__)

class MainPage(BasePage):
    async def content(self):
        logger.info("MainPage loaded")

        try:
            # Your code here
            pass
        except Exception as e:
            logger.error(f"Error in MainPage: {e}")
            ui.notify(f"Error: {str(e)}", type='negative')

Development Tips

  1. Use port 8081 for development to avoid conflicts
  2. Check browser console for JavaScript errors
  3. Monitor server logs for Python exceptions
  4. Use ui.notify() for user feedback
  5. Test with different screen sizes for responsiveness

📖 Examples

Complete Tool Example

Here's a complete example of a model comparison tool:

from typing import Dict, Callable, Awaitable
from nicegui import ui
from tools.base_tool import BaseTool, BasePage
from utils import ollama
import asyncio

class ModelCompareTool(BaseTool):
    @property
    def name(self) -> str:
        return "Model Compare"

    @property
    def description(self) -> str:
        return "Compare responses from different AI models"

    @property
    def icon(self) -> str:
        return "compare"

    @property
    def routes(self) -> Dict[str, Callable[[], Awaitable]]:
        return {
            '': lambda: ComparePage().create(self),
            '/history': lambda: HistoryPage().create(self),
        }

class ComparePage(BasePage):
    async def content(self):
        ui.label("Model Comparison").classes('text-2xl font-bold text-white mb-4')

        # Get available models
        models = await ollama.available_models()
        model_names = [m['name'] for m in models]

        with ui.row().classes('w-full gap-4'):
            # Left panel - inputs
            with ui.card().classes('metric-card p-6 flex-1'):
                ui.label("Setup").classes('text-lg font-bold text-white mb-4')

                self.model1 = ui.select(model_names, label='Model 1').props('outlined')
                self.model2 = ui.select(model_names, label='Model 2').props('outlined')
                self.prompt = ui.textarea('Prompt', placeholder='Enter your prompt here...').props('outlined')

                ui.button('Compare Models', icon='play_arrow',
                         on_click=self.compare_models).props('color=primary')

            # Right panel - results
            with ui.card().classes('metric-card p-6 flex-1'):
                ui.label("Results").classes('text-lg font-bold text-white mb-4')
                self.results_container = ui.column().classes('gap-4')

    async def compare_models(self):
        if not all([self.model1.value, self.model2.value, self.prompt.value]):
            ui.notify('Please fill all fields', type='warning')
            return

        self.results_container.clear()

        with self.results_container:
            ui.label("Comparing models...").classes('text-white')

            # Run both models concurrently
            tasks = [
                ollama.generate(self.model1.value, self.prompt.value),
                ollama.generate(self.model2.value, self.prompt.value)
            ]

            try:
                results = await asyncio.gather(*tasks)

                # Display results side by side
                with ui.row().classes('w-full gap-4'):
                    for i, (model_name, result) in enumerate(zip([self.model1.value, self.model2.value], results)):
                        with ui.card().classes('metric-card p-4 flex-1'):
                            ui.label(model_name).classes('text-lg font-bold text-white mb-2')
                            ui.html(f'<pre class="text-white text-sm">{result}</pre>')

            except Exception as e:
                ui.notify(f'Error: {str(e)}', type='negative')

class HistoryPage(BasePage):
    async def content(self):
        with ui.row().classes('items-center gap-4 mb-4'):
            ui.button(icon='arrow_back',
                     on_click=lambda: ui.navigate.to(self.tool.baseroute)).props('flat round')
            ui.label("Comparison History").classes('text-2xl font-bold text-white')

        # History implementation here
        ui.label("History feature coming soon...").classes('text-grey-5')

🚀 Publishing Your Tool

Code Quality

  • Follow Python PEP 8 style guidelines
  • Add type hints to your methods
  • Include docstrings for complex functions
  • Handle errors gracefully with try/catch blocks

Testing

  • Test your tool with different models
  • Verify responsive design on different screen sizes
  • Test enable/disable functionality
  • Ensure proper cleanup of resources

Documentation

  • Add comments to complex logic
  • Create usage examples
  • Document any configuration options
  • Include screenshots if helpful

Your tool is now ready to be shared with the ArchGPU Frontend community!


Happy tool building! 🛠️