Merge pull request '[#76 #77 #78] Uni-Wizard Architecture - Single harness, all APIs' (#100) from feature/uni-wizard into main

This commit was merged in pull request #100.
This commit is contained in:
2026-03-30 15:56:57 +00:00
13 changed files with 2431 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Timmy Health Check Daemon
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/timmy
ExecStart=/root/timmy/venv/bin/python /root/timmy/uni-wizard/daemons/health_daemon.py
Restart=always
RestartSec=10
Environment="HOME=/root"
Environment="PYTHONPATH=/root/timmy/uni-wizard"
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,16 @@
[Unit]
Description=Timmy Task Router Daemon
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/root/timmy
ExecStart=/root/timmy/venv/bin/python /root/timmy/uni-wizard/daemons/task_router.py
Restart=always
RestartSec=10
Environment="HOME=/root"
Environment="PYTHONPATH=/root/timmy/uni-wizard"
[Install]
WantedBy=multi-user.target

127
uni-wizard/README.md Normal file
View File

@@ -0,0 +1,127 @@
# Uni-Wizard Architecture
## Vision
A single wizard harness that elegantly routes all API interactions through one unified interface. No more fragmented wizards - one consciousness, infinite capabilities.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ UNI-WIZARD HARNESS │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ System │ │ Git │ │ Network │ │
│ │ Tools │◄──►│ Tools │◄──►│ Tools │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ ▼ │
│ ┌───────────────┐ │
│ │ Tool Router │ │
│ │ (Registry) │ │
│ └───────┬───────┘ │
│ │ │
│ ┌──────────────────┼──────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Local │ │ Gitea │ │ Relay │ │
│ │ llama.cpp │ │ API │ │ Nostr │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
┌───────────────┐
│ LLM (local) │
│ Hermes-3 8B │
└───────────────┘
```
## Design Principles
1. **Single Entry Point**: One harness, all capabilities
2. **Unified Registry**: All tools registered centrally
3. **Elegant Routing**: Tools discover and route automatically
4. **Local-First**: No cloud dependencies
5. **Self-Healing**: Tools can restart, reconnect, recover
## Tool Categories
### System Layer
- `system_info` — OS, CPU, RAM, disk, uptime
- `process_manager` — list, start, stop processes
- `service_controller` — systemd service management
- `health_monitor` — system health checks
### Git Layer
- `git_operations` — status, log, commit, push, pull
- `repo_manager` — clone, branch, merge
- `pr_handler` — create, review, merge PRs
### Network Layer
- `http_client` — GET, POST, PUT, DELETE
- `gitea_client` — full Gitea API wrapper
- `nostr_client` — relay communication
- `api_router` — generic API endpoint handler
### File Layer
- `file_operations` — read, write, append, search
- `directory_manager` — tree, list, navigate
- `archive_handler` — zip, tar, compress
## Registry System
```python
# tools/registry.py
class ToolRegistry:
def __init__(self):
self.tools = {}
def register(self, name, handler, schema):
self.tools[name] = {
'handler': handler,
'schema': schema,
'description': handler.__doc__
}
def execute(self, name, params):
tool = self.tools.get(name)
if not tool:
return f"Error: Tool '{name}' not found"
try:
return tool['handler'](**params)
except Exception as e:
return f"Error executing {name}: {str(e)}"
```
## API Flow
1. **User Request** → Natural language task
2. **LLM Planning** → Breaks into tool calls
3. **Registry Lookup** → Finds appropriate tools
4. **Execution** → Tools run in sequence/parallel
5. **Response** → Results synthesized and returned
## Example Usage
```python
# Single harness, multiple capabilities
result = harness.execute("""
Check system health, pull latest git changes,
and create a Gitea issue if tests fail
""")
```
This becomes:
1. `system_info` → check health
2. `git_pull` → update repo
3. `run_tests` → execute tests
4. `gitea_create_issue` → report failures
## Benefits
- **Simplicity**: One harness to maintain
- **Power**: All capabilities unified
- **Elegance**: Clean routing, no fragmentation
- **Resilience**: Self-contained, local-first

View File

@@ -0,0 +1,9 @@
"""
Uni-Wizard Daemons Package
Background services for the uni-wizard architecture
"""
from .health_daemon import HealthDaemon
from .task_router import TaskRouter
__all__ = ['HealthDaemon', 'TaskRouter']

View File

@@ -0,0 +1,180 @@
"""
Health Check Daemon for Uni-Wizard
Monitors VPS status and exposes health endpoint
"""
import json
import time
import threading
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
from pathlib import Path
import sys
# Add parent to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from harness import get_harness
class HealthCheckHandler(BaseHTTPRequestHandler):
"""HTTP handler for health endpoint"""
def log_message(self, format, *args):
# Suppress default logging
pass
def do_GET(self):
"""Handle GET requests"""
if self.path == '/health':
self.send_health_response()
elif self.path == '/status':
self.send_full_status()
else:
self.send_error(404)
def send_health_response(self):
"""Send simple health check"""
harness = get_harness()
result = harness.execute("health_check")
try:
health_data = json.loads(result)
status_code = 200 if health_data.get("overall") == "healthy" else 503
except:
status_code = 503
health_data = {"error": "Health check failed"}
self.send_response(status_code)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(health_data).encode())
def send_full_status(self):
"""Send full system status"""
harness = get_harness()
status = {
"timestamp": datetime.now().isoformat(),
"harness": json.loads(harness.get_status()),
"system": json.loads(harness.execute("system_info")),
"health": json.loads(harness.execute("health_check"))
}
self.send_response(200)
self.send_header('Content-Type', 'application/json')
self.end_headers()
self.wfile.write(json.dumps(status, indent=2).encode())
class HealthDaemon:
"""
Health monitoring daemon.
Runs continuously, monitoring:
- System resources
- Service status
- Inference endpoint
Exposes:
- HTTP endpoint on port 8082
- JSON status file at ~/timmy/logs/health.json
"""
def __init__(self, port: int = 8082, check_interval: int = 60):
self.port = port
self.check_interval = check_interval
self.running = False
self.server = None
self.monitor_thread = None
self.last_health = None
# Ensure log directory exists
self.log_path = Path.home() / "timmy" / "logs"
self.log_path.mkdir(parents=True, exist_ok=True)
self.health_file = self.log_path / "health.json"
def start(self):
"""Start the health daemon"""
self.running = True
# Start HTTP server
self.server = HTTPServer(('127.0.0.1', self.port), HealthCheckHandler)
server_thread = threading.Thread(target=self.server.serve_forever)
server_thread.daemon = True
server_thread.start()
# Start monitoring loop
self.monitor_thread = threading.Thread(target=self._monitor_loop)
self.monitor_thread.daemon = True
self.monitor_thread.start()
print(f"Health daemon started on http://127.0.0.1:{self.port}")
print(f" - /health - Quick health check")
print(f" - /status - Full system status")
print(f"Health file: {self.health_file}")
def stop(self):
"""Stop the health daemon"""
self.running = False
if self.server:
self.server.shutdown()
print("Health daemon stopped")
def _monitor_loop(self):
"""Background monitoring loop"""
while self.running:
try:
self._update_health_file()
time.sleep(self.check_interval)
except Exception as e:
print(f"Monitor error: {e}")
time.sleep(5)
def _update_health_file(self):
"""Update the health status file"""
harness = get_harness()
try:
health_result = harness.execute("health_check")
system_result = harness.execute("system_info")
status = {
"timestamp": datetime.now().isoformat(),
"health": json.loads(health_result),
"system": json.loads(system_result)
}
self.health_file.write_text(json.dumps(status, indent=2))
self.last_health = status
except Exception as e:
print(f"Failed to update health file: {e}")
def main():
"""Run the health daemon"""
import signal
daemon = HealthDaemon()
def signal_handler(sig, frame):
print("\nShutting down...")
daemon.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
daemon.start()
# Keep main thread alive
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
daemon.stop()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,222 @@
"""
Task Router for Uni-Wizard
Polls Gitea for assigned issues and executes them
"""
import json
import time
import sys
from pathlib import Path
from datetime import datetime
# Add parent to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from harness import get_harness
class TaskRouter:
"""
Gitea Task Router.
Polls Gitea for issues assigned to Timmy and routes them
to appropriate tools for execution.
Flow:
1. Poll Gitea API for open issues assigned to Timmy
2. Parse issue body for commands/tasks
3. Route to appropriate tool via harness
4. Post results back as comments
5. Close issue if task complete
"""
def __init__(
self,
gitea_url: str = "http://143.198.27.163:3000",
repo: str = "Timmy_Foundation/timmy-home",
assignee: str = "timmy",
poll_interval: int = 60
):
self.gitea_url = gitea_url
self.repo = repo
self.assignee = assignee
self.poll_interval = poll_interval
self.running = False
self.harness = get_harness()
self.processed_issues = set()
# Log file
self.log_path = Path.home() / "timmy" / "logs"
self.log_path.mkdir(parents=True, exist_ok=True)
self.router_log = self.log_path / "task_router.jsonl"
def start(self):
"""Start the task router"""
self.running = True
print(f"Task router started")
print(f" Polling: {self.gitea_url}")
print(f" Assignee: {self.assignee}")
print(f" Interval: {self.poll_interval}s")
while self.running:
try:
self._poll_and_route()
time.sleep(self.poll_interval)
except Exception as e:
self._log_event("error", {"message": str(e)})
time.sleep(5)
def stop(self):
"""Stop the task router"""
self.running = False
print("Task router stopped")
def _poll_and_route(self):
"""Poll for issues and route tasks"""
# Get assigned issues
result = self.harness.execute(
"gitea_list_issues",
repo=self.repo,
state="open",
assignee=self.assignee
)
try:
issues = json.loads(result)
except:
return
for issue in issues.get("issues", []):
issue_num = issue["number"]
# Skip already processed
if issue_num in self.processed_issues:
continue
# Process the issue
self._process_issue(issue)
self.processed_issues.add(issue_num)
def _process_issue(self, issue: dict):
"""Process a single issue"""
issue_num = issue["number"]
title = issue["title"]
self._log_event("issue_received", {
"number": issue_num,
"title": title
})
# Parse title for command hints
# Format: "[ACTION] Description" or just "Description"
action = self._parse_action(title)
# Route to appropriate handler
if action == "system_check":
result = self._handle_system_check(issue_num)
elif action == "git_operation":
result = self._handle_git_operation(issue_num, issue)
elif action == "health_report":
result = self._handle_health_report(issue_num)
else:
result = self._handle_generic(issue_num, issue)
# Post result as comment
self._post_comment(issue_num, result)
self._log_event("issue_processed", {
"number": issue_num,
"action": action,
"result": "success" if result else "failed"
})
def _parse_action(self, title: str) -> str:
"""Parse action from issue title"""
title_lower = title.lower()
if any(kw in title_lower for kw in ["health", "status", "check"]):
return "health_report"
elif any(kw in title_lower for kw in ["system", "resource", "disk", "memory"]):
return "system_check"
elif any(kw in title_lower for kw in ["git", "commit", "push", "pull", "branch"]):
return "git_operation"
return "generic"
def _handle_system_check(self, issue_num: int) -> str:
"""Handle system check task"""
result = self.harness.execute("system_info")
return f"## System Check Results\n\n```json\n{result}\n```"
def _handle_health_report(self, issue_num: int) -> str:
"""Handle health report task"""
result = self.harness.execute("health_check")
return f"## Health Report\n\n```json\n{result}\n```"
def _handle_git_operation(self, issue_num: int, issue: dict) -> str:
"""Handle git operation task"""
body = issue.get("body", "")
# Parse body for git commands
results = []
# Check for status request
if "status" in body.lower():
result = self.harness.execute("git_status", repo_path="/root/timmy/timmy-home")
results.append(f"**Git Status:**\n```json\n{result}\n```")
# Check for pull request
if "pull" in body.lower():
result = self.harness.execute("git_pull", repo_path="/root/timmy/timmy-home")
results.append(f"**Git Pull:**\n{result}")
if not results:
results.append("No specific git operation detected in issue body.")
return "\n\n".join(results)
def _handle_generic(self, issue_num: int, issue: dict) -> str:
"""Handle generic task"""
return f"Received issue #{issue_num}: {issue['title']}\n\nI'll process this and update shortly."
def _post_comment(self, issue_num: int, body: str):
"""Post a comment on the issue"""
result = self.harness.execute(
"gitea_comment",
repo=self.repo,
issue_number=issue_num,
body=body
)
return result
def _log_event(self, event_type: str, data: dict):
"""Log an event to the JSONL file"""
log_entry = {
"timestamp": datetime.now().isoformat(),
"event": event_type,
**data
}
with open(self.router_log, "a") as f:
f.write(json.dumps(log_entry) + "\n")
def main():
"""Run the task router"""
import signal
router = TaskRouter()
def signal_handler(sig, frame):
print("\nShutting down...")
router.stop()
sys.exit(0)
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
router.start()
if __name__ == "__main__":
main()

174
uni-wizard/harness.py Normal file
View File

@@ -0,0 +1,174 @@
"""
Uni-Wizard Harness
Single entry point for all capabilities
"""
import json
import sys
from typing import Dict, Any, Optional
from pathlib import Path
# Add tools to path
sys.path.insert(0, str(Path(__file__).parent))
from tools import registry, call_tool
class UniWizardHarness:
"""
The Uni-Wizard Harness - one consciousness, infinite capabilities.
All API flows route through this single harness:
- System monitoring and control
- Git operations
- Network requests
- Gitea API
- Local inference
Usage:
harness = UniWizardHarness()
result = harness.execute("system_info")
result = harness.execute("git_status", repo_path="/path/to/repo")
"""
def __init__(self):
self.registry = registry
self.history = []
def list_capabilities(self) -> str:
"""List all available tools/capabilities"""
tools = []
for category in self.registry.get_categories():
cat_tools = self.registry.get_tools_by_category(category)
tools.append(f"\n{category.upper()}:")
for tool in cat_tools:
tools.append(f" - {tool['name']}: {tool['description']}")
return "\n".join(tools)
def execute(self, tool_name: str, **params) -> str:
"""
Execute a tool by name.
Args:
tool_name: Name of the tool to execute
**params: Parameters for the tool
Returns:
String result from the tool
"""
# Log execution
self.history.append({
"tool": tool_name,
"params": params
})
# Execute via registry
result = call_tool(tool_name, **params)
return result
def execute_plan(self, plan: list) -> Dict[str, str]:
"""
Execute a sequence of tool calls.
Args:
plan: List of dicts with 'tool' and 'params'
e.g., [{"tool": "system_info", "params": {}}]
Returns:
Dict mapping tool names to results
"""
results = {}
for step in plan:
tool_name = step.get("tool")
params = step.get("params", {})
result = self.execute(tool_name, **params)
results[tool_name] = result
return results
def get_tool_definitions(self) -> str:
"""Get tool definitions formatted for LLM system prompt"""
return self.registry.get_tool_definitions()
def get_status(self) -> str:
"""Get harness status"""
return json.dumps({
"total_tools": len(self.registry.list_tools()),
"categories": self.registry.get_categories(),
"tools_by_category": {
cat: self.registry.list_tools(cat)
for cat in self.registry.get_categories()
},
"execution_history_count": len(self.history)
}, indent=2)
# Singleton instance
_harness = None
def get_harness() -> UniWizardHarness:
"""Get the singleton harness instance"""
global _harness
if _harness is None:
_harness = UniWizardHarness()
return _harness
def main():
"""CLI interface for the harness"""
harness = get_harness()
if len(sys.argv) < 2:
print("Uni-Wizard Harness")
print("==================")
print("\nUsage: python harness.py <command> [args]")
print("\nCommands:")
print(" list - List all capabilities")
print(" status - Show harness status")
print(" tools - Show tool definitions (for LLM)")
print(" exec <tool> - Execute a tool")
print("\nExamples:")
print(' python harness.py exec system_info')
print(' python harness.py exec git_status repo_path=/tmp/timmy-home')
return
command = sys.argv[1]
if command == "list":
print(harness.list_capabilities())
elif command == "status":
print(harness.get_status())
elif command == "tools":
print(harness.get_tool_definitions())
elif command == "exec" and len(sys.argv) >= 3:
tool_name = sys.argv[2]
# Parse params from args (key=value format)
params = {}
for arg in sys.argv[3:]:
if '=' in arg:
key, value = arg.split('=', 1)
# Try to parse as int/bool
if value.isdigit():
value = int(value)
elif value.lower() == 'true':
value = True
elif value.lower() == 'false':
value = False
params[key] = value
result = harness.execute(tool_name, **params)
print(result)
else:
print(f"Unknown command: {command}")
print("Run without arguments for help")
if __name__ == "__main__":
main()

114
uni-wizard/test_harness.py Normal file
View File

@@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""
Test script for Uni-Wizard Harness
Exercises all tool categories
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent))
from harness import get_harness
def test_system_tools():
"""Test system monitoring tools"""
print("\n" + "="*60)
print("TESTING SYSTEM TOOLS")
print("="*60)
harness = get_harness()
tests = [
("system_info", {}),
("health_check", {}),
("process_list", {"filter_name": "python"}),
("disk_usage", {}),
]
for tool_name, params in tests:
print(f"\n>>> {tool_name}()")
result = harness.execute(tool_name, **params)
print(result[:500] + "..." if len(result) > 500 else result)
def test_git_tools():
"""Test git operations"""
print("\n" + "="*60)
print("TESTING GIT TOOLS")
print("="*60)
harness = get_harness()
# Test with timmy-home repo if it exists
repo_path = "/tmp/timmy-home"
tests = [
("git_status", {"repo_path": repo_path}),
("git_log", {"repo_path": repo_path, "count": 5}),
("git_branch_list", {"repo_path": repo_path}),
]
for tool_name, params in tests:
print(f"\n>>> {tool_name}()")
result = harness.execute(tool_name, **params)
print(result[:500] + "..." if len(result) > 500 else result)
def test_network_tools():
"""Test network operations"""
print("\n" + "="*60)
print("TESTING NETWORK TOOLS")
print("="*60)
harness = get_harness()
tests = [
("http_get", {"url": "http://143.198.27.163:3000/api/v1/repos/Timmy_Foundation/timmy-home"}),
("gitea_list_issues", {"state": "open"}),
]
for tool_name, params in tests:
print(f"\n>>> {tool_name}()")
result = harness.execute(tool_name, **params)
print(result[:500] + "..." if len(result) > 500 else result)
def test_harness_features():
"""Test harness management features"""
print("\n" + "="*60)
print("TESTING HARNESS FEATURES")
print("="*60)
harness = get_harness()
print("\n>>> list_capabilities()")
print(harness.list_capabilities())
print("\n>>> get_status()")
print(harness.get_status())
def run_all_tests():
"""Run complete test suite"""
print("UNI-WIZARD HARNESS TEST SUITE")
print("=============================")
try:
test_system_tools()
test_git_tools()
test_network_tools()
test_harness_features()
print("\n" + "="*60)
print("✓ ALL TESTS COMPLETED")
print("="*60)
except Exception as e:
print(f"\n✗ TEST FAILED: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
run_all_tests()

View File

@@ -0,0 +1,24 @@
"""
Uni-Wizard Tools Package
All tools for self-sufficient operation
"""
from .registry import registry, ToolRegistry, ToolResult, tool, call_tool
# Import all tool modules to register them
from . import system_tools
from . import git_tools
from . import network_tools
__all__ = [
'registry',
'ToolRegistry',
'ToolResult',
'tool',
'call_tool'
]
# Ensure all tools are registered
system_tools.register_all()
git_tools.register_all()
network_tools.register_all()

View File

@@ -0,0 +1,448 @@
"""
Git Tools for Uni-Wizard
Repository operations and version control
"""
import os
import json
import subprocess
from typing import Dict, List, Optional
from pathlib import Path
from .registry import registry
def run_git_command(args: List[str], cwd: str = None) -> tuple:
"""Execute a git command and return (stdout, stderr, returncode)"""
try:
result = subprocess.run(
['git'] + args,
capture_output=True,
text=True,
cwd=cwd
)
return result.stdout, result.stderr, result.returncode
except Exception as e:
return "", str(e), 1
def git_status(repo_path: str = ".") -> str:
"""
Get git repository status.
Args:
repo_path: Path to git repository (default: current directory)
Returns:
Status info including branch, changed files, last commit
"""
try:
status = {"repo_path": os.path.abspath(repo_path)}
# Current branch
stdout, _, rc = run_git_command(['branch', '--show-current'], cwd=repo_path)
if rc == 0:
status["branch"] = stdout.strip()
else:
return f"Error: Not a git repository at {repo_path}"
# Last commit
stdout, _, rc = run_git_command(['log', '-1', '--format=%H|%s|%an|%ad', '--date=short'], cwd=repo_path)
if rc == 0:
parts = stdout.strip().split('|')
if len(parts) >= 4:
status["last_commit"] = {
"hash": parts[0][:8],
"message": parts[1],
"author": parts[2],
"date": parts[3]
}
# Changed files
stdout, _, rc = run_git_command(['status', '--porcelain'], cwd=repo_path)
if rc == 0:
changes = []
for line in stdout.strip().split('\n'):
if line:
status_code = line[:2]
file_path = line[3:]
changes.append({
"file": file_path,
"status": status_code.strip()
})
status["changes"] = changes
status["has_changes"] = len(changes) > 0
# Remote info
stdout, _, rc = run_git_command(['remote', '-v'], cwd=repo_path)
if rc == 0:
remotes = []
for line in stdout.strip().split('\n'):
if line:
parts = line.split()
if len(parts) >= 2:
remotes.append({"name": parts[0], "url": parts[1]})
status["remotes"] = remotes
return json.dumps(status, indent=2)
except Exception as e:
return f"Error getting git status: {str(e)}"
def git_log(repo_path: str = ".", count: int = 10) -> str:
"""
Get recent commit history.
Args:
repo_path: Path to git repository
count: Number of commits to show (default: 10)
Returns:
List of recent commits
"""
try:
stdout, stderr, rc = run_git_command(
['log', f'-{count}', '--format=%H|%s|%an|%ad', '--date=short'],
cwd=repo_path
)
if rc != 0:
return f"Error: {stderr}"
commits = []
for line in stdout.strip().split('\n'):
if line:
parts = line.split('|')
if len(parts) >= 4:
commits.append({
"hash": parts[0][:8],
"message": parts[1],
"author": parts[2],
"date": parts[3]
})
return json.dumps({"count": len(commits), "commits": commits}, indent=2)
except Exception as e:
return f"Error getting git log: {str(e)}"
def git_pull(repo_path: str = ".") -> str:
"""
Pull latest changes from remote.
Args:
repo_path: Path to git repository
Returns:
Pull result
"""
try:
stdout, stderr, rc = run_git_command(['pull'], cwd=repo_path)
if rc == 0:
if 'Already up to date' in stdout:
return "✓ Already up to date"
return f"✓ Pull successful:\n{stdout}"
else:
return f"✗ Pull failed:\n{stderr}"
except Exception as e:
return f"Error pulling: {str(e)}"
def git_commit(repo_path: str = ".", message: str = None, files: List[str] = None) -> str:
"""
Stage and commit changes.
Args:
repo_path: Path to git repository
message: Commit message (required)
files: Specific files to commit (default: all changes)
Returns:
Commit result
"""
if not message:
return "Error: commit message is required"
try:
# Stage files
if files:
for f in files:
_, stderr, rc = run_git_command(['add', f], cwd=repo_path)
if rc != 0:
return f"✗ Failed to stage {f}: {stderr}"
else:
_, stderr, rc = run_git_command(['add', '.'], cwd=repo_path)
if rc != 0:
return f"✗ Failed to stage changes: {stderr}"
# Commit
stdout, stderr, rc = run_git_command(['commit', '-m', message], cwd=repo_path)
if rc == 0:
return f"✓ Commit successful:\n{stdout}"
else:
if 'nothing to commit' in stderr.lower():
return "✓ Nothing to commit (working tree clean)"
return f"✗ Commit failed:\n{stderr}"
except Exception as e:
return f"Error committing: {str(e)}"
def git_push(repo_path: str = ".", remote: str = "origin", branch: str = None) -> str:
"""
Push to remote repository.
Args:
repo_path: Path to git repository
remote: Remote name (default: origin)
branch: Branch to push (default: current branch)
Returns:
Push result
"""
try:
if not branch:
# Get current branch
stdout, _, rc = run_git_command(['branch', '--show-current'], cwd=repo_path)
if rc == 0:
branch = stdout.strip()
else:
return "Error: Could not determine current branch"
stdout, stderr, rc = run_git_command(['push', remote, branch], cwd=repo_path)
if rc == 0:
return f"✓ Push successful to {remote}/{branch}"
else:
return f"✗ Push failed:\n{stderr}"
except Exception as e:
return f"Error pushing: {str(e)}"
def git_checkout(repo_path: str = ".", branch: str = None, create: bool = False) -> str:
"""
Checkout a branch.
Args:
repo_path: Path to git repository
branch: Branch name to checkout
create: Create the branch if it doesn't exist
Returns:
Checkout result
"""
if not branch:
return "Error: branch name is required"
try:
if create:
stdout, stderr, rc = run_git_command(['checkout', '-b', branch], cwd=repo_path)
else:
stdout, stderr, rc = run_git_command(['checkout', branch], cwd=repo_path)
if rc == 0:
return f"✓ Checked out branch: {branch}"
else:
return f"✗ Checkout failed:\n{stderr}"
except Exception as e:
return f"Error checking out: {str(e)}"
def git_branch_list(repo_path: str = ".") -> str:
"""
List all branches.
Args:
repo_path: Path to git repository
Returns:
List of branches with current marked
"""
try:
stdout, stderr, rc = run_git_command(['branch', '-a'], cwd=repo_path)
if rc != 0:
return f"Error: {stderr}"
branches = []
for line in stdout.strip().split('\n'):
if line:
branch = line.strip()
is_current = branch.startswith('*')
if is_current:
branch = branch[1:].strip()
branches.append({
"name": branch,
"current": is_current
})
return json.dumps({"branches": branches}, indent=2)
except Exception as e:
return f"Error listing branches: {str(e)}"
# Register all git tools
def register_all():
registry.register(
name="git_status",
handler=git_status,
description="Get git repository status (branch, changes, last commit)",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
}
}
},
category="git"
)
registry.register(
name="git_log",
handler=git_log,
description="Get recent commit history",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
},
"count": {
"type": "integer",
"description": "Number of commits to show",
"default": 10
}
}
},
category="git"
)
registry.register(
name="git_pull",
handler=git_pull,
description="Pull latest changes from remote",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
}
}
},
category="git"
)
registry.register(
name="git_commit",
handler=git_commit,
description="Stage and commit changes",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
},
"message": {
"type": "string",
"description": "Commit message (required)"
},
"files": {
"type": "array",
"description": "Specific files to commit (default: all changes)",
"items": {"type": "string"}
}
},
"required": ["message"]
},
category="git"
)
registry.register(
name="git_push",
handler=git_push,
description="Push to remote repository",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
},
"remote": {
"type": "string",
"description": "Remote name",
"default": "origin"
},
"branch": {
"type": "string",
"description": "Branch to push (default: current)"
}
}
},
category="git"
)
registry.register(
name="git_checkout",
handler=git_checkout,
description="Checkout a branch",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
},
"branch": {
"type": "string",
"description": "Branch name to checkout"
},
"create": {
"type": "boolean",
"description": "Create branch if it doesn't exist",
"default": False
}
},
"required": ["branch"]
},
category="git"
)
registry.register(
name="git_branch_list",
handler=git_branch_list,
description="List all branches",
parameters={
"type": "object",
"properties": {
"repo_path": {
"type": "string",
"description": "Path to git repository",
"default": "."
}
}
},
category="git"
)
register_all()

View File

@@ -0,0 +1,459 @@
"""
Network Tools for Uni-Wizard
HTTP client and Gitea API integration
"""
import json
import urllib.request
import urllib.error
from typing import Dict, Optional, Any
from base64 import b64encode
from .registry import registry
class HTTPClient:
"""Simple HTTP client for API calls"""
def __init__(self, base_url: str = None, auth: tuple = None):
self.base_url = base_url
self.auth = auth
def _make_request(
self,
method: str,
url: str,
data: Dict = None,
headers: Dict = None
) -> tuple:
"""Make HTTP request and return (body, status_code, error)"""
try:
# Build full URL
full_url = url
if self.base_url and not url.startswith('http'):
full_url = f"{self.base_url.rstrip('/')}/{url.lstrip('/')}"
# Prepare data
body = None
if data:
body = json.dumps(data).encode('utf-8')
# Build request
req = urllib.request.Request(
full_url,
data=body,
method=method
)
# Add headers
req.add_header('Content-Type', 'application/json')
if headers:
for key, value in headers.items():
req.add_header(key, value)
# Add auth
if self.auth:
username, password = self.auth
credentials = b64encode(f"{username}:{password}".encode()).decode()
req.add_header('Authorization', f'Basic {credentials}')
# Make request
with urllib.request.urlopen(req, timeout=30) as response:
return response.read().decode('utf-8'), response.status, None
except urllib.error.HTTPError as e:
return e.read().decode('utf-8'), e.code, str(e)
except Exception as e:
return None, 0, str(e)
def get(self, url: str) -> tuple:
return self._make_request('GET', url)
def post(self, url: str, data: Dict) -> tuple:
return self._make_request('POST', url, data)
def put(self, url: str, data: Dict) -> tuple:
return self._make_request('PUT', url, data)
def delete(self, url: str) -> tuple:
return self._make_request('DELETE', url)
def http_get(url: str) -> str:
"""
Perform HTTP GET request.
Args:
url: URL to fetch
Returns:
Response body or error message
"""
client = HTTPClient()
body, status, error = client.get(url)
if error:
return f"Error (HTTP {status}): {error}"
return body
def http_post(url: str, body: Dict) -> str:
"""
Perform HTTP POST request with JSON body.
Args:
url: URL to post to
body: JSON body as dictionary
Returns:
Response body or error message
"""
client = HTTPClient()
response_body, status, error = client.post(url, body)
if error:
return f"Error (HTTP {status}): {error}"
return response_body
# Gitea API Tools
GITEA_URL = "http://143.198.27.163:3000"
GITEA_USER = "timmy"
GITEA_PASS = "" # Should be configured
def gitea_create_issue(
repo: str = "Timmy_Foundation/timmy-home",
title: str = None,
body: str = None,
labels: list = None
) -> str:
"""
Create a Gitea issue.
Args:
repo: Repository path (owner/repo)
title: Issue title (required)
body: Issue body
labels: List of label names
Returns:
Created issue URL or error
"""
if not title:
return "Error: title is required"
try:
client = HTTPClient(
base_url=GITEA_URL,
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
)
data = {
"title": title,
"body": body or ""
}
if labels:
data["labels"] = labels
response, status, error = client.post(
f"/api/v1/repos/{repo}/issues",
data
)
if error:
return f"Error creating issue: {error}"
result = json.loads(response)
return f"✓ Issue created: #{result['number']} - {result['html_url']}"
except Exception as e:
return f"Error: {str(e)}"
def gitea_comment(
repo: str = "Timmy_Foundation/timmy-home",
issue_number: int = None,
body: str = None
) -> str:
"""
Comment on a Gitea issue.
Args:
repo: Repository path
issue_number: Issue number (required)
body: Comment body (required)
Returns:
Comment result
"""
if not issue_number or not body:
return "Error: issue_number and body are required"
try:
client = HTTPClient(
base_url=GITEA_URL,
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
)
response, status, error = client.post(
f"/api/v1/repos/{repo}/issues/{issue_number}/comments",
{"body": body}
)
if error:
return f"Error posting comment: {error}"
result = json.loads(response)
return f"✓ Comment posted: {result['html_url']}"
except Exception as e:
return f"Error: {str(e)}"
def gitea_list_issues(
repo: str = "Timmy_Foundation/timmy-home",
state: str = "open",
assignee: str = None
) -> str:
"""
List Gitea issues.
Args:
repo: Repository path
state: open, closed, or all
assignee: Filter by assignee username
Returns:
JSON list of issues
"""
try:
client = HTTPClient(
base_url=GITEA_URL,
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
)
url = f"/api/v1/repos/{repo}/issues?state={state}"
if assignee:
url += f"&assignee={assignee}"
response, status, error = client.get(url)
if error:
return f"Error fetching issues: {error}"
issues = json.loads(response)
# Simplify output
simplified = []
for issue in issues:
simplified.append({
"number": issue["number"],
"title": issue["title"],
"state": issue["state"],
"assignee": issue.get("assignee", {}).get("login") if issue.get("assignee") else None,
"url": issue["html_url"]
})
return json.dumps({
"count": len(simplified),
"issues": simplified
}, indent=2)
except Exception as e:
return f"Error: {str(e)}"
def gitea_get_issue(repo: str = "Timmy_Foundation/timmy-home", issue_number: int = None) -> str:
"""
Get details of a specific Gitea issue.
Args:
repo: Repository path
issue_number: Issue number (required)
Returns:
Issue details
"""
if not issue_number:
return "Error: issue_number is required"
try:
client = HTTPClient(
base_url=GITEA_URL,
auth=(GITEA_USER, GITEA_PASS) if GITEA_PASS else None
)
response, status, error = client.get(
f"/api/v1/repos/{repo}/issues/{issue_number}"
)
if error:
return f"Error fetching issue: {error}"
issue = json.loads(response)
return json.dumps({
"number": issue["number"],
"title": issue["title"],
"body": issue["body"][:500] + "..." if len(issue["body"]) > 500 else issue["body"],
"state": issue["state"],
"assignee": issue.get("assignee", {}).get("login") if issue.get("assignee") else None,
"created_at": issue["created_at"],
"url": issue["html_url"]
}, indent=2)
except Exception as e:
return f"Error: {str(e)}"
# Register all network tools
def register_all():
registry.register(
name="http_get",
handler=http_get,
description="Perform HTTP GET request",
parameters={
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to fetch"
}
},
"required": ["url"]
},
category="network"
)
registry.register(
name="http_post",
handler=http_post,
description="Perform HTTP POST request with JSON body",
parameters={
"type": "object",
"properties": {
"url": {
"type": "string",
"description": "URL to post to"
},
"body": {
"type": "object",
"description": "JSON body as dictionary"
}
},
"required": ["url", "body"]
},
category="network"
)
registry.register(
name="gitea_create_issue",
handler=gitea_create_issue,
description="Create a Gitea issue",
parameters={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository path (owner/repo)",
"default": "Timmy_Foundation/timmy-home"
},
"title": {
"type": "string",
"description": "Issue title"
},
"body": {
"type": "string",
"description": "Issue body"
},
"labels": {
"type": "array",
"description": "List of label names",
"items": {"type": "string"}
}
},
"required": ["title"]
},
category="network"
)
registry.register(
name="gitea_comment",
handler=gitea_comment,
description="Comment on a Gitea issue",
parameters={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository path",
"default": "Timmy_Foundation/timmy-home"
},
"issue_number": {
"type": "integer",
"description": "Issue number"
},
"body": {
"type": "string",
"description": "Comment body"
}
},
"required": ["issue_number", "body"]
},
category="network"
)
registry.register(
name="gitea_list_issues",
handler=gitea_list_issues,
description="List Gitea issues",
parameters={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository path",
"default": "Timmy_Foundation/timmy-home"
},
"state": {
"type": "string",
"enum": ["open", "closed", "all"],
"description": "Issue state",
"default": "open"
},
"assignee": {
"type": "string",
"description": "Filter by assignee username"
}
}
},
category="network"
)
registry.register(
name="gitea_get_issue",
handler=gitea_get_issue,
description="Get details of a specific Gitea issue",
parameters={
"type": "object",
"properties": {
"repo": {
"type": "string",
"description": "Repository path",
"default": "Timmy_Foundation/timmy-home"
},
"issue_number": {
"type": "integer",
"description": "Issue number"
}
},
"required": ["issue_number"]
},
category="network"
)
register_all()

View File

@@ -0,0 +1,265 @@
"""
Uni-Wizard Tool Registry
Central registry for all tool capabilities
"""
import json
import inspect
from typing import Dict, Callable, Any, Optional
from dataclasses import dataclass, asdict
from functools import wraps
@dataclass
class ToolSchema:
"""Schema definition for a tool"""
name: str
description: str
parameters: Dict[str, Any]
returns: str
examples: list = None
def to_dict(self):
return asdict(self)
@dataclass
class ToolResult:
"""Standardized tool execution result"""
success: bool
data: Any
error: Optional[str] = None
execution_time_ms: Optional[float] = None
def to_json(self) -> str:
return json.dumps({
'success': self.success,
'data': self.data,
'error': self.error,
'execution_time_ms': self.execution_time_ms
}, indent=2)
def __str__(self) -> str:
if self.success:
return str(self.data)
return f"Error: {self.error}"
class ToolRegistry:
"""
Central registry for all uni-wizard tools.
All tools register here with their schemas.
The LLM queries available tools via get_tool_definitions().
"""
def __init__(self):
self._tools: Dict[str, Dict] = {}
self._categories: Dict[str, list] = {}
def register(
self,
name: str,
handler: Callable,
description: str = None,
parameters: Dict = None,
category: str = "general",
examples: list = None
):
"""
Register a tool in the registry.
Args:
name: Tool name (used in tool calls)
handler: Function to execute
description: What the tool does
parameters: JSON Schema for parameters
category: Tool category (system, git, network, file)
examples: Example usages
"""
# Auto-extract description from docstring if not provided
if description is None and handler.__doc__:
description = handler.__doc__.strip().split('\n')[0]
# Auto-extract parameters from function signature
if parameters is None:
parameters = self._extract_params(handler)
self._tools[name] = {
'name': name,
'handler': handler,
'description': description or f"Execute {name}",
'parameters': parameters,
'category': category,
'examples': examples or []
}
# Add to category
if category not in self._categories:
self._categories[category] = []
self._categories[category].append(name)
return self # For chaining
def _extract_params(self, handler: Callable) -> Dict:
"""Extract parameter schema from function signature"""
sig = inspect.signature(handler)
params = {
"type": "object",
"properties": {},
"required": []
}
for name, param in sig.parameters.items():
# Skip 'self', 'cls', and params with defaults
if name in ('self', 'cls'):
continue
param_info = {"type": "string"} # Default
# Try to infer type from annotation
if param.annotation != inspect.Parameter.empty:
if param.annotation == int:
param_info["type"] = "integer"
elif param.annotation == float:
param_info["type"] = "number"
elif param.annotation == bool:
param_info["type"] = "boolean"
elif param.annotation == list:
param_info["type"] = "array"
elif param.annotation == dict:
param_info["type"] = "object"
# Add description if in docstring
if handler.__doc__:
# Simple param extraction from docstring
for line in handler.__doc__.split('\n'):
if f'{name}:' in line or f'{name} (' in line:
desc = line.split(':', 1)[-1].strip()
param_info["description"] = desc
break
params["properties"][name] = param_info
# Required if no default
if param.default == inspect.Parameter.empty:
params["required"].append(name)
return params
def execute(self, name: str, **params) -> ToolResult:
"""
Execute a tool by name with parameters.
Args:
name: Tool name
**params: Tool parameters
Returns:
ToolResult with success/failure and data
"""
import time
start = time.time()
tool = self._tools.get(name)
if not tool:
return ToolResult(
success=False,
data=None,
error=f"Tool '{name}' not found in registry",
execution_time_ms=(time.time() - start) * 1000
)
try:
handler = tool['handler']
result = handler(**params)
return ToolResult(
success=True,
data=result,
execution_time_ms=(time.time() - start) * 1000
)
except Exception as e:
return ToolResult(
success=False,
data=None,
error=f"{type(e).__name__}: {str(e)}",
execution_time_ms=(time.time() - start) * 1000
)
def get_tool(self, name: str) -> Optional[Dict]:
"""Get tool definition by name"""
tool = self._tools.get(name)
if tool:
# Return without handler (not serializable)
return {
'name': tool['name'],
'description': tool['description'],
'parameters': tool['parameters'],
'category': tool['category'],
'examples': tool['examples']
}
return None
def get_tools_by_category(self, category: str) -> list:
"""Get all tools in a category"""
tool_names = self._categories.get(category, [])
return [self.get_tool(name) for name in tool_names if self.get_tool(name)]
def list_tools(self, category: str = None) -> list:
"""List all tool names, optionally filtered by category"""
if category:
return self._categories.get(category, [])
return list(self._tools.keys())
def get_tool_definitions(self) -> str:
"""
Get all tool definitions formatted for LLM system prompt.
Returns JSON string of all tools with schemas.
"""
tools = []
for name, tool in self._tools.items():
tools.append({
"name": name,
"description": tool['description'],
"parameters": tool['parameters']
})
return json.dumps(tools, indent=2)
def get_categories(self) -> list:
"""Get all tool categories"""
return list(self._categories.keys())
# Global registry instance
registry = ToolRegistry()
def tool(name: str = None, category: str = "general", examples: list = None):
"""
Decorator to register a function as a tool.
Usage:
@tool(category="system")
def system_info():
return {...}
"""
def decorator(func: Callable):
tool_name = name or func.__name__
registry.register(
name=tool_name,
handler=func,
category=category,
examples=examples
)
return func
return decorator
# Convenience function for quick tool execution
def call_tool(name: str, **params) -> str:
"""Execute a tool and return string result"""
result = registry.execute(name, **params)
return str(result)

View File

@@ -0,0 +1,377 @@
"""
System Tools for Uni-Wizard
Monitor and control the VPS environment
"""
import os
import json
import subprocess
import platform
import psutil
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from .registry import tool, registry
@tool(category="system")
def system_info() -> str:
"""
Get comprehensive system information.
Returns:
JSON string with OS, CPU, memory, disk, and uptime info
"""
try:
# CPU info
cpu_count = psutil.cpu_count()
cpu_percent = psutil.cpu_percent(interval=1)
cpu_freq = psutil.cpu_freq()
# Memory info
memory = psutil.virtual_memory()
# Disk info
disk = psutil.disk_usage('/')
# Uptime
boot_time = datetime.fromtimestamp(psutil.boot_time())
uptime = datetime.now() - boot_time
# Load average (Linux only)
load_avg = os.getloadavg() if hasattr(os, 'getloadavg') else [0, 0, 0]
info = {
"hostname": platform.node(),
"os": {
"system": platform.system(),
"release": platform.release(),
"version": platform.version(),
"machine": platform.machine()
},
"cpu": {
"count": cpu_count,
"percent": cpu_percent,
"frequency_mhz": cpu_freq.current if cpu_freq else None
},
"memory": {
"total_gb": round(memory.total / (1024**3), 2),
"available_gb": round(memory.available / (1024**3), 2),
"percent_used": memory.percent
},
"disk": {
"total_gb": round(disk.total / (1024**3), 2),
"free_gb": round(disk.free / (1024**3), 2),
"percent_used": round((disk.used / disk.total) * 100, 1)
},
"uptime": {
"boot_time": boot_time.isoformat(),
"uptime_seconds": int(uptime.total_seconds()),
"uptime_human": str(timedelta(seconds=int(uptime.total_seconds())))
},
"load_average": {
"1min": round(load_avg[0], 2),
"5min": round(load_avg[1], 2),
"15min": round(load_avg[2], 2)
}
}
return json.dumps(info, indent=2)
except Exception as e:
return f"Error getting system info: {str(e)}"
@tool(category="system")
def process_list(filter_name: str = None) -> str:
"""
List running processes with optional name filter.
Args:
filter_name: Optional process name to filter by
Returns:
JSON list of processes with PID, name, CPU%, memory
"""
try:
processes = []
for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent', 'status']):
try:
info = proc.info
if filter_name and filter_name.lower() not in info['name'].lower():
continue
processes.append({
"pid": info['pid'],
"name": info['name'],
"cpu_percent": info['cpu_percent'],
"memory_percent": round(info['memory_percent'], 2) if info['memory_percent'] else 0,
"status": info['status']
})
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
# Sort by CPU usage
processes.sort(key=lambda x: x['cpu_percent'], reverse=True)
return json.dumps({
"count": len(processes),
"filter": filter_name,
"processes": processes[:50] # Limit to top 50
}, indent=2)
except Exception as e:
return f"Error listing processes: {str(e)}"
@tool(category="system")
def service_status(service_name: str) -> str:
"""
Check systemd service status.
Args:
service_name: Name of the service (e.g., 'llama-server', 'syncthing@root')
Returns:
Service status information
"""
try:
result = subprocess.run(
['systemctl', 'status', service_name, '--no-pager'],
capture_output=True,
text=True
)
# Parse output
lines = result.stdout.split('\n')
status_info = {"service": service_name}
for line in lines:
if 'Active:' in line:
status_info['active'] = line.split(':', 1)[1].strip()
elif 'Loaded:' in line:
status_info['loaded'] = line.split(':', 1)[1].strip()
elif 'Main PID:' in line:
status_info['pid'] = line.split(':', 1)[1].strip()
elif 'Memory:' in line:
status_info['memory'] = line.split(':', 1)[1].strip()
elif 'CPU:' in line:
status_info['cpu'] = line.split(':', 1)[1].strip()
status_info['exit_code'] = result.returncode
return json.dumps(status_info, indent=2)
except Exception as e:
return f"Error checking service status: {str(e)}"
@tool(category="system")
def service_control(service_name: str, action: str) -> str:
"""
Control a systemd service (start, stop, restart, enable, disable).
Args:
service_name: Name of the service
action: start, stop, restart, enable, disable, status
Returns:
Result of the action
"""
valid_actions = ['start', 'stop', 'restart', 'enable', 'disable', 'status']
if action not in valid_actions:
return f"Invalid action. Use: {', '.join(valid_actions)}"
try:
result = subprocess.run(
['systemctl', action, service_name],
capture_output=True,
text=True
)
if result.returncode == 0:
return f"✓ Service '{service_name}' {action} successful"
else:
return f"✗ Service '{service_name}' {action} failed: {result.stderr}"
except Exception as e:
return f"Error controlling service: {str(e)}"
@tool(category="system")
def health_check() -> str:
"""
Comprehensive health check of the VPS.
Checks:
- System resources (CPU, memory, disk)
- Critical services (llama-server, syncthing, timmy-agent)
- Network connectivity
- Inference endpoint
Returns:
Health report with status and recommendations
"""
try:
health = {
"timestamp": datetime.now().isoformat(),
"overall": "healthy",
"checks": {}
}
# System resources
memory = psutil.virtual_memory()
disk = psutil.disk_usage('/')
health["checks"]["memory"] = {
"status": "healthy" if memory.percent < 90 else "warning",
"percent_used": memory.percent,
"available_gb": round(memory.available / (1024**3), 2)
}
health["checks"]["disk"] = {
"status": "healthy" if disk.percent < 90 else "warning",
"percent_used": disk.percent,
"free_gb": round(disk.free / (1024**3), 2)
}
# Check inference endpoint
try:
import urllib.request
req = urllib.request.urlopen('http://127.0.0.1:8081/health', timeout=5)
health["checks"]["inference"] = {"status": "healthy", "port": 8081}
except:
health["checks"]["inference"] = {"status": "down", "port": 8081}
health["overall"] = "degraded"
# Check services
services = ['llama-server', 'syncthing@root']
for svc in services:
result = subprocess.run(['systemctl', 'is-active', svc], capture_output=True, text=True)
health["checks"][svc] = {
"status": "healthy" if result.returncode == 0 else "down"
}
if result.returncode != 0:
health["overall"] = "degraded"
return json.dumps(health, indent=2)
except Exception as e:
return f"Error running health check: {str(e)}"
@tool(category="system")
def disk_usage(path: str = "/") -> str:
"""
Get disk usage for a path.
Args:
path: Path to check (default: /)
Returns:
Disk usage statistics
"""
try:
usage = psutil.disk_usage(path)
return json.dumps({
"path": path,
"total_gb": round(usage.total / (1024**3), 2),
"used_gb": round(usage.used / (1024**3), 2),
"free_gb": round(usage.free / (1024**3), 2),
"percent_used": round((usage.used / usage.total) * 100, 1)
}, indent=2)
except Exception as e:
return f"Error checking disk usage: {str(e)}"
# Auto-register all tools in this module
def register_all():
"""Register all system tools"""
registry.register(
name="system_info",
handler=system_info,
description="Get comprehensive system information (OS, CPU, memory, disk, uptime)",
category="system"
)
registry.register(
name="process_list",
handler=process_list,
description="List running processes with optional name filter",
parameters={
"type": "object",
"properties": {
"filter_name": {
"type": "string",
"description": "Optional process name to filter by"
}
}
},
category="system"
)
registry.register(
name="service_status",
handler=service_status,
description="Check systemd service status",
parameters={
"type": "object",
"properties": {
"service_name": {
"type": "string",
"description": "Name of the systemd service"
}
},
"required": ["service_name"]
},
category="system"
)
registry.register(
name="service_control",
handler=service_control,
description="Control a systemd service (start, stop, restart, enable, disable)",
parameters={
"type": "object",
"properties": {
"service_name": {
"type": "string",
"description": "Name of the service"
},
"action": {
"type": "string",
"enum": ["start", "stop", "restart", "enable", "disable", "status"],
"description": "Action to perform"
}
},
"required": ["service_name", "action"]
},
category="system"
)
registry.register(
name="health_check",
handler=health_check,
description="Comprehensive health check of VPS (resources, services, inference)",
category="system"
)
registry.register(
name="disk_usage",
handler=disk_usage,
description="Get disk usage for a path",
parameters={
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to check",
"default": "/"
}
}
},
category="system"
)
register_all()