tooling and docs
This commit is contained in:
36
src/tools/__init__.py
Normal file
36
src/tools/__init__.py
Normal 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
105
src/tools/base_tool.py
Normal 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
|
||||
0
src/tools/example_tool/__init__.py
Normal file
0
src/tools/example_tool/__init__.py
Normal file
135
src/tools/example_tool/tool.py
Normal file
135
src/tools/example_tool/tool.py
Normal 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')
|
||||
Reference in New Issue
Block a user