tooling and docs

This commit is contained in:
2025-09-18 22:54:40 +02:00
parent d07eb7dfd4
commit ca3ebcf02a
13 changed files with 1522 additions and 411 deletions

36
src/tools/__init__.py Normal file
View File

@@ -0,0 +1,36 @@
import os
import importlib
from typing import List, Dict
from .base_tool import BaseTool
def discover_tools() -> Dict[str, BaseTool]:
"""Auto-discover and load all tools"""
tools = {}
tools_dir = os.path.dirname(__file__)
for item in os.listdir(tools_dir):
tool_path = os.path.join(tools_dir, item)
if os.path.isdir(tool_path) and not item.startswith('_'):
try:
# Import the tool module
module = importlib.import_module(f'tools.{item}.tool')
# Find Tool class (should be named like CensorTool, etc.)
for attr_name in dir(module):
attr = getattr(module, attr_name)
if (isinstance(attr, type) and
issubclass(attr, BaseTool) and
attr != BaseTool):
tool_instance = attr()
# Only register enabled tools
if tool_instance.enabled:
tools[tool_instance.baseroute] = tool_instance
break
except ImportError as e:
print(f"Failed to load tool {item}: {e}")
return tools
TOOLS = discover_tools()

105
src/tools/base_tool.py Normal file
View File

@@ -0,0 +1,105 @@
from abc import ABC, abstractmethod
from typing import Dict, Callable, Awaitable, Optional
from nicegui import ui
from niceguiasyncelement import AsyncColumn
import inspect
class ToolContext:
"""Global context providing access to system monitors and shared resources"""
def __init__(self, system_monitor=None, gpu_monitor=None, ollama_monitor=None):
self.system_monitor = system_monitor
self.gpu_monitor = gpu_monitor
self.ollama_monitor = ollama_monitor
# Global context instance
_tool_context: Optional[ToolContext] = None
def get_tool_context() -> ToolContext:
"""Get the global tool context"""
if _tool_context is None:
raise RuntimeError("Tool context not initialized. Call set_tool_context() first.")
return _tool_context
def set_tool_context(context: ToolContext):
"""Set the global tool context"""
global _tool_context
_tool_context = context
class BaseTool(ABC):
@property
def context(self) -> ToolContext:
"""Access to shared system monitors and resources"""
return get_tool_context()
@property
def baseroute(self) -> str:
"""Auto-generate route from module name"""
# Get the module path: tools.example_tool.tool
module = inspect.getmodule(self)
if module:
module_name = module.__name__
else:
raise ValueError("no module name specified.")
# Extract package name: example_tool
package_name = module_name.split('.')[-2]
# Convert to route: /example-tool
return f"/{package_name.replace('_', '-')}"
@property
@abstractmethod
def name(self) -> str:
"""Tool name for display"""
pass
@property
@abstractmethod
def description(self) -> str:
"""Tool description"""
pass
@property
@abstractmethod
def icon(self) -> str:
"""Material icon name"""
pass
@property
def enabled(self) -> bool:
"""Whether this tool is enabled (default: True)"""
return True
@property
@abstractmethod
def routes(self) -> Dict[str, Callable[[], Awaitable]]:
"""Define sub-routes relative to baseroute
Returns: Dict of {sub_path: handler_method}
Example: {
'': lambda: MainPage().create(self),
'/settings': lambda: SettingsPage().create(self),
'/history': lambda: HistoryPage().create(self)
}
"""
pass
class BasePage(AsyncColumn):
"""Base class for all tool pages - handles common setup"""
tool: 'BaseTool'
async def build(self, tool: 'BaseTool'):
"""Common setup for all pages"""
self.classes('main-content')
self.tool = tool
with self:
await self.content()
@abstractmethod
async def content(self):
"""Override this to provide page-specific content"""
pass

View File

View File

