Complete uni-wizard implementation with unified tool registry: **Core Architecture:** - harness.py - Single entry point for all capabilities - tools/registry.py - Central tool registry with schema generation - Elegant routing: One harness, infinite capabilities **Tool Categories (13 tools total):** - System: system_info, process_list, service_status, service_control, health_check, disk_usage - Git: git_status, git_log, git_pull, git_commit, git_push, git_checkout, git_branch_list - Network: http_get, http_post, gitea_create_issue, gitea_comment, gitea_list_issues, gitea_get_issue **Daemons:** - health_daemon.py - HTTP endpoint on :8082, writes to ~/timmy/logs/health.json - task_router.py - Polls Gitea for assigned issues, routes to tools, posts results **Systemd Services:** - timmy-health.service - Health monitoring daemon - timmy-task-router.service - Gitea task router daemon **Testing:** - test_harness.py - Exercises all tool categories **Design Principles:** - Local-first: No cloud dependencies - Self-healing: Tools can restart, reconnect, recover - Unified: One consciousness, all capabilities Closes #76, #77, #78
This commit is contained in:
16
configs/timmy-health.service
Normal file
16
configs/timmy-health.service
Normal 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
|
||||||
16
configs/timmy-task-router.service
Normal file
16
configs/timmy-task-router.service
Normal 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
127
uni-wizard/README.md
Normal 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
|
||||||
9
uni-wizard/daemons/__init__.py
Normal file
9
uni-wizard/daemons/__init__.py
Normal 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']
|
||||||
180
uni-wizard/daemons/health_daemon.py
Normal file
180
uni-wizard/daemons/health_daemon.py
Normal 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()
|
||||||
222
uni-wizard/daemons/task_router.py
Normal file
222
uni-wizard/daemons/task_router.py
Normal 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
174
uni-wizard/harness.py
Normal 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
114
uni-wizard/test_harness.py
Normal 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()
|
||||||
24
uni-wizard/tools/__init__.py
Normal file
24
uni-wizard/tools/__init__.py
Normal 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()
|
||||||
448
uni-wizard/tools/git_tools.py
Normal file
448
uni-wizard/tools/git_tools.py
Normal 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()
|
||||||
459
uni-wizard/tools/network_tools.py
Normal file
459
uni-wizard/tools/network_tools.py
Normal 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()
|
||||||
265
uni-wizard/tools/registry.py
Normal file
265
uni-wizard/tools/registry.py
Normal 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)
|
||||||
377
uni-wizard/tools/system_tools.py
Normal file
377
uni-wizard/tools/system_tools.py
Normal 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()
|
||||||
Reference in New Issue
Block a user