gguf loader addon api - GGUFloader/gguf-loader GitHub Wiki
Addon API Reference
Complete API reference for developing GGUF Loader addons.
๐๏ธ Core API
Addon Registration
Every addon must implement a register()
function:
def register(parent=None):
"""
Register function called by GGUF Loader when loading the addon.
Args:
parent: The main GGUF Loader application instance
Returns:
QWidget: The addon's UI widget, or None for background addons
"""
pass
Main Application Interface
The parent
parameter provides access to the main GGUF Loader application:
class GGUFLoaderApp:
"""Main GGUF Loader application interface."""
# Properties
model: Optional[Any] # Currently loaded GGUF model
ai_chat: AIChat # AI chat interface
addon_manager: AddonManager # Addon management system
# Methods
def get_model_backend(self) -> Optional[Any]:
"""Get the current model backend for addons."""
def is_model_loaded(self) -> bool:
"""Check if a model is currently loaded."""
# Signals
model_loaded = Signal(object) # Emitted when model is loaded
model_unloaded = Signal() # Emitted when model is unloaded
๐ค Model API
Accessing the Model
def get_model(self, gguf_app):
"""Get the currently loaded GGUF model."""
try:
# Method 1: Direct access
if hasattr(gguf_app, 'model') and gguf_app.model:
return gguf_app.model
# Method 2: Through AI chat
if hasattr(gguf_app, 'ai_chat') and hasattr(gguf_app.ai_chat, 'model'):
return gguf_app.ai_chat.model
# Method 3: Backend method
if hasattr(gguf_app, 'get_model_backend'):
return gguf_app.get_model_backend()
return None
except Exception as e:
logging.error(f"Error getting model: {e}")
return None
Model Interface
class LlamaModel:
"""GGUF Model interface (llama-cpp-python)."""
def __call__(self,
prompt: str,
max_tokens: int = 256,
temperature: float = 0.7,
top_p: float = 0.9,
top_k: int = 40,
repeat_penalty: float = 1.1,
stop: List[str] = None,
stream: bool = False) -> Union[str, Dict, Iterator]:
"""Generate text from the model."""
pass
def tokenize(self, text: str) -> List[int]:
"""Tokenize text."""
pass
def detokenize(self, tokens: List[int]) -> str:
"""Detokenize tokens to text."""
pass
Text Generation
def generate_text(self, model, prompt: str, **kwargs) -> str:
"""Generate text using the model."""
try:
response = model(
prompt,
max_tokens=kwargs.get('max_tokens', 200),
temperature=kwargs.get('temperature', 0.7),
top_p=kwargs.get('top_p', 0.9),
repeat_penalty=kwargs.get('repeat_penalty', 1.1),
stop=kwargs.get('stop', ["</s>", "\n\n"]),
stream=False
)
return self.extract_response_text(response)
except Exception as e:
logging.error(f"Text generation failed: {e}")
return f"Error: {str(e)}"
def extract_response_text(self, response) -> str:
"""Extract text from model response."""
if isinstance(response, dict) and 'choices' in response:
return response['choices'][0].get('text', '').strip()
elif isinstance(response, str):
return response.strip()
else:
return str(response).strip()
๐จ UI API
Widget Creation
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
from PySide6.QtCore import QTimer, Signal
class AddonWidget(QWidget):
"""Base addon widget class."""
# Signals
text_processed = Signal(str)
error_occurred = Signal(str)
def __init__(self, addon_instance):
super().__init__()
self.addon = addon_instance
self.setup_ui()
def setup_ui(self):
"""Setup the widget UI."""
layout = QVBoxLayout(self)
# Title
title = QLabel("My Addon")
title.setStyleSheet("font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Content
self.setup_content(layout)
def setup_content(self, layout):
"""Override this method to add custom content."""
pass
Common UI Components
# Status indicator
def create_status_indicator(self):
"""Create a status indicator widget."""
self.status_label = QLabel("Ready")
self.status_label.setStyleSheet("""
QLabel {
padding: 5px;
border-radius: 3px;
background-color: #4CAF50;
color: white;
}
""")
return self.status_label
def update_status(self, message: str, status_type: str = "info"):
"""Update status indicator."""
colors = {
"info": "#2196F3",
"success": "#4CAF50",
"warning": "#FF9800",
"error": "#F44336"
}
self.status_label.setText(message)
self.status_label.setStyleSheet(f"""
QLabel {{
padding: 5px;
border-radius: 3px;
background-color: {colors.get(status_type, colors['info'])};
color: white;
}}
""")
# Progress indicator
def create_progress_indicator(self):
"""Create a progress indicator."""
from PySide6.QtWidgets import QProgressBar
self.progress_bar = QProgressBar()
self.progress_bar.setVisible(False)
return self.progress_bar
def show_progress(self, message: str = "Processing..."):
"""Show progress indicator."""
self.progress_bar.setVisible(True)
self.progress_bar.setRange(0, 0) # Indeterminate
self.update_status(message, "info")
def hide_progress(self):
"""Hide progress indicator."""
self.progress_bar.setVisible(False)
Floating UI Components
from PySide6.QtCore import Qt
from PySide6.QtGui import QCursor
class FloatingWidget(QWidget):
"""Create floating widgets like the Smart Assistant."""
def __init__(self):
super().__init__()
self.setup_floating_widget()
def setup_floating_widget(self):
"""Setup floating widget properties."""
self.setWindowFlags(
Qt.ToolTip |
Qt.FramelessWindowHint |
Qt.WindowStaysOnTopHint
)
self.setAttribute(Qt.WA_TranslucentBackground)
def show_near_cursor(self, offset_x: int = 10, offset_y: int = -40):
"""Show widget near cursor position."""
cursor_pos = QCursor.pos()
self.move(cursor_pos.x() + offset_x, cursor_pos.y() + offset_y)
self.show()
๐ง System Integration API
Text Selection Detection
import pyautogui
import pyperclip
from PySide6.QtCore import QTimer
class TextSelectionMonitor:
"""Monitor for global text selection."""
def __init__(self, callback):
self.callback = callback
self.last_clipboard = ""
self.selected_text = ""
# Timer for checking selection
self.timer = QTimer()
self.timer.timeout.connect(self.check_selection)
self.timer.start(300) # Check every 300ms
def check_selection(self):
"""Check for text selection."""
try:
# Save current clipboard
original_clipboard = pyperclip.paste()
# Copy selection
pyautogui.hotkey('ctrl', 'c')
# Process after small delay
QTimer.singleShot(50, lambda: self.process_selection(original_clipboard))
except Exception as e:
logging.debug(f"Selection check failed: {e}")
def process_selection(self, original_clipboard):
"""Process the selection."""
try:
current_text = pyperclip.paste()
# Check if we got new selected text
if (current_text != original_clipboard and
current_text and
len(current_text.strip()) > 3):
self.selected_text = current_text.strip()
self.callback(self.selected_text)
# Restore clipboard
pyperclip.copy(original_clipboard)
except Exception as e:
logging.debug(f"Selection processing failed: {e}")
def stop(self):
"""Stop monitoring."""
self.timer.stop()
Clipboard Integration
import pyperclip
class ClipboardManager:
"""Manage clipboard operations."""
@staticmethod
def get_text() -> str:
"""Get text from clipboard."""
try:
return pyperclip.paste()
except Exception as e:
logging.error(f"Failed to get clipboard text: {e}")
return ""
@staticmethod
def set_text(text: str) -> bool:
"""Set text to clipboard."""
try:
pyperclip.copy(text)
return True
except Exception as e:
logging.error(f"Failed to set clipboard text: {e}")
return False
@staticmethod
def append_text(text: str) -> bool:
"""Append text to clipboard."""
try:
current = ClipboardManager.get_text()
new_text = f"{current}\n{text}" if current else text
return ClipboardManager.set_text(new_text)
except Exception as e:
logging.error(f"Failed to append clipboard text: {e}")
return False
Hotkey Registration
import keyboard
class HotkeyManager:
"""Manage global hotkeys."""
def __init__(self):
self.registered_hotkeys = {}
def register_hotkey(self, hotkey: str, callback, description: str = ""):
"""Register a global hotkey."""
try:
keyboard.add_hotkey(hotkey, callback)
self.registered_hotkeys[hotkey] = {
'callback': callback,
'description': description
}
logging.info(f"Registered hotkey: {hotkey}")
return True
except Exception as e:
logging.error(f"Failed to register hotkey {hotkey}: {e}")
return False
def unregister_hotkey(self, hotkey: str):
"""Unregister a hotkey."""
try:
keyboard.remove_hotkey(hotkey)
if hotkey in self.registered_hotkeys:
del self.registered_hotkeys[hotkey]
logging.info(f"Unregistered hotkey: {hotkey}")
return True
except Exception as e:
logging.error(f"Failed to unregister hotkey {hotkey}: {e}")
return False
def cleanup(self):
"""Clean up all registered hotkeys."""
for hotkey in list(self.registered_hotkeys.keys()):
self.unregister_hotkey(hotkey)
๐ Configuration API
Addon Configuration
import json
import os
from pathlib import Path
class AddonConfig:
"""Manage addon configuration."""
def __init__(self, addon_name: str):
self.addon_name = addon_name
self.config_dir = Path.home() / ".ggufloader" / "addons" / addon_name
self.config_file = self.config_dir / "config.json"
self.config = {}
self.load_config()
def load_config(self):
"""Load configuration from file."""
try:
if self.config_file.exists():
with open(self.config_file, 'r') as f:
self.config = json.load(f)
except Exception as e:
logging.error(f"Failed to load config: {e}")
self.config = {}
def save_config(self):
"""Save configuration to file."""
try:
self.config_dir.mkdir(parents=True, exist_ok=True)
with open(self.config_file, 'w') as f:
json.dump(self.config, f, indent=2)
except Exception as e:
logging.error(f"Failed to save config: {e}")
def get(self, key: str, default=None):
"""Get configuration value."""
return self.config.get(key, default)
def set(self, key: str, value):
"""Set configuration value."""
self.config[key] = value
self.save_config()
def update(self, updates: dict):
"""Update multiple configuration values."""
self.config.update(updates)
self.save_config()
๐ Event System API
Addon Events
from PySide6.QtCore import QObject, Signal
class AddonEventSystem(QObject):
"""Event system for addon communication."""
# Core events
addon_loaded = Signal(str) # addon_name
addon_unloaded = Signal(str) # addon_name
model_changed = Signal(object) # model
text_selected = Signal(str) # selected_text
text_processed = Signal(str, str) # original_text, processed_text
def __init__(self):
super().__init__()
self.event_handlers = {}
def emit_event(self, event_name: str, *args, **kwargs):
"""Emit a custom event."""
if hasattr(self, event_name):
signal = getattr(self, event_name)
signal.emit(*args, **kwargs)
def connect_event(self, event_name: str, handler):
"""Connect to an event."""
if hasattr(self, event_name):
signal = getattr(self, event_name)
signal.connect(handler)
๐งช Testing API
Addon Testing Utilities
import unittest
from unittest.mock import Mock, MagicMock
class AddonTestCase(unittest.TestCase):
"""Base test case for addon testing."""
def setUp(self):
"""Set up test environment."""
self.mock_gguf_app = Mock()
self.mock_model = Mock()
self.mock_gguf_app.model = self.mock_model
def create_mock_model_response(self, text: str):
"""Create a mock model response."""
return {
'choices': [{'text': text}]
}
def assert_model_called_with(self, expected_prompt: str):
"""Assert model was called with expected prompt."""
self.mock_model.assert_called()
call_args = self.mock_model.call_args
self.assertIn(expected_prompt, call_args[0][0])
# Example test
class TestMyAddon(AddonTestCase):
def test_text_processing(self):
from addons.my_addon.main import MyAddon
addon = MyAddon(self.mock_gguf_app)
self.mock_model.return_value = self.create_mock_model_response("Processed text")
result = addon.process_text("input text")
self.assertEqual(result, "Processed text")
self.assert_model_called_with("input text")
๐ Logging API
Addon Logging
import logging
from pathlib import Path
class AddonLogger:
"""Logging utilities for addons."""
@staticmethod
def setup_logger(addon_name: str, level=logging.INFO):
"""Setup logger for addon."""
logger = logging.getLogger(f"addon.{addon_name}")
logger.setLevel(level)
# Create file handler
log_dir = Path.home() / ".ggufloader" / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
file_handler = logging.FileHandler(log_dir / f"{addon_name}.log")
file_handler.setLevel(level)
# Create formatter
formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(formatter)
# Add handler to logger
logger.addHandler(file_handler)
return logger
# Usage in addon
logger = AddonLogger.setup_logger("my_addon")
logger.info("Addon initialized")
logger.error("Something went wrong")
๐ Security API
Safe Execution
import subprocess
import tempfile
import os
class SafeExecution:
"""Utilities for safe code execution."""
@staticmethod
def run_command_safely(command: list, timeout: int = 30) -> tuple:
"""Run command safely with timeout."""
try:
result = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout,
check=False
)
return result.returncode, result.stdout, result.stderr
except subprocess.TimeoutExpired:
return -1, "", "Command timed out"
except Exception as e:
return -1, "", str(e)
@staticmethod
def create_temp_file(content: str, suffix: str = ".tmp") -> str:
"""Create temporary file safely."""
with tempfile.NamedTemporaryFile(mode='w', suffix=suffix, delete=False) as f:
f.write(content)
return f.name
@staticmethod
def cleanup_temp_file(filepath: str):
"""Clean up temporary file."""
try:
if os.path.exists(filepath):
os.unlink(filepath)
except Exception as e:
logging.error(f"Failed to cleanup temp file: {e}")
๐ Additional Resources
- Smart Floater Example - Complete addon example
- Addon Development Guide - Step-by-step development guide
- Troubleshooting - Common issues and solutions
Need help with the API? Join our community discussions or contact [email protected]