@@ -0,0 +1,135 @@
from typing import Dict, Callable, Awaitable
from nicegui import ui
from tools.base_tool import BaseTool, BasePage
class ExampleTool(BaseTool):
@property
def name(self) -> str:
return "Example Tool"
@property
def description(self) -> str:
return "Shows how to build a tool with multiple pages."
@property
def icon(self) -> str:
return "extension"
@property
def enabled(self) -> bool:
"""Enable/disable this tool (set to False to hide from menu and disable routes)"""
return True # Set to False to disable this tool
@property
def routes(self) -> Dict[str, Callable[[], Awaitable]]:
"""Define the routes for this tool"""
return {
'': lambda: MainPage().create(self),
'/settings': lambda: SettingsPage().create(self),
'/history': lambda: HistoryPage().create(self),
}
class MainPage(BasePage):
"""Main page of the example tool"""
async def content(self):
ui.label(self.tool.name).classes('text-2xl font-bold text-white mb-4')
# Description
with ui.card().classes('metric-card p-4'):
ui.label('Main Page').classes('text-lg font-bold text-white mb-2')
ui.label(self.tool.description).classes('text-sm text-grey-5')
# Navigation to sub-pages
ui.label('Navigate to:').classes('text-sm text-grey-5 mt-4')
with ui.row().classes('gap-2'):
ui.button('Settings', icon='settings',
on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/settings')).props('color=primary')
ui.button('History', icon='history',
on_click=lambda: ui.navigate.to(f'{self.tool.baseroute}/history')).props('color=secondary')
# Example content with context usage
with ui.card().classes('metric-card p-6 mt-4'):
ui.label('Example Content').classes('text-lg font-bold text-white mb-2')
ui.label('This is the main page of the example tool.').classes('text-sm text-grey-5')
ui.label('Tools can have multiple pages using sub-routes!').classes('text-sm text-grey-5')
# Demonstrate context access
with ui.card().classes('metric-card p-6 mt-4'):
ui.label('Context Demo').classes('text-lg font-bold text-white mb-2')
# Access system monitors through context
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.system_monitor, 'cpu_percent',
backward=lambda x: f'CPU Usage: {x:.1f}%'
)
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.gpu_monitor, 'temperature',
backward=lambda x: f'GPU Temperature: {x:.0f}°C' if x > 0 else 'GPU Temperature: N/A'
)
ui.label().classes('text-sm text-white').bind_text_from(
self.tool.context.ollama_monitor, 'active_models',
backward=lambda x: f'Active Models: {len(x)}'
)
class SettingsPage(BasePage):
"""Settings sub-page"""
async def content(self):
# Header with 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(f'{self.tool.name} - Settings').classes('text-2xl font-bold text-white')
# Settings content
with ui.card().classes('metric-card p-6'):
ui.label('Tool Settings').classes('text-lg font-bold text-white mb-4')
with ui.column().classes('gap-4'):
# Example settings
with ui.row().classes('items-center justify-between'):
ui.label('Enable feature').classes('text-sm text-white')
ui.switch(value=True).props('color=cyan')
with ui.row().classes('items-center justify-between'):
ui.label('Update interval').classes('text-sm text-white')
ui.select(['1s', '2s', '5s', '10s'], value='2s').props('outlined dense color=cyan')
with ui.row().classes('items-center justify-between'):
ui.label('Max items').classes('text-sm text-white')
ui.number(value=100, min=1, max=1000).props('outlined dense')
class HistoryPage(BasePage):
"""History sub-page"""
async def content(self):
# Header with 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(f'{self.tool.name} - History').classes('text-2xl font-bold text-white')
# History content
with ui.card().classes('metric-card p-6'):
ui.label('Activity History').classes('text-lg font-bold text-white mb-4')
# Example history items
history_items = [
('Action performed', '2 minutes ago', 'check_circle', 'green'),
('Settings updated', '15 minutes ago', 'settings', 'cyan'),
('Process started', '1 hour ago', 'play_arrow', 'orange'),
('Error occurred', '2 hours ago', 'error', 'red'),
]
with ui.column().classes('gap-2'):
for action, time, icon, color in history_items:
with ui.row().classes('items-center gap-3 p-2 hover:bg-gray-800 hover:bg-opacity-30 rounded'):
ui.icon(icon, size='sm', color=color)
with ui.column().classes('flex-1 gap-0'):
ui.label(action).classes('text-sm text-white')
ui.label(time).classes('text-xs text-grey-5')