# 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 ```bash 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`: ```python 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: ```bash 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: ```python 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: ```python @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: ```python @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` ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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: ```python 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'
{response}'
except Exception as e:
ui.notify(f'Error: {str(e)}', type='negative')
```
## 🎨 Styling Guidelines
### CSS Classes
Use these standard classes for consistent styling:
```python
# 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:
```python
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:
```python
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:
```python
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:
```python
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'{result}')
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! 🛠️**