Phase 2: Enhanced TUI/CLI with search, export/import, and custom tools for Proxmox and Ollama

This commit is contained in:
truetype committed 2026-03-20 16:08:26 +01:00
1 parent f5026965f7
commit 9f3298fa13
7 files changed
+1353 -16

No files matched your search

+14
View File
@@ -0,0 +1,14 @@
"""
MaxClaw Agent Tools Package
Provides custom tools for Proxmox and Ollama management
"""
from .proxmox_tools import get_proxmox_tools, ProxmoxTools
from .ollama_tools import get_ollama_tools, OllamaTools
__all__ = [
'get_proxmox_tools',
'ProxmoxTools',
'get_ollama_tools',
'OllamaTools'
]
+360
View File
@@ -0,0 +1,360 @@
"""
Ollama management tools for MaxClaw Agent
Provides tools for managing Ollama models and instances
"""
import requests
import json
from typing import Dict, List, Optional, Any
from langchain.tools import Tool
class OllamaTools:
"""Tools for managing Ollama instances and models"""
def __init__(self, base_url: str = "http://localhost:11434"):
"""
Initialize Ollama tools
Args:
base_url: Base URL of the Ollama instance
"""
self.base_url = base_url.rstrip('/')
self.api_url = f"{self.base_url}/api"
def _make_request(self, endpoint: str, method: str = "GET", data: Optional[Dict] = None) -> Dict[str, Any]:
"""
Make a request to the Ollama API
Args:
endpoint: API endpoint
method: HTTP method (GET, POST, etc.)
data: Optional data for POST requests
Returns:
Dictionary with response data
"""
try:
url = f"{self.api_url}/{endpoint}"
if method.upper() == "GET":
response = requests.get(url, timeout=10)
elif method.upper() == "POST":
response = requests.post(url, json=data, timeout=30)
else:
return {"success": False, "error": f"Unsupported method: {method}"}
if response.status_code == 200:
try:
return {"success": True, "data": response.json()}
except json.JSONDecodeError:
return {"success": True, "data": response.text}
else:
return {"success": False, "error": f"HTTP {response.status_code}: {response.text}"}
except requests.exceptions.Timeout:
return {"success": False, "error": "Request timed out"}
except requests.exceptions.ConnectionError:
return {"success": False, "error": f"Connection failed to {self.base_url}"}
except Exception as e:
return {"success": False, "error": str(e)}
def list_models(self) -> str:
"""
List all available models on the Ollama instance
Returns:
Formatted string with model information
"""
result = self._make_request("tags")
if not result["success"]:
return f"Error listing models: {result['error']}"
models = result["data"].get("models", [])
if not models:
return "No models found on the Ollama instance."
output = "Available Ollama Models:\n"
output += "=" * 50 + "\n"
for model in models:
name = model.get("name", "Unknown")
size = model.get("size", 0)
modified = model.get("modified_at", "")
# Convert size to human readable format
if size > 1024 * 1024 * 1024:
size_str = f"{size / (1024 * 1024 * 1024):.1f} GB"
elif size > 1024 * 1024:
size_str = f"{size / (1024 * 1024):.1f} MB"
else:
size_str = f"{size / 1024:.1f} KB"
# Format modification date
if modified:
from datetime import datetime
try:
dt = datetime.fromisoformat(modified.replace('Z', '+00:00'))
modified_str = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
modified_str = modified
else:
modified_str = "N/A"
output += f"Name: {name}\n"
output += f"Size: {size_str}\n"
output += f"Modified: {modified_str}\n"
output += "-" * 30 + "\n"
return output
def get_model_info(self, model_name: str) -> str:
"""
Get detailed information about a specific model
Args:
model_name: Name of the model
Returns:
Formatted string with model details
"""
result = self._make_request("show", method="POST", data={"name": model_name})
if not result["success"]:
return f"Error getting model info: {result['error']}"
model_info = result["data"]
output = f"Model Information: {model_name}\n"
output += "=" * 50 + "\n"
# Basic information
if "license" in model_info:
output += f"License: {model_info['license']}\n"
if "modelfile" in model_info:
output += f"Modelfile: {model_info['modelfile'][:100]}...\n"
if "parameters" in model_info:
output += f"Parameters: {model_info['parameters']}\n"
if "template" in model_info:
output += f"Template: {model_info['template'][:100]}...\n"
# System information
if "system" in model_info:
output += f"System: {model_info['system'][:100]}...\n"
# Details
if "details" in model_info:
details = model_info["details"]
output += "\nModel Details:\n"
output += "-" * 30 + "\n"
if "parent_model" in details:
output += f"Parent Model: {details['parent_model']}\n"
if "format" in details:
output += f"Format: {details['format']}\n"
if "family" in details:
output += f"Family: {details['family']}\n"
if "families" in details:
output += f"Families: {', '.join(details['families'])}\n"
if "parameter_size" in details:
output += f"Parameter Size: {details['parameter_size']}\n"
if "quantization_level" in details:
output += f"Quantization Level: {details['quantization_level']}\n"
return output
def pull_model(self, model_name: str) -> str:
"""
Pull a model from the Ollama library
Args:
model_name: Name of the model to pull
Returns:
Result message
"""
result = self._make_request("pull", method="POST", data={"name": model_name})
if result["success"]:
return f"✅ Model '{model_name}' pulled successfully."
else:
return f"❌ Failed to pull model '{model_name}': {result['error']}"
def delete_model(self, model_name: str) -> str:
"""
Delete a model from the Ollama instance
Args:
model_name: Name of the model to delete
Returns:
Result message
"""
result = self._make_request("delete", method="POST", data={"name": model_name})
if result["success"]:
return f"✅ Model '{model_name}' deleted successfully."
else:
return f"❌ Failed to delete model '{model_name}': {result['error']}"
def generate_completion(self, model: str, prompt: str, system: Optional[str] = None) -> str:
"""
Generate a completion using a model
Args:
model: Model name
prompt: Prompt text
system: Optional system prompt
Returns:
Generated text
"""
data = {
"model": model,
"prompt": prompt,
"stream": False
}
if system:
data["system"] = system
result = self._make_request("generate", method="POST", data=data)
if not result["success"]:
return f"Error generating completion: {result['error']}"
response = result["data"]
return response.get("response", "No response generated")
def check_ollama_status(self) -> str:
"""
Check if the Ollama instance is running and responsive
Returns:
Status message
"""
try:
response = requests.get(f"{self.base_url}/", timeout=5)
if response.status_code == 200:
return f"✅ Ollama instance at {self.base_url} is running and responsive."
else:
return f"⚠️ Ollama instance at {self.base_url} returned status {response.status_code}."
except requests.exceptions.ConnectionError:
return f"❌ Cannot connect to Ollama instance at {self.base_url}."
except requests.exceptions.Timeout:
return f"❌ Connection to Ollama instance at {self.base_url} timed out."
except Exception as e:
return f"❌ Error checking Ollama status: {str(e)}"
def get_running_models(self) -> str:
"""
Get information about currently running models
Returns:
Formatted string with running model information
"""
result = self._make_request("ps")
if not result["success"]:
return f"Error getting running models: {result['error']}"
running = result["data"].get("models", [])
if not running:
return "No models currently running."
output = "Currently Running Models:\n"
output += "=" * 50 + "\n"
for model in running:
name = model.get("name", "Unknown")
size = model.get("size", 0)
digest = model.get("digest", "")
expires_at = model.get("expires_at", "")
# Convert size to human readable format
if size > 1024 * 1024 * 1024:
size_str = f"{size / (1024 * 1024 * 1024):.1f} GB"
elif size > 1024 * 1024:
size_str = f"{size / (1024 * 1024):.1f} MB"
else:
size_str = f"{size / 1024:.1f} KB"
# Format expiration time
if expires_at:
from datetime import datetime
try:
dt = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
expires_str = dt.strftime("%Y-%m-%d %H:%M:%S")
except:
expires_str = expires_at
else:
expires_str = "N/A"
output += f"Name: {name}\n"
output += f"Size: {size_str}\n"
output += f"Digest: {digest[:20]}...\n"
output += f"Expires: {expires_str}\n"
output += "-" * 30 + "\n"
return output
def get_ollama_tools(base_url: str = "http://localhost:11434") -> List[Tool]:
"""
Get a list of Ollama tools for the MaxClaw agent
Args:
base_url: Base URL of the Ollama instance
Returns:
List of LangChain tools for Ollama management
"""
ollama = OllamaTools(base_url)
tools = [
Tool(
name="ollama_list_models",
func=lambda: ollama.list_models(),
description="List all available models on the Ollama instance"
),
Tool(
name="ollama_model_info",
func=lambda model_name: ollama.get_model_info(model_name),
description="Get detailed information about a specific model"
),
Tool(
name="ollama_pull_model",
func=lambda model_name: ollama.pull_model(model_name),
description="Pull a model from the Ollama library"
),
Tool(
name="ollama_delete_model",
func=lambda model_name: ollama.delete_model(model_name),
description="Delete a model from the Ollama instance"
),
Tool(
name="ollama_generate",
func=lambda model, prompt, system="": ollama.generate_completion(model, prompt, system if system else None),
description="Generate a completion using a model with optional system prompt"
),
Tool(
name="ollama_status",
func=lambda: ollama.check_ollama_status(),
description="Check if the Ollama instance is running and responsive"
),
Tool(
name="ollama_running_models",
func=lambda: ollama.get_running_models(),
description="Get information about currently running models"
)
]
return tools
+320
View File
@@ -0,0 +1,320 @@
"""
Proxmox management tools for MaxClaw Agent
Provides tools for managing Proxmox virtual machines and containers
"""
import subprocess
import json
from typing import Dict, List, Optional, Any
from langchain.tools import Tool
class ProxmoxTools:
"""Tools for managing Proxmox virtual environment"""
def __init__(self, host: str = "192.168.0.100", user: str = "root",
password: Optional[str] = None, api_token: Optional[str] = None):
"""
Initialize Proxmox tools
Args:
host: Proxmox host address
user: Username for authentication
password: Password for authentication (optional if using API token)
api_token: API token for authentication (optional if using password)
"""
self.host = host
self.user = user
self.password = password
self.api_token = api_token
self.base_url = f"https://{host}:8006/api2/json"
def _run_pvesh_command(self, command: str) -> Dict[str, Any]:
"""
Run a pvesh command and return the result
Args:
command: The pvesh command to run
Returns:
Dictionary with command result
"""
try:
# Build the full command
full_command = f"pvesh {command}"
# Add authentication if needed
if self.api_token:
full_command = f"pvesh {command} --apitoken {self.api_token}"
elif self.password:
# Note: In production, use proper authentication methods
full_command = f"pvesh {command} --username {self.user} --password {self.password}"
# Run the command
result = subprocess.run(
full_command,
shell=True,
capture_output=True,
text=True,
timeout=30
)
if result.returncode == 0:
try:
return {"success": True, "data": json.loads(result.stdout)}
except json.JSONDecodeError:
return {"success": True, "data": result.stdout}
else:
return {"success": False, "error": result.stderr}
except subprocess.TimeoutExpired:
return {"success": False, "error": "Command timed out"}
except Exception as e:
return {"success": False, "error": str(e)}
def list_vms(self) -> str:
"""
List all virtual machines in the Proxmox cluster
Returns:
Formatted string with VM information
"""
result = self._run_pvesh_command("get /cluster/resources --type vm")
if not result["success"]:
return f"Error listing VMs: {result['error']}"
vms = result["data"]
if not vms:
return "No virtual machines found."
output = "Virtual Machines:\n"
output += "=" * 50 + "\n"
for vm in vms:
vmid = vm.get("vmid", "N/A")
name = vm.get("name", "Unknown")
status = vm.get("status", "Unknown")
node = vm.get("node", "Unknown")
cpu = vm.get("cpu", 0) * 100
mem = vm.get("mem", 0) / (1024 * 1024) # Convert to MB
maxmem = vm.get("maxmem", 0) / (1024 * 1024 * 1024) # Convert to GB
output += f"VMID: {vmid}\n"
output += f"Name: {name}\n"
output += f"Status: {status}\n"
output += f"Node: {node}\n"
output += f"CPU Usage: {cpu:.1f}%\n"
output += f"Memory: {mem:.1f} MB / {maxmem:.1f} GB\n"
output += "-" * 30 + "\n"
return output
def get_vm_status(self, vmid: int) -> str:
"""
Get the status of a specific virtual machine
Args:
vmid: Virtual machine ID
Returns:
Formatted string with VM status
"""
result = self._run_pvesh_command(f"get /nodes/proxmox/qemu/{vmid}/status/current")
if not result["success"]:
return f"Error getting VM status: {result['error']}"
vm = result["data"]
status = vm.get("status", "Unknown")
cpu = vm.get("cpu", 0) * 100
mem = vm.get("mem", 0) / (1024 * 1024) # Convert to MB
maxmem = vm.get("maxmem", 0) / (1024 * 1024 * 1024) # Convert to GB
uptime = vm.get("uptime", 0)
# Convert uptime to human readable format
days = uptime // (24 * 3600)
hours = (uptime % (24 * 3600)) // 3600
minutes = (uptime % 3600) // 60
output = f"VM {vmid} Status:\n"
output += "=" * 30 + "\n"
output += f"Status: {status}\n"
output += f"CPU Usage: {cpu:.1f}%\n"
output += f"Memory: {mem:.1f} MB / {maxmem:.1f} GB\n"
output += f"Uptime: {days} days, {hours} hours, {minutes} minutes\n"
return output
def start_vm(self, vmid: int) -> str:
"""
Start a virtual machine
Args:
vmid: Virtual machine ID
Returns:
Result message
"""
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/start")
if result["success"]:
return f"✅ VM {vmid} started successfully."
else:
return f"❌ Failed to start VM {vmid}: {result['error']}"
def stop_vm(self, vmid: int) -> str:
"""
Stop a virtual machine
Args:
vmid: Virtual machine ID
Returns:
Result message
"""
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/stop")
if result["success"]:
return f"✅ VM {vmid} stopped successfully."
else:
return f"❌ Failed to stop VM {vmid}: {result['error']}"
def restart_vm(self, vmid: int) -> str:
"""
Restart a virtual machine
Args:
vmid: Virtual machine ID
Returns:
Result message
"""
result = self._run_pvesh_command(f"create /nodes/proxmox/qemu/{vmid}/status/reboot")
if result["success"]:
return f"✅ VM {vmid} restarted successfully."
else:
return f"❌ Failed to restart VM {vmid}: {result['error']}"
def create_snapshot(self, vmid: int, snapname: str, description: str = "") -> str:
"""
Create a snapshot of a virtual machine
Args:
vmid: Virtual machine ID
snapname: Snapshot name
description: Optional description
Returns:
Result message
"""
command = f"create /nodes/proxmox/qemu/{vmid}/snapshot"
command += f" --snapname {snapname}"
if description:
command += f" --description \"{description}\""
result = self._run_pvesh_command(command)
if result["success"]:
return f"✅ Snapshot '{snapname}' created for VM {vmid}."
else:
return f"❌ Failed to create snapshot for VM {vmid}: {result['error']}"
def list_snapshots(self, vmid: int) -> str:
"""
List all snapshots for a virtual machine
Args:
vmid: Virtual machine ID
Returns:
Formatted string with snapshot information
"""
result = self._run_pvesh_command(f"get /nodes/proxmox/qemu/{vmid}/snapshot")
if not result["success"]:
return f"Error listing snapshots: {result['error']}"
snapshots = result["data"]
if not snapshots:
return f"No snapshots found for VM {vmid}."
output = f"Snapshots for VM {vmid}:\n"
output += "=" * 40 + "\n"
for snap in snapshots:
name = snap.get("name", "Unknown")
description = snap.get("description", "")
snaptime = snap.get("snaptime", 0)
# Convert timestamp to readable format
if snaptime:
from datetime import datetime
dt = datetime.fromtimestamp(snaptime)
time_str = dt.strftime("%Y-%m-%d %H:%M:%S")
else:
time_str = "N/A"
output += f"Name: {name}\n"
output += f"Description: {description}\n"
output += f"Created: {time_str}\n"
output += "-" * 30 + "\n"
return output
def get_proxmox_tools(host: str = "192.168.0.100", user: str = "root",
password: Optional[str] = None, api_token: Optional[str] = None) -> List[Tool]:
"""
Get a list of Proxmox tools for the MaxClaw agent
Args:
host: Proxmox host address
user: Username for authentication
password: Password for authentication
api_token: API token for authentication
Returns:
List of LangChain tools for Proxmox management
"""
proxmox = ProxmoxTools(host, user, password, api_token)
tools = [
Tool(
name="proxmox_list_vms",
func=lambda: proxmox.list_vms(),
description="List all virtual machines in the Proxmox cluster with their status and resource usage"
),
Tool(
name="proxmox_vm_status",
func=lambda vmid: proxmox.get_vm_status(int(vmid)),
description="Get detailed status of a specific virtual machine by VMID"
),
Tool(
name="proxmox_start_vm",
func=lambda vmid: proxmox.start_vm(int(vmid)),
description="Start a virtual machine by VMID"
),
Tool(
name="proxmox_stop_vm",
func=lambda vmid: proxmox.stop_vm(int(vmid)),
description="Stop a virtual machine by VMID"
),
Tool(
name="proxmox_restart_vm",
func=lambda vmid: proxmox.restart_vm(int(vmid)),
description="Restart a virtual machine by VMID"
),
Tool(
name="proxmox_create_snapshot",
func=lambda vmid, snapname, description="": proxmox.create_snapshot(int(vmid), snapname, description),
description="Create a snapshot of a virtual machine with optional description"
),
Tool(
name="proxmox_list_snapshots",
func=lambda vmid: proxmox.list_snapshots(int(vmid)),
description="List all snapshots for a specific virtual machine"
)
]
return tools